From d8e6fc3f22de47b6bc823dd4f2a3afd08478671e Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Wed, 12 Aug 2026 15:19:49 +0500 Subject: [PATCH 1/9] docs(decisions): add a decision record log alongside design proposals Design proposals are intent, written before the work. When implementation contradicts the design the proposal gets revised in place, so it ends up reading as though it always said the current thing -- and the reasoning that changed our minds survives only in a pull-request diff. Add decisions/ for that reasoning: one page per settled decision, never edited after merge, superseded rather than rewritten. Reviewed for accuracy by one maintainer rather than by consensus, because the decision has already been made. Wire it into the "Where do I file?" table and rewrite Proposal Drift to say what to do when the implementation diverges. Assisted-By: Claude Signed-off-by: Myasnikov Daniil --- README.md | 2 + decisions/README.md | 91 ++++++++++++++++++++++++++++++++++++++ decisions/template.md | 59 ++++++++++++++++++++++++ design-proposals/README.md | 4 +- 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 decisions/README.md create mode 100644 decisions/template.md diff --git a/README.md b/README.md index ae43aef..f1da810 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This repository serves as the home for everything related to how we collaborate, This repo contains: - [Design proposals](./design-proposals/README.md) — cross-cutting and architectural changes +- [Decision records](./decisions/README.md) — what we decided and why, once a question is settled - [The community meeting](./community_meeting.md) — when it happens, how to join, how to add a topic - [The meeting archive](./meetings/README.md) — every past meeting, its topics and its recording - Community resources and helpful links (below) @@ -26,6 +27,7 @@ Cozystack work is split across two repositories. This repo is for **how we decid | A **bug** or a **concrete, scoped feature** | [cozystack/cozystack issues](https://github.com/cozystack/cozystack/issues/new/choose) | | A **usage question** or an early idea | [cozystack/cozystack Discussions](https://github.com/cozystack/cozystack/discussions) | | A **cross-cutting / architectural change** — affects multiple components or APIs, or needs a decision before code | A [design proposal](./design-proposals/README.md) (a PR) **here** | +| A **decision already made** whose reasoning would otherwise survive only in a pull-request thread | A [decision record](./decisions/README.md) (a PR) **here** | | **Governance, process, or community** matters | An issue **here** | ## Community Resources diff --git a/decisions/README.md b/decisions/README.md new file mode 100644 index 0000000..8a26b86 --- /dev/null +++ b/decisions/README.md @@ -0,0 +1,91 @@ +# Cozystack Decision Records + +This folder holds **decision records**: short, dated notes on architectural decisions the project has actually made, and why. + +A [design proposal](../design-proposals/README.md) is intent — what we think we should build, written before the work. A decision record is history — what we settled on, written once the question is closed. Both are useful; the second is the one we have been missing. + +## Why a separate folder + +Design proposals get edited. When implementation contradicts the design — and it regularly does — the proposal is revised in place, so it ends up reading as though it always said the current thing. The reasoning that changed our minds (*we tried X, hit a concrete constraint, moved to Y*) then survives only in a pull-request diff that nobody will find in a year. + +That reasoning is the most expensive thing we produce and the easiest to lose. A decision record is where it goes. + +| | Design proposal | Decision record | +|---|---|---| +| Written | before the work, to get agreement | once the question is settled | +| Answers | "should we, and how might we?" | "what did we decide, and why not the alternatives?" | +| Edited after merge | yes, as the design evolves | never — superseded by a new record | +| Length | as long as it needs to be | one page | +| Review | consensus from maintainers | one maintainer checks it for accuracy | + +Both have an alternatives section, and they are not the same list. A proposal weighs the options we could imagine *before* building. A record names the option that lost *during* building — frequently the proposal's own original design. + +## When to write one + +Write a decision record when a future contributor would otherwise have to reconstruct the reasoning from a pull-request thread. In practice: + +- Implementation contradicted an accepted proposal and the design changed course. +- Two viable approaches existed and we picked one for reasons that are not visible in the resulting code. +- We hit a constraint that now shapes the design — an upstream limitation, a Kubernetes semantic, a vulnerability class. +- The code enforces a contract it cannot explain: a field is immutable, a release name is load-bearing, an ordering is required. +- We deliberately decided *not* to do something, and the question keeps coming back. + +Do not write one for: + +- Routine code choices that the diff and the tests already explain. +- Operator-facing how-to — that is user documentation, and it belongs on [the website](https://cozystack.io/docs/). +- The mechanics of the product repository's own workflow (release process, changelog conventions) — those live next to the code in [cozystack/cozystack](https://github.com/cozystack/cozystack). +- A decision that has not been made yet. That is a design proposal, or an open question inside one. + +## How to write one + +1. Copy [`template.md`](./template.md) to `decisions/NNNN-short-slug.md`, taking the next free number: + + ``` + ./decisions/0007-etcd-is-per-cluster-not-per-tenant.md + ``` + +1. **Title the decision, not the topic.** `storageClass is immutable after creation`, not `storageClass immutability`. Someone scanning the folder should learn what we decided from the filename alone. + +1. **Keep it to a page.** Link out to the proposal, the code and the pull requests for detail. A record that grows into a second design document will not get read. + +1. **Link both ways.** The record links its proposal and the pull requests that implemented it; the proposal's `Decisions` section links back to the record. + +1. As with all commits in CNCF projects, sign the commit for the DCO check: + + ```bash + git commit --signoff + ``` + +Two pull requests can claim the same number. Whoever merges second renumbers on rebase — a file rename and a couple of link fixes. + +## Status and immutability + +| Status | Meaning | +|---|---| +| `Accepted` | Current. The decision stands. | +| `Superseded by NNNN` | A later record replaced it. The original text stays exactly as written. | +| `Reverted` | We undid it. Say what we do instead. | + +Once merged, **the body of an accepted record is not edited.** Fix typos and broken links; change nothing else. If the decision changes, write a new record and mark the old one `Superseded by NNNN`. + +This one rule is what makes the folder a history instead of a second set of documents to keep current. A record that gets quietly rewritten is worth no more than the proposal it was meant to supplement. + +## Review + +A decision record documents a decision that has already been made. Review therefore checks that the record is **accurate** — not whether the reviewer agrees with it. One maintainer's approval is enough, and it should be quick. + +If review turns into re-litigating the decision, that is a signal the decision was not actually settled. Close the pull request and open a design proposal or an issue instead. + +## Where this sits relative to everything else + +| The question you are answering | Where it is answered | +|---|---| +| "Why is it built this way, and not the obvious other way?" | A decision record, here | +| "What should we build?" | A [design proposal](../design-proposals/README.md), here | +| "How do I use it?" | [User documentation](https://cozystack.io/docs/) on the website | +| "How does this repository release, or write changelogs?" | [cozystack/cozystack](https://github.com/cozystack/cozystack), next to the code | + +## Inspiration + +The format follows [Michael Nygard's architecture decision records](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) and [MADR](https://adr.github.io/madr/), trimmed to the sections we will actually fill in. diff --git a/decisions/template.md b/decisions/template.md new file mode 100644 index 0000000..ae64a11 --- /dev/null +++ b/decisions/template.md @@ -0,0 +1,59 @@ + +# NNNN. State the decision here, as a fact + +- **Number:** `NNNN` +- **Date:** `YYYY-MM-DD` +- **Status:** Accepted | Superseded by `NNNN` | Reverted +- **Deciders:** `@your-github-handle, @codecider` +- **Proposal:** `design-proposals//README.md` — or `none` +- **Implemented in:** `cozystack/cozystack#NNNN` — or `not yet` + + + +## Context + + + +## Decision + + + +## Why not the alternatives + + + +## Consequences + + + +## Revisit if + + + +--- + + diff --git a/design-proposals/README.md b/design-proposals/README.md index 83fba99..e473ce3 100644 --- a/design-proposals/README.md +++ b/design-proposals/README.md @@ -58,7 +58,9 @@ Proposals are reviewed in GitHub PRs. Once a proposal receives consensus from co We understand that real-world implementation may diverge from initial designs. This is normal and expected. -While it's helpful to update the proposal if the divergence is significant, the **codebase and user documentation are the final source of truth**. +Update the proposal when the divergence is significant. When the divergence came from a decision worth remembering — an approach that failed, a constraint you discovered, an alternative you picked instead — also write a [decision record](../decisions/README.md) and link it from the proposal's `Decisions` section. Editing the proposal alone loses the reasoning: the revised text reads as though it always said the current thing, and *why* the design changed course survives only in the pull-request diff. + +The **codebase and user documentation remain the final source of truth** for what the system does. The decision records are the source of truth for why it does it that way. ## Inspiration From 4a6f381fd9039ef223d7a861de187b453cf83e97 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Wed, 12 Aug 2026 15:19:55 +0500 Subject: [PATCH 2/9] docs(decisions): backfill 0001, ComputePlane as an operator-owned module The ComputePlane delivery shape was decided three times: a tenant module with an inline cluster HelmRelease (#17), preset fields on kind: Kubernetes (#27, closed), and finally an operator-owned extra module wrapping the unchanged apps/kubernetes (#33). Only the last one is visible in the proposal today; why the preset-field model lost lives in the #33 body. Record it as the first decision, and add the Decisions section to the proposal template so later records get linked from the proposal they amend. Assisted-By: Claude Signed-off-by: Myasnikov Daniil --- ...plane-ships-as-an-operator-owned-module.md | 44 +++++++++++++++++++ design-proposals/compute-plane/README.md | 4 ++ design-proposals/template.md | 12 +++++ 3 files changed, 60 insertions(+) create mode 100644 decisions/0001-computeplane-ships-as-an-operator-owned-module.md diff --git a/decisions/0001-computeplane-ships-as-an-operator-owned-module.md b/decisions/0001-computeplane-ships-as-an-operator-owned-module.md new file mode 100644 index 0000000..0936623 --- /dev/null +++ b/decisions/0001-computeplane-ships-as-an-operator-owned-module.md @@ -0,0 +1,44 @@ +# 0001. ComputePlane ships as an operator-owned module, not as preset fields on `kind: Kubernetes` + +- **Number:** `0001` +- **Date:** `2026-07-18` +- **Status:** Accepted +- **Deciders:** `@kvaps, @myasnikovdaniil` +- **Proposal:** [`design-proposals/compute-plane/README.md`](../design-proposals/compute-plane/README.md) +- **Implemented in:** not yet — see the proposal's Rollout section + +## Context + +The first ComputePlane revision ([#17](https://github.com/cozystack/community/pull/17), merged 2026-06-30) delivered a hardened, operator-controlled Kubernetes cluster for running code-executing catalog apps — notebooks, workflow code nodes, plugin systems — behind a per-VM kernel boundary instead of as shared-kernel pods on the management cluster. It was shaped as a tenant module selected by a single string (`computePlane: ""`), with the cluster's HelmRelease rendered inline in `apps/tenant/templates`. + +Two reviews converged on the same objection ([#26](https://github.com/cozystack/community/issues/26), and Timofei Larkin on [#17](https://github.com/cozystack/community/pull/17)): the cluster a ComputePlane runs on *is* an ordinary managed `kind: Kubernetes` — Kamaji control plane, KubeVirt-VM workers, operator-held kubeconfig — so ComputePlane should not become a distinct kind with a parallel cluster implementation, reconcile loop and RBAC surface. + +Revision [#27](https://github.com/cozystack/community/pull/27) answered that by putting the posture directly on the existing app as two orthogonal user-facing preset fields: `isolationProfile` (`standard` | `sandbox` | `cluster-meshed`) × `componentProfile` (`minimal` | `standard-addons` | `gpu`). A ComputePlane would then be nothing more than `kind: Kubernetes` with `isolationProfile: sandbox`. + +The isolation mechanism itself was never in question at any point — one-way remote Flux apply via `HelmRelease.spec.kubeConfig.secretRef`, untrusted code behind a disposable guest kernel, single-tenant, scoped egress. Only the delivery surface was contested. + +## Decision + +ComputePlane ships as a Cozystack-owned **tenant module** (`packages/extra/computeplane`) that provisions a hardened `kind: Kubernetes` by wrapping the unchanged `apps/kubernetes` chart with operator-fixed values, sourced through the existing PackageSource source-only-chart mechanism that NATS and SeaweedFS already use. + +Because a tenant module is registered through an `ApplicationDefinition`, ComputePlane does get its own `apps.cozystack.io` kind, with its own API endpoint and input schema — the same CRD-free way every managed service is registered. So the accurate claim is not "no new kind": it is **no new CRD, no new controller or reconcile path, and no new fields on `kind: Kubernetes`**. Catalog apps route onto the cluster through a `placement: ManagementPlane | ComputePlane` field on `ApplicationDefinition`. + +## Why not the alternatives + +- **User-facing preset fields on `kind: Kubernetes` ([#27](https://github.com/cozystack/community/pull/27)).** Putting the posture on the app object turns an operator's choice into tenant-editable API surface. The entire value of a sandbox is that the tenant *cannot* weaken it, and any field that configures the hardening is a field that can relax it. It also loads a hardened posture onto the one `ApplicationDefinition` that has to stay tenant-generic. +- **A distinct, heavyweight `kind: ComputePlane` with its own cluster implementation ([#17](https://github.com/cozystack/community/pull/17) as merged).** Duplicates the cluster reconcile and RBAC path, which was the substance of the #26 / #17-review objection. Note this decision does still register a `ComputePlane` kind — it departs from #26's literal "no new kind at all" — but as a thin wrapper over the unchanged app, which is what the objection was actually about. +- **The single-string `computePlane:` module rendered inline in `apps/tenant/templates` (the first revision's implementation).** Right delivery shape, wrong packaging: the cluster HelmRelease was inlined into the tenant chart instead of shipping as a first-class chart with its own PackageSource, the way `extra/seaweedfs` does. +- **Hardening containers in the tenant namespace instead.** Rejected as the primary boundary: hardening does not make container isolation multi-tenant, and it breaks the apps in scope. +- **gVisor or a sandboxed runtime as the primary boundary.** Rejected for incomplete syscall coverage and no blast-radius containment on a kernel panic. Still valid as an *inner* layer for per-task sandboxes inside the cluster, and tracked as a future runtime option rather than dismissed. + +## Consequences + +- The hardening is tamper-proof by construction. The cluster's values live entirely in an operator-owned chart and the tenant holds no admin kubeconfig, so "withhold admin" and "own the configuration" become one fact rather than two features to keep in sync. +- `kind: Kubernetes` gains no fields and stays tenant-generic. Node-provisioning and addon changes to the app apply to ComputePlane transparently. +- The cost is composability: several differently-hardened postures per tenant now require separate module variants rather than a combination of two fields. The `placement: ` path that would give a tenant N sandbox clusters is deferred. +- The `computeplane` release name is load-bearing (proposal, Design §1). Any later move of the chart between directories must preserve it. +- [#39](https://github.com/cozystack/community/pull/39) (folding `extra` into `apps` as declarative `ApplicationDefinition` capabilities) composes with this rather than conflicting: ComputePlane becomes a directory move plus `visibility: module` and `cardinality: {scope: tenant, max: 1}`, and the two-piece structure carries over unchanged. + +## Revisit if + +[#39](https://github.com/cozystack/community/pull/39) lands and changes how module kinds are registered, or a concrete need appears for several simultaneous, differently-hardened compute clusters per tenant — the case the single-module shape does not serve and the deferred `placement: ` path would. diff --git a/design-proposals/compute-plane/README.md b/design-proposals/compute-plane/README.md index a64f978..27710f8 100644 --- a/design-proposals/compute-plane/README.md +++ b/design-proposals/compute-plane/README.md @@ -36,6 +36,10 @@ The capability is generic and intended to live in Cozystack core as a reusable p - **`design-proposals/kubernetes-nodes-split`** / **`kubernetes-nodes-hybrid-clusters`** (PR #8/#9): the substrate is the existing managed-`kubernetes` app (Kamaji + CAPI/KubeVirt); node-provisioning changes apply transparently. - **Deferred:** billing/metering of cluster resource and API consumption; secret delivery of managed-service connection strings into sandbox workloads; the per-instance/label granularity of the visibility control (Design §6). (Cross-tenant *sharing* of a cluster is **not** deferred — it is rejected by design; see Non-goals.) +## Decisions + +- [0001. ComputePlane ships as an operator-owned module, not as preset fields on `kind: Kubernetes`](../../decisions/0001-computeplane-ships-as-an-operator-owned-module.md) — why the preset-field revision (#27) was set aside. + ## Context Today Cozystack already has every primitive needed *except* the glue that ties them into "deploy this catalog app onto a hardened, operator-controlled `kind: Kubernetes` the tenant does not administer": diff --git a/design-proposals/template.md b/design-proposals/template.md index 176743a..ae2541c 100644 --- a/design-proposals/template.md +++ b/design-proposals/template.md @@ -20,6 +20,18 @@ A reader who stops here should know what you're asking for and roughly why. --> Link them by repo path or URL. If this proposal must land before or after another, say so. Omit the section if there are no related proposals. --> +## Decisions + + + ## Context + # NNNN. State the decision here, as a fact - **Number:** `NNNN` - **Date:** `YYYY-MM-DD` - **Status:** Accepted | Superseded by `NNNN` | Reverted - **Deciders:** `@your-github-handle, @codecider` -- **Proposal:** `design-proposals//README.md` — or `none` +- **Proposal:** [`../README.md`](../README.md) +- **Decided in:** `cozystack/community#NNNN` — the pull request or issue where it was argued - **Implemented in:** `cozystack/cozystack#NNNN` — or `not yet` @@ -32,6 +46,14 @@ it lost. This is the section that keeps the question from being reopened every six months, so spend your words here. + Source each one — link the comment, review or pull request it came + from. An alternative written from memory is where inaccuracy gets in, + and a claim nobody can trace back cannot be checked for accuracy. + + Prefer the argument that survives a refactor. If an option lost + because of how something is packaged today, and it would also lose on + a structural fact about the API, give the structural reason. + If an alternative lost on a judgement call rather than a hard fact, say so — it tells a future reader how firm this decision really is. --> @@ -56,4 +78,7 @@ Format follows Michael Nygard's architecture decision records (https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) and MADR (https://adr.github.io/madr/). + +Once merged, the header block above is maintained (Status, Superseded by, +Implemented in must track reality) and the prose below it is frozen. --> diff --git a/design-proposals/template.md b/design-proposals/template.md index ae2541c..cfb8afc 100644 --- a/design-proposals/template.md +++ b/design-proposals/template.md @@ -23,14 +23,14 @@ another, say so. Omit the section if there are no related proposals. --> ## Decisions +See ../README.md#decision-records. --> ## Context From c0b9617a1e7ff8450830c237cfdab2fda1aadddd Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Thu, 20 Aug 2026 23:48:24 +0500 Subject: [PATCH 4/9] docs(compute-plane): correct decision 0001 against the record MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Myasnikov Daniil --- ...plane-ships-as-an-operator-owned-module.md | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/design-proposals/compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md b/design-proposals/compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md index 0936623..1edf469 100644 --- a/design-proposals/compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md +++ b/design-proposals/compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md @@ -3,17 +3,20 @@ - **Number:** `0001` - **Date:** `2026-07-18` - **Status:** Accepted -- **Deciders:** `@kvaps, @myasnikovdaniil` -- **Proposal:** [`design-proposals/compute-plane/README.md`](../design-proposals/compute-plane/README.md) -- **Implemented in:** not yet — see the proposal's Rollout section +- **Deciders:** `@kvaps, @lllamnyp` +- **Proposal:** [`../README.md`](../README.md) +- **Decided in:** [`cozystack/community#33`](https://github.com/cozystack/community/pull/33) +- **Implemented in:** [`cozystack/cozystack#3280`](https://github.com/cozystack/cozystack/pull/3280) ## Context The first ComputePlane revision ([#17](https://github.com/cozystack/community/pull/17), merged 2026-06-30) delivered a hardened, operator-controlled Kubernetes cluster for running code-executing catalog apps — notebooks, workflow code nodes, plugin systems — behind a per-VM kernel boundary instead of as shared-kernel pods on the management cluster. It was shaped as a tenant module selected by a single string (`computePlane: ""`), with the cluster's HelmRelease rendered inline in `apps/tenant/templates`. -Two reviews converged on the same objection ([#26](https://github.com/cozystack/community/issues/26), and Timofei Larkin on [#17](https://github.com/cozystack/community/pull/17)): the cluster a ComputePlane runs on *is* an ordinary managed `kind: Kubernetes` — Kamaji control plane, KubeVirt-VM workers, operator-held kubeconfig — so ComputePlane should not become a distinct kind with a parallel cluster implementation, reconcile loop and RBAC surface. +[#26](https://github.com/cozystack/community/issues/26), filed by @myasnikovdaniil, reopened the delivery question and forced the revision: the cluster a ComputePlane runs on *is* an ordinary managed `kind: Kubernetes` — Kamaji control plane, KubeVirt-VM workers, operator-held kubeconfig — so ComputePlane should not become a distinct kind with a parallel cluster implementation, reconcile loop and RBAC surface. -Revision [#27](https://github.com/cozystack/community/pull/27) answered that by putting the posture directly on the existing app as two orthogonal user-facing preset fields: `isolationProfile` (`standard` | `sandbox` | `cluster-meshed`) × `componentProfile` (`minimal` | `standard-addons` | `gpu`). A ComputePlane would then be nothing more than `kind: Kubernetes` with `isolationProfile: sandbox`. +@lllamnyp's [review on #17](https://github.com/cozystack/community/pull/17#pullrequestreview-4959656115) was an approval, and it argued something different that turned out to be load-bearing here: that "hidden from the tenant" had been conflated with the security boundary, and the real justification for withholding cluster access is **tamper-resistance** — withhold admin, not visibility. #26 quoted that approval in support of its own case, but the "not a distinct kind" argument is #26's alone. + +Revision [#27](https://github.com/cozystack/community/pull/27) answered #26 by putting the posture directly on the existing app as two orthogonal user-facing preset fields: `isolationProfile` (`standard` | `sandbox` | `cluster-meshed`) × `componentProfile` (`minimal` | `standard-addons` | `gpu`). A ComputePlane would then be nothing more than `kind: Kubernetes` with `isolationProfile: sandbox`. It was closed unmerged on 2026-07-13, superseded by [#33](https://github.com/cozystack/community/pull/33) the same day. The isolation mechanism itself was never in question at any point — one-way remote Flux apply via `HelmRelease.spec.kubeConfig.secretRef`, untrusted code behind a disposable guest kernel, single-tenant, scoped egress. Only the delivery surface was contested. @@ -25,19 +28,19 @@ Because a tenant module is registered through an `ApplicationDefinition`, Comput ## Why not the alternatives -- **User-facing preset fields on `kind: Kubernetes` ([#27](https://github.com/cozystack/community/pull/27)).** Putting the posture on the app object turns an operator's choice into tenant-editable API surface. The entire value of a sandbox is that the tenant *cannot* weaken it, and any field that configures the hardening is a field that can relax it. It also loads a hardened posture onto the one `ApplicationDefinition` that has to stay tenant-generic. -- **A distinct, heavyweight `kind: ComputePlane` with its own cluster implementation ([#17](https://github.com/cozystack/community/pull/17) as merged).** Duplicates the cluster reconcile and RBAC path, which was the substance of the #26 / #17-review objection. Note this decision does still register a `ComputePlane` kind — it departs from #26's literal "no new kind at all" — but as a thin wrapper over the unchanged app, which is what the objection was actually about. -- **The single-string `computePlane:` module rendered inline in `apps/tenant/templates` (the first revision's implementation).** Right delivery shape, wrong packaging: the cluster HelmRelease was inlined into the tenant chart instead of shipping as a first-class chart with its own PackageSource, the way `extra/seaweedfs` does. +- **User-facing preset fields on `kind: Kubernetes`** ([#27](https://github.com/cozystack/community/pull/27), closed unmerged). Putting the posture on the app object turns an operator's choice into tenant-editable API surface, and the entire value of a sandbox is that the tenant *cannot* weaken it. The durable reason is structural, not a matter of how the chart is packaged today: capabilities and schemas are per-kind, so two postures over one chart need two `ApplicationDefinition`s; `ApplicationDefinitionSpec` ([`api/v1alpha1/applicationdefinitions_types.go`](https://github.com/cozystack/cozystack/blob/main/api/v1alpha1/applicationdefinitions_types.go)) has no fixed-values facility, and the conversion makes the tenant's spec *become* the Helm values (`Values: app.Spec`, [`pkg/registry/apps/application/rest.go`](https://github.com/cozystack/cozystack/blob/main/pkg/registry/apps/application/rest.go)), so operator-fixed values have nowhere to live but a chart. A narrow `openAPISchema` does not substitute: a field you can default is a field the tenant can override, and a field absent from the schema receives the chart's own unhardened default. "Hardened and unreachable" is not expressible in a structural schema. +- **A distinct, heavyweight `kind: ComputePlane` with its own cluster implementation** ([#17](https://github.com/cozystack/community/pull/17) as merged). Duplicates the cluster reconcile and RBAC path, which was the substance of [#26](https://github.com/cozystack/community/issues/26). Note this decision does still register a `ComputePlane` kind — it departs from #26's literal "no new kind at all" — but as a thin wrapper over the unchanged app, which is what the objection was actually about. +- **The single-string `computePlane:` module rendered inline in `apps/tenant/templates`** (the first revision's implementation). Right delivery shape, wrong packaging: the cluster HelmRelease was inlined into the tenant chart instead of shipping as a first-class chart with its own PackageSource, the way `extra/seaweedfs` does. - **Hardening containers in the tenant namespace instead.** Rejected as the primary boundary: hardening does not make container isolation multi-tenant, and it breaks the apps in scope. - **gVisor or a sandboxed runtime as the primary boundary.** Rejected for incomplete syscall coverage and no blast-radius containment on a kernel panic. Still valid as an *inner* layer for per-task sandboxes inside the cluster, and tracked as a future runtime option rather than dismissed. ## Consequences -- The hardening is tamper-proof by construction. The cluster's values live entirely in an operator-owned chart and the tenant holds no admin kubeconfig, so "withhold admin" and "own the configuration" become one fact rather than two features to keep in sync. +- The hardening is tamper-proof by construction — the argument from the [#17 review](https://github.com/cozystack/community/pull/17#pullrequestreview-4959656115). The cluster's values live entirely in an operator-owned chart and the tenant holds no admin kubeconfig, so "withhold admin" and "own the configuration" become one fact rather than two features to keep in sync. - `kind: Kubernetes` gains no fields and stays tenant-generic. Node-provisioning and addon changes to the app apply to ComputePlane transparently. - The cost is composability: several differently-hardened postures per tenant now require separate module variants rather than a combination of two fields. The `placement: ` path that would give a tenant N sandbox clusters is deferred. -- The `computeplane` release name is load-bearing (proposal, Design §1). Any later move of the chart between directories must preserve it. -- [#39](https://github.com/cozystack/community/pull/39) (folding `extra` into `apps` as declarative `ApplicationDefinition` capabilities) composes with this rather than conflicting: ComputePlane becomes a directory move plus `visibility: module` and `cardinality: {scope: tenant, max: 1}`, and the two-piece structure carries over unchanged. +- The `computeplane` release name is load-bearing. It is enforced in code rather than restated here: [`templates/check-release-name.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/extra/computeplane/templates/check-release-name.yaml) fails the render on a non-canonical name, and [`tests/release_name_test.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/extra/computeplane/tests/release_name_test.yaml) pins it, with the mechanism in the suite comment. +- **The ordering constraint against [#39](https://github.com/cozystack/community/pull/39) is now live.** #33 was asked to co-land with #39 or land after it, because once tenants can set `computeplane` the release-name invariant stops being a markdown disagreement and becomes a migration of live Kamaji clusters holding tenant data. #3280 merged on 2026-07-29 and #39 is still open, so that migration is now a real cost carried by whoever lands #39, rather than a change that purely composes with this one. ## Revisit if From 4dae30b20da2c4d54f72f43ce5ae4134d6fb8b21 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Thu, 20 Aug 2026 23:50:27 +0500 Subject: [PATCH 5/9] docs(decision-records): introduce the mechanism through the mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reasoning for why decision records exist and where they live was going to survive only in the #56 thread — which is precisely the failure the change is written to fix. It reproduced its own diagnosis on itself. So it lands as a design proposal, design-proposals/decision-records, with the drift evidence that was previously only in the pull-request description, and a record under it for the placement decision. The proposal states the change and the rationale; the operative rules stay in design-proposals/README.md, because a merged proposal is a reference and not a binding spec. This is not ceremony, on one condition: the record has a losing alternative. "We will write decision records" has none, and a record for it would be a form to copy. The placement question has four real arguments on the other side, and 0001 states them at full strength — the citable monotonic id, Nygard's tooling, the single directory that lists itself, and cross-proposal decisions having no home — then says what decided each one. It also demonstrates per-proposal numbering immediately: two proposals, two records, both 0001. Recorded a fifth alternative that the review did not raise and neither did the original proposal: freezing merged proposals and superseding them, the Rust RFC and Python PEP model, where the history is automatic because supersession is the only way to change anything. Rejected — it contradicts "a reference, not a binding spec" and makes every implementation finding a new proposal — but it is the model the largest peers picked, and Kubernetes keeping living KEPs has exactly this gap and fills it with milestone dates only. A future reader will ask why we did not do that, so the answer is on the record. Assisted-By: Claude Signed-off-by: Myasnikov Daniil --- design-proposals/decision-records/README.md | 117 ++++++++++++++++++ ...ision-records-live-with-their-proposals.md | 40 ++++++ 2 files changed, 157 insertions(+) create mode 100644 design-proposals/decision-records/README.md create mode 100644 design-proposals/decision-records/decisions/0001-decision-records-live-with-their-proposals.md diff --git a/design-proposals/decision-records/README.md b/design-proposals/decision-records/README.md new file mode 100644 index 0000000..f7669dc --- /dev/null +++ b/design-proposals/decision-records/README.md @@ -0,0 +1,117 @@ +# Decision records + +- **Title:** `Record architectural decisions alongside the proposals they amend` +- **Author(s):** `@myasnikovdaniil` +- **Date:** `2026-08-20` +- **Status:** Review + +## Overview + +This proposal introduces **decision records** to this repository: short, dated notes on architectural decisions the project has already made, and why — stored under the design proposal each one amends. + +A design proposal is intent, written before the work. When implementation contradicts it we revise the proposal in place, so the proposal ends up reading as though it always said the current thing, and the reasoning that changed our minds survives only in a pull-request diff. That reasoning is the most expensive thing we produce and the easiest to lose. This proposal gives it a home. + +## Scope and related proposals + +The operative rules — when a record is required, the template, the numbering, the immutability rule, the review bar — live in [`design-proposals/README.md`](../README.md#decision-records) and [`design-proposals/decision-template.md`](../decision-template.md), not here. A merged proposal is "a reference, not a binding spec", so this document states the change and the rationale; the process text is the law. + +## Decisions + +- [0001. Decision records live with the proposal they amend](./decisions/0001-decision-records-live-with-their-proposals.md) — why records are per-proposal rather than a root-level log. + +## Context + +### The problem + +Proposal drift is not hypothetical here; it is the normal case, and it is already happening in a way that loses reasoning: + +- [#42](https://github.com/cozystack/community/pull/42) and [#44](https://github.com/cozystack/community/pull/44) rewrote the database-horizontal-autoscaling proposal after an implementation spike disproved its load-bearing premise — that the autoscaler could be the enforced single owner of the application's `replicas` value. +- [#53](https://github.com/cozystack/community/pull/53) then reopened that proposal's actuation mechanism on a live CloudNativePG finding: the `Cluster` `/scale` subresource exposes no `status.selector`, and upstream [cloudnative-pg#7923](https://github.com/cloudnative-pg/cloudnative-pg/issues/7923) is closed as not planned. +- [#40](https://github.com/cozystack/community/pull/40), [#41](https://github.com/cozystack/community/pull/41) and [#36](https://github.com/cozystack/community/pull/36) are the same shape: an implementation finding rewriting an accepted proposal, with the *why* left in the pull-request body. +- The compute-plane proposal carried a `Revision (this PR):` paragraph in its metadata block because that rationale had nowhere else to live. "This PR" stops resolving the moment the next revision lands. + +Three improvised solutions to one problem already exist in this repository — that metadata paragraph, the database-autoscaler's `Why this changed` section, and its spike appendix. None of them is protected from the next in-place edit. + +### Prior art + +The projects with the largest proposal processes split two ways on this, and the split is instructive. + +| Project | Merged proposal | How superseded reasoning survives | +|---|---|---| +| [Rust RFCs](https://github.com/rust-lang/rfcs) | Frozen — "once accepted, RFCs should not be substantially changed… More substantial changes should be new RFCs, with a note added to the original RFC" | A new RFC, plus a note on the original | +| [Python PEPs](https://peps.python.org/pep-0001/) | Frozen — "PEPs are no longer substantially modified after they have reached the Accepted, Final, Rejected or Superseded state" | `Replaces` / `Superseded-By` headers, `Resolution` link | +| [Kubernetes KEPs](https://github.com/kubernetes/enhancements/blob/master/keps/sig-architecture/0000-kep-process/README.md) | Living, edited per release | `superseded-by` / `replaces` metadata and an `Implementation History` section — which records **milestones and dates, not reasoning** | + +Cozystack is in the Kubernetes camp by explicit choice: proposals here are a reference rather than a binding spec, and the codebase is the source of truth. That choice is right for this project, and it is also exactly why the gap exists — Rust and Python do not need decision records because freezing the proposal makes supersession the only way to change anything, so the history is automatic. + +Projects that keep living proposals and want the reasoning tend to add a second, immutable document class: [Backstage](https://backstage.io/docs/architecture-decisions/) keeps an ADR log in its docs tree, and the [GOV.UK Design System](https://github.com/alphagov/govuk-design-system-architecture/blob/main/proposals/001-use-rfcs-and-adrs-to-discuss-proposals-and-record-decisions.md) adopted RFCs and ADRs together, dividing them exactly this way — the RFC explores options, the ADR records what was decided. + +## Goals + +- The reasoning behind a design change is recoverable a year later without reading a pull-request thread. +- A record is cheap enough to write that it actually gets written: one page, one reviewer, no new front-door concept. +- The record cannot be quietly rewritten into agreement with the present. +- Where code can enforce a decision, the code stays the primary record and the note does not duplicate it. + +### Non-goals + +- Not a second design document. A record that grows into one has failed. +- Not a changelog, and not user documentation. +- Not a governance process for making decisions — only for recording ones already made. +- Not retroactive. Backfilling every past decision is not proposed; records start from the ones being made now, plus [0001](../compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md) as a worked example. + +## Design + +A record is a one-page markdown file at `design-proposals//decisions/NNNN-slug.md`, numbered per proposal, with a maintained header block and frozen prose. It carries Context, Decision, Why not the alternatives, Consequences, and Revisit if. + +Two rules carry the design: + +1. **The prose is frozen after merge; the header is maintained.** `Status`, `Superseded by` and `Implemented in` must track reality or the lifecycle breaks; the body must not, or the record becomes just another document that agrees with the present. +2. **Where a decision is a fact about code, the code is the primary record.** A comment at the site plus a test that fails when the invariant is violated, and the record links to it. Cozystack already does this better than prose does — the `computeplane` release-name guard and its test state the mechanism more precisely than the first draft of record 0001 managed to. + +The full rules are in [`design-proposals/README.md`](../README.md#decision-records). + +## User-facing changes + +None for users of Cozystack. For contributors: one new optional document type, one new template, and a pull-request checkbox asking whether a proposal-revising change needs a record. + +## Upgrade and rollback compatibility + +Not applicable — no code, no API, no cluster state. Rolling this back means deleting the template and the process section; existing records stay readable as ordinary markdown. + +## Security + +No new trust boundary, tenant input, RBAC surface or secret. Records are public documents in a public repository and must not carry cluster-identifying or client-identifying detail. + +## Failure and edge cases + +- **A record is written for a decision that was not actually settled** → review turns into re-litigating it. That is the signal; close the pull request and open a proposal or an issue instead. +- **Two pull requests claim the same number under one proposal** → whoever merges second renumbers on rebase. Per-proposal numbering makes this rare rather than constant. +- **The decision spans several proposals** → file it under the one it changes most and link it from the others; if it belongs to no proposal, it is a proposal. +- **The decision is a standing constraint on the platform rather than on one proposal** → out of scope here. See [Open questions](#open-questions). +- **A proposal is withdrawn** → its records go with it, which is correct; nothing is orphaned. +- **Nobody writes records** → the failure mode Backstage names in its own ADR001, and the reason enforcement is a reviewer checkbox rather than a convention. + +## Testing + +There is no CI in this repository — `.github/` contains issue templates only — so there is nothing to assert mechanically. Validation is that the next proposal-revising pull request either adds a record or says why it does not, and that the reviewer checkbox is the place that gets noticed. + +## Rollout + +1. This proposal, the process text, and the template land together. +2. [0001 under compute-plane](../compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md) ships as a backfilled worked example, and [0001 under this proposal](./decisions/0001-decision-records-live-with-their-proposals.md) as one written by its own author from an argument they were present for. +3. A second record from a different subsystem — the database-horizontal-autoscaling rev1 rejection — is requested on [#53](https://github.com/cozystack/community/pull/53) from the contributor who owns that argument, rather than backfilled here by someone who does not. +4. The improvised in-proposal narratives are extracted as their proposals are next revised, starting with compute-plane's `Revision (this PR):` field in [#57](https://github.com/cozystack/community/pull/57). + +## Open questions + +- **Where does a design principle live?** Some constraints surfaced by a decision are not about that proposal at all — that `ApplicationDefinition` has no operator-fixed-values facility, and that settability and defaultability are the same property in a structural schema, bind every operator-owned module anyone builds next. A record filed under one proposal is the wrong home, and there is no principles document in `cozystack/cozystack` today. Tracked separately so this log does not absorb it by default. +- **Does the lighter review bar need explicit maintainer agreement?** One approval for a record, against consensus for a proposal, is a governance change and is called out as one rather than merged as documentation. + +## Alternatives considered + +- **A root-level `decisions/` log with a global sequence.** The original shape of this proposal. Rejected; the argument is recorded in [0001](./decisions/0001-decision-records-live-with-their-proposals.md). +- **Freeze merged proposals instead, Rust/PEP style.** Supersession then produces the history for free and no second document class is needed. Rejected as a heavier governance change that contradicts the existing "reference, not a binding spec" position and would make every implementation finding a new proposal — but it is the option the largest peers picked, and it remains the coherent alternative if records are not written in practice. +- **Leave the reasoning in pull-request bodies and rely on search.** This is the status quo, and the six pull requests in Context are what it produces: reasoning that is technically present and practically unfindable, attached to a diff rather than to the design. +- **Put the reasoning in the proposal itself, in a revision-history section.** Already tried three times here, in three different shapes, none protected from the next in-place edit. It also makes the proposal longer exactly where a reader wants it shorter. +- **Record decisions only in code comments and tests.** Correct for anything code can enforce, and now a rule rather than an alternative. It does not cover decisions about what *not* to build, or ones whose subject is a constraint rather than a line of code. diff --git a/design-proposals/decision-records/decisions/0001-decision-records-live-with-their-proposals.md b/design-proposals/decision-records/decisions/0001-decision-records-live-with-their-proposals.md new file mode 100644 index 0000000..72bebf5 --- /dev/null +++ b/design-proposals/decision-records/decisions/0001-decision-records-live-with-their-proposals.md @@ -0,0 +1,40 @@ +# 0001. Decision records live with the proposal they amend + +- **Number:** `0001` +- **Date:** `2026-08-20` +- **Status:** Accepted +- **Deciders:** `@myasnikovdaniil, @lllamnyp` +- **Proposal:** [`../README.md`](../README.md) +- **Decided in:** [`cozystack/community#56`](https://github.com/cozystack/community/pull/56) +- **Implemented in:** [`cozystack/community#56`](https://github.com/cozystack/community/pull/56) + +## Context + +Decision records were first proposed as a root-level `decisions/` directory with a single global sequence — `decisions/0001-…`, `decisions/0002-…` — following Michael Nygard's convention and the layout `adr-tools` produces. That shape was written, reviewed and complete before the placement question was raised in review of [#56](https://github.com/cozystack/community/pull/56). + +The objection that reopened it came from the proposal's own template. The `Proposal:` field read `design-proposals//README.md` — **or `none`**, which presupposes that a decision can exist in this repository without a proposal. The repository's front-door table routes every candidate somewhere: a cross-cutting architectural change to a design proposal here, a bug or scoped feature to `cozystack/cozystack`, governance to an issue here. There is no residue. A deliberation weighty enough to need a record, with no proposal to attach it to, has demonstrated that it is a proposal. + +At the time of the decision the repository held five design proposals and routinely had five or so pull requests open at once. + +## Decision + +Records live under the proposal they amend, at `design-proposals//decisions/NNNN-slug.md`, numbered per proposal. The process text lives in `design-proposals/README.md` and the template at `design-proposals/decision-template.md`. There is no root-level `decisions/` tree and no global sequence, and the root README continues to route two document classes rather than three. + +## Why not the alternatives + +- **A root-level `decisions/` log with a global monotonic sequence.** The case for it is real and giving it up costs something. (a) A monotonic id is short, citable and survives a directory rename — `ADR 0007` is a durable reference in a way `compute-plane ADR 0002` is not, and external citations are exactly what a global id serves. (b) Nygard's convention and its tooling assume one log; `adr-tools` defaults to a single `doc/adr`, and choosing otherwise forfeits it. (c) "What did we decide lately?" is a real question a single directory answers by listing itself. (d) Decisions that span several proposals have no single home under per-proposal placement, and this nearly carried the argument. What decided it against: (a) is answered by accepting a more verbose but more informative citation; (b) by the fact that nothing in this repository would consume that tooling — there is no CI here at all, and the template already departs from Nygard's four sections; (c) by `git log -- 'design-proposals/*/decisions/[0-9]*.md'`, which answers it in any layout and better than directory order does, since it carries dates and authors; and (d) by scoping cross-proposal *principles* out of this log entirely rather than letting the log absorb them — see the proposal's open question. Against all four stands the concrete cost of the global sequence: with five pull requests routinely open, a global number collides constantly, and "whoever merges second renumbers on rebase" invalidates any citation already written down. Per-proposal numbering collides only when two pull requests touch the same proposal. +- **Freezing merged proposals instead, and superseding them — the Rust RFC and Python PEP model.** [Rust](https://github.com/rust-lang/rfcs) holds that "once accepted, RFCs should not be substantially changed"; [PEP 1](https://peps.python.org/pep-0001/) that "PEPs are no longer substantially modified after they have reached the Accepted, Final, Rejected or Superseded state". Under that model supersession is the only way to change anything, so the history is automatic and no second document class is needed. Rejected here because it contradicts this repository's existing position that merged proposals are "a reference, not a binding spec" and that the codebase is the source of truth, and because it would turn every implementation finding into a new proposal — a much heavier governance change than the one under discussion. It is the coherent alternative rather than a weak one, and it is the model the largest peer processes actually chose; [Kubernetes](https://github.com/kubernetes/enhancements/blob/master/keps/sig-architecture/0000-kep-process/README.md), which keeps living proposals as this repository does, has the same gap and fills it only with milestone dates. +- **Records in the product repository, next to the code.** Rejected: the decisions in scope are decisions about proposals, and the proposals are here. A decision whose subject is a fact about code has its primary home in a comment and a test at that site, which is now a rule rather than an alternative. +- **A separate repository for decisions.** Rejected as a third place to look for the same argument, for a repository that currently holds five proposals. + +## Consequences + +- Both link directions come for free: a reader browsing a proposal's directory sees its decisions, and a record's parent directory is its proposal. Nothing to maintain, and hand-maintained backlinks are the first thing to rot. The `Decisions` section in the proposal template becomes a convenience rather than the only path between the two. +- Citations become longer and qualified — `compute-plane ADR 0002` rather than `ADR 0014`. External references to a record are correspondingly more fragile if a proposal directory is ever renamed. +- A withdrawn proposal takes its decisions with it instead of orphaning entries in a global log. +- Cross-proposal standing constraints are now explicitly homeless, which is the honest outcome: they were never served by either layout, and this decision refuses to pretend otherwise by filing them under an arbitrary proposal. Finding them a home is tracked as an open question against `cozystack/cozystack`. +- Restructuring cost was paid once, while the log had one entry. It would not have stayed cheap: the review's argument for deciding now rather than later was that the move stops being mechanical once citations point at the global ids from other repositories. + +## Revisit if + +Cross-proposal decisions become common enough that scoping them out stops being tenable, or a principles document lands in `cozystack/cozystack` and turns out to be the natural home for records that bind more than one proposal. Also revisit if records are not being written in practice — that would make the frozen-proposal alternative the live option rather than this one. From 0849089cf438b9db16c25eae0199dd76b8e44b61 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Thu, 20 Aug 2026 23:50:42 +0500 Subject: [PATCH 6/9] docs: ask on every pull request whether a record is needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no CI in this repository — .github holds issue templates only — so the rule in Proposal Drift is only as binding as the place a reviewer looks. A pull-request template is the one enforcement mechanism available, and it costs a single file. One substantive checkbox: if this revises a merged proposal, it adds a record or says why none is needed. The escape hatch is deliberate and the template names what legitimately takes it — wording fixes, corrections of fact, and decisions the code already explains — because a checkbox that cannot be declined honestly gets ticked without being read. Assisted-By: Claude Signed-off-by: Myasnikov Daniil --- .github/pull_request_template.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..c0bf577 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ + + +## Before review + +- [ ] **If this revises a merged design proposal:** it adds a decision record under that proposal's `decisions/` directory, or says below why none is needed. + + + +## DCO + +- [ ] Commits are signed off (`git commit --signoff`). From 0fa43d83e7274ba9cd88fb5716055c958c594520 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Fri, 21 Aug 2026 13:16:19 +0500 Subject: [PATCH 7/9] docs(compute-plane): retire the Revision (this PR) metadata field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field held a whole paragraph on why the preset-field revision was set aside. "This PR" stops resolving the moment the next revision lands, so it was unmaintainable by construction, and Overview directly below it already said the same thing — a third copy of one rationale with no place of its own. Decision 0001 is that place now, so the field becomes Supersedes plus a link. This was #57, stacked on this branch. Folded here instead: the change is one line in a file this branch already edits, so a separate stacked pull request bought nothing and needed a rebase to survive the move of the records directory. Assisted-By: Claude Signed-off-by: Myasnikov Daniil --- design-proposals/compute-plane/README.md | 2 +- design-proposals/decision-records/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/design-proposals/compute-plane/README.md b/design-proposals/compute-plane/README.md index bb7bf99..73c9885 100644 --- a/design-proposals/compute-plane/README.md +++ b/design-proposals/compute-plane/README.md @@ -5,7 +5,7 @@ - **Author(s):** `@kvaps` - **Date:** `2026-06-23` - **Status:** Accepted -- **Revision (this PR):** Supersedes the preset-field revision (#27). ComputePlane is delivered as a Cozystack-owned **Tenant module** (`packages/extra/computeplane`) that, under the hood, deploys the ordinary `apps/kubernetes` chart with operator-fixed values, sourced through the existing PackageSource "source-only chart" mechanism (the same one NATS and SeaweedFS use — only the wrapped chart comes from `apps/` instead of `system/`). Like every Cozystack managed service, the module registers its **own `apps.cozystack.io` kind** (`ComputePlane`, via an `ApplicationDefinition` with `dashboard.module: true`) — its own API endpoint and input schema, served by `cozystack-api` and converted to a HelmRelease. So this is **not** the literal "no new kind" surface #27 aimed for; the honest positioning is: no new **CRD**, no new **controller / reconcile path**, and **no fields added to `kind: Kubernetes`** — the `ComputePlane` kind is a thin operator-owned wrapper over the unchanged `apps/kubernetes`. The tenant gets the *same* `kind: Kubernetes` cluster but owns none of its settings — only the knobs the operator exposes. The isolation mechanism (remote Flux apply onto Kamaji+KubeVirt, untrusted code behind a per-VM kernel boundary) is unchanged from the merged first revision; this revision is about the **delivery surface**. +- **Supersedes:** the preset-field revision (#27) — the delivery surface and why the preset fields were set aside are recorded in [decision 0001](./decisions/0001-computeplane-ships-as-an-operator-owned-module.md). ## Overview diff --git a/design-proposals/decision-records/README.md b/design-proposals/decision-records/README.md index f7669dc..86a511a 100644 --- a/design-proposals/decision-records/README.md +++ b/design-proposals/decision-records/README.md @@ -101,7 +101,7 @@ There is no CI in this repository — `.github/` contains issue templates only 1. This proposal, the process text, and the template land together. 2. [0001 under compute-plane](../compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md) ships as a backfilled worked example, and [0001 under this proposal](./decisions/0001-decision-records-live-with-their-proposals.md) as one written by its own author from an argument they were present for. 3. A second record from a different subsystem — the database-horizontal-autoscaling rev1 rejection — is requested on [#53](https://github.com/cozystack/community/pull/53) from the contributor who owns that argument, rather than backfilled here by someone who does not. -4. The improvised in-proposal narratives are extracted as their proposals are next revised, starting with compute-plane's `Revision (this PR):` field in [#57](https://github.com/cozystack/community/pull/57). +4. The improvised in-proposal narratives are extracted as their proposals are next revised. Compute-plane's `Revision (this PR):` field is retired here, since this change already touches that proposal; the database-autoscaler's `Why this changed` section and spike appendix go with the record requested on [#53](https://github.com/cozystack/community/pull/53). ## Open questions From c833fd5011201287d254dba0c62565bd7044c50a Mon Sep 17 00:00:00 2001 From: Timofei Larkin Date: Fri, 21 Aug 2026 13:54:47 +0300 Subject: [PATCH 8/9] docs(compute-plane): point the #17 review links at the right review Both links to the #17 review carried review id 4959656115, which is the review on #56 rather than the one on #17. The #17 approval is 4593662101; they landed on the right pull request with a dead anchor. Link fix only, permitted on a merged record by the immutability rule; no prose changed. Assisted-By: Claude Opus 5 Signed-off-by: Timofei Larkin --- .../0001-computeplane-ships-as-an-operator-owned-module.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/design-proposals/compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md b/design-proposals/compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md index 1edf469..055f0c4 100644 --- a/design-proposals/compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md +++ b/design-proposals/compute-plane/decisions/0001-computeplane-ships-as-an-operator-owned-module.md @@ -14,7 +14,7 @@ The first ComputePlane revision ([#17](https://github.com/cozystack/community/pu [#26](https://github.com/cozystack/community/issues/26), filed by @myasnikovdaniil, reopened the delivery question and forced the revision: the cluster a ComputePlane runs on *is* an ordinary managed `kind: Kubernetes` — Kamaji control plane, KubeVirt-VM workers, operator-held kubeconfig — so ComputePlane should not become a distinct kind with a parallel cluster implementation, reconcile loop and RBAC surface. -@lllamnyp's [review on #17](https://github.com/cozystack/community/pull/17#pullrequestreview-4959656115) was an approval, and it argued something different that turned out to be load-bearing here: that "hidden from the tenant" had been conflated with the security boundary, and the real justification for withholding cluster access is **tamper-resistance** — withhold admin, not visibility. #26 quoted that approval in support of its own case, but the "not a distinct kind" argument is #26's alone. +@lllamnyp's [review on #17](https://github.com/cozystack/community/pull/17#pullrequestreview-4593662101) was an approval, and it argued something different that turned out to be load-bearing here: that "hidden from the tenant" had been conflated with the security boundary, and the real justification for withholding cluster access is **tamper-resistance** — withhold admin, not visibility. #26 quoted that approval in support of its own case, but the "not a distinct kind" argument is #26's alone. Revision [#27](https://github.com/cozystack/community/pull/27) answered #26 by putting the posture directly on the existing app as two orthogonal user-facing preset fields: `isolationProfile` (`standard` | `sandbox` | `cluster-meshed`) × `componentProfile` (`minimal` | `standard-addons` | `gpu`). A ComputePlane would then be nothing more than `kind: Kubernetes` with `isolationProfile: sandbox`. It was closed unmerged on 2026-07-13, superseded by [#33](https://github.com/cozystack/community/pull/33) the same day. @@ -36,7 +36,7 @@ Because a tenant module is registered through an `ApplicationDefinition`, Comput ## Consequences -- The hardening is tamper-proof by construction — the argument from the [#17 review](https://github.com/cozystack/community/pull/17#pullrequestreview-4959656115). The cluster's values live entirely in an operator-owned chart and the tenant holds no admin kubeconfig, so "withhold admin" and "own the configuration" become one fact rather than two features to keep in sync. +- The hardening is tamper-proof by construction — the argument from the [#17 review](https://github.com/cozystack/community/pull/17#pullrequestreview-4593662101). The cluster's values live entirely in an operator-owned chart and the tenant holds no admin kubeconfig, so "withhold admin" and "own the configuration" become one fact rather than two features to keep in sync. - `kind: Kubernetes` gains no fields and stays tenant-generic. Node-provisioning and addon changes to the app apply to ComputePlane transparently. - The cost is composability: several differently-hardened postures per tenant now require separate module variants rather than a combination of two fields. The `placement: ` path that would give a tenant N sandbox clusters is deferred. - The `computeplane` release name is load-bearing. It is enforced in code rather than restated here: [`templates/check-release-name.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/extra/computeplane/templates/check-release-name.yaml) fails the render on a non-canonical name, and [`tests/release_name_test.yaml`](https://github.com/cozystack/cozystack/blob/main/packages/extra/computeplane/tests/release_name_test.yaml) pins it, with the mechanism in the suite comment. From 692a0f9eefc98811d87d87319d181d7b43f11c3d Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Fri, 21 Aug 2026 16:54:52 +0500 Subject: [PATCH 9/9] docs(decision-records): correct stale #53 precedent Signed-off-by: Myasnikov Daniil --- design-proposals/decision-records/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design-proposals/decision-records/README.md b/design-proposals/decision-records/README.md index 86a511a..f3ba2ee 100644 --- a/design-proposals/decision-records/README.md +++ b/design-proposals/decision-records/README.md @@ -26,7 +26,7 @@ The operative rules — when a record is required, the template, the numbering, Proposal drift is not hypothetical here; it is the normal case, and it is already happening in a way that loses reasoning: - [#42](https://github.com/cozystack/community/pull/42) and [#44](https://github.com/cozystack/community/pull/44) rewrote the database-horizontal-autoscaling proposal after an implementation spike disproved its load-bearing premise — that the autoscaler could be the enforced single owner of the application's `replicas` value. -- [#53](https://github.com/cozystack/community/pull/53) then reopened that proposal's actuation mechanism on a live CloudNativePG finding: the `Cluster` `/scale` subresource exposes no `status.selector`, and upstream [cloudnative-pg#7923](https://github.com/cloudnative-pg/cloudnative-pg/issues/7923) is closed as not planned. +- [#53](https://github.com/cozystack/community/pull/53) revised that proposal again after a live PoC exposed a CloudNativePG version floor: CNPG 1.27's `Cluster` `/scale` subresource lacked `status.selector`, but [cloudnative-pg#8996](https://github.com/cloudnative-pg/cloudnative-pg/pull/8996) added it in CNPG 1.28.4, 1.29.2 and 1.30.0, so the resolution was a version bump rather than a design change. - [#40](https://github.com/cozystack/community/pull/40), [#41](https://github.com/cozystack/community/pull/41) and [#36](https://github.com/cozystack/community/pull/36) are the same shape: an implementation finding rewriting an accepted proposal, with the *why* left in the pull-request body. - The compute-plane proposal carried a `Revision (this PR):` paragraph in its metadata block because that rationale had nowhere else to live. "This PR" stops resolving the moment the next revision lands.