Four findings from the gemini re-review:
- `.Values.oidc.customConfig.secretRef.name` and
`.Values.oidc.customConfig.config` — replaced direct chained
access with `dig "customConfig" "…" "" .Values.oidc` so a CR that
explicitly nulls `customConfig` doesn't nil-panic. `.Values.oidc`
itself has a kubebuilder default of `{}` so it stays reachable.
- `.Values.controlPlane.apiServer.extraArgs` in the collision guard
— wrapped with `| default list` so an explicit null renders as an
empty range rather than a nil deref. (`dig` on `.Values` itself
doesn't work because it's `chartutil.Values`, not
`map[string]interface{}`.)
- `base64 -w 0` in the OIDC kubeconfig patch — replaced with
`base64 | tr -d '\n'`. Both work on the current alpine/k8s image
(BusyBox 1.37 supports `-w`), but the pipe form is portable back
to older BusyBox releases and to macOS/BSD base64 (which does not
know `-w`).
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Same class of upgrade regression: a customer who wired their own
structured AuthenticationConfiguration by hand via
`controlPlane.apiServer.extraArgs: [--authentication-config=...]`
would hit a duplicate-flag scenario the moment they toggle
`spec.oidc.mode` — kube-apiserver would receive two conflicting
--authentication-config= arguments and the last-wins behaviour is
undefined between apiserver versions.
Add a second render-time guard that fails when a legacy
--authentication-config= is present in the user-supplied extraArgs
AND spec.oidc.mode is not None, pointing at the CustomConfig
migration path (customConfig.config or customConfig.secretRef.name).
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Reviewer flagged a real customer-upgrade regression: the previous
values.yaml docstring on `controlPlane.apiServer.extraArgs` documented
`--oidc-issuer-url`/`--oidc-client-id` as a valid usage. An existing
customer who wired OIDC by hand through that passthrough (following
the doc) would see their tenant kube-apiserver BOOT-FAIL the moment
they toggle `spec.oidc.mode` to `System` or `CustomConfig`:
kube-apiserver refuses to boot with both `--authentication-config`
(injected by the new spec.oidc) and any legacy `--oidc-*` flag —
`--authentication-config cannot be used with any of --oidc-*`.
Add a render-time guard: when `spec.oidc.mode != None`, scan the
user-supplied `controlPlane.apiServer.extraArgs` for any flag with
prefix `--oidc-` and fail with a clear message pointing at the
`spec.oidc.mode: CustomConfig` migration path (they can carry the
same issuer/audience inside `customConfig.config`).
When `spec.oidc.mode: None` the passthrough still works — customers
who manage OIDC entirely by hand keep working until they opt into
the new selector explicitly.
Also update the values.yaml docstring on `extraArgs` to steer OIDC
users to `spec.oidc.mode` and warn against the collision.
Two new helm-unittest cases: the collision fails, and mode=None with
legacy flags renders normally (backward compat).
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Adversarial review flag: the previous rewording implied the cozy realm
has `email` in its client-scope defaults, which is misleading — the
per-cluster client my chart creates lists only `groups` and the
per-cluster audience scope as defaults. The `email` claim reaches the
token because the chart-generated `kubectl oidc-login` block passes
`--oidc-extra-scope=email` explicitly, not because of realm-side
default-scope hygiene.
State the actual mechanic: `email` is a built-in OIDC scope available
in every conformant issuer, requested by the client at auth time via
the kubeconfig's exec block. No realm-side configuration required.
For `CustomConfig`, note that the operator should verify their BYO
issuer emits `email` when the client requests the `email` scope
(every conformant provider does).
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Two follow-ups from the final adversarial review:
- The chart-generated OIDC kubeconfig exec block requested only
`--oidc-extra-scope=groups`. On a Keycloak realm where `email` is
not a default client scope, kubectl-oidc-login would fetch a token
that lacks the `email` claim — apiserver would then fail to
identify the user, causing a silent auth failure. Add
`--oidc-extra-scope=email` explicitly so the flow does not depend
on realm-level default-scope hygiene.
- `docs/oidc-tenant.md` still said the `User:` subject "must match
the `preferred_username` claim emitted by the issuer" — obsolete
after the claim switch. Rewrite to say `email`, and add a note
that the chart requests the `email` scope explicitly.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Real bug caught by an end-to-end OIDC login test on dev3: apiserver
accepts the JWT (auth succeeds) but the RBAC check fails with 403
because the CRB `User:` subject was `alice-oidc@test3.example` (from
`users[].email`) but the apiserver-visible identity was `alice-oidc`
(from `preferred_username`, which in the cozy realm is the Keycloak
*username*, not the email).
The design proposal example used `claim: preferred_username`, but
the operator-facing `users[].email` field explicitly asks for the
email address. In the cozy realm — verified — `preferred_username`
is the Keycloak username (`kvaps`, `i.okhotnikov`), so the two do
NOT coincide. Every user whose Keycloak username differs from their
email would silently 403 after a successful OIDC handshake.
Switch the AuthenticationConfiguration `claimMappings.username.claim`
to `email`, aligning the apiserver-visible identity with the
operator-declared `users[].email`. Every conformant OIDC provider
emits `email` — cozy realm does (verified), and any BYO
`CustomConfig` issuer that follows OIDC does too.
The switch is invisible to installers where `preferred_username`
already equals the email (a common Keycloak setup); for installers
where it doesn't, this fix is required for RBAC to work at all.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Caught by dev3 Job runtime: `kubectl patch --type=merge` fetches the
current object before computing the merged state, so the Role needs
`get` in addition to `patch` on the specific Secret name. Without it
the Job fails with `secrets "..." is forbidden: ... cannot get
resource "secrets"` and the OIDC kubeconfig stays on its placeholder.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Tighten the release Role — the `delete` verb on `keycloakclients` and
`keycloakclientscopes` now carries `resourceNames` naming exactly the
per-cluster client and its audience scope. Without the scope, the SA
could delete any Keycloak object in the tenant namespace.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Follow-up to the KCP cleanup extension: Kamaji's SSA field manager
takes ownership of `spec.apiServer.extraArgs` and
`spec.apiServer.extraVolumeMounts` too, not just
`spec.deployment.extraVolumes`. The previous normalize patch only
replaced extraVolumes; the two remaining fields still carried the
`--authentication-config=` flag and the corresponding mount even
after mode was toggled to None.
Extend the merge patch to REPLACE all three fields at once, sourced
from Helm values so user-supplied entries in
`controlPlane.apiServer.extra{Args,VolumeMounts,Volumes}` remain
intact — only the chart-owned OIDC-only entries drop off.
Verified on dev3: with the extended patch, mode=System → mode=None
leaves the tenant KCP with `extraArgs: []`,
`extraVolumeMounts: []`, and `deployment.extraVolumes` back to just
`talos-ca`, `talos-tls-cert` (or those + operator-supplied extras).
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Follow-up to the KCP normalize commit: Helm-controller's SSA-based
apply also does not delete namespaced Secrets and cozy-realm
Keycloak resources when the new manifest omits them — the resources
stay as ghost owner-labeled-by-Helm objects. The dashboard picks
them up (via the cozyrds `secrets.include` list) and shows the
placeholder kubeconfig even for clusters where OIDC was explicitly
turned off.
Extend the `mode=None` normalize path to also `kubectl delete`:
- `<release>-oidc-kubeconfig` Secret
- `<release>-oidc-authn-config` Secret
- `<namespace>-<release>` KeycloakClient in the cozy realm
- `<namespace>-<release>-audience` KeycloakClientScope
The delete verbs are added to the release-owned Role scoped to the
specific resourceNames (Secrets) and the API group (Keycloak — the
EDP operator does not support resourceNames on cluster-scoped-ish
CRDs). No new privileges beyond what the chart itself would need to
render these objects.
Together with the previous KCP-normalize commit, `mode=System →
mode=None` now leaves ZERO chart-owned OIDC leftovers.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Kamaji's controller takes SSA field-ownership on
`KamajiControlPlane.spec.deployment.extraVolumes` when it syncs the
resource back from the TenantControlPlane, and Helm's subsequent
apply (with the same field-manager but a manifest that omits the
OIDC volume) then no longer removes it — the tenant apiserver Pod
keeps an unmounted-but-declared `authentication-config` volume
pointing at a Secret the chart already reaped. Harmless at runtime
(the volume is not mounted after the extraVolumeMounts fix), but the
CRD gets dirty and the operator wants zero leftovers.
Fix: the OIDC bootstrap Job is now rendered for ALL modes (not only
`System`/`CustomConfig`). When `mode=None` the Job takes the
normalize path — a single `kubectl patch --type=merge` that replaces
`spec.deployment.extraVolumes` with the chart baseline (talos-ca +
talos-tls-cert). We verified end-to-end that `kubectl patch
--type=merge` on this array REPLACES it deterministically (the
earlier Helm patch didn't).
For `mode=System`/`CustomConfig` the Job continues to apply
per-user ClusterRoleBindings inside the tenant cluster, prune stale
ones labelled by this release, and patch the pre-rendered
`<release>-oidc-kubeconfig` Secret with a real kubeconfig.
The Role gains `patch` on the specific `kamajicontrolplanes/<release>`
so the normalize path has just enough privilege; nothing else new.
The pre-delete cleanup Job stays conditional on `mode != None` — no
CRBs to clean up when OIDC was never enabled.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Two real bugs caught by the dev3 e2e test — both would have shipped
without live-cluster validation:
- `rm --force` in the bootstrap and cleanup Job scripts is rejected
by BusyBox `rm` (the alpine/k8s image uses BusyBox coreutils, not
GNU). The bootstrap Job on dev3 died at the prune step with
`rm: unrecognized option: force`. Switch to `rm -f` (works on
both GNU and BusyBox). Also swap
`grep --line-regexp --quiet --fixed-strings` for `grep -x -q -F`
for the same reason.
- Kamaji's `KamajiControlPlane` is a CRD, and Helm applies
JSON-merge-patch to CRDs. That patch does not remove array-typed
fields the new manifest simply omits — it leaves the live array
untouched. Toggling `oidc.mode: System` → `None` therefore left
the tenant kube-apiserver with a dangling `--authentication-config`
flag pointing at a Secret the chart had already reaped, and the
apiserver would refuse to start on the next kubelet restart.
Fix: always render `extraArgs` and `extraVolumeMounts` on the
KamajiControlPlane, emitting `[]` on the off-path so the patch
REPLACES the stale array. Verified end-to-end on dev3.
Adds two helm-unittest cases: the mode=None regression, and an
adjustment of the baseline default-values test.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Three items from the gemini-code-assist and coderabbitai passes that
survived the Phase-1 redesign:
- `oidc-rbac-job.yaml` (gemini HIGH): pre-delete cleanup uses
`kubectl --request-timeout=30s` in addition to `--timeout=30s`
on the delete verb, so an unreachable tenant apiserver cannot
stall the pre-delete hook on the HTTP round-trip itself. The
`|| echo WARNING` fallback still keeps a truly unreachable
cluster from blocking `helm uninstall`.
- `oidc-keycloak.yaml` + `_helpers.tpl` (gemini LOW): switch
`_cluster.{oidc-enabled,root-host}` reads to `dig` with an empty
map default so a missing `_cluster` key nil-safely emits an
empty string and hits the platform-OIDC guard instead of a
template panic on `.Values._cluster`.
- `hack/e2e-apps/kubernetes-oidc-{system,customconfig}.bats`
(coderabbit MAJOR): add an existence-poll `timeout ... until
kubectl get secret ... ; do sleep ...; done` before the
`kubectl wait secret --for=jsonpath` line. Without it a runner
that reaches the wait before the HR has rendered the Secret
gets an immediate `NotFound` and the test flakes.
Adds one helm-unittest case covering the nil-safe `_cluster` path.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Self-review fix caught by deploying to dev3: the EDP Keycloak operator
CRD v1 does not declare `standardFlow` (the canonical field is
`standardFlowEnabled`, default true), and `webUrl` is meaningful only
for clients with a real web UI (not for the kubectl oidc-login flow
which is keyed only by `redirectUris`).
Both lines violated the CRD's openAPIV3Schema and Helm refused the
install with `field not declared in schema`. Dropping the empty
`webUrl` and relying on `standardFlowEnabled`'s default (true) keeps
the manifest minimal and CRD-clean. helm-unittest did not catch this
because it does not validate templates against live CRD schemas.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Self-review fix: the Role granted `["get", "patch"]` on the OIDC
kubeconfig Secret, but the bootstrap Job script never issues `kubectl
get` against the management apiserver — it only `kubectl patch`es the
pre-rendered Secret. The tenant-side `kubectl get clusterrolebindings`
uses the mounted admin kubeconfig, which is a separate trust path.
Tightens the SA's permission surface to exactly what the script uses.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Self-review fixes for two factual errors:
- The "How a user logs in" example named the Secret
`prod-oidc-kubeconfig`, but the cozystack-api prefixes the
cluster name with `kubernetes-` when materialising the
`HelmRelease`, so the actual Secret is
`kubernetes-prod-oidc-kubeconfig`. The NOTES.txt template was
already correct; only the doc example was off.
- The CRB hash formula was described as `<release>:<email>` but
the code uses `<release>-<email>` as the SHA input.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Self-review fix: the design proposal explicitly lists TWO bats files:
`kubernetes-oidc-system.bats` and `kubernetes-oidc-customconfig.bats`.
The first revision shipped only one combined file; this commit splits
them.
The CustomConfig bats covers what System cannot: the
operator-supplied AuthenticationConfiguration is plumbed onto the
KamajiControlPlane, the resulting Secret carries the BYO issuer (and
emphatically NOT a `cozy` issuer), nothing lands in the `cozy` realm,
and no OIDC kubeconfig Secret is pre-rendered (the operator
distributes their own).
The System bats header is updated to call out the deferred parts
(in-tenant CRBs / view-vs-admin RBAC behavior / browser-flow login)
that need a full Kamaji bringup, which is already covered for the
cluster path by `kubernetes-latest.bats`.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Self-review fix: the KeycloakClientScope rendered for the per-cluster
audience used `included.custom.audience` (raw string), but the
project convention — set in
`packages/system/keycloak-configure/templates/configure-kk.yaml` for
the legacy global `kubernetes-client` scope — is
`included.client.audience` (refers to a clientId in the same realm).
Both keys make Keycloak emit the audience claim with the same value
in our setup, but the `client.audience` form is the idiomatic one
when the audience IS a registered client in the realm.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Self-review fix: the design proposal specifies
`name: authentication-config` and
`mountPath: /etc/kubernetes/authentication-config`, but the previous
revision had renamed both to `cozy-oidc-authn-config` and
`/etc/kubernetes/cozy-oidc` to dodge a collision with the volume
name used in `apiserver_authentication_test.yaml`. That test was
demonstrating the GENERIC user-supplied `extra*` passthrough — not
the OIDC use case — so rename the test fixture instead and restore
the design-spec names on the chart side.
The reservation guard against user-supplied volumes reusing
`authentication-config` only fires when `spec.oidc.mode` is not
None, so clusters with OIDC off retain the previous freedom to name
their own volumes anything.
Renames in `apiserver_authentication_test.yaml`: the user-supplied
volume name `authentication-config` becomes `tenant-byo-mount`; the
Secret name `tenant-authentication-config` and the apiserver flag
`--authentication-config=` are untouched.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Self-review fixes for four linked lifecycle issues:
- The post-install Job created `<release>-oidc-kubeconfig` via
`kubectl apply` from inside the script; the resulting Secret had
no Helm release labels and was orphaned on `helm uninstall`. Now
a release-owned placeholder Secret is rendered up front in
`oidc-kubeconfig-secret.yaml` and the Job patches its `data`
field at runtime (the tenant CA still has to come from the
Kamaji-issued admin kubeconfig, which appears asynchronously).
- The ServiceAccount/Role/RoleBinding for the bootstrap Job were
hook resources with `helm.sh/hook-delete-policy: before-hook-creation`,
so they leaked on `helm uninstall`. They are now regular release
resources (no hook annotations) and are reaped by `helm uninstall`
alongside everything else. Both the post-install bootstrap Job
and the pre-delete cleanup Job find a working SA.
- The previous design tried to add `hook-succeeded` to the SA, but
that would delete the SA right after the bootstrap hook
succeeded — leaving the pre-delete cleanup Job with no SA to
schedule. Release-owned resources sidestep this race entirely.
- The Role had `verbs: ["create"]` on `secrets` without
`resourceNames` (Kubernetes RBAC cannot scope `create` by name).
With the placeholder Secret pre-rendered the Job only needs
`get`/`patch` on the specific name — the broad `create` rule is
removed.
Also tightens the script: `set -euo pipefail` (was `set -eu`, which
silently swallowed `kubectl get` failures inside command
substitutions) and uses `kubectl patch --type=merge` against the
existing Secret instead of `kubectl create --dry-run | kubectl apply`.
Adds three helm-unittest cases for the placeholder Secret across all
three modes.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Add the operator-facing guide for the new selector — what the three
modes mean, what the chart provisions, how a user logs in, what the
failure modes are, and what is intentionally out of scope (per-tenant
realms, federation, cross-cluster SSO). The architectural rationale
points back to cozystack/community#24.
Update the chart NOTES.txt to print the OIDC kubeconfig retrieval
snippet, the per-cluster client/issuer values, and the listed
`users[]` roster — only when OIDC is on, otherwise the previous
admin-kubeconfig hint is unchanged.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Validate on a live cluster that:
- the new `spec.oidc.*` fields are admitted by the Kubernetes CRD;
- cozystack-api accepts the updated schema and renders a HelmRelease;
- the KamajiControlPlane gains `--authentication-config` and the
chart-owned `cozy-oidc-authn-config` mount and volume;
- the AuthenticationConfiguration Secret carries the cozy realm
issuer URL and the per-cluster audience;
- per-cluster `KeycloakClient` + audience `KeycloakClientScope`
land in the cozy realm (with the EDP operator present).
The full end-to-end RBAC flow (Job → CRBs inside the tenant cluster)
needs a Kamaji bringup, which kubernetes-{latest,previous}.bats
already cover for the cluster path; the OIDC browser flow is
explicitly out of scope per the design proposal. This file covers
only what the render-side regression budget can pay for.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Cover the three branches of the new selector API:
- `mode: None` (default) — every OIDC template is empty, the
KamajiControlPlane has no extra args and the standard two talos
volumes, the bootstrap Job is not rendered.
- `mode: System` — per-cluster `KeycloakClient` + audience scope
in the `cozy` realm, namespace-prefixed clientId, PKCE attribute
on the client, soft-render when the EDP Keycloak operator CRDs
are absent, the AuthenticationConfiguration Secret pinned to the
cozy issuer, the chart-owned `--authentication-config` flag and
`cozy-oidc-authn-config` mount injected into the KCP, the
bootstrap Job carrying `policy.cozystack.io/allow-to-apiserver`,
and one ClusterRoleBinding per `users[]` entry with the right
ClusterRole (`cluster-admin` vs `view`).
- `mode: CustomConfig` — inline config written verbatim; secretRef
path mounted on the KCP without a chart-owned Secret; no
Keycloak objects emitted; mutual-exclusion and missing-source
guards both hard-fail.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Add `kubernetes-<name>-oidc-kubeconfig` to the Kubernetes
ApplicationDefinition's `spec.secrets.include[].resourceNames` so
the dashboard surfaces the OIDC kubeconfig to tenant viewers
alongside the existing admin kubeconfig.
The Secret is produced by the post-install,post-upgrade bootstrap
Job in `apps/kubernetes` when `spec.oidc.mode: System` is set.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Add a post-install,post-upgrade Helm-hook Job that reconciles the
operator-supplied `users[]` list into the tenant cluster:
- For every entry, apply a ClusterRoleBinding labelled by the
release (managed-by=cozystack-oidc, instance=<release>).
`role: admin` maps to `cluster-admin`; `role: view` maps to
`view`. The subject is the literal `email` value, matched
against the issuer's `preferred_username` claim.
- Prune any orphan CRBs with the same labels — removing a user
from `users[]` revokes their kubectl access on the next loop.
For `mode: System` the same Job also writes
`<release>-oidc-kubeconfig` on the management cluster: a ready
kubeconfig with the per-cluster apiserver endpoint, the tenant CA
extracted from the Kamaji-issued admin kubeconfig, and a
`kubectl oidc-login` exec block keyed to the per-cluster client.
A pre-delete cleanup Job removes the labelled CRBs before the
control plane is torn down (best-effort: the CRBs disappear with
the tenant cluster anyway).
Pod is labelled `policy.cozystack.io/allow-to-apiserver: "true"`
so Cilium lets it reach the konnectivity tunnel.
ServiceAccount/Role/RoleBinding are minimum-privilege — only the
specific admin-kubeconfig and OIDC kubeconfig Secrets are listed.
Renders nothing for `mode: None`.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Render a per-release Secret carrying the structured kube-apiserver
`AuthenticationConfiguration` (apiserver.config.k8s.io/v1beta1) and
mount it into the KamajiControlPlane.
For `mode: System` the Secret is chart-owned: one JWT entry pointing
at the `cozy` realm issuer with the per-cluster audience, plus the
standard `preferred_username`/`groups` claim mappings. For `mode:
CustomConfig` the chart either writes the operator-supplied inline
config into a Secret, or — when `secretRef.name` is set — mounts an
existing operator-owned Secret directly (mutually exclusive; the
chart fails the render if neither or both are set).
The `--authentication-config=` flag, mount, and volume on the
KamajiControlPlane are kept orthogonal to the user-supplied
`controlPlane.apiServer.extra*` knobs: the OIDC additions are
appended, and the chart-managed volume name `cozy-oidc-authn-config`
is reserved (only when `mode` is not None) so user-supplied volumes
cannot collide.
Switches from the legacy `--oidc-*` flag form to the file form so
multiple issuers, inline private-CA PEM, and future Phase-2 issuers
extend the same Secret without rewiring the chart.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
For `oidc.mode: System`, emit a per-cluster public `KeycloakClient`
plus a per-cluster `KeycloakClientScope` inside the platform `cozy`
realm. The scope carries an `oidc-audience-mapper` that pins
`id_token.aud` to the per-cluster clientId, so a token minted for
cluster A is rejected by cluster B's apiserver.
The clientId is namespace-prefixed (`<ns>-<release>`) to stay unique
across the realm — two clusters with the same name in different
tenants would otherwise collide.
The client is `public: true`, redirects locked to localhost:8000 /
localhost:18000 (the kubelogin/oidc-login defaults), with PKCE
required via the standard Keycloak attribute. `defaultClientScopes`
includes both `groups` and the per-cluster audience scope.
A render-time guard fails fast when `oidc.mode: System` is set but
`_cluster.oidc-enabled` is not `"true"` — the Keycloak operator CRDs
are otherwise absent and the chart would silently no-op.
Nothing is rendered for `mode: CustomConfig` or `mode: None`.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Introduce a flat selector — `mode: System | CustomConfig | None` —
plus a `users[]` list and a `customConfig` block for tenant-supplied
AuthenticationConfiguration. Default `None` preserves today's
behavior.
The `System` mode trusts the platform `cozy` realm via a per-cluster
public client; `CustomConfig` accepts an inline `AuthenticationConfiguration`
or a Secret reference (mutually exclusive). The `users[]` list drives
per-user ClusterRoleBindings inside the tenant cluster, with two
roles: `admin` and `view`.
Regenerates deepcopy, JSON schema, README, and the kubernetes-rd
cozyrds embedded schema.
Implements Phase 1 API of the per-tenant OIDC design proposal
(cozystack/community#24).
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
## What this PR does
Reverts #3006, restoring upstream's persistent EFI/TPM firmware state
for the `windows.11` / `windows.2k22` / `windows.2k25` preferences (and
their `.virtio` variants).
#3006 dropped persistent EFI/TPM to "unblock live-migration", on the
assumption that an RWO Filesystem backend-storage PVC pins the VM to its
node. That assumption was outdated: KubeVirt has live-migrated
RWO-Filesystem backend storage since v1.4 (kubevirt/kubevirt#12629) — on
migration it creates a fresh target state PVC and copies the small state
blob. So persistent-EFI/TPM VMs on the default `replicated` storage
live-migrate fine; the regression described in #3005 was a misdiagnosis.
Verified on a KubeVirt v1.8.2 cluster: a persistent-EFI VM with an RWO
Filesystem backend PVC reports `LiveMigratable=True`, and the
copy-on-target backend PVC is created when a migration starts. KubeVirt
maintainers confirmed the same upstream.
This restores persistent firmware state so in-guest Microsoft UEFI CA /
Secure Boot enrollment survives reboots and migrations.
### Release note
```release-note
fix(kubevirt-instancetypes): restore persistent EFI/TPM state for Windows preferences — RWO-Filesystem backend storage already live-migrates (KubeVirt v1.4+), so persistence no longer needs to be stripped
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Updated Windows instance type preferences to keep TPM persistence
enabled, improving compatibility and reducing storage-related issues.
* Ensured firmware and TPM persistence settings are preserved during
instance type preference generation for more reliable outcomes.
* **Chores**
* Streamlined the preference generation step so instance type templates
are produced consistently without removing persistence configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The PR Auto-Label workflow triggered on pull_request_target 'edited',
which fires on every PR title *and body* edit. Long-lived bot PRs such
as Renovate repeatedly edit their description on schedule, causing the
label job to re-run dozens of times against an unchanged commit (e.g.
~48 runs on #2998, all on the same head SHA).
Drop 'edited' so the job only runs on opened/reopened/synchronize.
Labels derived from the PR title are re-applied on the next push or on
reopen. Title-only edits between pushes no longer trigger a relabel,
which is an acceptable trade for eliminating the churn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
nightly.yaml publishes each build under 0.0.0-nightly.<date>.<run-id>.<attempt>
(the run-id + attempt suffix makes every nightly tag immutable), but the docs
still showed the old date-only 0.0.0-nightly.<YYYYMMDD> form in the mirror-stage
description and both the helm-install and oras-pull pin examples. Update them to
the full form; the floating `nightly` example is unchanged.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Two issues flagged in review on promote-rc.yaml:
- The "Validate rc is promotable" github-script called core.setFailed()
but kept running: a missing rc release then hit `rcRel.assets` and a
missing disk asset hit `disk.id`, throwing a TypeError that masked the
intended failure message. Return immediately after each setFailed so the
clean diagnostic is what surfaces.
- "Create draft stable release and upload assets" ran `make upload_assets`
without COZYSTACK_VERSION. upload_assets depends on `manifests`, which
re-renders the operator manifests; with no version it blanks the
platformVersion, silently undoing the stable stamp the Restamp step just
applied. Pass COZYSTACK_VERSION=${STABLE_VERSION} so the re-render
re-stamps the stable version.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
"What CI does on a tag push" only described one of the four triggers;
promote-rc.yaml uses manual dispatch and pull-requests-release.yaml fires
on PR merge. Rename to "What CI does during the release process" so
maintainers find the right entry point regardless of which workflow they
are debugging.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on docs/release.md: the Regular and
Patch sections still told maintainers to push vX.Y.Z to start the build, while
the rest of the doc said the stable tag is created write-once at the merge
commit and stable comes only from rc promotion (with a 'can also push directly'
escape hatch). Rewrite both sections to the single promotion model — push an rc,
promote it via promote-rc.yaml, merge the promote PR, finalize creates the
write-once tag — drop the direct-push escape hatch, and add promote-rc.yaml to
the workflow list.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on .github/workflows/tags.yaml: the
single `skip` output meant 'a release already exists' but was reused to gate
update-website-docs, so a promoted stable tag (whose draft promote-rc already
created) skipped the website-docs job, not just the rebuild. Rename the output
to release_exists to reflect its real meaning and drop it from the
update-website-docs gate so a promotion still publishes docs (the rebuild steps
inside prepare-release stay gated on it).
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on pkg/version/version_test.go: the
env-unset case only skipped t.Setenv, so a COZYSTACK_VERSION inherited from the
test runner would make it skip the baked-fallback path it is meant to cover.
Unset the variable (restoring it on cleanup) for that case.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on hack/promote-retag.sh: the loop
copied each rc digest onto ${repo}:${STABLE} unconditionally, so a partial
prior run or a manual push could mutate already-released stable bytes before
the GitHub release/tag guards catch it. Inspect the destination first — no-op
when it already points at the rc digest (idempotent re-run), fail when it
points elsewhere. :latest stays mutable.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on
.github/workflows/pull-requests-release.yaml: the fast-forward catch block
treated every updateRef failure as a recoverable warning, so an auth, API, or
malformed-ref error would silently leave the maintenance branch stale. Warn
only on the 422 non-fast-forward rejection and fail the job otherwise.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on .github/workflows/promote-rc.yaml:
the rc staging checkout left the app token in .git/config even though the
Prepare stable branch step re-injects it via 'git remote set-url'. Set
persist-credentials: false to drop the redundant exposure.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on .github/workflows/promote-rc.yaml:
the validate step only checked for an existing stable release, so a stray
refs/tags/vX.Y.Z (partial run or manual push) would slip through and fail later
in finalize — after promote-retag had already retagged images. Also fail early
when the stable git tag already exists.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on .github/workflows/promote-rc.yaml:
actions/create-github-app-token was used at the floating @v1 and without a
repositories restriction, so the minted token carried owner-wide access. Pin it
to the SHA the rest of the repo uses and scope it to this repository.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on .github/workflows/nightly.yaml:
actions/checkout writes the token into .git/config by default, and these jobs
copy the checkout into /tmp/$SANDBOX_NAME before running repo targets. Set
persist-credentials: false on all three source checkouts so the token is not
carried into the sandbox copies.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on .github/workflows/nightly.yaml:
the version was 0.0.0-nightly.<date>, so a same-day re-dispatch or re-run
republished every image, chart and artifact under the same tag — the dated
nightly was not write-once. Append the run id and attempt so each build gets a
distinct, immutable tag, and widen retention.yaml's selector regex to match the
extra numeric components (still excluding the floating :nightly and any release
tag).
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on .github/workflows/retention.yaml:
gh api --paginate with --jq runs the filter once per page and emits one JSON
array per page, so total/drop_ids miscount once a package's versions span more
than one page. Pipe the raw pages through 'jq -s add' to merge them into a
single array before counting and slicing.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from coderabbitai on .github/workflows/nightly.yaml:
the resolve step read cozystack-packages:main and the pull step re-fetched
the mutable :main tag, so a concurrent build-main push could check out commit
A while mirroring tree B. Resolve the :main digest once, derive the revision
sha from it, and pull the baked tree by that same digest.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Match the repo-wide SHA pin main applies to every github-script use, so
the new promote-rc workflow doesn't regress the Scorecard pinned-dependencies
check.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The console bakes VITE_APP_VERSION into its bundle at build time, so a
promote-by-retag image (which is retagged, never rebuilt) would keep
reporting the rc version in the UI after promotion to stable.
Inject the version at deploy time instead: the chart derives it from the
console image tag (build-stamped vX.Y.Z, rewritten rc->stable on promotion
by promote-rc.yaml) and writes it into the existing console config.json
ConfigMap, which the SPA already fetches. App.tsx now prefers
config.version and falls back to the baked VITE_APP_VERSION when absent
(dev / non-version tags like :latest).
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Replace the nightly-rc-tag description with the new model: a nightly is
an installable copy of main on GHCR (mirror OCIR->GHCR by digest +
re-published packages artifact, cozy-installer chart and nocloud disk),
validated by the full e2e suite, with no GitHub release and no Go-module
tag. Document the helm install path and the dated/floating tag scheme,
and update the Tag-immutability section and workflow link list.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Nightlies are now GHCR OCI tags (0.0.0-nightly.<date>), not git tags or
GitHub releases, so:
- retention.yaml now prunes old GHCR container-package versions via the
Packages API instead of git tags/releases. Scoped to versions whose
tags are ALL 0.0.0-nightly.<YYYYMMDD>, so the floating :nightly tag,
release tags (vX.Y.Z, latest, rc, main) and untagged versions are
never touched. Keeps newest 14 per package; dry-run by default.
- nightly-rc.yaml is removed: it cut nightly *git tags* that drove a
release/e2e path we no longer use. The nightly.yaml workflow replaces
it with the mirror + full-e2e flow.
actionlint clean.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Scheduled nightly that publishes an installable nightly of main without
rebuilding it, then runs the full e2e suite against the published bits:
- mirror: resolves the exact commit cozystack-packages:main was built
from, runs nightly-mirror.sh to copy every cozystack-owned component
image OCIR->GHCR by digest, re-publishes the rewritten packages
artifact and the cozy-installer chart to GHCR (0.0.0-nightly.<date>
+ floating :nightly).
- build-disk: assembles the Talos nocloud disk from the upstream imager
(no cozystack-image dependency, no rebuild) and publishes it as a GHCR
OCI artifact (cozystack-nocloud) via oras, so a nightly is installable
on real hardware, not just e2e.
- e2e: stages the GHCR install closure (rewritten tree + chart pinned to
the GHCR packages artifact + the disk) and runs the FULL app suite (no
Test Impact Analysis - a nightly has no diff to scope against).
No GitHub release and no api/apps/v1alpha1 Go-module tag are created.
Gated by the NIGHTLY_ENABLED repo variable. actionlint clean.
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>