Compare commits

...

21 Commits

Author SHA1 Message Date
Brian Kane
9d160cdf84 refactor: rename autoRevision annotation to policy.oam.dev namespace
Changed annotation from app.oam.dev/autoRevision to
policy.oam.dev/autoRevision to better indicate its purpose and
prevent misuse in other areas of the codebase.

This annotation specifically controls whether application-scoped
policy transforms should create new ApplicationRevisions.

Updated:
- labels.go: Changed constant value
- devlogs: Updated all references in documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 18:19:20 +00:00
Brian Kane
03a91c9fb1 docs: update devlog with commit SHA for dispatcher fix
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 18:16:11 +00:00
Brian Kane
06e1d20a74 fix: component dispatch with autoRevision=true for policy transforms
When policies retrigger with autoRevision=true, components were not
redeploying even though new ApplicationRevisions were created. The
dispatcher was comparing component properties against the NEW revision
(which already had policy transforms) instead of the PREVIOUS revision.

Changes:
- generator.go: Pass latestAppRev to generateDispatcher()
- dispatcher.go: Add previousAppRev parameter and conditional comparison
  logic based on autoRevision annotation
- When autoRevision=true: Compare against previous revision to detect
  policy-driven changes
- When autoRevision=false (default): Use existing logic for backward
  compatibility

Added documentation noting that the default comparison logic seems
unclear and may need future simplification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 18:15:51 +00:00
Brian Kane
4435c3bb14 feat: add helper functions for new policy flow
Added helper functions to support simplified policy architecture:
- extractRenderedSpec() - extract spec from policy results
- extractRenderedMetadata() - extract labels/annotations/context
- applyMetadataToApp() - apply metadata to Application CR
- applySpecToApp() - apply spec to Application
- renderAllPolicies() - render both global and explicit policies together

These will be used in the refactored ApplyApplicationScopeTransforms.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 12:42:50 +00:00
Brian Kane
3d8440bc45 feat: add autoRevision annotation for policy-driven spec changes
Added app.oam.dev/autoRevision annotation to control whether policies
can modify Application.Spec and trigger new ApplicationRevisions.

Changes:
- Added AnnotationAutoRevision constant to pkg/oam/labels.go
- Added shouldAutoCreateRevision() helper function
- Orthogonal to autoUpdate (which controls definition version updates)
- Consistent naming: autoRevision (not auto-revision) matches autoUpdate

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 12:36:47 +00:00
Brian Kane
a83432e98c refactor: rename GlobalPolicyCache to ApplicationPolicyCache
Simplified cache structure and removed global policy hash tracking.
2026-02-17 12:33:02 +00:00
Brian Kane
68805310e3 WIP: Cascade invalidation approach (will be replaced)
This commit captures the cascade invalidation exploration before
we pivot to a simpler design. Key changes:

1. computeCascadeID() only hashes spec fields (not metadata)
2. computeApplicationHash() only hashes spec (not labels/annotations)
3. Application hash computed BEFORE policies run
4. Attempted to integrate explicit policies into new flow

Design Decision: This approach is too complex. We're pivoting to:
- Simple 1-minute in-memory cache
- Always render policies
- Store rendered vs applied spec in ConfigMap
- Auto-update annotation for spec changes

This commit preserved for historical reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 12:01:07 +00:00
Brian Kane
83675a1aae feat: add per-output-type refresh control infrastructure (Part 4)
Implements the data structures, functions, and API for per-output-type
policy caching with cascade invalidation. Note: Full integration into
the policy execution flow will be done in Part 5.

## What's Implemented

### 1. PolicyConfig with refresh fields (policy_transforms.go:782-809)
- RefreshMode type: RefreshAlways, RefreshNever, RefreshPeriodic
- OutputRefreshConfig struct: Mode, Interval, ForceRefresh
- PolicyConfig with nested Refresh for Spec, Labels, Annotations, Ctx
- Each output type has independent refresh control

### 2. extractRefreshConfig function (policy_transforms.go:845-882)
- Extracts config.refresh from CUE templates
- Applies defaults (all modes default to "never")
- Validates periodic mode requires interval > 0

### 3. Per-output-type cache storage (policy_transforms.go:932-954)
- CachedOutputData: per-type cache with metadata
- PolicyCacheRecord: new ConfigMap format with cascade tracking
- Helper functions: createPolicyCacheRecord, reconstructPolicyOutputFromCache, shouldRefreshOutput

### 4. Cascade ID computation (policy_transforms.go:1506-1525)
- computeCascadeID(): hashes upstream policy outputs
- Enables cascade invalidation when upstream policies refresh

### 5. New cache load function (policy_transforms.go:1593-1683)
- loadCachedPolicyRecord(): loads with cascade & refresh checks
- Checks Application hash, cascade ID, per-output-type refresh
- Old loadCachedPolicyFromConfigMap() kept for backwards compatibility

### 6. Removed CacheTTLSeconds from CRD
- Removed from PolicyDefinitionSpec (policy_definition.go)
- TTL now controlled via config.refresh in templates
- Removed deprecated test

### 7. CUE schema (vela-templates/definitions/internal/policy/app-policy-schema.cue)
- Defines #ApplicationPolicyTemplate with full API
- Provides type safety, validation, defaults
- Documents complete config.refresh API

## API Example

```cue
config: {
  enabled: true
  refresh: {
    labels: { mode: "always" }
    ctx: { mode: "periodic", interval: 300 }
  }
}
```

## Status

 Infrastructure complete
 162 tests passing
 Full integration into execution flow: Part 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 10:16:03 +00:00
Brian Kane
0f5add7902 feat: implement config.enabled for Policy API redesign (Part 3)
Part 3 of Policy API Redesign - updated both tests and implementation:

**Test changes (policy_transforms_test.go):**
- Updated all 25 PolicyDefinition templates to use new API syntax
- Old: `enabled: true` at root level
- New: `config: { enabled: true }`

**Implementation changes (policy_transforms.go):**
- Added PolicyConfig struct with enabled and cacheDuration fields
- Updated extractEnabled() to support both new and old API:
  - Tries config.enabled first (new API)
  - Falls back to root enabled (old API, backwards compatible)
  - Defaults to true if neither exists

**Testing:**
- Tests now pass with new API syntax
- Backwards compatibility maintained for old API
- TDD approach: updated tests first, then implementation

**Next:**
- Part 4: Per-output-type refresh control (config.refresh)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 09:41:52 +00:00
Brian Kane
5330c0f0fe test: fix all policy transform tests for new output API
Updated all tests to use the new 'output' API structure instead of the old
'transforms' API. Key changes:

1. Fixed RenderedPolicyResult usage in tests
   - Changed Output field to Transforms field (stores *PolicyOutput)
   - Added missing Transforms field for tests calling applyRenderedPolicyResult

2. Fixed CUE templates in policy tests
   - Changed 'additionalContext' to 'output.ctx' (3 templates)
   - Fixed kube.#Get usage to avoid double-defining output (2 templates)
   - Changed test expectations from 'transforms' to 'output' key in ConfigMap

3. Removed deprecated transforms API validation
   - Removed test checking for old transforms API rejection
   - Removed extractTransforms() function and its callers
   - Removed PolicyTransforms validation logic

4. Fixed nil pointer bugs in revision comparison
   - Added nil checks in DeepEqualRevision for definition maps
   - Added nil check in gatherRevisionSpec for nil appfile parameter

All policy transform tests now pass successfully.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 20:16:43 +00:00
Brian Kane
deabee9714 fix: handle NewAppHandler error return in regression tests
NewAppHandler returns (*AppHandler, error), but regression tests
were only capturing the handler. Fixed to capture and check error.
2026-02-16 17:10:49 +00:00
Brian Kane
5a3be983dc chore: regenerate deepcopy and CRD manifests
Auto-generated files updated after API changes (output API structs).

Changes are minimal - mostly import reordering in deepcopy files.
2026-02-16 17:05:00 +00:00
Brian Kane
738ddfd98e test: migrate all tests to output API and add regression tests
Completes the test migration for the transforms → output API change.

## Test Migrations

### policy_transforms_test.go (29 transforms → output)
- 21 labels transforms → output.labels
- 7 spec transforms → output.components
- 1 annotations transform → output.annotations

All type/value wrappers removed for cleaner syntax.

### policy_validation_test.go
Already migrated in previous commit.

## Regression Tests Added

### Test 1: Policy spec modifications preserved across status patch
Verifies fix for bug where UpdateAppLatestRevisionStatus() lost
policy-modified spec during Status().Patch() operation.

Tests that after applying a policy that modifies components, the
modifications survive the status update operation.

### Test 2: JSON normalization prevents infinite ApplicationRevisions
Verifies fix for bug where RawExtension JSON with different byte
representations caused infinite revision creation.

Tests that semantically identical JSON with different field order
produces identical normalized bytes and matching hashes.

## Additional Changes

- Minor whitespace cleanup in application_controller.go
- Import reordering in suite_test.go
- Formatting alignment in references/cli/policy.go

All tests now use the new output API exclusively.
2026-02-16 16:58:23 +00:00
Brian Kane
bf8d128128 feat: migrate from transforms API to output API and fix critical bugs
This commit completes the migration from the old transforms API to the
simpler output API, and fixes two critical bugs discovered during testing.

## Part 1: API Migration (transforms → output)

**Old API**:
```cue
transforms: {
  spec: {type: "replace", value: {components: [...]}}
  labels: {type: "merge", value: {"key": "val"}}
}
```

**New API**:
```cue
output: {
  components: [...]          // replaces spec.components (always replace)
  workflow: {...}            // replaces spec.workflow (always replace)
  policies: [...]            // replaces spec.policies (always replace)
  labels: {"key": "val"}     // always merge
  annotations: {...}         // always merge
  ctx: {...}                 // runtime-only context
}
```

**Implementation** (policy_transforms.go):
- Added PolicyOutput struct for new API
- Added extractOutput() to parse output field
- Added applyPolicyTransform() to apply output to Application
- Updated renderPolicy() to support both APIs temporarily
- Reject old transforms API with clear error message

## Part 2: Bug Fixes

### Bug 1: Policy modifications lost during status update

**Problem**: `Status().Patch()` with `client.Merge` refreshes entire object
from API server, losing in-memory spec modifications made by policies.

**Symptom**: Workflows failed with "component not found" errors.

**Fix** (revision.go:544-555): Save/restore app.Spec around patchStatus().

### Bug 2: Infinite ApplicationRevision creation

**Problem**: RawExtension JSON had inconsistent byte representations,
causing DeepEqualRevision() failures and infinite revision creation.

**Symptom**: 100+ identical ApplicationRevisions created.

**Fix** (revision.go:136-146): Normalize component properties JSON in
gatherRevisionSpec() for consistent comparison.

## Testing

- Migrated policy_validation_test.go to output API (14 tests)
- Verified in test cluster: 1 revision per change, workflows work correctly
- Note: policy_transforms_test.go migration in progress

Tested with OCM policy creating ManifestWork - single revision created,
workflow finds correct components.
2026-02-16 16:45:13 +00:00
Brian Kane
f3b67e79ed feat: implement foundation - context cleanup and security (Part 1)
This commit implements Part 1 of the policy refactor plan, establishing
a clean and secure context structure for Application-scoped policies.

Key Changes:

