Fail-closed GPU admission must remain available during controller
rollouts and node disruptions. Replicas sharing one node would fail
together, and an unready replica must not receive admission traffic.
The availability dependency must be limited to VMs using class entries.
Other VMs and pods should not wait for a GPU webhook they do not need.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The class status and the license copier run in cozystack-controller
behind --enable-gpu, which the chart passes from gpu.enabled. The
status controller is off without the flag, since it caches every
Node. The copier always runs: without the flag it deletes every
copy, so a token does not stay in tenant namespaces after GPU
features are switched off, and clears the free counts and conditions
the stopped status controller would otherwise leave stale.
License Secrets are watched on the metadata-only Secret cache the CA
projection already owns. The manager's own Secret cache is scoped by
label to wildcard replicas, and a watch on it would never see a
hoster's Secret. The envtest suite drives both controllers through
their watches, since a mis-scoped watch fails without an error.
No RBAC rule is added: the controller already reads every kind, holds
every verb on cozystack.io and the cluster-wide Secret write.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
A hoster publishes each GPU offer as a cluster-scoped GPUClass: one
resource name per offer, the tolerations its VMs need on the class's
nodes, and for vGPU classes the license Secret and FeatureType the
guests get. VMs and worker pools name a class instead of a raw
device-plugin name, so the offer can be budgeted per tenant and shown
in a catalog with its free units.
The kind joins cozystack.io instead of a group of its own: a CRD
cannot move between groups in place, and nothing in the design needs
separate versioning or serving. The class name becomes part of the
budget key <class>.gpuclass.cozystack.io/devices, so it is a DNS
label, and a resource name in that domain is refused.
Vendor, model, mode, profile, memory and the source kind never
change, so a class name stands for one offer for its whole life and
billing records keyed by it stay meaningful. A class carries no node
selector: its nodes are the nodes that advertise its resource name.
The CEL tests run the shipped CRD through the apiserver's own prune,
default, schema and CEL steps, so a marker that failed to generate
fails a test instead of admitting a bad class.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
An empty selector matches every Pod, PVC, Service and DataVolume in the
namespace, so one such monitor goes non-operational on any import there.
No chart renders one, but the schema marked the field required without
requiring any label, and the API accepted it.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
- cozystack-api, cozystack-controller, lineage-controller-webhook: conditional
resources block via `{{- with .Values.*.resources }}`, defaulting to `{}` so the
default render stays unchanged
- cozystack-controller: replicas comes from values (was hardcoded 1); the chart
now passes --leader-elect unconditionally and grants the Lease verbs through a
namespaced Role and RoleBinding in cozy-system (the Lease lives in the manager's
own namespace), so raising replicas adds a standby instead of a second active
writer over the reconcilers that patch HelmReleases, Gateways, Certificates,
ResourceQuotas and Secrets
Signed-off-by: Nikita Aboltin <aboltin.nikita@rwb.ru>
The suite's "does not grant a blanket wildcard verb on secrets" assert
was a notContains over a rule object. helm-unittest compares every field
it is handed by value equality, and `any: true` changes which fields are
compared rather than how, so no spelling of that assert bounds what a
verbs list may hold: `verbs: ["get", "*"]` satisfies all of them.
Measured against the rendered ClusterRole, seven realistic spellings of
a widened Secret grant left that assert green: a wildcard in resources,
a wildcard in apiGroups, a "*" appended to the declared verb list, an
extra field beside the verbs, deletecollection spelled out, and two
where the grant is added as a separate rule. Those last two leave the
whole suite green - appending apiGroups:['*'] resources:['*']
verbs:['*'] keeps the positive pin satisfied while the controller gains
every verb on every Secret cluster-wide.
Replace it with a bats guard over the rendered manifests. It unions the
verbs that every ClusterRole and Role rule confers on core Secrets,
expanding "*" on each of the three axes, and compares that union with
the declared set. Equality rather than an upper bound: the controller is
cluster-scoped, so a widening reaches every tenant namespace, and a
narrowing breaks the two reconcilers that share the grant.
The positive pin stays in tests/rbac_test.yaml. Value equality is the
right primitive for pinning a rule as written and the wrong one for
bounding what the rule may become.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Every catalog engine issues a per-release CA, but the object holding
ca.crt usually holds a private key beside it (cert-manager's tls.key,
CloudNativePG's ca.key), so granting a tenant read on it hands over key
material too. Add a controller that publishes the trust anchor as one
canonical, key-free Secret per release, "<release>.tenant-ca", holding
only ca.crt.
A chart declares the anchor by rendering a namespaced TenantProjection
sentinel (internal.cozystack.io/v1alpha1) that names the source Secret
and key. The controller rebuilds ca.crt from the parsed certificate DER
rather than copying the source, refuses any value carrying private key
material, and owns the projection by owner reference so a foreign Secret
at the same name is never overwritten. No tenant RBAC grants any verb on
internal.cozystack.io; a ValidatingAdmissionPolicy pins writes to
helm-controller as defence in depth.
The controller does not grant tenant read access — the lineage webhook
does, from the ApplicationDefinition's spec.secrets. This side stamps the
engine-agnostic internal.cozystack.io/tenant-ca label and forces a
re-admission when those selectors change.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The operator-provided wildcard MVP serves the root tenant only: the root
ingress controller and Gateway read a TLS Secret from their own namespace.
Child tenants run their own ingress controller / Gateway in a separate,
default-deny namespace and cannot read that Secret cross-namespace.
Add a controller that mirrors the operator wildcard Secret into every
tenant namespace that owns a TLS termination point (its
namespace.cozystack.io/ingress or .../gateway label equals its own name).
The source is identified by name from the platform values channel
(_cluster.wildcard-secret-name) with no extra operator input; replicas
carry a managed-by label and refresh on rotation. An individual replica is
removed when its namespace stops terminating TLS; every replica is torn
down only on an explicit disable (clearing wildcard-secret-name). A missing
or mistyped source, or a transiently-absent values channel, keeps existing
replicas, so tenant TLS never drops on a transient gap. The controller's
Secret cache is scoped to managed replicas and the values channel, not
every cluster Secret. Only the platform controller's ServiceAccount gains
Secret writes — no tenant RBAC is widened, and every consumer reads only
its own-namespace copy.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>