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

634 lines
19 KiB
Go

/*
Copyright 2021 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 (
"bytes"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/cmd/core/app/config"
"github.com/oam-dev/kubevela/cmd/core/app/options"
commonconfig "github.com/oam-dev/kubevela/pkg/controller/common"
"github.com/oam-dev/kubevela/version"
)
/*
Test Organization Notes:
- Unit tests for all server helper functions are in this file
- Tests use mocks and fakes to avoid needing real Kubernetes components
- All tests use Ginkgo for consistency
*/
var (
testdir = "testdir"
testTimeout = 2 * time.Second
testInterval = 1 * time.Second
testEnv *envtest.Environment
testConfig *rest.Config
)
func TestGinkgo(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "test main")
}
var _ = BeforeSuite(func() {
By("bootstrapping test environment")
useExistCluster := false
// Resolve the CRD path relative to the test file location
crdPath := filepath.Join("..", "..", "..", "charts", "vela-core", "crds")
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: 2 * time.Minute, // Increased timeout for CI
ControlPlaneStopTimeout: time.Minute,
CRDDirectoryPaths: []string{
crdPath,
},
UseExistingCluster: &useExistCluster,
ErrorIfCRDPathMissing: true, // Fail fast if CRDs are not found
}
var err error
testConfig, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(testConfig).ToNot(BeNil())
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
if testEnv != nil {
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
}
})
var _ = Describe("Server Tests", func() {
Describe("waitWebhookSecretVolume", func() {
BeforeEach(func() {
err := os.MkdirAll(testdir, 0755)
Expect(err).NotTo(HaveOccurred())
})
AfterEach(func() {
os.RemoveAll(testdir)
})
When("dir not exist or empty", func() {
It("return timeout error", func() {
err := waitWebhookSecretVolume(testdir, testTimeout, testInterval)
Expect(err).To(HaveOccurred())
By("remove dir")
os.RemoveAll(testdir)
err = waitWebhookSecretVolume(testdir, testTimeout, testInterval)
Expect(err).To(HaveOccurred())
})
})
When("dir contains empty file", func() {
It("return timeout error", func() {
By("add empty file")
err := os.WriteFile(testdir+"/emptyFile", []byte{}, 0644)
Expect(err).NotTo(HaveOccurred())
err = waitWebhookSecretVolume(testdir, testTimeout, testInterval)
Expect(err).To(HaveOccurred())
})
})
When("files in dir are not empty", func() {
It("return nil", func() {
By("add non-empty file")
err := os.WriteFile(testdir+"/file", []byte("test"), 0600)
Expect(err).NotTo(HaveOccurred())
err = waitWebhookSecretVolume(testdir, testTimeout, testInterval)
Expect(err).NotTo(HaveOccurred())
})
})
})
Describe("syncConfigurations", func() {
var coreOpts *options.CoreOptions
BeforeEach(func() {
coreOpts = options.NewCoreOptions()
})
Context("with all configs populated", func() {
It("should sync all configuration values to global variables", func() {
// Set some test values using actual fields from the config structs
coreOpts.Workflow.MaxWaitBackoffTime = 120
coreOpts.Workflow.MaxFailedBackoffTime = 600
coreOpts.Application.ReSyncPeriod = 30 * time.Minute
coreOpts.Kubernetes.InformerSyncPeriod = 10 * time.Hour
// Call sync function
syncConfigurations(context.Background(), coreOpts)
// Verify globals were updated (this is a smoke test - actual values depend on implementation)
// The key point is the function runs without panicking
Expect(func() { syncConfigurations(context.Background(), coreOpts) }).NotTo(Panic())
})
})
Context("with partial configs", func() {
It("should handle nil configs gracefully", func() {
opts := &options.CoreOptions{
Workflow: config.NewWorkflowConfig(),
CUE: config.NewCUEConfig(),
Application: nil, // Intentionally nil
Performance: config.NewPerformanceConfig(),
Resource: config.NewResourceConfig(),
OAM: config.NewOAMConfig(),
}
// Should not panic even with nil fields
Expect(func() {
syncConfigurations(context.Background(), opts)
}).NotTo(Panic())
})
})
Context("with empty CoreOptions", func() {
It("should handle nil options safely", func() {
nilOpts := &options.CoreOptions{}
// Should not panic even with nil fields
Expect(func() {
syncConfigurations(context.Background(), nilOpts)
}).NotTo(Panic())
})
})
})
Describe("setupLogging", func() {
var origStderr *os.File
BeforeEach(func() {
origStderr = os.Stderr
})
AfterEach(func() {
os.Stderr = origStderr
// Reset klog settings
klog.LogToStderr(true)
flag.Set("logtostderr", "true")
})
Context("debug logging", func() {
It("should configure debug logging when LogDebug is true", func() {
obsConfig := &config.ObservabilityConfig{
LogDebug: true,
}
setupLogging(obsConfig)
// Verify debug level was set (we can't directly check flag values easily)
// But we can verify the function doesn't panic
Expect(func() { setupLogging(obsConfig) }).NotTo(Panic())
})
})
Context("file logging", func() {
It("should configure file logging when LogFilePath is set", func() {
tempDir := GinkgoT().TempDir()
logFile := filepath.Join(tempDir, "test.log")
obsConfig := &config.ObservabilityConfig{
LogFilePath: logFile,
LogFileMaxSize: 100,
}
setupLogging(obsConfig)
// Verify flags were set (indirectly by checking no panic)
Expect(func() { setupLogging(obsConfig) }).NotTo(Panic())
})
})
Context("dev logging", func() {
It("should configure dev logging with color output", func() {
obsConfig := &config.ObservabilityConfig{
DevLogs: true,
}
// Capture output to verify color writer is used
var buf bytes.Buffer
klog.SetOutput(&buf)
defer klog.SetOutput(os.Stderr)
setupLogging(obsConfig)
// The function should complete without error
Expect(func() { setupLogging(obsConfig) }).NotTo(Panic())
})
})
Context("standard logging", func() {
It("should configure standard logging when DevLogs is false", func() {
obsConfig := &config.ObservabilityConfig{
DevLogs: false,
}
setupLogging(obsConfig)
Expect(func() { setupLogging(obsConfig) }).NotTo(Panic())
})
})
})
Describe("configureFeatureGates", func() {
var coreOpts *options.CoreOptions
var originalPeriod time.Duration
BeforeEach(func() {
coreOpts = options.NewCoreOptions()
originalPeriod = commonconfig.ApplicationReSyncPeriod
})
AfterEach(func() {
commonconfig.ApplicationReSyncPeriod = originalPeriod
feature.DefaultMutableFeatureGate.Set("ApplyOnce=false")
})
Context("when ApplyOnce is enabled", func() {
It("should configure ApplicationReSyncPeriod", func() {
// Enable the feature gate
feature.DefaultMutableFeatureGate.Set("ApplyOnce=true")
testPeriod := 5 * time.Minute
coreOpts.Kubernetes.InformerSyncPeriod = testPeriod
configureFeatureGates(coreOpts)
Expect(commonconfig.ApplicationReSyncPeriod).To(Equal(testPeriod))
})
})
Context("when ApplyOnce is disabled", func() {
It("should not change ApplicationReSyncPeriod", func() {
feature.DefaultMutableFeatureGate.Set("ApplyOnce=false")
coreOpts.Kubernetes.InformerSyncPeriod = 10 * time.Minute
configureFeatureGates(coreOpts)
Expect(commonconfig.ApplicationReSyncPeriod).To(Equal(originalPeriod))
})
})
Context("with different sync periods", func() {
DescribeTable("should handle various sync periods correctly",
func(enabled bool, syncPeriod time.Duration, expectedResult time.Duration) {
flagValue := fmt.Sprintf("ApplyOnce=%v", enabled)
feature.DefaultMutableFeatureGate.Set(flagValue)
coreOpts.Kubernetes.InformerSyncPeriod = syncPeriod
configureFeatureGates(coreOpts)
if enabled {
Expect(commonconfig.ApplicationReSyncPeriod).To(Equal(expectedResult))
} else {
Expect(commonconfig.ApplicationReSyncPeriod).To(Equal(originalPeriod))
}
},
Entry("enabled with 5 minutes", true, 5*time.Minute, 5*time.Minute),
Entry("enabled with 10 minutes", true, 10*time.Minute, 10*time.Minute),
Entry("disabled with 5 minutes", false, 5*time.Minute, originalPeriod),
Entry("disabled with 10 minutes", false, 10*time.Minute, originalPeriod),
)
})
})
Describe("performCleanup", func() {
var coreOpts *options.CoreOptions
BeforeEach(func() {
coreOpts = options.NewCoreOptions()
})
Context("with log file path", func() {
It("should flush logs when LogFilePath is set", func() {
coreOpts.Observability.LogFilePath = "/tmp/test.log"
// Should not panic
Expect(func() { performCleanup(coreOpts) }).NotTo(Panic())
// Verify klog.Flush was called (indirectly)
performCleanup(coreOpts)
})
})
Context("without log file path", func() {
It("should do nothing when LogFilePath is empty", func() {
coreOpts.Observability.LogFilePath = ""
// Should not panic
Expect(func() { performCleanup(coreOpts) }).NotTo(Panic())
})
})
DescribeTable("should handle various log file configurations",
func(logFilePath string) {
coreOpts.Observability.LogFilePath = logFilePath
// Should not panic
Expect(func() { performCleanup(coreOpts) }).NotTo(Panic())
},
Entry("empty path", ""),
Entry("tmp file", "/tmp/test.log"),
Entry("relative path", "test.log"),
Entry("nested path", "/var/log/kubevela/test.log"),
)
})
Describe("configureKubernetesClient", func() {
Context("when creating Kubernetes config", func() {
It("should configure REST config with correct parameters using ENVTEST", func() {
// Create a test Kubernetes config with specific values
k8sConfig := &config.KubernetesConfig{
QPS: 100,
Burst: 200,
}
// Create a config provider that returns our test config from ENVTEST
configProvider := func() (*rest.Config, error) {
// Create a copy of the test config to avoid modifying the shared config
cfg := rest.CopyConfig(testConfig)
return cfg, nil
}
// Call the function under test with dependency injection
resultConfig, err := configureKubernetesClientWithProvider(k8sConfig, configProvider)
// Assert no error occurred
Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).NotTo(BeNil())
// Verify that QPS and Burst were set correctly
Expect(resultConfig.QPS).To(Equal(float32(100)))
Expect(resultConfig.Burst).To(Equal(200))
// Verify UserAgent was set
Expect(resultConfig.UserAgent).To(ContainSubstring(types.KubeVelaName))
Expect(resultConfig.UserAgent).To(ContainSubstring(version.GitRevision))
// Verify that the config has the impersonating round tripper wrapper
Expect(resultConfig.Wrap).NotTo(BeNil())
})
It("should handle config provider errors gracefully", func() {
k8sConfig := &config.KubernetesConfig{
QPS: 100,
Burst: 200,
}
// Create a config provider that returns an error
configProvider := func() (*rest.Config, error) {
return nil, fmt.Errorf("failed to get config")
}
// Call the function and expect an error
resultConfig, err := configureKubernetesClientWithProvider(k8sConfig, configProvider)
// Assert error occurred
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to get config"))
Expect(resultConfig).To(BeNil())
})
It("should apply impersonating round tripper wrapper", func() {
k8sConfig := &config.KubernetesConfig{
QPS: 50,
Burst: 100,
}
configProvider := func() (*rest.Config, error) {
cfg := rest.CopyConfig(testConfig)
return cfg, nil
}
resultConfig, err := configureKubernetesClientWithProvider(k8sConfig, configProvider)
Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).NotTo(BeNil())
// Verify the wrap function was applied
// We can't directly test the round tripper, but we can verify Wrap is not nil
Expect(resultConfig.Wrap).NotTo(BeNil())
})
})
})
Describe("buildManagerOptions", func() {
var (
coreOpts *options.CoreOptions
ctx context.Context
cancel context.CancelFunc
)
BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())
coreOpts = options.NewCoreOptions()
// Configure options for testing
coreOpts.Server.EnableLeaderElection = false
coreOpts.Server.HealthAddr = ":8081"
coreOpts.Observability.MetricsAddr = ":8080"
coreOpts.Webhook.UseWebhook = false
coreOpts.Webhook.CertDir = GinkgoT().TempDir()
coreOpts.Webhook.WebhookPort = 9443
})
AfterEach(func() {
if cancel != nil {
cancel()
}
})
Context("when building manager options", func() {
It("should construct options with correct values from CoreOptions", func() {
// Call the function under test
managerOpts := buildManagerOptions(ctx, coreOpts)
// Verify metrics configuration
Expect(managerOpts.Metrics.BindAddress).To(Equal(":8080"))
// Verify health probe configuration
Expect(managerOpts.HealthProbeBindAddress).To(Equal(":8081"))
// Verify leader election configuration
Expect(managerOpts.LeaderElection).To(BeFalse())
Expect(managerOpts.LeaderElectionID).NotTo(BeEmpty())
// Verify scheme is set
Expect(managerOpts.Scheme).NotTo(BeNil())
// Verify webhook server is configured
Expect(managerOpts.WebhookServer).NotTo(BeNil())
// Verify timing configurations
Expect(managerOpts.LeaseDuration).NotTo(BeNil())
Expect(*managerOpts.LeaseDuration).To(Equal(coreOpts.Server.LeaseDuration))
Expect(managerOpts.RenewDeadline).NotTo(BeNil())
Expect(*managerOpts.RenewDeadline).To(Equal(coreOpts.Server.RenewDeadline))
Expect(managerOpts.RetryPeriod).NotTo(BeNil())
Expect(*managerOpts.RetryPeriod).To(Equal(coreOpts.Server.RetryPeriod))
// Verify client configuration
Expect(managerOpts.NewClient).NotTo(BeNil())
})
It("should handle leader election enabled configuration", func() {
// Configure with leader election enabled
coreOpts.Server.EnableLeaderElection = true
coreOpts.Server.LeaderElectionNamespace = "test-namespace"
coreOpts.Server.LeaseDuration = 10 * time.Second
coreOpts.Server.RenewDeadline = 8 * time.Second
coreOpts.Server.RetryPeriod = 2 * time.Second
managerOpts := buildManagerOptions(ctx, coreOpts)
// Verify leader election is enabled
Expect(managerOpts.LeaderElection).To(BeTrue())
Expect(managerOpts.LeaderElectionNamespace).To(Equal("test-namespace"))
// Verify timing configurations match
Expect(*managerOpts.LeaseDuration).To(Equal(10 * time.Second))
Expect(*managerOpts.RenewDeadline).To(Equal(8 * time.Second))
Expect(*managerOpts.RetryPeriod).To(Equal(2 * time.Second))
})
It("should construct leader election ID correctly", func() {
// Test without controller requirement flag
coreOpts.Controller.IgnoreAppWithoutControllerRequirement = false
managerOpts := buildManagerOptions(ctx, coreOpts)
leaderElectionID := managerOpts.LeaderElectionID
Expect(leaderElectionID).To(ContainSubstring("kubevela"))
Expect(leaderElectionID).NotTo(BeEmpty())
// Test with controller requirement flag
coreOpts.Controller.IgnoreAppWithoutControllerRequirement = true
managerOpts2 := buildManagerOptions(ctx, coreOpts)
leaderElectionID2 := managerOpts2.LeaderElectionID
// Leader election ID should be different when flag changes
Expect(leaderElectionID2).NotTo(Equal(leaderElectionID))
})
It("should configure webhook server with correct port and certDir", func() {
coreOpts.Webhook.WebhookPort = 9999
coreOpts.Webhook.CertDir = "/custom/cert/dir"
managerOpts := buildManagerOptions(ctx, coreOpts)
// Note: WebhookServer is already constructed, we can't directly inspect
// port and certDir after construction, but we verify it's not nil
Expect(managerOpts.WebhookServer).NotTo(BeNil())
})
})
})
Describe("setupControllers", func() {
var (
ctx context.Context
cancel context.CancelFunc
coreOpts *options.CoreOptions
)
BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())
coreOpts = options.NewCoreOptions()
coreOpts.Webhook.UseWebhook = false // Disable webhooks for simpler testing
})
AfterEach(func() {
if cancel != nil {
cancel()
}
})
Context("error handling", func() {
It("should require a valid manager", func() {
// setupControllers requires a real manager and will panic with nil
// This documents the current behavior - the function assumes valid inputs
Expect(func() {
_ = setupControllers(ctx, nil, coreOpts)
}).To(Panic())
// Note: In production, setupControllers is only called after successful
// createControllerManager, so nil manager should never occur
})
})
// Note: Full integration tests with real manager require:
// - Complete ENVTEST infrastructure with CRDs
// - Controller manager initialization
// - Webhook server setup (if enabled)
})
Describe("startApplicationMonitor", func() {
var (
ctx context.Context
cancel context.CancelFunc
)
BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())
})
AfterEach(func() {
if cancel != nil {
cancel()
}
})
Context("error handling", func() {
It("should require a valid manager", func() {
// startApplicationMonitor requires a real manager and will panic with nil
// This documents the current behavior - the function assumes valid inputs
Expect(func() {
_ = startApplicationMonitor(ctx, nil)
}).To(Panic())
// Note: In production, startApplicationMonitor is only called after
// successful manager creation, so nil manager should never occur
})
})
// Note: Full integration tests require:
// - Initialized controller manager with running informers
// - Metrics registry setup
// - Application resources in cluster
})
// Note: The run() function requires full Kubernetes environment with CRDs.
// These unit tests focuses on individual functions with mocked dependencies.
})