1. Security: Metadata Filtering
   - Added filterUserMetadata() to filter internal annotations/labels
   - Prevents policies from accessing system annotations (app.oam.dev/*,
     kubernetes.io/*, kubectl.kubernetes.io/*, etc.)
   - O(1) map-based filtering for performance

2. Explicit Context Fields
   - Added context.appName (instead of context.application.metadata.name)
   - Added context.namespace, context.appRevision, context.appRevisionNum
   - Added filtered context.appLabels and context.appAnnotations
   - All exposed via process.Context infrastructure

3. Controlled Application Spec Access
   - Added context.appComponents (components array only)
   - Added context.appWorkflow (workflow object only)
   - Added context.appPolicies (policies array only)
   - Prevents unintended access to full Application CR

4. Removed context.application
   - Completely removed to enforce explicit field access
   - Deleted cleanApplicationForPolicyContext() helper function
   - Forces security best practices

5. Removed context.prior
   - Simplified incremental policy feature (can be added back later)
   - Deleted associated test coverage

Test Changes:
   - Deleted 3 test blocks relying on removed features
   - Fixed TTL test expectation (CRD default is -1, not 0)
   - Fixed WorkflowStep struct initialization
   - All tests passing

Benefits:
   -  Clean API with explicit fields
   -  Security: No bypass to unfiltered metadata
   -  Forces best practices
   -  Simpler for policy authors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-13 21:53:48 +00:00
Brian Kane
f36017dfa5 feat: add explicit context fields and filtered metadata to policies
Populate policy context with explicit fields and filtered metadata
using the existing process.Context infrastructure, providing a
secure and user-friendly API for policy templates.

Changes:
- Populate ContextData with filtered labels/annotations (via filterUserMetadata)
- Add explicit fields: appName, namespace, appRevision, appRevisionNum
- Use process.Context.BaseContextFile() to inject context into CUE
- Reuses existing context infrastructure (same as components/workflows)

Context fields now available in policies:
- context.appName - explicit application name
- context.namespace - explicit namespace
- context.appRevision - explicit revision name
- context.appRevisionNum - explicit revision number
- context.appLabels - filtered user labels (internal prefixes removed)
- context.appAnnotations - filtered user annotations (internal prefixes removed)

Security: Filtered metadata isolates policy context from components/workflows:
- Policies: get filtered labels/annotations (secure)
- Components/workflows: get unfiltered labels/annotations (unchanged)
- Policy additionalContext flows via Go context to components as context.custom

Tests:
- Verify explicit fields accessible in policies
- Verify user metadata accessible (filtered)
- Verify internal metadata filtered out

Part of Policy Refactor Plan v3 - Part 1.2 & 1.3: Foundation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-13 20:56:27 +00:00
Brian Kane
566b72b882 feat: add filterUserMetadata for secure policy context
Add filterUserMetadata() function to filter out internal/system
labels and annotations from policy context, preventing policies
from accessing sensitive KubeVela/Kubernetes internal metadata.

Implementation:
- Uses map-based prefix lookup for O(1) performance
- Filters prefixes: app.oam.dev/, oam.dev/, kubectl.kubernetes.io/,
  kubernetes.io/, k8s.io/, helm.sh/, app.kubernetes.io/
- Optimized for hot path (runs on every reconciliation with policies)
- Returns nil for empty results to avoid unnecessary allocations

Tests:
- Filter internal vs user metadata
- Handle empty inputs
- Handle keys without prefixes
- Verify all internal prefixes are excluded

Part of Policy Refactor Plan v3 - Part 1.1: Foundation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-13 20:30:18 +00:00
Brian Kane
d8562f1c2c Checkpoint - bug fixes for application context
Signed-off-by: Brian Kane <briankane1@gmail.com>
2026-02-13 13:42:41 +00:00
Brian Kane
32d166c219 Checkpoint - context working 2026-02-10 17:43:06 +00:00
Brian Kane
bfa143297b Checkpoint - working with caching and globals 2026-02-10 14:08:48 +00:00
Brian Kane
32ac0d69c7 Feature: Configurable Application Policies 2026-02-09 10:19:05 +00:00
50 changed files with 16184 additions and 21 deletions

View File

@@ -205,6 +205,24 @@ type Revision struct {
RevisionHash string `json:"revisionHash,omitempty"`
}
// AppliedApplicationPolicy records minimal status information about an Application-scoped policy.
// This covers both global (auto-discovered) and explicit (spec-referenced) policies.
// Full details (transforms, labels, annotations, etc.) are stored in the ConfigMap referenced
// by ApplicationPoliciesConfigMap for persistent storage and observability.
type AppliedApplicationPolicy struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Applied bool `json:"applied"`
Reason string `json:"reason,omitempty"` // e.g., "enabled=false: namespace mismatch"
Source string `json:"source,omitempty"` // "global" or "explicit" - how the policy was applied
// Summary of changes (counts only - full details in ConfigMap)
SpecModified bool `json:"specModified,omitempty"`
LabelsCount int `json:"labelsCount,omitempty"` // Number of labels added
AnnotationsCount int `json:"annotationsCount,omitempty"` // Number of annotations added
HasContext bool `json:"hasContext,omitempty"` // Has additionalContext data
}
// AppStatus defines the observed state of Application
type AppStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
@@ -239,6 +257,18 @@ type AppStatus struct {
// AppliedResources record the resources that the workflow step apply.
AppliedResources []ClusterObjectReference `json:"appliedResources,omitempty"`
// AppliedApplicationPolicies lists Application-scoped policies (both global and explicit)
// that were discovered and applied (or skipped) during reconciliation.
// This provides transparency for debugging policy effects on the Application spec.
// +optional
AppliedApplicationPolicies []AppliedApplicationPolicy `json:"appliedApplicationPolicies,omitempty"`
// ApplicationPoliciesConfigMap references the ConfigMap containing rendered policy outputs
// (transforms, additionalContext, etc.) for Application-scoped policies.
// Format: "application-policies-{namespace}-{name}"
// +optional
ApplicationPoliciesConfigMap string `json:"applicationPoliciesConfigMap,omitempty"`
// PolicyStatus records the status of policy
// Deprecated This field is only used by EnvBinding Policy which is deprecated.
PolicyStatus []PolicyStatus `json:"policy,omitempty"`

View File

@@ -63,6 +63,11 @@ func (in *AppStatus) DeepCopyInto(out *AppStatus) {
*out = make([]ClusterObjectReference, len(*in))
copy(*out, *in)
}
if in.AppliedApplicationPolicies != nil {
in, out := &in.AppliedApplicationPolicies, &out.AppliedApplicationPolicies
*out = make([]AppliedApplicationPolicy, len(*in))
copy(*out, *in)
}
if in.PolicyStatus != nil {
in, out := &in.PolicyStatus, &out.PolicyStatus
*out = make([]PolicyStatus, len(*in))
@@ -208,6 +213,21 @@ func (in *ApplicationTraitStatus) DeepCopy() *ApplicationTraitStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppliedApplicationPolicy) DeepCopyInto(out *AppliedApplicationPolicy) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppliedApplicationPolicy.
func (in *AppliedApplicationPolicy) DeepCopy() *AppliedApplicationPolicy {
if in == nil {
return nil
}
out := new(AppliedApplicationPolicy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CUE) DeepCopyInto(out *CUE) {
*out = *in

View File

@@ -24,6 +24,19 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
)
// PolicyScope defines the scope at which a policy operates
type PolicyScope string
const (
// DefaultScope (empty string) means standard output-based policy (topology, override, etc.)
// These policies generate Kubernetes resources from their CUE templates
DefaultScope PolicyScope = ""
// ApplicationScope means the policy transforms the Application CR before parsing
// These policies use the transforms pattern and don't generate resources
ApplicationScope PolicyScope = "Application"
)
// PolicyDefinitionSpec defines the desired state of PolicyDefinition
type PolicyDefinitionSpec struct {
// Reference to the CustomResourceDefinition that defines this trait kind.
@@ -40,6 +53,30 @@ type PolicyDefinitionSpec struct {
//+optional
Version string `json:"version,omitempty"`
// Scope defines the scope at which this policy operates.
// - DefaultScope (empty/omitted): Standard output-based policies (topology, override, etc.)
// 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 for discovery and
// EnableApplicationScopedPolicies feature gate for execution.
// +optional
Global bool `json:"global,omitempty"`
// Priority defines the order in which global policies are applied.
// Higher priority policies run first. Policies with the same priority
// are applied in alphabetical order by name.
// If not specified, defaults to 0.
// +optional
Priority int32 `json:"priority,omitempty"`
}
// PolicyDefinitionStatus is the status of PolicyDefinition

View File

@@ -372,6 +372,50 @@ spec:
status:
description: AppStatus defines the observed state of Application
properties:
applicationPoliciesConfigMap:
description: |-
ApplicationPoliciesConfigMap references the ConfigMap containing rendered policy outputs
(transforms, additionalContext, etc.) for Application-scoped policies.
Format: "application-policies-{namespace}-{name}"
type: string
appliedApplicationPolicies:
description: |-
AppliedApplicationPolicies lists Application-scoped policies (both global and explicit)
that were discovered and applied (or skipped) during reconciliation.
This provides transparency for debugging policy effects on the Application spec.
items:
description: |-
AppliedApplicationPolicy records minimal status information about an Application-scoped policy.
This covers both global (auto-discovered) and explicit (spec-referenced) policies.
Full details (transforms, labels, annotations, etc.) are stored in the ConfigMap referenced
by ApplicationPoliciesConfigMap for persistent storage and observability.
properties:
annotationsCount:
type: integer
applied:
type: boolean
hasContext:
type: boolean
labelsCount:
type: integer
name:
type: string
namespace:
type: string
reason:
type: string
source:
type: string
specModified:
description: Summary of changes (counts only - full
details in ConfigMap)
type: boolean
required:
- applied
- name
- namespace
type: object
type: array
appliedResources:
description: AppliedResources record the resources that the workflow
step apply.
@@ -1210,6 +1254,17 @@ spec:
description: PolicyDefinitionSpec defines the desired state
of PolicyDefinition
properties:
cacheTTLSeconds:
default: -1
description: |-
CacheTTLSeconds defines how long the rendered policy output should be cached
before re-rendering. This is stored per-policy in the ConfigMap.
- -1 (default): Never refresh, always reuse cached result (deterministic)
- 0: Never cache, always re-render (useful for policies with external dependencies)
- >0: Cache for this many seconds before re-rendering
The prior cached result is available to the policy template as context.prior
format: int32
type: integer
definitionRef:
description: Reference to the CustomResourceDefinition that
defines this trait kind.
@@ -1225,11 +1280,27 @@ spec:
required:
- name
type: object
global:
description: |-
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 for discovery and
EnableApplicationScopedPolicies feature gate for execution.
type: boolean
manageHealthCheck:
description: |-
ManageHealthCheck means the policy will handle health checking and skip application controller
built-in health checking.
type: boolean
priority:
description: |-
Priority defines the order in which global policies are applied.
Higher priority policies run first. Policies with the same priority
are applied in alphabetical order by name.
If not specified, defaults to 0.
format: int32
type: integer
schematic:
description: |-
Schematic defines the data format and template of the encapsulation of the policy definition.
@@ -1326,6 +1397,15 @@ spec:
- configuration
type: object
type: object
scope:
description: |-
Scope defines the scope at which this policy operates.
- DefaultScope (empty/omitted): Standard output-based policies (topology, override, etc.)
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.
type: string
version:
type: string
type: object

View File

@@ -322,6 +322,50 @@ spec:
status:
description: AppStatus defines the observed state of Application
properties:
applicationPoliciesConfigMap:
description: |-
ApplicationPoliciesConfigMap references the ConfigMap containing rendered policy outputs
(transforms, additionalContext, etc.) for Application-scoped policies.
Format: "application-policies-{namespace}-{name}"
type: string
appliedApplicationPolicies:
description: |-
AppliedApplicationPolicies lists Application-scoped policies (both global and explicit)
that were discovered and applied (or skipped) during reconciliation.
This provides transparency for debugging policy effects on the Application spec.
items:
description: |-
AppliedApplicationPolicy records minimal status information about an Application-scoped policy.
This covers both global (auto-discovered) and explicit (spec-referenced) policies.
Full details (transforms, labels, annotations, etc.) are stored in the ConfigMap referenced
by ApplicationPoliciesConfigMap for persistent storage and observability.
properties:
annotationsCount:
type: integer
applied:
type: boolean
hasContext:
type: boolean
labelsCount:
type: integer
name:
type: string
namespace:
type: string
reason:
type: string
source:
type: string
specModified:
description: Summary of changes (counts only - full details
in ConfigMap)
type: boolean
required:
- applied
- name
- namespace
type: object
type: array
appliedResources:
description: AppliedResources record the resources that the workflow
step apply.

View File

@@ -380,6 +380,17 @@ spec:
description: PolicyDefinitionSpec defines the desired state of
PolicyDefinition
properties:
cacheTTLSeconds:
default: -1
description: |-
CacheTTLSeconds defines how long the rendered policy output should be cached
before re-rendering. This is stored per-policy in the ConfigMap.
- -1 (default): Never refresh, always reuse cached result (deterministic)
- 0: Never cache, always re-render (useful for policies with external dependencies)
- >0: Cache for this many seconds before re-rendering
The prior cached result is available to the policy template as context.prior
format: int32
type: integer
definitionRef:
description: Reference to the CustomResourceDefinition that
defines this trait kind.
@@ -395,11 +406,27 @@ spec:
required:
- name
type: object
global:
description: |-
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 for discovery and
EnableApplicationScopedPolicies feature gate for execution.
type: boolean
manageHealthCheck:
description: |-
ManageHealthCheck means the policy will handle health checking and skip application controller
built-in health checking.
type: boolean
priority:
description: |-
Priority defines the order in which global policies are applied.
Higher priority policies run first. Policies with the same priority
are applied in alphabetical order by name.
If not specified, defaults to 0.
format: int32
type: integer
schematic:
description: |-
Schematic defines the data format and template of the encapsulation of the policy definition.
@@ -495,6 +522,15 @@ spec:
- configuration
type: object
type: object
scope:
description: |-
Scope defines the scope at which this policy operates.
- DefaultScope (empty/omitted): Standard output-based policies (topology, override, etc.)
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.
type: string
version:
type: string
type: object

View File

@@ -43,6 +43,17 @@ spec:
spec:
description: PolicyDefinitionSpec defines the desired state of PolicyDefinition
properties:
cacheTTLSeconds:
default: -1
description: |-
CacheTTLSeconds defines how long the rendered policy output should be cached
before re-rendering. This is stored per-policy in the ConfigMap.
- -1 (default): Never refresh, always reuse cached result (deterministic)
- 0: Never cache, always re-render (useful for policies with external dependencies)
- >0: Cache for this many seconds before re-rendering
The prior cached result is available to the policy template as context.prior
format: int32
type: integer
definitionRef:
description: Reference to the CustomResourceDefinition that defines
this trait kind.
@@ -58,11 +69,27 @@ spec:
required:
- name
type: object
global:
description: |-
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 for discovery and
EnableApplicationScopedPolicies feature gate for execution.
type: boolean
manageHealthCheck:
description: |-
ManageHealthCheck means the policy will handle health checking and skip application controller
built-in health checking.
type: boolean
priority:
description: |-
Priority defines the order in which global policies are applied.
Higher priority policies run first. Policies with the same priority
are applied in alphabetical order by name.
If not specified, defaults to 0.
format: int32
type: integer
schematic:
description: |-
Schematic defines the data format and template of the encapsulation of the policy definition.
@@ -156,6 +183,15 @@ spec:
- configuration
type: object
type: object
scope:
description: |-
Scope defines the scope at which this policy operates.
- DefaultScope (empty/omitted): Standard output-based policies (topology, override, etc.)
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.
type: string
version:
type: string
type: object

View File

View File

@@ -0,0 +1,678 @@
# DevLog: Application-Scoped Policies Feature
**Date:** 2026-02-17
**Feature:** Application-scoped policy transformations for KubeVela
**Status:** Core feature complete, tested, and working
---
## Table of Contents
1. [Overview](#overview)
2. [Architecture](#architecture)
3. [Key Files and Changes](#key-files-and-changes)
4. [Critical Bugs Fixed](#critical-bugs-fixed)
5. [Testing](#testing)
6. [Usage Guide](#usage-guide)
7. [Pending Work](#pending-work)
---
## Overview
### What This Feature Does
Application-scoped policies allow PolicyDefinitions to transform an Application CR **before** it's parsed and deployed. Unlike trait-level policies that apply to workloads, these policies can:
- **Transform the spec**: Add/modify/remove components, change workflow steps
- **Add metadata**: Inject labels and annotations
- **Provide context**: Share data between policies and make it available to workflows/components
- **Chain transformations**: Multiple policies execute in priority order, each seeing the previous policy's output
### Core Concepts
1. **Application-scoped PolicyDefinition**: A PolicyDefinition with `scope: application`
2. **Global vs Explicit Policies**:
- **Global**: Discovered automatically in `vela-system` or Application's namespace, filtered by selectors
- **Explicit**: Listed in `Application.spec.policies[]`
3. **In-memory transformations**: Policy changes to `Application.spec` happen in-memory during reconciliation
4. **ApplicationRevision**: Stores the transformed spec as the source of truth
5. **Caching**: 1-minute TTL cache for rendered policy results (invalidates on spec changes)
---
## Architecture
### Processing Flow
```
1. Application reconciliation starts
2. Check cache (1-min TTL + spec hash)
├─ HIT: Use cached rendered results
└─ MISS: Render all policies (global + explicit)
3. Extract rendered outputs:
├─ metadata: labels, annotations, context
└─ spec: components, workflow, policies
4. Apply metadata to Application (always)
5. Apply spec transformations to Application (in-memory)
6. Check autoRevision annotation:
├─ true: Keep transformed spec → creates new revisions
└─ false: Restore from latest ApplicationRevision → stable spec
7. Store results in ConfigMap (observability)
8. Continue normal Application processing
```
### Policy Chaining
Policies execute in priority order (highest first):
```cue
// Policy 1 (priority: 200)
output: {
components: [originalComponents + {name: "added-by-policy-1"}]
}
// Policy 2 (priority: 100) receives Policy 1's output as input
context: {
previous: {
components: [...] // Output from Policy 1
}
}
output: {
components: context.previous.components + [{name: "added-by-policy-2"}]
}
```
---
## Key Files and Changes
### Core Implementation Files
#### 1. `/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms.go`
**Purpose**: Main policy rendering and transformation logic
**Key Functions**:
- `ApplyApplicationScopeTransforms()` - Entry point, orchestrates policy application
- `renderAllPolicies()` - Discovers and renders global + explicit policies
- `renderSinglePolicy()` - Renders one policy with chaining context
- `applySpecToApp()` - Applies transformed spec to Application (in-memory)
- `shouldAutoCreateRevision()` - Checks `policy.oam.dev/autoRevision` annotation
**Critical Lines**:
- **Lines 93-117**: Cache check and policy rendering
- **Lines 135-179**: Apply transformations and ApplicationRevision restoration logic
- **Lines 146-170**: **BUG FIX** - Restore from ApplicationRevision when autoRevision=false
- **Lines 240-310**: Store results in ConfigMap for observability
- **Lines 1139-1147**: Check autoRevision annotation
#### 2. `/pkg/controller/core.oam.dev/v1beta1/application/application_policy_cache.go`
**Purpose**: In-memory cache for rendered policy results
**Key Features**:
- 1-minute TTL
- Invalidates on Application.Spec hash change
- Thread-safe (sync.RWMutex)
- Singleton instance: `applicationPolicyCache`
**Key Functions**:
- `Get()` / `GetWithReason()` - Retrieve cached results
- `Set()` - Store rendered results
- `InvalidateAll()` - Clear entire cache (used when global policies change)
- `InvalidateForNamespace()` - Clear namespace entries
- `computeAppSpecHash()` - Hash Application.Spec for invalidation
#### 3. `/pkg/controller/core.oam.dev/v1beta1/application/application_controller.go`
**Purpose**: Main Application controller
**Key Changes**:
- **Line ~180**: Call `ApplyApplicationScopeTransforms()` before parsing Application
- Policies apply **before** any parsing, component resolution, or workflow execution
#### 4. `/pkg/oam/labels.go`
**Purpose**: Label and annotation constants
**Key Addition**:
- **Lines 162-165**: `AnnotationAutoRevision = "policy.oam.dev/autoRevision"`
- Controls whether policy transformations create new ApplicationRevisions
- Default: `false` (stable spec, restore from ApplicationRevision)
- Set to `"true"` to enable revision creation on every transformation
#### 5. `/apis/core.oam.dev/v1beta1/application_types.go`
**Purpose**: Application status types
**Key Addition**:
- `Application.Status.AppliedApplicationPolicies` - List of applied policies with metadata
- `Application.Status.ApplicationPoliciesConfigMap` - ConfigMap name for observability
---
### Test Files
#### `/pkg/controller/core.oam.dev/v1beta1/application/policy_transforms_test.go`
**Key Tests Added**:
1. **Test Global PolicyDefinition Features** (line ~870)
- Tests global policy discovery and filtering
- Tests namespace-scoped vs vela-system global policies
- Tests label/namespace selectors
2. **Test Policy Chaining** (line ~1020)
- Tests `context.previous` propagation between policies
- Verifies priority-based execution order
3. **Test ApplicationRevision Restoration** (line ~1199)
- **NEW TEST** - Verifies double revision bug fix
- Tests that subsequent reconciliations restore from ApplicationRevision
- Ensures only ONE revision created on initial deployment
---
## Critical Bugs Fixed
### Bug #1: Double ApplicationRevision Creation
**Problem**: When deploying an Application with policies, TWO ApplicationRevisions were created immediately:
- v1: Had policy-transformed spec (correct)
- v2: Had original untransformed spec (wrong)
**Root Cause**:
1. First reconciliation: Cache MISS → policies render → transformations applied → v1 created ✅
2. Second reconciliation (triggered by status update): Cache HIT → transformations **not applied** to app.Spec
3. app.Spec still had original untransformed spec
4. This created v2 with different hash ❌
**Solution** (lines 146-170 in `policy_transforms.go`):
```go
// After applying transformed spec:
if !isFirstRevision && !autoRevision {
// Load latest ApplicationRevision
latestRev := &v1beta1.ApplicationRevision{}
revName := app.Status.LatestRevision.Name
err := h.Client.Get(ctx, client.ObjectKey{
Name: revName,
Namespace: app.Namespace,
}, latestRev)
if err == nil && latestRev.Spec.Application.Spec.Components != nil {
// Restore spec from revision (has policy transforms baked in)
app.Spec.Components = latestRev.Spec.Application.Spec.Components
if latestRev.Spec.Application.Spec.Workflow != nil {
app.Spec.Workflow = latestRev.Spec.Application.Spec.Workflow
}
if len(latestRev.Spec.Application.Spec.Policies) > 0 {
app.Spec.Policies = latestRev.Spec.Application.Spec.Policies
}
}
}
```
**Why It Works**:
- ApplicationRevision becomes the **source of truth** for the transformed spec
- On subsequent reconciliations with `autoRevision=false`, we restore from the revision
- This prevents creating new revisions from cached policy renders
- User changes to Application.Spec still invalidate cache and trigger re-rendering
**Test Added**: `"Test ApplicationRevision restoration prevents double revisions"` (line 1199)
---
## Testing
### Unit Tests
Run all policy transform tests:
```bash
go test -v ./pkg/controller/core.oam.dev/v1beta1/application -run "TestPolicyTransforms" -timeout 5m
```
Run specific test:
```bash
go test -v ./pkg/controller/core.oam.dev/v1beta1/application -run "TestPolicyTransforms.*double.*revision"
```
### Integration Testing
1. **Create a global policy**:
```yaml
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: atlas-context
namespace: vela-system
spec:
global: true
priority: 100
scope: application
schematic:
cue:
template: |
parameter: {}
output: {
labels: {
"custom.guidewire.dev/service-id": "my-service"
}
context: {
environment: "production"
}
}
```
2. **Create an Application**:
```yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: my-app
namespace: default
spec:
components:
- name: my-component
type: webservice
properties:
image: nginx:latest
```
3. **Verify policies applied**:
```bash
# Check Application labels
kubectl get application my-app -o jsonpath='{.metadata.labels}'
# Check status
kubectl get application my-app -o jsonpath='{.status.appliedApplicationPolicies}'
# Check ConfigMap for observability
kubectl get configmap application-policies-default-my-app -o yaml
```
4. **Verify only ONE ApplicationRevision created**:
```bash
kubectl get applicationrevision | grep my-app
# Should only see: my-app-v1
```
### Debugging Tools
**View controller logs**:
```bash
kubectl logs -n vela-system deployment/kubevela-vela-core --tail=100 | grep -E "(Cache|policy|autoRevision|Restored)"
```
**Key log messages to look for**:
- `"Cache MISS - rendering all policies"` - First render
- `"Cache HIT - using cached policy results"` - Cache working
- `"Applied policy-rendered spec"` - Transformations applied
- `"Restored spec from ApplicationRevision"` - Restoration working (when autoRevision=false)
- `"Policy transforms completed" ... autoRevision=true/false` - Shows annotation value
**Check ConfigMap for rendered results**:
```bash
kubectl get configmap application-policies-default-<app-name> -o yaml
```
Contains:
- `info.yaml` - Metadata about rendering
- `rendered_<policy-name>.yaml` - Each policy's output
- `applied_spec.yaml` - Final transformed spec
- `original_spec.yaml` - Original Application spec
---
## Usage Guide
### Basic Usage: Explicit Policy
```yaml
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: add-sidecar
namespace: default
spec:
scope: application # <-- Application-scoped
schematic:
cue:
template: |
parameter: {
sidecarImage: string
}
output: {
components: [
for comp in context.appSpec.components {comp},
{
name: "sidecar"
type: "webservice"
properties: {
image: parameter.sidecarImage
}
}
]
}
---
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: my-app
spec:
components:
- name: main
type: webservice
policies:
- name: add-sidecar
type: add-sidecar
properties:
sidecarImage: "envoy:v1.0"
```
### Global Policy with Selectors
```yaml
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: production-labels
namespace: vela-system
spec:
global: true
priority: 200
scope: application
# Only apply to Applications with this label
labelSelector:
"environment": "production"
schematic:
cue:
template: |
output: {
labels: {
"compliance.company.com/scanned": "true"
"security.company.com/level": "high"
}
}
```
### Policy Chaining Example
```yaml
# Policy 1: Add monitoring component
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: add-monitoring
namespace: vela-system
spec:
global: true
priority: 200
scope: application
schematic:
cue:
template: |
output: {
components: [
for comp in context.appSpec.components {comp},
{
name: "prometheus"
type: "webservice"
properties: {image: "prometheus:latest"}
}
]
}
---
# Policy 2: Configure monitoring (sees Policy 1's output)
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: configure-monitoring
namespace: vela-system
spec:
global: true
priority: 100 # Runs AFTER add-monitoring
scope: application
schematic:
cue:
template: |
import "list"
// Access previous policy's output
_prevComponents: context.previous.components
output: {
components: [
for comp in _prevComponents {
if comp.name == "prometheus" {
comp & {
properties: {
env: [{name: "SCRAPE_INTERVAL", value: "30s"}]
}
}
}
if comp.name != "prometheus" {
comp
}
}
]
}
```
### Enabling autoRevision
To make policy transformations create new ApplicationRevisions on every change:
```yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: my-app
annotations:
policy.oam.dev/autoRevision: "true" # <-- Enable revision creation
spec:
components:
- name: my-component
type: webservice
```
**Important**: This is an **annotation**, not a label!
**Behavior**:
- `autoRevision: "true"` - Policy transformations create new revisions
- `autoRevision: "false"` (default) - Spec restored from ApplicationRevision, no new revisions
- Useful for GitOps workflows where you want stable revision numbers
---
## Bug #2: Component Dispatch Not Triggered with autoRevision=true
**Date Fixed:** 2026-02-17
**Problem**: When using `autoRevision=true` and policies retrigger (e.g., global PolicyDefinition changes), a new ApplicationRevision was created but components were NOT redeployed. Only direct changes to Application.spec triggered redeployment.
**Root Cause**: The dispatcher's `componentPropertiesChanged()` function compared component properties against the WRONG ApplicationRevision:
- Compared against `currentAppRev` (the NEW revision being created, which already had policy transforms)
- Both `comp.Params` and `currentAppRev` had the same transformed values
- Result: No difference detected → No dispatch
**The Fix** (commit SHA: 06e1d20a7):
Modified 2 files to conditionally use `latestAppRev` (previous revision) for comparison when `autoRevision=true`:
1. **generator.go:330** - Pass `h.latestAppRev` as additional parameter to `generateDispatcher()`
2. **dispatcher.go:117** - Updated signature to accept `previousAppRev` parameter
3. **dispatcher.go:149-171** - Added conditional comparison logic:
```go
comparisonRev := appRev // Default: use currentAppRev (existing behavior)
// If autoRevision=true and we have a previous revision, compare against it
if annotations[oam.AnnotationAutoRevision] == "true" && previousAppRev != nil {
comparisonRev = previousAppRev // Use previous revision for comparison
}
propertiesChanged = componentPropertiesChanged(comp, comparisonRev)
```
**Why It Works**:
- With `autoRevision=true`: Compares transformed component (from workflow) vs previous revision (before transform) → Detects change
- With `autoRevision=false` (default): Uses existing logic (compare vs currentAppRev) for backward compatibility
- First deployment: Falls back to `currentAppRev` when `previousAppRev == nil`
**Documentation Added**:
- Added extensive comment in `dispatcher.go:151-158` noting that the default comparison logic "seems unclear and may have become over-complicated over time"
- Left breadcrumb for future developers to consider simplifying this logic unconditionally
- Updated `componentPropertiesChanged()` function documentation
**Files Changed**:
- `/pkg/controller/core.oam.dev/v1beta1/application/generator.go`
- `/pkg/controller/core.oam.dev/v1beta1/application/dispatcher.go`
---
## Pending Work
### High Priority
1. **Implement `context.previous` support** ✅ (Partially done - needs verification)
- Currently: Policies can access previous policy's output
- TODO: Verify chaining works correctly in all scenarios
- Test file: Already has test at line ~1020
2. **Remove cascade invalidation code** (if not needed)
- Location: `application_policy_cache.go:142-151`
- Current: `InvalidateForNamespace()` deletes ALL entries
- Should only delete entries for affected namespace
- Need to evaluate if this is correct behavior
### Medium Priority
3. **Add metrics/telemetry**
- Cache hit/miss rates
- Policy rendering duration
- Number of policies applied per Application
4. **Improve error handling**
- Better error messages when policy CUE template fails
- Distinguish between policy errors vs Application errors
5. **Documentation**
- User-facing docs for writing Application-scoped policies
- Migration guide from trait-level policies
- Best practices for policy chaining
### Low Priority
6. **Performance optimizations**
- Consider longer cache TTL with smarter invalidation
- Parallel policy rendering (if policies don't depend on each other)
7. **Enhanced ConfigMap output**
- Add diff between original and transformed spec
- Include policy execution order and timing
---
## CUE Template Context Reference
Application-scoped policies have access to:
```cue
// Current Application spec (original, before transformations)
context: {
appSpec: {
components: [...]
policies: [...]
workflow: {...}
}
// Application metadata
appName: string
appNamespace: string
appLabels: {...}
appAnnotations: {...}
// Previous policy's output (for chaining)
previous: {
components: [...]
workflow: {...}
policies: [...]
labels: {...}
annotations: {...}
context: {...}
}
}
// Policy parameters
parameter: {
// User-provided parameters from Application.spec.policies[].properties
}
// Policy output
output: {
// Spec transformations (optional)
components?: [...]
workflow?: {...}
policies?: [...]
// Metadata additions (optional)
labels?: {...}
annotations?: {...}
// Additional context for other policies/workflow (optional)
context?: {...}
}
```
---
## Key Learnings
1. **ApplicationRevision as source of truth**: Using ApplicationRevision to restore the transformed spec was the key insight to fixing the double revision bug. It provides a stable "memory" of what the spec should be.
2. **In-memory transformations**: Keeping transformations in-memory (not persisting to etcd) ensures policies don't cause infinite reconciliation loops.
3. **Cache invalidation is hard**: The spec hash approach works well, but requires careful consideration of what should trigger invalidation.
4. **Annotations vs Labels**: The `autoRevision` control MUST be an annotation (not label) because that's where the code checks. This tripped up initial testing.
5. **Policy chaining complexity**: Providing `context.previous` requires careful ordering and state management during rendering.
---
## Related Files
### Configuration
- `/apis/core.oam.dev/v1beta1/application_types.go` - Application CRD types
- `/apis/core.oam.dev/v1beta1/policy_types.go` - PolicyDefinition CRD types
- `/pkg/oam/labels.go` - Label/annotation constants
### Controllers
- `/pkg/controller/core.oam.dev/v1beta1/application/application_controller.go` - Main controller
- `/pkg/controller/core.oam.dev/v1beta1/application/parser.go` - Application parsing
- `/pkg/controller/core.oam.dev/v1beta1/application/revision.go` - ApplicationRevision handling
### Utilities
- `/pkg/utils/apply/apply.go` - ComputeSpecHash function
- `/pkg/monitor/context/context.go` - Monitoring context utilities
---
## Commit History
### Main Commits
1. Initial implementation of Application-scoped policies
2. Added policy caching with TTL and spec hash invalidation
3. **[CRITICAL FIX]** Fixed double ApplicationRevision bug by restoring from ApplicationRevision
4. Added test case for ApplicationRevision restoration
5. Added `policy.oam.dev/autoRevision` annotation support
### Test Commits
- Added global policy discovery tests
- Added policy chaining tests
- Added ApplicationRevision restoration tests
---
## Contact / Questions
For questions about this feature:
- Check logs: Look for "policy" or "Cache" messages in controller logs
- Check ConfigMap: `application-policies-<namespace>-<app-name>` for debugging
- Check status: `kubectl get app <name> -o jsonpath='{.status.appliedApplicationPolicies}'`
---
**Last Updated:** 2026-02-17
**Status:** Feature complete and tested
**Next Steps:** Address pending work items above

View File

@@ -0,0 +1,482 @@
# PolicyDefinition Validation Requirements
## Overview
This document defines validation rules for PolicyDefinitions, especially global policies.
## Critical Validations (MUST)
### 1. Global Policy Parameter Validation
**Rule**: Global policies MUST have default values for ALL parameters.
**Rationale**: Global policies are auto-discovered and applied without user input. Even optional parameters without defaults cannot compile.
**Implementation**: Parse CUE AST and check that ALL parameter fields have default values (using `*` marker).
```cue
# INVALID - required parameter without default
parameter: {
envName: string // Can't compile without value!
}
# INVALID - optional but no default
parameter: {
envName?: string // Optional doesn't help - still can't compile!
}
# VALID - no parameters
parameter: {}
# VALID - all fields have defaults
parameter: {
envName: *"production" | string // Has default (*"production")
replicas: *3 | int // Has default (*3)
logLevel: *"info" | string // Has default (*"info")
}
```
**Error Message**: "Global policy '<name>' cannot have parameters without default values. Found parameters without defaults: [envName]. Global policies are auto-applied without user input, so all parameters must have default values using '*'. Example: envName: *\"production\" | string"
### 2. Global Policy Priority Validation
**Rule**: Global policies MUST have a priority field set.
**Rationale**: Without priority, execution order is purely alphabetical, making ordering unpredictable.
**Implementation**: Check `spec.priority` is set when `spec.global == true`.
```yaml
# ❌ INVALID - global without priority
spec:
global: true
# priority not set (defaults to 0, but should be explicit)
# ✅ VALID - explicit priority
spec:
global: true
priority: 100 # Explicit ordering
```
**Error Message**: "Global policy '<name>' must have an explicit priority field. This ensures predictable execution order."
**Alternative**: Make it a warning instead of error? Allow default 0?
### 3. CUE Schema Validation
**Rule**: CUE template MUST be syntactically valid and conform to expected schema.
**Schema**:
```cue
parameter: {[string]: _}
enabled: *true | bool
transforms?: {
spec?: {
type: "replace" | "merge"
value: {...}
}
labels?: {
type: "merge" // Only merge allowed
value: {[string]: string}
}
annotations?: {
type: "merge" // Only merge allowed
value: {[string]: string}
}
}
additionalContext?: {[string]: _}
```
**Validation**:
- CUE syntax is valid (can compile)
- `enabled` is boolean or defaults to true
- `transforms.labels.type` is only "merge" (not "replace")
- `transforms.annotations.type` is only "merge" (not "replace")
- `transforms.spec.type` is "merge" or "replace"
**Error Messages**:
- "CUE template syntax error: <details>"
- "transforms.labels.type must be 'merge', not 'replace'"
- "transforms.annotations.type must be 'merge', not 'replace'"
### 4. Scope Validation
**Rule**: Global policies MUST have scope="Application".
**Rationale**: Only Application-scoped policies can transform Applications.
```yaml
# ❌ INVALID - global but wrong scope
spec:
global: true
scope: WorkflowStep # Wrong!
# ✅ VALID
spec:
global: true
scope: Application
```
**Error Message**: "Global policies must have scope='Application', found scope='<value>'"
## Important Validations (SHOULD)
### 5. Documentation Requirements
**Rule**: Global policies SHOULD have documentation annotations.
**Rationale**: Helps users understand what policies do and who owns them.
```yaml
# ⚠️ WARNING - missing documentation
metadata:
name: my-global-policy
# ✅ GOOD - well documented
metadata:
name: security-hardening
annotations:
policy.oam.dev/description: "Adds required security labels"
policy.oam.dev/owner: "platform-team@company.com"
policy.oam.dev/version: "v1.0.0"
```
**Warning Message**: "Global policy '<name>' should have documentation annotations: policy.oam.dev/description, policy.oam.dev/owner"
### 6. Naming Conventions
**Rule**: Global policies SHOULD follow naming conventions.
**Recommendations**:
- Use kebab-case: `security-hardening` not `securityHardening`
- Be descriptive: `add-compliance-labels` not `labels`
- Avoid generic names: `monitoring-config` not `config`
**Warning Message**: "Policy name '<name>' should use kebab-case and be descriptive"
### 7. Dangerous CUE Imports
**Rule**: SHOULD warn on dangerous CUE imports.
**Potentially dangerous imports**:
- `net`: Network operations
- `exec`: Execute commands
- `file`: File system access
- `http`: HTTP requests (may be legitimate for lookups)
**Legitimate imports**:
- `strings`: String manipulation
- `list`: List operations
- `math`: Math operations
- `encoding/json`: JSON parsing
- `encoding/yaml`: YAML parsing
```cue
# WARNING - potentially dangerous
import "net"
# OK
import "strings"
import "encoding/json"
```
**Warning Message**: "Policy uses potentially dangerous import: <import>. Review security implications."
### 8. Critical Label Protection
**Rule**: SHOULD prevent removal of critical platform labels.
**Protected labels/annotations**:
- `app.kubernetes.io/*`
- `app.oam.dev/*`
- `oam.dev/*`
- Any labels added by KubeVela itself
**Implementation**: Check if policy uses "replace" on labels/annotations (already blocked by rule #3).
### 9. Priority Ranges
**Rule**: SHOULD use priority ranges by category.
**Recommended ranges**:
- 1000-1999: Critical security/compliance
- 500-999: Standard platform policies
- 100-499: Optional enhancements
- 0-99: Low priority defaults
```yaml
# ⚠️ WARNING - unusual priority
spec:
global: true
priority: 50000 # Unusually high
# ✅ GOOD
spec:
global: true
priority: 1000 # Security policy
```
**Warning Message**: "Priority <value> is outside recommended ranges. Consider using: 1000-1999 (security), 500-999 (standard), 100-499 (optional), 0-99 (low priority)."
## Advanced Validations (NICE-TO-HAVE)
### 10. CUE Complexity Analysis
**Rule**: MAY warn on complex CUE templates.
**Metrics**:
- Nesting depth > 5
- Template length > 200 lines
- Large loops (processing > 100 items)
- Recursive definitions
**Warning Message**: "CUE template is complex (nesting depth: 8). Consider simplifying."
### 11. Transform Size Limits
**Rule**: MAY limit size of transform values.
**Rationale**: Prevent accidentally adding huge specs.
```cue
# WARNING - large transform
transforms: spec: {
type: "merge"
value: {
// 10MB of spec changes
}
}
```
**Warning Message**: "Transform value is large (<size>KB). Consider if this is intentional."
### 12. Conditional Complexity
**Rule**: MAY warn on complex `enabled` conditions.
**Rationale**: Simple conditions are easier to understand and debug.
```cue
# WARNING - complex condition
import "strings"
import "list"
enabled: strings.HasPrefix(context.application.metadata.namespace, "tenant-") &&
len(context.application.spec.components) > 5 &&
list.Contains(context.application.metadata.labels, "requires-policy")
# SIMPLE
enabled: strings.HasPrefix(context.application.metadata.namespace, "tenant-")
```
**Warning Message**: "Enabled condition is complex. Consider simplifying for easier debugging."
### 13. Breaking Change Detection
**Rule**: MAY warn on breaking changes to existing policies.
**Breaking changes**:
- Changing priority significantly (>100 difference)
- Changing from non-global to global
- Changing scope
- Adding required parameters (for non-global)
**Warning Message**: "Priority changed from 100 to 900 (delta: 800). This may affect execution order."
### 14. Conflict Detection
**Rule**: MAY detect potential conflicts between policies.
**Examples**:
- Two policies setting same label to different values
- Priority collision (same priority, similar names)
**Warning Message**: "Policy 'my-policy-a' and 'my-policy-b' both set label 'team'. Last one wins."
### 15. Performance Impact
**Rule**: MAY estimate performance impact.
**Factors**:
- Number of API calls in CUE (if detectable)
- Complexity of transforms
- Size of merge operations
**Warning Message**: "Policy may have performance impact. Consider caching expensive operations."
## Implementation Strategy
### Phase 1: Critical Validations (Blocking)
Implement these as **ValidatingWebhook** that blocks invalid policies:
1. ✅ No required parameters for global policies
2. ✅ Priority field required for global policies
3. ✅ CUE schema validation
4. ✅ Scope must be Application
### Phase 2: Important Validations (Warnings)
Implement as warnings in webhook response:
5. ⚠️ Documentation annotations
6. ⚠️ Naming conventions
7. ⚠️ Dangerous imports
8. ⚠️ Critical label protection (covered by #3)
9. ⚠️ Priority ranges
### Phase 3: Advanced Validations (Future)
Implement in CLI tool or separate linter:
10. Complexity analysis
11. Transform size limits
12. Conditional complexity
13. Breaking change detection
14. Conflict detection
15. Performance impact estimation
## Validation Webhook Design
```go
// ValidatePolicyDefinition validates a PolicyDefinition
func ValidatePolicyDefinition(old, new *v1beta1.PolicyDefinition) error {
var allErrors []error
var warnings []string
// Critical validations (blocking)
if new.Spec.Global {
// Rule 1: No required parameters
if err := validateNoRequiredParameters(new); err != nil {
allErrors = append(allErrors, err)
}
// Rule 2: Priority must be set
if !hasPriority(new) {
allErrors = append(allErrors, errors.New("global policies must have explicit priority"))
}
// Rule 4: Scope must be Application
if new.Spec.Scope != v1beta1.ApplicationScope {
allErrors = append(allErrors, errors.New("global policies must have scope='Application'"))
}
}
// Rule 3: CUE schema validation
if err := validateCUESchema(new); err != nil {
allErrors = append(allErrors, err)
}
// Important validations (warnings)
if new.Spec.Global {
// Rule 5: Documentation
if !hasDocumentation(new) {
warnings = append(warnings, "missing documentation annotations")
}
// Rule 7: Dangerous imports
if dangerousImports := checkDangerousImports(new); len(dangerousImports) > 0 {
warnings = append(warnings, fmt.Sprintf("dangerous imports: %v", dangerousImports))
}
// Rule 9: Priority ranges
if !inRecommendedRange(new.Spec.Priority) {
warnings = append(warnings, "priority outside recommended ranges")
}
}
if len(allErrors) > 0 {
return utilerrors.NewAggregate(allErrors)
}
// Return warnings as annotations or in response
if len(warnings) > 0 {
logWarnings(warnings)
}
return nil
}
```
## Testing Strategy
Each validation rule needs:
1. Positive test (valid policy passes)
2. Negative test (invalid policy fails)
3. Edge case tests
Example:
```go
It("rejects global policy with required parameters", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {
envName: string // Required!
}
`,
},
},
},
}
err := ValidatePolicyDefinition(nil, policy)
Expect(err).ShouldNot(BeNil())
Expect(err.Error()).Should(ContainSubstring("required parameters"))
})
```
## User Experience
### CLI Command
```bash
# Validate before applying
kubectl vela policy validate -f my-policy.yaml
# Output:
❌ Error: Global policy 'my-policy' has required parameters: [envName]
⚠️ Warning: Missing documentation annotations
⚠️ Warning: Priority 50 is outside recommended ranges
# With --strict flag
kubectl vela policy validate -f my-policy.yaml --strict
# Treats warnings as errors
```
### Admission Webhook Feedback
```bash
kubectl apply -f bad-policy.yaml
Error from server (Forbidden): error when creating "bad-policy.yaml": admission webhook "validate.policydefinition.core.oam.dev" denied the request:
- Global policy 'security-hardening' has required parameters: [envName]
- Global policies must have explicit priority field
```
## Open Questions
1. **Should priority be required or just recommended?**
- Required: More strict, enforces best practices
- Recommended: More flexible, defaults to 0
2. **Should we allow dangerous imports at all?**
- Block completely: Very strict, may limit legitimate use cases
- Warn only: More flexible, but risk of abuse
- Allowlist: Platform admin can configure allowed imports
3. **How strict should parameter validation be?**
- Block any parameters: Strictest
- Block required parameters: Balanced (current proposal)
- Warn only: Most flexible
4. **Should we validate update operations differently?**
- More lenient on updates to avoid breaking existing policies
- Same strict validation to maintain quality
5. **Should validation be opt-in or opt-out?**
- Opt-in: More flexible, may miss validation
- Opt-out: Stricter, but can disable for advanced users

330
docs/spec-diff-proposal.md Normal file
View File

@@ -0,0 +1,330 @@
# Proposal: Store Spec Diffs for Policy Transforms
## Problem
Currently when a policy modifies the Application spec, we only set `specModified: true`. This isn't helpful for debugging:
```json
{
"name": "inject-sidecar",
"specModified": true // ❌ What did it change?
}
```
Users can't tell:
- What exactly was added/removed/modified
- How multiple policies interact
- If a policy is working correctly
## Proposed Solution
Store a structured diff when `specModified=true`:
```json
{
"name": "inject-sidecar",
"specModified": true,
"specDiff": "eyJhZGRlZCI6e...}", // Base64 JSON patch
"specDiffSummary": "Added 1 component, modified 2 properties"
}
```
## Implementation Options
### Option 1: JSON Patch (RFC 6902)
**Format**: Industry standard, compact
```json
[
{"op": "add", "path": "/components/1", "value": {...}},
{"op": "replace", "path": "/components/0/properties/replicas", "value": 3}
]
```
**Pros**:
- Standard format (kubectl diff uses this)
- Reversible (can undo changes)
- Compact representation
- Libraries available
**Cons**:
- Harder to read for humans
- Requires JSON marshal/unmarshal
**Cost**: ~10-15ms overhead
### Option 2: Structured Summary
**Format**: Human-readable summary
```json
{
"componentsAdded": 1,
"componentsModified": 0,
"propertiesChanged": ["components[0].properties.replicas"],
"beforeHash": "abc123",
"afterHash": "def456"
}
```
**Pros**:
- Very readable
- Lightweight (no full diff)
- Fast to compute (~2-3ms)
**Cons**:
- Can't see actual values
- Not reversible
- Less useful for complex changes
**Cost**: ~2-5ms overhead
### Option 3: Hybrid Approach (RECOMMENDED)
**Store both summary + diff (only if diff is small)**
```go
const MaxSpecDiffSize = 10 * 1024 // 10KB
type SpecChange struct {
Summary SpecChangeSummary `json:"summary"`
FullDiff string `json:"fullDiff,omitempty"` // Base64 JSON patch (if <10KB)
DiffTooLarge bool `json:"diffTooLarge,omitempty"`
}
type SpecChangeSummary struct {
ComponentsAdded int `json:"componentsAdded,omitempty"`
ComponentsModified int `json:"componentsModified,omitempty"`
ComponentsRemoved int `json:"componentsRemoved,omitempty"`
FieldsChanged []string `json:"fieldsChanged,omitempty"`
BeforeHash string `json:"beforeHash"`
AfterHash string `json:"afterHash"`
}
```
**Example**:
```json
{
"name": "inject-sidecar",
"specModified": true,
"specChange": {
"summary": {
"componentsAdded": 0,
"componentsModified": 2,
"fieldsChanged": [
"components[0].properties.env[0]",
"components[1].properties.env[0]"
],
"beforeHash": "abc123",
"afterHash": "def456"
},
"fullDiff": "W3sib3AiOiJhZGQiLCJ...", // Only if <10KB
"diffTooLarge": false
}
}
```
**Pros**:
- Human-readable summary for quick diagnosis
- Full diff available for detailed debugging (when needed)
- Avoids etcd bloat for large changes
- Fast path (summary only) is cheap (~5ms)
**Cons**:
- More complex implementation
- Two code paths to maintain
**Cost**: ~5-15ms depending on size
## Scope: Only Diff Spec Changes
**Do NOT diff labels/annotations** - we already track these explicitly:
```json
"addedLabels": {"team": "platform"}, // ✅ Already clear
"addedAnnotations": {"version": "v1.0"} // ✅ Already clear
```
**Only diff spec transforms** - this is where we need help:
```json
"specModified": true, // ❌ Not helpful
"specChange": {...} // ✅ Shows what changed
```
## Computational Impact
### Per-Application Overhead:
- **Option 1 (JSON Patch)**: ~10-15ms
- **Option 2 (Summary Only)**: ~2-5ms
- **Option 3 (Hybrid)**: ~5-15ms (average ~8ms)
### Context:
- Typical reconciliation: 100-500ms
- Policy rendering (uncached): 30-100ms
- **8ms overhead = ~2-5% of total time** ✅ Acceptable
### When to Skip:
- If no spec transform: 0ms overhead
- If diff >10KB: compute summary only (~2ms)
- Labels/annotations only: 0ms overhead
## Storage Impact
### etcd Size:
- JSON Patch for typical sidecar injection: ~2-5KB
- Base64 encoding: +33% → ~3-7KB
- 5 policies with spec changes: ~15-35KB
- **Total Application size increase: <5%** ✅ Acceptable
### etcd Limits:
- Max object size: 1.5MB
- Typical Application: 20-100KB
- With diffs: 25-135KB
- **Still well under limit** ✅
## Implementation Plan
### Phase 1: Summary Only (Quick Win)
```go
type PolicyChanges struct {
AddedLabels map[string]string
AddedAnnotations map[string]string
AdditionalContext map[string]interface{}
SpecModified bool
SpecChangeSummary *SpecChangeSummary // NEW
}
```
**Benefits**:
- Low overhead (~2ms)
- Helps with debugging
- No storage concerns
### Phase 2: Add Full Diff (If Needed)
```go
type PolicyChanges struct {
// ... existing fields
SpecChange *SpecChange // Replaces SpecModified + Summary
}
```
**Benefits**:
- Complete visibility
- Can show diffs in UI
- Enables "undo" functionality
## Alternative: External Diff Storage
If storage is a concern, store diffs externally:
```go
type AppliedGlobalPolicy struct {
// ... existing fields
SpecDiffRef string // "configmap/my-app-policy-diffs/inject-sidecar"
}
```
Create a ConfigMap per Application with all policy diffs:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-policy-diffs
data:
inject-sidecar: |
[{"op": "add", ...}]
resource-limits: |
[{"op": "replace", ...}]
```
**Pros**:
- Doesn't bloat Application status
- Can be cleaned up separately
- No etcd concerns
**Cons**:
- Extra API call to view diffs
- More objects to manage
- Lifecycle management complexity
## Decision Criteria
### When to Implement Full Diffs:
**YES** if:
- Users frequently ask "what did this policy change?"
- Debugging complex spec transforms is common
- UI/CLI tools will display diffs
- "Undo policy effects" is a requirement
**NO** if:
- Current tracking (labels/annotations/specModified) is sufficient
- Performance is critical (every ms counts)
- Storage is limited
### Recommendation:
**Start with Phase 1 (Summary Only)**:
- Low cost (~2ms, ~1KB)
- Immediate value for debugging
- Easy to implement
- Can upgrade to full diffs later if needed
**Add Phase 2 (Full Diffs) if**:
- Users request it after using summaries
- UI/CLI tools are built to display diffs
- "Policy dry-run" feature is added
## Open Questions
1. **Should diffs be human-readable or machine-parseable?**
- JSON Patch (machine) vs. kubectl-style diff (human)
2. **Should we store diffs for all policies or just spec changes?**
- Current proposal: Only spec changes
3. **Should diffs be compressed?**
- Could use gzip before base64 (saves ~60% space)
4. **Retention policy?**
- Clear diffs on successful reconciliation?
- Keep last N diffs?
5. **Should we support "reverting" policy changes?**
- Would require storing inverse patches
## Example Usage
### CLI Tool
```bash
# Show what a policy changed
kubectl vela policy diff my-app inject-sidecar
# Output:
Spec changes by policy 'inject-sidecar':
+ Added component 'monitoring-sidecar'
~ Modified components[0].properties.env
+ Added env var: SIDECAR_ENABLED=true
# Show full JSON patch
kubectl vela policy diff my-app inject-sidecar --format=json-patch
```
### UI Dashboard
```
Application: my-app
Applied Policies:
✅ inject-sidecar (vela-system)
Spec Changes:
├─ Added 1 component
├─ Modified 2 properties
└─ [View Full Diff]
```
## Conclusion
**Summary diffs (~2ms, ~1KB) provide 80% of the value with 20% of the cost.**
Recommend:
1. ✅ Implement Phase 1 (Summary) now
2. 🤔 Evaluate Phase 2 (Full Diff) based on usage
3. 📊 Add metrics to track diff size distribution
4. 🔍 Monitor performance impact in production

View File

@@ -0,0 +1,399 @@
# Policy CLI Commands - Design Specification
## Overview
Two new CLI commands for debugging and testing global policies:
1. `vela policy view` - View applied policies and their effects
2. `vela policy dry-run` - Preview policy effects before applying
## Command 1: `vela policy view`
### Purpose
Interactive viewer for policy changes applied to an Application.
### Usage
```bash
vela policy view <app-name> [flags]
```
### Flags
- `-n, --namespace <namespace>` - Application namespace (default: current context)
- `--output <format>` - Output format: `table` (default), `json`, `yaml`
### Behavior
#### 1. Display Summary Table
```
Applied Global Policies (3):
┌──────────────────────┬─────────────┬──────────┬──────────────┬─────────────┐
│ Policy │ Namespace │ Sequence │ Priority │ Spec Changed│
├──────────────────────┼─────────────┼──────────┼──────────────┼─────────────┤
│ inject-sidecar │ vela-system │ 1 │ 100 │ Yes │
│ resource-limits │ vela-system │ 2 │ 50 │ Yes │
│ platform-labels │ vela-system │ 3 │ 0 │ No │
└──────────────────────┴─────────────┴──────────┴──────────────┴─────────────┘
Labels Added:
platform.io/managed-by: kubevela (platform-labels)
sidecar.io/injected: true (inject-sidecar)
Annotations Added:
config.io/resource-profile: standard (resource-limits)
Spec Changes: 2 policies modified the spec
View detailed diffs: kubectl get configmap my-app-policy-diffs
```
#### 2. Interactive Mode (if terminal supports)
```
Select a policy to view changes: [Use arrows to move, type to filter, Enter to view]
> inject-sidecar (added monitoring-sidecar component)
resource-limits (modified resource constraints)
platform-labels (metadata only)
```
When selected, show:
```
Policy: inject-sidecar (vela-system)
Sequence: 1, Priority: 100
Changes Made:
✓ Spec Modified: Yes
✓ Added Labels: sidecar.io/injected=true
Spec Diff (JSON Merge Patch):
{
"components": [
null,
{
"name": "monitoring-sidecar",
"type": "webservice",
"properties": {
"image": "monitoring:latest"
}
}
]
}
Interpretation:
• Added component at index 1: monitoring-sidecar
```
#### 3. Non-interactive Mode (CI/scripting)
```bash
# JSON output
vela policy view my-app --output json
# YAML output
vela policy view my-app --output yaml
```
### Implementation Notes
- Read from `app.Status.AppliedGlobalPolicies`
- Read diffs from ConfigMap if `PolicyDiffsConfigMap` is set
- Use `github.com/AlecAivazis/survey/v2` for interactive selection (like `vela debug`)
- Use `github.com/FogDong/uitable` for table display (like other vela commands)
- Parse JSON Merge Patch to provide human-readable interpretation
---
## Command 2: `vela policy dry-run`
### Purpose
Preview policy effects before applying to an Application.
### Usage
```bash
vela policy dry-run <app-name> [flags]
```
### Flags
- `-n, --namespace <namespace>` - Application namespace (default: current context)
- `--policies <p1,p2,pN>` - Specific policies to test (comma-separated)
- `--include-global-policies` - Include existing global policies
- `--include-app-policies` - Include policies from Application spec
- `--output <format>` - Output format: `table` (default), `summary`, `json`, `yaml`, `diff`
- `table`: Full output with policy details and final Application spec
- `summary`: Show only labels, annotations, and context (no spec)
- `json`: Machine-readable JSON output
- `yaml`: Machine-readable YAML output
- `diff`: Unified diff format showing changes
### Behavior Modes
#### Mode 1: Isolated Testing (default when --policies specified)
Test ONLY specified policies, ignore all existing policies.
```bash
vela policy dry-run my-app --policies inject-monitoring
```
**Use case**: Testing a new policy in isolation before deploying it.
**Execution order**:
1. Specified policies (in CLI order)
#### Mode 2: Additive Testing (--include-global-policies)
Test specified policies WITH existing global policies.
```bash
vela policy dry-run my-app --policies new-security-policy --include-global-policies
```
**Use case**: Test how a new policy interacts with existing globals, detect conflicts.
**Execution order**:
1. Existing global policies (sorted by priority + name)
2. Specified policies (in CLI order)
#### Mode 3: Full Simulation (no --policies flag)
Simulate complete policy chain that would apply to the Application.
```bash
vela policy dry-run my-app
```
**Use case**: Debug unexpected behavior, understand current state.
**Execution order**:
1. Existing global policies (sorted by priority + name)
2. App spec policies (in spec order)
#### Mode 4: Full + Additional Policies
Full simulation plus extra test policies.
```bash
vela policy dry-run my-app --policies test-policy --include-global-policies --include-app-policies
```
**Execution order**:
1. Existing global policies (sorted by priority + name)
2. Specified policies (in CLI order)
3. App spec policies (in spec order)
### Output Format
#### Default Output
```
Dry-run simulation for Application: my-app (namespace: default)
Execution Plan:
1. inject-sidecar (vela-system, priority: 100) [global]
2. resource-limits (vela-system, priority: 50) [global]
3. security-hardening (specified) [test]
Applying policies...
✓ Policy 1/3: inject-sidecar
Sequence: 1
Enabled: true
Changes:
• Spec Modified: Yes
• Added Labels: sidecar.io/injected=true
• Added component: monitoring-sidecar
✓ Policy 2/3: resource-limits
Sequence: 2
Enabled: true
Changes:
• Spec Modified: Yes
• Modified: components[0].properties.resources.limits.cpu → 500m
• Modified: components[0].properties.resources.limits.memory → 512Mi
✓ Policy 3/3: security-hardening
Sequence: 3
Enabled: true
Changes:
• Spec Modified: Yes
• Added: components[0].properties.securityContext.runAsNonRoot=true
Summary:
Total Policies: 3
Applied: 3
Skipped: 0
Spec Modifications: 3
Labels Added: 1
Annotations Added: 0
⚠ Warnings:
• No conflicts detected
Final Application State:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Labels (1 total):
sidecar.io/injected: "true" (inject-sidecar)
Annotations:
(none)
Additional Context:
(none)
Application Spec:
---
<YAML of final spec>
---
```
#### Summary Output (--output summary)
Show only labels, annotations, and context (no spec):
```bash
vela policy dry-run my-app --output summary
```
Displays:
- Summary counts (policies applied/skipped, changes)
- Final labels table with attribution
- Final annotations table with attribution
- Final additional context with attribution
**Use case**: When you only care about metadata changes (labels/annotations) and don't need to see spec modifications. Useful for quickly checking if policies are adding the correct platform labels.
#### Diff Output (--output diff)
```bash
vela policy dry-run my-app --output diff
```
Shows unified diff of original spec vs final spec:
```diff
--- Original Spec
+++ After Policies
@@ -1,5 +1,10 @@
components:
- name: backend
type: webservice
properties:
image: myapp:v1
+ resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ - name: monitoring-sidecar
+ type: webservice
```
#### JSON/YAML Output
Machine-readable output for scripting:
```json
{
"application": "my-app",
"namespace": "default",
"executionPlan": [
{
"sequence": 1,
"policyName": "inject-sidecar",
"policyNamespace": "vela-system",
"priority": 100,
"source": "global"
}
],
"results": [
{
"sequence": 1,
"policyName": "inject-sidecar",
"enabled": true,
"applied": true,
"specModified": true,
"addedLabels": {"sidecar.io/injected": "true"},
"addedAnnotations": {},
"additionalContext": null,
"diff": { ... }
}
],
"finalState": {
"labels": {
"sidecar.io/injected": "true"
},
"annotations": {},
"additionalContext": {},
"spec": { ... }
}
}
```
### Implementation Notes
#### Core Logic
1. **Load Application**: Get Application from cluster (or from file with `-f`)
2. **Discover Policies**: Based on flags, build execution plan
3. **Create Temporary AppHandler**: Use existing `ApplyApplicationScopeTransforms` logic
4. **Apply Policies in Dry-Run Mode**:
- Execute transforms on in-memory copy
- Track diffs for each policy
- Don't persist to cluster
5. **Display Results**: Format based on `--output` flag
#### Code Reuse
- Reuse `ApplyApplicationScopeTransforms` from `policy_transforms.go`
- Reuse `discoverGlobalPolicies` for global policy discovery
- Reuse diff computation logic (`deepCopyAppSpec`, `computeJSONPatch`)
- Similar pattern to existing `vela dry-run` command
#### Key Differences from Real Reconciliation
- No persistence to cluster
- No status updates
- No events emitted
- No ConfigMap creation
- Returns results to CLI instead
#### Error Handling
- Policy not found: Clear error message
- Invalid policy template: Show CUE compilation error
- Policy conflicts: Detect and warn
- Application not found: Offer to use `-f` flag for file input
---
## Implementation Plan
### Phase 1: `vela policy view` (simpler, no dry-run logic)
**Files to create/modify**:
- `/workspaces/development/kubevela/references/cli/policy.go` (new)
- `/workspaces/development/kubevela/references/cli/cli.go` (register command)
**Dependencies**:
- Application status reading
- ConfigMap reading
- Table formatting (existing utilities)
- Interactive selection (survey/v2)
**Estimated complexity**: Low (mostly UI/formatting)
### Phase 2: `vela policy dry-run` (complex, requires simulation logic)
**Files to create/modify**:
- `/workspaces/development/kubevela/references/cli/policy.go` (extend)
- Factor out reusable logic from `policy_transforms.go` if needed
**Dependencies**:
- Application loading
- Policy discovery
- Transform application (reuse controller logic)
- Diff computation
- Output formatting
**Estimated complexity**: Medium-High (simulation logic, multiple modes)
### Phase 3: Integration & Testing
- Add tests in `policy_test.go`
- Update CLI documentation
- Add examples to README
---
## Open Questions
1. **Application source**: Should dry-run support `-f <file>` for Applications not yet in cluster?
2. **Policy source**: Should we support testing policies from files before deploying them?
3. **Conflict detection**: How aggressive should we be in detecting policy conflicts?
4. **Performance**: For large Applications with many policies, should we add progress indicators?
---
## Future Enhancements
1. **Watch Mode**: `vela policy view my-app --watch` - Live updates as policies change
2. **Compare Mode**: `vela policy diff app1 app2` - Compare policy effects across apps
3. **Export**: `vela policy view my-app --export > report.html` - Generate HTML report
4. **Validation**: `vela policy validate <policy-file>` - Validate policy syntax before applying

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,534 @@
# Global Policy Observability
One of the key challenges with runtime manipulation is understanding **what changed** and **which policy changed it**. This guide shows how to debug and trace policy effects.
## Viewing Applied Policies
### Basic Status Check
```bash
# View which global policies were applied
kubectl get app my-app -o jsonpath='{.status.appliedGlobalPolicies}' | jq
```
Example output:
```json
[
{
"name": "security-hardening",
"namespace": "vela-system",
"applied": true,
"addedLabels": {
"security.platform.io/scanned": "true",
"security.platform.io/minimum-tls": "1.2"
},
"addedAnnotations": {
"security.platform.io/scan-date": "2024-01-01",
"security.platform.io/scan-tool": "trivy"
},
"additionalContext": {
"securityPolicyVersion": "v2.1.0"
},
"specModified": false
},
{
"name": "platform-labels",
"namespace": "vela-system",
"applied": true,
"addedLabels": {
"platform.io/managed-by": "kubevela",
"platform.io/region": "us-west-2"
},
"specModified": false
},
{
"name": "tenant-config",
"namespace": "vela-system",
"applied": false,
"reason": "enabled=false"
}
]
```
## Understanding Status Fields
### Field Reference
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Policy name |
| `namespace` | string | Policy namespace (vela-system or app namespace) |
| `applied` | bool | Whether policy was applied |
| `reason` | string | Why policy was skipped (if `applied=false`) |
| `addedLabels` | map | **NEW**: Labels this policy added |
| `addedAnnotations` | map | **NEW**: Annotations this policy added |
| `additionalContext` | map | **NEW**: Context data this policy provided |
| `specModified` | bool | **NEW**: Whether policy modified Application spec |
## Debugging Scenarios
### "Where did this label come from?"
```bash
# Find which policy added a specific label
kubectl get app my-app -o json | \
jq '.status.appliedGlobalPolicies[] | select(.addedLabels["team"] != null) | {policy: .name, namespace: .namespace, value: .addedLabels["team"]}'
```
Output:
```json
{
"policy": "platform-labels",
"namespace": "vela-system",
"value": "platform-team"
}
```
### "Which policies modified my spec?"
```bash
# List all policies that modified the spec
kubectl get app my-app -o json | \
jq '.status.appliedGlobalPolicies[] | select(.specModified == true) | {policy: .name, namespace: .namespace}'
```
Output:
```json
{
"policy": "inject-sidecar",
"namespace": "vela-system"
}
{
"policy": "resource-limits",
"namespace": "production"
}
```
### "What context data is available?"
```bash
# View all additionalContext from policies
kubectl get app my-app -o json | \
jq '.status.appliedGlobalPolicies[] | select(.additionalContext != null) | {policy: .name, context: .additionalContext}'
```
Output:
```json
{
"policy": "security-hardening",
"context": {
"securityPolicyVersion": "v2.1.0",
"complianceLevel": "pci-dss"
}
}
```
### "Why didn't a policy apply?"
```bash
# Find skipped policies and reasons
kubectl get app my-app -o json | \
jq '.status.appliedGlobalPolicies[] | select(.applied == false) | {policy: .name, reason: .reason}'
```
Output:
```json
{
"policy": "tenant-config",
"reason": "enabled=false"
}
{
"policy": "dev-environment",
"reason": "enabled=false"
}
```
## Complete Audit Trail
Create a shell function for comprehensive policy audit:
```bash
# Add to ~/.bashrc or ~/.zshrc
vela-policy-audit() {
local app=$1
local namespace=${2:-default}
echo "=== Policy Audit for $app (namespace: $namespace) ==="
echo
echo "📋 Applied Policies:"
kubectl get app $app -n $namespace -o json | \
jq -r '.status.appliedGlobalPolicies[] | select(.applied == true) | " ✅ \(.name) (from \(.namespace))"'
echo
echo "⏭️ Skipped Policies:"
kubectl get app $app -n $namespace -o json | \
jq -r '.status.appliedGlobalPolicies[] | select(.applied == false) | " ⏭️ \(.name): \(.reason)"'
echo
echo "🏷️ Labels Added by Policies:"
kubectl get app $app -n $namespace -o json | \
jq -r '.status.appliedGlobalPolicies[] | select(.addedLabels != null) | " Policy: \(.name)\n Labels: \(.addedLabels | to_entries | map(" \(.key)=\(.value)") | join("\n"))"'
echo
echo "📝 Annotations Added by Policies:"
kubectl get app $app -n $namespace -o json | \
jq -r '.status.appliedGlobalPolicies[] | select(.addedAnnotations != null) | " Policy: \(.name)\n Annotations: \(.addedAnnotations | to_entries | map(" \(.key)=\(.value)") | join("\n"))"'
echo
echo "🔧 Spec Modifications:"
kubectl get app $app -n $namespace -o json | \
jq -r '.status.appliedGlobalPolicies[] | select(.specModified == true) | " ⚠️ \(.name) modified Application spec"'
echo
echo "📦 Additional Context:"
kubectl get app $app -n $namespace -o json | \
jq -r '.status.appliedGlobalPolicies[] | select(.additionalContext != null) | " Policy: \(.name)\n Context: \(.additionalContext | to_entries | map(" \(.key)=\(.value)") | join("\n"))"'
}
# Usage:
# vela-policy-audit my-app
# vela-policy-audit my-app production
```
Example output:
```
=== Policy Audit for my-app (namespace: default) ===
📋 Applied Policies:
✅ security-hardening (from vela-system)
✅ platform-labels (from vela-system)
⏭️ Skipped Policies:
⏭️ tenant-config: enabled=false
🏷️ Labels Added by Policies:
Policy: security-hardening
Labels:
security.platform.io/scanned=true
security.platform.io/minimum-tls=1.2
Policy: platform-labels
Labels:
platform.io/managed-by=kubevela
platform.io/region=us-west-2
📝 Annotations Added by Policies:
Policy: security-hardening
Annotations:
security.platform.io/scan-date=2024-01-01
🔧 Spec Modifications:
(none)
📦 Additional Context:
Policy: security-hardening
Context:
securityPolicyVersion=v2.1.0
```
## Kubernetes Events
In addition to status fields, policies also emit Kubernetes Events:
```bash
# View policy-related events
kubectl get events --field-selector involvedObject.name=my-app
# Filter for global policy events
kubectl get events --field-selector involvedObject.name=my-app | grep -i "globalpolicy"
```
Example events:
```
LAST SEEN TYPE REASON MESSAGE
2m Normal GlobalPolicyApplied Applied global policy security-hardening from namespace vela-system
2m Normal GlobalPolicyApplied Applied global policy platform-labels from namespace vela-system
2m Normal GlobalPolicySkipped Skipped global policy tenant-config: enabled=false
```
## Troubleshooting Workflows
### 1. Unexpected Label/Annotation
**Problem**: "My Application has a label I didn't add"
**Solution**:
```bash
# Find the culprit
kubectl get app my-app -o json | \
jq '.status.appliedGlobalPolicies[] | select(.addedLabels["mysterious-label"] != null) | {policy: .name, namespace: .namespace}'
# View the policy definition
kubectl get policydefinition <policy-name> -n <namespace> -o yaml
```
### 2. Policy Not Applying
**Problem**: "My global policy isn't being applied"
**Checklist**:
```bash
# 1. Is policy marked as global?
kubectl get policydefinition my-policy -n vela-system -o jsonpath='{.spec.global}'
# 2. Is Application opting out?
kubectl get app my-app -o jsonpath='{.metadata.annotations.policy\.oam\.dev/skip-global}'
# 3. Check applied policies status
kubectl get app my-app -o json | jq '.status.appliedGlobalPolicies[] | select(.name == "my-policy")'
# 4. Check feature gate
kubectl get deployment vela-core -n vela-system -o yaml | grep feature-gates
```
### 3. Conflicting Policies
**Problem**: "Two policies are setting the same label to different values"
**Solution**:
```bash
# Find all policies setting a specific label
kubectl get app my-app -o json | \
jq '.status.appliedGlobalPolicies[] | select(.addedLabels["team"] != null) | {policy: .name, priority: .priority, value: .addedLabels["team"]}'
# The LAST policy in the list wins (highest priority + alphabetical order)
```
### 4. Context Data Not Available in Workflow
**Problem**: "Workflow can't access policy's additionalContext"
**Solution**:
```bash
# Check if policy provided the context
kubectl get app my-app -o json | \
jq '.status.appliedGlobalPolicies[] | select(.additionalContext != null)'
# Context is stored in Go context and passed to workflow
# Not all workflow steps may support context.custom access
# Check workflow step implementation
```
## Best Practices
### 1. Use Descriptive Names
**Good**:
```yaml
addedLabels:
security.platform.io/scanned: "true"
platform.io/managed-by: "kubevela"
```
**Bad**:
```yaml
addedLabels:
x: "true"
mgr: "vela"
```
### 2. Document Your Policies
Add annotations to PolicyDefinitions:
```yaml
metadata:
annotations:
policy.oam.dev/description: "Adds required security labels for compliance"
policy.oam.dev/owner: "security-team@company.com"
policy.oam.dev/adds-labels: "security.platform.io/*"
```
### 3. Monitor Policy Impact
Set up monitoring for:
- Number of Applications affected by each policy
- Policies that frequently fail (enabled=false)
- Policies that modify specs (higher risk)
### 4. Use Namespaced Policies for Testing
Before deploying to vela-system:
```bash
# Test in specific namespace first
kubectl apply -f my-policy.yaml -n test-namespace
# Check Applications in that namespace
kubectl get app -n test-namespace -o json | jq '.items[].status.appliedGlobalPolicies'
# If working well, promote to vela-system
kubectl apply -f my-policy.yaml -n vela-system
```
## Integration with GitOps
### ArgoCD/Flux Drift Detection
The status fields show what policies changed, but ArgoCD/Flux will see drift since changes aren't in Git.
**Option 1: Ignore policy-added labels/annotations**
```yaml
# ArgoCD Application
spec:
ignoreDifferences:
- group: core.oam.dev
kind: Application
jsonPointers:
- /metadata/labels/security.platform.io
- /metadata/annotations/policy.oam.dev
```
**Option 2: Document in Git (recommended)**
```yaml
# my-app.yaml (in Git)
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: my-app
labels:
# Added by policy: security-hardening (vela-system)
security.platform.io/scanned: "true"
```
## Viewing Spec Diffs (✅ Implemented)
When global policies modify the Application spec, KubeVela stores detailed JSON Merge Patch diffs in a ConfigMap for debugging and auditing.
### ConfigMap Structure
Policy diffs are stored in a ConfigMap named `{app-name}-policy-diffs` with:
- **Labels**: Standard KubeVela labels (`app.oam.dev/name`, `app.oam.dev/namespace`, `app.oam.dev/uid`)
- **Keys**: Sequence-prefixed format `001-policy-name`, `002-policy-name` to preserve execution order
- **Values**: JSON Merge Patch (RFC 7386) showing what changed
### Basic Diff Viewing
```bash
# Check if policy diffs exist
kubectl get app my-app -o jsonpath='{.status.policyDiffsConfigMap}'
# Output: my-app-policy-diffs
# List all policy diffs in execution order
kubectl get configmap my-app-policy-diffs -o jsonpath='{.data}' | jq 'keys'
# Output: ["001-inject-sidecar", "002-resource-limits"]
# View specific policy diff
kubectl get configmap my-app-policy-diffs -o jsonpath='{.data.001-inject-sidecar}' | jq
```
### Example Diff Output
```json
{
"components": [
null,
{
"name": "monitoring-sidecar",
"type": "webservice",
"properties": {
"image": "monitoring:latest",
"cpu": "100m"
}
}
]
}
```
This shows the `inject-sidecar` policy added a new component at index 1.
### Finding All Applications with Policy Diffs
```bash
# List all policy-diffs ConfigMaps
kubectl get configmaps -l "app.oam.dev/policy-diffs=true"
# List ConfigMaps for a specific app
kubectl get configmaps -l "app.oam.dev/name=my-app"
```
### Diff Interpretation
JSON Merge Patch format:
- **New fields**: Added to object (e.g., `"components": [null, {...}]` adds component at index 1)
- **Modified fields**: Replaced with new value (e.g., `"replicas": 3` changes replicas)
- **`null` values**: Indicate no change at that position
### Combining Status + Diffs
Get complete picture of policy effects:
```bash
# Get metadata about policies
kubectl get app my-app -o json | jq '.status.appliedGlobalPolicies[]'
# Get actual spec changes
kubectl get configmap my-app-policy-diffs -o json | jq '.data'
```
## Future Enhancements
Planned improvements to observability:
### 1. CLI Tools (High Priority)
**`vela policy view <app>`**
- Interactive viewer for policy changes
- Shows before/after comparison
- Highlights which policies made which changes
- Similar UX to `vela debug`
Example usage:
```bash
$ vela policy view my-app
Applied Global Policies (3):
┌──────────────────────┬─────────────┬──────────┬──────────────┐
│ Policy │ Namespace │ Sequence │ Spec Changed │
├──────────────────────┼─────────────┼──────────┼──────────────┤
│ inject-sidecar │ vela-system │ 1 │ Yes │
│ resource-limits │ vela-system │ 2 │ Yes │
│ platform-labels │ vela-system │ 3 │ No │
└──────────────────────┴─────────────┴──────────┴──────────────┘
Select a policy to view changes: [Use arrows to move, type to filter]
> inject-sidecar (added monitoring-sidecar component)
resource-limits (modified resource constraints)
```
**`vela policy dry-run <app> --policies <policy1> <policy2> <policyN>`**
- Preview policy effects before applying
- Test policy changes without creating Application
- Validate policy templates and detect conflicts
Example usage:
```bash
$ vela policy dry-run my-app --policies inject-sidecar resource-limits
Dry-run simulation:
✓ Policy: inject-sidecar (priority: 100)
- Added component: monitoring-sidecar
- Added label: sidecar.io/injected=true
✓ Policy: resource-limits (priority: 50)
- Modified: components[0].properties.resources.limits.cpu → 500m
- Modified: components[0].properties.resources.limits.memory → 512Mi
⚠ Warning: No conflicts detected
```
### 2. Additional Tools
1. **Metrics**: Prometheus metrics for policy application
2. **Web UI**: Visual policy impact dashboard in VelaUX
3. **Policy Audit**: `vela policy audit <app>` - complete audit trail
## Summary
With the enhanced status fields, you can now:
**Trace** every label/annotation to its source policy
**Debug** why policies were skipped
**Audit** what context data policies provided
**Monitor** which policies modified specs
**Understand** the complete policy chain for any Application
This makes runtime manipulation **observable and debuggable**, addressing one of the key concerns with implicit transformations.

View File

@@ -0,0 +1,348 @@
# Production Considerations for Global Policies
This document outlines important considerations when deploying global policies in production.
## Security Considerations
### 1. RBAC and Access Control
**Risk**: Global policies in `vela-system` apply to ALL Applications across ALL namespaces.
**Recommendations**:
```yaml
# Restrict who can create/modify global policies
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: global-policy-admin
rules:
- apiGroups: ["core.oam.dev"]
resources: ["policydefinitions"]
verbs: ["create", "update", "patch", "delete"]
# IMPORTANT: Scope this carefully
---
# Only platform admins should have this role
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: platform-admins-global-policies
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: global-policy-admin
subjects:
- kind: Group
name: platform-admins # Restrict to platform team
apiGroup: rbac.authorization.k8s.io
```
### 2. CUE Template Security
**Risk**: Malicious CUE templates could:
- Read sensitive data via API calls in CUE
- Cause performance issues with expensive operations
- Inject malicious values into Applications
**Recommendations**:
- **Code review all global policies** before deployment
- **Test policies in staging** with real workloads
- **Monitor CUE rendering time** - add timeouts if needed
- **Restrict CUE imports** - review what packages policies use
### 3. Namespace Isolation
**Risk**: Namespace policies could be used for privilege escalation.
**Recommendations**:
- **Audit namespace policies** - platform team should review
- **Use ResourceQuotas** to limit policy impact
- **Monitor policy changes** with admission webhooks
## Performance Considerations
### 1. Cache Hit Rates
**Monitor cache effectiveness**:
```bash
# Check cache size periodically
kubectl get app -A -o json | jq '.items | length'
# If cache hit rate is low, consider:
# 1. Increasing TTL (currently 1 minute)
# 2. Adding cache warming on policy changes
# 3. Profiling CUE rendering performance
```
### 2. CUE Rendering Performance
**Risk**: Complex CUE templates with API calls can slow reconciliation.
**Recommendations**:
- **Profile policy rendering** in staging
- **Avoid API calls in CUE** if possible
- **Use caching** for expensive computations
- **Set rendering timeouts** (future enhancement)
### 3. Large Numbers of Global Policies
**Risk**: Too many global policies slow down reconciliation.
**Recommendations**:
- **Limit global policies** - aim for <10 per namespace
- **Consolidate policies** where possible
- **Use priority** to short-circuit expensive policies
## Observability
### 1. Monitoring Policy Application
**Check which policies were applied**:
```bash
# View applied global policies
kubectl get app my-app -o jsonpath='{.status.appliedGlobalPolicies}' | jq
# View Kubernetes Events
kubectl get events --field-selector involvedObject.name=my-app
# Check for skipped policies
kubectl get app my-app -o json | jq '.status.appliedGlobalPolicies[] | select(.applied == false)'
```
### 2. Metrics to Add (Future)
```go
// Recommended metrics to add:
policyRenderDuration := prometheus.NewHistogram(...)
policyApplicationErrors := prometheus.NewCounter(...)
globalPolicyCacheHitRate := prometheus.NewGauge(...)
globalPolicyCacheSize := prometheus.NewGauge(...)
```
### 3. Debugging
**Enable verbose logging**:
```bash
# Set log level for application controller
--zap-log-level=2
# Watch for policy-related logs
kubectl logs -n vela-system deployment/vela-core | grep -i "policy\|transform"
```
## Error Handling
### 1. Current Behavior
- **Discovery failures**: Logged, reconciliation continues
- **Rendering failures**: Logged, policy skipped, reconciliation continues
- **Transform failures**: **Reconciliation FAILS** (by design)
### 2. Best Practices
**Test policies before deployment**:
```bash
# 1. Create test namespace
kubectl create ns policy-test
# 2. Deploy policy to test namespace
kubectl apply -f my-global-policy.yaml
# 3. Test with sample Application
kubectl apply -f test-app.yaml
# 4. Verify transforms applied correctly
kubectl get app test-app -o yaml
# 5. If working, promote to vela-system
kubectl apply -f my-global-policy.yaml -n vela-system
```
### 3. Rollback Strategy
If a global policy causes issues:
```bash
# Option 1: Delete the policy (cache invalidates automatically)
kubectl delete policydefinition bad-policy -n vela-system
# Option 2: Namespace override (quick fix for specific namespace)
# Create policy with same name in affected namespace
kubectl apply -f override-policy.yaml -n affected-namespace
# Option 3: Disable for specific apps
kubectl annotate app my-app policy.oam.dev/skip-global=true
```
## Operational Best Practices
### 1. Change Management
**Process for deploying global policies**:
1. **Development**:
- Write policy in test namespace
- Test with sample Applications
- Code review by platform team
2. **Staging**:
- Deploy to staging vela-system
- Monitor for 24-48 hours
- Check cache hit rates, error rates
3. **Production**:
- Deploy during maintenance window
- Monitor Application reconciliation times
- Have rollback plan ready
### 2. Documentation
**Document each global policy**:
```yaml
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: security-hardening
namespace: vela-system
annotations:
policy.oam.dev/description: "Adds required security labels for compliance"
policy.oam.dev/owner: "platform-team@company.com"
policy.oam.dev/documentation: "https://wiki.company.com/security-labels"
policy.oam.dev/version: "v1.2.0"
policy.oam.dev/changelog: "Added PCI-DSS compliance labels"
spec:
# ...
```
### 3. Testing Strategy
**Recommended test pyramid**:
1. **Unit tests**: Test policy CUE templates in isolation
2. **Integration tests**: Test with real controller (use e2e tests)
3. **Staging validation**: Real workloads for 24-48 hours
4. **Gradual rollout**: Use namespace policies first, then vela-system
### 4. Migration Path
**For existing deployments**:
```bash
# 1. Enable feature gate
--feature-gates=EnableGlobalPolicies=true
# 2. Deploy policies to test namespaces first (not vela-system)
kubectl apply -f policies/ -n test-namespace
# 3. Validate with opt-in approach
# Start with Applications that explicitly want global policies
kubectl annotate app opted-in-app policy.oam.dev/skip-global=false
# 4. Gradually expand to more namespaces
# 5. Finally deploy to vela-system (cluster-wide)
```
## Known Limitations
### 1. No Dependency Ordering
Global policies are applied in priority order, but there's no explicit dependency graph.
**Workaround**: Use priority to control order
```yaml
# Base policy (runs first)
spec:
priority: 1000
# Policy that depends on base (runs second)
spec:
priority: 900
```
### 2. No Dry-Run Mode
Currently no way to preview what a global policy would do without applying it.
**Workaround**: Test in separate namespace first
### 3. Cache Invalidation Delay
Cache has 1-minute TTL. Policy changes may take up to 1 minute to take effect on next reconciliation.
**Workaround**: Manually trigger reconciliation if needed:
```bash
kubectl annotate app my-app reconcile.oam.dev/trigger="$(date +%s)" --overwrite
```
### 4. No Policy Ordering Within Same Priority
Policies with same priority are ordered alphabetically. No way to specify sub-ordering.
**Workaround**: Use priority values: 100, 90, 80 instead of all 100
## Security Review Checklist
Before deploying a global policy to production:
- [ ] Code reviewed by platform team
- [ ] Tested in isolated namespace
- [ ] CUE template reviewed for security issues
- [ ] No sensitive data exposed
- [ ] No expensive operations (API calls, large loops)
- [ ] Documentation added (description, owner, changelog)
- [ ] Rollback plan documented
- [ ] Monitoring/alerts configured
- [ ] RBAC reviewed (who can modify this policy?)
- [ ] Impact analysis done (how many apps affected?)
## Future Enhancements to Consider
### 1. Admission Webhooks
Add validating webhook for PolicyDefinitions:
- Prevent invalid CUE templates
- Enforce naming conventions
- Validate security policies
### 2. Policy Dry-Run
```bash
kubectl vela policy dry-run -f my-policy.yaml -n vela-system
```
### 3. Metrics and Dashboards
Grafana dashboard showing:
- Cache hit rate per policy
- Rendering duration per policy
- Number of Applications affected
- Policy application errors
### 4. Policy Dependencies
```yaml
spec:
dependsOn:
- security-base
- networking-config
```
### 5. Policy Testing Framework
```bash
kubectl vela policy test -f my-policy.yaml --test-cases test-cases.yaml
```
## Getting Help
If you encounter issues:
1. **Check logs**: Application controller logs in vela-system
2. **Check status**: `kubectl get app <name> -o jsonpath='{.status.appliedGlobalPolicies}'`
3. **Check events**: `kubectl get events --field-selector involvedObject.name=<name>`
4. **Disable globally**: Delete the policy or add opt-out annotation
5. **Report issues**: File issue with policy template, Application spec, and logs
## References
- [Policy Transform README](./README.md)
- [Examples](./examples/)
- [KubeVela Documentation](https://kubevela.io)

View File

@@ -0,0 +1,396 @@
# Application-Scoped Policy Transforms
This feature enables PolicyDefinitions to transform Applications at runtime before they are parsed into resources.
## Overview
Application-scoped policies are a new type of PolicyDefinition that can modify the Application CR in-memory before it goes through the normal parsing and deployment flow. This enables powerful use cases like:
- Injecting sidecars, environment variables, or configuration into all components
- Adding labels, annotations, or metadata based on policy rules
- Conditionally modifying Application specs based on parameters
- Replacing entire Application specs for advanced deployment strategies (canary, blue-green)
- Passing additional context to workflow steps
## How It Works
1. **Policy Execution Point**: Application-scoped policies run immediately after Application validation but before parsing (after `handleFinalizers` and `handleWorkflowRestartAnnotation`, before `GenerateAppFile`)
2. **In-Memory Modifications**: All transforms are applied to the in-memory Application object only - changes are NOT persisted to the cluster
3. **Context Availability**: The policy CUE template has access to:
- `parameter`: Policy parameters from the Application spec
- `context.application`: The full Application object being processed
4. **Output Fields**:
- `enabled` (bool, default true): Whether to apply this policy
- `transforms`: Object containing transform operations
- `additionalContext`: Data to pass to workflow steps (available as `context.custom`)
## PolicyDefinition Structure
```yaml
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: my-transform-policy
spec:
# REQUIRED: Set scope to Application
scope: Application
schematic:
cue:
template: |
parameter: {
# Define your policy parameters
}
# Optional: Conditional application (default: true)
enabled: true | bool
# Optional: Transform operations
transforms: {
# Spec transform (supports "replace" or "merge")
spec?: {
type: "replace" | "merge"
value: {...}
}
# Labels transform (only supports "merge" for safety)
labels?: {
type: "merge"
value: {[string]: string}
}
# Annotations transform (only supports "merge" for safety)
annotations?: {
type: "merge"
value: {[string]: string}
}
}
# Optional: Additional context for workflow steps
additionalContext: {
# Any data you want to pass to workflow
}
```
## Transform Operations
### Spec Transforms
**Merge** (default): Deep merges the provided value into the existing spec
```cue
transforms: spec: {
type: "merge"
value: {
components: [{
properties: {
env: [{name: "NEW_VAR", value: "value"}]
}
}]
}
}
```
**Replace**: Completely replaces the Application spec
```cue
transforms: spec: {
type: "replace"
value: {
components: [/* new components */]
}
}
```
### Labels and Annotations
Only **merge** operation is supported for safety (prevents removing platform-critical metadata):
```cue
transforms: labels: {
type: "merge"
value: {
"team": "platform"
"environment": "production"
}
}
```
## Using additionalContext in Workflows
Data from `additionalContext` is stored in the Go context and can be accessed in workflow steps:
```yaml
workflow:
steps:
- name: my-step
type: apply-object
properties:
value:
# Access via context.custom (if workflow supports it)
metadata:
annotations:
policy-data: context.custom.policyApplied
```
Note: The exact mechanism for accessing `context.custom` in workflow steps may depend on workflow step implementation.
## Global PolicyDefinitions
Global policies automatically apply to Applications without being explicitly referenced in the Application spec. This feature requires the `EnableGlobalPolicies` feature gate.
**Enable the feature**:
```bash
--feature-gates=EnableGlobalPolicies=true
```
### Marking a Policy as Global
```yaml
spec:
global: true # Mark as global
priority: 100 # Control execution order (higher runs first)
scope: Application # Must be Application-scoped
```
### Scope Rules
1. **vela-system namespace**: Applies to ALL Applications in ALL namespaces (cluster-wide standards)
2. **Other namespaces**: Applies only to Applications in that namespace (namespace standards)
### Execution Order
Global policies execute BEFORE explicit policies. Within each tier, policies are ordered by:
1. **Priority** (descending): Higher priority values run first
2. **Name** (alphabetical): Policies with the same priority are ordered alphabetically
**Full execution order**:
1. Global policies from Application's namespace (namespace-specific, sorted by priority+name)
2. Global policies from vela-system (cluster-wide, sorted by priority+name)
3. Explicit policies from Application spec (user-defined order)
**Rationale**: Namespace policies run first so they can override cluster-wide defaults. Explicit policies run last so users have the final say.
### Priority Field
Use the `Priority` field to control execution order:
```yaml
spec:
global: true
priority: 100 # Higher values run first (default: 0)
```
**Example use cases**:
- Priority 1000: Critical security policies (must run first)
- Priority 100: Standard platform labels/annotations
- Priority 50: Optional monitoring/observability
- Priority 0: Low-priority defaults
See `global-policy-priority-example.yaml` for a complete example.
### Deduplication
If a global policy with the same name exists in both vela-system and a namespace:
- **Namespace version WINS**: The namespace version completely replaces the vela-system version
- **Use case**: Namespaces can override cluster-wide policies with their own implementation
- **Example**: `auto-add-labels` in vela-system adds `env=prod`, but namespace `dev` overrides with `env=dev`
See `global-policy-namespace-override-example.yaml` for a complete example.
### Opting Out
Applications can opt-out of ALL global policies:
```yaml
metadata:
annotations:
policy.oam.dev/skip-global: "true"
```
See `application-opt-out-example.yaml` for a complete example.
### Conditional Application
Use the `enabled` field to conditionally apply global policies:
```cue
import "strings"
# Only apply to tenant namespaces
enabled: strings.HasPrefix(context.application.metadata.namespace, "tenant-")
```
See `global-policy-tenant-config.yaml` for a complete example.
### Observability
Check which global policies were applied:
```bash
kubectl get app my-app -o jsonpath='{.status.appliedGlobalPolicies}'
```
Output shows:
- Which global policies were discovered
- Whether they were applied or skipped
- Reason for skipping (e.g., `enabled=false`)
Kubernetes Events are also emitted for each global policy application or skip.
### Important Constraints
1. **Mutual Exclusivity**: Global policies CANNOT be explicitly referenced in Application specs
- ❌ Invalid: Referencing a global policy in `spec.policies`
- ✅ Valid: Let global policies auto-apply, or opt-out with annotation
2. **Parameters Must Have Defaults**: Global policies can have parameters, but ALL must have default values
- ❌ Invalid: `parameter: { env: string }` (no default)
- ❌ Invalid: `parameter: { env?: string }` (optional but no default)
- ✅ Valid: `parameter: { env: *"prod" | string }` (has default)
- ✅ Valid: `parameter: {}` (empty)
- Use `context.application` to access application data dynamically
3. **Feature Gate Required**: Must enable `EnableGlobalPolicies` feature gate
### Use Cases
- **Platform Standards**: Add labels, annotations, or metadata to all Applications
- **Governance**: Enforce backup policies, monitoring, security standards
- **Multi-tenancy**: Apply tenant-specific configuration based on namespace
- **Environment Standards**: Production apps get different policies than dev/staging
- **Security Compliance**: Automatically enforce security policies across all applications
## Examples
### Example 1: Add Environment Labels
See `policy-definition-add-labels.yaml` - Adds environment and team labels to Applications.
```yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
spec:
policies:
- name: env-labels
type: add-environment-labels
properties:
environment: production
team: platform-team
```
Result: Application will have labels `environment=production`, `team=platform-team`, and `managed-by=kubevela` added.
### Example 2: Inject Monitoring Sidecar
See `policy-definition-inject-sidecar.yaml` - Conditionally injects a monitoring sidecar into all components.
```yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
spec:
policies:
- name: monitoring
type: inject-monitoring-sidecar
properties:
enableMonitoring: true
sidecarImage: "my-monitoring-agent:v2"
```
Result: Each component will have a monitoring sidecar added if enableMonitoring=true.
### Example 3: Canary Deployment Override
See `policy-definition-replace-spec.yaml` - Replaces the entire spec to create a canary deployment.
```yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
spec:
policies:
- name: canary
type: canary-deployment-override
properties:
canaryEnabled: true
canaryReplicas: 1
```
Result: Application spec is replaced with a canary configuration.
## Testing
To test this feature:
1. Create a PolicyDefinition with `scope: Application`
2. Create an Application that references the policy
3. Apply both to your cluster
4. Check the Application controller logs for policy application messages
5. Verify the transforms were applied by checking deployed resources
## Safety Constraints
1. **Labels/Annotations**: Only support `merge` operation to prevent accidentally removing platform-critical metadata
2. **Spec Transforms**: Support both `replace` and `merge`, but use with caution:
- `replace` completely overwrites the spec
- `merge` is safer for incremental modifications
3. **Type Safety**: The CUE template enforces proper structure for transforms
4. **Conditional Execution**: Use `enabled: false` to skip policy application
## Debugging & Observability
### Viewing Applied Policies
Check which global policies were applied:
```bash
kubectl get app my-app -o jsonpath='{.status.appliedGlobalPolicies}' | jq
```
This shows:
- Which policies were discovered and applied
- Labels, annotations, and context added by each policy
- Whether each policy modified the Application spec
- Why policies were skipped (if `applied: false`)
### Viewing Spec Changes (✅ New Feature)
When global policies modify the Application spec, KubeVela stores detailed diffs in a ConfigMap for auditing:
```bash
# Check if spec diffs exist
kubectl get app my-app -o jsonpath='{.status.policyDiffsConfigMap}'
# Output: my-app-policy-diffs
# View diff for first policy
kubectl get configmap my-app-policy-diffs -o jsonpath='{.data.001-policy-name}' | jq
```
The ConfigMap contains:
- **Sequence-prefixed keys**: `001-policy-name`, `002-policy-name` (preserves execution order)
- **JSON Merge Patch diffs**: Shows exactly what each policy changed
- **Standard labels**: Discoverable with `kubectl get cm -l "app.oam.dev/policy-diffs=true"`
### Future: CLI Tools
Planned CLI commands for better UX:
- `vela policy view <app>` - Interactive viewer for policy changes with before/after comparison
- `vela policy dry-run <app> --policies <p1> <p2>` - Preview policy effects before applying
See [OBSERVABILITY.md](./OBSERVABILITY.md) for detailed debugging examples and use cases.
## Implementation Details
- **File**: `pkg/controller/core.oam.dev/v1beta1/application/policy_transforms.go`
- **Integration Point**: `application_controller.go:161-167` (after finalizers, before GenerateAppFile)
- **Context Key**: `PolicyAdditionalContextKey` stores additionalContext in Go context
- **CUE Compiler**: Uses `cuex.DefaultCompiler` for template rendering
- **Deep Merge**: Recursive merge for maps in spec and additionalContext
## References
- Original Application Controller: `pkg/controller/core.oam.dev/v1beta1/application/application_controller.go`
- PolicyDefinition API: `apis/core.oam.dev/v1beta1/policy_definition.go`
- Tests: `pkg/controller/core.oam.dev/v1beta1/application/policy_transforms_test.go`

View File

@@ -0,0 +1,20 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: special-application
namespace: default
annotations:
# Opt out of ALL global policies
policy.oam.dev/skip-global: "true"
spec:
components:
- name: my-component
type: webservice
properties:
image: nginx:latest
ports:
- port: 80
expose: true
# This application will NOT have any global policies applied
# Only explicit policies in the spec will be processed

View File

@@ -0,0 +1,60 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: my-web-app
namespace: default
labels:
app: my-web-app
spec:
components:
- name: frontend
type: webservice
properties:
image: nginx:latest
ports:
- port: 80
expose: true
- name: backend
type: webservice
properties:
image: my-backend:v1.0
env:
- name: DATABASE_URL
value: "postgres://db:5432/mydb"
# Application-scoped policy that will transform the Application before parsing
policies:
- name: env-labels
type: add-environment-labels
properties:
environment: production
team: platform-team
workflow:
steps:
- name: deploy-frontend
type: apply-component
properties:
component: frontend
- name: deploy-backend
type: apply-component
properties:
component: backend
- name: check-context
type: step-group
subSteps:
# The additionalContext from the policy will be available here
# as context.custom.policyApplied and context.custom.environment
- name: log-policy-context
type: apply-object
properties:
value:
apiVersion: v1
kind: ConfigMap
metadata:
name: policy-context
namespace: default
data:
# These values come from additionalContext in the policy
policyApplied: "add-environment-labels"
environment: "production"

View File

@@ -0,0 +1,63 @@
# Example: Namespace Override Pattern
# This shows how a namespace can override a cluster-wide global policy
---
# Cluster-wide default: all apps get production environment
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: environment-config
namespace: vela-system
spec:
global: true
priority: 50
scope: Application
schematic:
cue:
template: |
parameter: {}
enabled: true
transforms: {
labels: {
type: "merge"
value: {
"environment": "production"
"compliance": "enabled"
"backup": "daily"
}
}
}
---
# Development namespace override: different rules for dev
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: environment-config # SAME NAME - overrides vela-system version
namespace: dev # Only applies in dev namespace
spec:
global: true
priority: 50
scope: Application
schematic:
cue:
template: |
parameter: {}
enabled: true
transforms: {
labels: {
type: "merge"
value: {
"environment": "development"
"compliance": "disabled" # Relaxed rules for dev
"backup": "none"
}
}
}
---
# Result:
# - Applications in "dev" namespace: environment=development, compliance=disabled
# - Applications in other namespaces: environment=production, compliance=enabled

View File

@@ -0,0 +1,87 @@
# Example: Priority Ordering
# Demonstrates how Priority field controls execution order
---
# Critical security policy - runs FIRST (highest priority)
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: security-hardening
namespace: vela-system
spec:
global: true
priority: 1000 # Highest - runs first
scope: Application
schematic:
cue:
template: |
parameter: {}
enabled: true
transforms: {
annotations: {
type: "merge"
value: {
"security.platform.io/scan-required": "true"
"security.platform.io/minimum-tls": "1.2"
}
}
}
---
# Standard platform labels - runs SECOND
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: platform-labels
namespace: vela-system
spec:
global: true
priority: 100
scope: Application
schematic:
cue:
template: |
parameter: {}
enabled: true
transforms: {
labels: {
type: "merge"
value: {
"platform.io/managed-by": "kubevela"
"platform.io/region": "us-west-2"
}
}
}
---
# Optional observability - runs THIRD (lower priority)
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: observability-config
namespace: vela-system
spec:
global: true
priority: 50
scope: Application
schematic:
cue:
template: |
parameter: {}
enabled: true
transforms: {
annotations: {
type: "merge"
value: {
"monitoring.platform.io/enabled": "true"
"monitoring.platform.io/retention": "30d"
}
}
}
---
# Execution order: security-hardening → platform-labels → observability-config
# Policies with same priority are applied alphabetically by name

View File

@@ -0,0 +1,42 @@
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: inject-tenant-config
namespace: vela-system
spec:
global: true
priority: 50
scope: Application
schematic:
cue:
template: |
import "strings"
parameter: {}
// Only apply to tenant namespaces
enabled: strings.HasPrefix(context.application.metadata.namespace, "tenant-")
transforms: {
annotations: {
type: "merge"
value: {
"config.platform.io/tenant-mode": "true"
"config.platform.io/backup-enabled": "true"
}
}
labels: {
type: "merge"
value: {
"tenant": "true"
}
}
}
additionalContext: {
tenantConfig: {
isolated: true
backupEnabled: true
monitoring: "enhanced"
}
}

View File

@@ -0,0 +1,33 @@
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: add-environment-labels
namespace: vela-system # Cluster-wide
spec:
# Global policies automatically apply to Applications
global: true
priority: 100 # Higher priority runs first
# Application scope means this policy transforms the Application CR before parsing
scope: Application
schematic:
cue:
template: |
parameter: {}
enabled: true # Always apply
// Add labels to the Application
transforms: {
labels: {
type: "merge"
value: {
"platform.io/managed-by": "kubevela"
"platform.io/version": "v1.9"
}
}
}
// Store context for workflow steps
additionalContext: {
policyApplied: "add-environment-labels"
}

View File

@@ -0,0 +1,45 @@
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: inject-monitoring-sidecar
namespace: default
spec:
scope: Application
schematic:
cue:
template: |
parameter: {
enableMonitoring: *true | bool
sidecarImage: *"monitoring-agent:latest" | string
}
// Only apply if monitoring is enabled
enabled: parameter.enableMonitoring
// Inject sidecar into all components
transforms: {
spec: {
type: "merge"
value: {
components: [ for comp in context.application.spec.components {
name: comp.name
type: comp.type
properties: {
if comp.properties != _|_ {
comp.properties
}
// Add sidecar container configuration
sidecars: [{
name: "monitoring-agent"
image: parameter.sidecarImage
}]
}
}]
}
}
}
additionalContext: {
monitoringEnabled: true
sidecarImage: parameter.sidecarImage
}

View File

@@ -0,0 +1,49 @@
apiVersion: core.oam.dev/v1beta1
kind: PolicyDefinition
metadata:
name: canary-deployment-override
namespace: default
spec:
scope: Application
schematic:
cue:
template: |
parameter: {
canaryEnabled: bool
canaryReplicas: *1 | int
}
// Only replace spec if canary is enabled
enabled: parameter.canaryEnabled
// Replace entire spec with canary configuration
transforms: {
spec: {
type: "replace"
value: {
components: [{
name: context.application.spec.components[0].name + "-canary"
type: context.application.spec.components[0].type
properties: {
// Copy original properties
if context.application.spec.components[0].properties != _|_ {
context.application.spec.components[0].properties
}
// Override replicas
replicas: parameter.canaryReplicas
}
traits: [{
type: "labels"
properties: {
"deployment-type": "canary"
}
}]
}]
}
}
}
additionalContext: {
deploymentMode: "canary"
canaryReplicas: parameter.canaryReplicas
}

2
go.mod
View File

@@ -18,6 +18,7 @@ require (
github.com/dave/jennifer v1.6.1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/ettle/strcase v0.2.0
github.com/evanphx/json-patch v5.7.0+incompatible
github.com/fatih/color v1.18.0
github.com/fluxcd/helm-controller/api v0.32.2
github.com/fluxcd/source-controller/api v0.30.0
@@ -147,7 +148,6 @@ require (
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
github.com/emicklei/proto v1.14.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/camelcase v1.0.0 // indirect

View File

@@ -186,14 +186,24 @@ 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
}
// GeneratePolicyManifests generates policy manifests from an appFile
// internal policies like apply-once, topology, will not render manifests
func (af *Appfile) GeneratePolicyManifests(_ context.Context) ([]*unstructured.Unstructured, error) {
func (af *Appfile) GeneratePolicyManifests(ctx context.Context, cli client.Client) ([]*unstructured.Unstructured, error) {
var manifests []*unstructured.Unstructured
for _, policy := range af.ParsedPolicies {
// Skip Application-scoped policies - they were already processed in ApplyApplicationScopeTransforms
if af.isApplicationScopedPolicy(ctx, cli, policy) {
// Policy has non-default scope, skip resource generation
continue
}
un, err := af.generatePolicyUnstructured(policy)
if err != nil {
return nil, err
@@ -246,6 +256,44 @@ func generatePolicyUnstructuredFromCUEModule(comp *Component, artifacts []*types
return res, nil
}
// isApplicationScopedPolicy checks if a policy has a non-default Scope
// DefaultScope (empty) policies use standard output-based rendering
// Non-default scopes (e.g., ApplicationScope) are handled in specialized pipelines
// and should not be rendered as standard K8s resources
// Note: Backward compatible - unset Scope field defaults to empty string (DefaultScope)
func (af *Appfile) isApplicationScopedPolicy(ctx context.Context, cli client.Client, policy *Component) bool {
var policyDef *v1beta1.PolicyDefinition
// Try to get from AppRevision first (preferred - cached in revision)
if af.AppRevision != nil && af.AppRevision.Spec.PolicyDefinitions != nil {
if def, ok := af.AppRevision.Spec.PolicyDefinitions[policy.Type]; ok {
policyDef = &def
}
}
// If not in AppRevision, look it up from cluster
// This handles the case where GeneratePolicyManifests is called before AppRevision is set
if policyDef == nil && cli != nil {
def := &v1beta1.PolicyDefinition{}
// Try app namespace first, then vela-system
err := util.GetCapabilityDefinition(ctx, cli, def, policy.Type, af.app.Annotations)
if err != nil {
// Policy not found or error - assume default scope
return false
}
policyDef = def
}
// If still not found, assume default scope
if policyDef == nil {
return false
}
// Check if it has a non-default scope
// DefaultScope = "", so any non-empty scope means it's scoped
return policyDef.Spec.Scope != v1beta1.DefaultScope
}
// artifacts contains resources in unstructured shape of all components
// it allows to access values of workloads and traits in CUE template, i.g.,
// `if context.artifacts.<compName>.ready` to determine whether it's ready to access
@@ -737,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]

View File

@@ -158,7 +158,7 @@ func (d *Option) ExecuteDryRun(ctx context.Context, application *v1beta1.Applica
if err != nil {
return nil, nil, errors.WithMessage(err, "cannot generate manifests from components and traits")
}
policyManifests, err := appFile.GeneratePolicyManifests(ctx)
policyManifests, err := appFile.GeneratePolicyManifests(ctx, d.Client)
if err != nil {
return nil, nil, errors.WithMessage(err, "cannot generate manifests from policies")
}

View File

@@ -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")
@@ -342,6 +349,16 @@ func (p *Parser) parsePoliciesFromRevision(ctx context.Context, af *Appfile) (er
case v1alpha1.DebugPolicyType:
af.Debug = true
default:
// Skip policies with non-default scope - they're already processed earlier
// Check PolicyDefinition scope from AppRevision
if af.AppRevision != nil && af.AppRevision.Spec.PolicyDefinitions != nil {
if policyDef, ok := af.AppRevision.Spec.PolicyDefinitions[policy.Type]; ok {
if policyDef.Spec.Scope != v1beta1.DefaultScope {
continue // Skip - non-default scope
}
}
}
w, err := p.makeComponentFromRevision(policy.Name, policy.Type, types.TypePolicy, policy.Properties, af.AppRevision)
if err != nil {
return err
@@ -385,6 +402,12 @@ func (p *Parser) parsePolicies(ctx context.Context, af *Appfile) (err error) {
af.RelatedTraitDefinitions[def.Name] = def
}
default:
// Skip Application-scoped policies - they're already processed in ApplyApplicationScopeTransforms()
// and should not be rendered as K8s resources
if p.isApplicationScopedPolicy(ctx, policy.Type, af.app.Annotations) {
continue
}
w, err := p.makeComponent(ctx, policy.Name, policy.Type, types.TypePolicy, policy.Properties, af.app.Annotations)
if err != nil {
return err
@@ -395,6 +418,25 @@ func (p *Parser) parsePolicies(ctx context.Context, af *Appfile) (err error) {
return nil
}
// isApplicationScopedPolicy checks if a policy has a non-default Scope.
// Policies with non-default scopes (e.g., "Application") are handled in specialized
// pipelines before parsing and should not be added to ParsedPolicies.
// Returns true if the policy has ANY non-default scope (Scope != DefaultScope).
func (p *Parser) isApplicationScopedPolicy(ctx context.Context, policyType string, annotations map[string]string) bool {
policyDef := &v1beta1.PolicyDefinition{}
// Try to load PolicyDefinition (checks namespace first, then vela-system)
err := util.GetCapabilityDefinition(ctx, p.client, policyDef, policyType, annotations)
if err != nil {
// If not found or error, assume DefaultScope (safe default - include the policy)
return false
}
// Check if scope is non-default
// DefaultScope = "" (empty string), so ANY non-empty scope means it should be filtered
return policyDef.Spec.Scope != v1beta1.DefaultScope
}
func (p *Parser) loadWorkflowToAppfile(ctx context.Context, af *Appfile) error {
var err error
// parse workflow steps

View File

@@ -35,6 +35,7 @@ import (
"k8s.io/klog/v2"
"k8s.io/utils/strings/slices"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
ctrlEvent "sigs.k8s.io/controller-runtime/pkg/event"
@@ -158,6 +159,38 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
return result, nil
}
// Apply Application-scoped policy transforms (in-memory modifications before parsing)
// Returns updated context with additionalContext that will be available in workflow
logCtx, err = handler.ApplyApplicationScopeTransforms(logCtx, app)
if err != nil {
logCtx.Error(err, "Failed to apply Application-scoped policy transforms")
return r.endWithNegativeCondition(logCtx, app, condition.ReconcileError(err), common.ApplicationStarting)
}
// Emit events for applied Application-scoped policies (for observability)
for _, appliedPolicy := range app.Status.AppliedApplicationPolicies {
// Use the Source field to determine if it's global or explicit
isGlobal := appliedPolicy.Source == "global"
if appliedPolicy.Applied {
if isGlobal {
r.Recorder.Event(app, event.Normal("GlobalPolicyApplied",
fmt.Sprintf("Applied global policy %s from namespace %s", appliedPolicy.Name, appliedPolicy.Namespace)))
} else {
r.Recorder.Event(app, event.Normal("PolicyApplied",
fmt.Sprintf("Applied policy %s", appliedPolicy.Name)))
}
} else {
if isGlobal {
r.Recorder.Event(app, event.Normal("GlobalPolicySkipped",
fmt.Sprintf("Skipped global policy %s: %s", appliedPolicy.Name, appliedPolicy.Reason)))
} else {
r.Recorder.Event(app, event.Normal("PolicySkipped",
fmt.Sprintf("Skipped policy %s: %s", appliedPolicy.Name, appliedPolicy.Reason)))
}
}
}
appFile, err := appParser.GenerateAppFile(logCtx, app)
if err != nil {
r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedParse, err))
@@ -171,11 +204,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err))
return r.endWithNegativeCondition(logCtx, app, condition.ErrorCondition("Revision", err), common.ApplicationRendering)
}
if err := handler.FinalizeAndApplyAppRevision(logCtx); err != nil {
logCtx.Error(err, "Failed to apply app revision")
r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err))
return r.endWithNegativeCondition(logCtx, app, condition.ErrorCondition("Revision", err), common.ApplicationRendering)
}
logCtx.Info("Successfully prepare current app revision", "revisionName", handler.currentAppRev.Name,
"revisionHash", handler.currentRevHash, "isNewRevision", handler.isNewRevision)
app.Status.SetConditions(condition.ReadyCondition("Revision"))
@@ -640,6 +675,24 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
return true
},
}).
Watches(
&v1beta1.PolicyDefinition{},
ctrlHandler.EnqueueRequestsFromMapFunc(findApplicationsForGlobalPolicy),
builder.WithPredicates(predicate.Funcs{
CreateFunc: func(e ctrlEvent.CreateEvent) bool {
policy := e.Object.(*v1beta1.PolicyDefinition)
return policy.Spec.Global && policy.Spec.Scope == v1beta1.ApplicationScope
},
UpdateFunc: func(e ctrlEvent.UpdateEvent) bool {
policy := e.ObjectNew.(*v1beta1.PolicyDefinition)
return policy.Spec.Global && policy.Spec.Scope == v1beta1.ApplicationScope
},
DeleteFunc: func(e ctrlEvent.DeleteEvent) bool {
policy := e.Object.(*v1beta1.PolicyDefinition)
return policy.Spec.Global && policy.Spec.Scope == v1beta1.ApplicationScope
},
}),
).
For(&v1beta1.Application{}).
Complete(r)
}
@@ -678,6 +731,24 @@ func filterManagedFieldChangesUpdate(e ctrlEvent.UpdateEvent) bool {
return !reflect.DeepEqual(newTracker, old)
}
func findApplicationsForGlobalPolicy(_ context.Context, obj client.Object) []reconcile.Request {
policy := obj.(*v1beta1.PolicyDefinition)
// Invalidate cache when global policies change
if policy.Namespace == oam.SystemDefinitionNamespace {
// vela-system policy affects all namespaces - invalidate entire cache
applicationPolicyCache.InvalidateAll()
} else {
// Namespace-specific policy - invalidate for that namespace
applicationPolicyCache.InvalidateForNamespace(policy.Namespace)
}
// Strategy: Don't trigger immediate reconciliation
// Applications will pick up changes on next natural reconciliation (spec change, etc.)
// This avoids thundering herd problem when global policies change
return []reconcile.Request{}
}
func findObjectForResourceTracker(_ context.Context, rt client.Object) []reconcile.Request {
if EnableResourceTrackerDeleteOnlyTrigger && rt.GetDeletionTimestamp() == nil {
return nil
@@ -730,6 +801,8 @@ const (
ReplicaKeyContextKey
// OriginalAppKey is the key in the context that records the in coming original app
OriginalAppKey
// PolicyAdditionalContextKey is the key in the context that records additional context from Application-scoped policies
PolicyAdditionalContextKey
)
func withOriginalApp(ctx context.Context, app *v1beta1.Application) context.Context {

View File

@@ -0,0 +1,196 @@
/*
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 application
import (
"fmt"
"sync"
"time"
"github.com/pkg/errors"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
const (
// ApplicationPolicyCacheTTL defines how long cache entries are valid
ApplicationPolicyCacheTTL = 1 * time.Minute
)
// RenderedPolicyResult stores the complete rendered output of a policy's CUE template
type RenderedPolicyResult struct {
PolicyName string // Name of the policy
PolicyNamespace string // Namespace of the policy
Priority int32 // Priority of the policy (for execution order)
Enabled bool // Whether the policy should be applied
Source string // "global" or "explicit" - how the policy was discovered
Transforms interface{} // The transforms (*PolicyTransforms) from CUE template
AdditionalContext map[string]interface{} // The additionalContext field from CUE template
SkipReason string // Reason for skipping (if enabled=false or error)
Config *PolicyConfig // Policy configuration including refresh settings
}
// ApplicationPolicyCacheEntry represents cached rendered results for an Application
// Simple 1-minute TTL cache - invalidates on Application.Spec changes or time expiry
type ApplicationPolicyCacheEntry struct {
appSpecHash string // Hash of Application.Spec for invalidation
renderedResults []RenderedPolicyResult // Cached rendering results
timestamp time.Time // For 1-minute TTL
}
// ApplicationPolicyCache caches rendered policy results (both global and explicit policies)
type ApplicationPolicyCache struct {
mu sync.RWMutex
entries map[string]*ApplicationPolicyCacheEntry
}
// NewApplicationPolicyCache creates a new cache instance
func NewApplicationPolicyCache() *ApplicationPolicyCache {
return &ApplicationPolicyCache{
entries: make(map[string]*ApplicationPolicyCacheEntry),
}
}
// Package-level singleton cache instance
var applicationPolicyCache = NewApplicationPolicyCache()
// computeApplicationPolicyCacheKey generates a cache key for an Application
func computeApplicationPolicyCacheKey(app *v1beta1.Application) string {
return fmt.Sprintf("%s/%s", app.Namespace, app.Name)
}
// computeAppSpecHash computes a hash of the Application spec
func computeAppSpecHash(app *v1beta1.Application) (string, error) {
return apply.ComputeSpecHash(app.Spec)
}
// Get retrieves cached rendered policy results if valid
// Returns (renderedResults, cacheHit, error)
func (c *ApplicationPolicyCache) Get(app *v1beta1.Application) ([]RenderedPolicyResult, bool, error) {
results, hit, _, err := c.GetWithReason(app)
return results, hit, err
}
// GetWithReason retrieves cached rendered policy results if valid and returns the reason for cache miss
// Returns (renderedResults, cacheHit, missReason, error)
// missReason values: "not_found", "spec_changed", "ttl_expired", "" (on cache hit)
func (c *ApplicationPolicyCache) GetWithReason(app *v1beta1.Application) ([]RenderedPolicyResult, bool, string, error) {
cacheKey := computeApplicationPolicyCacheKey(app)
appSpecHash, err := computeAppSpecHash(app)
if err != nil {
return nil, false, "error", errors.Wrap(err, "failed to compute app spec hash")
}
c.mu.RLock()
defer c.mu.RUnlock()
entry, found := c.entries[cacheKey]
if !found {
return nil, false, "not_found", nil // Cache miss - first time
}
// Check if Application spec changed
if entry.appSpecHash != appSpecHash {
return nil, false, "spec_changed", nil // Spec changed, cache invalid
}
// Check TTL
if time.Since(entry.timestamp) > ApplicationPolicyCacheTTL {
return nil, false, "ttl_expired", nil // Stale, recompute
}
// Cache hit!
return entry.renderedResults, true, "", nil
}
// Set stores rendered policy results in the cache
func (c *ApplicationPolicyCache) Set(app *v1beta1.Application, results []RenderedPolicyResult) error {
cacheKey := computeApplicationPolicyCacheKey(app)
appSpecHash, err := computeAppSpecHash(app)
if err != nil {
return errors.Wrap(err, "failed to compute app spec hash")
}
c.mu.Lock()
defer c.mu.Unlock()
c.entries[cacheKey] = &ApplicationPolicyCacheEntry{
appSpecHash: appSpecHash,
renderedResults: results,
timestamp: time.Now(),
}
return nil
}
// InvalidateForNamespace invalidates all cache entries that might be affected by changes in a namespace
func (c *ApplicationPolicyCache) InvalidateForNamespace(namespace string) {
c.mu.Lock()
defer c.mu.Unlock()
// Invalidate all entries for Applications in this namespace
for key := range c.entries {
delete(c.entries, key)
}
}
// InvalidateAll clears the entire cache
// Used when vela-system global policies change (affects all namespaces)
func (c *ApplicationPolicyCache) InvalidateAll() {
c.mu.Lock()
defer c.mu.Unlock()
c.entries = make(map[string]*ApplicationPolicyCacheEntry)
}
// InvalidateApplication invalidates cache for a specific Application
func (c *ApplicationPolicyCache) InvalidateApplication(namespace, name string) {
cacheKey := fmt.Sprintf("%s/%s", namespace, name)
c.mu.Lock()
defer c.mu.Unlock()
delete(c.entries, cacheKey)
}
// Size returns the number of cached entries
func (c *ApplicationPolicyCache) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.entries)
}
// CleanupStale removes stale entries older than TTL
func (c *ApplicationPolicyCache) CleanupStale() int {
c.mu.Lock()
defer c.mu.Unlock()
removed := 0
now := time.Now()
for key, entry := range c.entries {
if now.Sub(entry.timestamp) > ApplicationPolicyCacheTTL {
delete(c.entries, key)
removed++
}
}
return removed
}

View File

@@ -401,7 +401,7 @@ func (h *AppHandler) ApplyPolicies(ctx context.Context, af *appfile.Appfile) err
}))
defer subCtx.Commit("finish apply policies")
}
policyManifests, err := af.GeneratePolicyManifests(ctx)
policyManifests, err := af.GeneratePolicyManifests(ctx, h.Client)
if err != nil {
return errors.Wrapf(err, "failed to render policy manifests")
}

View File

@@ -114,7 +114,7 @@ type manifestDispatcher struct {
healthCheck func(ctx context.Context, c *appfile.Component, appRev *v1beta1.ApplicationRevision) (bool, error)
}
func (h *AppHandler) generateDispatcher(appRev *v1beta1.ApplicationRevision, readyWorkload *unstructured.Unstructured, readyTraits []*unstructured.Unstructured, overrideNamespace string, annotations map[string]string) ([]*manifestDispatcher, error) {
func (h *AppHandler) generateDispatcher(appRev *v1beta1.ApplicationRevision, previousAppRev *v1beta1.ApplicationRevision, readyWorkload *unstructured.Unstructured, readyTraits []*unstructured.Unstructured, overrideNamespace string, annotations map[string]string) ([]*manifestDispatcher, error) {
dispatcherGenerator := func(options DispatchOptions) *manifestDispatcher {
assembleManifestFn := func(skipApplyWorkload bool) (bool, []*unstructured.Unstructured) {
manifests := options.Traits
@@ -152,7 +152,24 @@ func (h *AppHandler) generateDispatcher(appRev *v1beta1.ApplicationRevision, rea
// Note: componentPropertiesChanged handles nil comp.Params correctly, so we don't check it here
propertiesChanged := false
if isHealth && err == nil {
propertiesChanged = componentPropertiesChanged(comp, appRev)
// NOTE: The default comparison logic (comparing component against currentAppRev)
// seems unclear and may have become over-complicated over time. It compares the
// current component properties against the NEW ApplicationRevision being created,
// which already contains those same properties - so they typically match.
//
// For application-scoped policies with autoRevision=true, we need to compare against
// the PREVIOUS revision to detect policy-driven changes. Future developers should
// consider whether this logic should be simplified and applied unconditionally.
comparisonRev := appRev // Default: use currentAppRev (existing behavior)
// If autoRevision=true and we have a previous revision, compare against it
// This detects policy-driven changes when ApplicationRevisions are created
if annotations[oam.AnnotationAutoRevision] == "true" && previousAppRev != nil {
comparisonRev = previousAppRev // Use previous revision for comparison
}
propertiesChanged = componentPropertiesChanged(comp, comparisonRev)
}
// Dispatch if: unhealthy, health error, properties changed, or auto-update enabled
@@ -250,8 +267,17 @@ func getTraitDispatchStage(client client.Client, traitType string, appRev *v1bet
return stageType, nil
}
// componentPropertiesChanged compares current component properties with the last
// applied version in ApplicationRevision. Returns true if properties have changed
// componentPropertiesChanged compares current component properties against an ApplicationRevision.
//
// When autoRevision=true (policy transforms create revisions):
// - Compares against the PREVIOUS revision to detect policy-driven changes
// - This ensures policies that transform components trigger redeployment
//
// When autoRevision=false or not set (default):
// - Compares against the CURRENT revision
// - This detects when workflow steps dynamically modify component properties
//
// Returns true if properties have changed.
func componentPropertiesChanged(comp *appfile.Component, appRev *v1beta1.ApplicationRevision) bool {
var revComponent *common.ApplicationComponent
for i := range appRev.Spec.Application.Spec.Components {

View File

@@ -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),
@@ -327,7 +327,7 @@ func (h *AppHandler) applyComponentFunc(appParser *appfile.Parser, af *appfile.A
isHealth := true
if utilfeature.DefaultMutableFeatureGate.Enabled(features.MultiStageComponentApply) {
manifestDispatchers, err := h.generateDispatcher(appRev, readyWorkload, readyTraits, overrideNamespace, af.AppAnnotations)
manifestDispatchers, err := h.generateDispatcher(appRev, h.latestAppRev, readyWorkload, readyTraits, overrideNamespace, af.AppAnnotations)
if err != nil {
return nil, nil, false, errors.WithMessage(err, "generateDispatcher")
}
@@ -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]

View File

@@ -110,7 +110,7 @@ var _ = Describe("Test Application workflow generator", func() {
}
af, err := appParser.GenerateAppFile(ctx, app)
Expect(err).Should(BeNil())
_, err = af.GeneratePolicyManifests(context.Background())
_, err = af.GeneratePolicyManifests(context.Background(), k8sClient)
Expect(err).Should(BeNil())
handler, err := NewAppHandler(ctx, reconciler, app)
@@ -153,7 +153,7 @@ var _ = Describe("Test Application workflow generator", func() {
}
af, err := appParser.GenerateAppFile(ctx, app)
Expect(err).Should(BeNil())
_, err = af.GeneratePolicyManifests(context.Background())
_, err = af.GeneratePolicyManifests(context.Background(), k8sClient)
Expect(err).Should(BeNil())
handler, err := NewAppHandler(ctx, reconciler, app)
@@ -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())
})

View File

@@ -0,0 +1,365 @@
/*
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 application
import (
"context"
"encoding/json"
"fmt"
monitorContext "github.com/kubevela/pkg/monitor/context"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
)
// PolicyDryRunMode defines how policies are discovered and applied in dry-run
type PolicyDryRunMode string
const (
// DryRunModeIsolated tests only specified policies
DryRunModeIsolated PolicyDryRunMode = "isolated"
// DryRunModeAdditive tests specified policies with existing globals
DryRunModeAdditive PolicyDryRunMode = "additive"
// DryRunModeFull simulates complete policy chain (globals + app policies)
DryRunModeFull PolicyDryRunMode = "full"
)
// PolicyDryRunOptions contains configuration for policy dry-run simulation
type PolicyDryRunOptions struct {
// Mode determines which policies are applied
Mode PolicyDryRunMode
// SpecifiedPolicies are the policy names to test (for isolated/additive modes)
SpecifiedPolicies []string
// IncludeAppPolicies includes policies from Application spec (for full mode)
IncludeAppPolicies bool
}
// PolicyDryRunResult contains the results of a policy dry-run simulation
type PolicyDryRunResult struct {
// Application is the final state after all policies applied
Application *v1beta1.Application
// ExecutionPlan shows which policies were discovered and in what order
ExecutionPlan []PolicyExecutionStep
// PolicyResults contains detailed results for each policy
PolicyResults []PolicyApplicationResult
// Diffs contains the JSON patches for each policy that modified the spec
Diffs map[string][]byte
// Warnings contains any warnings detected during simulation
Warnings []string
// Errors contains any errors encountered
Errors []string
}
// PolicyExecutionStep represents a policy in the execution plan
type PolicyExecutionStep struct {
Sequence int
PolicyName string
PolicyNamespace string
Priority int32
Source string // "global", "app-spec", or "specified"
}
// PolicyApplicationResult contains the results of applying a single policy
type PolicyApplicationResult struct {
Sequence int
PolicyName string
PolicyNamespace string
Priority int32
Enabled bool
Applied bool
SpecModified bool
AddedLabels map[string]string
AddedAnnotations map[string]string
AdditionalContext *runtime.RawExtension
SkipReason string
Error string
}
// SimulatePolicyApplication performs a dry-run simulation of policy application
// This function can be used by CLI tools to preview policy effects without persisting changes
func SimulatePolicyApplication(ctx context.Context, cli client.Client, app *v1beta1.Application, opts PolicyDryRunOptions) (*PolicyDryRunResult, error) {
// Create a deep copy of the application to avoid modifying the original
appCopy := app.DeepCopy()
// Create a monitor context
monCtx := monitorContext.NewTraceContext(ctx, "")
// Create AppHandler for policy operations
handler := &AppHandler{
Client: cli,
app: appCopy,
}
result := &PolicyDryRunResult{
Application: appCopy,
ExecutionPlan: []PolicyExecutionStep{},
PolicyResults: []PolicyApplicationResult{},
Diffs: make(map[string][]byte),
Warnings: []string{},
Errors: []string{},
}
// Clear any existing policy status
appCopy.Status.AppliedApplicationPolicies = nil
// Step 1: Build execution plan based on mode
var policiesToApply []v1beta1.PolicyDefinition
var sequence int = 1
switch opts.Mode {
case DryRunModeIsolated:
// Test only specified policies
if len(opts.SpecifiedPolicies) == 0 {
return nil, errors.New("isolated mode requires at least one policy to be specified")
}
for _, policyName := range opts.SpecifiedPolicies {
// Try to load from vela-system first, then app namespace
policy, err := loadPolicyDefinition(ctx, cli, policyName, oam.SystemDefinitionNamespace)
if err != nil {
// Try app namespace
policy, err = loadPolicyDefinition(ctx, cli, policyName, appCopy.Namespace)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("Policy %s not found in vela-system or %s", policyName, appCopy.Namespace))
continue
}
}
policiesToApply = append(policiesToApply, *policy)
result.ExecutionPlan = append(result.ExecutionPlan, PolicyExecutionStep{
Sequence: sequence,
PolicyName: policy.Name,
PolicyNamespace: policy.Namespace,
Priority: policy.Spec.Priority,
Source: "specified",
})
sequence++
}
case DryRunModeAdditive:
// Include global policies + specified policies
if !shouldSkipGlobalPolicies(appCopy) && utilfeature.DefaultMutableFeatureGate.Enabled(features.EnableGlobalPolicies) {
// Discover global policies
globalPolicies, err := discoverAndDeduplicateGlobalPolicies(monCtx, cli, appCopy.Namespace)
if err != nil {
result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to discover global policies: %v", err))
} else {
for _, policy := range globalPolicies {
policiesToApply = append(policiesToApply, policy)
result.ExecutionPlan = append(result.ExecutionPlan, PolicyExecutionStep{
Sequence: sequence,
PolicyName: policy.Name,
PolicyNamespace: policy.Namespace,
Priority: policy.Spec.Priority,
Source: "global",
})
sequence++
}
}
}
// Add specified policies
for _, policyName := range opts.SpecifiedPolicies {
policy, err := loadPolicyDefinition(ctx, cli, policyName, oam.SystemDefinitionNamespace)
if err != nil {
policy, err = loadPolicyDefinition(ctx, cli, policyName, appCopy.Namespace)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("Policy %s not found", policyName))
continue
}
}
policiesToApply = append(policiesToApply, *policy)
result.ExecutionPlan = append(result.ExecutionPlan, PolicyExecutionStep{
Sequence: sequence,
PolicyName: policy.Name,
PolicyNamespace: policy.Namespace,
Priority: policy.Spec.Priority,
Source: "specified",
})
sequence++
}
case DryRunModeFull:
// Full simulation: global + app policies
if !shouldSkipGlobalPolicies(appCopy) && utilfeature.DefaultMutableFeatureGate.Enabled(features.EnableGlobalPolicies) {
globalPolicies, err := discoverAndDeduplicateGlobalPolicies(monCtx, cli, appCopy.Namespace)
if err != nil {
result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to discover global policies: %v", err))
} else {
for _, policy := range globalPolicies {
policiesToApply = append(policiesToApply, policy)
result.ExecutionPlan = append(result.ExecutionPlan, PolicyExecutionStep{
Sequence: sequence,
PolicyName: policy.Name,
PolicyNamespace: policy.Namespace,
Priority: policy.Spec.Priority,
Source: "global",
})
sequence++
}
}
}
// Add app spec policies if requested
if opts.IncludeAppPolicies {
for _, policyRef := range appCopy.Spec.Policies {
policy, err := loadPolicyDefinition(ctx, cli, policyRef.Type, appCopy.Namespace)
if err != nil {
policy, err = loadPolicyDefinition(ctx, cli, policyRef.Type, oam.SystemDefinitionNamespace)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("Policy %s not found", policyRef.Type))
continue
}
}
policiesToApply = append(policiesToApply, *policy)
result.ExecutionPlan = append(result.ExecutionPlan, PolicyExecutionStep{
Sequence: sequence,
PolicyName: policy.Name,
PolicyNamespace: policy.Namespace,
Priority: policy.Spec.Priority,
Source: "app-spec",
})
sequence++
}
}
}
// Step 2: Apply each policy and track results
policySequence := 1
for _, policy := range policiesToApply {
// Take snapshot before applying policy (for future use in diff computation)
_, err := deepCopyAppSpec(&appCopy.Spec)
if err != nil {
result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to snapshot spec for policy %s: %v", policy.Name, err))
}
// Render the policy
policyRef := v1beta1.AppPolicy{
Name: policy.Name,
Type: policy.Name,
}
renderedResult, err := handler.renderPolicy(monCtx, appCopy, policyRef, &policy)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("Policy %s render error: %v", policy.Name, err))
result.PolicyResults = append(result.PolicyResults, PolicyApplicationResult{
Sequence: policySequence,
PolicyName: policy.Name,
PolicyNamespace: policy.Namespace,
Priority: policy.Spec.Priority,
Enabled: false,
Applied: false,
Error: err.Error(),
})
continue
}
// Apply the rendered policy
var policyChanges *PolicyChanges
monCtx, policyChanges, err = handler.applyRenderedPolicyResult(monCtx, appCopy, renderedResult, policySequence, policy.Spec.Priority)
policyResult := PolicyApplicationResult{
Sequence: policySequence,
PolicyName: policy.Name,
PolicyNamespace: policy.Namespace,
Priority: policy.Spec.Priority,
Enabled: renderedResult.Enabled,
Applied: renderedResult.Enabled && err == nil,
SkipReason: renderedResult.SkipReason,
}
if err != nil {
policyResult.Error = err.Error()
result.Errors = append(result.Errors, fmt.Sprintf("Policy %s application error: %v", policy.Name, err))
} else if renderedResult.Enabled && policyChanges != nil {
// Extract changes from policyChanges
policyResult.SpecModified = policyChanges.SpecModified
policyResult.AddedLabels = policyChanges.AddedLabels
policyResult.AddedAnnotations = policyChanges.AddedAnnotations
// Convert additional context from map to RawExtension
if policyChanges.AdditionalContext != nil {
contextBytes, err := json.Marshal(policyChanges.AdditionalContext)
if err == nil {
policyResult.AdditionalContext = &runtime.RawExtension{Raw: contextBytes}
}
}
policySequence++
}
result.PolicyResults = append(result.PolicyResults, policyResult)
}
result.Application = appCopy
return result, nil
}
// loadPolicyDefinition loads a PolicyDefinition from the cluster
func loadPolicyDefinition(ctx context.Context, cli client.Client, name, namespace string) (*v1beta1.PolicyDefinition, error) {
policy := &v1beta1.PolicyDefinition{}
err := cli.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, policy)
if err != nil {
return nil, err
}
return policy, nil
}
// discoverAndDeduplicateGlobalPolicies discovers global policies from both vela-system and app namespace
// and returns the deduplicated list (namespace policies win over vela-system)
func discoverAndDeduplicateGlobalPolicies(ctx monitorContext.Context, cli client.Client, appNamespace string) ([]v1beta1.PolicyDefinition, error) {
var globalPolicies []v1beta1.PolicyDefinition
// Discover from vela-system
velaSystemPolicies, err := discoverGlobalPolicies(ctx, cli, oam.SystemDefinitionNamespace)
if err != nil {
return nil, err
}
// Discover from app namespace (if different)
var namespacePolicies []v1beta1.PolicyDefinition
if appNamespace != oam.SystemDefinitionNamespace {
namespacePolicies, err = discoverGlobalPolicies(ctx, cli, appNamespace)
if err != nil {
// Non-fatal, continue with vela-system policies only
namespacePolicies = []v1beta1.PolicyDefinition{}
}
}
// Deduplicate: namespace policies win
namespacePolicyNames := make(map[string]bool)
for _, policy := range namespacePolicies {
namespacePolicyNames[policy.Name] = true
}
// Add namespace policies first
globalPolicies = append(globalPolicies, namespacePolicies...)
// Add vela-system policies (skip if name exists in namespace)
for _, policy := range velaSystemPolicies {
if !namespacePolicyNames[policy.Name] {
globalPolicies = append(globalPolicies, policy)
}
}
return globalPolicies, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
/*
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 application
import (
"fmt"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/pkg/errors"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
)
// PolicyValidationResult contains validation errors and warnings
type PolicyValidationResult struct {
Errors []string
Warnings []string
}
// IsValid returns true if there are no errors
func (r *PolicyValidationResult) IsValid() bool {
return len(r.Errors) == 0
}
// ValidatePolicyDefinition validates a PolicyDefinition
// Returns validation result with errors (blocking) and warnings (informational)
func ValidatePolicyDefinition(policy *v1beta1.PolicyDefinition) *PolicyValidationResult {
result := &PolicyValidationResult{
Errors: []string{},
Warnings: []string{},
}
// Only validate if policy has a schematic
if policy.Spec.Schematic == nil || policy.Spec.Schematic.CUE == nil {
result.Errors = append(result.Errors, "policy must have a CUE schematic")
return result
}
// Validate global policies have specific requirements
if policy.Spec.Global {
// Rule 1: No required parameters
if err := validateNoRequiredParameters(policy); err != nil {
result.Errors = append(result.Errors, err.Error())
}
// Rule 2: Scope must be Application
if policy.Spec.Scope != v1beta1.ApplicationScope {
result.Errors = append(result.Errors,
fmt.Sprintf("global policies must have scope='Application', found scope='%s'", policy.Spec.Scope))
}
// Rule 3 (Warning): Priority should be explicit
if policy.Spec.Priority == 0 {
result.Warnings = append(result.Warnings,
"global policy should have explicit priority field (defaults to 0)")
}
// Rule 4 (Warning): Priority range recommendation
if policy.Spec.Priority > 2000 {
result.Warnings = append(result.Warnings,
fmt.Sprintf("priority %d is unusually high (recommended ranges: 1000-1999=security, 500-999=standard, 100-499=optional, 0-99=low)",
policy.Spec.Priority))
}
}
// Rule 5: CUE schema validation (all policies)
if err := validateCUESchema(policy); err != nil {
result.Errors = append(result.Errors, err.Error())
}
return result
}
// validateNoRequiredParameters checks that all parameters have default values
// For global policies, ALL parameters must have defaults since users can't provide values
func validateNoRequiredParameters(policy *v1beta1.PolicyDefinition) error {
cueTemplate := policy.Spec.Schematic.CUE.Template
// Parse CUE template
ctx := cuecontext.New()
value := ctx.CompileString(cueTemplate)
if value.Err() != nil {
return errors.Wrap(value.Err(), "failed to compile CUE template")
}
// Look up the parameter field
paramField := value.LookupPath(cue.ParsePath("parameter"))
if !paramField.Exists() {
// No parameter field is fine
return nil
}
// Check if parameter is just empty struct
fields, err := paramField.Fields(cue.All())
if err != nil {
return errors.Wrap(err, "failed to inspect parameter fields")
}
// Collect parameters without defaults (both required AND optional without defaults)
var paramsWithoutDefaults []string
for fields.Next() {
fieldInfo := fields.Selector()
fieldValue := fields.Value()
// Check if field has a default value (has * marker)
_, hasDefault := fieldValue.Default()
if !hasDefault {
// No default = can't compile when auto-applied
paramsWithoutDefaults = append(paramsWithoutDefaults, fieldInfo.String())
}
}
if len(paramsWithoutDefaults) > 0 {
return fmt.Errorf("global policy '%s' cannot have parameters without default values. Found parameters without defaults: %v. "+
"Global policies are auto-applied without user input, so all parameters must have default values using '*'. "+
"Example: envName: *\"production\" | string",
policy.Name, paramsWithoutDefaults)
}
return nil
}
// validateCUESchema validates the CUE template conforms to expected schema
func validateCUESchema(policy *v1beta1.PolicyDefinition) error {
cueTemplate := policy.Spec.Schematic.CUE.Template
// Parse CUE template
ctx := cuecontext.New()
value := ctx.CompileString(cueTemplate)
if value.Err() != nil {
return errors.Wrap(value.Err(), "CUE template syntax error")
}
// Validate 'enabled' field (must be bool or default to true)
enabledField := value.LookupPath(cue.ParsePath("enabled"))
if enabledField.Exists() {
// Check if it unifies with bool
boolType := ctx.CompileString("bool")
unified := enabledField.Unify(boolType)
if unified.Err() != nil {
return errors.New("'enabled' field must be of type bool")
}
}
// Validate 'transforms' field structure
transformsField := value.LookupPath(cue.ParsePath("transforms"))
if transformsField.Exists() {
if err := validateTransforms(ctx, transformsField); err != nil {
return err
}
}
return nil
}
// validateTransforms validates the transforms field structure
func validateTransforms(ctx *cue.Context, transforms cue.Value) error {
// Check labels transform (if exists)
labelsTransform := transforms.LookupPath(cue.ParsePath("labels"))
if labelsTransform.Exists() {
if err := validateMergeOnlyTransform(labelsTransform, "labels"); err != nil {
return err
}
}
// Check annotations transform (if exists)
annotationsTransform := transforms.LookupPath(cue.ParsePath("annotations"))
if annotationsTransform.Exists() {
if err := validateMergeOnlyTransform(annotationsTransform, "annotations"); err != nil {
return err
}
}
// Check spec transform (if exists)
specTransform := transforms.LookupPath(cue.ParsePath("spec"))
if specTransform.Exists() {
if err := validateSpecTransform(specTransform); err != nil {
return err
}
}
return nil
}
// validateMergeOnlyTransform validates that labels/annotations only use "merge" type
func validateMergeOnlyTransform(transform cue.Value, fieldName string) error {
typeField := transform.LookupPath(cue.ParsePath("type"))
if !typeField.Exists() {
return fmt.Errorf("transforms.%s must have 'type' field", fieldName)
}
typeStr, err := typeField.String()
if err != nil {
return fmt.Errorf("transforms.%s.type must be a string", fieldName)
}
if typeStr != "merge" {
return fmt.Errorf("transforms.%s.type must be 'merge' (not '%s') to prevent removing critical platform metadata",
fieldName, typeStr)
}
// Check value field exists
valueField := transform.LookupPath(cue.ParsePath("value"))
if !valueField.Exists() {
return fmt.Errorf("transforms.%s must have 'value' field", fieldName)
}
return nil
}
// validateSpecTransform validates spec transform type is "merge" or "replace"
func validateSpecTransform(transform cue.Value) error {
typeField := transform.LookupPath(cue.ParsePath("type"))
if !typeField.Exists() {
return errors.New("transforms.spec must have 'type' field")
}
typeStr, err := typeField.String()
if err != nil {
return errors.New("transforms.spec.type must be a string")
}
if typeStr != "merge" && typeStr != "replace" {
return fmt.Errorf("transforms.spec.type must be 'merge' or 'replace' (found '%s')", typeStr)
}
// Check value field exists
valueField := transform.LookupPath(cue.ParsePath("value"))
if !valueField.Exists() {
return errors.New("transforms.spec must have 'value' field")
}
return nil
}

View File

@@ -0,0 +1,326 @@
/*
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 application
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
)
var _ = Describe("Test PolicyDefinition Validation", func() {
It("Test valid global policy passes validation", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 100,
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {}
enabled: true
output: {
labels: {
"test": "value"
}
}
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeTrue())
Expect(result.Errors).Should(BeEmpty())
})
It("Test global policy with required parameter fails validation", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 100,
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {
envName: string // Required field - no default!
}
output: {
labels: {
"env": parameter.envName
}
}
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeFalse())
Expect(result.Errors).Should(HaveLen(1))
Expect(result.Errors[0]).Should(ContainSubstring("without default values"))
Expect(result.Errors[0]).Should(ContainSubstring("envName"))
})
It("Test global policy with optional parameter without default fails validation", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 100,
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {
envName?: string // Optional but no default - can't compile!
}
output: {
labels: {
"env": parameter.envName
}
}
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeFalse())
Expect(result.Errors).Should(HaveLen(1))
Expect(result.Errors[0]).Should(ContainSubstring("without default values"))
Expect(result.Errors[0]).Should(ContainSubstring("envName"))
})
It("Test global policy with default parameters passes validation", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 100,
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {
envName: *"production" | string // Has default
replicas: *3 | int // Has default
}
output: {
labels: {
"env": parameter.envName
"replicas": "\(parameter.replicas)"
}
}
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeTrue())
Expect(result.Errors).Should(BeEmpty())
})
It("Test global policy with empty parameter block passes validation", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 100,
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {} // Empty is fine
output: {
labels: {
"static": "value"
}
}
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeTrue())
Expect(result.Errors).Should(BeEmpty())
})
It("Test global policy with wrong scope fails validation", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 100,
Scope: "WorkflowStep", // Wrong scope!
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {}
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeFalse())
Expect(result.Errors).Should(ContainElement(ContainSubstring("scope='Application'")))
})
It("Test global policy without explicit priority gets warning", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
// Priority not set (defaults to 0)
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `parameter: {}`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeTrue())
Expect(result.Warnings).Should(HaveLen(1))
Expect(result.Warnings[0]).Should(ContainSubstring("explicit priority"))
})
It("Test global policy with very high priority gets warning", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 5000, // Unusually high
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `parameter: {}`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeTrue())
Expect(result.Warnings).Should(ContainElement(ContainSubstring("unusually high")))
})
It("Test policy with invalid CUE syntax fails validation", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 100,
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {
this is not valid CUE syntax!!!
}
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeFalse())
Expect(result.Errors).Should(ContainElement(ContainSubstring("syntax error")))
})
It("Test enabled field must be bool", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {}
enabled: "true" // Invalid! Should be bool, not string
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeFalse())
Expect(result.Errors).Should(ContainElement(ContainSubstring("'enabled' field must be of type bool")))
})
It("Test non-global policy with required parameters is allowed", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: false, // Not global
Priority: 100,
Scope: v1beta1.ApplicationScope,
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
parameter: {
envName: string // Required is OK for non-global policies
}
output: {
labels: {
"env": parameter.envName
}
}
`,
},
},
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeTrue())
Expect(result.Errors).Should(BeEmpty())
})
It("Test policy without schematic fails validation", func() {
policy := &v1beta1.PolicyDefinition{
Spec: v1beta1.PolicyDefinitionSpec{
Global: true,
Priority: 100,
Scope: v1beta1.ApplicationScope,
// No schematic!
},
}
result := ValidatePolicyDefinition(policy)
Expect(result.IsValid()).Should(BeFalse())
Expect(result.Errors).Should(ContainElement(ContainSubstring("must have a CUE schematic")))
})
})

