Skip to content

feat(kubernetes): give each tenant Kubernetes cluster its own etcd - #4817

Draft
Timofei Larkin (lllamnyp) wants to merge 3 commits into
mainfrom
feat/kubernetes-own-etcd
Draft

Timofei Larkin (lllamnyp) wants to merge 3 commits into
mainfrom
feat/kubernetes-own-etcd

Conversation

@lllamnyp

Copy link
Copy Markdown
Member

What this PR does

A tenant Kubernetes cluster could not start until an ancestor Tenant had spec.etcd: true. The chart read the datastore name from _namespace.etcd and, with none, rendered only a <release>-awaiting-etcd ConfigMap. Every cluster below that tenant then shared one etcd, one CA and one disk, separated by key prefix only. Whoever creates a cluster usually may not edit the Tenant, so anything ordering clusters on a tenant's behalf (the portal's controller, for one) had to flip etcd: true first.

This PR makes the Kubernetes application carry its own etcd. It implements phases 1 and 2 of the per-cluster etcd design proposal, cozystack/community#25, as three commits that can be split later:

  1. refactor(etcd): the etcd chart's EtcdCluster, cert-manager PKI, DataStore, WorkloadMonitor, defrag CronJob, VPA and VMPodScrape move into named templates in cozy-lib (packages/library/cozy-lib/templates/_etcd.tpl), parameterised by the cluster name. packages/extra/etcd calls them with "etcd", and its rendered output is byte-identical before and after (see Verification).
  2. feat(kubernetes): a new cluster renders its own EtcdCluster and DataStore from those templates, and a cluster that already runs on a shared tenant etcd keeps it.
  3. feat(platform): migration 60 → 61 reports which clusters stay on a shared etcd and changes nothing.

Phases 3 and 4 (deprecating, then removing Tenant.spec.etcd, _namespace.etcd and cozy-lib.ns-etcd) are left for later.

Design

Names. For release kubernetes-<name> in namespace <ns>:

Object Name
EtcdCluster, its headless Service, members <…>-xxxxx, <…>-client Service kubernetes-<name>-etcd
Secrets kubernetes-<name>-etcd-{ca,client,server,peer,peer-ca}-tls
DataStore (cluster-scoped) <ns>.kubernetes-<name>
DataStore endpoint kubernetes-<name>-etcd.<ns>.svc:2379

