Skip to content

docs(design-proposals): add the platform and catalogs split proposal - #106

Open
myasnikovdaniil wants to merge 4 commits into
mainfrom
proposal/cozystack-split
Open

myasnikovdaniil wants to merge 4 commits into
mainfrom
proposal/cozystack-split

Conversation

@myasnikovdaniil

Copy link
Copy Markdown
Contributor

This PR adds design proposal for splitting cozystack into platform and two catalogs, virtualization and services. It supersedes community#43 and community#46, what is taken from each is listed in the Scope section, both can be closed after this one is accepted.

Main part for review is contracts K1-K6 between platform and catalogs, they are the seam of the split and the rest depends on them. Contracts have no versions of their own, catalog declares minimum platform version and the properties it needs from network, storage and GPU.

Proposal also sets contribution policy for new applications: they go to separate catalog repositories and connect through PackageSource and cozypkg (marketplace from 1.7). Step 0 work starts before acceptance and will be tracked in an epic in cozystack/cozystack.

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.

New proposal, it does not revise a merged one.

DCO

  • Commits are signed off (git commit --signoff).

Split Cozystack into the platform and two catalogs, virtualization and
services, with contracts K1-K6 between them, a contribution policy for
new applications and a rollout in four steps. Supersedes community#43
and community#46.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b42bf4d6-a942-453d-afec-235f8526235e
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@IvanHunters

Copy link
Copy Markdown
Contributor

Strong proposal. Monorepo-first with separate repositories as the last step is the right call, the contracts are written out concretely instead of hand-waved, and the cut is backed by data. The boundary lint with a measurable zero-forbidden-edges criterion is the right spine to hang it on. Most of what follows is about sequencing against work already in flight, plus a few failure modes the contracts don't cover yet. I checked the current-state claims against main; where I cite code, that's where it's from.

Blocking (would want these resolved before it leaves Draft)

  1. K2 transport has no failure story for the projection controller. Swapping the cozystack-values Secret for a typed resource plus one controller that projects _namespace into a ConfigMap per tenant namespace puts a new component on the critical path of every chart in every tenant. The three writers today are real and have already drifted, so folding them into one is worth doing: packages/core/platform/templates/apps.yaml for cozy-system, packages/system/cozystack-basics/templates/cozystack-values-secret.yaml for tenant-root, packages/apps/tenant/templates/namespace.yaml for children. But only the happy path is covered. What does a new tenant get when the controller is down or lagging? How is a partial projection, half the namespaces updated, detected? And what guarantees the ConfigMap is populated before the first HelmRelease that valuesFrom-references it? There's also a trap in the migration: "missing key means empty string" plus the transport swap means a present-but-empty key renders a chart cleanly with an empty ingress or monitoring host, which is exactly the silent failure the contracts section is trying to kill. The dual-write window ("both objects exist, releases switch in one reconcile") needs the switch trigger, the source of truth while both exist, and the rollback of a half-switched state all spelled out.

  2. The backup driver handoff is a cross-component ownership race that the "mechanism lands, apps switch, old code removed" ordering doesn't address. On main the dispatch already keys off strategyRef.Kind and the default branch is a silent skip (return ctrl.Result{}, nil), so moving a driver out means the platform drops its case and the catalog adds a controller for the same kind. With platform and catalog on independent versions inside the N-1/N/N+1 window, two bad states open up: the kind orphaned (platform skips it, catalog not upgraded yet), which silently stops taking backups, or double-owned, where both reconcile the same BackupJob. Step 1 says an unknown kind should end the job with "driver not installed", but flipping today's silent skip to a hard fail reds every handed-off BackupJob during the upgrade window. So the design has to tell "kind handed to a catalog, mid-transition" apart from "kind never known". internal/backupcontroller/restorejob_controller.go is the sharper edge: it still imports velerov1 and runs best-effort Velero cleanup for any unrecognised kind on delete, so once Velero moves to virtualization the platform restorejob is reaping a catalog's restore side-state.

  3. Policy enforcement is tied to acceptance instead of to the marketplace actually being safe. The policy shuts the monorepo path for new apps with no grace period, but the one sanctioned path isn't safe yet. createOrUpdateHelmRelease in internal/operator/package_reconciler.go overwrites any HelmRelease of the same namespace and name, ownerReferences included, with no owning-Package guard. Connect still installs a publisher chart as cluster-admin. And internal/marketplace/naming/naming.go collapses "." to "-", so acme.web and acme-web collide. Those are readiness items 2/3/5, and they should gate enforcement. Otherwise there's a window where the monorepo door is shut and the door left open has a cluster-admin takeover behind it.

