design-proposals: fold extra into apps — tenant modules as apps with declarative capabilities - #39
design-proposals: fold extra into apps — tenant modules as apps with declarative capabilities#39myasnikovdaniil wants to merge 1 commit into
Conversation
|
Warning Review limit reached
Next review available in: 26 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a design proposal to retire the extra package bucket in Cozystack and fold its functionality into apps using declarative capabilities on ApplicationDefinition (such as visibility, cardinality, protection, and shared-provider capabilities). The review feedback highlights several critical areas for improvement in the design: ensuring the capability propagation logic respects intermediate overrides, explicitly detailing the cleanup and re-evaluation process when a capability provider is deleted, recommending the use of finalizers over validating webhooks for deletion protection, and suggesting asynchronous cardinality enforcement in the HelmRelease reconciler to prevent GitOps pipelines from getting stuck.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| 1. An app instance with `capability.provides: [monitoring]` created in a tenant namespace, marked shareable (`capability.share: children`, §2), causes the platform to set on the tenant's namespace `namespace.cozystack.io/capability.monitoring: <tenantName>` and to propagate it into descendant tenants' `_namespace.capability.monitoring` — **the same label + `_namespace` propagation that exists today**, keyed by capability class instead of a hardcoded name. One honest difference: the *resolution algorithm* is unchanged, but the *writer* moves — from Helm render time into a controller. The writer specification below pins down who writes what. | ||
| 2. A consumer app with `capability.consumes: [monitoring]` resolves the nearest ancestor provider from `_namespace.capability.monitoring`, with the identical nearest-ancestor-inclusive fallback (child's own provider wins; otherwise the parent's; otherwise none → the existing `awaiting-*` beacon pattern, cf. `apps/kubernetes`'s `awaiting-etcd`). | ||
|
|
||
| **Writer specification.** Today's writer is render-time: `tenant/templates/namespace.yaml` computes the five provider names from the Tenant chart's bools plus the parent's `_namespace.*`, stamps them as `namespace.cozystack.io/<module>` labels on the child Namespace, and writes them into the child's `cozystack-values` Secret (labelled `reconcile.fluxcd.io/watch: Enabled`); every app HelmRelease has `valuesFrom: cozystack-values` pinned by `cozystack-api` (`pkg/registry/apps/application/rest.go`) and by the ApplicationDefinition helm reconciler (`internal/controller/applicationdefinition_helmreconciler.go`), so a Secret change re-reconciles the consumers. There is no tenant reconciler in Go today, and while render time *can* observe cluster state (`namespace.yaml` already `lookup`s the parent Namespace for its ownerReference uid), it cannot *fire*: creating a provider instance in tenant X touches nothing that any descendant tenant's release watches, so a lookup-based read-back through the existing `_namespace.*` path would converge only on the periodic reconcile interval — staleness unbounded and multiplied per tree level — and would render empty under helm-unittest. The new trigger needs a controller. **The owner is a new capability reconciler in the existing `cozystack-controller`** (`cmd/cozystack-controller`) — the binary that already hosts the tenant-namespace-label-adjacent reconcilers (`internal/controller/tenantgateway` consumes `namespace.cozystack.io/gateway` and renders cross-namespace ReferenceGrants; `tenantquota`; and the ApplicationDefinition reconcilers already watch both `ApplicationDefinition`s and app HelmReleases). *Trigger → write → propagate:* watch app HelmReleases (already labelled `apps.cozystack.io/application.kind`), join to the owning `ApplicationDefinition`'s `capability.provides` + `share: children`; when a provider instance exists in tenant *X*, SSA-apply `namespace.cozystack.io/capability.<class>: X` on *X*'s namespace and on every descendant tenant namespace without a nearer provider — "descendants of *X*" is a plain label selector, because every tenant namespace already carries its full ancestor chain as `tenant.cozystack.io/<ancestor>` labels (`tenant.ancestorTenantLabels`) — and write the matching `_namespace.capability.*` values. *Values vehicle:* the controller must **not** co-write `cozystack-values` — its `_namespace.*` lives inside the single `values.yaml` stringData key that the Tenant chart re-renders wholesale, so two writers of one field would fight on every tenant upgrade. It owns a second, controller-managed Secret per tenant namespace instead (working name `cozystack-capability-values`), likewise labelled `reconcile.fluxcd.io/watch: Enabled` and appended as an optional `valuesFrom` at the same two injection points — descendant releases re-resolve on change through the identical Flux watch mechanism. Those two injection points are verified single-writer chokepoints, which makes the append well-defined: `rest.go` hardcodes `ValuesFrom: [{Secret cozystack-values}]` on every HelmRelease it emits, and the helm reconciler's `expectedValuesFrom()` + `valuesFromEqual` actively *revert* any HelmRelease whose `valuesFrom` list diverges from the expected one (the comparison covers `Optional` too) — so this is a code change at exactly those two sites, after which the same enforcement guarantees every app HelmRelease carries both references, and nothing else can add or strip entries. Merge order is safe by helm-controller's composition rules: `valuesFrom` entries deep-merge in list order (later wins), then inline `spec.values` merges last — so the capability Secret, second in the list, nests `_namespace.capability.*` under `_namespace` alongside the legacy keys without touching them (only an identical leaf key could collide, which is exactly why the capability keyspace is `capability.<class>` rather than reusing `_namespace.<module>`), and the user's own app values keep their existing precedence over both Secrets (`Values: app.Spec` in `rest.go` — inline values already outrank `cozystack-values` today, unchanged trust property). Note the Secret is load-bearing as the *trigger*, not just the vehicle: the `reconcile.fluxcd.io/watch: Enabled` label is what re-reconciles descendant releases when a value changes — a namespace label alone re-renders nothing, which is why "consumers read the capability label directly" is not a simpler alternative. Net effect on the consumer contract: consumers keep the exact `.Values._namespace.*` read pattern; only the key gains a `capability.` segment, and during the compat window both keys resolve. *Who wins during the compat window:* nobody has to — ownership is disjoint, not merged. The Tenant chart (helm-controller's field manager) stays the sole writer of the legacy label keys (`namespace.cozystack.io/monitoring`, …) and of `cozystack-values`; the capability reconciler (its own SSA field manager) is the sole writer of the `namespace.cozystack.io/capability.*` keys and of its own Secret. Disjoint label keys on the Namespace and disjoint Secret objects mean the two field managers never contend, and a Tenant-chart upgrade cannot strip labels it never owned; phase 4 is simply the Tenant chart ceasing to emit the legacy keys. (Home: **recommended `cozystack-controller`** — confirm with maintainers, but the evidence is one-sided rather than taste. That binary already registers every watch surface this reconciler needs (`cmd/cozystack-controller/main.go`): the ApplicationDefinition reconcilers that manage app HelmReleases by the `apps.cozystack.io/application.*` labels, and `tenantgateway`, which already does the exact shape of write proposed here — SSA-patching a `namespace.cozystack.io/*` label onto tenant namespaces (`ensureNamespaceLabels` / `patchNamespaceGatewayLabel`, `internal/controller/tenantgateway/reconciler.go`). `cozystack-operator` registers only the PackageSource/Package reconcilers plus `cozyvaluesreplicator`, whose target selector is `cozystack.io/system=true` (`cmd/cozystack-operator/main.go`) — platform bootstrap and system-namespace scope, with no tenant-namespace, ApplicationDefinition, or app-HelmRelease watches. The capability write path is tenantgateway-shaped, not replicator-shaped.) |
There was a problem hiding this comment.
The propagation logic needs to handle intermediate overrides carefully. If the controller uses a plain label selector (e.g., tenant.cozystack.io/X: "true") to find all descendants of namespace.cozystack.io/capability.<class>: X, it will overwrite descendant namespaces that have a "nearer" provider
| 1. An app instance with `capability.provides: [monitoring]` created in a tenant namespace, marked shareable (`capability.share: children`, §2), causes the platform to set on the tenant's namespace `namespace.cozystack.io/capability.monitoring: <tenantName>` and to propagate it into descendant tenants' `_namespace.capability.monitoring` — **the same label + `_namespace` propagation that exists today**, keyed by capability class instead of a hardcoded name. One honest difference: the *resolution algorithm* is unchanged, but the *writer* moves — from Helm render time into a controller. The writer specification below pins down who writes what. | ||
| 2. A consumer app with `capability.consumes: [monitoring]` resolves the nearest ancestor provider from `_namespace.capability.monitoring`, with the identical nearest-ancestor-inclusive fallback (child's own provider wins; otherwise the parent's; otherwise none → the existing `awaiting-*` beacon pattern, cf. `apps/kubernetes`'s `awaiting-etcd`). | ||
|
|
||
| **Writer specification.** Today's writer is render-time: `tenant/templates/namespace.yaml` computes the five provider names from the Tenant chart's bools plus the parent's `_namespace.*`, stamps them as `namespace.cozystack.io/<module>` labels on the child Namespace, and writes them into the child's `cozystack-values` Secret (labelled `reconcile.fluxcd.io/watch: Enabled`); every app HelmRelease has `valuesFrom: cozystack-values` pinned by `cozystack-api` (`pkg/registry/apps/application/rest.go`) and by the ApplicationDefinition helm reconciler (`internal/controller/applicationdefinition_helmreconciler.go`), so a Secret change re-reconciles the consumers. There is no tenant reconciler in Go today, and while render time *can* observe cluster state (`namespace.yaml` already `lookup`s the parent Namespace for its ownerReference uid), it cannot *fire*: creating a provider instance in tenant X touches nothing that any descendant tenant's release watches, so a lookup-based read-back through the existing `_namespace.*` path would converge only on the periodic reconcile interval — staleness unbounded and multiplied per tree level — and would render empty under helm-unittest. The new trigger needs a controller. **The owner is a new capability reconciler in the existing `cozystack-controller`** (`cmd/cozystack-controller`) — the binary that already hosts the tenant-namespace-label-adjacent reconcilers (`internal/controller/tenantgateway` consumes `namespace.cozystack.io/gateway` and renders cross-namespace ReferenceGrants; `tenantquota`; and the ApplicationDefinition reconcilers already watch both `ApplicationDefinition`s and app HelmReleases). *Trigger → write → propagate:* watch app HelmReleases (already labelled `apps.cozystack.io/application.kind`), join to the owning `ApplicationDefinition`'s `capability.provides` + `share: children`; when a provider instance exists in tenant *X*, SSA-apply `namespace.cozystack.io/capability.<class>: X` on *X*'s namespace and on every descendant tenant namespace without a nearer provider — "descendants of *X*" is a plain label selector, because every tenant namespace already carries its full ancestor chain as `tenant.cozystack.io/<ancestor>` labels (`tenant.ancestorTenantLabels`) — and write the matching `_namespace.capability.*` values. *Values vehicle:* the controller must **not** co-write `cozystack-values` — its `_namespace.*` lives inside the single `values.yaml` stringData key that the Tenant chart re-renders wholesale, so two writers of one field would fight on every tenant upgrade. It owns a second, controller-managed Secret per tenant namespace instead (working name `cozystack-capability-values`), likewise labelled `reconcile.fluxcd.io/watch: Enabled` and appended as an optional `valuesFrom` at the same two injection points — descendant releases re-resolve on change through the identical Flux watch mechanism. Those two injection points are verified single-writer chokepoints, which makes the append well-defined: `rest.go` hardcodes `ValuesFrom: [{Secret cozystack-values}]` on every HelmRelease it emits, and the helm reconciler's `expectedValuesFrom()` + `valuesFromEqual` actively *revert* any HelmRelease whose `valuesFrom` list diverges from the expected one (the comparison covers `Optional` too) — so this is a code change at exactly those two sites, after which the same enforcement guarantees every app HelmRelease carries both references, and nothing else can add or strip entries. Merge order is safe by helm-controller's composition rules: `valuesFrom` entries deep-merge in list order (later wins), then inline `spec.values` merges last — so the capability Secret, second in the list, nests `_namespace.capability.*` under `_namespace` alongside the legacy keys without touching them (only an identical leaf key could collide, which is exactly why the capability keyspace is `capability.<class>` rather than reusing `_namespace.<module>`), and the user's own app values keep their existing precedence over both Secrets (`Values: app.Spec` in `rest.go` — inline values already outrank `cozystack-values` today, unchanged trust property). Note the Secret is load-bearing as the *trigger*, not just the vehicle: the `reconcile.fluxcd.io/watch: Enabled` label is what re-reconciles descendant releases when a value changes — a namespace label alone re-renders nothing, which is why "consumers read the capability label directly" is not a simpler alternative. Net effect on the consumer contract: consumers keep the exact `.Values._namespace.*` read pattern; only the key gains a `capability.` segment, and during the compat window both keys resolve. *Who wins during the compat window:* nobody has to — ownership is disjoint, not merged. The Tenant chart (helm-controller's field manager) stays the sole writer of the legacy label keys (`namespace.cozystack.io/monitoring`, …) and of `cozystack-values`; the capability reconciler (its own SSA field manager) is the sole writer of the `namespace.cozystack.io/capability.*` keys and of its own Secret. Disjoint label keys on the Namespace and disjoint Secret objects mean the two field managers never contend, and a Tenant-chart upgrade cannot strip labels it never owned; phase 4 is simply the Tenant chart ceasing to emit the legacy keys. (Home: **recommended `cozystack-controller`** — confirm with maintainers, but the evidence is one-sided rather than taste. That binary already registers every watch surface this reconciler needs (`cmd/cozystack-controller/main.go`): the ApplicationDefinition reconcilers that manage app HelmReleases by the `apps.cozystack.io/application.*` labels, and `tenantgateway`, which already does the exact shape of write proposed here — SSA-patching a `namespace.cozystack.io/*` label onto tenant namespaces (`ensureNamespaceLabels` / `patchNamespaceGatewayLabel`, `internal/controller/tenantgateway/reconciler.go`). `cozystack-operator` registers only the PackageSource/Package reconcilers plus `cozyvaluesreplicator`, whose target selector is `cozystack.io/system=true` (`cmd/cozystack-operator/main.go`) — platform bootstrap and system-namespace scope, with no tenant-namespace, ApplicationDefinition, or app-HelmRelease watches. The capability write path is tenantgateway-shaped, not replicator-shaped.) |
There was a problem hiding this comment.
The design should explicitly specify how the controller handles the deletion of a capability provider. When a provider in tenant namespace.cozystack.io/capability.<class> label from cozystack-capability-values Secret accordingly to trigger re-reconciliation of the consumers.\n\nWithout this cleanup and re-evaluation, consumers will continue to reference the deleted provider, leading to broken connections.
|
|
||
| - Model tenant singletons as `cardinality.scope: tenant, max: 1`, or introduce a distinct "tenant service" flavour? (Prefer the former — fewer concepts.) | ||
| - Keep the Tenant bools as permanent ergonomic sugar, or force explicit app creation after phase 4? | ||
| - `protection`: finalizer vs validating webhook vs RBAC-only — which enforcement? |
There was a problem hiding this comment.
For protection enforcement, a finalizer is highly recommended over a validating webhook or RBAC-only rules.\n\nValidating webhooks can be bypassed under certain cluster administration conditions or during API server disruptions, and RBAC-only rules do not prevent accidental deletions by authorized administrators or automated service accounts. A finalizer ensures that the resource cannot be physically deleted from the cluster until all active references (e.g., child tenant dependencies) are verified to be gone, preventing dangling references and broken states.
| - Model tenant singletons as `cardinality.scope: tenant, max: 1`, or introduce a distinct "tenant service" flavour? (Prefer the former — fewer concepts.) | ||
| - Keep the Tenant bools as permanent ergonomic sugar, or force explicit app creation after phase 4? | ||
| - `protection`: finalizer vs validating webhook vs RBAC-only — which enforcement? | ||
| - Cardinality enforcement point: `cozystack-api` admission only sees instances created through the aggregated API; the Tenant sugar (§4) and hand-authored `HelmRelease`s do not pass through it. Enforce additionally at the `HelmRelease` level (validating webhook / policy), or treat platform-rendered releases as trusted and document the gap? |
There was a problem hiding this comment.
For the cardinality enforcement point, using a validating webhook on HelmRelease can be problematic for GitOps workflows (e.g., Flux), as blocking the admission of a HelmRelease can cause the GitOps pipeline to get stuck.\n\nA more GitOps-friendly approach is to enforce this asynchronously in the HelmRelease reconciler (within cozystack-controller). If a HelmRelease violates the cardinality constraint defined in its ApplicationDefinition, the reconciler can mark the HelmRelease status as Failed or Errored with a clear message, rather than blocking the API write.
Retire the packages/extra bucket by folding those packages into apps and expressing hidden-from-catalog / per-tenant-singleton / shared-down-the-tree as declarative capabilities on ApplicationDefinition (visibility, cardinality, protection, capability with share). Supersedes community#4 (tenant module overrides); complementary to community#25 (per-cluster etcd). Grounded against cozystack main @ cece5c23b. Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
1836bc8 to
5ab45ec
Compare
…rapping apps/kubernetes Supersede the preset-field revision (#27): deliver ComputePlane as a Cozystack-owned tenant module (packages/extra/computeplane) that deploys the ordinary apps/kubernetes chart with operator-fixed values via a source-only PackageSource re-declaration. No new CRD, no new controller, no fields on kind: Kubernetes; the module registers its own thin ComputePlane module-kind. Addressing the review: state the computeplane release name as a stable external contract (renaming it would at best dangle every placement: ComputePlane secretRef and at worst have Helm uninstall a live Kamaji cluster), and ground the design's shape structurally — a second ApplicationDefinition because AD properties are per-kind, plus a wrapper chart because the AD carries no fixed-values facility — rather than in the apps-vs-extra directory split, so the rationale composes with #39's capability-field model tracked as a separate stream. Assisted-By: Claude Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
…rapping apps/kubernetes Supersede the preset-field revision (#27): deliver ComputePlane as a Cozystack-owned tenant module (packages/extra/computeplane) that deploys the ordinary apps/kubernetes chart with operator-fixed values via a source-only PackageSource re-declaration. No new CRD, no new controller, no fields on kind: Kubernetes; the module registers its own thin ComputePlane module-kind. Addressing the review: state the computeplane release name as a stable external contract (renaming it would at best dangle every placement: ComputePlane secretRef and at worst have Helm uninstall a live Kamaji cluster), and ground the design's shape structurally — a second ApplicationDefinition because AD properties are per-kind, plus a wrapper chart because the AD carries no fixed-values facility — rather than in the apps-vs-extra directory split, so the rationale composes with #39's capability-field model tracked as a separate stream. Assisted-By: Claude Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Overall I like this and I don't see a blocker. Let me add some history on why Why
|
The record was backfilled from an argument between other people and got several things wrong. Corrections, all verified against the threads and the code: Deciders is @kvaps and @lllamnyp — the author of #33 and the maintainer who approved it. @myasnikovdaniil's #26 forced the reopening and is now credited in Context, where it belongs; its literal ask (no new kind at all) is not what was adopted. The record never cited #33, the pull request where the decision was actually argued and merged, and where its 2026-07-18 date comes from. A record whose job is to be the durable pointer to the reasoning pointed everywhere except at it. It is now in the new Decided in field. The #17 attribution was wrong. That review was an approval, and it did not argue that ComputePlane should not be a distinct kind — that is #26's argument alone, which #26 made while quoting the approval in support. What the review did contribute is the tamper-resistance argument (withhold admin, not visibility), which is the load-bearing rationale of this record's own Decision and Consequences and was credited to nobody. Implemented in was "not yet"; cozystack/cozystack#3280 merged 2026-07-29. The rejection of the preset-field model rested on the fragile argument — packages/extra/computeplane and the PackageSource convention, both of which #39 would dissolve. Replaced with the structural one, which survives it: capabilities and schemas are per-kind, ApplicationDefinition has no fixed-values facility, the conversion makes the tenant's spec become the Helm values, and a narrow openAPISchema does not substitute because a field you can default is a field the tenant can override. The release-name invariant is now a link to the guard and its test rather than a restatement. The prose said it constrained moving the chart between directories; the actual mechanism is that the aggregated API rebuilds HelmRelease specs without spec.releaseName, which the test's suite comment states correctly. Added the dropped consequence: #33 was asked to co-land with #39 or land after it, because once tenants can set computeplane the invariant becomes a migration of live Kamaji clusters holding tenant data. #3280 has merged and #39 is open, so that constraint is live now rather than composing. Noted that #27 was closed unmerged, superseded by #33 the same day. In a document that is explicitly a history, that matters. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The record was backfilled from an argument between other people and got several things wrong. Corrections, all verified against the threads and the code: Deciders is @kvaps and @lllamnyp — the author of #33 and the maintainer who approved it. @myasnikovdaniil's #26 forced the reopening and is now credited in Context, where it belongs; its literal ask (no new kind at all) is not what was adopted. The record never cited #33, the pull request where the decision was actually argued and merged, and where its 2026-07-18 date comes from. A record whose job is to be the durable pointer to the reasoning pointed everywhere except at it. It is now in the new Decided in field. The #17 attribution was wrong. That review was an approval, and it did not argue that ComputePlane should not be a distinct kind — that is #26's argument alone, which #26 made while quoting the approval in support. What the review did contribute is the tamper-resistance argument (withhold admin, not visibility), which is the load-bearing rationale of this record's own Decision and Consequences and was credited to nobody. Implemented in was "not yet"; cozystack/cozystack#3280 merged 2026-07-29. The rejection of the preset-field model rested on the fragile argument — packages/extra/computeplane and the PackageSource convention, both of which #39 would dissolve. Replaced with the structural one, which survives it: capabilities and schemas are per-kind, ApplicationDefinition has no fixed-values facility, the conversion makes the tenant's spec become the Helm values, and a narrow openAPISchema does not substitute because a field you can default is a field the tenant can override. The release-name invariant is now a link to the guard and its test rather than a restatement. The prose said it constrained moving the chart between directories; the actual mechanism is that the aggregated API rebuilds HelmRelease specs without spec.releaseName, which the test's suite comment states correctly. Added the dropped consequence: #33 was asked to co-land with #39 or land after it, because once tenants can set computeplane the invariant becomes a migration of live Kamaji clusters holding tenant data. #3280 has merged and #39 is open, so that constraint is live now rather than composing. Noted that #27 was closed unmerged, superseded by #33 the same day. In a document that is explicitly a history, that matters. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…se all open questions Second draft of the per-cluster etcd proposal. The framing changes and every open question from the first review round is answered. Reframing: the proposal is about completing packages/apps/kubernetes, which since the control-plane / node-pool split (community#8) is the control-plane app. Every control-plane component is rendered by that chart except the one holding all of its state. Retiring the etcd tenant-module wiring is a consequence of fixing that, not the goal. This makes the relationship to community#39 explicit: #39 owns where packages live and how modules declare themselves, this owns what the control-plane app contains. Disjoint files, neither blocks the other. Resolved questions, replacing the five open ones: - Migration: the CAPI provider does re-project dataStoreName onto TenantControlPlane.spec.dataStore on every reconcile, so a live switch works, but it costs a read-only freeze window and a kubelet restart on every tenant worker node. Migration is therefore an operator-initiated maintenance runbook, never an unattended platform migration. - Standalone etcd: survives. Only the Tenant.spec.etcd bool and the _namespace.etcd propagation retire; the chart and its ApplicationDefinition stay. - Default replicas: 3. - Reuse mechanism: shared named templates in cozy-lib, not a subchart. The etcd chart hard-fails unless the release is named etcd, a subchart inherits the parent release name, and the literal etcd-*-tls names are pinned by an immutable spec.tls on adopted clusters. - Footprint: surfaced in NOTES.txt and the app README. Also folded in: the minimal values surface from cozystack#3179 (only etcd.replicas, with the StorageClass derived from it at platform level), etcd-operator v1alpha2 and the Available condition throughout, a name-length section with the real 53-character release cap and the operator's derived names, a delete hook that reaps a per-cluster etcd but never a shared one, corrected source citations, and a migration slot of 56+ rather than the stale 49. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The record was backfilled from an argument between other people and got several things wrong. Corrections, all verified against the threads and the code: Deciders is @kvaps and @lllamnyp — the author of #33 and the maintainer who approved it. @myasnikovdaniil's #26 forced the reopening and is now credited in Context, where it belongs; its literal ask (no new kind at all) is not what was adopted. The record never cited #33, the pull request where the decision was actually argued and merged, and where its 2026-07-18 date comes from. A record whose job is to be the durable pointer to the reasoning pointed everywhere except at it. It is now in the new Decided in field. The #17 attribution was wrong. That review was an approval, and it did not argue that ComputePlane should not be a distinct kind — that is #26's argument alone, which #26 made while quoting the approval in support. What the review did contribute is the tamper-resistance argument (withhold admin, not visibility), which is the load-bearing rationale of this record's own Decision and Consequences and was credited to nobody. Implemented in was "not yet"; cozystack/cozystack#3280 merged 2026-07-29. The rejection of the preset-field model rested on the fragile argument — packages/extra/computeplane and the PackageSource convention, both of which #39 would dissolve. Replaced with the structural one, which survives it: capabilities and schemas are per-kind, ApplicationDefinition has no fixed-values facility, the conversion makes the tenant's spec become the Helm values, and a narrow openAPISchema does not substitute because a field you can default is a field the tenant can override. The release-name invariant is now a link to the guard and its test rather than a restatement. The prose said it constrained moving the chart between directories; the actual mechanism is that the aggregated API rebuilds HelmRelease specs without spec.releaseName, which the test's suite comment states correctly. Added the dropped consequence: #33 was asked to co-land with #39 or land after it, because once tenants can set computeplane the invariant becomes a migration of live Kamaji clusters holding tenant data. #3280 has merged and #39 is open, so that constraint is live now rather than composing. Noted that #27 was closed unmerged, superseded by #33 the same day. In a document that is explicitly a history, that matters. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Summary
Retire the
packages/extrabucket. Every package underpackages/extrabecomes a regularappspackage (or moves tocore/system), and the three things that actually distinguish anextrapackage today — hidden from the catalog, at most one per tenant, and shared down the tenant tree — become declarative capabilities on anyApplicationDefinition(visibility,cardinality,protection,capabilitywithshare), not a directory boundary.extrais not a distinct runtime mechanism: anextrapackage uses the samePackageSourceregistration, the same chart patterns, and the sameApplicationDefinitionas anappspackage. The differences are a few dashboard/release flags and the way the Tenant chart wires provider discovery. Expressed as data, the second bucket disappears and any app can opt into per-tenant-singleton, hidden, or shared-provider behaviour.Relationship to other proposals
@embed/valuesOverridelayer is unnecessary once modules are first-class apps with opt-in capabilities; this proposal will close design-proposals: tenant module overrides #4 as Superseded.extramodule). design-proposal(compute-plane): deliver as an operator-owned extra module wrapping apps/kubernetes (supersedes #27) #33 is the live pull toward extendingextra; this proposal argues that direction taxes every new module with the rigid extra shape. Sequencing to be coordinated.Status
Draft PR — opening for early visibility while the open questions settle. Grounded against
cozystackmain @cece5c23b; every code claim (registration parity, tenant-chart provider resolution, console filter, migration mechanism, writer path) was verified against the source. Open questions are listed in the doc.