Files
5a44b5f10c Feat: auto remediate cue issues (#7199)
* feat(cue/upgrade): auto-remediate legacy CUE syntax at render time

Transparently rewrite CUE templates that use deprecated list arithmetic
(+, *) and conflicting field names (error) so that older definitions
continue to work with CUE ≥ v0.14 (KubeVela ≥ 1.11).

- CUEUpgradeFunc registry with ID, CUE/KubeVela version guards, precheck,
  and upgrade function fields
- upgradeListConcatenation: rewrites list1+list2 → list.Concat([list1,list2])
  and list*n → list.Repeat(list, n); adds "list" import as needed
- collectAddChain + extractListConcatArgs: flatten left-associative + chains
  and existing list.Concat([...]) leaves into a single flat call, so both
  fresh chains (a+b+c+d) and partially-upgraded chains produce one
  list.Concat([a,b,c,d]) with no nesting across repeated passes
- upgradeErrorFieldLabel: rewrites unquoted `error` field labels to "error"
  to avoid conflict with the CUE 0.14 built-in; precheck uses a tighter
  \berror\s*: regex to avoid false positives on identifiers like errorMessage
- EnsureCueVersionCompatibility: single entry point used at render time;
  LRU cache with TTL eviction, Prometheus metrics, feature flag
- ParseVersion: regex anchored to reject garbage suffixes (e.g. "1.11foo")
  while accepting pre-release+build metadata (e.g. "v1.13.0-alpha.1+dev")

- template.go: call EnsureCueVersionCompatibility for every template area
  (main, health, custom status, status detail) with correct DefinitionKind
  derived from which definition pointer is non-nil
- validate.go: upgrade policy templates before compiling in
  validateNoRequiredParameters

- `vela def upgrade FILE [-o OUTPUT]`: upgrades a single .cue file
- `vela def upgrade FILE --validate [--quiet]`: exit 1 if upgrade needed
- `vela def compat definitions` / `vela def compat applications`: scan
  cluster definitions/apps for compat issues; output as table or YAML
- Cyclomatic complexity kept below threshold by extracting scanDefinitions,
  scanDefRevisions, buildDefCompatReport, scanApplications, scanAppRevision
  as standalone functions with options structs
- revisionNum() helper for numeric vN comparison (avoids lexicographic bugs)
- mergeImports() dedup helper shared by ToCUEString and formatCUEString
- ANSI escape sequences replaced with fatih/color for portability
- goconst: "yaml" → outputFormatYAML named constant throughout

- Component, trait, and policy definition validating handlers: removed
  spurious obj.Name argument from fmt.Sprintf in warning messages

- FromCUEString: only prepend importString to the stored template when
  imports are non-empty; empty importString ("\n") was causing a leading
  newline that made yaml.v3 use |2 block scalar on every generated YAML

- gen_sdk testdata: removed unused imports (vela/op, encoding/base64) from
  one_of.cue that were exposed by our importString+templateString change
- e2e test: fix flaky trait-order assertion using ContainElements instead
  of index-based equality

Upgraded all built-in .cue files that used deprecated list arithmetic:
- vela-templates/definitions/internal/component/cron-task.cue
- vela-templates/definitions/internal/trait/command.cue
- vela-templates/definitions/internal/trait/container-ports.cue
- vela-templates/definitions/internal/trait/env.cue
- vela-templates/definitions/internal/trait/init-container.cue

Removed unused stdlib imports that caused `def gen-api` to fail:
- vela-templates/definitions/internal/workflowstep/apply-deployment.cue
- vela-templates/definitions/internal/workflowstep/apply-terraform-provider.cue
- vela-templates/definitions/internal/workflowstep/build-push-image.cue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>

* fix(cue/upgrade): address PR review comments

- sync.atomic.Pointer for compatCache to fix data race on reinit
- SummaryVec → HistogramVec for both duration metrics (aggregatable
  across HA replicas); buckets tuned to sub-millisecond upgrade path
  and millisecond render path respectively
- errorFieldLabelRe: extend to match optional (?) and required (!)
  field constraint markers before the colon
- cue-compatibility-cache-size: clamp negative values to 0 (disabled)
  with warning log; document 0=disabled in flag help; cache put is
  no-op when capacity <= 0
- webhook: replace RequiresUpgrade+EnsureCueVersionCompatibility double
  parse with single EnsureCueVersionCompatibility call; use string
  comparison to detect upgrade and emit warning
- def compat: log warning when ApplicationRevision fetch fails instead
  of silently skipping (partial results are preserved)
- e2e: only delete definitions in DeferCleanup if this test created
  them (avoid deleting pre-existing shared resources)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>

* fix(cue/upgrade): address further PR review comments

- EnsureCueVersionCompatibility: return (string, bool) where bool
  indicates semantic upgrades were applied (len(applied)>0), not
  string inequality — prevents false-positive warnings from
  formatting-only normalisation; update all call sites
- webhook handlers (component, trait, policy): switch from
  RequiresUpgrade+EnsureCueVersionCompatibility double-call to single
  EnsureCueVersionCompatibility call using wasUpgraded bool; remove
  now-unused strings imports
- cache: skip eviction goroutine when capacity==0 (disabled); set
  compatCacheCancel=nil on disabled path to avoid stale cancel on
  next InitCompatibilityCache call
- e2e: replace boolean ownership tracking with createAndTrack helper
  that checks pre-existence via Get before Create, eliminating both
  the ambiguous-create leak and the boilerplate booleans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>

* fix: address reviewer comments — cache determinism, e2e ownership race

- cache: store normalised string in compatEntry.upgraded even when no
  semantic fixes were applied, so cache-hit and cache-miss paths return
  identical output (fixes non-deterministic behaviour flagged in review)
- upgrade: return entry.upgraded on the requiresUpgrade=false cache-hit
  path instead of the raw input cueStr
- e2e: replace GET-then-CREATE ownership inference with atomic CREATE-
  first pattern; err==nil means we created it (register DeferCleanup),
  IsAlreadyExists means it pre-existed (skip cleanup), eliminating the
  GET/CREATE race window that could misattribute ownership

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>

---------

Signed-off-by: Brian Kane <briankane1@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 05:57:23 -07:00

564 lines
22 KiB
Go

/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package app
import (
"context"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"time"
velaclient "github.com/kubevela/pkg/controller/client"
"github.com/kubevela/pkg/controller/sharding"
"github.com/kubevela/pkg/meta"
"github.com/kubevela/pkg/util/profiling"
"github.com/pkg/errors"
"github.com/spf13/cobra"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"k8s.io/klog/v2/textlogger"
ctrl "sigs.k8s.io/controller-runtime"
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
ctrlwebhook "sigs.k8s.io/controller-runtime/pkg/webhook"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/cmd/core/app/config"
"github.com/oam-dev/kubevela/cmd/core/app/hooks"
"github.com/oam-dev/kubevela/cmd/core/app/hooks/crdvalidation"
"github.com/oam-dev/kubevela/cmd/core/app/options"
"github.com/oam-dev/kubevela/pkg/auth"
"github.com/oam-dev/kubevela/pkg/cache"
commonconfig "github.com/oam-dev/kubevela/pkg/controller/common"
oamv1beta1 "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/application"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/logging"
"github.com/oam-dev/kubevela/pkg/monitor/watcher"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/util"
oamwebhook "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev"
"github.com/oam-dev/kubevela/version"
)
var (
scheme = common.Scheme
waitSecretTimeout = 90 * time.Second
waitSecretInterval = 2 * time.Second
)
// NewCoreCommand creates a *cobra.Command object with default parameters
func NewCoreCommand() *cobra.Command {
coreOptions := options.NewCoreOptions()
cmd := &cobra.Command{
Use: "vela-core",
Long: `The KubeVela controller manager is a daemon that embeds the core control loops shipped with KubeVela`,
RunE: func(cmd *cobra.Command, args []string) error {
return run(signals.SetupSignalHandler(), coreOptions)
},
SilenceUsage: true,
FParseErrWhitelist: cobra.FParseErrWhitelist{
// Allow unknown flags for backward-compatibility.
UnknownFlags: true,
},
}
flags := cmd.Flags()
namedFlagSets := coreOptions.Flags()
for _, flagSet := range namedFlagSets.FlagSets {
flags.AddFlagSet(flagSet)
}
meta.Name = types.VelaCoreName
klog.InfoS("KubeVela information", "version", version.VelaVersion, "revision", version.GitRevision)
klog.InfoS("Vela-Core init", "definition namespace", oam.SystemDefinitionNamespace)
return cmd
}
func run(ctx context.Context, coreOptions *options.CoreOptions) error {
klog.InfoS("Starting KubeVela core controller",
"context", "initialization",
"leaderElection", coreOptions.Server.EnableLeaderElection,
"webhookEnabled", coreOptions.Webhook.UseWebhook)
// Sync configurations
klog.V(2).InfoS("Syncing configurations to global variables")
syncConfigurations(ctx, coreOptions)
klog.InfoS("Configuration sync completed successfully")
// Setup logging
klog.V(2).InfoS("Setting up logging configuration",
"debug", coreOptions.Observability.LogDebug,
"devLogs", coreOptions.Observability.DevLogs,
"logFilePath", coreOptions.Observability.LogFilePath)
setupLogging(coreOptions.Observability)
// Configure Kubernetes client
klog.InfoS("Configuring Kubernetes client",
"QPS", coreOptions.Kubernetes.QPS,
"burst", coreOptions.Kubernetes.Burst)
kubeConfig, err := configureKubernetesClient(coreOptions.Kubernetes)
if err != nil {
klog.ErrorS(err, "Failed to configure Kubernetes client")
return fmt.Errorf("failed to configure Kubernetes client: %w", err)
}
// Start profiling server
klog.V(2).InfoS("Starting profiling server in background")
go profiling.StartProfilingServer(nil)
// Setup multi-cluster if enabled
if coreOptions.MultiCluster.EnableClusterGateway {
klog.InfoS("Multi-cluster gateway enabled, setting up multi-cluster capability",
"enableMetrics", coreOptions.MultiCluster.EnableClusterMetrics,
"metricsInterval", coreOptions.MultiCluster.ClusterMetricsInterval)
if err := setupMultiCluster(ctx, kubeConfig, coreOptions.MultiCluster); err != nil {
klog.ErrorS(err, "Failed to setup multi-cluster")
return fmt.Errorf("failed to setup multi-cluster: %w", err)
}
klog.InfoS("Multi-cluster setup completed successfully")
}
// Configure feature gates
klog.V(2).InfoS("Configuring feature gates")
configureFeatureGates(coreOptions)
// Create controller manager
klog.InfoS("Creating controller manager",
"metricsAddr", coreOptions.Observability.MetricsAddr,
"healthAddr", coreOptions.Server.HealthAddr,
"webhookPort", coreOptions.Webhook.WebhookPort)
manager, err := createControllerManager(ctx, kubeConfig, coreOptions)
if err != nil {
klog.ErrorS(err, "Failed to create controller manager")
return fmt.Errorf("failed to create controller manager: %w", err)
}
klog.InfoS("Controller manager created successfully")
// Register health checks
klog.V(2).InfoS("Registering health and readiness checks")
if err := registerHealthChecks(manager); err != nil {
klog.ErrorS(err, "Failed to register health checks")
return fmt.Errorf("failed to register health checks: %w", err)
}
// Setup controllers based on sharding mode
klog.InfoS("Setting up controllers",
"shardingEnabled", sharding.EnableSharding,
"shardID", sharding.ShardID)
if err := setupControllers(ctx, manager, coreOptions); err != nil {
klog.ErrorS(err, "Failed to setup controllers")
return fmt.Errorf("failed to setup controllers: %w", err)
}
klog.InfoS("Controllers setup completed successfully")
// Start application monitor
klog.InfoS("Starting application metrics monitor")
if err := startApplicationMonitor(ctx, manager); err != nil {
klog.ErrorS(err, "Failed to start application monitor")
return fmt.Errorf("failed to start application monitor: %w", err)
}
// Start the manager
klog.InfoS("Starting controller manager")
if err := manager.Start(ctx); err != nil {
klog.ErrorS(err, "Failed to run manager")
return err
}
// Cleanup
performCleanup(coreOptions)
klog.InfoS("Program safely stopped")
return nil
}
// syncConfigurations syncs parsed config values to external package global variables
func syncConfigurations(ctx context.Context, coreOptions *options.CoreOptions) {
if coreOptions.Workflow != nil {
klog.V(3).InfoS("Syncing workflow configuration")
coreOptions.Workflow.SyncToWorkflowGlobals()
}
if coreOptions.CUE != nil {
klog.V(3).InfoS("Syncing CUE configuration")
coreOptions.CUE.SyncToCUEGlobals(ctx)
}
if coreOptions.Application != nil {
klog.V(3).InfoS("Syncing application configuration")
coreOptions.Application.SyncToApplicationGlobals()
}
if coreOptions.Performance != nil {
klog.V(3).InfoS("Syncing performance configuration")
coreOptions.Performance.SyncToPerformanceGlobals()
}
if coreOptions.Resource != nil {
klog.V(3).InfoS("Syncing resource configuration")
coreOptions.Resource.SyncToResourceGlobals()
}
if coreOptions.OAM != nil {
klog.V(3).InfoS("Syncing OAM configuration")
coreOptions.OAM.SyncToOAMGlobals()
}
}
// setupLogging configures klog based on parsed observability settings
func setupLogging(observabilityConfig *config.ObservabilityConfig) {
// Configure klog verbosity
if observabilityConfig.LogDebug {
_ = flag.Set("v", strconv.Itoa(int(commonconfig.LogDebug)))
}
// Configure log file output
if observabilityConfig.LogFilePath != "" {
_ = flag.Set("logtostderr", "false")
_ = flag.Set("log_file", observabilityConfig.LogFilePath)
_ = flag.Set("log_file_max_size", strconv.FormatUint(observabilityConfig.LogFileMaxSize, 10))
}
// Set logger (use --dev-logs=true for local development)
if observabilityConfig.DevLogs {
logOutput := logging.NewColorWriter(os.Stdout)
klog.LogToStderr(false)
klog.SetOutput(logOutput)
ctrl.SetLogger(textlogger.NewLogger(textlogger.NewConfig(textlogger.Output(logOutput))))
} else {
ctrl.SetLogger(textlogger.NewLogger(textlogger.NewConfig()))
}
}
// ConfigProvider is a function type that provides a Kubernetes REST config
type ConfigProvider func() (*rest.Config, error)
// configureKubernetesClient creates and configures the Kubernetes REST config
func configureKubernetesClient(kubernetesConfig *config.KubernetesConfig) (*rest.Config, error) {
return configureKubernetesClientWithProvider(kubernetesConfig, ctrl.GetConfig)
}
// configureKubernetesClientWithProvider creates and configures the Kubernetes REST config
// using a provided config provider function. This allows for dependency injection in tests.
func configureKubernetesClientWithProvider(kubernetesConfig *config.KubernetesConfig, configProvider ConfigProvider) (*rest.Config, error) {
// Gracefully handle error returns instead of panicking
kubeConfig, err := configProvider()
if err != nil {
return nil, err
}
kubeConfig.UserAgent = types.KubeVelaName + "/" + version.GitRevision
kubeConfig.QPS = float32(kubernetesConfig.QPS)
kubeConfig.Burst = kubernetesConfig.Burst
kubeConfig.Wrap(auth.NewImpersonatingRoundTripper)
klog.InfoS("Kubernetes Config Loaded",
"UserAgent", kubeConfig.UserAgent,
"QPS", kubeConfig.QPS,
"Burst", kubeConfig.Burst,
)
return kubeConfig, nil
}
// setupMultiCluster initializes multi-cluster capability
func setupMultiCluster(ctx context.Context, kubeConfig *rest.Config, multiClusterConfig *config.MultiClusterConfig) error {
klog.V(2).InfoS("Initializing multi-cluster client")
clusterClient, err := multicluster.Initialize(kubeConfig, true)
if err != nil {
klog.ErrorS(err, "Failed to enable multi-cluster capability")
return err
}
klog.InfoS("Multi-cluster client initialized successfully")
if multiClusterConfig.EnableClusterMetrics {
klog.InfoS("Enabling cluster metrics collection",
"interval", multiClusterConfig.ClusterMetricsInterval)
_, err := multicluster.NewClusterMetricsMgr(ctx, clusterClient, multiClusterConfig.ClusterMetricsInterval)
if err != nil {
klog.ErrorS(err, "Failed to enable multi-cluster-metrics capability")
return err
}
klog.InfoS("Cluster metrics manager initialized successfully")
}
return nil
}
// configureFeatureGates sets up feature-dependent configurations
func configureFeatureGates(coreOptions *options.CoreOptions) {
if utilfeature.DefaultMutableFeatureGate.Enabled(features.ApplyOnce) {
klog.V(2).InfoS("ApplyOnce feature gate enabled, configuring application re-sync period",
"period", coreOptions.Kubernetes.InformerSyncPeriod)
commonconfig.ApplicationReSyncPeriod = coreOptions.Kubernetes.InformerSyncPeriod
}
}
// buildManagerOptions constructs ctrl.Options from CoreOptions for creating a controller manager.
// This function is extracted for testability - it contains the option construction logic
// without the side effects of creating a manager or starting background processes.
func buildManagerOptions(ctx context.Context, coreOptions *options.CoreOptions) ctrl.Options {
leaderElectionID := util.GenerateLeaderElectionID(types.KubeVelaName, coreOptions.Controller.IgnoreAppWithoutControllerRequirement)
leaderElectionID += sharding.GetShardIDSuffix()
return ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: coreOptions.Observability.MetricsAddr,
},
LeaderElection: coreOptions.Server.EnableLeaderElection,
LeaderElectionNamespace: coreOptions.Server.LeaderElectionNamespace,
LeaderElectionID: leaderElectionID,
WebhookServer: ctrlwebhook.NewServer(ctrlwebhook.Options{
Port: coreOptions.Webhook.WebhookPort,
CertDir: coreOptions.Webhook.CertDir,
}),
HealthProbeBindAddress: coreOptions.Server.HealthAddr,
LeaseDuration: &coreOptions.Server.LeaseDuration,
RenewDeadline: &coreOptions.Server.RenewDeadline,
RetryPeriod: &coreOptions.Server.RetryPeriod,
NewClient: velaclient.DefaultNewControllerClient,
NewCache: cache.BuildCache(ctx,
ctrlcache.Options{
Scheme: scheme,
SyncPeriod: &coreOptions.Kubernetes.InformerSyncPeriod,
// SyncPeriod is configured with default value, aka. 10h. First, controller-runtime does not
// recommend use it as a time trigger, instead, it is expected to work for failure tolerance
// of controller-runtime. Additionally, set this value will affect not only application
// controller but also all other controllers like definition controller. Therefore, for
// functionalities like state-keep, they should be invented in other ways.
},
&v1beta1.Application{}, &v1beta1.ApplicationRevision{}, &v1beta1.ResourceTracker{},
),
Client: ctrlclient.Options{
Cache: &ctrlclient.CacheOptions{
DisableFor: cache.NewResourcesToDisableCache(),
},
},
}
}
// createControllerManager creates and configures the controller-runtime manager
func createControllerManager(ctx context.Context, kubeConfig *rest.Config, coreOptions *options.CoreOptions) (ctrl.Manager, error) {
leaderElectionID := util.GenerateLeaderElectionID(types.KubeVelaName, coreOptions.Controller.IgnoreAppWithoutControllerRequirement)
leaderElectionID += sharding.GetShardIDSuffix()
klog.V(2).InfoS("Creating controller manager with configuration",
"leaderElectionID", leaderElectionID,
"leaderElection", coreOptions.Server.EnableLeaderElection,
"leaderElectionNamespace", coreOptions.Server.LeaderElectionNamespace,
"leaseDuration", coreOptions.Server.LeaseDuration,
"renewDeadline", coreOptions.Server.RenewDeadline)
managerOptions := buildManagerOptions(ctx, coreOptions)
manager, err := ctrl.NewManager(kubeConfig, managerOptions)
if err != nil {
klog.ErrorS(err, "Unable to create a controller manager")
return nil, err
}
return manager, nil
}
// setupControllers sets up controllers based on sharding configuration
func setupControllers(ctx context.Context, manager ctrl.Manager, coreOptions *options.CoreOptions) error {
if !sharding.EnableSharding {
return prepareRun(ctx, manager, coreOptions)
}
return prepareRunInShardingMode(ctx, manager, coreOptions)
}
// startApplicationMonitor starts the application metrics watcher
func startApplicationMonitor(ctx context.Context, manager ctrl.Manager) error {
klog.InfoS("Starting vela application monitor")
applicationInformer, err := manager.GetCache().GetInformer(ctx, &v1beta1.Application{})
if err != nil {
klog.ErrorS(err, "Unable to get informer for application")
return err
}
watcher.StartApplicationMetricsWatcher(applicationInformer)
klog.V(2).InfoS("Application metrics watcher started successfully")
return nil
}
// performCleanup handles any necessary cleanup operations
func performCleanup(coreOptions *options.CoreOptions) {
klog.V(2).InfoS("Performing cleanup operations")
if coreOptions.Observability.LogFilePath != "" {
klog.V(3).InfoS("Flushing log file", "path", coreOptions.Observability.LogFilePath)
klog.Flush()
}
}
// prepareRunInShardingMode initializes the controller manager in sharding mode where workload
// is distributed across multiple controller instances. In sharding mode:
// - Master shard handles webhooks, scheduling, and full controller setup
// - Non-master shards only run the Application controller for their assigned Applications
// This enables horizontal scaling of the KubeVela control plane across multiple pods.
func prepareRunInShardingMode(ctx context.Context, manager manager.Manager, coreOptions *options.CoreOptions) error {
if sharding.IsMaster() {
klog.InfoS("Controller running in sharding mode",
"shardType", "master",
"webhookAutoSchedule", !utilfeature.DefaultMutableFeatureGate.Enabled(features.DisableWebhookAutoSchedule))
if !utilfeature.DefaultMutableFeatureGate.Enabled(features.DisableWebhookAutoSchedule) {
klog.V(2).InfoS("Starting webhook auto-scheduler in background")
go sharding.DefaultScheduler.Get().Start(ctx)
}
if err := prepareRun(ctx, manager, coreOptions); err != nil {
return err
}
} else {
klog.InfoS("Controller running in sharding mode",
"shardType", "worker",
"shardID", sharding.ShardID)
klog.V(2).InfoS("Setting up application controller for worker shard")
if err := application.Setup(manager, coreOptions.Controller.Args); err != nil {
klog.ErrorS(err, "Failed to setup application controller in sharding mode")
return err
}
klog.InfoS("Application controller setup completed for worker shard")
}
return nil
}
// prepareRun sets up the complete KubeVela controller manager with all necessary components:
// - Configures and registers OAM webhooks if enabled
// - Sets up all OAM controllers (Application, ComponentDefinition, WorkflowStepDefinition, PolicyDefinition, and TraitDefinition)
// - Initializes multi-cluster capabilities and cluster info
// - Runs pre-start validation hooks to ensure system readiness
// This function is used in single-instance mode or by the master shard in sharding mode.
func prepareRun(ctx context.Context, manager manager.Manager, coreOptions *options.CoreOptions) error {
// Bootstrap provider registry early before other initialization
klog.V(2).InfoS("Initializing provider registry")
bootstrapProviderRegistry()
if coreOptions.Webhook.UseWebhook {
klog.InfoS("Webhook enabled, registering OAM webhooks",
"port", coreOptions.Webhook.WebhookPort,
"certDir", coreOptions.Webhook.CertDir)
oamwebhook.Register(manager, coreOptions.Controller.Args)
klog.V(2).InfoS("Waiting for webhook secret volume",
"timeout", waitSecretTimeout,
"checkInterval", waitSecretInterval)
if err := waitWebhookSecretVolume(coreOptions.Webhook.CertDir, waitSecretTimeout, waitSecretInterval); err != nil {
klog.ErrorS(err, "Unable to get webhook secret")
return err
}
klog.InfoS("Webhook secret volume ready, webhooks registered successfully")
}
klog.InfoS("Setting up OAM controllers")
if err := oamv1beta1.Setup(manager, coreOptions.Controller.Args); err != nil {
klog.ErrorS(err, "Unable to setup the OAM controller")
return err
}
klog.InfoS("OAM controllers setup completed successfully")
klog.V(2).InfoS("Initializing control plane cluster info")
if err := multicluster.InitClusterInfo(manager.GetConfig()); err != nil {
klog.ErrorS(err, "Failed to init control plane cluster info")
return err
}
klog.InfoS("Starting vela controller manager with pre-start validation")
for _, hook := range []hooks.PreStartHook{crdvalidation.NewHook()} {
hookName := hook.Name()
klog.InfoS("Running pre-start hook", "hook", hookName)
if err := hook.Run(ctx); err != nil {
klog.ErrorS(err, "Failed to run pre-start hook", "hook", hookName)
return fmt.Errorf("failed to run hook %s: %w", hookName, err)
}
klog.InfoS("Pre-start hook completed successfully", "hook", hookName)
}
klog.InfoS("All pre-start validation hooks completed successfully")
return nil
}
// registerHealthChecks is used to create readiness&liveness probes
func registerHealthChecks(manager ctrl.Manager) error {
klog.InfoS("Registering readiness and health checks")
if err := manager.AddReadyzCheck("ping", healthz.Ping); err != nil {
klog.ErrorS(err, "Failed to add readiness check")
return err
}
klog.V(3).InfoS("Readiness check registered", "check", "ping")
// TODO: change the health check to be different from readiness check
if err := manager.AddHealthzCheck("ping", healthz.Ping); err != nil {
klog.ErrorS(err, "Failed to add health check")
return err
}
klog.V(3).InfoS("Health check registered", "check", "ping")
return nil
}
// waitWebhookSecretVolume waits for webhook secret ready to avoid manager running crash
func waitWebhookSecretVolume(certDir string, timeout, interval time.Duration) error {
start := time.Now()
for {
time.Sleep(interval)
if time.Since(start) > timeout {
return fmt.Errorf("getting webhook secret timeout after %s", timeout.String())
}
klog.InfoS("Wait webhook secret", "time consumed(second)", int64(time.Since(start).Seconds()),
"timeout(second)", int64(timeout.Seconds()))
if _, err := os.Stat(certDir); !os.IsNotExist(err) {
ready := func() bool {
certDirectory, err := os.Open(filepath.Clean(certDir))
if err != nil {
return false
}
defer func() {
if err := certDirectory.Close(); err != nil {
klog.ErrorS(err, "Failed to close directory")
}
}()
// check if dir is empty
if _, err := certDirectory.Readdir(1); errors.Is(err, io.EOF) {
return false
}
// check if secret files are empty
err = filepath.Walk(certDir, func(path string, fileInfo os.FileInfo, err error) error {
// even Cert dir is created, cert files are still empty for a while
if fileInfo.Size() == 0 {
return errors.New("secret is not ready")
}
return nil
})
if err == nil {
klog.InfoS("Webhook secret is ready", "time consumed(second)",
int64(time.Since(start).Seconds()))
return true
}
return false
}()
if ready {
return nil
}
}
}
}