mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-19 04:26:39 +00:00
* 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>
634 lines
19 KiB
Go
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.
|
|
})
|