0006 — ApplicationInstance placeholder mode¶
Status: Accepted (2026-06-27)
Context¶
Operation CRs (backup, restore, upgrade, migration, runcommand, runbook) are
standalone Kubernetes resources, but at every layer they are hard-coupled to a
target ApplicationInstance via spec.targetRef:
- the admission webhook rejects an
Operationwhose targetApplicationInstancedoes not exist, and requires the target to carry the matching capability annotationops.vworkspace.io/<verb>=<engine>; - the operation controller loads the target by name and blocks if it is absent.
There is no way to express an infra / cluster-level operation — "run this
command on the cluster", "execute this runbook" — that is not "about" a deployed
application. The engines that matter for infra operations (job for
runcommand, helmHookJob for migration, workflow for runbook) only need
the target's namespace; they never read a Helm release. So a per-cluster
placeholder / cluster-ops ApplicationInstance that owns no workload but
advertises infra capability annotations is sufficient to be a valid targetRef.
The blocker was the reconciler: today it always runs EnsureRelease and
derives Ready purely from HelmRelease status (see ADR
0002). A no-op placeholder backed by a
real HelmRelease (Option A in the hub design) drags in spurious Helm objects
and a Flux dependency for something conceptually empty.
The cross-cutting product design lives in the hub:
vworkspace/docs/design/cluster-ops-placeholder-instance.md (Option A vs B).
Decision¶
Add a mode field to ApplicationInstanceSpec:
// +kubebuilder:validation:Enum=managed;placeholder
type InstanceMode string
const (
InstanceModeManaged InstanceMode = "managed"
InstanceModePlaceholder InstanceMode = "placeholder"
)
spec.modedefaults tomanaged(CRD+kubebuilder:default=managed); empty is treated asmanagedin code. Existing instances are unaffected.spec.chart,spec.release, andspec.valuesbecome optional at the CRD level. Managed-mode validation still requires them (unchanged); placeholder validation forbidschart/valuesand, ifreleaseis set, still enforcesrelease.namespace == metadata.namespace.
Reconciler behaviour for mode: placeholder:
- skip
EnsureRelease(andDeleteReleaseon finalize) — no Helm interaction at all, and nohelmengine.Enginedependency required; - set
Ready=True(Reason=Placeholder),Reconciling=False,Degraded=False,Blocked=Falsedirectly and deterministically; - preserve the
ops.vworkspace.io/*capability annotations untouched so the operation webhook capability check passes; - keep finalizer handling clean — nothing to uninstall.
managed keeps the existing Helm-first behaviour from ADR 0002 verbatim. The
admission webhook applies the same placeholder contract as the reconciler —
rejecting forbidden chart/values and an out-of-namespace release at
admission time — while skipping only the inline-secret scan for placeholders
(they carry no chart values). It is unchanged for managed instances.
To avoid orphaning a Helm release, the webhook also rejects a managed →
placeholder update for any instance that owns (or may own) a release — i.e.
status.helmReleaseRef is set, or the old managed spec has chart/release
configured (a release may already be materializing before its status is
written). The placeholder reconcile path performs no Helm work, so it would
never uninstall the release. Operators must delete the instance — which
uninstalls the release through the managed finalizer — and recreate it as a
placeholder. Only an unconfigured managed instance (no chart/release and no
release status) may be converted in place. As defence-in-depth,
reconcilePlaceholder also clears any leftover status.helmReleaseRef /
lastAppliedChart so a placeholder never reports Ready next to a stale
release reference.
This is Option B from the hub design — the recommended target end-state. Option A (empty/raw chart) is not implemented.
Consequences¶
- Clean placeholders. A cluster-ops instance reaches
Readyinstantly with no strayHelmRelease, no Flux dependency, and no workload — honestly modelling "this instance has no chart". - A narrow, well-contained operator change. One enum field, one reconciler
branch, mode-aware validation, and tests. The
Operationpipeline, the capability/concurrency model, and the engines are untouched. - Backward compatible.
modedefaults tomanaged; loosening the chart/release/values CRD requirements only relaxes admission — managed instances that omit them are still blocked by controller validation, exactly as before they could not be created. - Security boundary unchanged. The capability annotation set is still the
allow-list enforced by the webhook; a placeholder advertises only the infra
verbs it needs. The per-cluster ops-instance lifecycle/UI and RBAC profile
review are tracked separately (hub
P6-T005). - Does not supersede ADR 0002. Helm-first remains the default and the only behaviour for managed instances; this ADR adds a non-Helm mode for the cluster-ops sentinel only.