PackageRevision Lifecycle#
A PackageRevision moves through four states encoded in spec.lifecycle :
| State | Meaning |
|---|---|
Draft | In-progress editing; content can be freely modified |
Proposed | Submitted for approval; mutations blocked by webhook |
Published | Canonical / approved; a "main" branch twin is created in the Git repository |
DeletionProposed | Scheduled for deletion; prevents further edits |
Because spec.lifecycle is user-owned (not system-computed), every change to it increments metadata.generation, which has downstream effects on the Rendered condition's freshness — see Field Manager Design below.
Controller Architecture#
The v1alpha2 PackageRevision controller (introduced in PR #514) runs three sequential sub-reconcilers per cycle :
reconcileSource → reconcileRender → reconcileLifecycle
reconcileSource— Handles one-time package creation fromspec.source(init,clone,copy,upgrade). Runs only if the source has not yet been materialized.reconcileRender— Executes KRM function pipelines asynchronously. Triggered by a render-request annotation. Render results are written using thepackagerev-controller-renderfield manager (see Field Manager Design).reconcileLifecycle— Compares the desiredspec.lifecycleagainst the current state and executes the transition (e.g., git push forPublished).
Critical: re-read after render. Because rendering is asynchronous and can take seconds, a concurrent API patch may advance spec.lifecycle while rendering is running. Without a re-read, reconcileLifecycle would see the pre-render snapshot and silently revert the transition. PR #964 fixed this by re-reading the PackageRevision from the API server after reconcileRender before calling reconcileLifecycle .
When a revision transitions to Published, the engine calls RefreshCache() synchronously and broadcasts changes via NotifyPackageRevisionChange() so that the published blueprint is immediately visible to callers .
Webhook Validation#
A validating admission webhook (issue #918, PR #1100) enforces rules at admission time — before the controller ever reconciles — giving users immediate feedback instead of a deferred Ready=False.
CREATE rules:
spec.lifecyclemust beDraftorProposed- Exactly one
spec.sourcefield may be set - The referenced
RepositoryCR must exist and carry thev1alpha2annotation spec.workspaceNamemust be unique within the repo+package combination
UPDATE rules:
- Immutable fields:
spec.source,spec.packageName,spec.repository,spec.workspaceName - Only allowed lifecycle transitions are accepted (e.g.,
Draft→Proposed,Proposed→Published) - Lifecycle changes are rejected while a render is in progress (render race prevention)
DELETE rules:
- Deletion is rejected if other
PackageRevisions reference this package as an upstream
The webhook is registered at /validate-porch-kpt-dev-v1alpha2-packagerevision on port 9443. As of issue #1092 , structured logging and Prometheus metrics for rejection reasons are planned but not yet implemented — webhook rejections currently fail silently in production logs.
Field Manager Design (SSA)#
Status fields are managed via Server-Side Apply with three distinct field managers :
| Field Manager | Owned Fields |
|---|---|
packagerev-controller | Ready condition, publication metadata, locks, resource sizes |
packagerev-controller-render | Rendered condition, render tracking annotation |
packagerev-controller-kptfile | Kptfile-derived fields |
Known issue: stale observedGeneration on Rendered condition (issue #1075) .
Because SSA enforces field ownership, updateStatus (the main field manager) cannot touch the Rendered condition — that is owned by packagerev-controller-render. And updateRenderStatus is only called when a render actually executes. Any spec change that increments metadata.generation without triggering a render (lifecycle transitions, packageMetadata changes) leaves the Rendered condition's observedGeneration pointing at the previous generation.
Practical impact: kubectl wait --for=condition=Rendered (kubectl ≥ 1.35) checks condition.observedGeneration >= metadata.generation. After a Draft→Proposed patch, this wait hangs indefinitely even if content is fully rendered. CI pipelines doing push → wait Rendered → propose → wait Rendered break on the second wait.
Proposed fix: In reconcileRender, when render is skipped but Rendered=True with stale observedGeneration, call updateRenderStatus with the same values but updated generation — a no-content SSA patch using the existing field manager.
See the split field manager implementation in status.go.
Race Conditions & Known Issues#
Render Staleness Detection#
checkRenderStale re-reads the PackageRevision after a render completes and compares the AnnotationRenderRequest value recorded at render-start against the current value . A mismatch means a concurrent user push changed the annotation during rendering; the controller logs "render stale, requeuing" and requeueing prevents the stale result from overwriting the newer push.
Cache Gap: PackageRevision Stuck on Repo Ready (issue #1107)#
When a PackageRevision is created immediately after a Repository reports Ready=True, the PR controller's internal content cache may not yet have opened the repository. The draft creation fails with "repository not found" and — because errors here are swallowed rather than requeued — the package is permanently stuck in Failed state with no retry . Reproduced at ~22% failure rate under stress testing.
Concurrent CUD Serialization#
Concurrent Create/Update/Delete requests on the same revision are serialized via a per-revision mutex map (pkgRevOperationMutexes) in packagecommon.go. If TryLock() fails, the server returns HTTP 409 with "another request is already in progress on package revision [key]" .
Key Source Files#
| File | Concern |
|---|---|
api/porch/v1alpha2/packagerevision_types.go | Lifecycle state constants |
controllers/.../packagerevision_controller.go | Reconcile loop, re-read after render |
controllers/.../render.go | checkRenderStale, annotation-based stale detection |
controllers/.../status.go | Field manager split, updateStatus, updateRenderStatus |
pkg/registry/porch/packagecommon.go | Per-revision mutex, HTTP 409 conflict |
pkg/engine/engine.go | RefreshCache, NotifyPackageRevisionChange post-publish |