Values. One new field, etcd.replicas: 3 (default) or 1. The schema bounds it to 1..3 and the chart refuses 2 at render time, because cozyvalues-gen cannot express an integer enum (it emits string members for one). Size (4Gi), resources (1000m / 512Mi limits) and the etcd version (3.6.11, the etcd chart's default) are chart-internal.

StorageClass. Two new platform values, kubernetesEtcd.storageClassLocal and kubernetesEtcd.storageClassReplicated, reach the chart as _cluster.etcd-storage-class-local (used with 3 members) and _cluster.etcd-storage-class-replicated (used with 1 member). Empty keys fall back to the namespace default StorageClass. spec.storage.storageClassName is immutable on an EtcdCluster, so the chart reads the live object's class on every render after the first and never re-derives it; scaling 1 → 3 keeps the class the cluster started with.

New clusters vs existing ones. The chart decides from the live KamajiControlPlane, looked up by release name:

  • no KamajiControlPlane, or one whose spec.dataStoreName is the cluster's own name: own etcd;
  • one that names any other DataStore: that DataStore is kept, and the chart renders no etcd of its own.

Clusters stuck in the old awaiting-etcd state have no KamajiControlPlane, so they get their own etcd on their next reconcile. _namespace.etcd is deliberately not the signal. packages/system/cozystack-basics/templates/cozystack-values-secret.yaml hardcodes etcd: tenant-root, and packages/apps/tenant/templates/namespace.yaml passes it down, so every tenant publishes a value whether or not an etcd runs behind it. Honouring it for new clusters would change nothing for anyone. Following it for existing clusters is also wrong: today, switching on spec.etcd for a mid-level tenant silently moves every cluster below it to a new datastore, which is a Kamaji datastore migration. With this PR, the live binding sticks.

An existing cluster renders exactly what it rendered before, apart from randomly generated Talos tokens, so the upgrade rolls no control-plane pods. The new pod label goes on new clusters only.

What goes away. The awaiting-etcd beacon goes, along with the _namespace.etcd gate on the ~25 addon and satellite templates (helmreleases/*, csi/deploy.yaml, kccm/manager.yaml, cluster-autoscaler/deployment.yaml, ingress.yaml, the Proxmox CCM and CSI). A datastore now always exists, so these gates were always true.

Deletion. The existing pre-delete hook deletes the TenantControlPlane and waits up to 120s. Before this PR it exited 0 even when the TenantControlPlane was still there. On a cluster that owns its etcd, Helm deletes the etcd right after the hook, and Kamaji then cannot finish its finalizer, because it needs the datastore to drop the tenant's role, prefix and user. So on such a cluster the hook now fails while the TenantControlPlane remains, keeping the etcd up and making Flux retry the uninstall. After it:

  • Helm removes the EtcdCluster, DataStore and Certificates;
  • the member PVCs go by garbage collection, because the operator owns them through the EtcdMembers;
  • a new post-delete Job removes the five cert-manager Secrets, which carry no owner reference.

The hook never touches a shared etcd, and the Secret cleanup Job is not rendered for a cluster on one.

Deviations from cozystack/community#25

  • DataStore name is <namespace>.<release>, not <namespace>-<release>. Tenant names are alphanumeric and nest with dashes, so the dashed form collides: Kubernetes foo in tenant-a gives tenant-a-kubernetes-foo, which is also the namespace, and therefore the shared DataStore name, of tenant foo under tenant kubernetes under tenant-a. A namespace cannot contain a dot. Kamaji copies the DataStore name into the pod label component.kamaji.clastix.io/datastore, so the name is capped at 63 characters, truncated with an 8-character hash past that.
  • No truncate-and-hash for kubernetes.etcdName. The app name is now capped at 32 characters (@maxLength 32, added with the nodes split), so kubernetes-<name>-etcd is at most 48 characters. That is inside the 56 the operator needs: the <name>-client Service and the generated member names must fit in 63. The helper fails the render past 56 rather than rename anything.
  • "Honour an explicit _namespace.etcd" means keeping a cluster on the DataStore its live control plane names, for the reason above. It does not mean following _namespace.etcd for new clusters.
  • etcd.replicas validation is a schema range of 1..3 plus a render-time refusal of 2, rather than a schema enum.
  • The delete-hook branch keys on the same live-KamajiControlPlane condition as the render, as Release 0.1.0 #25 asks, but it fails the uninstall rather than ordering deletions inside the hook. Helm deletes release resources only after a successful pre-delete hook, so that ordering already holds once the hook refuses to pass early.
  • TLS reuses the etcd chart's secretRef shape (chart-issued certificates) rather than the operator's certManager mode. Kamaji's DataStore needs the CA private key to sign each tenant's client certificate, and secretRef keeps that key in a Secret the chart controls. The server certificate also carries the operator's <name>-client Service names, which the etcd chart's does not.
  • Satellites. The PrometheusRule stays in the etcd chart only: its expressions match every etcd job, so one copy per cluster would duplicate every alert. VMPodScrape, VPA and the CiliumNetworkPolicy render only where their APIs are served.
  • Migration. The opt-in migration runbook is not implemented here (see below), and no talosconfig is issued. The numbered migration only reports, as Release 0.1.0 #25 suggests.

Gaps #25 did not cover

  • Backups. Decision: not covered in this PR. The Etcd backup strategy (internal/backupcontroller/etcdstrategy_controller.go, packages/system/backupstrategy-controller/templates/strategy-etcd-default.yaml) is keyed on the apps.cozystack.io/Etcd application and on the fixed EtcdCluster name etcd, and cozy-default routes no Kubernetes kind. A cluster's own etcd is therefore not backed up by the platform. A cluster still on a shared tenant etcd keeps exactly the coverage it had. The guest-side Velero addon covers workloads, not control-plane state. An operator can snapshot a cluster's etcd by hand with an EtcdSnapshot against kubernetes-<name>-etcd; the server certificate covers the -client Service the snapshot Job dials. Backup alone could route Kubernetes to the existing driver with a name lookup. Restore is the hard part: it means recreating the EtcdCluster under a live Kamaji TenantControlPlane, and needs its own design before anything is wired. The README states the gap.
  • Per-cluster network policy. A CiliumNetworkPolicy named after the etcd selects its member pods with ingressDeny on 2379/2380 against two sets of pods. The first is pods in the tenant namespace that are neither members nor carry policy.cozystack.io/allow-to-etcd-cluster: <etcd name>, a label set on the control-plane pods and the defrag Job. The second is pods in any other namespace except kube-system and namespaces labelled cozystack.io/system: "true", which is where Kamaji, its migrate Job and etcd-operator run. Deny rules are needed because the tenant's own policies allow all traffic within a namespace and from ancestor tenants, and allow rules only add. Metrics on 2381 stay open. The pods also carry cozystack.io/service: kubernetes-etcd rather than cozystack.io/service: etcd, so the tenant-wide <tenant>-ingress-etcd policy does not admit child tenants' control planes to them. The policy renders only where Cilium serves cilium.io/v2/CiliumNetworkPolicy.
  • Platform ordering. cozystack.kubernetes-application now dependsOn cozystack.etcd-operator. Both come with the isp-full variants, the only ones the iaas bundle allows. The selective-install fixture is unchanged (hack/select-install.sh --disabled postgres produces the same set). The same edge makes an etcd-operator change select the kubernetes-latest, kubernetes-previous and computeplane e2e suites besides etcd, and hack/select-e2e_test.bats now expects that.
  • e2e.
    • hack/e2e-install-cozystack.bats no longer turns on tenant-root's etcd, which only Kubernetes clusters used. A cluster that fell back on a shared etcd would therefore not come up.
    • run_kubernetes_test asserts the control plane names tenant-test.kubernetes-<name> and that kubernetes-<name>-etcd reaches Available.
    • cozy_cleanup waits until the EtcdCluster, DataStore, member PVCs and Secrets are gone.
    • The Proxmox suite's tenant drops etcd: true and the comment calling it mandatory.
    • The ComputePlane suite's comment no longer cites tenant: tenants below depth 1 cannot reach etcd inherited from tenant-root #4726.
    • hack/admin-kubeconfig-invariant.bats drops its two awaiting-etcd cases, and the now-unused tests/values-ci-no-etcd.yaml goes with them.
  • Generated artifacts come from the tooling. make -C packages/apps/kubernetes generate produced values.schema.json, the README table, api/apps/v1alpha1/kubernetes/types.go and packages/system/kubernetes-rd/cozyrds/kubernetes.yaml. Root make generate regenerated zz_generated.deepcopy.go, and pkg/generated/openapi did not change. make generate in every touched package leaves the tree clean.
  • Existing clusters. Migration 60 → 61 lists every KamajiControlPlane whose DataStore its own release did not render, judged by Helm's release annotations on the DataStore, with that DataStore's endpoints. It writes nothing but the version stamp. Slot check on 2026-10-08:
  • README. packages/apps/kubernetes/README.md claimed a dedicated etcd per cluster while clusters shared the tenant's. It now describes what this PR ships, including the legacy case and the backup gap. The ComputePlane README no longer lists a tenant etcd as a prerequisite.

Migrating an existing cluster (designed, not implemented)

Nothing here moves a cluster, and nothing should until the runbook below has an e2e. It combines #25's runbook with SerjioTT's Talos measurements of 2026-09-03 (about 20s copy, 43s from patch to Ready, reads uninterrupted) and with what Kamaji 26.9.4-edge's code does.

  1. Opt-in. The opt-in has to make the chart render the cluster's own etcd beside the shared one, without switching dataStoreName. The likely shape is a chart-internal value, or an annotation the chart reads, that the operator sets. Wait for Available=True.
  2. Snapshot the source etcd. This is the rollback path.
  3. Pre-flight reachability from the cluster's namespace, not Kamaji's (SerjioTT finding 1). Kamaji's migrate Job runs in Kamaji's namespace and can reach any etcd, so the copy "succeeds" toward a target the control-plane pods cannot dial; the new apiserver generation then crash-loops and the cluster stays write-frozen. Run nc -zv <target> 2379 from a pod carrying the control plane's labels in the cluster namespace. For the per-cluster target this holds by construction (same namespace, and the deny policy admits the control plane), but the check stays because rollback goes the other way.
  4. Switch. Point KamajiControlPlane.spec.dataStoreName at <ns>.<release>, with the CAPI provider re-projecting it onto the TenantControlPlane. kamaji.clastix.io/migration-timeout can go on the KamajiControlPlane, because the provider copies annotations to the TenantControlPlane. Before switching, delete any leftover migrate-<TCP UID> Job in Kamaji's namespace:
    • a leftover with identical arguments is taken as success and switches the datastore without copying;
    • one with different arguments is deleted and recreated, with the freeze already installed.
  5. Verify writes, not status (SerjioTT finding 2). Kamaji installs a kamaji-freeze ValidatingWebhookConfiguration in the guest, and only its soot controller removes it, only when the TenantControlPlane status is exactly Ready. A paused TenantControlPlane, or one sitting in WriteLimited, Sleeping or Upgrading, keeps the freeze. Node leases are exempt and reads are untouched, so nodes and kubectl get look healthy. The success criterion is a canary write through the guest API; if kamaji-freeze is still in the guest after the TenantControlPlane is Ready, delete it.
  6. Restart every worker kubelet, one node at a time (SerjioTT finding 3). Until it restarts, a kubelet keeps running existing pods but never starts a newly bound one, so the cluster looks healthy and silently stops starting workloads. The per-node check is a new pod starting on that node.
    • On Talos there is no operator path to the workers' API today: the chart has a per-cluster talos-ca Issuer but issues no client credentials from it, so the fallback is virtctl restart per VM.
    • Issuing an operator talosconfig from that CA alongside the admin kubeconfig is the follow-up that makes this step cheap. It is not in this PR: it is a separate credential with its own exposure question.
    • Any pod-based restarter must be deployed before the switch, because a deaf kubelet will not start it.
  7. Retain the source prefix and the snapshot for a cool-off period, then reclaim. Kamaji does not clean the old prefix after a migration. Rollback is the same procedure in reverse, against the retained snapshot.

Test plan

Done here, no cluster involved:

  • helm template of packages/extra/etcd before and after the extraction is byte-identical: 12 renders (default; 1 member + local SC + 10Gi; resources with CPU and memory allocation ratios + another version; a global topology spread; a hard affinity with 5 members and 512Mi; leftover backup.* values with 0 members), each in tenant-root and tenant-abc, all identical by sha256.
  • helm template of packages/apps/kubernetes with Cilium served. With _namespace.etcd set to tenant-root, or empty, both with 3 members: EtcdCluster/kubernetes-demo-etcd replicas=3 on the namespace default StorageClass, and DataStore/tenant-demo.kubernetes-demo pointing at kubernetes-demo-etcd.tenant-demo.svc:2379. The KamajiControlPlane names that DataStore, and the policy renders.
  • With the platform StorageClass keys set: 1 member uses replicated and 3 members use local. 2 members is refused at render time; 5 is refused by the schema.
  • An existing cluster (live-KamajiControlPlane lookup stubbed to return tenant-root) renders identically to main with values-ci.yaml, apart from random Talos tokens. A new cluster's render differs from main only by the old shared dataStoreName line and the added etcd objects.
  • helm-unittest:
    • packages/apps/kubernetes: 359 tests, including a new 19-case tests/etcd_test.yaml. It covers names, the second cluster in one namespace, the 63-character DataStore cap, StorageClass choice and stickiness, refusal of 2, the policy, the Secret cleanup and the delete-hook gate. For an existing cluster it covers staying on the shared DataStore, ignoring a changed _namespace.etcd, no own etcd, no Secret cleanup and no gate.
    • packages/extra/etcd: 22. packages/core/platform: 258, including a new wiring test for the StorageClass keys.
    • cozy-lib-tests, packages/apps/tenant and packages/extra/computeplane: green.
  • bats:
    • hack/migration-60-shared-etcd-report.bats (new, 4 cases);
    • every hack/run-kubernetes*_test.bats, admin-kubeconfig-invariant, check-hook-request-timeout, app-cleanup-hooks_test, bats-no-negated-assert, bats-no-exit-trap, select-install_test, select-e2e_test, helmrelease-readiness, proxmox-e2e-fixture_test, talos-image-cache_test, linstor-storage-mode_test and cozystack-version-stamp.
  • go build ./..., go test ./pkg/registry/... and the api/apps/v1alpha1 module.
  • make generate in every touched package and at the root leaves no diff; hack/check-commit-trailers.sh origin/main..HEAD passes.

Needs a live cluster:

  • The e2e lanes: kubernetes-latest, kubernetes-previous and computeplane, now without tenant-root's etcd. The Proxmox lane on its self-hosted runner.
  • Upgrade from main with an existing cluster on tenant-root's etcd. It must stay on tenant-root with no control-plane rollout, and migration 60 must list it.
  • The deny policy: from a pod in the tenant namespace without the label, and from a guest worker VM, nc -zv kubernetes-<name>-etcd.<ns>.svc 2379 must be refused. The control plane, the defrag Job, an EtcdSnapshot Job, Kamaji and etcd-operator must still connect.
  • Delete a cluster whose TenantControlPlane is held past the 120s wait. The uninstall must fail and retry rather than leave a TenantControlPlane stuck on its finalizer.
  • A tenant whose ResourceQuota cannot fit 3 × 1000m/512Mi limits and 3 × 4Gi requests must surface the quota error on the etcd member pods.

Follow-ups, not opened

These are listed here at the maintainer's request rather than filed:

  • cozystack/website:
    • content/en/docs/next/operations/configuration/platform-package.md gains kubernetesEtcd.storageClassLocal and kubernetesEtcd.storageClassReplicated;
    • content/en/docs/next/kubernetes/relocate-etcd.md describes the shared tenant etcd as what a cluster uses;
    • tenant and Kubernetes docs telling users to set etcd: true for a cluster, which is now an obsolete workaround;
    • the Kubernetes app reference is regenerated from the README on the next stable tag.
  • cozystack/terraform-provider-cozystack: internal/provider/kubernetes_schema.go and kubernetes_model.go need etcd.replicas, with the default of 3, a 1..3 range and the expand/flatten pair.
  • cozystack/cozystack, after this lands:
    • backup (and later restore) of a cluster's own etcd;
    • the opt-in migration path and its e2e;
    • an operator talosconfig;
    • phases 3 and 4.

Screenshots

Not a UI change.

Downstream repositories

Follow-ups are listed above under "Follow-ups, not opened". By the maintainer's decision they are not filed from this draft, so no box is ticked.

Release note

feat(kubernetes): each new tenant Kubernetes cluster now gets its own etcd (EtcdCluster `kubernetes-<name>-etcd` and Kamaji DataStore `<namespace>.kubernetes-<name>`), created and deleted with the cluster, so no ancestor Tenant needs `etcd: true` and the `awaiting-etcd` state is gone. The new `etcd.replicas` field chooses 3 members (default) or 1. Each member has 1000m CPU and 512Mi memory limits and a 4Gi volume, counted against the tenant's quota. The platform values `kubernetesEtcd.storageClassLocal` and `kubernetesEtcd.storageClassReplicated` choose the StorageClass for 3 members and 1 member respectively. Existing clusters keep the shared tenant etcd they run on, unchanged; the upgrade lists them in the migration log. The platform's `Etcd` backup strategy does not cover a cluster's own etcd.

The kubernetes chart is about to render an etcd of its own per cluster,
and it needs the same EtcdCluster, PKI, DataStore, defrag, VPA and
scrape shapes the etcd chart renders. It cannot take the etcd chart as a
subchart: the chart refuses any release not named "etcd", and the names
it renders are pinned by the immutable spec.tls of adopted clusters.

The shapes therefore move into cozy-lib, parameterised by the cluster
name, and the etcd chart calls them with "etcd". Its rendered output is
byte-identical before and after.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <[email protected]>
A tenant Kubernetes cluster could only start once an ancestor Tenant had
an etcd module turned on, which nobody creating a cluster may do, and
every cluster below that tenant then shared one etcd, one CA and one
disk. Anything that orders clusters on a tenant's behalf had to flip the
tenant's etcd on first.

A new cluster now renders its own EtcdCluster and DataStore, from the
cozy-lib shapes the etcd chart uses, and is created and deleted with
them. The awaiting-etcd state goes, since a datastore always exists.

An existing cluster stays on the DataStore its live KamajiControlPlane
names. Changing it starts a Kamaji datastore migration, which freezes
the tenant's writes and leaves every worker kubelet needing a restart,
so no upgrade may do it. _namespace.etcd cannot decide this: tenant-root
publishes one to every tenant whether or not an etcd runs behind it.

The pre-delete hook now fails while the TenantControlPlane remains,
because Kamaji cannot finalise one whose etcd Helm has already removed.
A Cilium deny policy keeps the rest of the tenant namespace off the
etcd ports, and the platform waits for etcd-operator before offering
the application.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <[email protected]>
Existing tenant clusters keep the shared tenant etcd they run on, and
moving one is a per-cluster operator decision that the platform never
takes on an upgrade. Retiring Tenant.spec.etcd has to wait until no
cluster uses it, so the upgrade lists the clusters that still do. It
writes nothing beyond the version stamp.

Assisted-by: LLM
Signed-off-by: Timofei Larkin <[email protected]>
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · 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

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files labels Oct 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Timofei Larkin (@lllamnyp) NOT LGTM. The design holds up, but the branch conflicts with main, two existing bats suites fail because of this change, long cluster names cannot install, and a tenant that keeps its shared etcd gets its quorum alert diluted.

Business context: a tenant Kubernetes cluster gets its own etcd and Kamaji DataStore instead of waiting for an ancestor Tenant to turn on a shared one, while existing clusters stay on the shared datastore they already use.

I tried hardest to break the parts that touch existing control planes, and they hold:

  1. Upgrade. kubernetes.etcd.sharedDataStore keys on the live KamajiControlPlane, so a cluster already on tenant-root keeps that dataStoreName and renders no etcd of its own. When I forced that helper to return empty, five cases in etcd_test.yaml failed, so the stickiness is pinned. Migration 60 only reads and stamps, and its bats pass.
  2. Kamaji wiring. The TenantControlPlane webhook only requires the DataStore to exist, and the release creates it next to the KamajiControlPlane, so the provider's retry covers the ordering.
  3. Deletion. Member PVCs are controller-owned by the EtcdMembers in etcd-operator, so they go with the EtcdCluster. The five cert-manager Secrets go through the post-delete Job. When I disabled the hook gate, etcd_test.yaml caught it.

On the PR head helm unittest passes for kubernetes (359), extra/etcd (22), platform (258), tenant (76), computeplane (13) and cozy-lib-tests (62), and select-install_test.bats passes.

Blockers

B1: conflicts with main

packages/apps/kubernetes/templates/delete.yaml, packages/extra/etcd/templates/etcd-defrag.yaml and packages/system/kubernetes-rd/cozyrds/kubernetes.yaml conflict, and GitHub reports CONFLICTING. One of them needs more than a textual fix. fe59a6b added --compaction=false and --etcd-storage-quota-bytes to the etcd chart's defrag CronJob, and that CronJob now lives in cozy-lib.etcd.defrag. The flags have to go into the cozy-lib template so both charts get them, with the quota computed from each caller's size (4Gi for a cluster's own etcd). Otherwise the extraction silently reverts that fix. The defrag_test.yaml that main added will catch the etcd chart side, but nothing checks the per-cluster side. 904bc38 also changed the same pre-delete script this PR edits.

B2: two existing bats suites fail on this head

Unit & controller tests is red (run of 2026-10-08T10:30Z, attempt 1), so E2E (in-tree) was skipped. That means none of the "Needs a live cluster" items has run, the upgrade case included. I reproduced both failures locally:

  1. hack/kubernetes-pre-delete-hook.bats cases 5, 6, 8 and 10 still expect exit 0 when the TenantControlPlane outlives step 4 or its probe fails. Case 10 says "an all-failures run exited 1, which would wedge the release". The new exit 1 is the opposite contract, so those cases have to be split into a shared-datastore case (still exits 0) and an own-etcd case (fails closed). This file is not in the test plan.
  2. hack/check-render-matrix.bats "fixture states enable OIDC groups and shared-service resources" expects the fresh fixture, which has _namespace.etcd: "", to render no kubernetes-check-metrics-server. With the etcd gates gone, it does render. The test and the fixture's meaning need updating, not just the expected count.

B3: the defrag CronJob name exceeds 52 characters for long cluster names

cozy-lib.etcd.defrag names the CronJob <etcd name>-defrag, which for this chart is kubernetes-<name>-etcd-defrag. The app name allows 32 characters, so the CronJob name reaches 55. The apiserver rejects CronJob names longer than 52, so any cluster named with 30 to 32 characters fails to install. I rendered kubernetes-abcdefghijklmnopqrstuvwxyz0123 and got the 53-character kubernetes-abcdefghijklmnopqrstuvwxyz0123-etcd-defrag. The 56-character guard in kubernetes.etcd.name does not cover this. Either shorten the CronJob name (truncate plus hash) or lower the guard, and add a test at the maximum name length.

B4: a tenant's shared etcd scrapes the cluster etcds in its namespace

packages/extra/etcd/templates/podscrape.yaml selects app.kubernetes.io/name: etcd, and etcd-operator puts that label on every member (clusterLabels in controllers/helpers.go). In a tenant with etcd: true that creates a cluster in the same namespace, etcd-pod-scrape now also scrapes kubernetes-<name>-etcd. Those members are then scraped twice, and the shared etcd's job counts six members instead of three. etcdInsufficientMembers sums up by job, so with two of the three shared members down it sees 4 of 6 up and stays quiet. Before this PR nothing else in that namespace carried the label. Selecting on app.kubernetes.io/instance: etcd (or etcd-operator.cozystack.io/cluster: etcd) fixes it.

Decisions to confirm, not blocking

  1. New clusters lose platform backup. Before, a new cluster's control plane lived in the tenant etcd, which an Etcd BackupJob covers. After this PR it lives in an etcd no strategy routes to. The README and the release note say so. I'd still like a maintainer to accept that explicitly before this leaves draft, because it is a regression for anyone who backs up tenant etcd today.
  2. The new exit 1 wedges the uninstall whenever the TenantControlPlane cannot finalize, including when the cluster's own etcd never became Available, for example because of the quota case in your test plan. Whether Kamaji can finalize a TenantControlPlane whose datastore was never reachable decides whether that deletion ever finishes without manual work. That case belongs in the live test list.
  3. kubernetesEtcd.storageClassLocal defaults to empty, so on a default install three members land on the namespace default class. That is usually the replicated one, which is the double replication the values comment says the local class avoids.

apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ $name }}-defrag

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

B3: for this chart this is kubernetes-<name>-etcd-defrag, which is 53 to 55 characters when the app name is 30 to 32. The apiserver caps CronJob names at 52.

selector:
matchLabels:
app.kubernetes.io/name: etcd
{{ include "cozy-lib.etcd.podScrape" (dict "name" "etcd" "selector" (dict "app.kubernetes.io/name" "etcd")) }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

B4: every etcd-operator member carries app.kubernetes.io/name: etcd, so this now also matches kubernetes-<name>-etcd pods in the same namespace. app.kubernetes.io/instance: etcd keeps the job to this cluster.

TenantControlPlane whose datastore no longer answers: its
finalizer then stays until removed by hand. Failing here
keeps the etcd up and makes Flux retry the uninstall. */}}
if [ "$tcp_lookup" != 0 ] || [ -n "$tcp" ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

B2: hack/kubernetes-pre-delete-hook.bats cases 5, 6, 8 and 10 still expect exit 0 on this path and fail.

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

Labels

area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants