PackageVariant Reconciliation
Overview
The PackageVariant controller implements a continuous synchronization pattern between upstream and downstream packages. It monitors upstream PackageRevisions for changes, detects when downstream packages need updating, and creates appropriate drafts (clone, upgrade, or edit) to maintain synchronization while applying configured mutations.
High-Level Architecture
┌─────────────────────────────────────────────────────────┐
│ PackageVariant Reconciliation System │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ State Machine │ │ Upstream │ │
│ │ │ ──> │ Tracking │ │
│ │ • No Downstream │ │ │ │
│ │ • Upstream Chg │ │ • UpstreamLock │ │
│ │ • Mutation Chg │ │ • Comparison │ │
│ │ • Up-to-date │ │ • Detection │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ └───────────┬─────────────┘ │
│ ↓ │
│ ┌──────────────────┐ │
│ │ Draft │ │
│ │ Management │ │
│ │ │ │
│ │ • Clone │ │
│ │ • Upgrade │ │
│ │ • Edit │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
Reconciliation State Machine
The controller implements a state machine that determines actions based on current state:
State Determination Flow
Reconcile Triggered
↓
Get PackageVariant
↓
List PackageRevisions
↓
Validate Spec
↓
Find Upstream PR
↓
Get Downstream PRs
↓
Downstream Exists? ──No──> State: NO_DOWNSTREAM
│
Yes
↓
Check UpstreamLock
↓
Up-to-date? ──No──> State: UPSTREAM_CHANGED
│
Yes
↓
Calculate Draft Resources
↓
Mutations Changed? ──Yes──> State: MUTATIONS_CHANGED
│
No
↓
State: UP_TO_DATE
State transitions:
- NO_DOWNSTREAM: No downstream package exists → Create clone draft
- UPSTREAM_CHANGED: Upstream version changed → Create upgrade draft
- MUTATIONS_CHANGED: Mutations changed but upstream same → Create edit draft
- UP_TO_DATE: Everything synchronized → No action
State Actions
NO_DOWNSTREAM state:
Create PackageRevision
↓
• Tasks: [Clone{UpstreamRef}]
• WorkspaceName: "packagevariant-1"
• Lifecycle: Draft
• OwnerReferences: [PackageVariant UID]
↓
Fetch PackageRevisionResources
↓
Apply Mutations
↓
Update PackageRevisionResources
↓
Return
UPSTREAM_CHANGED state:
Check Downstream Lifecycle
↓
Published? ──No──> Error (can't upgrade draft)
│
Yes
↓
Get Old Upstream (from UpstreamLock)
↓
Get New Upstream (from spec)
↓
Create PackageRevision
↓
• Tasks: [Upgrade{
OldUpstream, NewUpstream,
LocalPackageRevision, Strategy
}]
• WorkspaceName: "packagevariant-N"
↓
Porch Performs Three-Way Merge
↓
Fetch PackageRevisionResources
↓
Apply Mutations
↓
Update PackageRevisionResources
↓
Return
MUTATIONS_CHANGED state:
Check Downstream Lifecycle
↓
Published? ──Yes──> Create Edit Draft
│
No (Draft/Proposed)
↓
Update Existing Draft
↓
Fetch PackageRevisionResources
↓
Apply Mutations
↓
Update PackageRevisionResources
↓
Return
Upstream Change Detection
The controller uses UpstreamLock comparison to detect when upstream packages change:
UpstreamLock Structure
Location and content:
- Stored in:
PackageRevision.Status.UpstreamLock - Contains: Git reference pointing to upstream package revision
- Example refs:
refs/tags/v1,refs/tags/v2,refs/tags/packagevariant-3 - Revision extraction: Parse number from ref path
Comparison Algorithm
isUpToDate(pv, downstream)
↓
UpstreamLock exists? ──No──> Consider up-to-date (warning logged)
│
Yes
↓
Git ref exists? ──No──> Consider up-to-date (warning logged)
│
Yes
↓
Git ref starts with "drafts"? ──Yes──> NOT up-to-date
│
No
↓
Extract revision from Git ref
↓
• Parse last segment after "/"
• Convert to integer
↓
Compare with pv.Spec.Upstream.Revision
↓
Match? ──Yes──> Up-to-date
│
No
↓
NOT up-to-date
Comparison logic:
- Missing UpstreamLock: Assume up-to-date (log warning, avoid breaking)
- Missing Git ref: Assume up-to-date (log warning, avoid breaking)
- Draft upstream: Always needs update (target is always published)
- Revision mismatch: Needs update (upstream version changed)
- Revision match: Up-to-date (no action needed)
Revision extraction:
- Git ref format:
refs/tags/v{revision}orrefs/tags/{workspace} - Extract last segment after final
/ - Convert to integer using repository utility
- Compare with desired revision from PackageVariant spec
Implications:
- Fast detection: No need to fetch upstream content
- Efficient: Simple integer comparison
- Automatic: Detects new upstream versions without manual intervention
- Graceful degradation: Missing data doesn’t break reconciliation
Draft Creation Flows
The controller creates three types of drafts depending on the situation:
Clone Draft Flow
When used:
- No downstream package exists
- Initial package creation from upstream
Creation process:
ensurePackageVariant()
↓
No Existing Downstream
↓
Create PackageRevision Object
↓
• ObjectMeta:
- Namespace: same as PackageVariant
- OwnerReferences: [PackageVariant UID]
- Labels: from PackageVariant.Spec.Labels
- Annotations: from PackageVariant.Spec.Annotations
↓
• Spec:
- PackageName: downstream.Package
- RepositoryName: downstream.Repo
- WorkspaceName: newWorkspaceName()
- Tasks: [Clone{UpstreamRef: upstream.Name}]
↓
POST to Porch API
↓
Porch Creates Draft Workspace
↓
Porch Executes Clone Task
↓
Controller Fetches PackageRevisionResources
↓
Controller Applies Mutations
↓
Controller Updates PackageRevisionResources
↓
Draft Ready for Approval
Workspace naming:
- Prefix:
packagevariant- - Numbering: Incremental (1, 2, 3, …)
- Algorithm: Scan existing PRs for same package/repo, find highest number, increment
- Scope: Per package/repository combination
- Ensures: Unique workspace names across all revisions
Upgrade Draft Flow
When used:
- Downstream exists and is published
- Upstream version changed (UpstreamLock mismatch)
Creation process:
findAndUpdateExistingRevisions()
↓
Downstream Not Up-to-date
↓
Downstream Published? ──No──> Error
│
Yes
↓
Extract Old Upstream Revision
↓
• Get revision from UpstreamLock
• Find published PR with that revision
↓
Get New Upstream
↓
• Use pv.Spec.Upstream
• Find current upstream PR
↓
Create PackageRevision Object
↓
• Copy metadata from source
• New WorkspaceName
• Tasks: [Upgrade{
OldUpstream: old PR name,
NewUpstream: new PR name,
LocalPackageRevision: current downstream name,
Strategy: ResourceMerge
}]
↓
POST to Porch API
↓
Porch Performs Three-Way Merge
↓
• Base: OldUpstream
• Theirs: NewUpstream
• Ours: LocalPackageRevision
↓
Controller Applies Mutations
↓
Draft Ready for Approval
Three-way merge:
- Base: Old upstream version (common ancestor)
- Theirs: New upstream version (upstream changes)
- Ours: Current downstream (local changes)
- Strategy: ResourceMerge (field-level merge)
- Result: Merged package with both upstream and local changes
Requirements:
- Source must be published (cannot upgrade draft)
- Old upstream must be published and findable
- New upstream must exist
- All three package revisions must be accessible
Edit Draft Flow
When used:
- Downstream exists and is published
- Mutations changed but upstream same
- Need to apply new mutations to published package
Creation process:
findAndUpdateExistingRevisions()
↓
Calculate Draft Resources
↓
Resources Changed? ──No──> Done
│
Yes
↓
Downstream Published? ──No──> Update Existing Draft
│
Yes
↓
Create PackageRevision Object
↓
• Copy metadata from source
• New WorkspaceName
• Tasks: [Edit{Source: current PR name}]
↓
POST to Porch API
↓
Porch Copies Package
↓
Controller Recalculates Mutations
↓
Controller Applies New Mutations
↓
Draft Ready for Approval
Edit characteristics:
- Copies published package to new draft
- Preserves upstream relationship
- Reapplies all mutations to new draft
- Used when only mutations changed (not upstream)
Draft vs Published handling:
- Published source: Create new edit draft
- Draft/Proposed source: Update existing draft in-place
- Rationale: Avoid creating multiple drafts for same package
Adoption and Deletion
The controller manages ownership of downstream packages through policies:
Adoption Flow
AdoptionPolicy: adoptNone (default):
Get Downstream PRs
↓
For Each PR:
↓
Has Our OwnerReference? ──No──> Skip
│
Yes
↓
Process PR
AdoptionPolicy: adoptExisting:
Get Downstream PRs
↓
For Each PR:
↓
Matches Downstream Repo/Package? ──No──> Skip
│
Yes
↓
Has Our OwnerReference? ──Yes──> Process PR
│
No
↓
Adopt Package Revision
↓
• Add our OwnerReference
• Apply our Labels
• Apply our Annotations
↓
Update PR
↓
Process PR
Adoption process:
- Check if PR matches downstream repo and package
- Add PackageVariant UID to OwnerReferences
- Merge PackageVariant labels into PR labels
- Merge PackageVariant annotations into PR annotations
- Update PR via Porch API
Adoption rationale:
- adoptNone: Safe default, prevents accidental takeover
- adoptExisting: Enables gradual migration to controller management
- Use case: Existing packages created manually, now want controller to manage
Deletion Flow
DeletionPolicy: delete (default):
PackageVariant Deleted
↓
DeletionTimestamp Set
↓
For Each Owned PR:
↓
Check Lifecycle
↓
Draft/Proposed? ──Yes──> Delete Immediately
│
No (Published)
↓
Set Lifecycle = DeletionProposed
↓
Update PR
↓
Wait for Approval
DeletionPolicy: orphan:
PackageVariant Deleted
↓
DeletionTimestamp Set
↓
For Each Owned PR:
↓
Remove Our OwnerReference
↓
Update PR
↓
Leave Package in Place
Deletion handling by lifecycle:
- Draft/Proposed: Delete immediately (not yet approved)
- Published: Set to DeletionProposed (requires approval)
- DeletionProposed: No action (already proposed)
Special case:
- If DeletionProposed PR exists when PackageVariant deleted
- Must orphan it (remove OwnerReference)
- Otherwise it will be auto-deleted by Kubernetes garbage collection
- Allows approval process to complete
Finalizer coordination:
- Finalizer:
config.porch.kpt.dev/packagevariants - Added when PackageVariant created
- Prevents deletion until cleanup complete
- Removed after all owned PRs handled
Error Handling
The controller handles errors at multiple stages:
Validation Errors
Validate PackageVariant
↓
Errors Found? ──Yes──> Set Conditions
│ ↓
No Stalled=True
↓ Ready=False
Continue ↓
Do NOT Requeue
Validation failures:
- Set Stalled condition to True with error message
- Set Ready condition to False
- Do NOT requeue (requires PackageVariant spec change)
- Error message includes all validation errors combined
Validation checks:
- Upstream field presence and completeness
- Downstream field presence and completeness
- AdoptionPolicy valid value
- DeletionPolicy valid value
- PackageContext reserved keys not used
- Injector names specified
Upstream Not Found Errors
Find Upstream PR
↓
Not Found? ──Yes──> Set Conditions
│ ↓
No Stalled=True
↓ Ready=False
Continue ↓
Requeue (may appear)
Upstream not found:
- Set Stalled condition to True
- Set Ready condition to False
- Requeue (upstream may appear later)
- Watch on PackageRevisions will trigger reconciliation when upstream appears
Reconciliation Errors
ensurePackageVariant()
↓
Error? ──Yes──> Set Conditions
│ ↓
No Ready=False
↓ Stalled=False
Success ↓
↓ Requeue (may be transient)
Ready=True
Stalled=False
Reconciliation failures:
- Set Ready condition to False with error message
- Keep Stalled condition as False (validation passed)
- Requeue (may be transient error)
- Examples: API errors, network issues, Porch unavailable
Upgrade Constraint Errors
Create Upgrade Draft
↓
Source Published? ──No──> Return Error
│
Yes
↓
Old Upstream Published? ──No──> Return Error
│
Yes
↓
Create Upgrade
Upgrade constraints:
- Source (current downstream) must be published
- Old upstream must be published and findable
- Cannot upgrade from draft (must publish first)
- Error returned to reconciliation loop
- Requeued for retry
Condition Management
Condition types:
Stalled condition:
- True: Validation error or upstream not found (no progress possible)
- False: All validation passed, upstream found (can make progress)
- Reasons: “ValidationError” or “Valid”
Ready condition:
- True: Successfully ensured downstream package
- False: Error during reconciliation
- Reasons: “NoErrors” or “Error”
Status update:
- Deferred to end of reconciliation
- Updated even if reconciliation fails
- Provides visibility into controller state
- Used by users and automation to monitor progress
DownstreamTargets tracking:
- List of downstream PackageRevisions created/adopted
- Includes Name and RenderStatus
- Updated after each reconciliation
- Preserved across reconciliations when possible
- Provides visibility into managed packages