Commit Graph
5128 Commits
Author SHA1 Message Date
myasnikovdaniil 48a3cfd341 Merge branch 'main' into ci/scorecard-hardening
Signed-off-by: myasnikovdaniil <60174387+myasnikovdaniil@users.noreply.github.com>
2026-07-07 16:49:29 +05:00
Aleksei Sviridkin e60ec04674 ci(actions): pin third-party GitHub Actions to commit SHAs (#3227)
## 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 -->
2026-07-07 13:14:16 +03:00
Aleksei Sviridkin 43104c9075 fix(ingress): guard the immutable loadBalancerClass migration on existing Services
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>
2026-07-07 12:39:30 +03:00
Aleksei Sviridkin a1748009ac fix(ingress): scope host loadBalancerClass to the external-facing ingress
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>
2026-07-07 12:39:30 +03:00
Andrei Kvapil 3dbc7d3662 refactor(network): drop ExposureClass/ServiceExposure for native loadBalancerClass
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>
2026-07-07 12:39:30 +03:00
Andrei Kvapil 75452f36a3 ci(actions): pin third-party GitHub Actions to commit SHAs
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>
2026-07-07 11:28:53 +02:00
Myasnikov DaniilandClaude Opus 4.8 a3f97e5a0c ci: align pre-commit zizmor with CI and set persist-credentials on ui-test
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>
2026-07-07 14:14:37 +05:00
Timur Tukaev 63fcebd804 docs(security): align security docs with current controls for incubation DD (#3225)
## 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.
2026-07-07 13:52:45 +05:00
tym83andClaude 60704530ed docs(security): align security docs with current controls for incubation DD
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>
2026-07-07 13:52:17 +05:00
Timur Tukaev 73f5798ec2 docs(security): add security self-assessment (#3224)
## 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`.
2026-07-07 13:20:42 +05:00
tym83andClaude 9a4bcfb13c docs(security): mark self-assessment stage as completed
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: tym83 <6355522@gmail.com>
2026-07-07 13:19:44 +05:00
tym83andClaude 19ab910f66 docs(security): add security self-assessment
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>
2026-07-07 13:11:34 +05:00
Myasnikov DaniilandClaude Opus 4.8 1f0c1949e7 test(flux-shard-operator): add fuzz targets for shard config parsers
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>
2026-07-07 11:31:23 +05:00
Myasnikov DaniilandClaude Opus 4.8 a0bd53f84d ci: pin workflow actions, scope token permissions, add zizmor gate
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>
2026-07-07 11:31:13 +05:00
Timofei Larkin 0f55a150ba fix(ci): catch templated CRDs and APIService-only groups in the API gate (#3184)
## 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 -->
2026-07-06 19:35:39 +04:00
Aleksei Sviridkin 3bf8fec399 docs(security): add design-level threat model (#3217)
## 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 -->
2026-07-06 18:30:55 +03:00
Aleksei Sviridkin 286f260570 docs(security): add design-level threat model
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>
2026-07-06 18:23:11 +03:00
Timofei Larkin 7b65cf2afd feat(platform): migrate to etcd-operator v1alpha2 (#2859)
<!-- 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 -->
2026-07-06 19:04:59 +04:00
Myasnikov DaniilandClaude Fable 5 ff7c8fa1ad fix(dashboard): guard empty COZYSTACK_VERSION in the console APP_VERSION build-arg
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>
2026-07-06 19:12:49 +05:00
Myasnikov DaniilandClaude Fable 5 8aebc34250 ci(release): compare-before-force the promote staging-branch push
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>
2026-07-06 19:12:23 +05:00
IvanHunters a0db444733 feat(apps/kubernetes): per-cluster OIDC selector for tenant kube-apiserver (Phase 1) (#3044) 2026-07-06 15:53:01 +03:00
Myasnikov DaniilandClaude Fable 5 70adc01e79 ci(release): nits — skopeo login via stdin; tighten dashboard version regex
- 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>
2026-07-06 17:49:40 +05:00
Myasnikov DaniilandClaude Fable 5 8acd7f7d6b ci(release): harden tags.yaml — publish rcs, gate stable-only jobs, reject hand-pushed stables
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>
2026-07-06 17:48:26 +05:00
Timofei Larkin 50f8548ba9 fix(ci): stop double-parsing every file in the API gate's content walk
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>
2026-07-06 15:48:07 +03:00
Timofei Larkin 985f17b7be fix(ci): catch templated CRDs and APIService-only groups in the API gate
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>
2026-07-06 15:48:07 +03:00
Myasnikov DaniilandClaude Fable 5 d252fc93a0 ci(release): make rc promotion transactional; defer registry writes to finalize
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>
2026-07-06 17:44:53 +05:00
Andrei Kvapil 18e141f77d fix(keycloak): use 6-field CNPG cron and gate ScheduledBackup on destinationPath
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>
2026-07-06 14:02:11 +02:00
Andrei Kvapil 31245bde54 feat(keycloak): add S3 backup for the CNPG database
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>
2026-07-06 14:02:10 +02:00
Myasnikov DaniilandClaude Opus 4.8 727f4e745e fix(dashboard): keep the v prefix on the reported platform version
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>
2026-07-06 16:49:42 +05:00
Myasnikov DaniilandClaude Opus 4.8 6d3d4908de test(installer): cover the generic variant in the version test
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>
2026-07-06 16:49:42 +05:00
Myasnikov DaniilandClaude Opus 4.8 4f2530f9eb fix(release): scope the nightly-mirror host rewrite to the collected depth
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>
2026-07-06 16:49:31 +05:00
Myasnikov DaniilandClaude Opus 4.8 c69f0f61c7 ci(release): annotate the nightly nocloud push with image.source
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>
2026-07-06 16:49:24 +05:00
Myasnikov DaniilandClaude Opus 4.8 846626ba12 fix(ci): promote against GHCR and publish the stable installer chart
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>
2026-07-06 16:49:18 +05:00
Andrey Kolkov d0740ed8c7 test(etcd): gate S3 backup round-trip behind ETCD_E2E_S3_ROUNDTRIP
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-06 14:51:48 +04:00
Andrey Kolkov 011d6d1639 fix FUs
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-06 14:41:33 +04:00
Andrey Kolkov f2866edcf0 review fixes
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-06 14:41:33 +04:00
Timofei Larkin 5f76ac0d76 test(etcd): capture backup Job Pod logs, fail fast on terminal phase
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>
2026-07-06 14:41:33 +04:00
Timofei Larkin bd1c8d1070 test(etcd): gate contracts test on readyMembers=3 not Available
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>
2026-07-06 14:41:33 +04:00
Andrey Kolkov 80ad7048e0 test(etcd): select member Pods by strict operator labels in e2e reads
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>
2026-07-06 14:41:33 +04:00
Andrey Kolkov ee12295d30 fix(etcd-operator): set seccompProfile RuntimeDefault on controller pod
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>
2026-07-06 14:41:33 +04:00
Andrey Kolkov 087a8c4be9 chore(etcd-operator): bump to v0.5.2
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-06 14:41:33 +04:00
Andrey Kolkov cc5d688e15 feat(platform): migrate to etcd-operator v1alpha2
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-07-06 14:41:33 +04:00
Ivan Okhotnikov 57fc2cd566 fix(e2e): retry kubectl wait on transient management-etcd errors
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>
2026-07-06 12:19:05 +03:00
IvanHunters f1014264f7 ci(pr-labeler): drop 'edited' trigger to stop bot PR re-run churn (#3211)
## 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 -->
2026-07-06 09:30:22 +03:00
Ivan Okhotnikov 5f0bec6222 test(e2e): guard oidc-system grep against leading-dash pattern
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>
2026-07-06 09:25:07 +03:00
Ivan Okhotnikov b637989fb0 test(e2e): port OIDC bats to cozytest lifecycle conventions
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>
2026-07-06 09:25:07 +03:00
Ivan Okhotnikov de6240270c docs(oidc): reframe emailVerified as prescriptive, not chart-enforced
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>
2026-07-06 09:25:07 +03:00
Ivan Okhotnikov 4e3a22992c docs(apps/kubernetes): fix stale preferred_username refs in OIDC template comments
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>
2026-07-06 09:25:07 +03:00
Ivan Okhotnikov ef6dd50570 docs(oidc): note emailVerified prerequisite for cozy-realm users
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>
2026-07-06 09:25:07 +03:00
Ivan Okhotnikov 7b8a330ad4 docs(apps/kubernetes): update users[].email docstring — email claim, not preferred_username
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>
2026-07-06 09:25:07 +03:00