Mutation Application
Overview
The PackageVariant controller applies mutations to downstream package revisions to customize them based on the PackageVariant specification. Mutations are systematic transformations applied to package resources after cloning or upgrading from upstream, enabling configuration injection, function pipeline modification, and dynamic resource generation.
High-Level Architecture
┌─────────────────────────────────────────────────────────┐
│ Mutation Application System │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Package │ │ KRM │ │
│ │ Context │ ───> │ Functions │ │
│ │ │ │ │ │
│ │ • ConfigMap │ │ • Prepend │ │
│ │ Data │ │ • Naming │ │
│ │ • Add/Remove │ │ • Pipeline │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ └────────┬────────────────┘ │
│ ↓ │
│ ┌──────────────────┐ │
│ │ Config │ │
│ │ Injection │ │
│ │ │ │
│ │ • Annotation │ │
│ │ • Selector │ │
│ │ • Field Copy │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
Mutation Orchestration
Mutations are applied in a specific order during package revision processing:
Application Flow
calculateDraftResources()
↓
Fetch PackageRevisionResources
↓
Store Original Resources
↓
Apply Mutations in Order:
↓
1. ensurePackageContext()
↓
2. ensureKRMFunctions()
↓
3. ensureConfigInjection()
↓
Compare Original vs Modified
↓
Changed? ──Yes──> Update PackageRevisionResources
│
No
↓
Return Unchanged
Process characteristics:
- Sequential application: Mutations applied in fixed order
- Change detection: Compares original and modified resources
- Idempotent: Safe to apply multiple times
- Atomic: All mutations succeed or none applied
When mutations applied:
- After clone draft creation (initial package creation)
- After upgrade draft creation (upstream version change)
- After edit draft creation (mutation-only changes)
- During reconciliation when mutations changed
Package Context Injection
Injects or modifies data in the package-context.yaml ConfigMap:
Injection Process
ensurePackageContext()
↓
PackageContext specified? ──No──> Skip
│
Yes
↓
Data or RemoveKeys present? ──No──> Skip
│
Yes
↓
Locate package-context.yaml
↓
Parse as ConfigMap
↓
Extract data field
↓
Add/Update Keys from Data
↓
Remove Keys from RemoveKeys
↓
Update ConfigMap
↓
Write to Resources
Process steps:
- Check specification: Skip if PackageContext nil or empty
- Locate ConfigMap: Find package-context.yaml in package resources
- Parse ConfigMap: Extract as KubeObject with validation
- Extract data field: Get existing data map from ConfigMap
- Apply additions: Add or update keys from PackageContext.Data
- Apply removals: Delete keys listed in PackageContext.RemoveKeys
- Update ConfigMap: Set modified data field
- Write resources: Update package-context.yaml in resources map
Data Manipulation
Adding or updating keys:
- Keys from PackageContext.Data merged into ConfigMap data
- Existing keys overwritten with new values
- New keys added to data map
- Preserves other existing keys
Removing keys:
- Keys listed in PackageContext.RemoveKeys deleted from data
- Non-existent keys ignored (no error)
- Removal happens after additions (additions take precedence)
Reserved keys:
- “name” and “package-path” are reserved
- Cannot be specified in Data or RemoveKeys
- Validation rejects PackageVariants with these keys
- These keys auto-generated by other mechanisms
Error Handling
Validation errors:
- ConfigMap not found in package resources
- Invalid ConfigMap structure (not a ConfigMap kind)
- Missing data field in ConfigMap
- Invalid data field type (not a map)
Error behavior:
- Errors returned immediately
- Mutation application stops
- Draft not updated
- Reconciliation fails with error condition
KRM Function Injection
Prepends KRM functions to the Kptfile pipeline:
Injection Process
ensureKRMFunctions()
↓
Parse Kptfile
↓
Extract pipeline.mutators
↓
Extract pipeline.validators
↓
For Each Field (mutators, validators):
↓
Remove Old PV Functions
↓
Generate New PV Functions
↓
Prepend to Field
↓
Update Kptfile
↓
Write to Resources
Process steps:
- Parse Kptfile: Extract as KubeObject
- Extract pipeline: Get pipeline.mutators and pipeline.validators arrays
- Remove old functions: Delete functions with PackageVariant naming pattern
- Generate new functions: Create functions from PackageVariant spec
- Assign unique names: Use naming pattern with position
- Prepend functions: Add new functions to start of arrays
- Update Kptfile: Set modified pipeline
- Write resources: Update Kptfile in resources map
Function Naming Pattern
Naming format:
PackageVariant.{pvName}.{funcName}.{position}
Example:
- PackageVariant name: “my-variant”
- Function name: “set-namespace”
- Position: 0 (first function)
- Generated name: “PackageVariant.my-variant.set-namespace.0”
Naming characteristics:
- Prefix: “PackageVariant” identifies controller-managed functions
- PV name: Links function to specific PackageVariant
- Function name: Original function name from spec
- Position: Index in PackageVariant function list
- Uniqueness: Combination ensures unique names
Identification logic:
- Split name by dots (must have exactly 4 segments)
- Check prefix is “PackageVariant”
- Check PV name matches current PackageVariant
- Check position is valid integer
- Used to identify and remove old functions
Function Ordering
Prepending rationale:
New Pipeline Order:
1. PackageVariant functions (prepended)
2. User-defined functions (existing)
3. Other controller functions (existing)
Why prepend:
- PackageVariant functions run first
- Ensures base configuration applied before user functions
- User functions can override if needed
- Consistent ordering across reconciliations
Function removal:
- Old PackageVariant functions removed before prepending
- Prevents duplicate functions
- Allows function list changes
- Maintains clean pipeline
Pipeline Cleanup
Empty field handling:
- If mutators array empty after processing, field removed from pipeline
- If validators array empty after processing, field removed from pipeline
- Avoids dangling empty arrays in Kptfile
- Cleaner YAML output
Empty pipeline handling:
- If both mutators and validators empty, pipeline field removed
- Prevents dangling empty pipeline object
- Kptfile remains valid without pipeline
Config Injection
Injects data from in-cluster Kubernetes resources into package resources:
Injection Process
ensureConfigInjection()
↓
Parse All Package Files
↓
Find Injection Points
↓
• Scan for annotation
• Create injection point objects
↓
Validate Injection Points
↓
• Check duplicates
• Check annotation values
↓
Inject Resources
↓
• Load in-cluster resources
• Match selectors
• Copy allowed fields
↓
Update Modified Files
↓
Set Kptfile Conditions/Gates
Process steps:
- Parse files: Convert all package resources to KubeObjects
- Find injection points: Scan for config-injection annotations
- Validate: Check for duplicates and invalid annotations
- Load in-cluster resources: Query Kubernetes for matching resources
- Match selectors: Find in-cluster resource matching injector spec
- Inject fields: Copy allowed fields from in-cluster to in-package
- Update files: Write modified resources back to package
- Set conditions: Update Kptfile with injection status
Injection Point Discovery
Annotation-based discovery:
- Annotation key: “kpt.dev/config-injection”
- Annotation values: “required” or “optional”
- Scanned across all package resources
- Each annotated resource becomes injection point
Injection point structure:
- file: Filename containing the resource
- object: KubeObject to inject into
- conditionType: Unique condition identifier
- required: Whether injection must succeed
- errors: Validation or injection errors
- injected: Whether injection succeeded
- injectedName: Name of injected resource
Condition type generation:
Format: config.injection.{Kind}.{Name}
Example: config.injection.ConfigMap.my-config
Selector Matching
Injector specification:
- Name: Name of in-cluster resource to inject
- Group: Optional API group filter
- Version: Optional API version filter
- Kind: Optional kind filter
Matching process:
For Each Injector:
↓
Check Group/Version/Kind
↓
Matches In-Package Object? ──No──> Skip
│
Yes
↓
Search In-Cluster Resources
↓
Find by Name
↓
Found? ──Yes──> Inject Resource
│
No
↓
Try Next Injector
Matching characteristics:
- GVK filters optional (nil means match any)
- Name matching required (exact match)
- First matching injector wins
- Remaining injectors skipped after match
Field Injection
Allowed fields:
- ConfigMap: “data” field
- All other kinds: “spec” field
- Whitelist prevents arbitrary field injection
- Security by design (limited field access)
Injection process:
injectResource()
↓
Check Allowed Fields
↓
For Each Allowed Field:
↓
Group/Kind Match? ──No──> Continue
│
Yes
↓
Extract Field from In-Cluster
↓
Set Field in In-Package
↓
Set Injected Annotation
↓
Mark Injected
Injected resource annotation:
- Annotation key: “kpt.dev/injected-resource”
- Annotation value: Name of in-cluster resource
- Added to in-package resource
- Tracks injection source
Kptfile Conditions and Gates
Readiness gates:
- Added for required injection points
- Condition type from injection point
- Prevents package approval until injection succeeds
- Optional injection points don’t add gates
Status conditions:
- One condition per injection point
- Condition type from injection point
- Status: True (injected) or False (not injected)
- Reason: “ConfigInjected” or “NoResourceSelected”
- Message: Injected resource name or error details
Condition management:
For Each Injection Point:
↓
Required? ──Yes──> Add Readiness Gate
│
No
↓
Set Status Condition
↓
• Type: injection point condition type
• Status: True/False based on injection
• Reason: success or failure reason
• Message: details or error
Mutation Ordering
Mutations applied in fixed order for consistency:
Order Rationale
1. Package Context (first):
- Provides base configuration data
- Other mutations may depend on context
- ConfigMap must exist before other operations
2. KRM Functions (second):
- Modifies function pipeline
- Functions will execute during render
- Must be set before injection (functions may use injected data)
3. Config Injection (last):
- Injects cluster-specific configuration
- May reference package context
- Functions can process injected data during render
Why this order:
- Dependencies flow downward
- Each mutation can build on previous
- Render task (after mutations) sees complete configuration
Change Detection
The controller detects whether mutations changed package resources:
Detection Process
calculateDraftResources()
↓
Store Original Resources
↓
Apply All Mutations
↓
Compare Original vs Modified
↓
File Count Changed? ──Yes──> Changed
│
No
↓
For Each File:
↓
File Deleted? ──Yes──> Changed
│
No
↓
Content Changed? ──Yes──> Changed
│
No
↓
Continue
↓
All Files Unchanged? ──Yes──> Unchanged
Comparison logic:
- File count: Different number of files indicates change
- File deletion: File in original but not in modified indicates change
- Content comparison: Byte-by-byte comparison of file contents
- Kptfile special case: Semantic comparison (not byte-by-byte)
Kptfile semantic comparison:
- Parse both Kptfiles as structured objects
- Compare semantically using kptfileutil.Equal
- Ignores formatting differences (whitespace, indentation)
- Prevents false positives from YAML rendering differences
Change Handling
If changed:
- Return modified PackageRevisionResources
- Return changed=true flag
- Caller updates PackageRevisionResources via Porch API
- Triggers render task execution
If unchanged:
- Return original PackageRevisionResources
- Return changed=false flag
- Caller skips update (no API call)
- No render task execution needed
Implications:
- Avoids unnecessary API calls
- Prevents spurious render executions
- Reduces load on Porch server
- Improves reconciliation performance
Error Handling
Mutation errors handled at multiple levels:
Mutation-Level Errors
Package context errors:
- ConfigMap not found
- Invalid ConfigMap structure
- Missing or invalid data field
- Field manipulation errors
KRM function errors:
- Kptfile not found
- Invalid Kptfile structure
- Pipeline manipulation errors
- Function name generation errors
Config injection errors:
- File parsing errors
- Duplicate injection points
- Invalid annotation values
- In-cluster resource query errors
- Field injection errors
Error Propagation
Mutation Error
↓
Return Error
↓
calculateDraftResources()
↓
Return Error
↓
ensurePackageVariant()
↓
Return Error
↓
Reconcile()
↓
Set Ready=False
↓
Requeue
Error flow:
- Mutation errors returned immediately
- Stops mutation application
- Propagates to reconciliation loop
- Sets Ready condition to False
- Triggers requeue for retry
Error Recovery
Transient errors:
- In-cluster resource temporarily unavailable
- Network errors during resource query
- Automatic retry on next reconciliation
Permanent errors:
- Invalid PackageVariant specification
- Missing required resources in package
- Requires PackageVariant or package fix
Mutation Characteristics
Idempotency
Idempotent operations:
- Package context: Overwrites existing keys
- KRM functions: Removes old, adds new
- Config injection: Replaces field content
Implications:
- Safe to apply multiple times
- Produces same result regardless of repetition
- Reconciliation can retry without side effects
Isolation
Mutation isolation:
- Each mutation operates independently
- No shared state between mutations
- Order enforced by orchestration, not dependencies
Draft isolation:
- Mutations modify draft workspace only
- No effect on published packages
- Changes visible only after draft closure
Validation
Pre-mutation validation:
- PackageVariant spec validated before mutations
- Reserved keys checked
- Injector names validated
- Prevents invalid mutations
Post-mutation validation:
- Render task validates modified package
- Functions can fail on invalid configuration
- Errors prevent draft closure
Performance Considerations
Optimization Strategies
Change detection:
- Avoids unnecessary API calls
- Skips render when unchanged
- Reduces reconciliation time
Selective file parsing:
- Only parses files matching resource patterns
- Skips non-resource files (README, etc.)
- Reduces parsing overhead
In-memory operations:
- All mutations in-memory
- No disk I/O during mutations
- Fast transformation operations
Scalability
Package size:
- Mutations scale linearly with package size
- Large packages take longer to parse and compare
- Change detection overhead proportional to file count
Injection points:
- Multiple injection points increase processing time
- Each injection point queries Kubernetes API
- Consider limiting injection points per package
Function count:
- Function manipulation scales with pipeline size
- Large pipelines take longer to process
- Prepending is O(n) operation