## What this PR does
Pins the remaining floating version tags in the `build-main` and
`ui-test` workflows to immutable commit SHAs, keeping the original tag
in a trailing comment.
This is a supply-chain hardening change: a mutable tag such as `@v4` can
be force-repointed (by a compromised or malicious upstream) to arbitrary
code, whereas a commit SHA is immutable. Most workflows in this
repository were already SHA-pinned — this closes the last gap so that
every third-party action is referenced by digest.
Actions pinned in this PR:
- `build-main.yaml`: `actions/checkout`, `docker/login-action`,
`docker/setup-buildx-action`
- `ui-test.yaml`: `actions/checkout`, `pnpm/action-setup`,
`actions/setup-node`
Each SHA is the exact commit the tag currently resolves to, so there is
**no functional change**. The trailing `# vX` comment records the
original tag; Renovate's `github-actions` manager understands this
format and can keep the pins updated automatically.
> Note: `.github/renovate.json` currently sets `enabledManagers:
["gomod"]`, so the `github-actions` manager is not active yet. For
Renovate to auto-maintain these (and the other already-pinned) action
digests, that manager needs to be enabled — suggested as a separate
follow-up.
### Screenshots
N/A — CI-only change, no user-facing or UI impact.
### Release note
```release-note
NONE
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Updated CI workflows to use fixed, version-pinned action releases for
build and UI test steps.
* This helps make automated builds and tests more consistent and
reliable over time.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
spec.loadBalancerClass is immutable once a Service is type LoadBalancer, so
setting, changing, or clearing publishing.loadBalancerClass on a cluster whose
host ingress Service already exists would make Flux wedge on an API-rejected
patch instead of migrating. Fail the render early with the exact recreate command
whenever an existing type: LoadBalancer Service's class differs from the requested
value; a fresh install (lookup nil) or a ClusterIP Service transitioning to
LoadBalancer is left untouched. Note the caveat in the values comment and cover
the guard with helm-unittest.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
publishing.loadBalancerClass is a host-publishing setting, but its
_cluster.load-balancer-class block was emitted on any ingress taking the
LoadBalancer branch — including tenant ingresses — unlike expose-external-ips
and proxy-protocol, which gate on the host namespace. A tenant ingress would
then be claimed by an LB controller the tenant never selected. Gate it the
same way, and add helm-unittest coverage for the host/tenant split and the
platform to _cluster wiring.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Remove the network.cozystack.io API group (ExposureClass + ServiceExposure)
and its reconciler. Managed-application charts own their Service, so native
type: LoadBalancer + loadBalancerClass + an admin-provisioned pool covers
external exposure without a dedicated API group and controller. The host
ingress -- the only consumer -- keeps rendering type: LoadBalancer and gains
an optional publishing.loadBalancerClass to pick the LB controller; the
externalIPs (node-IP) default path is unchanged.
Rationale in cozystack#3164.
Assisted-By: Claude
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Pin the remaining floating version tags in the build-main and ui-test
workflows to immutable commit SHAs, preserving the original tag in a
trailing comment so Renovate can keep the pins up to date.
Supply-chain hardening against tag hijacking/mutation; no functional
change — each SHA is the commit the tag currently resolves to.
Assisted-By: Claude
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Review feedback (non-blocking):
- .pre-commit-config.yaml pinned zizmor at v1.22.0 while the CI gate installs
1.26.1; the zizmorcore/zizmor-pre-commit rev is the zizmor version 1:1, so the
gap meant local and CI runs could differ. Align the hook to v1.26.1.
- ui-test.yaml's checkout did not set persist-credentials: false, unlike the
other two workflows hardened here (build-main.yaml, zizmor.yml). Set it so the
token is not left in .git/config for the later pnpm install step to read.
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Aligns the security documentation with controls that are already in
place, and adds the two coordinated-disclosure items flagged during the
Incubation due-diligence review (cncf/toc#1916). Documentation only — no
code or workflow changes.
## What
- **`docs/security/self-assessment.md`**
- DCO: corrected to *enforced by an automated DCO check on every PR* (it
already is).
- Private intake: **GitHub Private Vulnerability Reporting** (enabled),
with a `MAINTAINERS.md` fallback.
- OpenSSF Best Practices: **passing badge achieved 2026-04-01**, working
toward silver.
- Advisory history and incident-response paragraphs reworded to match
the real (GHSA-based) process.
- **`SECURITY.md`**
- Route reports to Private Vulnerability Reporting first,
`MAINTAINERS.md` as fallback (no longer points at `CODEOWNERS`).
- New *Disclosure timeline*: a **90-day** coordinated-disclosure window
plus a CVSS v3.1 remediation-target table.
- **`.github/CODEOWNERS`**
- Header documents that this file encodes the area-scoped **Reviewer**
role from the Contributor Ladder, intentionally distinct from the
`MAINTAINERS.md` governance roster, and that security reporting is not
routed through it.
- **`docs/security/incident-response.md`** (new)
- Public incident runbook: roles, an 8-step lifecycle, and
severity/timeline references — each step linked to the existing
mechanism it relies on.
## Why
The self-assessment was written conservatively; several items it listed
as gaps are in fact already implemented (DCO enforcement, private
vulnerability reporting, OpenSSF passing badge). This corrects those
statements and adds the committed disclosure window and a public
incident-response summary, so the self-assessment accurately reflects
the project's controls for the DD review.
Bring the security documentation in line with controls that are already
in place, and add the two coordinated-disclosure items flagged during the
incubation due-diligence review.
- self-assessment: DCO is enforced by an automated check; private intake
is GitHub Private Vulnerability Reporting; OpenSSF passing badge achieved
2026-04-01 (working toward silver); GHSA/PVR framing for advisory history;
incident-response now summarized publicly.
- SECURITY.md: route reports to Private Vulnerability Reporting with a
MAINTAINERS.md fallback; add a 90-day coordinated-disclosure window and a
CVSS-based remediation-target table.
- CODEOWNERS: document that it encodes the area-scoped Reviewer role, which
is intentionally distinct from the MAINTAINERS.md governance roster, and
that security reporting is not routed through this file.
- Add docs/security/incident-response.md as the public incident runbook.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: tym83 <6355522@gmail.com>
## Summary
Adds the CNCF security self-assessment for Cozystack, required for the
Incubation due-diligence review (cncf/toc#1916). The document was
drafted
by the maintainers and previously lived in a gist; this lands it in-tree
next to the existing design-level threat model.
## What
- `docs/security/self-assessment.md` — trust boundaries (T0–T4 actors
and
actions), security functions & features (critical / security-relevant),
project compliance, secure development practices, security issue
resolution, and an appendix with adopters, case studies, and known gaps.
## Why
The DD triage on the Incubation application flagged the Security
Self-Assessment as a required, blocking item. Keeping it in-repo —
rather
than a personal gist — makes it discoverable and maintainable alongside
`docs/security/threat-model.md`.
Add the CNCF security self-assessment required for the Incubation due
diligence review, alongside the existing design-level threat model. It
documents the trust boundaries, actors/actions, security functions,
secure development practices, and known gaps.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: tym83 <6355522@gmail.com>
Native Go fuzzing over the flag parsers in config.go, asserting the
invariants that matter rather than just no-panic:
- ParseShardIndex round-trips with ShardName (accepted input is the canonical
name of the returned index; index is non-negative).
- ShardName round-trips back through ParseShardIndex for every valid index.
- ParseShardCount: a non-auto success yields count >= 1; auto yields 0.
- ParsePinnedTenants: every entry in a success has a non-empty key mapped to a
valid shard name.
Adds an OSS-Fuzz-detectable fuzzing signal for the OpenSSF Scorecard Fuzzing
check while covering parsers that sit on the sharding hot path.
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build-main.yaml and ui-test.yaml (the latter added by the in-tree console
move) landed without following the repo's workflow conventions: no top-level
permissions block and actions referenced by moving tags. Together they zero out
two OpenSSF Scorecard checks the rest of the tree already satisfies:
- Token-Permissions: scored on the weakest workflow, so one workflow with an
undeclared (default-write) top-level token drops the whole check to 0.
- Pinned-Dependencies: the six tag-pinned action refs count as unpinned.
Fix both files (top-level read-only token; job-level packages:write kept where
needed; all action refs pinned to the SHA already used elsewhere in the tree)
and add a durable gate so it cannot recur:
- .github/zizmor.yml enables the two Scorecard-relevant audits (unpinned-uses,
excessive-permissions), clean tree-wide as of this commit. The remaining
audits carry pre-existing debt and are disabled with a TODO to burn down and
re-enable one at a time.
- .pre-commit-config.yaml runs zizmor (offline) on changed workflows.
- .github/workflows/zizmor.yml runs the same audit tree-wide on every PR that
touches a workflow, failing on any reintroduced regression.
Also wire scorecard.yml to an optional SCORECARD_TOKEN secret: the default
GITHUB_TOKEN cannot read branch-protection settings, so the Branch-Protection
check is scored on partial data. The token is a no-op until the secret is added.
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## What this PR does
Closes two coverage gaps found while shadow-observing the
API-owner-review gate (#3167) against real PRs in the first hours after
merge:
1. **CRD discovery was gated on directory name** (`crds/`,
`definitions/`, `manifests/`), so a CRD shipped inside a chart's
`templates/` dir — a valid, existing convention when the CRD is wrapped
in a `{{- if }}` guard — was invisible to the gate. This was not
hypothetical: `#3149` (cozyplane) adds four brand-new CRDs (`VPC`,
`VPCBinding`, `VPCPeering`, `Port`) exactly this way, and none of them
tripped a review requirement.
2. **Groups served only by an external aggregated apiserver had no
representation at all.** The same `#3149` also registers
`sdn.cozystack.io` via a `kind: APIService` pointing at a prebuilt image
built outside this repository — no CRD, no vendored Go types, nothing to
diff.
### Fix
- CRD/APIService discovery is now content-based: every `.yaml`/`.yml`
file under `packages/` and `internal/` is a candidate, and whether it's
kept depends on whether it parses into the right `kind` with a
first-party group — not which directory it sits in.
- Both parsers now tolerate a Helm directive line (`{{- if }}`, `{{- end
}}`, a comment block) sharing a `---`-delimited document with
otherwise-valid YAML, rather than dropping the whole document. This
matters because the closing `{{- end }}` of a wrapping conditional has
no `---` before it, so it lands in the same document as the manifest it
closes.
- Added a fourth resource source, `SourceAPIService`, for `kind:
APIService` manifests registering a first-party group. Like the existing
static Go-backed groups, these carry no checked-in schema, so they
participate only in new-group / new-resource / removal detection — never
a fabricated breaking-change diff against a schema this repo can't see.
Verified against the real `feat/cozyplane` branch: the gate now reports
all four new CRDs, plus the removal of the old
`securitygroups`/`APIService` registration for the same group — a full
API-surface migration that was previously invisible end to end.
### Release note
```release-note
fix(ci): the API-owner-review gate now discovers CRDs and APIService registrations by content instead of directory name, closing a gap where a CRD or an externally-served aggregated API group could ship without triggering the required review.
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Discovery now includes APIService registrations, enabling detection of
groups/versions exposed only via aggregated APIService manifests
(including merging versions from multiple templates).
* **Bug Fixes**
* Improved YAML discovery to tolerate mixed/templated multi-document
streams: non-CRD/non-APIService documents no longer stop discovery, and
embedded template directives are handled more reliably.
* File processing is more resilient during snapshot loading (includes a
safety cap on YAML size).
* **Tests**
* Added coverage for APIService discovery, templated neighbor documents,
and correct exclusion of third-party manifests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What this PR does
Adds `docs/security/threat-model.md`, a design-level threat model for
Cozystack. It documents the actors, the actions that cross trust
boundaries, and the security goals and non-goals, organized by trust
boundary (T0–T4) following the CNCF TAG-Security Actors/Actions model.
It records the usage and multitenancy model (managed-services backend vs
direct dashboard/API access; combined hard multitenancy for tenant VMs
and Kubernetes clusters, soft multitenancy for managed data services),
the primary trust-boundary invariant (tenant RBAC grants no HelmRelease
or arbitrary core writes; the aggregated API server fixes the chart
reference server-side), and the two admission enforcement points (core
kube-apiserver vs `cozystack-api`). Hostname integrity covers both the
Gateway API path and the default legacy-Ingress path
(`cozystack-ingress-hostname-policy`, #3200). Every architectural claim
cites a source file so the model can be re-verified as the code evolves.
This is the in-repo companion to the project's CNCF TAG-Security
self-assessment, which references it.
### Screenshots
N/A — documentation only.
### Release note
```release-note
docs(security): add a design-level threat model (docs/security/threat-model.md)
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Added a security threat model for the platform.
* Clarified system boundaries, trust tiers, and the main actors
involved.
* Documented key security goals, non-goals, and important enforcement
points.
* Described expected behavior for tenant actions, hostname handling, and
cross-boundary access.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Document the platform's actors, boundary-crossing actions, and security
goals/non-goals, organized by trust boundary (T0-T4) per the CNCF
TAG-Security Actors/Actions model. Records the primary trust-boundary
invariant (tenant RBAC grants no HelmRelease/core writes; the aggregated
API server fixes the chart reference server-side) and the two admission
enforcement points (core kube-apiserver vs cozystack-api). Every claim
cites a source file so the model can be re-verified as the code evolves.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->
## What this PR does
### Screenshots
<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->
### Release note
<!-- Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->
```release-note
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added support for the newer etcd snapshot workflow
(BackupClass/BackupJob/RestoreJob) with in-place restore.
* Updated etcd chart/operator packaging and migration flow for the
v1alpha2 operator release.
* **Bug Fixes**
* Switched readiness/condition handling to **Available**.
* Improved waits to cover all etcd member pods during restore and
scenario steps.
* **Documentation**
* Refreshed etcd backup/restore docs and example scenarios to match the
v1alpha2 APIs and parameters.
* **Tests**
* Updated e2e coverage to validate v1alpha2 operator runtime contracts
and replaced legacy backup-block tests with a snapshot round-trip test.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Address non-blocking review feedback from lexfrei on
packages/system/dashboard/Makefile:34.
When COZYSTACK_VERSION is empty (a local build with no matching git tag) the
build-arg baked a bare `APP_VERSION=v` into the console bundle. Match the guard
the root Makefile already uses for platformVersion so an empty version yields an
empty APP_VERSION instead of "v". Dev-only cosmetics — the config.json path
overrides the displayed version at deploy time — but it removes the mismatch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review feedback from lexfrei on .github/workflows/promote-rc.yaml:200.
The "Prepare stable branch" step built the release-X.Y.Z branch with
`git checkout -B` + `git commit -s` (a fresh, timestamp-dependent SHA every run)
and pushed it with a plain non-force push. On a re-dispatch — retrying after a
later step failed, or promoting a newer rc to the same version — the new commit
is a sibling of the already-pushed one (same parent, different SHA), so the push
is rejected non-fast-forward and promotion wedges until the branch is deleted by
hand. That contradicts both the validate step (which tolerates a leftover draft
precisely so a newer rc can be re-promoted) and the workflow header's
"overwritable branch, nothing to wedge a later re-promotion".
Reuse the compare-before-force pattern from tags.yaml's Create release branch
step: skip when the remote already matches, force-push + log when the branch
genuinely moves. It is a mutable staging branch, so overwriting it is correct.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
- nightly.yaml: pass the registry tokens to `skopeo login` over stdin
(--password-stdin) instead of `-p` on argv, matching promote-rc/finalize and
keeping the token out of the process list.
- dashboard configmap: require digits-then-dot (^v?[0-9]+\.) when deriving the
displayed platform version from the console image tag. The old ^v?[0-9] let a
tagless, port-carrying ref (ghcr.io:443/.../console, whose last ":"-segment is
"443/.../console") pass as a version. Verified: v1.5.0 / 1.5.0 still surface;
the port ref and :latest no longer do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The tag workflow assumed the old hand-pushed-stable flow in several places that
break (or misbehave) under rc-promotion:
- Drop PUBLISH_FLOATING from the Build step. This job now only ever builds a
prerelease (a stable tag skips via its pre-existing draft, or is rejected —
below), so moving :latest here would track the newest rc, and an rc on an old
line would drag :latest backwards. :latest belongs to promotion (finalize).
- Publish rc releases. tags.yaml created the rc release as a draft and nothing
ever published it, so the community could not see the rc page or download its
nocloud-amd64.raw.xz — defeating the rc's stated purpose. Publish it
(prerelease:true, make_latest:false) once assets are uploaded; rc tags are
write-once, nothing to protect with draft status.
- Reject hand-pushed stable tags. A stable vX.Y.Z with no pre-existing draft is
a hand-push down the legacy full-rebuild path (full rebuild, moved :latest,
rogue release PR). Fail fast with a message pointing at promote-rc.yaml. This
replaces two now-unreachable steps that shared the same gate: the submodule
Go-module tag (moved to finalize in the previous commit) and "Create pull
request if not exists" (promote-rc owns PR creation).
- Gate generate-changelog and update-website-docs on stable (ref_name has no
'-'), so each rc no longer opens a changelog-vX.Y.Z-rc.N PR that never merges
and an "update managed apps for vX.Y.Z-rc.N" website PR.
- Paginate the release lookups (check_release, draft-reuse): a promote-created
draft can sit past the default first 30 and be missed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
branch-review on #3184 measured a real regression: switching CRD/
APIService discovery to content-based scanning made every file in
packages/+internal a candidate, and each was split into documents and
fully unmarshaled twice — once per parser — inflating a full snapshot
load from ~0.5s to ~3.4s.
Split each file's documents once and probe each document's kind once
before deciding which shape to try, so the common case (a document
that is neither a CRD nor an APIService — the overwhelming majority
of what the wider walk now sees) pays for at most one parse attempt
instead of two. Add a raw substring fast-reject, at both the
per-document and whole-file level, ahead of any YAML parsing: neither
kind this package cares about can appear without its own name
appearing as a literal substring first.
Also fixes two smaller gaps flagged in the same review: an untrusted
PR head checkout could point discovery at an arbitrarily large file
with no size guard (capped at 8 MiB), and a group's merged APIService
resource only recorded the first contributing file's path, dropping
the others from the report.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
Shadow-observation of the merged gate found a real miss: a CRD shipped
inside a chart's templates/ dir (guarded by a Helm {{- if }}) was
invisible because discovery was gated on directory name (crds/,
definitions/, manifests/), and a group served only by an external
aggregated apiserver (kind: APIService, no CRD, no vendored Go types)
had no representation at all.
Switch CRD/APIService discovery from directory-name to content
sniffing, and tolerate Helm directive lines sharing a document with
otherwise-valid YAML (the closing `{{- end }}` of a wrapping
conditional has no leading `---`, so it lands in the same document as
the manifest). Add a fourth source, SourceAPIService, reusing the
existing empty-schema mechanism so a first-party group's first
APIService appearance requires review without pretending to diff a
schema that lives outside this repository.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
Promotion mutated the registry at dispatch time — retagging ~37 images to
vX.Y.Z (+:latest), publishing the stable cozy-installer chart — before the
promote PR's e2e ran and before any merge. An abandoned promotion then left
stable-named bytes for a version that never shipped, and the (correct) image
write-once check wedged any later re-promotion to the same version.
Move every irreversible registry side effect into pull-requests-release.yaml
finalize, which runs only after the promote PR merged (= after its e2e passed),
making promotion atomic. promote-rc.yaml now only stages the release-X.Y.Z
branch, drafts the stable release, and opens the PR; an abandoned promotion
leaves nothing but an overwritable draft + branch.
Folded into the same restructure:
- Cut the api/apps/v1alpha1/vX.Y.Z Go-module tag in finalize, at the stable
tag's commit. It was gated in tags.yaml on `release_exists==false &&
is_stable`, unsatisfiable under promotion (the draft always pre-exists), so
promoted stables never got their submodule tag — a regression for Go
consumers. Finalize is where the stable tag is born, so it cuts the matching
submodule tag write-once (stable only).
- Gate :latest by max-semver. promote-retag.sh now moves :latest only when
MOVE_LATEST=1 (default off); finalize passes it the same make_latest decision
the release gets, so a patch on an older line moves neither the release's
`latest` nor the images' :latest. Added MOVE_LATEST bats coverage.
- Stamp platformVersion into the published stable chart's default values
(uncommitted, at package time) so `helm --install --version X.Y.Z` reports the
stable version in telemetry instead of the rc version baked into the operator
image. Not committed to the branch — that would leak platformVersion=vX.Y.Z
onto main and mis-stamp the next rc.
- promote-rc: serialize all promotions (static concurrency group, not per-rc);
label the PR full-e2e so the full suite runs; scope the rc->stable sed to the
depth-2 values.yaml glob plus the kubernetes .tag files (the build's own write
set); tolerate a leftover draft so re-promotion is not wedged.
- finalize: assert the PR author is cozystack-ci[bot], and run on an ephemeral
oracle-vm runner (needs skopeo/helm).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Address review: CNPG ScheduledBackup.spec.schedule is a 6-field cron
(leading seconds field), so the 5-field default was invalid — switch it to
"0 0 2 * * *". Also gate the ScheduledBackup on backup.destinationPath to
match db.yaml/backup-secret.yaml: schedule has a non-empty default, so the
old guard rendered a ScheduledBackup against a Cluster with no object store
whenever backups were enabled without a destinationPath.
Assisted-By: Claude
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Wire optional scheduled S3 backups for the keycloak-db CNPG cluster via
Barman. When backup.enabled=true with a destinationPath, the Cluster gets
spec.backup.barmanObjectStore (gzip-compressed data + WAL, configurable
retention) and a ScheduledBackup drives recurring base backups.
Credentials are inlined into a Secret or referenced via existingSecretName
(AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY). Disabled by default.
Assisted-By: Claude
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The console config ConfigMap stripped the leading v (trimPrefix "v") while
the operator's COZYSTACK_VERSION and the vX.Y.Z release tags keep it, so the
dashboard displayed 1.4.0 where the operator reported v1.4.0. Keep the tag
verbatim, and build the baked VITE_APP_VERSION fallback as v$(COZYSTACK_
VERSION) too so the deploy-time and build-time version strings stay
consistent with each other and with the operator.
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 COZYSTACK_VERSION env test pinned talos and hosted but not generic,
leaving the generic branch of the endpoint-env interleave (KUBERNETES_
SERVICE_HOST/PORT from cozystack.apiServerHost/Port) unguarded. Add a
generic + platformVersion case so a refactor can't silently drop
COZYSTACK_VERSION or mis-render the generic endpoint block.
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>
collect_refs scans "$TREE"/*/*/values.yaml (depth-2), but the host rewrite
used `find "$TREE" -name values.yaml` (any depth). A values.yaml nested
deeper than group/pkg would then have its image host rewritten to GHCR
without its image ever being mirrored there, leaving a dangling ref. Rewrite
over the same depth-2 glob so the rewritten set is exactly the mirrored set
(this also matches the existing --dry-run message).
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 first-ever `oras push` of cozystack-nocloud creates the GHCR package,
but without org.opencontainers.image.source it is created unlinked to the
repo, and the follow-up `oras tag … nightly` can 403 permission_denied:
write_package until someone links it by hand. Set the annotation on push so
the package is linked from the start (matches the --label the image builds
already carry).
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>
Addresses two blockers that stop a stable release from being cut by the
promote flow (both reproduced against a real registry in review).
B1 — wrong registry: the workflow forced env.REGISTRY to the OCIR CI
registry and logged in only to OCIR, but release-tag images (rc and
stable) are built and pushed to GHCR by tags.yaml (hack/common-envs.mk
default). promote-retag.sh filters refs by the $REGISTRY/ prefix, so with
REGISTRY=OCIR every GHCR ref was dropped, the selection went empty, and
the retag step exited 1 before doing anything. Point the job at
ghcr.io/cozystack/cozystack (matching the script default) and log in to
GHCR via --password-stdin (also resolves the token-on-argv nit).
B2 — missing chart: promote-retag.sh only retags image refs found in
values.yaml, and the cozy-installer OCI chart is a separate artifact in no
values.yaml. make build (which pushes the chart) is gated on a
not-yet-existing release, so a promoted stable never published
cozy-installer:X.Y.Z — the documented `helm --install … --version X.Y.Z`
404'd with manifest-unknown. Repackage the chart from the restamped stable
tree (no rebuild — it references the retagged images + packages artifact by
digest) and helm push it, mirroring the nightly flow. Adds helm to the job.
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 backup round-trip test could wedge for 20 min and then report a bare
EtcdSnapshot phase=Started with no root cause: dump_diagnostics dumped the
(healthy) EtcdCluster and the operator log, but the snapshot agent runs in
its own Job Pod, so the etcd-read / S3-upload error lives only in that Pod's
log and in the BackupJob/EtcdSnapshot status.
- dump_diagnostics now dumps the whole backup chain: BackupJob/Backup and
EtcdSnapshot describe, each snapshot Job's describe + Pod logs (selected by
the operator's etcd-operator.cozystack.io/cluster label), and recent
namespace events (the primary signal when a hung Pod logs nothing).
- wait_for_field gains an optional terminal-failure value; steps 04/05 pass
Failed so the BackupJob/RestoreJob waits return immediately on a terminal
phase instead of polling to the timeout — which also keeps the snapshot
Job Pod within its 600s TTL so its log is still readable at dump time.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
The v1alpha2 operator sets Available=True early (reason ClusterDiscovered)
as soon as the seed member forms the cluster, then scales the remaining
members up one at a time. Gating the runtime-contracts test on Available
raced the member-Pod count assertion and saw only 1/3 Pods, failing the
run. Gate on status.readyMembers=3 instead — the count of members whose
EtcdMember Ready condition is True (and the /scale status path), which
only reaches 3 once every member Pod is Running and serving. Bump the
wait timeout to 300s to cover sequential one-at-a-time rollout.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
Address review feedback from coderabbitai on hack/e2e-apps/etcd.bats: read the container name, metrics port, and Pod IP with the strict member selector (name+instance+managed-by) plus a Running-phase filter instead of the broad app.kubernetes.io/name=etcd, so .items[0] cannot resolve to a non-member or not-yet-running Pod. Also assert a non-empty Pod IP before the metrics probe.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
Address review feedback from gemini-code-assist on packages/system/etcd-operator/templates/deployment.yaml: add an explicit seccompProfile (RuntimeDefault) to the controller-manager pod securityContext.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
kubernetes-previous.bats intermittently fails on
`kubectl wait deploy --for=condition=available` with
`Error from server: etcdserver: leader changed` even when the target
Deployment is milliseconds away from becoming Ready. Observed in run
28772259934 on head `5f0bec622`: three of the four deployments
already reported `condition met`, the fourth (the Kamaji tenant CP
Deployment) was ~200ms from Ready per the crust-gather snapshot
captured 20s later (`readyReplicas: 2`), and the wait exited
non-zero on the FIRST etcd-side error it saw on the watch channel.
Cause: the e2e sandbox management cluster is a 3-node kind on Talos
VMs with a 3-instance etcd HA cluster. By the time
kubernetes-previous starts (after kubernetes-latest,
kubernetes-oidc-customconfig, kubernetes-oidc-system, plus the
storageclass-fallback probe), accumulated CDI+DRBD IO pressure and
lingering "still attached to node" PVCs from prior tests push etcd
fsync latency high enough to trip heartbeat timeouts on the leader.
The election that follows resolves in seconds, but `kubectl wait`
sees the server-side error mid-watch and exits — clients would
normally reconnect, but `wait` treats a watch-channel error as
terminal.
Wrap the four management-cluster `kubectl wait` calls in
run_kubernetes_test with a retry helper that:
- Retries only on a curated allowlist of server-side transients
(`etcdserver: leader changed`, `etcdserver: request timed out`,
`the server was unable to return a response in the time
allotted`).
- Never swallows legitimate errors (NotFound, --timeout expired,
permission denied, etc.).
- Caps at 3 attempts with a 5s backoff so a persistently-broken
etcd still surfaces loudly.
Untouched:
- `kubectl wait namespace tenant-test --timeout=20s` at line 308 —
short-lived, hits mgmt but the object always exists ~instantly.
- `kubectl wait deployment ... --kubeconfig tenantkubeconfig-...` at
line 509 — waits on the TENANT apiserver's own etcd, a different
failure domain (single-instance kine or CNPG-backed via Kamaji,
not the 3-instance mgmt etcd that flapped).
- Other bats files with mgmt-cluster `kubectl wait` — out of scope
for this PR; if the same flake reappears there a follow-up can
lift the helper into a shared library.
Signed-off-by: Ivan Okhotnikov <xorokhotnikov@gmail.com>
## Problem
The **PR Auto-Label** workflow (`.github/workflows/pr-labeler.yaml`)
triggers on `pull_request_target` with `types: [opened, edited,
reopened, synchronize]`.
The `edited` action fires on every PR title **and body** edit.
Long-lived bot PRs — notably Renovate — repeatedly edit their own
description on schedule (rebase checkbox, changelog tables,
dependency-dashboard state). Each body edit re-runs the `label` job
against an **unchanged commit**.
Example: on #2998 (a Renovate PR) the `label` job has run **~48 times**,
all on the same head SHA, from Jun 22 through today. It shows up on the
/checks page as a wall of duplicate labeler runs.
## Change
Drop `edited` from the trigger types:
```yaml
on:
pull_request_target:
types: [opened, reopened, synchronize]
```
## Trade-off
Labels derived from the PR title are re-applied on the next push
(`synchronize`) or on reopen. A **title-only** edit made between pushes
will no longer trigger an immediate relabel — an acceptable trade for
eliminating the constant bot-driven churn. (Title edits are rare and the
next push catches up.)
No behavior change for the common case: label-on-open and label-on-push
both still work.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Improved pull request labeling behavior so labels are refreshed on new
commits or when a PR is reopened, while avoiding unnecessary updates
from body-only edits.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The audience-list assertion used \`grep -qF "- tenant-test-..."\`.
Because the pattern begins with a dash, grep treats the leading \`-\`
as an option flag, aborting the test with \`grep: invalid option --
' '\` before the actual match ever runs. Insert an explicit \`--\`
end-of-options separator so the YAML list-item pattern is passed as a
pattern rather than a flag.
Custom-config counterpart is unaffected — its grep patterns do not
start with a dash.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
cozytest.sh (the e2e runner) is not real bats — it never invokes
setup()/teardown(). The OIDC files were structured around those hooks
to seed TEST_NAME and cleanup the tenant CR between tests, so with
cozytest strict-mode wrapping (\`set -eu\`), the first \${TEST_NAME}
expansion inside an @test aborted with 'parameter not set' before the
apply could even run.
Rework both files to the convention documented in external-dns.bats:
seed TEST_NAME at file scope (no per-test uniqueness needed — the
tenant-test namespace is single-suite), keep cleanup_kr() as a shared
helper, add a file-level cozy_cleanup() wrapper (the hook cozytest
actually calls on suite exit / first-failure), and move the pre-test
CR delete inline at the top of the first @test that creates the CR.
Subsequent @tests only read state and rely on the CR persisting until
cozy_cleanup.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Prior wording overstated Phase 1: the rendered
AuthenticationConfiguration carries no claimValidationRules, so the
chart itself does not gate on email_verified. Rewrite the failure-mode
bullet to describe the layered guarantees operators actually rely on:
1. provisioning KeycloakRealmUsers with emailVerified: true,
2. Keycloak's realm-level duplicateEmails: false as an
impersonation-through-second-account guard,
3. the k8s upstream side-effect that rejects tokens explicitly
carrying email_verified: false.
Also flag CEL claimValidationRules as a reasonable follow-up hardening
path, but explicitly out of scope for Phase 1 per the design doc.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
The claim mapping switched to email in de0299f33, but two header
comments still described the earlier design:
oidc-authn-config.yaml:15 — "Username claim is preferred_username"
oidc-rbac-job.yaml:28 — "matched against preferred_username"
The rendered config (oidc-authn-config.yaml:49) maps
username.claim: email, the CRB subject is rendered from .email
(oidc-rbac-job.yaml:264), and the email-claim contract is pinned by
tests/oidc_test.yaml:260-261. Only the comments were stale; no code
change.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
The tenant kube-apiserver auto-applies
`claims.?email_verified.orValue(true) == true` when username.claim is
email, so a Keycloak user with `emailVerified: false` fails login with
`oidc: email not verified` even though token issuance succeeds.
Document the requirement in the operator guide alongside the other
System-mode failure modes.
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>
Gemini flagged that the `values.yaml` @field docstring on
`oidc.users[].email` still described the claim mapping as
`preferred_username`, misleading operators after the claim was
switched to `email` for real (see the earlier fix and its
end-to-end verification on dev3). Rewrite the description to
match the actual mechanic: `email` claim, requested via
`--oidc-extra-scope=email` in the chart-generated kubeconfig,
emitted by every conformant OIDC provider.
`make generate` re-propagates the update through README.md,
types.go and the cozyrds embedded schema.
Note on the other finding in the same review — Gemini also
flagged `audiences` as being incorrectly nested under `issuer`
in the AuthenticationConfiguration Secret. That is a
false positive: per the kube-apiserver v1beta1 spec
(apiserver.config.k8s.io/v1beta1, `JWTAuthenticator.Issuer`),
`audiences` IS a child of `issuer`, not a sibling of `jwt[i]`.
The current shape is correct and verified end-to-end on dev3
(alice/bob tokens carry `aud: [tenant-test-kubernetes-test3,
kubernetes]` and apiserver validates them successfully with
CRB-driven RBAC).
Signed-off-by: Ivan Okhotnikov <ivan.okhotnikov@aenix.io>