From 40984504c5b501cb482e51468d8c3cdfed91c660 Mon Sep 17 00:00:00 2001 From: Fiachra Corcoran Date: Wed, 6 May 2026 20:02:53 +0100 Subject: [PATCH] docs: add PackageRevision controller architecture docs (draft) Add v1alpha2 architecture documentation for the PackageRevision controller as Hugo draft pages. These render in deploy previews but are hidden from the production build. New pages: - deployment-modes.md: v1alpha1 vs v1alpha2 architecture comparison - packagerevision-controller/_index.md: controller overview - packagerevision-controller/design.md: internal design - packagerevision-controller/interactions.md: component interactions Also adds docs/config/development.toml (buildDrafts=true) and docs/config/production.toml so the existing Netlify build command picks up environment-specific settings. --- docs/config/development.toml | 3 + docs/config/production.toml | 3 + .../packagerevision-controller/_index.md | 75 ++++++++++++ .../packagerevision-controller/design.md | 101 ++++++++++++++++ .../interactions.md | 108 ++++++++++++++++++ .../deployment-modes.md | 77 +++++++++++++ 6 files changed, 367 insertions(+) create mode 100644 docs/config/development.toml create mode 100644 docs/config/production.toml create mode 100644 docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/_index.md create mode 100644 docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/design.md create mode 100644 docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/interactions.md create mode 100644 docs/content/en/docs/5_architecture_and_components/deployment-modes.md diff --git a/docs/config/development.toml b/docs/config/development.toml new file mode 100644 index 000000000..6b0666e57 --- /dev/null +++ b/docs/config/development.toml @@ -0,0 +1,3 @@ +# Development/preview environment overrides +# Enables draft content in deploy previews and branch deploys +buildDrafts = true diff --git a/docs/config/production.toml b/docs/config/production.toml new file mode 100644 index 000000000..6b3c65834 --- /dev/null +++ b/docs/config/production.toml @@ -0,0 +1,3 @@ +# Production environment overrides +# Drafts are excluded by default (Hugo's default behavior) +# This file exists so the Netlify build command doesn't warn about a missing config merge diff --git a/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/_index.md b/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/_index.md new file mode 100644 index 000000000..0b19f8055 --- /dev/null +++ b/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/_index.md @@ -0,0 +1,75 @@ +--- +title: "PackageRevision Controller" +type: docs +weight: 2 +draft: true +description: | + Kubernetes controller for package revision lifecycle management (v1alpha2). +--- + +## Overview + +The PackageRevision Controller manages the full lifecycle of package revisions as native Kubernetes CRDs. In the v1alpha1 architecture, the Porch API Server and Engine handle all operations synchronously within the request path. The PR controller takes a different approach — it watches `PackageRevision` CRDs in etcd and reconciles their desired state against Git asynchronously, following standard Kubernetes controller patterns. + +This means users interact with package revisions the same way they interact with any other Kubernetes resource: create a CRD with the desired state, and the controller makes it so. + +## How It Works + +``` +┌─────────────────────┐ ┌──────────────────────────────┐ ┌─────────────────┐ +│ PackageRevision CRD │ │ PR Controller │ │ Shared Cache │ +│ (etcd) │────>│ │────>│ (from Repo Ctr) │ +│ │ │ • Source execution │ │ │ +│ • spec.source │ │ • Render pipeline │ │ • Git read/write│ +│ • spec.lifecycle │ │ • Lifecycle transitions │ │ • Draft mgmt │ +│ • annotations │ │ • Status updates (SSA) │ │ • Content cache │ +└─────────────────────┘ └──────────────────────────────┘ └─────────────────┘ + │ + ▼ + ┌──────────────────────┐ + │ Function Runner │ + │ (gRPC) │ + └──────────────────────┘ +``` + +The controller does not manage repository connections or synchronization. That responsibility stays with the Repository Controller, which populates the shared cache. The PR controller reads from and writes to that cache — it never opens a Git connection directly. + +## Reconciliation Pipeline + +Each reconcile executes three phases in sequence. If any phase produces an error or requires a requeue, subsequent phases are skipped. + +**Source execution** handles one-time package creation. When a user creates a PackageRevision with `spec.source` set (init, clone, copy, or upgrade), the controller executes that source operation to produce the initial package content in Git. Once `status.creationSource` is populated, this phase becomes a no-op on future reconciles. + +**Rendering** runs the KRM function pipeline defined in the package's Kptfile. Two events trigger rendering: a content push via the PRR handler (signalled by the `porch.kpt.dev/render-request` annotation), or the completion of source execution. The controller reads resources from the cache, invokes kpt render through the function runner, and writes the results back. + +**Lifecycle transition** compares the desired lifecycle in `spec.lifecycle` with the actual lifecycle in Git. If they differ, the controller transitions the package in Git. On publish, it assigns a revision number and updates the `latest-revision` label across all revisions of the same package. + +## Relationship to Other Components + +The PR controller sits alongside the Repository Controller in the controllers deployment. It depends on the shared cache that the Repository Controller creates and populates — this is enforced at startup by initializing the repo reconciler first and injecting its cache into the PR reconciler. + +The Porch API Server and Engine continue to serve `PackageRevisionResources` for content access. When a user pushes content through PRR, the API Server writes to Git via the Engine and then patches the render-request annotation on the PackageRevision CRD. This annotation change triggers the PR controller to pick up the new content and render it. + +PackageVariant and PackageVariantSet controllers create PackageRevision CRDs as part of their automation. The PR controller reconciles these like any other PackageRevision — it doesn't know or care who created the CRD. + +## Enabling the Controller + +The PR controller is enabled via the `--reconcilers` flag on the controllers deployment: + +``` +--reconcilers=packagerevisions +``` + +It requires the Repository Controller to be running (for the shared cache), the `PackageRevision` CRD to be installed, and the `FUNCTION_RUNNER_ADDRESS` environment variable to be set if external function evaluation is needed. + +## Configuration + +The controller exposes flags for tuning concurrency and retry behavior: + +| Flag | Default | Description | +|------|---------|-------------| +| `packagerevisions.max-concurrent-reconciles` | 50 | Maximum parallel reconciles | +| `packagerevisions.max-concurrent-renders` | 20 | Maximum parallel render operations | +| `packagerevisions.render-requeue-delay` | 2s | Delay before requeue when render limit reached | +| `packagerevisions.repo-operation-retry-attempts` | 3 | Retry count for git operations | +| `packagerevisions.max-grpc-message-size` | 6MB | Max gRPC message size for fn-runner | diff --git a/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/design.md b/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/design.md new file mode 100644 index 000000000..8ffaafe0b --- /dev/null +++ b/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/design.md @@ -0,0 +1,101 @@ +--- +title: "Design" +type: docs +weight: 2 +draft: true +description: | + Internal design and architecture of the PackageRevision Controller. +--- + +## Controller Structure + +The PR controller is a standard controller-runtime reconciler. Its internal structure mirrors the reconciliation pipeline — each concern is handled by a dedicated sub-reconciler that returns early if its work is not needed: + +``` +PackageRevisionReconciler +├── reconcileFinalizer() — Finalizer + ownerReference management, deletion gating +├── reconcileSource() — One-time package creation (init/clone/copy/upgrade) +├── reconcileRender() — KRM function pipeline execution +└── reconcileLifecycle() — Git lifecycle transitions, revision numbering +``` + +## CRD as Intent, Git as Content + +The fundamental design decision is the separation of intent from content. The `PackageRevision` CRD in etcd is the source of truth for **what the user wants** — which lifecycle state the package should be in, how it was created, whether rendering is requested. Git is the source of truth for **what the package contains** — the actual KRM resource files. + +The controller bridges these two stores. A user sets `spec.lifecycle: Published` on the CRD; the controller transitions the package in Git to published state and updates `status` to reflect the result. This is standard Kubernetes controller semantics — spec is desired state, status is observed state. + +## Shared Cache + +The controller does not open Git repositories directly. All Git interaction goes through the `ContentCache` interface, which is backed by the Repository Controller's shared cache. This design centralizes repository connection management, credential handling, and cache invalidation in a single component. + +The cache provides six operations that cover the controller's needs: + +- **GetPackageContent** — read package state and files from the cache +- **CreateNewDraft** — open a new draft for writing initial content +- **CreateDraftFromExisting** — open an existing package for modification (used by render) +- **CloseDraft** — commit a draft to Git +- **UpdateLifecycle** — transition a package's lifecycle state in Git +- **DeletePackage** — remove git refs (branches/tags) for a package + +The controller never needs to know whether the underlying cache is CR-based or DB-based. It works identically with either implementation. + +## Server-Side Apply for Status + +All status updates use Server-Side Apply with distinct field managers to avoid ownership conflicts. This is important because multiple actors write to the same PackageRevision — the Repository Controller sets initial values during discovery, and the PR controller takes over during reconciliation. + +Three field managers partition the status fields: + +**packagerev-controller** owns the core status: Ready condition, observedGeneration, revision number, publishedBy/At timestamps, upstream and self locks, and creationSource. + +**packagerev-controller-render** owns the render tracking fields: Rendered condition, renderingPrrResourceVersion, and observedPrrResourceVersion. Separating these prevents a lifecycle status update from accidentally clearing render state. + +**packagerev-controller-kptfile** owns fields synced from the Kptfile after rendering: readinessGates, packageMetadata, and packageConditions. These are written to the CRD spec and status so that external controllers can read Kptfile-derived data without parsing package content. + +## Concurrency-Limited Rendering + +Rendering calls the function runner via gRPC, which is resource-intensive. Rather than allowing all 50 concurrent reconciles to render simultaneously, the controller uses a channel-based semaphore to bound concurrent renders to a configurable limit (default 20). + +When the semaphore is full, the reconcile doesn't block — it returns a `RequeueAfter` result and tries again after a short delay. This keeps the controller responsive and prevents it from overwhelming the function runner or exhausting gRPC connections. + +## Stale Render Detection + +A race exists between rendering and content pushes. While the controller is rendering (which may take seconds), the user might push new content through PRR, changing the render-request annotation. If the controller wrote back the now-stale render results, the user's latest content would be overwritten. + +To handle this, after rendering completes the controller re-reads the PackageRevision directly from etcd (bypassing the informer cache) and compares the current annotation value with the one that triggered the render. If they differ, the render results are discarded and the reconcile requeues to pick up the newer content. + +## Deletion Gating + +Published packages cannot be deleted directly. This is a safety mechanism — deleting a published package from Git is destructive and irreversible. The controller enforces this through a finalizer: + +When a user deletes the CRD, Kubernetes sets `deletionTimestamp` but the finalizer prevents actual removal. The controller checks the package's lifecycle: + +- If the package is Published and its owner Repository still exists, the controller does nothing. The object stays in Terminating state until the user first transitions it to DeletionProposed. +- If the package is DeletionProposed (or any non-Published state), the controller cleans up Git refs and removes the finalizer, allowing Kubernetes to complete the deletion. +- If the owner Repository has been deleted (Kubernetes GC cascade), the controller allows deletion regardless of lifecycle — there's no point protecting packages whose repository is gone. + +## OwnerReference to Repository + +Each PackageRevision gets an ownerReference pointing to its Repository CRD. This serves two purposes: it enables Kubernetes garbage collection (deleting a Repository cascades to all its packages), and it allows the controller to detect GC cascade during deletion gating. + +The ownerReference is set on first reconcile if not already present, in the same patch that adds the finalizer. + +## Source Execution + +Source execution is idempotent — it only runs once per PackageRevision. The guard is `status.creationSource`: if it's already set, the source phase is skipped entirely. + +**Init** creates a brand new package by generating a Kptfile with the specified metadata (name, description, keywords). No external dependencies. + +**Clone** copies content from an upstream package. Two modes are supported: cloning from a registered PackageRevision (by name reference) or from a raw Git URL. In both cases, the Kptfile's upstream and upstreamLock fields are set to track the source. + +**Copy** creates a new revision from an existing published revision of the same package in the same repository. This is the mechanism for "edit an existing package" — copy the latest published revision into a new draft workspace. + +**Upgrade** performs a 3-way merge between the old upstream, new upstream, and current local package. It supports multiple merge strategies (resource-merge, fast-forward, force-delete-replace, copy-merge). After merging, the Kptfile upstream/upstreamLock are updated to point at the new upstream. + +After any source execution, the controller creates a draft in the cache, writes the resources, closes the draft (committing to Git), and requeues to trigger rendering. + +## Latest-Revision Labels + +The controller maintains a `porch.kpt.dev/latest-revision` label on all PackageRevisions. The published revision with the highest revision number gets `"true"`; all others get `"false"`. This label enables efficient queries like "give me the latest published version of package X" without listing and sorting all revisions. + +Labels are updated on two events: when a package is published (the new revision becomes latest), and when a published package is deleted (the previous revision becomes latest again). diff --git a/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/interactions.md b/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/interactions.md new file mode 100644 index 000000000..835c01ab5 --- /dev/null +++ b/docs/content/en/docs/5_architecture_and_components/controllers/packagerevision-controller/interactions.md @@ -0,0 +1,108 @@ +--- +title: "Interactions" +type: docs +weight: 3 +draft: true +description: | + How the PackageRevision Controller interacts with other Porch components. +--- + +## Component Interaction Overview + +``` + ┌─────────────────────────────────────────────────────────┐ + │ User / GitOps │ + └───────────┬─────────────────────────────┬───────────────┘ + │ kubectl apply │ kubectl get PRR + ▼ ▼ +┌───────────────────────────────────────────┐ ┌─────────────────────────────┐ +│ PackageRevision CRD (etcd) │ │ Porch API Server │ +│ │ │ │ +│ • spec.source (init/clone/copy/upgrade) │ │ • PackageRevisionResources │ +│ • spec.lifecycle (Draft/Published/...) │ │ • Engine (content access) │ +│ • annotations (render-request) │ │ │ +└───────────────────┬───────────────────────┘ └──────────────┬──────────────┘ + │ watch │ read/write + ▼ ▼ +┌───────────────────────────────────────────────────────────────────────────────┐ +│ Shared Cache (ContentCache) │ +│ │ +│ GetPackageContent · CreateNewDraft · CreateDraftFromExisting │ +│ CloseDraft · UpdateLifecycle · DeletePackage │ +│ │ +└───────────────────────────────────────────────────────────────────────────────┘ + ▲ populated by │ git operations + │ ▼ +┌───────────────────────────┐ ┌───────────────────────────┐ +│ Repository Controller │ │ Git Repository │ +└───────────────────────────┘ └───────────────────────────┘ +``` + +## Repository Controller + +The PR controller and Repository Controller never communicate directly. Their coupling is entirely through the shared cache — the Repository Controller creates it, populates it by syncing Git repositories on schedule, and the PR controller reads from and writes to it. + +Startup ordering is enforced in `controllers/main.go`: the repo reconciler initializes first and its cache reference is injected into the PR reconciler before setup. If the Repository Controller is not enabled, the PR controller cannot start (it requires a non-nil cache). + +At runtime, the Repository Controller's sync loop keeps the cache fresh with the latest Git state. The PR controller's writes (creating drafts, closing them, transitioning lifecycle) go through the same cache and are immediately visible to subsequent reads. There is no separate notification channel between the two controllers — the cache is the shared state. + +If the Repository Controller stops running, the PR controller continues to operate on whatever the cache already holds. No new repositories will be synced and external Git changes won't be picked up, but in-flight operations complete normally. + +## Porch API Server and Engine + +In v1alpha2 mode, the API Server and Engine serve a narrower role: they handle `PackageRevisionResources` (PRR) — the aggregated API that provides read/write access to package file contents. + +The interaction between the API Server and the PR controller is event-driven through an annotation. When a user pushes content via PRR: + +1. The API Server writes the new content to Git through the Engine and cache. +2. The API Server patches the `porch.kpt.dev/render-request` annotation on the PackageRevision CRD with the PRR's resourceVersion. +3. The PR controller's predicate filter detects the annotation change and triggers a reconcile. +4. The controller reads the updated content from the cache, renders it, and writes the results back. + +This handoff means the API Server doesn't need to know how rendering works — it just signals that new content is available. The PR controller doesn't need to know how content was written — it just reads whatever is in the cache. + +The Engine's role in v1alpha2 is limited to content access for PRR. It no longer handles lifecycle transitions, task execution, or rendering — those responsibilities have moved to the PR controller. + +## Function Runner + +The PR controller calls the function runner during the render phase. The function runner is a standalone gRPC service that executes KRM functions — both builtin Go functions compiled into the binary and external functions running in containers. + +The controller creates a `kptRenderer` during initialization, configured with the function runner's gRPC address and runner options (image prefix, etc.). During render, the controller writes package resources to an in-memory filesystem, invokes kpt's render library (which calls functions through the gRPC runtime), and reads the results back. + +Concurrency is bounded by the `max-concurrent-renders` setting. If the function runner is unavailable, renders fail and the Rendered condition is set to False with the error message. The controller does not retry failed renders automatically — it waits for the next trigger (annotation change or manual requeue). + +If `FUNCTION_RUNNER_ADDRESS` is not set, only builtin Go functions are available. External container-based functions will fail. + +## PackageVariant and PackageVariantSet Controllers + +These controllers create PackageRevision CRDs as part of their automation workflows. When a PackageVariantSet detects a new upstream revision, it creates downstream PackageRevision CRDs with `spec.source.cloneFrom` set. The PR controller then reconciles these exactly like user-created packages — executing the clone, rendering, and managing lifecycle. + +There is no special coordination between the PV controllers and the PR controller. The CRD in etcd is the interface contract. The PV controllers write the desired state; the PR controller makes it real. + +## Kubernetes Integration + +**Garbage Collection**: Each PackageRevision has an ownerReference to its Repository CRD. When a Repository is deleted, Kubernetes garbage collection cascades deletion to all owned PackageRevisions. The PR controller detects this scenario (owner repo gone) and allows deletion of Published packages that would normally be blocked by the finalizer. + +**Field Selectors**: The controller registers field indexes on the PackageRevision CRD for efficient server-side filtering. This enables queries like "list all published packages in repository X" without client-side filtering. The indexes cover `spec.repository`, `spec.packageName`, `spec.workspaceName`, `spec.lifecycle`, and `status.revision`. + +**Predicates**: The controller uses two event filters to avoid unnecessary reconciles. `GenerationChangedPredicate` skips reconciles when only metadata (labels, annotations other than render-request) changes. A custom `renderRequestChanged` predicate fires specifically when the render-request annotation changes, ensuring content pushes trigger rendering even though they don't bump generation. + +## Data Flow: Creating a Package + +A user creates a PackageRevision CRD with `spec.source.init` and `spec.lifecycle: Draft`. The CRD lands in etcd and the controller's informer picks it up. + +The controller runs `reconcileSource`, which calls `initPackage` to generate a Kptfile in memory. It then opens a draft in the cache, writes the resources, and closes the draft — committing to Git. Status is updated with `creationSource: init` and the reconcile requeues. + +On the next reconcile, source is skipped (already done). The controller runs `reconcileRender`, reads the resources from the cache, invokes kpt render, and writes the rendered output back. The Rendered condition is set to True. + +Finally, `reconcileLifecycle` checks that `spec.lifecycle` matches the Git state. Both are Draft, so Ready is set to True and the reconcile completes. + +When the user later patches `spec.lifecycle` to Published, the controller calls `UpdateLifecycle` on the cache, which transitions the package in Git (typically moving from a branch to a tag). A revision number is assigned, latest-revision labels are updated, and Ready remains True. + +## Data Flow: Pushing Content and Rendering + +A user edits package content through `PackageRevisionResources`. The API Server writes the new content to Git via the Engine, then patches the render-request annotation on the PackageRevision CRD. + +The annotation change triggers a reconcile. Source is skipped (already done). The controller enters `reconcileRender`, sees that the annotation value differs from `status.observedPrrResourceVersion`, and proceeds to render. + +It reads the updated resources from the cache, acquires the render semaphore, and calls kpt render. After rendering completes, it re-reads the CRD from etcd to check for staleness. If the annotation hasn't changed during rendering, it writes the rendered resources back to the cache and updates the Rendered condition to True. The observedPrrResourceVersion is set to the annotation value, preventing the same content from being rendered again. diff --git a/docs/content/en/docs/5_architecture_and_components/deployment-modes.md b/docs/content/en/docs/5_architecture_and_components/deployment-modes.md new file mode 100644 index 000000000..2400da794 --- /dev/null +++ b/docs/content/en/docs/5_architecture_and_components/deployment-modes.md @@ -0,0 +1,77 @@ +--- +title: "Deployment Modes" +type: docs +weight: 6 +draft: true +description: | + Comparison of v1alpha1 (aggregated API) and v1alpha2 (CRD + controller) architectures. +--- + +## Overview + +Porch supports two deployment modes for managing PackageRevisions. Both modes share the same underlying concepts — packages, lifecycle states, workspaces, upstream/downstream relationships — but differ in how the Kubernetes API is structured and where orchestration logic lives. + +## v1alpha1: Aggregated API Mode + +The original architecture. `PackageRevision` is served by the Porch API Server as an aggregated API resource with custom REST storage. When a client creates or updates a PackageRevision, the request flows synchronously through the API Server into the Engine, which orchestrates the operation against Git through the cache. + +``` +┌──────────────┐ ┌──────────────────┐ ┌─────────┐ ┌─────┐ +│ kubectl │────>│ Porch API Server │────>│ Engine │────>│ Git │ +│ │<────│ (aggregated API) │<────│ │<────│ │ +└──────────────┘ └──────────────────┘ └─────────┘ └─────┘ +``` + +The API Server is the single point of orchestration. It handles validation, lifecycle transitions, task execution, rendering, and watch streams — all within the request path. The Engine is the brain; the API Server is the interface. + +PackageRevisions are not stored in etcd. The custom REST storage translates Kubernetes API semantics into Engine operations, and the Engine reads/writes Git through the cache. This means watches are custom (implemented via WatcherManager) rather than native Kubernetes watches. + +## v1alpha2: CRD + Controller Mode + +The newer architecture. `PackageRevision` is a standard Kubernetes CRD stored in etcd. A dedicated controller watches these CRDs and reconciles their desired state against Git asynchronously. + +``` +┌──────────────┐ ┌──────────────────┐ +│ kubectl │────>│ etcd (CRD) │ +│ │<────│ PackageRevision │ +└──────────────┘ └────────┬─────────┘ + │ watch + ▼ + ┌──────────────────┐ ┌───────────────┐ ┌─────┐ + │ PR Controller │────>│ Shared Cache │────>│ Git │ + └──────────────────┘ └───────────────┘ └─────┘ +``` + +The CRD is the interface; the controller is the brain. Users express intent by writing to the CRD (set lifecycle, specify source), and the controller makes it happen in Git. Operations are eventually consistent — the controller reconciles on its own schedule rather than within the API request path. + +Content access (`PackageRevisionResources`) still flows through the API Server and Engine, unchanged: + +``` +┌──────────────┐ ┌──────────────────┐ ┌─────────┐ ┌─────┐ +│ kubectl │────>│ Porch API Server │────>│ Engine │────>│ Git │ +└──────────────┘ └──────────────────┘ └─────────┘ └─────┘ +``` + +The Engine's role narrows from full orchestration to content access only. + +## Key Differences + +**Storage model.** In v1alpha1, PackageRevision exists only in Git — the API Server synthesizes it on the fly. In v1alpha2, PackageRevision lives in etcd as a real CRD, with Git as the backing store for content. This gives you native Kubernetes features for free: field selectors, server-side filtering, standard watches, SSA, and standard RBAC without custom authorization logic. + +**Execution model.** v1alpha1 is synchronous — a create request blocks until the package is written to Git and rendered. v1alpha2 is asynchronous — the CRD is created immediately in etcd, and the controller reconciles it in the background. Status conditions (Ready, Rendered) tell you when the work is done. + +**Observability.** In v1alpha1, debugging requires reading API Server logs to understand what happened. In v1alpha2, the CRD's status conditions, events, and standard `kubectl describe` output show the current state and any errors. The controller's reconcile loop is visible through standard controller-runtime metrics. + +**Scalability.** The v1alpha1 API Server is a single process handling all operations. In v1alpha2, the controller scales independently — you can tune concurrency, and the async model naturally handles bursts by queuing work rather than blocking requests. + +**Engine role.** In v1alpha1, the Engine handles everything: lifecycle, tasks, rendering, content access, validation. In v1alpha2, the Engine handles only content access for PackageRevisionResources. Lifecycle, source execution, and rendering move to the PR controller. + +## What's Shared + +Both modes use the same Repository Controller for Git synchronization, the same function runner for KRM function evaluation, and the same PackageVariant/Set controllers for automation. The lifecycle model (Draft → Proposed → Published → DeletionProposed), workspace semantics, revision numbering, and upstream/downstream relationships are identical. + +The Git storage format is also shared — branches for drafts, tags for published packages. A package created in one mode is visible in Git the same way as a package created in the other. + +## Coexistence + +Both modes can run in the same cluster. The v1alpha1 aggregated API and v1alpha2 CRD operate on different API resources and don't conflict. However, they manage separate sets of packages — a package created via v1alpha1 is not automatically visible as a v1alpha2 CRD. Migration tooling exists to move packages between modes.