PackageVariant Reconciliation

Detailed architecture of PackageVariant reconciliation flows and state management.

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:

  1. Missing UpstreamLock: Assume up-to-date (log warning, avoid breaking)
  2. Missing Git ref: Assume up-to-date (log warning, avoid breaking)
  3. Draft upstream: Always needs update (target is always published)
  4. Revision mismatch: Needs update (upstream version changed)
  5. Revision match: Up-to-date (no action needed)

Revision extraction:

  • Git ref format: refs/tags/v{revision} or refs/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