From 32d166c2195a092fe78298cbb4d52f4886647d67 Mon Sep 17 00:00:00 2001 From: Brian Kane Date: Tue, 10 Feb 2026 17:43:06 +0000 Subject: [PATCH] Checkpoint - context working --- .../core.oam.dev/v1beta1/policy_definition.go | 4 +- pkg/appfile/appfile.go | 5 + pkg/appfile/parser.go | 7 + .../v1beta1/application/generator.go | 6 +- .../v1beta1/application/generator_test.go | 4 +- .../v1beta1/application/policy_transforms.go | 97 ++- .../application/policy_transforms_test.go | 584 ++++++++++++++++++ .../v1beta1/application/suite_test.go | 5 +- pkg/cue/process/handle.go | 19 + pkg/features/controller_features.go | 10 +- 10 files changed, 710 insertions(+), 31 deletions(-) diff --git a/apis/core.oam.dev/v1beta1/policy_definition.go b/apis/core.oam.dev/v1beta1/policy_definition.go index b58dbc3e5..b7a81e801 100644 --- a/apis/core.oam.dev/v1beta1/policy_definition.go +++ b/apis/core.oam.dev/v1beta1/policy_definition.go @@ -59,13 +59,15 @@ type PolicyDefinitionSpec struct { // These generate Kubernetes resources from CUE templates with an 'output' field. // - ApplicationScope: Transform-based policies that modify the Application CR before parsing. // These use 'transforms' instead of 'output' and don't generate resources. + // Requires EnableApplicationScopedPolicies feature gate. // +optional Scope PolicyScope `json:"scope,omitempty"` // Global indicates this policy should automatically apply to all Applications // in this namespace (or all namespaces if in vela-system). // Global policies cannot be explicitly referenced in Application specs. - // Requires EnableGlobalPolicies feature gate. + // Requires EnableGlobalPolicies feature gate for discovery and + // EnableApplicationScopedPolicies feature gate for execution. // +optional Global bool `json:"global,omitempty"` diff --git a/pkg/appfile/appfile.go b/pkg/appfile/appfile.go index a9ce18a6a..86e3319c6 100644 --- a/pkg/appfile/appfile.go +++ b/pkg/appfile/appfile.go @@ -186,6 +186,10 @@ type Appfile struct { app *v1beta1.Application + // GoContext stores the Go context from the controller, which may contain + // policy additionalContext for components to access via context.custom + GoContext context.Context + Debug bool } @@ -781,6 +785,7 @@ func GenerateContextDataFromAppFile(appfile *Appfile, wlName string) velaprocess CompName: wlName, AppRevisionName: appfile.AppRevisionName, Components: appfile.Components, + Ctx: appfile.GoContext, // Pass Go context with policy additionalContext } if appfile.AppAnnotations != nil { data.WorkflowName = appfile.AppAnnotations[oam.AnnotationWorkflowName] diff --git a/pkg/appfile/parser.go b/pkg/appfile/parser.go index b01262749..f65287e85 100644 --- a/pkg/appfile/parser.go +++ b/pkg/appfile/parser.go @@ -114,6 +114,13 @@ func (p *Parser) GenerateAppFileFromApp(ctx context.Context, app *v1beta1.Applic appFile.AppRevisionName = app.Status.LatestRevision.Name } + // Store the Go context so component rendering can access policy additionalContext + if monCtx, ok := ctx.(monitorContext.Context); ok { + appFile.GoContext = monCtx.GetContext() + } else { + appFile.GoContext = ctx + } + var err error if err = p.parseComponents(ctx, appFile); err != nil { return nil, errors.Wrap(err, "failed to parseComponents") diff --git a/pkg/controller/core.oam.dev/v1beta1/application/generator.go b/pkg/controller/core.oam.dev/v1beta1/application/generator.go index fa0be3549..aa33975ca 100644 --- a/pkg/controller/core.oam.dev/v1beta1/application/generator.go +++ b/pkg/controller/core.oam.dev/v1beta1/application/generator.go @@ -82,7 +82,7 @@ func (h *AppHandler) GenerateApplicationSteps(ctx monitorContext.Context, oam.LabelAppName: app.Name, oam.LabelAppNamespace: app.Namespace, } - pCtx := velaprocess.NewContext(generateContextDataFromApp(app, appRev.Name)) + pCtx := velaprocess.NewContext(generateContextDataFromApp(ctx.GetContext(), app, appRev.Name)) ctxWithRuntimeParams := oamprovidertypes.WithRuntimeParams(ctx.GetContext(), oamprovidertypes.RuntimeParams{ ComponentApply: h.applyComponentFunc(appParser, af), ComponentRender: h.renderComponentFunc(appParser, af), @@ -539,12 +539,14 @@ func getComponentResources(ctx context.Context, manifest *types.ComponentManifes } // generateContextDataFromApp builds the process context for workflow (non-component) execution. -func generateContextDataFromApp(app *v1beta1.Application, appRev string) velaprocess.ContextData { +// The goCtx parameter should contain any policy additionalContext stored by ApplyApplicationScopeTransforms. +func generateContextDataFromApp(goCtx context.Context, app *v1beta1.Application, appRev string) velaprocess.ContextData { data := velaprocess.ContextData{ Namespace: app.Namespace, AppName: app.Name, CompName: app.Name, AppRevisionName: appRev, + Ctx: goCtx, // Pass Go context so process.NewContext can extract policy additionalContext } if app.Annotations != nil { data.WorkflowName = app.Annotations[oam.AnnotationWorkflowName] diff --git a/pkg/controller/core.oam.dev/v1beta1/application/generator_test.go b/pkg/controller/core.oam.dev/v1beta1/application/generator_test.go index 78e4fbae6..24a0c05ab 100644 --- a/pkg/controller/core.oam.dev/v1beta1/application/generator_test.go +++ b/pkg/controller/core.oam.dev/v1beta1/application/generator_test.go @@ -296,7 +296,7 @@ var _ = Describe("Test Application workflow generator", func() { }, Spec: oamcore.ApplicationSpec{Components: []common.ApplicationComponent{}}, } - ctxData := generateContextDataFromApp(app, "apprev-with-meta") + ctxData := generateContextDataFromApp(context.Background(), app, "apprev-with-meta") Expect(ctxData.AppLabels).To(Equal(app.Labels)) Expect(ctxData.AppAnnotations).To(Equal(app.Annotations)) }) @@ -307,7 +307,7 @@ var _ = Describe("Test Application workflow generator", func() { ObjectMeta: metav1.ObjectMeta{Name: "app-without-meta", Namespace: namespaceName}, Spec: oamcore.ApplicationSpec{Components: []common.ApplicationComponent{}}, } - ctxData := generateContextDataFromApp(app, "apprev-without-meta") + ctxData := generateContextDataFromApp(context.Background(), app, "apprev-without-meta") Expect(ctxData.AppLabels).To(BeNil()) Expect(ctxData.AppAnnotations).To(BeNil()) }) diff --git a/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms.go b/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms.go index 22e4f5c91..73df2cc16 100644 --- a/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms.go +++ b/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms.go @@ -42,8 +42,13 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/appfile" + "github.com/oam-dev/kubevela/pkg/config" + velaprocess "github.com/oam-dev/kubevela/pkg/cue/process" "github.com/oam-dev/kubevela/pkg/features" "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/utils/apply" + oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) const ( @@ -166,6 +171,15 @@ func (h *AppHandler) ApplyApplicationScopeTransforms(ctx monitorContext.Context, // Apply the rendered global policy results (either from cache or freshly rendered) for _, result := range globalRenderedResults { + // Check feature gate for Application-scoped policies + if !utilfeature.DefaultMutableFeatureGate.Enabled(features.EnableApplicationScopedPolicies) { + ctx.Info("Skipping Application-scoped global policy (feature gate disabled)", + "policy", result.PolicyName, + "namespace", result.PolicyNamespace, + "featureGate", "EnableApplicationScopedPolicies") + continue + } + ctx.Info("Applying global policy result", "policy", result.PolicyName, "enabled", result.Enabled, "fromCache", cacheHit, "sequence", sequence) // Get priority from the result (stored during render) @@ -213,6 +227,15 @@ func (h *AppHandler) ApplyApplicationScopeTransforms(ctx monitorContext.Context, continue } + // Check feature gate for Application-scoped policies + if !utilfeature.DefaultMutableFeatureGate.Enabled(features.EnableApplicationScopedPolicies) { + ctx.Info("Skipping Application-scoped policy (feature gate disabled)", + "policy", policy.Type, + "name", policy.Name, + "featureGate", "EnableApplicationScopedPolicies") + continue + } + ctx.Info("Applying explicit Application-scoped policy", "policy", policy.Type, "name", policy.Name) // Render and apply (not cached - explicit policies can have unique parameters) @@ -622,50 +645,74 @@ func (h *AppHandler) applyRenderedPolicyResult(ctx monitorContext.Context, app * } // renderPolicyCUETemplate renders the policy CUE template with parameter and context.application -// Follows the same pattern as workloadDef.Complete() and traitDef.Complete() to properly handle import statements +// Now includes CueX support by creating a proper process.Context with runtime parameters. +// This enables CueX actions like kube.#Read while preserving all existing functionality: +// - context.application (Full Application CR) +// - context.prior (Previous policy result for incremental policies) +// - parameter (Policy parameters from Application spec) func (h *AppHandler) renderPolicyCUETemplate(ctx monitorContext.Context, app *v1beta1.Application, params map[string]interface{}, policyDef *v1beta1.PolicyDefinition, priorResult map[string]interface{}) (cue.Value, error) { - // Build CUE source following the pattern from pkg/cue/definition/template.go - // Order matters: template (with imports) must come first, then type annotations, then values - var cueSources []string + // Create runtime context with KubeClient so kube.#Read and other CueX actions work + runtimeCtx := oamprovidertypes.WithRuntimeParams(ctx.GetContext(), oamprovidertypes.RuntimeParams{ + KubeClient: h.Client, + ConfigFactory: config.NewConfigFactoryWithDispatcher(h.Client, func(goCtx context.Context, resources []*unstructured.Unstructured, applyOptions []apply.ApplyOption) error { + // Policies don't dispatch resources directly, but provide this for CueX consistency + return nil + }), + }) - // 1. Add the policy template FIRST (preserves any import statements at the top) - cueSources = append(cueSources, policyDef.Spec.Schematic.CUE.Template) + // Create a process.Context with proper runtime parameters embedded for CueX execution + pCtx := velaprocess.NewContext(velaprocess.ContextData{ + Namespace: app.Namespace, + AppName: app.Name, + CompName: app.Name, // Policy context doesn't have specific component + Ctx: runtimeCtx, // Use runtime context with CueX providers + }) - // 2. Add type annotations (following renderTemplate() pattern from template.go:489) - cueSources = append(cueSources, "parameter: _") - cueSources = append(cueSources, "context: _") - - // 3. Add parameter values + // Build parameter file (as JSON, not type annotation) + var paramFile string if params != nil { paramJSON, err := json.Marshal(params) if err != nil { return cue.Value{}, errors.Wrap(err, "failed to marshal parameters") } - cueSources = append(cueSources, fmt.Sprintf("parameter: %s", string(paramJSON))) + paramFile = fmt.Sprintf("parameter: %s", string(paramJSON)) } else { - cueSources = append(cueSources, "parameter: {}") + paramFile = "parameter: {}" } - // 4. Add context.application (convert Application to JSON) + // Build context object - PRESERVES ALL EXISTING FUNCTIONALITY + // context.application: Full Application CR (existing feature) + // context.prior: Previous policy result for incremental policies (existing feature) + contextParts := []string{} + + // Add application - SAME AS BEFORE appJSON, err := json.Marshal(app) if err != nil { return cue.Value{}, errors.Wrap(err, "failed to marshal Application") } - cueSources = append(cueSources, fmt.Sprintf("context: application: %s", string(appJSON))) + contextParts = append(contextParts, fmt.Sprintf("application: %s", string(appJSON))) - // 5. Add context.prior if available (previous cached policy result) + // Add prior if available - SAME AS BEFORE if priorResult != nil { priorJSON, err := json.Marshal(priorResult) if err != nil { return cue.Value{}, errors.Wrap(err, "failed to marshal prior result") } - cueSources = append(cueSources, fmt.Sprintf("context: prior: %s", string(priorJSON))) + contextParts = append(contextParts, fmt.Sprintf("prior: %s", string(priorJSON))) } - // Compile the CUE using the default CueX compiler - cueSource := strings.Join(cueSources, "\n") - val, err := cuex.DefaultCompiler.Get().CompileString(ctx.GetContext(), cueSource) + contextFile := fmt.Sprintf("context: {\n%s\n}", strings.Join(contextParts, "\n")) + // Build CUE source - NO baseContext needed! + // cuex.DefaultCompiler already has all the imports (kube, http, etc.) + cueSource := strings.Join([]string{ + policyDef.Spec.Schematic.CUE.Template, + paramFile, + contextFile, + }, "\n") + + // Compile with CueX execution enabled (cuex.DefaultCompiler automatically resolves actions) + val, err := cuex.DefaultCompiler.Get().CompileString(pCtx.GetCtx(), cueSource) if err != nil { return cue.Value{}, errors.Wrap(err, "failed to compile CUE template") } @@ -907,6 +954,10 @@ func deepMerge(target, source map[string]interface{}) map[string]interface{} { return result } +// policyAdditionalContextKeyString is the string key for storing additionalContext in Go context +// We use a string key to avoid type mismatches when accessing from different packages (e.g., pkg/cue/process) +const policyAdditionalContextKeyString = "kubevela.oam.dev/policy-additional-context" + // storeAdditionalContextInCtx stores additional policy context in the Go context // This context will be available in workflow steps as context.custom func storeAdditionalContextInCtx(ctx monitorContext.Context, additionalContext map[string]interface{}) monitorContext.Context { @@ -919,16 +970,16 @@ func storeAdditionalContextInCtx(ctx monitorContext.Context, additionalContext m // Merge new context into existing merged := deepMerge(existing, additionalContext) - // Store back in context using the PolicyAdditionalContextKey from application_controller.go + // Store back in context using a string key (avoids type mismatches across packages) // We need to extract the underlying context.Context, add our value, and wrap it back - baseCtx := context.WithValue(ctx.GetContext(), PolicyAdditionalContextKey, merged) + baseCtx := context.WithValue(ctx.GetContext(), policyAdditionalContextKeyString, merged) ctx.SetContext(baseCtx) return ctx } // getAdditionalContextFromCtx retrieves additional policy context from the Go context func getAdditionalContextFromCtx(ctx monitorContext.Context) map[string]interface{} { - if val := ctx.GetContext().Value(PolicyAdditionalContextKey); val != nil { + if val := ctx.GetContext().Value(policyAdditionalContextKeyString); val != nil { if contextMap, ok := val.(map[string]interface{}); ok { return contextMap } diff --git a/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms_test.go b/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms_test.go index 3636cdd94..1f69e9cde 100644 --- a/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms_test.go +++ b/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms_test.go @@ -26,11 +26,13 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + utilfeature "k8s.io/apiserver/pkg/util/feature" "sigs.k8s.io/controller-runtime/pkg/client" monitorContext "github.com/kubevela/pkg/monitor/context" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" ) @@ -472,6 +474,258 @@ output: { Expect(app.Labels).Should(HaveLen(1)) Expect(app.Labels["original"]).Should(Equal("value")) }) + + It("Test Application-scoped policy with CueX kube.#Get action", func() { + // Create a test ConfigMap that the policy will read + testConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-data-cm", + Namespace: namespace, + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + } + Expect(k8sClient.Create(ctx, testConfigMap)).Should(Succeed()) + + // Create PolicyDefinition with CueX kube.#Get action + policyDef := &v1beta1.PolicyDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cuex-read-policy", + Namespace: namespace, + }, + Spec: v1beta1.PolicyDefinitionSpec{ + Scope: v1beta1.ApplicationScope, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: ` +import "vela/kube" + +parameter: {} +enabled: true + +// Use kube.#Get to read a ConfigMap from the cluster +output: kube.#Get & { + $params: { + cluster: "" + resource: { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { + name: "test-data-cm" + namespace: "` + namespace + `" + } + } + } +} + +// The ConfigMap data will be available in additionalContext +additionalContext: { + configMapData: output.$returns.data +} + +// Add a label with data from the ConfigMap +transforms: { + labels: { + type: "merge" + value: { + "from-configmap": output.$returns.data.key1 + } + } +} +`, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, policyDef)).Should(Succeed()) + waitForPolicyDef(ctx, "cuex-read-policy", namespace) + + // Create Application that uses the policy + app := &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-app-cuex", + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{ + { + Name: "my-component", + Type: "webservice", + }, + }, + Policies: []v1beta1.AppPolicy{ + { + Name: "cuex-reader", + Type: "cuex-read-policy", + }, + }, + }, + } + + handler := &AppHandler{ + Client: k8sClient, + } + + monCtx := monitorContext.NewTraceContext(ctx, "test") + _, err := handler.ApplyApplicationScopeTransforms(monCtx, app) + Expect(err).Should(BeNil()) + + // Verify the CueX kube.#Get action executed successfully by checking: + // 1. No error occurred (CueX action executed) + // 2. The label transform was applied (using data from the ConfigMap) + Expect(app.Labels).ShouldNot(BeNil()) + Expect(app.Labels["from-configmap"]).Should(Equal("value1")) + }) + + It("Test policy additionalContext is available to components as context.custom", func() { + // Create a ConfigMap that the policy will read + testCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy-data-cm", + Namespace: namespace, + }, + Data: map[string]string{ + "apiEndpoint": "https://api.example.com", + "region": "us-west-2", + }, + } + Expect(k8sClient.Create(ctx, testCM)).Should(Succeed()) + + // Create PolicyDefinition that reads ConfigMap and exposes it via additionalContext + policyDef := &v1beta1.PolicyDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fetch-config-policy", + Namespace: namespace, + }, + Spec: v1beta1.PolicyDefinitionSpec{ + Scope: v1beta1.ApplicationScope, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: ` +import "vela/kube" + +parameter: {} +enabled: true + +output: kube.#Get & { + $params: { + cluster: "" + resource: { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { + name: "policy-data-cm" + namespace: "` + namespace + `" + } + } + } +} + +// Expose ConfigMap data via additionalContext so components can access it +additionalContext: { + config: { + endpoint: output.$returns.data.apiEndpoint + region: output.$returns.data.region + } +} +`, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, policyDef)).Should(Succeed()) + waitForPolicyDef(ctx, "fetch-config-policy", namespace) + + // Create ComponentDefinition that uses context.custom + compDef := &v1beta1.ComponentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "custom-context-comp", + Namespace: namespace, + }, + Spec: v1beta1.ComponentDefinitionSpec{ + Workload: common.WorkloadTypeDescriptor{ + Definition: common.WorkloadGVK{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + }, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: ` +import "encoding/json" + +output: { + apiVersion: "v1" + kind: "ConfigMap" + metadata: { + name: context.name + namespace: context.namespace + } + data: { + // Access policy additionalContext via context.custom + "from-policy": json.Marshal(context.custom.config) + } +} +`, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, compDef)).Should(Succeed()) + + // Create Application with the policy and component + app := &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-with-custom-context", + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Policies: []v1beta1.AppPolicy{ + { + Name: "fetch-config", + Type: "fetch-config-policy", + }, + }, + Components: []common.ApplicationComponent{ + { + Name: "my-component", + Type: "custom-context-comp", + }, + }, + }, + } + + // Apply Application-scoped policy transforms + handler := &AppHandler{ + Client: k8sClient, + } + + monCtx := monitorContext.NewTraceContext(ctx, "test-custom-context") + monCtx, err := handler.ApplyApplicationScopeTransforms(monCtx, app) + Expect(err).Should(BeNil()) + + // Verify additionalContext is stored in the Go context + // This data will be available to components via context.custom when rendering + const policyAdditionalContextKeyString = "kubevela.oam.dev/policy-additional-context" + + additionalCtx := monCtx.GetContext().Value(policyAdditionalContextKeyString) + Expect(additionalCtx).ShouldNot(BeNil()) + + // Cast and verify the structure + ctxMap, ok := additionalCtx.(map[string]interface{}) + Expect(ok).Should(BeTrue()) + Expect(ctxMap).Should(HaveKey("config")) + + config, ok := ctxMap["config"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + Expect(config["endpoint"]).Should(Equal("https://api.example.com")) + Expect(config["region"]).Should(Equal("us-west-2")) + + // This additionalContext will be extracted by process.NewContext(), wrapped under + // "custom" key, and made available to component/trait templates as context.custom.config + }) }) var _ = Describe("Test Global Policy Cache", func() { @@ -2939,3 +3193,333 @@ transforms: { }) }) }) + +var _ = Describe("Test Application-scoped policy feature gates", func() { + namespace := "policy-featuregate-test" + velaSystem := oam.SystemDefinitionNamespace + var ctx context.Context + + BeforeEach(func() { + ctx = util.SetNamespaceInCtx(context.Background(), namespace) + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + }) + + AfterEach(func() { + // Clean up policies in test namespace + policyList := &v1beta1.PolicyDefinitionList{} + _ = k8sClient.List(ctx, policyList, client.InNamespace(namespace)) + for _, policy := range policyList.Items { + _ = k8sClient.Delete(ctx, &policy) + } + + // Clean up policies in vela-system + velaSystemPolicyList := &v1beta1.PolicyDefinitionList{} + _ = k8sClient.List(ctx, velaSystemPolicyList, client.InNamespace(velaSystem)) + for _, policy := range velaSystemPolicyList.Items { + _ = k8sClient.Delete(ctx, &policy) + } + + // Restore both gates to enabled + Expect(utilfeature.DefaultMutableFeatureGate.Set("EnableGlobalPolicies=true")).ToNot(HaveOccurred()) + Expect(utilfeature.DefaultMutableFeatureGate.Set("EnableApplicationScopedPolicies=true")).ToNot(HaveOccurred()) + }) + + It("should skip explicit Application-scoped policies when EnableApplicationScopedPolicies=false", func() { + // Disable Application-scoped policy execution (but keep global discovery enabled) + Expect(utilfeature.DefaultMutableFeatureGate.Set("EnableApplicationScopedPolicies=false")).ToNot(HaveOccurred()) + + // Create Application-scoped PolicyDefinition + policyDef := &v1beta1.PolicyDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "add-label-explicit", + Namespace: namespace, + }, + Spec: v1beta1.PolicyDefinitionSpec{ + Scope: v1beta1.ApplicationScope, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: ` +parameter: {} +enabled: true +transforms: { + labels: { + type: "merge" + value: { + "test": "gate-disabled" + } + } +} +`, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, policyDef)).Should(Succeed()) + waitForPolicyDef(ctx, "add-label-explicit", namespace) + + // Create Application with explicit policy + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1beta1.SchemeGroupVersion.String(), + Kind: v1beta1.ApplicationKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-explicit-gate", + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{ + {Name: "my-comp", Type: "webservice"}, + }, + Policies: []v1beta1.AppPolicy{ + {Name: "test-policy", Type: "add-label-explicit"}, + }, + }, + } + Expect(k8sClient.Create(ctx, app)).Should(Succeed()) + + handler := &AppHandler{Client: k8sClient, app: app} + monCtx := monitorContext.NewTraceContext(ctx, "test") + _, err := handler.ApplyApplicationScopeTransforms(monCtx, app) + Expect(err).Should(BeNil()) + + // Verify policy was NOT applied (label not added) + Expect(app.Labels).ShouldNot(HaveKey("test")) + // Verify no policies in status + Expect(app.Status.AppliedApplicationPolicies).Should(BeEmpty()) + }) + + It("should discover but not apply global policies when EnableGlobalPolicies=true but EnableApplicationScopedPolicies=false", func() { + // Disable Application-scoped policy execution but keep discovery enabled + Expect(utilfeature.DefaultMutableFeatureGate.Set("EnableApplicationScopedPolicies=false")).ToNot(HaveOccurred()) + + // Create global Application-scoped PolicyDefinition in vela-system + policyDef := &v1beta1.PolicyDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "global-add-label", + Namespace: velaSystem, + }, + Spec: v1beta1.PolicyDefinitionSpec{ + Scope: v1beta1.ApplicationScope, + Global: true, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: ` +parameter: {} +enabled: true +transforms: { + labels: { + type: "merge" + value: { + "global": "policy" + } + } +} +`, + }, + }, + }, + } + velaCtx := util.SetNamespaceInCtx(context.Background(), velaSystem) + Expect(k8sClient.Create(velaCtx, policyDef)).Should(Succeed()) + waitForPolicyDef(velaCtx, "global-add-label", velaSystem) + + // Clear in-memory cache to ensure fresh discovery + globalPolicyCache.InvalidateAll() + + // Create Application + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1beta1.SchemeGroupVersion.String(), + Kind: v1beta1.ApplicationKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-global-gate", + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{ + {Name: "my-comp", Type: "webservice"}, + }, + }, + } + Expect(k8sClient.Create(ctx, app)).Should(Succeed()) + + handler := &AppHandler{Client: k8sClient, app: app} + monCtx := monitorContext.NewTraceContext(ctx, "test") + _, err := handler.ApplyApplicationScopeTransforms(monCtx, app) + Expect(err).Should(BeNil()) + + // Verify policy was NOT applied (label not added) + Expect(app.Labels).ShouldNot(HaveKey("global")) + // Verify no policies in status + Expect(app.Status.AppliedApplicationPolicies).Should(BeEmpty()) + }) + + It("should not discover global policies when EnableGlobalPolicies=false (even if EnableApplicationScopedPolicies=true)", func() { + // Disable global policy discovery but keep execution enabled + Expect(utilfeature.DefaultMutableFeatureGate.Set("EnableGlobalPolicies=false")).ToNot(HaveOccurred()) + + // Create global Application-scoped PolicyDefinition + policyDef := &v1beta1.PolicyDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "global-no-discovery", + Namespace: velaSystem, + }, + Spec: v1beta1.PolicyDefinitionSpec{ + Scope: v1beta1.ApplicationScope, + Global: true, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: ` +parameter: {} +enabled: true +transforms: { + labels: { + type: "merge" + value: { + "discovered": "false" + } + } +} +`, + }, + }, + }, + } + velaCtx := util.SetNamespaceInCtx(context.Background(), velaSystem) + Expect(k8sClient.Create(velaCtx, policyDef)).Should(Succeed()) + waitForPolicyDef(velaCtx, "global-no-discovery", velaSystem) + + // Clear in-memory cache + globalPolicyCache.InvalidateAll() + + // Create Application + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1beta1.SchemeGroupVersion.String(), + Kind: v1beta1.ApplicationKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-no-discovery", + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{ + {Name: "my-comp", Type: "webservice"}, + }, + }, + } + Expect(k8sClient.Create(ctx, app)).Should(Succeed()) + + handler := &AppHandler{Client: k8sClient, app: app} + monCtx := monitorContext.NewTraceContext(ctx, "test") + _, err := handler.ApplyApplicationScopeTransforms(monCtx, app) + Expect(err).Should(BeNil()) + + // Verify policy was NOT applied (not discovered) + Expect(app.Labels).ShouldNot(HaveKey("discovered")) + Expect(app.Status.AppliedApplicationPolicies).Should(BeEmpty()) + }) + + It("should apply both global and explicit policies when both gates are enabled", func() { + // Both gates already enabled in BeforeSuite + + // Create global PolicyDefinition + globalPolicy := &v1beta1.PolicyDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "global-full-func", + Namespace: velaSystem, + }, + Spec: v1beta1.PolicyDefinitionSpec{ + Scope: v1beta1.ApplicationScope, + Global: true, + Priority: 100, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: ` +parameter: {} +enabled: true +transforms: { + labels: { + type: "merge" + value: { + "global": "applied" + } + } +} +`, + }, + }, + }, + } + velaCtx := util.SetNamespaceInCtx(context.Background(), velaSystem) + Expect(k8sClient.Create(velaCtx, globalPolicy)).Should(Succeed()) + waitForPolicyDef(velaCtx, "global-full-func", velaSystem) + + // Create explicit PolicyDefinition + explicitPolicy := &v1beta1.PolicyDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "explicit-full-func", + Namespace: namespace, + }, + Spec: v1beta1.PolicyDefinitionSpec{ + Scope: v1beta1.ApplicationScope, + Schematic: &common.Schematic{ + CUE: &common.CUE{ + Template: ` +parameter: {} +enabled: true +transforms: { + labels: { + type: "merge" + value: { + "explicit": "applied" + } + } +} +`, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, explicitPolicy)).Should(Succeed()) + waitForPolicyDef(ctx, "explicit-full-func", namespace) + + // Clear in-memory cache + globalPolicyCache.InvalidateAll() + + // Create Application with explicit policy + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1beta1.SchemeGroupVersion.String(), + Kind: v1beta1.ApplicationKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-full-func", + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{ + {Name: "my-comp", Type: "webservice"}, + }, + Policies: []v1beta1.AppPolicy{ + {Name: "explicit-policy", Type: "explicit-full-func"}, + }, + }, + } + Expect(k8sClient.Create(ctx, app)).Should(Succeed()) + + handler := &AppHandler{Client: k8sClient, app: app} + monCtx := monitorContext.NewTraceContext(ctx, "test") + _, err := handler.ApplyApplicationScopeTransforms(monCtx, app) + Expect(err).Should(BeNil()) + + // Verify BOTH policies were applied + Expect(app.Labels).Should(HaveKeyWithValue("global", "applied")) + Expect(app.Labels).Should(HaveKeyWithValue("explicit", "applied")) + // Verify status shows both policies + Expect(app.Status.AppliedApplicationPolicies).Should(HaveLen(2)) + }) +}) diff --git a/pkg/controller/core.oam.dev/v1beta1/application/suite_test.go b/pkg/controller/core.oam.dev/v1beta1/application/suite_test.go index d4b2238fd..f4321c4fe 100644 --- a/pkg/controller/core.oam.dev/v1beta1/application/suite_test.go +++ b/pkg/controller/core.oam.dev/v1beta1/application/suite_test.go @@ -80,9 +80,10 @@ var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter))) rand.Seed(time.Now().UnixNano()) - // Enable global policies feature gate for tests + // Enable both Application-scoped policy feature gates for tests Expect(utilfeature.DefaultMutableFeatureGate.Set("EnableGlobalPolicies=true")).ToNot(HaveOccurred()) - logf.Log.Info("Enabled EnableGlobalPolicies feature gate for tests") + Expect(utilfeature.DefaultMutableFeatureGate.Set("EnableApplicationScopedPolicies=true")).ToNot(HaveOccurred()) + logf.Log.Info("Enabled Application-scoped policy feature gates for tests") By("bootstrapping test environment") var yamlPath string diff --git a/pkg/cue/process/handle.go b/pkg/cue/process/handle.go index 61da8381a..53facdf4d 100644 --- a/pkg/cue/process/handle.go +++ b/pkg/cue/process/handle.go @@ -53,8 +53,26 @@ type ContextData struct { Output interface{} } +// policyAdditionalContextKeyString is the string key for policy additionalContext in Go context +// We use a string key to avoid type mismatches across packages +const policyAdditionalContextKeyString = "kubevela.oam.dev/policy-additional-context" + // NewContext creates a new process context func NewContext(data ContextData) process.Context { + // Extract policy additionalContext from Go context if it exists + // This allows Application-scoped policies to inject data into component/trait rendering + var customData map[string]interface{} + if data.Ctx != nil { + if val := data.Ctx.Value(policyAdditionalContextKeyString); val != nil { + if contextMap, ok := val.(map[string]interface{}); ok { + // Wrap additionalContext under "custom" key so it's accessible as context.custom + customData = map[string]interface{}{ + "custom": contextMap, + } + } + } + } + ctx := process.NewContext(process.ContextData{ Namespace: data.Namespace, Name: data.CompName, @@ -64,6 +82,7 @@ func NewContext(data ContextData) process.Context { Ctx: data.Ctx, BaseHooks: data.BaseHooks, AuxiliaryHooks: data.AuxiliaryHooks, + CustomData: customData, }) ctx.PushData(ContextAppName, data.AppName) ctx.PushData(ContextAppRevision, data.AppRevisionName) diff --git a/pkg/features/controller_features.go b/pkg/features/controller_features.go index 3c777fe32..266bbe23d 100644 --- a/pkg/features/controller_features.go +++ b/pkg/features/controller_features.go @@ -124,8 +124,15 @@ const ( // ComponentDefinition/TraitDefinition/WorkflowStepDefinition/PolicyDefinition CUE templates exist in the cluster ValidateResourcesExist = "ValidateResourcesExist" - // EnableGlobalPolicies enables automatic application of global PolicyDefinitions to Applications + // EnableGlobalPolicies enables automatic discovery of global PolicyDefinitions + // Controls whether policies with global: true are discovered from vela-system and namespace EnableGlobalPolicies featuregate.Feature = "EnableGlobalPolicies" + + // EnableApplicationScopedPolicies enables the execution of Application-scoped policies. + // When disabled, policies with scope: Application will not be applied (both global and explicit). + // This gates the core Application transform functionality. Use EnableGlobalPolicies to + // separately control global policy discovery. + EnableApplicationScopedPolicies featuregate.Feature = "EnableApplicationScopedPolicies" ) var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ @@ -155,6 +162,7 @@ var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ EnableApplicationStatusMetrics: {Default: false, PreRelease: featuregate.Alpha}, ValidateResourcesExist: {Default: false, PreRelease: featuregate.Alpha}, EnableGlobalPolicies: {Default: false, PreRelease: featuregate.Alpha}, + EnableApplicationScopedPolicies: {Default: false, PreRelease: featuregate.Alpha}, } func init() {