Non-blocking but worth addressing

  1. The Go backup SDK is effectively a seventh contract and nothing in K1-K6 covers it. Once drivers live in separate modules they import pkg/backups/ (credential projection, the BackupClass resolver, Job and status helpers) and the strategy.backups.cozystack.io types: a public Go ABI with no schema, no deprecation window, and no place in the compatibility rule. K5 versions cozy-lib on the Helm side but not this. Either make it a contract, or state that third-party catalogs are limited to Job-template drivers.

  2. The Context section on backups is stale against main. It describes 11 strategies as methods of shared reconcilers with a switch on kind, plus operator-type imports, but internal/backupcontroller/backupjob_controller.go already dispatches on strategyRef.Kind from a single API group and imports no operator types. Decoupling Backup-kind from application-kind is done; what's left is the physical move of the driver methods, not the switch. Fixing this changes how a reviewer sizes the remaining work.

  3. Lookup removal is treated as an adjacent workstream rather than a prerequisite. There are ~168 lookups in chart templates, and the removal direction is both active and accepted: the user-secrets-api proposal (merged) with its postgres wave (#4922) replaces the password-preservation lookups, design-proposal: tenant-supplied secrets by reference through External Secrets Operator #82 covers ESO, #1942 the broader replacement. The boundary lint counts lookup edges but doesn't remove them, and the proposal doesn't say which lookups cross the boundary or who removes them. K3 network.stableVMAddress is the lookup "kubeovn.io/v1" "IP" in vm-instance, already guarded after it silently skipped (#4013, #4025, #5061); turning it into a declared property while the code still does a raw lookup is half a fix. Worth saying whether removal gates step 1/2, and what each lookup-removing contract puts in its place.

  4. This overlaps with the Cozystack API v1 proposal (design-proposal: Cozystack API v1 #73), and the two don't reference each other. design-proposal: Cozystack API v1 #73 closes the open spec object and makes server-side field removal mean something, against the same hardcoded dead-field list in pkg/registry/apps/application/rest.go that K1 moves toward admission policy. If design-proposal: Cozystack API v1 #73 closes spec to v1 and validates server-side, the plan to move warnings into a per-chart ValidatingAdmissionPolicy needs to be reconciled with it.

  5. The seven cross-version checks are inferred from code and haven't run on a cluster, yet they carry the weight of the contracts: the silent value drop for K4, the annotation-stops-the-API case for K1 (that one I can confirm is real in pkg/cmd/server/start.go). Running at least checks 1, 2 and 4 before K1/K2/K4 are finalised would put the contracts on observed behaviour instead of assumption.

Smaller

  1. "Minimal installation is the platform without catalogs" still ships SeaweedFS, COSI, Keycloak, cert-manager and monitoring. Object storage sits in the platform unconditionally while the backup framework is optional. Is object storage meant to be always-on with no catalogs and no backups, or optional too?

  2. One contract version for all six couples them: a catalog that only depends on K1 and K2 still has to bump its minimum and re-test when K6 changes. The trade-off is defensible, but the cost is worth naming out loud.

  3. The core-split trigger, "when platform releases start holding core fixes", is subjective next to the measurable step-3 criteria.

Questions

  • K2: what triggers a release switching from the Secret to the ConfigMap, and what's the source of truth while both exist? How do you roll back a half-switched state?
  • K2: HA and degradation behaviour of the projection controller?
  • Backups: who owns a strategy kind during the N-1/N/N+1 handoff window, and how is "handed off, not yet installed" told apart from "unknown"?
  • Is the Go backup SDK a contract, or are third-party catalogs limited to Job-template drivers?
  • Which lookups cross the platform/catalog boundary, who removes them, and is removal a hard prerequisite of step 1/2?
  • Does policy enforcement gate on readiness items 2-7 landing, or on acceptance?
  • What pins the N+1 contract schema so catalog CI against it stays deterministic?
  • Is object storage always-on, or optional without catalogs and backups?
  • How does this reconcile with API v1 (design-proposal: Cozystack API v1 #73)?

…ve them to authors

A separate organization for community applications would have no
access to the project's e2e runners and no CNCF rules, while still
carrying the project's name. An application either meets the bar for
one of our catalogs and lives in this tree until the catalogs move, or
its author publishes it as their own catalog. An interim repository
would only have to be moved again once the catalogs get their own.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
Where a new application goes is decided together with whether the
project takes it, and that is a proposal. An e2e requirement would
mostly re-test the upstream operator, and a fixed number of users is
easy to game.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
Comment on lines +106 to +107
| **platform** | Core (operator, CRDs, `cozystack-api`, `cozystack-controller`, lineage webhook). System packages (network with multus as its option, storage, the host GPU stack, cert-manager, monitoring, Keycloak). Tenant and tenant modules. Shared operators (`postgres-operator`, `objectstorage-controller`). Object storage (the SeaweedFS tenant module, the COSI controller, the Bucket application). The backup framework as optional component. The dashboard |
| **virtualization** | VMInstance, VMDisk, VirtualPrivateCloud, tenant Kubernetes, kubernetes-nodes, computeplane. KubeVirt with its GPU device wiring, CDI, CAPI, Kamaji, forklift. gpu-operator and HAMi as addons inside tenant clusters. VM and worker images. The VM backup driver and Velero. CSI for tenant clusters |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The network layer can be separate from the virtualization just as much as storage.


**Where a new component goes.** If it exists for VMs or for what runs on VMs, it belongs to virtualization, VM networks like VPC included. Multus stays in the platform as option of the network provider: it runs on top of the CNI config and has to match its plugin versions, and the cozyplane network builds VPC without it ([cozystack#4508](https://github.com/cozystack/cozystack/pull/4508)). Same for GPUs: the host GPU stack prepares nodes and is a platform setting, but handing GPUs to VMs and GPU addons of tenant clusters belong to virtualization. If it is finished software a tenant consumes, it belongs to services. If everything needs it, it belongs to the platform.

**Object storage stays in the platform.** The backup framework depends on COSI and the system bucket, the tenant chart enables SeaweedFS as tenant module, and both catalogs consume buckets: virtualization through Velero, services through Harbor, ClickHouse, CNPG and PSMDB. Moving it to services would add a platform → services edge. The bucket class names, `_namespace.seaweedfs` and the `cozy-backups-creds` secret become part of K2 and the backup SDK, so SeaweedFS stays the default provider but not the only one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backup framework doesn't have to depend on object storage. Some of the backup strategies that Cozystack ships make use of object storage and/or Velero, but if anyone writes a bespoke BackupStrategy to deliver a Backup to some other place, by some other protocol. they sidestep these dependencies completely.

| 2 | The tenant-admin role lists every catalog kind, and the platform grants rights and network policies for KubeVirt | `packages/system/cozystack-basics/templates/clusterroles.yaml`, `packages/system/cozystack-api/templates/rbac.yaml`, `packages/apps/tenant/templates/networkpolicy.yaml` | Each `*-rd` chart ships the role for its kind through the existing aggregation labels. Virtualization ships own roles and policies |
| 3 | The tenant chart renders computeplane by name | `packages/apps/tenant/templates/computeplane.yaml` | Generic list of tenant modules driven by `ApplicationDefinition` |
| 4 | Option providers in core Go, 8 of 14 for virtualization | `pkg/registry/core/option/providers.go` | Declarative option sources shipped by the catalog |
| 5 | Warnings and name limits for Kubernetes, Kafka, Postgres and MariaDB in `cozystack-api` | `pkg/registry/apps/application/rest.go` | Remove, or a `ValidatingAdmissionPolicy` with `Warn` in the application chart |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already done, the most problematic remaining limit is for kubernetes-nodes.

| 4 | Option providers in core Go, 8 of 14 for virtualization | `pkg/registry/core/option/providers.go` | Declarative option sources shipped by the catalog |
| 5 | Warnings and name limits for Kubernetes, Kafka, Postgres and MariaDB in `cozystack-api` | `pkg/registry/apps/application/rest.go` | Remove, or a `ValidatingAdmissionPolicy` with `Warn` in the application chart |
| 6 | WorkloadMonitor knows CDI `DataVolume` | `internal/controller/workloadmonitor_controller.go` | Generic readiness gates declared by the application |
| 7 | VM import: an API group, a controller and a binary in the root Go module | `api/migration/v1alpha1/`, `internal/migrationcontroller/`, `cmd/migration-controller/` | Moves to virtualization whole |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting, the API group was properly renamed to forklift and it is very much VM-specific. The broader problem is that several binaries are also part of the root module, e.g. kubeovn-plunger.


### Contracts

The contracts are the seam of the split. Today the platform and applications rely on each other through names, keys and conventions written down nowhere. They hold only because everything ships at one version. Once the platform and catalogs have own versions, the dangerous failures are the silent ones: Helm ignores an unknown values key, a `Capabilities` check switches a template branch off, a `lookup` returns nothing, a webhook lets an object through without labels. Each contract below lists what it covers, so every such assumption is either declared and checked, or removed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contracts are the seam of the split.

I'm sorry, this isn't something written for humans. It took another LLM to understand that this probably means

The split requires explicit, versioned contracts between the platform and catalogs to ensure they remain compatible as they evolve independently.

The contract:

- **Smaller first.** Helpers with one consumer move to that consumer: `keda`, `strings.hexToInt`, most of `_cozyconfig.tpl`. Helpers with no consumer are removed: `ipv4-cluster-cidr`, `ipv4-service-cidr` and `ipv4-join-cidr` read `_cluster` keys nothing writes, and only the library's own test chart calls them. What stays is the shared core: the tenant RBAC subject model, resource presets, image references, hostname and `required` helpers from K2. That is the public interface, listed in the contract.
- **Delivery.** `cozy-lib` is published as an OCI Helm library chart with every platform release, under the platform version. A catalog pins it in `Chart.yaml` `dependencies`, and `helm dependency build` puts the pinned version into the catalog artifact at build time. `cozypkg validate` checks the pin, `cozypkg init` scaffolds it. The cluster keeps copying what is in the artifact, so on the cluster side nothing changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you prototyped this? I recall attempting something along these lines last year and found that this makes local development quite a bit harder. The dependencies listed in Helm's Chart.yaml are either a local filesystem reference or an http URL. A reference that works for one mode (e.g. local development) would break on a live cluster. Or an http URL that works in one Cozystack installation breaks in e.g. an airgap installation.

Comment on lines +316 to +329
#### K6. Dashboard

What a catalog declares so the console shows its applications without a console release.

| Item | Contract |
|---|---|
| `AD.spec.dashboard` | the CRD schema. The console reads 5 of its 12 fields today (`category`, `description`, `icon`, `keysOrder`, `module`). The rest are read or declared deprecated. `tabs` becomes the typed list from step 1, `visibility` comes from [community#39](https://github.com/cozystack/community/pull/39) |
| schema extensions | `x-cozystack-options`, the new `x-cozystack-widget`, and the option source annotations, in the K1 extension schema |
| `core.cozystack.io` resources the console uses | options, tenant namespaces, tenant modules, tenant secrets, taps, and their served OpenAPI |
| application labels and release name | from K1 |
| sidebar section object | a CRD with a title, a list of CRDs and a placement |
| RBAC | each catalog grants tenants read access to its CRD schemas and its subresources through the tenant aggregation labels |

**Check:** the console has unit tests that render tabs, actions and sections from fixture ADs of every platform version it supports.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spirit is right, but this is worth a design proposal of its own.

- Application joins our catalog when all of these hold: a maintainer or reviewer takes it in CODEOWNERS, licenses of the application and its operator are acceptable for a CNCF project, it has tests for what the chart adds on top of the upstream operator, its upstream operator is maintained, and users need it, as the proposal shows.
- Until step 3 our catalogs live in `cozystack/cozystack`, so accepted application goes into its catalog directory there, under the coupling rule below, and moves together with the catalog later. We don't create interim repository for it.
- Any other application is published by its author as a catalog from author's own repository and installed through the marketplace. The project does not host such catalogs and has no separate organization for them.
- Recommended layout for such repository is `PackageSource` with `cozypkg`, the same format our own catalogs use after step 2. The older layout from `cozystack/external-apps-example`, a Git repository with a bootstrap `HelmRelease`, keeps working but is not recommended for new catalogs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How to add a compatible app to Cozystack is a contract in itself and should be documented. Off the top of my head it is indeed a PackageSource, a "system" package like newapp-rd with the ApplicationDefinition and an underlying Helm chart along the lines of packages/apps/newapp. Along the way we should also document how someone running an airgap installation or simply an installation with stricter security requirements could vendor a third-party app, so their Cozystack never explicitly reaches out to the third-party author's catalog, but only to their vendoring repo.

Comment on lines +508 to +512
**10. The API watches ApplicationDefinitions** (K1).
- Today `cozystack-api` reads all ADs once at start, and the controller restarts the API Deployment when an AD changes.
- The API watches ADs and adds or removes kinds at runtime. An error in one AD disables that kind with a status condition, and step 0 already stops it from taking down the whole API. The controller stops restarting the API.
- The core derives `chartRef` from the source and component instead of each AD copying the artifact name formula.
- Done when: installing or upgrading a catalog changes the served kinds without restarting `cozystack-api`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This work seems unrelated to the broader goals of the proposal. The API server is restarted on AD changes today because it's a fork of sample-apiserver that carries a lot of initialization code that noone ever untangled to start supporting runtime reconfiguration. This improvement, subject to the availability of manpower can be done at any time.

- a new application needs a platform release for tenant RBAC
- a new VM tab needs a console release

We would pay the full cost of the split (CI, versions, release sync) and reach none of the goals. Rejected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd be able to stop running the massive E2E suite though. That seems like a pretty significant win.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants