mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-23 22:33:58 +00:00
Compare commits
21 Commits
master
...
checkpoint
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d160cdf84 | ||
|
|
03a91c9fb1 | ||
|
|
06e1d20a74 | ||
|
|
4435c3bb14 | ||
|
|
3d8440bc45 | ||
|
|
a83432e98c | ||
|
|
68805310e3 | ||
|
|
83675a1aae | ||
|
|
0f5add7902 | ||
|
|
5330c0f0fe | ||
|
|
deabee9714 | ||
|
|
5a3be983dc | ||
|
|
738ddfd98e | ||
|
|
bf8d128128 | ||
|
|
f3b67e79ed | ||
|
|
f36017dfa5 | ||
|
|
566b72b882 | ||
|
|
d8562f1c2c | ||
|
|
32d166c219 | ||
|
|
bfa143297b | ||
|
|
32ac0d69c7 |
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
0
cmd/core/app/config/sedCGWemX
Normal file
0
cmd/core/app/config/sedCGWemX
Normal file
678
devlogs/2026-02-17-application-scoped-policies.md
Normal file
678
devlogs/2026-02-17-application-scoped-policies.md
Normal 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
|
||||
482
docs/policy-validation-requirements.md
Normal file
482
docs/policy-validation-requirements.md
Normal 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
330
docs/spec-diff-proposal.md
Normal 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
|
||||
399
examples/policy-transforms/CLI-DESIGN.md
Normal file
399
examples/policy-transforms/CLI-DESIGN.md
Normal 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
|
||||
1083
examples/policy-transforms/CLI-OUTPUT-EXAMPLES.md
Normal file
1083
examples/policy-transforms/CLI-OUTPUT-EXAMPLES.md
Normal file
File diff suppressed because it is too large
Load Diff
534
examples/policy-transforms/OBSERVABILITY.md
Normal file
534
examples/policy-transforms/OBSERVABILITY.md
Normal 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.
|
||||
348
examples/policy-transforms/PRODUCTION-CONSIDERATIONS.md
Normal file
348
examples/policy-transforms/PRODUCTION-CONSIDERATIONS.md
Normal 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)
|
||||
396
examples/policy-transforms/README.md
Normal file
396
examples/policy-transforms/README.md
Normal 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`
|
||||
20
examples/policy-transforms/application-opt-out-example.yaml
Normal file
20
examples/policy-transforms/application-opt-out-example.yaml
Normal 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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
42
examples/policy-transforms/global-policy-tenant-config.yaml
Normal file
42
examples/policy-transforms/global-policy-tenant-config.yaml
Normal 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"
|
||||
}
|
||||
}
|
||||
33
examples/policy-transforms/policy-definition-add-labels.yaml
Normal file
33
examples/policy-transforms/policy-definition-add-labels.yaml
Normal 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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
2
go.mod
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
365
pkg/controller/core.oam.dev/v1beta1/application/policy_dryrun.go
Normal file
365
pkg/controller/core.oam.dev/v1beta1/application/policy_dryrun.go
Normal 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
|
||||
}
|
||||
2252
pkg/controller/core.oam.dev/v1beta1/application/policy_transforms.go
Normal file
2252
pkg/controller/core.oam.dev/v1beta1/application/policy_transforms.go
Normal file
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
@@ -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
|
||||
}
|
||||
@@ -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")))
|
||||
})
|
||||
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
176
pkg/oam/sedyYAJzl
Normal 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
|
||||
@@ -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
829
references/cli/policy.go
Normal 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
|
||||
}
|
||||
@@ -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: {...}
|
||||
Reference in New Issue
Block a user