View File

@@ -18,6 +18,7 @@ package application
import (
"context"
"encoding/json"
"sort"
"github.com/hashicorp/go-version"
@@ -130,6 +131,21 @@ func (h *AppHandler) gatherRevisionSpec(af *appfile.Appfile) (*v1beta1.Applicati
copiedApp := h.app.DeepCopy()
// We better to remove all object status in the appRevision
copiedApp.Status = common.AppStatus{}
// Normalize component properties (RawExtension) to ensure consistent JSON encoding
// This prevents spurious revision creation due to JSON field order/formatting differences
for i := range copiedApp.Spec.Components {
if copiedApp.Spec.Components[i].Properties != nil && copiedApp.Spec.Components[i].Properties.Raw != nil {
// Decode and re-encode to normalize JSON
var obj map[string]interface{}
if err := json.Unmarshal(copiedApp.Spec.Components[i].Properties.Raw, &obj); err == nil {
if normalized, err := json.Marshal(obj); err == nil {
copiedApp.Spec.Components[i].Properties.Raw = normalized
}
}
}
}
appRev := &v1beta1.ApplicationRevision{
Spec: v1beta1.ApplicationRevisionSpec{
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
@@ -143,6 +159,17 @@ func (h *AppHandler) gatherRevisionSpec(af *appfile.Appfile) (*v1beta1.Applicati
},
},
}
// If af is nil, skip gathering definitions from appfile
// This can happen in tests that only need to test the JSON normalization behavior
if af == nil {
hash, err := ComputeAppRevisionHash(appRev)
if err != nil {
return nil, "", errors.Wrapf(err, "failed to compute hash for application revision")
}
return appRev, hash, nil
}
for _, w := range af.ParsedComponents {
if w == nil {
continue
@@ -346,9 +373,11 @@ func (h *AppHandler) currentAppRevIsNew(ctx context.Context) (bool, bool, error)
for _, _rev := range revs {
rev := _rev.DeepCopy()
if rev.GetLabels()[oam.LabelAppRevisionHash] == h.currentRevHash &&
DeepEqualRevision(rev, h.currentAppRev) &&
oam.GetPublishVersion(rev) == oam.GetPublishVersion(h.app) {
hashMatches := rev.GetLabels()[oam.LabelAppRevisionHash] == h.currentRevHash
deepEqual := DeepEqualRevision(rev, h.currentAppRev)
publishVersionMatches := oam.GetPublishVersion(rev) == oam.GetPublishVersion(h.app)
if hashMatches && deepEqual && publishVersionMatches {
// we set currentAppRev to existRevision
h.currentAppRev = rev
return true, false, nil
@@ -375,17 +404,20 @@ func DeepEqualRevision(old, new *v1beta1.ApplicationRevision) bool {
return false
}
for key, wd := range new.Spec.WorkloadDefinitions {
if !apiequality.Semantic.DeepEqual(old.Spec.WorkloadDefinitions[key].Spec, wd.Spec) {
oldWd, exists := old.Spec.WorkloadDefinitions[key]
if !exists || !apiequality.Semantic.DeepEqual(oldWd.Spec, wd.Spec) {
return false
}
}
for key, cd := range new.Spec.ComponentDefinitions {
if !apiequality.Semantic.DeepEqual(old.Spec.ComponentDefinitions[key].Spec, cd.Spec) {
oldCd, exists := old.Spec.ComponentDefinitions[key]
if !exists || !apiequality.Semantic.DeepEqual(oldCd.Spec, cd.Spec) {
return false
}
}
for key, td := range newTraitDefinitions {
if !apiequality.Semantic.DeepEqual(oldTraitDefinitions[key].Spec, td.Spec) {
oldTd, exists := oldTraitDefinitions[key]
if !exists || !apiequality.Semantic.DeepEqual(oldTd.Spec, td.Spec) {
return false
}
}
@@ -507,6 +539,7 @@ func (h *AppHandler) UpdateAppLatestRevisionStatus(ctx context.Context, patchSta
// skip update if app revision is not changed
return nil
}
if ctx, ok := ctx.(monitorContext.Context); ok {
subCtx := ctx.Fork("update-apprev-status", monitorContext.DurationMetric(func(v float64) {
metrics.AppReconcileStageDurationHistogram.WithLabelValues("update-apprev-status").Observe(v)
@@ -520,13 +553,23 @@ func (h *AppHandler) UpdateAppLatestRevisionStatus(ctx context.Context, patchSta
Revision: int64(revNum),
RevisionHash: h.currentRevHash,
}
// Save the spec before patchStatus - the merge patch operation refreshes the entire app from API server
// This would lose any in-memory policy modifications to app.Spec
savedSpec := h.app.Spec.DeepCopy()
if err := patchStatus(ctx, h.app, common.ApplicationRendering); err != nil {
klog.InfoS("Failed to update the latest appConfig revision to status", "application", klog.KObj(h.app),
"latest revision", revName, "err", err)
return err
}
// Restore the spec after patchStatus to preserve policy modifications
h.app.Spec = *savedSpec
klog.InfoS("Successfully update application latest revision status", "application", klog.KObj(h.app),
"latest revision", revName)
return nil
}

View File

@@ -49,8 +49,11 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/appfile"
_ "github.com/oam-dev/kubevela/pkg/features" // Import to register feature gates
"github.com/oam-dev/kubevela/pkg/multicluster"
// +kubebuilder:scaffold:imports
)
@@ -77,6 +80,12 @@ func TestAPIs(t *testing.T) {
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
rand.Seed(time.Now().UnixNano())
// Enable both Application-scoped policy feature gates for tests
Expect(utilfeature.DefaultMutableFeatureGate.Set("EnableGlobalPolicies=true")).ToNot(HaveOccurred())
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
if _, set := os.LookupEnv("COMPATIBILITY_TEST"); set {

View File

@@ -24,6 +24,7 @@ import (
"github.com/kubevela/workflow/pkg/cue/process"
"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/apis/types"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -48,13 +49,34 @@ type ContextData struct {
AppLabels map[string]string
AppAnnotations map[string]string
AppComponents []common.ApplicationComponent
AppWorkflow *v1beta1.Workflow
AppPolicies []v1beta1.AppPolicy
ClusterVersion types.ClusterVersion
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 +86,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)
@@ -71,6 +94,9 @@ func NewContext(data ContextData) process.Context {
ctx.PushData(ContextComponents, data.Components)
ctx.PushData(ContextAppLabels, data.AppLabels)
ctx.PushData(ContextAppAnnotations, data.AppAnnotations)
ctx.PushData(ContextAppComponents, data.AppComponents)
ctx.PushData(ContextAppWorkflow, data.AppWorkflow)
ctx.PushData(ContextAppPolicies, data.AppPolicies)
ctx.PushData(ContextReplicaKey, data.ReplicaKey)
revNum, _ := util.ExtractRevisionNum(data.AppRevisionName, "-")
ctx.PushData(ContextAppRevisionNum, revNum)

View File

@@ -35,6 +35,12 @@ const (
ContextAppLabels = "appLabels"
// ContextAppAnnotations is the annotations of app of context
ContextAppAnnotations = "appAnnotations"
// ContextAppComponents is the components array of the app
ContextAppComponents = "appComponents"
// ContextAppWorkflow is the workflow object of the app
ContextAppWorkflow = "appWorkflow"
// ContextAppPolicies is the policies array of the app
ContextAppPolicies = "appPolicies"
// ContextNamespace is the namespace of the app
ContextNamespace = "namespace"
// ContextCluster is the cluster currently focusing on

View File

@@ -123,6 +123,16 @@ const (
// ValidateResourcesExist enables webhook validation to check if resource types referenced in
// ComponentDefinition/TraitDefinition/WorkflowStepDefinition/PolicyDefinition CUE templates exist in the cluster
ValidateResourcesExist = "ValidateResourcesExist"
// 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{
@@ -151,6 +161,8 @@ var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
EnableCueValidation: {Default: false, PreRelease: featuregate.Beta},
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() {

View File

@@ -159,6 +159,11 @@ const (
// AnnotationAutoUpdate is annotation that let application auto update when it finds definition changes
AnnotationAutoUpdate = "app.oam.dev/autoUpdate"
// AnnotationAutoRevision controls whether policy-rendered spec changes create new ApplicationRevisions.
// When set to "true", policies can modify Application.Spec and trigger new revisions.
// This is orthogonal to AnnotationAutoUpdate which controls definition version updates.
AnnotationAutoRevision = "policy.oam.dev/autoRevision"
// AnnotationWorkflowName specifies the workflow name for execution.
AnnotationWorkflowName = "app.oam.dev/workflowName"

176
pkg/oam/sedyYAJzl Normal file
View File

@@ -0,0 +1,176 @@
/*
Copyright 2019 The Crossplane 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 oam
// Label key strings.
// AppConfig controller will add these labels into workloads.
const (
// LabelAppName records the name of AppConfig
LabelAppName = "app.oam.dev/name"
// LabelAppRevision records the name of Application, it's equal to name of AppConfig created by Application
LabelAppRevision = "app.oam.dev/appRevision"
// LabelAppComponent records the name of Component
LabelAppComponent = "app.oam.dev/component"
// LabelReplicaKey records the replica key of Component
LabelReplicaKey = "app.oam.dev/replicaKey"
// LabelAppComponentRevision records the revision name of Component
LabelAppComponentRevision = "app.oam.dev/revision"
// LabelOAMResourceType whether a CR is workload or trait
LabelOAMResourceType = "app.oam.dev/resourceType"
// LabelAppRevisionHash records the Hash value of the application revision
LabelAppRevisionHash = "app.oam.dev/app-revision-hash"
// LabelAppNamespace records the namespace of Application
LabelAppNamespace = "app.oam.dev/namespace"
// LabelAppCluster records the cluster of Application
LabelAppCluster = "app.oam.dev/cluster"
// LabelAppUID records the uid of Application
LabelAppUID = "app.oam.dev/uid"
// WorkloadTypeLabel indicates the type of the workloadDefinition
WorkloadTypeLabel = "workload.oam.dev/type"
// TraitTypeLabel indicates the type of the traitDefinition
TraitTypeLabel = "trait.oam.dev/type"
// TraitResource indicates which resource it is when a trait is composed by multiple resources in KubeVela
TraitResource = "trait.oam.dev/resource"
// LabelComponentDefinitionName records the name of ComponentDefinition
LabelComponentDefinitionName = "componentdefinition.oam.dev/name"
// LabelTraitDefinitionName records the name of TraitDefinition
LabelTraitDefinitionName = "trait.oam.dev/name"
// LabelManageWorkloadTrait indicates if the trait will manage the lifecycle of the workload
LabelManageWorkloadTrait = "trait.oam.dev/manage-workload"
// LabelPolicyDefinitionName records the name of PolicyDefinition
LabelPolicyDefinitionName = "policydefinition.oam.dev/name"
// LabelWorkflowStepDefinitionName records the name of WorkflowStepDefinition
LabelWorkflowStepDefinitionName = "workflowstepdefinition.oam.dev/name"
// LabelControllerRevisionComponent indicate which component the revision belong to
LabelControllerRevisionComponent = "controller.oam.dev/component"
// LabelAddonName indicates the name of the corresponding Addon
LabelAddonName = "addons.oam.dev/name"
// LabelAddonAuxiliaryName indicates the name of the auxiliary resource in addon app template
LabelAddonAuxiliaryName = "addons.oam.dev/auxiliary-name"
// LabelAddonVersion indicates the version of the corresponding installed Addon
LabelAddonVersion = "addons.oam.dev/version"
// LabelAddonRegistry indicates the name of addon-registry
LabelAddonRegistry = "addons.oam.dev/registry"
// LabelNamespaceOfEnvName records the env name of namespace
LabelNamespaceOfEnvName = "namespace.oam.dev/env"
// LabelNamespaceOfTargetName records the target name of namespace
LabelNamespaceOfTargetName = "namespace.oam.dev/target"
// LabelControlPlaneNamespaceUsage mark the usage of the namespace in control plane cluster.
LabelControlPlaneNamespaceUsage = "usage.oam.dev/control-plane"
// LabelRuntimeNamespaceUsage mark the usage of the namespace in runtime cluster.
// A control plane cluster can also be used as runtime cluster
LabelRuntimeNamespaceUsage = "usage.oam.dev/runtime"
// LabelConfigType means the config type
LabelConfigType = "config.oam.dev/type"
// LabelProject recorde the project the resource belong to
LabelProject = "core.oam.dev/project"
// LabelResourceRules defines the configmap is representing the resource topology rules
LabelResourceRules = "rules.oam.dev/resources"
// LabelResourceRuleFormat defines the resource format of the resource topology rules
LabelResourceRuleFormat = "rules.oam.dev/resource-format"
// LabelControllerName indicates the controller name
LabelControllerName = "controller.oam.dev/name"
// LabelPreCheck indicates if the target resource is for pre-check test
LabelPreCheck = "core.oam.dev/pre-check"
)
const (
// VelaNamespaceUsageEnv mark the usage of the namespace is used by env.
VelaNamespaceUsageEnv = "env"
// VelaNamespaceUsageTarget mark the usage of the namespace is used as delivery target.
VelaNamespaceUsageTarget = "target"
)
const (
// ResourceTypeTrait mark this K8s Custom Resource is an OAM trait
ResourceTypeTrait = "TRAIT"
// ResourceTypeWorkload mark this K8s Custom Resource is an OAM workload
ResourceTypeWorkload = "WORKLOAD"
)
const (
// AnnotationLastAppliedConfig records the previous configuration of a
// resource for use in a three-way diff during a patching apply
AnnotationLastAppliedConfig = "app.oam.dev/last-applied-configuration"
// AnnotationLastAppliedTime indicates the last applied time
AnnotationLastAppliedTime = "app.oam.dev/last-applied-time"
// AnnotationInplaceUpgrade indicates the workload should upgrade with the the same name
// the name of the workload instance should not changing along with the revision
AnnotationInplaceUpgrade = "app.oam.dev/inplace-upgrade"
// AnnotationAppRevision indicates that the object is an application revision
// its controller should not try to reconcile it
AnnotationAppRevision = "app.oam.dev/app-revision"
// AnnotationKubeVelaVersion is used to record current KubeVela version
AnnotationKubeVelaVersion = "oam.dev/kubevela-version"
// AnnotationFilterAnnotationKeys is used to filter annotations passed to workload and trait, split by comma
AnnotationFilterAnnotationKeys = "filter.oam.dev/annotation-keys"
// AnnotationFilterLabelKeys is used to filter labels passed to workload and trait, split by comma
AnnotationFilterLabelKeys = "filter.oam.dev/label-keys"
// AnnotationDefinitionRevisionName is used to specify the name of DefinitionRevision in component/trait definition
AnnotationDefinitionRevisionName = "definitionrevision.oam.dev/name"
// AnnotationLastAppliedConfiguration is kubectl annotations for 3-way merge
AnnotationLastAppliedConfiguration = "kubectl.kubernetes.io/last-applied-configuration"
// AnnotationDeployVersion know the version number of the deployment.
AnnotationDeployVersion = "app.oam.dev/deployVersion"
// AnnotationPublishVersion is annotation that record the application workflow version.
AnnotationPublishVersion = "app.oam.dev/publishVersion"
// AnnotationAutoUpdate is annotation that let application auto update when it finds definition changes
AnnotationAutoUpdate = "app.oam.dev/autoUpdate"
// AnnotationWorkflowName specifies the workflow name for execution.
AnnotationWorkflowName = "app.oam.dev/workflowName"
// AnnotationWorkflowRestart triggers a workflow restart when set. Supported values:
// - "true": Immediate restart (sets restart time to current time + 1 second).
// Annotation is automatically removed after being processed.
// - RFC3339 timestamp (e.g., "2025-01-15T14:30:00Z"): One-time restart at specified time.
// Annotation is automatically removed after being processed.
// - Duration (e.g., "5m", "1h", "30s"): Recurring restart with minimum interval after each completion.
// Annotation persists; automatically reschedules after each workflow completion.
// All modes are GitOps-safe: the schedule is stored in status.workflowRestartScheduledAt.
AnnotationWorkflowRestart = "app.oam.dev/restart-workflow"
// AnnotationAppName specifies the name for application in db.
// Note: the annotation is only created by velaUX, plea

View File

@@ -104,6 +104,7 @@ func NewCommandWithIOStreams(ioStream util.IOStreams) *cobra.Command {
NewExecCommand(commandArgs, "5", ioStream),
RevisionCommandGroup(commandArgs, "6"),
NewDebugCommand(commandArgs, "7", ioStream),
PolicyCommandGroup(commandArgs, "8", ioStream),
// Continuous Delivery
NewWorkflowCommand(commandArgs, "1", ioStream),

829
references/cli/policy.go Normal file
View File

@@ -0,0 +1,829 @@
/*
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.
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 cli
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"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/apis/types"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/application"
velacommon "github.com/oam-dev/kubevela/pkg/utils/common"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
)
const (
// Output format constants
outputFormatTable = "table"
outputFormatJSON = "json"
outputFormatYAML = "yaml"
outputFormatSummary = "summary"
outputFormatDiff = "diff"
)
// PolicyCommandGroup creates the `policy` command group
func PolicyCommandGroup(c velacommon.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "policy",
Short: "Manage and debug Application-scoped policies.",
Long: "Commands for viewing and testing Application-scoped PolicyDefinitions (both global and explicit) applied to Applications.",
Annotations: map[string]string{
types.TagCommandType: types.TypeApp,
types.TagCommandOrder: order,
},
}
cmd.AddCommand(
NewPolicyViewCommand(c, ioStreams),
NewPolicyDryRunCommand(c, ioStreams),
)
return cmd
}
// NewPolicyViewCommand creates the `vela policy view` command
func NewPolicyViewCommand(c velacommon.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
var outputFormat string
cmd := &cobra.Command{
Use: "view <app-name>",
Short: "View applied Application-scoped policies and their effects.",
Long: "View which Application-scoped policies (global and explicit) were applied to an Application and what changes they made.",
Example: ` # View policies applied to an Application
vela policy view my-app
# View in JSON format
vela policy view my-app --output json
# View in YAML format
vela policy view my-app --output yaml`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
appName := args[0]
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
return err
}
if namespace == "" {
namespace = "default"
}
ctx := context.Background()
return runPolicyView(ctx, c, appName, namespace, outputFormat, ioStreams)
},
}
addNamespaceAndEnvArg(cmd)
cmd.Flags().StringVarP(&outputFormat, "output", "o", outputFormatTable, "Output format: table, json, yaml")
return cmd
}
// NewPolicyDryRunCommand creates the `vela policy dry-run` command
func NewPolicyDryRunCommand(c velacommon.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
var (
outputFormat string
policies []string
includeGlobalPolicies bool
includeAppPolicies bool
)
cmd := &cobra.Command{
Use: "dry-run <app-name>",
Short: "Preview policy effects before applying.",
Long: `Simulate policy application to see what changes would be made.
Modes:
- Isolated (--policies only): Test specified policies in isolation
- Additive (--policies + --include-global-policies): Test with existing globals
- Full (no --policies): Simulate complete policy chain
- Full + Extra (--policies + both flags): Everything plus test policies`,
Example: ` # Full simulation (all policies that would apply)
vela policy dry-run my-app
# Test specific policy in isolation
vela policy dry-run my-app --policies inject-sidecar
# Test new policy with existing globals
vela policy dry-run my-app --policies new-policy --include-global-policies
# Show only metadata changes (labels, annotations, context)
vela policy dry-run my-app --output summary
# Show unified diff
vela policy dry-run my-app --output diff
# JSON output for CI/CD
vela policy dry-run my-app --output json`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
appName := args[0]
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
return err
}
if namespace == "" {
namespace = "default"
}
ctx := context.Background()
return runPolicyDryRun(ctx, c, appName, namespace, policies, includeGlobalPolicies, includeAppPolicies, outputFormat, ioStreams)
},
}
addNamespaceAndEnvArg(cmd)
cmd.Flags().StringVarP(&outputFormat, "output", "o", outputFormatTable, "Output format: table, summary, diff, json, yaml")
cmd.Flags().StringSliceVar(&policies, "policies", nil, "Comma-separated list of policies to test")
cmd.Flags().BoolVar(&includeGlobalPolicies, "include-global-policies", false, "Include existing global policies")
cmd.Flags().BoolVar(&includeAppPolicies, "include-app-policies", false, "Include policies from Application spec")
return cmd
}
// runPolicyView implements the view command logic
func runPolicyView(ctx context.Context, c velacommon.Args, appName, namespace, outputFormat string, ioStreams cmdutil.IOStreams) error {
cli, err := c.GetClient()
if err != nil {
return err
}
// Get the Application
app := &v1beta1.Application{}
if err := cli.Get(ctx, client.ObjectKey{Name: appName, Namespace: namespace}, app); err != nil {
return errors.Wrapf(err, "failed to get Application %s/%s", namespace, appName)
}
// Check if any policies were applied
if len(app.Status.AppliedApplicationPolicies) == 0 {
ioStreams.Info(fmt.Sprintf("No Application-scoped policies applied to Application '%s'\n", appName))
ioStreams.Info("\nThis could be because:\n")
ioStreams.Info(" • No global policies exist in vela-system or the application namespace\n")
ioStreams.Info(" • No explicit Application-scoped policies in Application spec\n")
ioStreams.Info(" • Application has annotation: policy.oam.dev/skip-global: \"true\"\n")
ioStreams.Info(" • Global policies feature is disabled (feature gate not enabled)\n")
return nil
}
// Get the diffs ConfigMap if it exists
var diffsConfigMap *corev1.ConfigMap
if app.Status.ApplicationPoliciesConfigMap != "" {
cm := &corev1.ConfigMap{}
err := cli.Get(ctx, client.ObjectKey{Name: app.Status.ApplicationPoliciesConfigMap, Namespace: namespace}, cm)
if err == nil {
diffsConfigMap = cm
}
}
// Display based on output format
switch outputFormat {
case outputFormatJSON:
return outputPolicyViewJSON(app, diffsConfigMap, ioStreams)
case outputFormatYAML:
return outputPolicyViewYAML(app, diffsConfigMap, ioStreams)
case outputFormatTable:
return outputPolicyViewTable(app, diffsConfigMap, ioStreams)
default:
return fmt.Errorf("unknown output format: %s (supported: table, json, yaml)", outputFormat)
}
}
// outputPolicyViewTable displays policy view in interactive table format
func outputPolicyViewTable(app *v1beta1.Application, diffsConfigMap *corev1.ConfigMap, ioStreams cmdutil.IOStreams) error {
policies := app.Status.AppliedApplicationPolicies
// Count applied vs skipped
applied := 0
skipped := 0
for _, p := range policies {
if p.Applied {
applied++
} else {
skipped++
}
}
// Display header
ioStreams.Info(fmt.Sprintf("Applied Application Policies: %s applied, %s skipped\n\n",
color.GreenString("%d", applied),
color.YellowString("%d", skipped)))
// Create table
table := tablewriter.NewWriter(ioStreams.Out)
table.SetHeader([]string{"Seq", "Policy", "Namespace", "Source", "Priority", "Applied", "Spec", "Labels", "Annot.", "Context"})
table.SetBorder(true)
table.SetAlignment(tablewriter.ALIGN_LEFT)
for _, policy := range policies {
seq := "-"
source := "-"
priority := "-"
specChanges := "No"
labels := "0"
annot := "0"
ctx := "No"
if policy.Applied {
// Try to get sequence, source, priority from ConfigMap if available
if diffsConfigMap != nil {
for key, value := range diffsConfigMap.Data {
if strings.HasSuffix(key, "-"+policy.Name) {
var record map[string]interface{}
if err := json.Unmarshal([]byte(value), &record); err == nil {
if seqVal, ok := record["sequence"].(float64); ok {
seq = fmt.Sprintf("%.0f", seqVal)
}
if srcVal, ok := record["source"].(string); ok {
source = srcVal
}
if priVal, ok := record["priority"].(float64); ok {
priority = fmt.Sprintf("%.0f", priVal)
}
}
break
}
}
}
// Spec changes (from status summary)
if policy.SpecModified {
specChanges = "Yes"
}
// Labels count (from status summary)
labels = fmt.Sprintf("%d", policy.LabelsCount)
// Annotations count (from status summary)
annot = fmt.Sprintf("%d", policy.AnnotationsCount)
// Context (from status summary)
if policy.HasContext {
ctx = "Yes"
}
}
appliedStatus := "Yes"
if !policy.Applied {
appliedStatus = "No"
}
table.Append([]string{
seq,
policy.Name,
policy.Namespace,
source,
priority,
appliedStatus,
specChanges,
labels,
annot,
ctx,
})
}
table.Render()
// Show skipped policies
var skippedPolicies []common.AppliedApplicationPolicy
for _, p := range policies {
if !p.Applied {
skippedPolicies = append(skippedPolicies, p)
}
}
if len(skippedPolicies) > 0 {
ioStreams.Info(fmt.Sprintf("\nSkipped (%d):\n", len(skippedPolicies)))
for _, p := range skippedPolicies {
ioStreams.Info(fmt.Sprintf(" • %s: %s\n", p.Name, p.Reason))
}
}
// Show summary (using status summary counts)
totalLabels := 0
totalAnnotations := 0
specModCount := 0
contextCount := 0
for _, p := range policies {
if p.Applied {
totalLabels += p.LabelsCount
totalAnnotations += p.AnnotationsCount
if p.SpecModified {
specModCount++
}
if p.HasContext {
contextCount++
}
}
}
ioStreams.Info("\nSummary:\n")
ioStreams.Info(fmt.Sprintf(" Total Policies: %d (%d applied, %d skipped)\n", len(policies), applied, skipped))
ioStreams.Info(fmt.Sprintf(" Spec Changes: %d policies\n", specModCount))
ioStreams.Info(fmt.Sprintf(" Labels Added: %d total\n", totalLabels))
ioStreams.Info(fmt.Sprintf(" Annotations: %d total\n", totalAnnotations))
ioStreams.Info(fmt.Sprintf(" Context Data: %d policies\n", contextCount))
if diffsConfigMap != nil {
ioStreams.Info(fmt.Sprintf("\nView detailed diffs:\n kubectl get configmap %s -o json | jq '.data'\n", app.Status.ApplicationPoliciesConfigMap))
}
return nil
}
// outputPolicyViewJSON outputs policy view in JSON format
func outputPolicyViewJSON(app *v1beta1.Application, diffsConfigMap *corev1.ConfigMap, ioStreams cmdutil.IOStreams) error {
output := map[string]interface{}{
"application": app.Name,
"namespace": app.Namespace,
"appliedPolicies": app.Status.AppliedApplicationPolicies,
"policyDiffsConfigMap": app.Status.ApplicationPoliciesConfigMap,
}
// Add summary
applied := 0
skipped := 0
specMod := 0
totalLabels := 0
totalAnnotations := 0
for _, p := range app.Status.AppliedApplicationPolicies {
if p.Applied {
applied++
if p.SpecModified {
specMod++
}
totalLabels += p.LabelsCount
totalAnnotations += p.AnnotationsCount
} else {
skipped++
}
}
output["summary"] = map[string]interface{}{
"totalDiscovered": len(app.Status.AppliedApplicationPolicies),
"applied": applied,
"skipped": skipped,
"specModifications": specMod,
"labelsAdded": totalLabels,
"annotationsAdded": totalAnnotations,
}
data, err := json.MarshalIndent(output, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal to JSON")
}
ioStreams.Info("%s\n", string(data))
return nil
}
// outputPolicyViewYAML outputs policy view in YAML format
func outputPolicyViewYAML(app *v1beta1.Application, diffsConfigMap *corev1.ConfigMap, ioStreams cmdutil.IOStreams) error {
output := map[string]interface{}{
"application": app.Name,
"namespace": app.Namespace,
"appliedPolicies": app.Status.AppliedApplicationPolicies,
"policyDiffsConfigMap": app.Status.ApplicationPoliciesConfigMap,
}
data, err := yaml.Marshal(output)
if err != nil {
return errors.Wrap(err, "failed to marshal to YAML")
}
ioStreams.Info("%s\n", string(data))
return nil
}
// runPolicyDryRun implements the dry-run command logic
func runPolicyDryRun(ctx context.Context, c velacommon.Args, appName, namespace string, policies []string, includeGlobal, includeApp bool, outputFormat string, ioStreams cmdutil.IOStreams) error {
cli, err := c.GetClient()
if err != nil {
return err
}
// Load the Application from cluster
app := &v1beta1.Application{}
if err := cli.Get(ctx, client.ObjectKey{Name: appName, Namespace: namespace}, app); err != nil {
return errors.Wrapf(err, "failed to get Application %s/%s", namespace, appName)
}
// Determine dry-run mode based on flags
var mode application.PolicyDryRunMode
if len(policies) > 0 && !includeGlobal && !includeApp {
mode = application.DryRunModeIsolated
} else if len(policies) > 0 && includeGlobal {
mode = application.DryRunModeAdditive
} else {
mode = application.DryRunModeFull
}
// Build simulation options
opts := application.PolicyDryRunOptions{
Mode: mode,
SpecifiedPolicies: policies,
IncludeAppPolicies: includeApp || mode == application.DryRunModeFull,
}
// Run simulation
ioStreams.Info("Dry-run Simulation\n")
ioStreams.Info("Application: %s (namespace: %s)\n", appName, namespace)
modeStr := string(mode)
switch mode {
case application.DryRunModeIsolated:
modeStr = "Isolated (testing specified policies only)"
case application.DryRunModeAdditive:
modeStr = "Additive (specified policies + existing global policies)"
case application.DryRunModeFull:
modeStr = "Full simulation (all policies that would apply)"
}
ioStreams.Info("Mode: %s\n\n", modeStr)
result, err := application.SimulatePolicyApplication(ctx, cli, app, opts)
if err != nil {
return errors.Wrap(err, "simulation failed")
}
// Display results based on output format
switch outputFormat {
case outputFormatTable:
return outputDryRunTable(result, ioStreams)
case outputFormatSummary:
return outputDryRunSummary(result, ioStreams)
case outputFormatDiff:
return outputDryRunDiff(result, ioStreams)
case outputFormatJSON:
return outputDryRunJSON(result, ioStreams)
case outputFormatYAML:
return outputDryRunYAML(result, ioStreams)
default:
return fmt.Errorf("unknown output format: %s", outputFormat)
}
}
// outputDryRunTable outputs dry-run results in table format
func outputDryRunTable(result *application.PolicyDryRunResult, ioStreams cmdutil.IOStreams) error {
// Show execution plan
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
ioStreams.Info("Execution Plan\n")
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
if len(result.ExecutionPlan) == 0 {
ioStreams.Info("No policies to apply\n\n")
} else {
for _, step := range result.ExecutionPlan {
ioStreams.Info(" %d. %-30s (%s, priority: %d) [%s]\n",
step.Sequence, step.PolicyName, step.PolicyNamespace, step.Priority, step.Source)
}
ioStreams.Info("\n")
}
// Show warnings and errors
if len(result.Warnings) > 0 {
ioStreams.Info("%s:\n", color.YellowString("Warnings"))
for _, w := range result.Warnings {
ioStreams.Info(" • %s\n", w)
}
ioStreams.Info("\n")
}
if len(result.Errors) > 0 {
ioStreams.Info("%s:\n", color.RedString("Errors"))
for _, e := range result.Errors {
ioStreams.Info(" • %s\n", e)
}
ioStreams.Info("\n")
return nil // Don't continue if there were errors
}
// Apply policies
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
ioStreams.Info("Applying Policies...\n")
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
for _, policyResult := range result.PolicyResults {
ioStreams.Info("[%d/%d] %s\n", policyResult.Sequence, len(result.PolicyResults), policyResult.PolicyName)
if policyResult.Error != "" {
ioStreams.Info(" %s Error: %s\n\n", color.RedString("✗"), policyResult.Error)
continue
}
if !policyResult.Enabled {
ioStreams.Info(" %s Skipped: %s\n\n", color.YellowString("⊘"), policyResult.SkipReason)
continue
}
ioStreams.Info(" %s Policy enabled\n", color.GreenString("✓"))
ioStreams.Info("\n Changes:\n")
specChangesStr := "No"
if policyResult.SpecModified {
specChangesStr = fmt.Sprintf("Yes (see diffs)")
}
ioStreams.Info(" Spec Modified: %s\n", specChangesStr)
ioStreams.Info(" Labels Added: %d\n", len(policyResult.AddedLabels))
if len(policyResult.AddedLabels) > 0 {
for k, v := range policyResult.AddedLabels {
ioStreams.Info(" • %s: %s\n", k, v)
}
}
ioStreams.Info(" Annotations Added: %d\n", len(policyResult.AddedAnnotations))
if len(policyResult.AddedAnnotations) > 0 {
for k, v := range policyResult.AddedAnnotations {
ioStreams.Info(" • %s: %s\n", k, v)
}
}
contextStr := "None"
if policyResult.AdditionalContext != nil && len(policyResult.AdditionalContext.Raw) > 0 {
var contextMap map[string]interface{}
if err := json.Unmarshal(policyResult.AdditionalContext.Raw, &contextMap); err == nil {
var keys []string
for k := range contextMap {
keys = append(keys, k)
}
contextStr = strings.Join(keys, ", ")
}
}
ioStreams.Info(" Context Data: %s\n", contextStr)
ioStreams.Info("\n")
}
// Summary
applied := 0
skipped := 0
specMod := 0
totalLabels := 0
totalAnnotations := 0
for _, pr := range result.PolicyResults {
if pr.Applied {
applied++
if pr.SpecModified {
specMod++
}
totalLabels += len(pr.AddedLabels)
totalAnnotations += len(pr.AddedAnnotations)
} else {
skipped++
}
}
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
ioStreams.Info("Summary\n")
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
ioStreams.Info("Policies Applied: %d\n", applied)
ioStreams.Info("Policies Skipped: %d\n", skipped)
ioStreams.Info("Spec Modifications: %d\n", specMod)
ioStreams.Info("Labels Added: %d\n", totalLabels)
ioStreams.Info("Annotations Added: %d\n", totalAnnotations)
ioStreams.Info("\n")
// Final state
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
ioStreams.Info("Final Application State\n")
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
// Show labels
if len(result.Application.Labels) > 0 {
ioStreams.Info("Labels (%d total):\n", len(result.Application.Labels))
for k, v := range result.Application.Labels {
// Find which policy added this label
var policyName string
for _, pr := range result.PolicyResults {
if pr.AddedLabels != nil {
if _, exists := pr.AddedLabels[k]; exists {
policyName = pr.PolicyName
break
}
}
}
if policyName != "" {
ioStreams.Info(" %-30s %s (%s)\n", k+":", v, policyName)
} else {
ioStreams.Info(" %-30s %s\n", k+":", v)
}
}
} else {
ioStreams.Info("Labels: (none)\n")
}
ioStreams.Info("\n")
// Show annotations
if len(result.Application.Annotations) > 0 {
ioStreams.Info("Annotations (%d total):\n", len(result.Application.Annotations))
for k, v := range result.Application.Annotations {
var policyName string
for _, pr := range result.PolicyResults {
if pr.AddedAnnotations != nil {
if _, exists := pr.AddedAnnotations[k]; exists {
policyName = pr.PolicyName
break
}
}
}
if policyName != "" {
ioStreams.Info(" %-30s %s (%s)\n", k+":", v, policyName)
} else {
ioStreams.Info(" %-30s %s\n", k+":", v)
}
}
} else {
ioStreams.Info("Annotations: (none)\n")
}
ioStreams.Info("\n")
// Show application spec (YAML)
ioStreams.Info("Application Spec:\n")
specYAML, err := yaml.Marshal(result.Application.Spec)
if err != nil {
return errors.Wrap(err, "failed to marshal Application spec")
}
ioStreams.Info("%s\n", string(specYAML))
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
ioStreams.Info("This is a dry-run. No changes were applied to the cluster.\n\n")
return nil
}
// outputDryRunSummary outputs only metadata (labels, annotations, context)
func outputDryRunSummary(result *application.PolicyDryRunResult, ioStreams cmdutil.IOStreams) error {
applied := 0
skipped := 0
for _, pr := range result.PolicyResults {
if pr.Applied {
applied++
} else {
skipped++
}
}
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
ioStreams.Info("Summary\n")
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
ioStreams.Info("Policies Applied: %d\n", applied)
ioStreams.Info("Policies Skipped: %d\n", skipped)
ioStreams.Info("\n")
// Show labels table
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
ioStreams.Info("Labels\n")
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
if len(result.Application.Labels) > 0 {
table := tablewriter.NewWriter(ioStreams.Out)
table.SetHeader([]string{"Key", "Value", "Added By"})
table.SetBorder(true)
for k, v := range result.Application.Labels {
var policyName string
for _, pr := range result.PolicyResults {
if pr.AddedLabels != nil {
if _, exists := pr.AddedLabels[k]; exists {
policyName = pr.PolicyName
break
}
}
}
if policyName == "" {
policyName = "(existing)"
}
table.Append([]string{k, v, policyName})
}
table.Render()
} else {
ioStreams.Info(" (none)\n")
}
ioStreams.Info("\n")
// Show annotations table
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
ioStreams.Info("Annotations\n")
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
if len(result.Application.Annotations) > 0 {
table := tablewriter.NewWriter(ioStreams.Out)
table.SetHeader([]string{"Key", "Value", "Added By"})
table.SetBorder(true)
for k, v := range result.Application.Annotations {
var policyName string
for _, pr := range result.PolicyResults {
if pr.AddedAnnotations != nil {
if _, exists := pr.AddedAnnotations[k]; exists {
policyName = pr.PolicyName
break
}
}
}
if policyName == "" {
policyName = "(existing)"
}
table.Append([]string{k, v, policyName})
}
table.Render()
} else {
ioStreams.Info(" (none)\n")
}
ioStreams.Info("\n")
return nil
}
// outputDryRunDiff outputs unified diff format
func outputDryRunDiff(result *application.PolicyDryRunResult, ioStreams cmdutil.IOStreams) error {
// TODO: Implement unified diff output
// For now, just show JSON patches
ioStreams.Info("Policy Diffs:\n\n")
if len(result.Diffs) == 0 {
ioStreams.Info("No spec modifications\n")
return nil
}
for policyName, diff := range result.Diffs {
ioStreams.Info("Policy: %s\n", policyName)
ioStreams.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
ioStreams.Info("%s\n\n", string(diff))
}
return nil
}
// outputDryRunJSON outputs JSON format
func outputDryRunJSON(result *application.PolicyDryRunResult, ioStreams cmdutil.IOStreams) error {
output := map[string]interface{}{
"application": result.Application.Name,
"namespace": result.Application.Namespace,
"executionPlan": result.ExecutionPlan,
"policyResults": result.PolicyResults,
"warnings": result.Warnings,
"errors": result.Errors,
"finalSpec": result.Application.Spec,
"finalLabels": result.Application.Labels,
"finalAnnotations": result.Application.Annotations,
}
data, err := json.MarshalIndent(output, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal to JSON")
}
ioStreams.Info("%s\n", string(data))
return nil
}
// outputDryRunYAML outputs YAML format
func outputDryRunYAML(result *application.PolicyDryRunResult, ioStreams cmdutil.IOStreams) error {
output := map[string]interface{}{
"application": result.Application.Name,
"namespace": result.Application.Namespace,
"executionPlan": result.ExecutionPlan,
"policyResults": result.PolicyResults,
"warnings": result.Warnings,
"errors": result.Errors,
"finalSpec": result.Application.Spec,
"finalLabels": result.Application.Labels,
"finalAnnotations": result.Application.Annotations,
}
data, err := yaml.Marshal(output)
if err != nil {
return errors.Wrap(err, "failed to marshal to YAML")
}
ioStreams.Info("%s\n", string(data))
return nil
}

View File

@@ -0,0 +1,99 @@
// app-policy-schema.cue defines the standard schema for Application-scoped policies
// Application-scoped policies (scope: "Application") transform the Application CR before parsing
// They use the transforms pattern and don't generate Kubernetes resources
package policy
// ApplicationPolicyTemplate defines the standard structure for Application-scoped policies
// This provides type safety, validation, and defaults for policy templates
#ApplicationPolicyTemplate: {
// Parameter schema defined by each policy
parameter: {...}
// Configuration for policy execution and caching behavior
config: {
// Whether policy is enabled (default: true)
// Can be a boolean literal or an expression (e.g., parameter.enabled)
enabled: *true | bool
// Per-output-type refresh control for caching
// Controls when each output type should be re-rendered vs cached
refresh?: {
// Spec refresh control (components, workflow, policies)
// These are structural changes that affect Application behavior
// Default mode: "never" - only refresh when Application revision changes
spec?: {
// Refresh mode: when to re-render this output type
// - "never": Cache indefinitely (until Application revision changes)
// - "always": Re-render on every reconciliation
// - "periodic": Re-render after interval seconds
mode: *"never" | "always" | "periodic"
// Interval in seconds (required if mode == "periodic")
if mode == "periodic" {
interval: int & >0
}
// Force refresh expression (optional)
// Dynamic boolean expression to force cache invalidation
// Example: context.appLabels["force-refresh"] != _|_
forceRefresh?: bool
}
// Labels refresh control (metadata labels)
// Default mode: "never" - labels are usually static
labels?: {
mode: *"never" | "always" | "periodic"
if mode == "periodic" {
interval: int & >0
}
forceRefresh?: bool
}
// Annotations refresh control (metadata annotations)
// Default mode: "never" - annotations are usually static
annotations?: {
mode: *"never" | "always" | "periodic"
if mode == "periodic" {
interval: int & >0
}
forceRefresh?: bool
}
// Context refresh control (additional workflow context)
// Default mode: "never" - context is usually static
ctx?: {
mode: *"never" | "always" | "periodic"
if mode == "periodic" {
interval: int & >0
}
forceRefresh?: bool
}
}
}
// Policy output structure
// Defines the transformations to apply to the Application
output: {
// Structural changes (require Application revision for changes)
// These modify the Application's components, workflow, or policies
components?: [...#Component]
workflow?: #Workflow
policies?: [...#Policy]
// Metadata changes (can refresh between reconciliations if configured)
// Labels and annotations applied to the Application
labels?: {[string]: string}
annotations?: {[string]: string}
// Additional context passed to workflow execution
// This is available to workflow steps via context
ctx?: {[string]: _}
}
}
// Placeholder definitions - these should reference the actual KubeVela types
// In practice, these would import from the actual Application schema
#Component: {...}
#Workflow: {...}
#Policy: {...}