Merge remote-tracking branch 'origin/main' into ci/build-arm64-leg

Signed-off-by: Aleksei Sviridkin <f@lex.la>
This commit is contained in:
Aleksei Sviridkin committed 2026-09-29 11:20:27 +03:00
commit c740031453
213 files changed
+6318 -501

No files matched your search

+7
View File
@@ -72,6 +72,13 @@
"groupName": "container image & action digests",
"schedule": ["before 6am on monday"]
},
{
"description": "The Harbor rebuilds reproduce one upstream release, so their build bases follow upstream's Makefile at that tag (GOBUILDIMAGE, NODEBUILDIMAGE) rather than the newest image. A golang tag bump builds Harbor with a toolchain upstream never shipped it with, and a node major breaks the Angular portal build. Tag bumps are disabled here; digest refreshes of the same tag still arrive through the digest group above. Move the tags by hand with HARBOR_VERSION.",
"matchManagers": ["dockerfile"],
"matchFileNames": ["packages/system/harbor/images/**"],
"matchUpdateTypes": ["major", "minor", "patch"],
"enabled": false
},
{
"description": "kubevirt.io/csi-driver is pinned to an untagged pseudo-version commit and publishes no release tags, so Renovate's go datasource returns no-result and the lookup failure lingers on the Dependency Dashboard. Disable it; this module is advanced deliberately alongside the kubevirt-csi-driver image build.",
"matchManagers": ["gomod"],
+9 -3
View File
@@ -61,11 +61,17 @@ jobs:
persist-credentials: false
# Ephemeral runners lack the flux CLI the installer's image-packages step
# shells out to; install if absent (idempotent).
# shells out to; install the pinned version when it is not already present.
- name: Set up build toolchain
env:
FLUX_VERSION: "2.8.6"
run: |
command -v flux >/dev/null \
|| curl -fsSL https://fluxcd.io/install.sh | sudo bash
if ! flux version --client 2>/dev/null | grep -Fqx "flux: v${FLUX_VERSION}"; then
install_script="$(mktemp)"
curl -fsSL https://fluxcd.io/install.sh -o "$install_script"
sudo env "FLUX_VERSION=$FLUX_VERSION" bash "$install_script"
rm -f "$install_script"
fi
- name: Set up Docker config
run: |
+9 -3
View File
@@ -107,11 +107,17 @@ jobs:
persist-credentials: false
# Ephemeral runners lack the flux CLI the installer's image-packages step
# shells out to; install if absent (idempotent).
# shells out to; install the pinned version when it is not already present.
- name: Set up build toolchain
env:
FLUX_VERSION: "2.8.6"
run: |
command -v flux >/dev/null \
|| curl -fsSL https://fluxcd.io/install.sh | sudo bash
if ! flux version --client 2>/dev/null | grep -Fqx "flux: v${FLUX_VERSION}"; then
install_script="$(mktemp)"
curl -fsSL https://fluxcd.io/install.sh -o "$install_script"
sudo env "FLUX_VERSION=$FLUX_VERSION" bash "$install_script"
rm -f "$install_script"
fi
- name: Set up Docker config
run: |
+9 -3
View File
@@ -353,13 +353,13 @@ jobs:
env:
# Pin the flux CLI instead of install.sh's rolling latest. This is the
# one privileged job that runs a remote installer as root right before
# the OCIR token is loaded, so it pins where the rest of CI does not;
# the OCIR token is loaded, where a pinned installer matters most;
# keep it in step with the flux distro cozystack ships
# (packages/system/fluxcd, 2.8.x).
# (internal/fluxinstall/manifests/fluxcd.yaml, v2.8.0).
FLUX_VERSION: "2.8.6"
run: |
command -v skopeo >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq skopeo; }
if ! command -v flux >/dev/null; then
if ! flux version --client 2>/dev/null | grep -Fqx "flux: v${FLUX_VERSION}"; then
s="$(mktemp)"
curl -fsSL https://fluxcd.io/install.sh -o "$s"
sudo env "FLUX_VERSION=$FLUX_VERSION" bash "$s"
@@ -373,6 +373,11 @@ jobs:
ref: refs/pull/${{ needs.resolve.outputs.pr_number }}/merge
fetch-depth: 0
persist-credentials: false
# checkout v4.4.0 and later refuse a fork's code under workflow_run
# without this opt-in. The boundary is the one the header describes:
# no fork-authored script runs in `publish`, and `e2e` holds no push
# token.
allow-unsafe-pr-checkout: true
# Guard against a TOCTOU race: the status is stamped on the SHA `resolve`
# saw, but refs/pull/N/merge tracks the live PR head. If the fork pushed
# again between resolve and here, HEAD^2 (the merge ref's PR-head parent)
@@ -566,6 +571,7 @@ jobs:
ref: refs/pull/${{ needs.resolve.outputs.pr_number }}/merge
fetch-depth: 0
persist-credentials: false
allow-unsafe-pr-checkout: true
# Same TOCTOU guard as `publish`: refuse to test a merge ref that moved off
# the SHA the status is stamped on (see the publish job for the rationale).
- name: Assert checkout matches resolved head SHA
+19 -2
View File
@@ -53,9 +53,16 @@ jobs:
packages_digest: ${{ steps.publish.outputs.packages_digest }}
steps:
- name: Set up toolchain (skopeo, flux, yq, helm)
env:
FLUX_VERSION: "2.8.6"
run: |
command -v skopeo >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq skopeo; }
command -v flux >/dev/null || curl -fsSL https://fluxcd.io/install.sh | sudo bash
if ! flux version --client 2>/dev/null | grep -Fqx "flux: v${FLUX_VERSION}"; then
install_script="$(mktemp)"
curl -fsSL https://fluxcd.io/install.sh -o "$install_script"
sudo env "FLUX_VERSION=$FLUX_VERSION" bash "$install_script"
rm -f "$install_script"
fi
command -v yq >/dev/null || { sudo curl -sSL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq; }
command -v helm >/dev/null || curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
@@ -201,6 +208,9 @@ jobs:
# rebuild of any cozystack image.
- name: Build nocloud disk
run: make -C packages/core/talos talos-nocloud
env:
# The e2e sandbox boots amd64 only.
PLATFORM: linux/amd64
- name: Publish disk to GHCR
env:
VERSION: ${{ needs.mirror.outputs.version }}
@@ -241,8 +251,15 @@ jobs:
fetch-depth: 1
persist-credentials: false
- name: Set up toolchain (flux, oras)
env:
FLUX_VERSION: "2.8.6"
run: |
command -v flux >/dev/null || curl -fsSL https://fluxcd.io/install.sh | sudo bash
if ! flux version --client 2>/dev/null | grep -Fqx "flux: v${FLUX_VERSION}"; then
install_script="$(mktemp)"
curl -fsSL https://fluxcd.io/install.sh -o "$install_script"
sudo env "FLUX_VERSION=$FLUX_VERSION" bash "$install_script"
rm -f "$install_script"
fi
command -v oras >/dev/null || { curl -fsSL https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz | sudo tar -xz -C /usr/local/bin oras; }
- name: Login to GHCR
env:
+13 -5
View File
@@ -353,13 +353,15 @@ jobs:
fetch-depth: 0
fetch-tags: true
# installer is built in finalize, but some package image targets shell out
# to flux; install it if absent (idempotent, cheap).
# to flux; install the pinned version when it is not already present.
- name: Set up build toolchain
env:
FLUX_VERSION: "2.8.6"
run: |
if ! command -v flux >/dev/null; then
if ! flux version --client 2>/dev/null | grep -Fqx "flux: v${FLUX_VERSION}"; then
install_script="$(mktemp)"
curl -fsSL https://fluxcd.io/install.sh -o "$install_script"
sudo bash "$install_script"
sudo env "FLUX_VERSION=$FLUX_VERSION" bash "$install_script"
rm -f "$install_script"
fi
- name: Set up Docker config
@@ -522,6 +524,10 @@ jobs:
env:
DOCKER_CONFIG: ${{ runner.temp }}/.docker
BUILDER: ${{ steps.buildx.outputs.name }}
# amd64 only, as in the build job. The arm64 installer needs no QEMU,
# but nothing a PR runs boots it. matchbox still gets the arm64 kernel
# and initramfs.
PLATFORM: linux/amd64
IMAGE_TAG: pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}
PUBLISH_VERSIONED: '0'
PUBLISH_FLOATING: '0'
@@ -578,11 +584,13 @@ jobs:
fetch-depth: 0
fetch-tags: true
- name: Set up build toolchain
env:
FLUX_VERSION: "2.8.6"
run: |
if ! command -v flux >/dev/null; then
if ! flux version --client 2>/dev/null | grep -Fqx "flux: v${FLUX_VERSION}"; then
install_script="$(mktemp)"
curl -fsSL https://fluxcd.io/install.sh -o "$install_script"
sudo bash "$install_script"
sudo env "FLUX_VERSION=$FLUX_VERSION" bash "$install_script"
rm -f "$install_script"
fi
- name: Download patch fragments
+12 -3
View File
@@ -254,12 +254,18 @@ jobs:
persist-credentials: false
# Ephemeral runners lack the flux CLI the installer's image-packages step
# shells out to; install if absent (idempotent).
# shells out to; install the pinned version when it is not already present.
- name: Set up build toolchain
if: steps.check_release.outputs.release_exists == 'false'
env:
FLUX_VERSION: "2.8.6"
run: |
command -v flux >/dev/null \
|| curl -fsSL https://fluxcd.io/install.sh | sudo bash
if ! flux version --client 2>/dev/null | grep -Fqx "flux: v${FLUX_VERSION}"; then
install_script="$(mktemp)"
curl -fsSL https://fluxcd.io/install.sh -o "$install_script"
sudo env "FLUX_VERSION=$FLUX_VERSION" bash "$install_script"
rm -f "$install_script"
fi
- name: Login to GHCR
if: steps.check_release.outputs.release_exists == 'false'
@@ -291,6 +297,9 @@ jobs:
run: |
mkdir -p "${PUSHED_TAGS_LOG%/*}"
make build
# The arm64 assets published below install this image, and building
# its arm64 half needs no QEMU, so it is pushed again as both arches.
make -C packages/core/talos image-talos PLATFORM=linux/amd64,linux/arm64
env:
DOCKER_CONFIG: ${{ runner.temp }}/.docker
# Cache manifests are registry-portable (oci-mediatypes=true) — read
+2 -7
View File
@@ -13,15 +13,10 @@ repos:
files: ^(api/|pkg/apis/|pkg/generated/|internal/crdinstall/manifests/|packages/system/cozystack-controller/definitions/|packages/system/application-definition-crd/definition/|packages/system/backup-controller/definitions/|packages/system/backupstrategy-controller/definitions/|hack/update-codegen\.sh$|hack/boilerplate\.go\.txt$|Makefile$)
pass_filenames: false
- id: run-make-generate
name: Run 'make generate' in all app directories
name: Run 'make generate' in all package directories
entry: |
sh -c 'exec flock -x "$(git rev-parse --git-common-dir)/pre-commit.lock" "$@"' -- sh -c '
for dir in ./packages/apps/*/ ./packages/extra/*/; do
if [ -d "$dir" ]; then
echo "Running make generate in $dir"
make generate -C "$dir" || exit $?
fi
done
hack/pre-commit-generate-packages.sh || exit $?
git diff --color=always | cat
'
language: system
+7
View File
@@ -13,18 +13,22 @@ build: build-deps
make -C packages/apps/mariadb image
make -C packages/apps/clickhouse image
make -C packages/apps/kubernetes image
make -C packages/apps/vpn image
make -C packages/system/cozystack-api image
make -C packages/system/cozystack-controller image
make -C packages/system/backup-controller image
make -C packages/system/backupstrategy-controller image
make -C packages/system/lineage-controller-webhook image
make -C packages/system/migration-controller image
make -C packages/system/flux-shard-operator image
make -C packages/system/flux-plunger image
make -C packages/system/cilium image
make -C packages/system/linstor image
make -C packages/system/linstor-gui image
make -C packages/system/kubeovn image
make -C packages/system/kubeovn-webhook image
make -C packages/system/kubeovn-plunger image
make -C packages/system/kilo image
make -C packages/system/dashboard image
make -C packages/system/metallb image
make -C packages/system/talos-log-collector image
@@ -37,7 +41,10 @@ build: build-deps
make -C packages/system/securitygroup-controller image
make -C packages/system/grafana-operator image
make -C packages/system/redis-operator image
make -C packages/system/harbor image
make -C packages/system/velero image
make -C packages/system/opensearch-operator image
make -C packages/system/keycloak-operator image
make -C packages/core/testing image
make -C packages/core/talos image
make -C packages/core/platform image
+1 -1
View File
@@ -29,7 +29,7 @@ type ConfigSpec struct {
// Deploy own Ingress Controller.
// +kubebuilder:default:=false
Ingress bool `json:"ingress"`
// Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). When unset (the default), the chart auto-enables the Gateway for tenants whose apex is derived from the parent (i.e. `host` is empty), and leaves it off for tenants with a custom non-derived apex. Set to `true` or `false` explicitly to override that auto-behaviour. Note: leave the key absent (do not write `gateway: null`) — the chart distinguishes "unset" via missing-key, not via null value, to satisfy the JSON schema generated from this comment.
// Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). When unset (the default) or set to `false`, the tenant does not own a Gateway and instead publishes through the nearest ancestor tenant that owns one, regardless of whether `host` is empty or custom (`false` currently behaves the same as unset and does not opt out of that inheritance). Set to `true` to give the tenant its own Gateway with its own LoadBalancer IP and Certificate. A tenant with a custom (non-derived) `host` that needs its apex publicly reachable must set `true` explicitly, because an ancestor's TLS certificate does not cover a custom apex. Note: leave the key absent (do not write `gateway: null`): the chart distinguishes "unset" via missing-key, not via null value, to satisfy the JSON schema generated from this comment.
Gateway bool `json:"gateway,omitempty"`
// GatewayClass for this tenant's own Gateway. Empty uses the platform default (`gateway.className`). Only the classes the operator lists in the platform's `gateway.tenantSelectableClasses`, plus that default, may be named here — anything else fails this tenant's own gateway release and reaches no other tenant. Read only when this tenant owns a Gateway; an inheriting tenant uses its ancestor's. Whether a class terminates TLS itself or at its provider's edge is declared once per class on the platform, not here.
// +kubebuilder:default:=""
+1 -1
View File
@@ -80,7 +80,7 @@ type Disk struct {
type Firmware struct {
// Bootloader to boot the VM with: "uefi" (OVMF) or "bios" (SeaBIOS). Empty inherits the instanceProfile default.
Bootloader string `json:"bootloader,omitempty"`
// Persist EFI NVRAM (e.g. enrolled Secure Boot keys, such as an updated Microsoft UEFI CA) across reboots. Only applies when bootloader is "uefi". On default RWO storage the VM is node-pinned (no live-migration).
// Persist EFI NVRAM (e.g. enrolled Secure Boot keys, such as an updated Microsoft UEFI CA) across reboots. Only applies when bootloader is "uefi".
EfiPersistent bool `json:"efiPersistent,omitempty"`
// Enable UEFI Secure Boot. Only applies when bootloader is "uefi".
SecureBoot bool `json:"secureBoot,omitempty"`
+4 -4
View File
@@ -44,7 +44,7 @@ If a package stamps a reference somewhere the two globs do not cover, add that f
- **Every first-party reference is tag+digest.** A bare tag with no digest is not reproducible and must not be committed. `securitygroup-controller:v0.0.0` is a live violation.
- **Exactly one producer stamps each reference.** A reference no `Makefile` writes will never be refreshed by anything, including release preparation — `chBackupClientImage` in `packages/system/backupstrategy-controller/values.yaml` is a live violation, still pinned to `platform-migrations:v1.4.0-rc.2`.
- **A package that stamps a reference is in the root `Makefile` `build:` list.** `build-matrix.sh`, the main build and release preparation all iterate that one list, so a package outside it is never rebuilt by any path and its pin freezes permanently. `flux-plunger`, `keycloak-operator` and `kilo` are live violations (tracked in #3416).
- **A package that stamps a reference is in the root `Makefile` `build:` list.** `build-matrix.sh`, the main build and release preparation all iterate that one list, so a package outside it is never rebuilt by any path and its pin freezes permanently. `hack/build-matrix_test.bats` computes the set of packages whose Makefile runs `docker buildx build` and fails on any the list misses.
- **Every consumer of references reads all three storage shapes**, via `hack/lib/image-refs.sh`.
- **Scan wider than you rewrite.** A rewrite is deliberately narrow to avoid corrupting vendored defaults, so the check that follows it must not share that blind spot. `hack/promote-rewrite-tags.sh` rewrites the enumerated files, then greps the whole tree and fails if any reference to the release candidate survives.
@@ -71,13 +71,13 @@ Two rows do not source the shared library, for opposite reasons. `hack/overlay-m
## Known gaps
These are real, currently unfixed, and predate the shared enumeration. They are recorded here so nobody rediscovers them as surprises.
These predate the shared enumeration. Each entry says whether the tree works around it today; they are recorded here so nobody rediscovers them as surprises.
**The nightly host rewrite cannot reach a host split across two keys.** The primary pass is a literal `<src-registry>/` substring replace, so it only rewrites a reference whose host sits contiguously in front of the repository. Two layouts defeat it, and only one is now handled.
**The nightly host rewrite cannot reach a host split across two keys.** The primary pass is a literal `<src-registry>/` substring replace, so it only rewrites a reference whose host sits contiguously in front of the repository. Two layouts in the tree defeated it; one is handled by a second expression and the other was removed from the tree.
`kubeovn` puts the whole host in `global.registry.address` with the bare repository in a sibling key, so there is no trailing slash to match. This one **is** rewritten now, by a second, end-of-line-anchored expression that fires only when the source registry is the complete scalar value — a contiguous `<src>/<repo>@<digest>` ref continues past the host, so the two expressions can never both fire on one reference. It had to be fixed when kubeovn's image build moved into this repository: until then the image was built and pushed to the public registry by cozystack/kubeovn-chart, the baked ref kept the committed public-registry host, and the ownership filter skipped it. The moment a CI build stamps the build-registry host into that key, the image is mirrored but the host is not rewritten, and the published tree points at the private build registry. Covered by `hack/nightly-mirror_test.bats`.
`keycloak-operator` remains unfixed. It splits at a different boundary — `registry: iad.ocir.io` beside `repository: idyksih5sir9/cozystack/<name>` — so neither key holds the source registry as a whole and no anchored replace can reach it; rewriting it needs a structure-aware pass that knows how the destination host splits. The gap stays dormant because no CI path rebuilds the package (#3416), and turns live the moment one does.
`keycloak-operator` split at a different boundary: `registry: ghcr.io` beside `repository: cozystack/cozystack/<name>`, with an `image:` target that stamped only `tag`. A CI build would have rendered the committed public host with a digest that exists only in the build registry, and stamping the build host into `registry` instead would spread the source registry across two keys (`iad.ocir.io` and `idyksih5sir9/cozystack/<name>`), where no anchored replace can reach it. Rather than a structure-aware rewrite pass, the package now sets `registry: ""` and keeps the whole host in `repository`, which renders the same image string because the upstream chart joins the two keys, and its `image:` target stamps `repository` the same way. The rewrite still cannot reach a host split at that boundary, so a package must not stamp one: keep the host inside `repository`, or make it the complete value of its key as kubeovn does. Covered by `hack/nightly-mirror_test.bats`.
Collection understands both shapes — the host is rejoined, so a split-key ref carrying the source-registry host is still selected and mirrored. This is a defect in the host dimension; the version dimension the promote rewrite handles is unaffected, because the version always lives in a `tag` key regardless of where the host is.
+2 -2
View File
@@ -94,9 +94,9 @@ Build environment variables:
- `REGISTRY` — Docker registry (default `ghcr.io/cozystack/cozystack`).
- `PUSH=1` / `LOAD=0` — control buildx push/load behaviour.
- `LOAD=1 PUSH=0` — load images locally instead of pushing. Builds for the host architecture only, because the classic docker image store cannot load a multi-platform index.
- `PLATFORM` — the buildx platforms. Defaults to `linux/amd64,linux/arm64` for anything but `LOAD=1`, so a push from either architecture publishes an index both can run. Needs a `docker-container` builder (`BUILDER`) or a docker daemon on the containerd image store, with QEMU registered for the non-native half. Compiling builder stages run natively on `$BUILDPLATFORM` and cross-compile for the target, but every `RUN` step of a stage that runs on the target platform (package installs, `setcap`, install scripts, patch steps) still runs emulated. `packages/core/talos` and `packages/core/testing` pin `linux/amd64`, because they carry amd64-only payloads: the Talos assets and an x86 KVM sandbox.
- `PLATFORM` — the buildx platforms. Defaults to `linux/amd64,linux/arm64` for anything but `LOAD=1`, so a push from either architecture publishes an index both can run. Needs a `docker-container` builder (`BUILDER`) or a docker daemon on the containerd image store, with QEMU registered for the non-native half. Compiling builder stages run natively on `$BUILDPLATFORM` and cross-compile for the target, but every `RUN` step of a stage that runs on the target platform (package installs, `setcap`, install scripts, patch steps) still runs emulated. `packages/core/testing` pins `linux/amd64`, because it carries an x86 KVM sandbox. `packages/core/talos` builds the Talos installer and release assets for the arches in `PLATFORM` with the upstream imager, which needs no QEMU for either; its matchbox image carries the amd64 and arm64 kernel and initramfs whatever `PLATFORM` says, because it network-boots machines of either arch.
- `PUSHED_TAGS_LOG` — a file the `image-tags` macro appends every pushed `<repo>:<tag>` to, one per line. Unset by default; the release build sets it so `hack/stitch-multiarch.sh` can move every tag it pushed, versioned ones included, onto the multi-arch index. The append happens at recipe expansion, so `make -n` writes it too.
- `CACHE_TAG` — the tag of the mode=max build cache each image reads (and writes under `WRITE_CACHE=1`). Defaults to `buildcache`. CI never builds both architectures in one job, and builds arm64 only for pre-release tags and in a nightly build of main: the images are built again on native arm64 runners under `<tag>-arm64` with `CACHE_TAG=buildcache-arm64`, which only the nightly writes. The talos and testing packages are amd64 only and get no arm64 build. For a pre-release, `hack/stitch-multiarch.sh` then joins each pair into one index, `hack/verify-multiarch.sh` fails the rc on any pinned image that is not an amd64+arm64 index, and the packages artifact is republished pinned on the index digests; see `docs/release.md`. The stitch skips any image whose digest is also pinned in a file it does not rewrite, which then fails that gate; the kamaji control-plane provider's gzipped manifest is the one such copy it does rewrite (see `docs/agents/image-refs.md`).
- `CACHE_TAG` — the tag of the mode=max build cache each image reads (and writes under `WRITE_CACHE=1`). Defaults to `buildcache`. CI never builds both architectures in one job, and builds arm64 only for pre-release tags and in a nightly build of main: the images are built again on native arm64 runners under `<tag>-arm64` with `CACHE_TAG=buildcache-arm64`, which only the nightly writes. The talos and testing packages get no arm64 leg: the release job builds the Talos installer for both arches itself, and testing is amd64 only. For a pre-release, `hack/stitch-multiarch.sh` then joins each pair into one index, `hack/verify-multiarch.sh` fails the rc on any pinned image that is not an amd64+arm64 index, and the packages artifact is republished pinned on the index digests; see `docs/release.md`. The stitch skips any image whose digest is also pinned in a file it does not rewrite, which then fails that gate; the kamaji control-plane provider's gzipped manifest is the one such copy it does rewrite (see `docs/agents/image-refs.md`).
### Values schema generation
+154
View File
@@ -0,0 +1,154 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.6.4
-->
# v1.6.4 (2026-09-28)
A patch release with backported fixes for VM live migration during eviction and scheduling, Kafka release deletion, KubeVirt-related billing labels, ingress client-IP trust, PostgreSQL-adjacent LINSTOR/CDI/ZFS reliability, MongoDB and RabbitMQ defaults, and a batch of e2e hardening, plus a new `talm` documentation site, an NVIDIA vGPU restore role for `ansible-cozystack`, and a large Marketplace documentation section on the website.
## Fixes
* **fix(cluster-api): backport live migration before host eviction**: `cluster-api-provider-kubevirt` now attempts live migration before falling back to drain and delete when a guest node is evicted, uses the guest node's own name for the drain fallback instead of the wrong identifier, and uncordons a recreated guest — so an eviction no longer forces an unnecessary VM restart when a live migration would have sufficed ([**@yankawai**](https://github.com/yankawai) in #4291, backport #4474).
* **fix(kubeovn): retry VM migration setup after scheduling**: Retries the VM migration network setup when the target Pod or source/target scheduling information is not yet available, instead of failing outright, and folds the Kube-OVN image into the regular in-tree build matrix — so a migration that starts before Kube-OVN has finished scheduling both endpoints no longer aborts ([**@yankawai**](https://github.com/yankawai) in #4253, backport #4328).
* **feat(kubevirt): expose migration configuration through platform values**: Cluster-wide KubeVirt migration settings (bandwidth per migration, parallel migrations per cluster, parallel outbound migrations per node) can now be set through the platform value `kubevirt.migrations` instead of hand-patching the generated `Package`, which the platform undoes on every render; leaving the value unset preserves existing defaults ([**@yankawai**](https://github.com/yankawai) in #4254, backport #4402).
* **fix(linstor): add opt-in graceful satellite shutdown on Talos**: Adds an opt-in Talos satellite `preStop` hook that releases unused Secondary DRBD resources, ZFS zvols included, before the satellite stops, without forcing, demoting or calling the Kubernetes API. Enable it with `talos.gracefulShutdown.enabled` for sequential node maintenance; existing installations keep today's behavior by default, and the hook also runs on ordinary pod restarts so it should be enabled only where node restarts are serialized ([**@yankawai**](https://github.com/yankawai) in #4292, backport #4401).
* **fix(workloadmonitor): populate VM instance-profile label from the KubeVirt preference annotation**: `WorkloadMonitor` read a `kubevirt.io/cluster-instanceprofile-name` annotation that KubeVirt never writes; the real annotation is `kubevirt.io/cluster-preference-name`. Reading the correct key means a VM's instance profile — a Windows guest, for example — now reaches the billing metric pipeline instead of being silently dropped ([**@IvanHunters**](https://github.com/IvanHunters) in #4448, backport #4454).
* **fix(opensearch-operator): name the component reconciler a stall sits in**: The OpenSearch cluster controller now logs a watchdog report naming which of its nine component reconcilers has been running for over a minute, and keeps reporting on an interval while it stays stuck, instead of leaving a stalled reconcile with no trace in the operator log at all ([**@IvanHunters**](https://github.com/IvanHunters) in #4333, backport #4377).
* **fix(manifestutil): report one message when the CRD wait is cancelled**: A CRD wait cut short by a cancelled context now always reports one error naming the CRD it was waiting on, instead of sometimes including the name and sometimes not, depending on which of two exit paths the scheduler happened to pick ([**@lexfrei**](https://github.com/lexfrei) in #3742, backport #4475).
* **fix(kafka): delete release topics before the topic operator is removed**: Deletes release-owned `KafkaTopic`s with a pre-delete hook before the Strimzi topic operator is removed, and fixes the hook's own `--request-timeout` flag, which made it skip in-cluster credentials and fail every deletion outright. Together these mean a Kafka release with the `strimzi.io/topic-operator` finalizer no longer leaves stray topics behind on reinstall, and release deletion itself no longer wedges ([**@yankawai**](https://github.com/yankawai), [**@IvanHunters**](https://github.com/IvanHunters) in #3938, #4280, backport #4456).
* **fix(ingress): trust CF-Connecting-IP only from Cloudflare ranges**: With `cloudflareProxy` enabled, `CF-Connecting-IP` is now trusted only from Cloudflare's published address ranges. Previously the ranges were passed under a key `ingress-nginx` ignores, so any client that reached the load balancer directly could set the header itself and bypass the ingress IP `whitelist` ([**@lexfrei**](https://github.com/lexfrei) in #4444, backport #4445).
* **fix(apps): let the cleanup hooks watch the objects they delete**: The Harbor, ClickHouse, MariaDB and Qdrant cleanup hooks now wait for the objects they delete to actually disappear, with the wait bounded so a stuck claim cannot hang the whole uninstall ([**@yankawai**](https://github.com/yankawai) in #4135, backport #4369).
* **fix(registry): preserve write options in aggregated storage**: The application, tenant-secret and security-group APIs now preserve Kubernetes write options (dry-run, field manager, field validation, delete propagation, grace periods, preconditions) when delegating to controller-runtime, instead of silently dropping them; a rejected write also keeps its machine-readable status, so a quota or admission failure now answers 403/409 with a reason instead of a generic 500 ([**@yankawai**](https://github.com/yankawai) in #3937, backport #4370).
* **fix(dashboard): let dashboard users read backup CRD schemas**: Grants dashboard users read access to the backup CRD schemas, fixing the backup create/edit forms that previously returned 403 ([**@androndo**](https://github.com/androndo) in #4236, backport #4307).
* **fix(backups): give the barman-cloud sidecar its own resources**: The barman-cloud sidecar previously had no resource requests or limits, so in tenant namespaces it inherited a 128Mi `LimitRange` default and was OOMKilled during backups even while the surrounding `ObjectStore` and `Cluster` stayed healthy. It now requests 100m/256Mi and is capped at 1Gi on both the chart and the platform's Go rendering path ([**@yankawai**](https://github.com/yankawai) in #4220, backport #4336).
* **fix(linstor): reconnect only the stalled peer in the satellite plunger**: The satellite plunger disconnected a stalled DRBD peer but then tried to reconnect every peer of the resource at once, which failed with "Device has a net-config" for peers that were never disconnected and left the stalled one permanently unreconnected. It now reconnects only the peer pair it disconnected, so a stalled resync actually recovers ([**@yankawai**](https://github.com/yankawai) in #4184, backport #4319).
* **fix(foundationdb): let tenant roles read the connection ConfigMap**: Tenants previously had no way to obtain their FoundationDB connection string: the cluster file lives in a ConfigMap tenant subjects could not read and the dashboard resource map did not expose. The ConfigMap is now in the dashboard resource map with a Role/RoleBinding granting the tenant `use` access level get/list/watch on it ([**@yankawai**](https://github.com/yankawai) in #4148, backport #4315).
* **fix(opensearch): remove the data PVCs when the application is deleted**: Adds a post-delete hook that reclaims an OpenSearch application's data PVCs when it is removed, instead of leaving them orphaned ([**@yankawai**](https://github.com/yankawai) in #4136, backport #4368).
* **fix(nats): merge config.merge.accounts with the generated accounts map**: `config.merge.accounts` is now merged into the generated NATS accounts map instead of being rendered as a second, conflicting copy of the same key ([**@yankawai**](https://github.com/yankawai) in #4134, backport #4321).
* **fix(clickhouse): keep the Keeper name inside the StatefulSet volume-name limit**: Shortens the generated Keeper name for application names longer than 15 characters, fixing the Keeper `StatefulSet` failing to create for those names ([**@yankawai**](https://github.com/yankawai) in #4133, backport #4314).
* **fix(linstor): wait for temporary probe devices**: Backports a LINSTOR upstream fix that waits for a ZFS probe device to appear before reading its I/O properties, instead of racing udev; without the wait, a pool could be left with incorrect block sizes and failed replica placement on 4K pools ([**@yankawai**](https://github.com/yankawai) in #4100, backport #4335).
* **fix(mongodb): fill the dashboard credentials on a first install**: The dashboard credentials Secret is now filled with the database password and connection URI on first install; previously it shipped empty until someone ran `helm upgrade` by hand, because the lookup it relied on could only see users the Percona operator had not yet created ([**@yankawai**](https://github.com/yankawai) in #4014, backport #4330).
* **fix(rabbitmq): right-size the default resources preset to s1.nano**: Raises the default `resourcesPreset` from `t1.nano` (128Mi) to `s1.nano` (250m CPU, 512Mi), because a default-sized RabbitMQ 4.2 broker was OOMKilled on `t1.nano` and never became Ready. Brokers with no stored preset are restarted on upgrade and gain a larger guaranteed memory reservation — check node headroom, and tenants near a `resourceQuotas` memory ceiling, before upgrading; brokers with an explicit `resourcesPreset` already set keep their existing size ([**@yankawai**](https://github.com/yankawai) in #3936, backport #4393).
* **fix(kafka): make topics[].config optional**: Kafka topics can now omit the optional Strimzi config map entirely, instead of the chart requiring one even when there is nothing to set ([**@yankawai**](https://github.com/yankawai) in #3935, backport #4334).
* **fix(kubernetes): pin KubeVirt CSI sidecars to SIG Storage releases**: Pins the KubeVirt CSI sidecars to versioned SIG Storage images, fixing sidecars that previously would not run on pre-x86-64-v3 CPUs ([**@yankawai**](https://github.com/yankawai) in #3934, backport #4313).
* **fix(cdi): update CDI to v1.66.1**: Updates CDI to v1.66.1, fixing HTTP imports that block volumes on 4Kn devices (DRBD/LINSTOR) via a repaired `qemu-img`, and a `resourceVersion` update storm on prime PVCs during `WaitForFirstConsumer` imports ([**@lexfrei**](https://github.com/lexfrei) in #3920, backport #4156).
* **feat(etcd-operator): bump to v0.5.5 and vendor EtcdDefragPolicy CRDs**: Bumps `etcd-operator` to v0.5.5, adding the `EtcdDefrag` and `EtcdDefragPolicy` APIs for on-demand and scheduled etcd defragmentation ([**@androndo**](https://github.com/androndo) in #3968, backport #4151).
## Development, Testing, and CI/CD
* **test(e2e): run the merge-gating lanes on Talos containers**: Moves the merge-gating E2E lanes onto Talos containers, fixing tenant node-join failures that were happening in 8 of the last 11 runs because tenant workers sat one virtualization level too deep on the shared runners and had their CSR signed after the readiness window closed ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #4020, backport #4437).
* **chore(e2e): ignore the artifacts the sandbox bringup writes to the repo root**: Adds `.gitignore` entries for the Talos secrets bundle, rendered machine configs, kubeconfigs, boot image and per-VM directories a local e2e sandbox bringup writes to the repo root, closing a path where a broad `git add` could have staged cluster private keys ([**@lexfrei**](https://github.com/lexfrei) in #3891, backport #4438).
* **fix(e2e): budget tenant scheduling separately from workload readiness**: Waits for a tenant node to actually accept a Pod, on its own budget, before creating the workload used by the kubernetes suites, instead of sharing one 300s deadline between node scheduling and workload readiness — fixing intermittent timeouts where scheduling alone consumed most of the shared window ([**@lexfrei**](https://github.com/lexfrei) in #3579, backport #4435).
* **fix(e2e): budget each linstor wait separately in post-install-prep**: Gives each of the two LINSTOR waits in the post-install-prep script its own deadline, started when the previous one finishes, instead of sharing a single 15-minute window anchored at the script's start — fixing failures where a slow HelmRelease left the following Deployment wait too little time ([**@lexfrei**](https://github.com/lexfrei) in #3715, backport #4434).
* **fix(reloader): add startupProbe to survive slow first start under load**: Adds a startup probe to `cozy-reloader` so a slow first start is no longer killed by the liveness probe and left crash-looping, which also unblocks the LINSTOR stack that depends on it starting cleanly ([**@lexfrei**](https://github.com/lexfrei) in #3470, backport #4433).
* **fix(e2e): stop the sandbox cluster CIDR overlapping the pod range**: Moves the e2e sandbox's Kubernetes cluster CIDR off the range Kube-OVN uses for pod IPAM, fixing intermittent Cilium/Kube-OVN IP collisions that either refused a pod outright with "IP already in use" or silently broke host-to-pod traffic ([**@lexfrei**](https://github.com/lexfrei) in #3750, backport #4432).
* **fix(e2e): disable guest fsync on ephemeral CI VM disks**: Boots e2e QEMU guests with `cache=unsafe` so ephemeral CI VM fsyncs no longer stall the management-cluster etcd, removing cross-operator leader-election flakiness that was failing a different chainsaw suite on every run ([**@kvaps**](https://github.com/kvaps) in #3455, backport #4431).
## Documentation
* **[website] feat(compliance): surface AI Conformance page, mark submission accepted**: Publishes the AI Conformance results page and marks the platform's submission as accepted ([**@tym83**](https://github.com/tym83) in cozystack/website#706).
* **[website] fix(docs): refresh trunk version pins to Cozystack v1.6.3**: Updates the documentation's trunk version references to Cozystack v1.6.3 ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#618).
* **[website] docs(platform-package): sync the publishing.\* table with the platform values**: Keeps the platform values reference's `publishing.*` table in sync with the actual platform values ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#679).
* **[website] docs(networking): GatewayClass selection, edge TLS mode, and the Cloudflare Tunnel package**: Documents GatewayClass selection, the edge TLS certificate mode, and the Cloudflare Tunnel Gateway API package ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#655).
* **[website] docs(platform-package): document KubeVirt migration settings**: Documents the new `kubevirt.migrations` platform value from the corresponding fix above ([**@yankawai**](https://github.com/yankawai) in cozystack/website#697).
* **[website] fix(demo): keep the screen when the demo is reloaded**: Fixes the live console demo losing its current screen on a browser reload ([**@kvaps**](https://github.com/kvaps) in cozystack/website#701).
* **[website] docs(platform-package): document kubevirt.disabledFeatureGates**: Documents the `kubevirt.disabledFeatureGates` platform value ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#678).
* **[website] chore(demo): refresh the console demo from the cozystack monorepo**: Refreshes the live demo's bundled `cozystack-ui` console from the monorepo ([**@kvaps**](https://github.com/kvaps) in cozystack/website#698).
* **[website] fix(site): point the Certified Kubernetes badge at the live landscape entry**: Fixes the Certified Kubernetes badge linking to a stale landscape entry ([**@tym83**](https://github.com/tym83) in cozystack/website#699).
* **[website] docs(compliance): add AI Conformance page, update Kubernetes Conformance**: Adds the initial AI Conformance page and updates the Kubernetes Conformance page ([**@tym83**](https://github.com/tym83) in cozystack/website#693).
* **[website] docs(blog): route independent-adopter replies to the project**: Redirects independent-adopter reply channels on the blog to the project instead of an individual ([**@tym83**](https://github.com/tym83) in cozystack/website#696).
* **[website] fix(site): resolve /roadmap and move the vendor logo off a project path**: Fixes the `/roadmap` link and moves a vendor logo off a project-owned path ([**@tym83**](https://github.com/tym83) in cozystack/website#695).
* **[website] feat(home): add a "where Cozystack fits" section**: Adds a "where Cozystack fits" section to the home page ([**@tym83**](https://github.com/tym83) in cozystack/website#694).
* **[website] fix(site): give the live-demo band its own layout**: Gives the live-demo band on the home page its own layout instead of sharing one with unrelated content ([**@tym83**](https://github.com/tym83) in cozystack/website#692).
* **[website] feat(site): verify the @cozystack.io Bluesky handle over HTTPS**: Serves the Bluesky handle DID at `/.well-known/atproto-did` to verify the `@cozystack.io` Bluesky handle over HTTPS ([**@kvaps**](https://github.com/kvaps) in cozystack/website#691).
* **[website] docs(marketplace): add Application Marketplace section**: Adds a large new Application Marketplace documentation section covering the CLI reference, package taps, subscriptions, trust model, and catalog behavior ([**@lexfrei**](https://github.com/lexfrei), [**@IvanHunters**](https://github.com/IvanHunters) in cozystack/website#672).
* **[website] fix(site): replace vendor social accounts in the footer with project channels**: Replaces vendor-owned social accounts in the site footer with the project's own channels ([**@tym83**](https://github.com/tym83) in cozystack/website#690).
* **[website] fix(networking): correct what disabledPackages does to ouroboros**: Corrects the documented effect of `disabledPackages` on the `ouroboros` package ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#683).
* **[website] docs(virtualization): add a VMware vSphere migration guide**: Adds a guide for migrating VMware vSphere VMs into Cozystack, including the VDDK image build ([**@kvaps**](https://github.com/kvaps) in cozystack/website#673).
* **[website] docs(kubernetes): add OIDC authentication guide**: Adds a guide for configuring OIDC authentication on managed Kubernetes clusters ([**@IvanHunters**](https://github.com/IvanHunters) in cozystack/website#596).
* **[website] ci(telemetry): put the monthly refresh back on a schedule**: Restores the scheduled monthly refresh of the telemetry dashboard data ([**@tym83**](https://github.com/tym83) in cozystack/website#645).
* **[website] docs: update managed apps reference for v1.6.3**: Refreshes the managed applications reference for the v1.6.3 release ([**@app/cozystack-ci**](https://github.com/apps/cozystack-ci) in cozystack/website#681).
* **[talm] docs: publish the manual as a documentation site**: Publishes `talm`'s manual as a documentation site at talm.cozystack.io, with a CLI reference generated from the Cobra command tree ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#235).
* **[talm] ci: unbreak the lint job under Go 1.27**: Fixes the `talm` lint job under Go 1.27 and requires Go 1.27 for the build ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#237).
* **[talm] chore(deps): bump Talos to v1.14.0**: Bumps the Talos dependency to v1.14.0, along with fixes so `applycheck` and `meta --insecure` keep working against it and a rendered config keeps every document instead of dropping some ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/talm#223).
* **[ansible-cozystack] feat(roles): restore NVIDIA vGPU host state across reboots**: Adds a role that restores NVIDIA vGPU host state after a reboot, instead of requiring it to be reconfigured by hand ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#70).
* **[ansible-cozystack] chore(deps): track cozystack installer v1.6.3**: Renovate dependency bump of the `cozy-installer` Docker image tag to follow the v1.6.3 Cozystack release ([**@app/renovate**](https://github.com/apps/renovate) in cozystack/ansible-cozystack#71).
* **[ansible-cozystack] chore(deps): update k3s to v1.37.0+k3s1**: Renovate dependency bump of the k3s Kubernetes distribution used by the management-cluster bootstrap role ([**@app/renovate**](https://github.com/apps/renovate) in cozystack/ansible-cozystack#72).
## Contributors
Thanks to everyone who contributed to this patch release:
* [**@androndo**](https://github.com/androndo)
* [**@IvanHunters**](https://github.com/IvanHunters)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil)
* [**@sircthulhu**](https://github.com/sircthulhu)
* [**@tym83**](https://github.com/tym83)
* [**@yankawai**](https://github.com/yankawai)
### New Contributors
We're excited to welcome our first-time contributor:
* [**@yankawai**](https://github.com/yankawai) - First contribution!
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.6.3...v1.6.4
+23
View File
@@ -169,3 +169,26 @@
actual=$(echo "$out" | jq 'length')
[ "$expected" -eq "$actual" ]
}
@test "every package that builds an image is in the build list" {
# The build list is maintained by hand, and a package missing from it is
# rebuilt by no CI path: its Dockerfile and patches keep changing while the
# pinned image stays whatever was last pushed by hand. Derive the set from
# the package Makefiles so a new image target cannot be forgotten.
listed=$(sed -n '/^build:/,/^[^[:space:]]/p' Makefile \
| grep -oE 'make -C packages/[A-Za-z0-9._/-]+ image' \
| sed -E 's/^make -C (packages[^ ]+) image$/\1/')
missing=""
checked=0
for m in packages/*/*/Makefile; do
grep -q 'docker buildx build' "$m" || continue
checked=$((checked + 1))
d=${m%/Makefile}
printf '%s\n' "$listed" | grep -qx "$d" || missing="$missing $d"
done
[ "$checked" -gt 0 ]
if [ -n "$missing" ]; then
echo "image-building packages missing from the root Makefile build: list:$missing" >&2
false
fi
}
+11 -4
View File
@@ -16,6 +16,8 @@ EXPECTED=(
)
fail=0
jq_err=$(mktemp)
trap 'rm -f "$jq_err"' EXIT
for f in packages/system/*-rd/cozyrds/*.yaml; do
schema=$(yq -r '.spec.application.openAPISchema // ""' "$f")
if [ -z "$schema" ]; then
@@ -24,12 +26,17 @@ for f in packages/system/*-rd/cozyrds/*.yaml; do
# Pull every resourcesPreset enum out of the schema. Key on the JSON
# path ending in "resourcesPreset" rather than a description heuristic,
# so an unrelated field with "preset" in its description does not match.
enums=$(printf '%s' "$schema" | jq -r '
# A failed read can leave empty or partial output; neither is an enum to check.
if ! enums=$(jq -r '
[paths(type == "object" and has("enum")) as $p
| select($p[-1] == "resourcesPreset")
| getpath($p).enum[]]
| .[]
' 2>/dev/null || true)
' <<<"$schema" 2>"$jq_err"); then
echo "FAIL: $f openAPISchema could not be read: $(tr '\n' ' ' <"$jq_err")" >&2
fail=1
continue
fi
if [ -z "$enums" ]; then
continue
fi
@@ -51,8 +58,8 @@ for f in packages/system/*-rd/cozyrds/*.yaml; do
done
if [ "$fail" -ne 0 ]; then
echo "Some RD schemas are out of sync with the canonical preset set." >&2
echo "Run 'make generate' inside the affected chart directory." >&2
echo "Some RD schemas failed the preset check; see the errors above." >&2
echo "For missing presets, run 'make generate' inside the affected chart directory." >&2
exit 1
fi
echo "All RD schemas carry the full 47-preset enum."
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bats
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
CHECK="$REPO_ROOT/hack/check-rd-presets.sh"
canonical_presets() {
for family in t1 c1 s1 u1 m1; do
for size in nano micro small medium large xlarge 2xlarge 4xlarge; do
echo "$family.$size"
done
done
printf '%s\n' nano micro small medium large xlarge 2xlarge
}
write_schema() {
{
printf 'spec:\n application:\n openAPISchema: |\n'
sed 's/^/ /'
} > packages/system/foo-rd/cozyrds/foo.yaml
}
enum_schema() {
jq -Rn '{type: "object", properties: {resourcesPreset: {enum: [inputs]}}}' enums
}
make_tree() {
dir=$(mktemp -d)
cd "$dir" || return 1
mkdir -p packages/system/foo-rd/cozyrds packages/apps packages/extra
canonical_presets > enums
enum_schema | write_schema
}
run_check() {
rc=0
out=$("$CHECK" 2>&1) || rc=$?
printf '%s\n' "$out"
}
assert_unreadable() {
[ "$rc" -ne 0 ]
printf '%s\n' "$out" | grep -F 'FAIL: packages/system/foo-rd/cozyrds/foo.yaml openAPISchema could not be read:'
printf '%s\n' "$out" | grep -F 'parse error:'
case "$out" in
*"resourcesPreset enum missing"*) return 1 ;;
esac
}
cleanup() {
cd "$REPO_ROOT" || return 1
rm -rf "$dir"
}
@test "malformed schema fails with the file and jq diagnostic" {
make_tree
printf '{\n' | write_schema
run_check
assert_unreadable
cleanup
}
@test "jq failure after emitting presets cannot pass or report drift" {
make_tree
printf '%s\n' t1.nano t1.micro > enums
# jq emits the first document's enum before parsing the malformed next one.
{ enum_schema; printf '{\n'; } | write_schema
run_check
assert_unreadable
cleanup
}
@test "a schema without resourcesPreset is skipped" {
make_tree
printf '{"type":"object"}\n' | write_schema
run_check
[ "$rc" -eq 0 ]
cleanup
}
@test "an absent schema is skipped" {
make_tree
printf 'spec: {}\n' > packages/system/foo-rd/cozyrds/foo.yaml
run_check
[ "$rc" -eq 0 ]
cleanup
}
@test "a complete canonical enum passes" {
make_tree
run_check
[ "$rc" -eq 0 ]
cleanup
}
@test "a preset list larger than the pipe buffer passes" {
make_tree
# Exceed the CI runner's 1 MiB pipe maximum to make a SIGPIPE regression deterministic.
while [ "$(wc -c < enums)" -lt 4194304 ]; do
cat enums enums > enums.next
mv enums.next enums
done
mkdir bin
cat > bin/jq <<'STUB'
#!/bin/sh
cat enums
STUB
chmod +x bin/jq
PATH="$dir/bin:$PATH" run_check
[ "$rc" -eq 0 ]
cleanup
}
@test "a missing preset is reported by name" {
make_tree
canonical_presets | sed '/^c1\.medium$/d' > enums
enum_schema | write_schema
run_check
[ "$rc" -ne 0 ]
printf '%s\n' "$out" | grep -F 'resourcesPreset enum missing: c1.medium'
cleanup
}
@test "similar preset names do not satisfy exact membership" {
make_tree
canonical_presets | sed 's/^t1\.nano$/t1.nanoX/; s/^c1\.small$/c1Xsmall/' > enums
enum_schema | write_schema
run_check
[ "$rc" -ne 0 ]
printf '%s\n' "$out" | grep -F 'resourcesPreset enum missing: t1.nano c1.small'
cleanup
}
@test "EXPECTED contains exactly the canonical names" {
make_tree
sort enums > canonical.sorted
sed -n '/^EXPECTED=(/,/^)/p' "$CHECK" | sed '1d;$d' | tr -s '[:space:]' '\n' | sed '/^$/d' | sort > actual.sorted
[ "$(wc -l < canonical.sorted)" -eq 47 ]
diff -u canonical.sorted actual.sorted
cleanup
}
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/env bats
# Shared by bats and cozytest.sh: inspect exit codes directly, without bats-only
# helpers. Scratch directories stay available if an assertion fails.
make_render_chart() {
mkdir -p "$1/templates"
printf 'apiVersion: v2\nname: render-test\nversion: 0.0.0\n' > "$1/Chart.yaml"
printf 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: render-test\n' > "$1/templates/object.yaml"
}
expect_render_failure() {
if output=$(dash hack/check-render-matrix.sh "$@" 2>&1); then
echo "expected render failure, got: $output" >&2
return 1
fi
printf '%s\n' "$output"
}
@test "rendered plus skipped accounts for every app chart" {
total=$(find packages/apps -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
output=$(dash hack/check-render-matrix.sh)
summary=$(printf '%s\n' "$output" | grep '^check-render-matrix:')
rendered=$(printf '%s\n' "$summary" | sed 's/.*: \([0-9]*\) rendered.*/\1/')
skipped=$(printf '%s\n' "$summary" | sed 's/.*, \([0-9]*\) skipped.*/\1/')
[ "$rendered" -gt 0 ]
[ "$((rendered + skipped))" -eq "$total" ]
}
@test "render matrix preserves multiple chart paths containing spaces" {
tmp=$(mktemp -d)
make_render_chart "$tmp/space in parent/first"
make_render_chart "$tmp/space in parent/second"
output=$(dash hack/check-render-matrix.sh "$tmp/space in parent/first/" "$tmp/space in parent/second")
printf '%s\n' "$output" | grep -Fx 'check-render-matrix: 2 rendered, 0 skipped'
rm -rf "$tmp"
}
@test "render matrix refuses empty discovery and ignores nested fixtures" {
tmp=$(mktemp -d)
mkdir -p "$tmp/hack/testdata/render-fixtures/validator"
cp hack/check-render-matrix.sh "$tmp/hack/"
cp hack/testdata/render-fixtures/fresh.yaml "$tmp/hack/testdata/render-fixtures/validator/nested.yaml"
output=$(cd "$tmp" && expect_render_failure)
printf '%s\n' "$output" | grep -F 'found no charts under packages/apps'
make_render_chart "$tmp/packages/apps/example"
output=$(cd "$tmp" && expect_render_failure)
printf '%s\n' "$output" | grep -F 'no fixtures under hack/testdata/render-fixtures'
rm -rf "$tmp"
}
@test "render matrix reports a broken chart and still checks the next chart" {
tmp=$(mktemp -d)
make_render_chart "$tmp/broken"
make_render_chart "$tmp/valid"
printf '{{ required "deliberately broken" .Values.nope }}\n' > "$tmp/broken/templates/object.yaml"
output=$(expect_render_failure "$tmp/broken" "$tmp/valid")
for fixture in hack/testdata/render-fixtures/*.yaml; do
state=$(basename "$fixture" .yaml)
printf '%s\n' "$output" | grep -Fx "FAIL broken ($state)"
done
printf '%s\n' "$output" | grep -F 'deliberately broken'
printf '%s\n' "$output" | grep -Fx 'check-render-matrix: 1 rendered, 0 skipped'
rm -rf "$tmp"
}
@test "render matrix fails on malformed YAML" {
tmp=$(mktemp -d)
make_render_chart "$tmp/badyaml"
printf 'a: b\n c: d\n' > "$tmp/badyaml/templates/object.yaml"
output=$(expect_render_failure "$tmp/badyaml")
printf '%s\n' "$output" | grep -F 'FAIL badyaml'
printf '%s\n' "$output" | grep -F 'YAML parse error'
rm -rf "$tmp"
}
@test "render matrix does not count a nonexistent chart as rendered" {
tmp=$(mktemp -d)
output=$(expect_render_failure "$tmp/missing")
printf '%s\n' "$output" | grep -F 'FAIL missing'
printf '%s\n' "$output" | grep -Fx 'check-render-matrix: 0 rendered, 0 skipped'
rm -rf "$tmp"
}
@test "render matrix rejects skips that now render or fail for another reason" {
tmp=$(mktemp -d)
for name in vm-instance kubernetes-nodes; do
make_render_chart "$tmp/$name"
output=$(expect_render_failure "$tmp/$name")
printf '%s\n' "$output" | grep -F 'chart now renders; remove its skip'
printf '{{ fail "unrelated render failure" }}\n' > "$tmp/$name/templates/object.yaml"
output=$(expect_render_failure "$tmp/$name")
printf '%s\n' "$output" | grep -F 'unrelated render failure'
printf '%s\n' "$output" | grep -Fx 'check-render-matrix: 0 rendered, 0 skipped'
done
rm -rf "$tmp"
}
@test "render matrix counts documented lookup failures as skipped" {
tmp=$(mktemp -d)
make_render_chart "$tmp/vm-instance"
make_render_chart "$tmp/kubernetes-nodes"
printf '%s\n' '{{ fail "Specified instanceType does not exist in the cluster: u1.medium" }}' > "$tmp/vm-instance/templates/object.yaml"
printf '%s\n' '{{ fail `specified instanceType "u1.medium" not found in cluster` }}' > "$tmp/kubernetes-nodes/templates/object.yaml"
output=$(dash hack/check-render-matrix.sh "$tmp/vm-instance" "$tmp/kubernetes-nodes")
printf '%s\n' "$output" | grep -F 'SKIP vm-instance ('
printf '%s\n' "$output" | grep -F 'SKIP kubernetes-nodes ('
printf '%s\n' "$output" | grep -Fx 'check-render-matrix: 0 rendered, 2 skipped'
rm -rf "$tmp"
}
@test "fixtures match the injected value types" {
for fixture in hack/testdata/render-fixtures/*.yaml; do
helm template fixture-check hack/testdata/render-fixtures/validator \
--set-file "fixture=$fixture"
done
}
@test "fixture validation rejects wrong scalar and scheduling types" {
tmp=$(mktemp -d)
for value in true 12 null '[]' '{}'; do
printf '_cluster:\n oidc-enabled: %s\n_namespace: {host: "example.org"}\n' "$value" > "$tmp/value.yaml"
if output=$(helm template fixture-check hack/testdata/render-fixtures/validator \
--set-file "fixture=$tmp/value.yaml" 2>&1); then
echo "accepted non-string oidc-enabled: $value" >&2
exit 1
fi
printf '%s\n' "$output" | grep -F '_cluster.oidc-enabled must be a string'
done
for value in true 12 null '[]' '{}'; do
printf '_cluster:\n scheduling:\n globalAppTopologySpreadConstraints: %s\n dedicatedNodesForWindowsVMs: "false"\n_namespace: {host: "example.org"}\n' "$value" > "$tmp/value.yaml"
if output=$(helm template fixture-check hack/testdata/render-fixtures/validator \
--set-file "fixture=$tmp/value.yaml" 2>&1); then
echo "accepted non-string scheduling constraint: $value" >&2
exit 1
fi
printf '%s\n' "$output" | grep -F '_cluster.scheduling.globalAppTopologySpreadConstraints must be a string'
done
rm -rf "$tmp"
}
@test "fixture validation rejects malformed maps and missing scheduling keys" {
tmp=$(mktemp -d)
for variant in malformed-yaml empty-namespace branding-scalar namespace-map scheduling-scalar missing-scheduling missing-constraint missing-windows; do
cluster_extra=''
scheduling='scheduling:'
namespace='host: "example.org"'
constraint='globalAppTopologySpreadConstraints: ""'
windows='dedicatedNodesForWindowsVMs: "false"'
case "$variant" in
malformed-yaml) constraint='['; windows=''; expected='error converting YAML to JSON' ;;
empty-namespace) namespace=''; expected='_namespace must be a non-empty map' ;;
branding-scalar) cluster_extra='branding: true'; expected='_cluster.branding must be a map' ;;
namespace-map) namespace='branding: {}'; expected='_namespace.branding must be a string' ;;
scheduling-scalar) constraint='true'; windows=''; expected='_cluster.scheduling must be a map' ;;
missing-scheduling) cluster_extra='oidc-enabled: "false"'; scheduling=''; constraint=''; windows=''; expected='_cluster.scheduling must be a map' ;;
missing-constraint) constraint=''; expected='_cluster.scheduling.globalAppTopologySpreadConstraints is missing' ;;
missing-windows) windows=''; expected='_cluster.scheduling.dedicatedNodesForWindowsVMs is missing' ;;
esac
printf '_cluster:\n %s\n %s\n %s\n %s\n_namespace: {%s}\n' \
"$cluster_extra" "$scheduling" "$constraint" "$windows" "$namespace" > "$tmp/value.yaml"
if output=$(helm template fixture-check hack/testdata/render-fixtures/validator \
--set-file "fixture=$tmp/value.yaml" 2>&1); then
echo "accepted invalid fixture: $variant" >&2
exit 1
fi
printf '%s\n' "$output" | grep -F "$expected"
done
rm -rf "$tmp"
}
@test "configured scheduling reaches the postgres manifest" {
tmp=$(mktemp -d)
for state in fresh configured wildcard; do
helm template postgres-render-check packages/apps/postgres -n tenant-test \
-f "hack/testdata/render-fixtures/$state.yaml" > "$tmp/$state.yaml"
done
if grep -q '^ topologySpreadConstraints:' "$tmp/fresh.yaml"; then
echo 'fresh unexpectedly enables topology spread' >&2
exit 1
fi
for state in configured wildcard; do
grep -A 7 '^ topologySpreadConstraints:' "$tmp/$state.yaml" > "$tmp/constraints"
grep -F 'maxSkew: 1' "$tmp/constraints"
grep -F 'topologyKey: topology.kubernetes.io/zone' "$tmp/constraints"
grep -F 'cnpg.io/cluster: postgres-render-check' "$tmp/constraints"
done
rm -rf "$tmp"
}
@test "render matrix reaches capability-gated OIDC templates" {
tmp=$(mktemp -d)
make_render_chart "$tmp/tenant"
cat > "$tmp/tenant/templates/object.yaml" <<'EOF'
{{- if and (eq (index .Values._cluster "oidc-enabled") "true") (.Capabilities.APIVersions.Has "v1.edp.epam.com/v1") }}
{{- fail "OIDC branch rendered" }}
{{- end }}
EOF
output=$(expect_render_failure "$tmp/tenant")
printf '%s\n' "$output" | grep -Fx 'FAIL tenant (configured)'
printf '%s\n' "$output" | grep -Fx 'FAIL tenant (wildcard)'
printf '%s\n' "$output" | grep -F 'OIDC branch rendered'
if printf '%s\n' "$output" | grep -Fq 'FAIL tenant (fresh)'; then
echo 'OIDC branch enabled for fresh fixture' >&2
exit 1
fi
rm -rf "$tmp"
}
@test "fixture states enable OIDC groups and shared-service resources" {
tmp=$(mktemp -d)
for state in fresh configured wildcard; do
helm template tenant-check packages/apps/tenant -n tenant-test \
--api-versions v1.edp.epam.com/v1 \
-f "hack/testdata/render-fixtures/$state.yaml" > "$tmp/tenant.yaml"
helm template kubernetes-check packages/apps/kubernetes -n tenant-test \
-f "hack/testdata/render-fixtures/$state.yaml" > "$tmp/kubernetes.yaml"
groups=$(awk '/^kind: KeycloakRealmGroup$/ {n++} END {print n+0}' "$tmp/tenant.yaml")
metrics=$(awk '/^ name: kubernetes-check-metrics-server$/ {n++} END {print n+0}' "$tmp/kubernetes.yaml")
case "$state" in
fresh) [ "$groups" -eq 0 ]; [ "$metrics" -eq 0 ] ;;
*) [ "$groups" -eq 4 ]; [ "$metrics" -eq 1 ] ;;
esac
done
rm -rf "$tmp"
}
@test "certificate fixtures exercise per-host ACME and wildcard ingress" {
tmp=$(mktemp -d)
for state in configured wildcard; do
helm template harbor-check packages/apps/harbor -n tenant-test \
-f "hack/testdata/render-fixtures/$state.yaml" \
--show-only templates/ingress.yaml > "$tmp/$state.yaml"
grep -Fx 'kind: Ingress' "$tmp/$state.yaml"
grep -F 'harbor-check.example.org' "$tmp/$state.yaml"
done
grep -F 'acme.cert-manager.io/http01-ingress-ingressclassname: tenant-test' "$tmp/configured.yaml"
grep -F 'cert-manager.io/cluster-issuer: letsencrypt-prod' "$tmp/configured.yaml"
grep -F 'secretName: harbor-check-ingress-tls' "$tmp/configured.yaml"
grep -Fx ' tls:' "$tmp/wildcard.yaml"
if grep -Eq '^[[:space:]]+(acme.cert-manager.io/|cert-manager.io/cluster-issuer:|secretName:)' "$tmp/wildcard.yaml"; then
echo 'wildcard ingress unexpectedly requests a per-host certificate' >&2
exit 1
fi
rm -rf "$tmp"
}
@test "every pair of fixture states produces different deterministic manifests" {
tmp=$(mktemp -d)
count=0
for fixture in hack/testdata/render-fixtures/*.yaml; do
state=$(basename "$fixture" .yaml)
count=$((count + 1))
# This compares whole states; the tests above assert specific branches.
# Render twice so randomness cannot masquerade as state coverage.
for run in first second; do
helm template tenant-check packages/apps/tenant -n tenant-test \
--api-versions v1.edp.epam.com/v1 -f "$fixture" > "$tmp/$state-$run.yaml"
done
cmp "$tmp/$state-first.yaml" "$tmp/$state-second.yaml"
cp "$tmp/$state-first.yaml" "$tmp/$state.manifests"
done
[ "$count" -ge 3 ]
for a in "$tmp"/*.manifests; do
for b in "$tmp"/*.manifests; do
[ "$a" != "$b" ] || continue
if cmp -s "$a" "$b"; then
echo "fixture states produce identical manifests: $a and $b" >&2
exit 1
fi
done
done
rm -rf "$tmp"
}
+87
View File
@@ -0,0 +1,87 @@
#!/bin/sh
# Render app charts on their defaults plus injected cluster/namespace values.
# helm-unittest renders only templates selected by a suite; this smoke check
# also reaches templates with no assertions. It checks rendering, not Kubernetes
# schema validity or runtime behavior. See testdata/render-fixtures/README.md.
# Usage (from the repository root): hack/check-render-matrix.sh [chart-dir ...]
set -eu
FIXTURE_DIR="hack/testdata/render-fixtures"
# Empty lookup results are the only accepted reason to skip a chart. Match the
# actual diagnostic, so another error cannot keep an obsolete skip alive.
skip_reason() {
case "$1" in
vm-instance) echo 'Specified instanceType does not exist in the cluster: u1.medium' ;;
kubernetes-nodes) echo 'specified instanceType "u1.medium" not found in cluster' ;;
*) echo "" ;;
esac
}
render_chart() {
chart=$1 fixture=$2
name=$(basename "$chart")
case "$name" in
tenant) set -- tenant-sub --api-versions v1.edp.epam.com/v1 ;;
# Satisfy the parent-cluster and release-name contract before the lookup.
kubernetes-nodes) set -- kubernetes-nodes-render-check --set-string cluster=render ;;
*) set -- "$name-render-check" ;;
esac
helm template "$@" "$chart" -n tenant-test -f "$fixture"
}
if [ "$#" -eq 0 ]; then
set -- packages/apps/*/
if [ ! -d "$1" ]; then
echo "check-render-matrix: found no charts under packages/apps" >&2
exit 1
fi
fi
# Check exactly the files the render loop will consume, not nested fixtures.
fixtures=0
for fixture in "$FIXTURE_DIR"/*.yaml; do
[ -f "$fixture" ] || continue
fixtures=$((fixtures + 1))
done
if [ "$fixtures" -eq 0 ]; then
echo "check-render-matrix: no fixtures under $FIXTURE_DIR" >&2
exit 1
fi
errors=$(mktemp)
trap 'rm -f "$errors"' EXIT
rc=0 rendered=0 skipped=0
for dir in "$@"; do
name=$(basename "$dir")
reason=$(skip_reason "$name")
chart_failed=0
for fixture in "$FIXTURE_DIR"/*.yaml; do
[ -f "$fixture" ] || continue
state=$(basename "$fixture" .yaml)
if render_chart "$dir" "$fixture" >/dev/null 2>"$errors"; then
if [ -z "$reason" ]; then
continue
fi
echo "FAIL $name ($state): chart now renders; remove its skip"
else
if [ -n "$reason" ] && grep -Fq "$reason" "$errors"; then
continue
fi
echo "FAIL $name ($state)"
sed -n '/^level=INFO/d; s/^/ /; p' "$errors"
fi
rc=1
chart_failed=1
done
[ "$chart_failed" -eq 0 ] || continue
if [ -n "$reason" ]; then
echo "SKIP $name ($reason)"
skipped=$((skipped + 1))
else
rendered=$((rendered + 1))
fi
done
echo "check-render-matrix: $rendered rendered, $skipped skipped"
exit "$rc"
+14
View File
@@ -175,3 +175,17 @@
[ ! -e "$tmp/export-log" ]
rm -rf "$tmp"
}
@test "talos pushes one installer index for PLATFORM and matchbox boots both arches" {
# -B prints every asset build whether or not a local _out/assets has it.
out=$(make -n -B -C packages/core/talos image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)
echo "$out" | grep -q 'for a in amd64 arm64; do skopeo copy'
echo "$out" | grep -q 'hack/installer-index.sh [^ ]* amd64 arm64;'
echo "$out" | grep -q 'skopeo copy --all "$SRC" docker://[^ ]*/talos:pr-1-abc'
# The installer follows PLATFORM, but matchbox serves machines of either arch
# wherever it runs, so it still needs the arm64 kernel and initramfs.
out=$(make -n -B -C packages/core/talos image IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b PLATFORM=linux/amd64)
if echo "$out" | grep -q 'installer-arm64'; then echo "FAIL: an amd64 build builds the arm64 installer"; false; fi
echo "$out" | grep -q 'profiles/kernel-arm64.yaml'
echo "$out" | grep -q 'profiles/initramfs-arm64.yaml'
}
@@ -99,3 +99,61 @@ spec:
set -eu
cd ../../..
NAMESPACE="$NAMESPACE" examples/backups/kafka-metadata/cleanup.sh
# cleanup.sh above deleted both Kafka apps; the node pools the migration hook
# created outside the release manifest must go with their uninstalls. The pod
# checks select on strimzi.io/cluster on purpose, so they cover every pod
# Strimzi runs for each cluster, not only the brokers.
#
# 12m per check, because cleanup.sh returns without waiting for the
# uninstalls. The pools are deleted inside each release's pre-delete Job,
# which activeDeadlineSeconds 270 plus terminationGracePeriodSeconds 10 bound
# at 280s; the pods then take Strimzi's default 30s termination grace period.
# If the two uninstalls run one after the other, 2 x 280 + 30 = 590s, and the
# remaining 130s cover helm-controller picking up each deletion and garbage
# collection. Every check carries the whole budget, since the order in which
# the two finish is not fixed; a passing run waits only as long as the
# uninstalls take.
- name: verify-uninstall-removes-node-pools
try:
- error:
timeout: 12m
resource:
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaNodePool
metadata:
labels:
strimzi.io/cluster: kafka-kafka-meta-src
- error:
timeout: 12m
resource:
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaNodePool
metadata:
labels:
strimzi.io/cluster: kafka-kafka-meta-target
- error:
timeout: 12m
resource:
apiVersion: v1
kind: Pod
metadata:
labels:
strimzi.io/cluster: kafka-kafka-meta-src
- error:
timeout: 12m
resource:
apiVersion: v1
kind: Pod
metadata:
labels:
strimzi.io/cluster: kafka-kafka-meta-target
catch:
# A failed error op already names each object still matching. This adds
# the StrimziPodSets between the pools and the pods, and the pods' phase.
- script:
timeout: 1m
content: |
kubectl -n "$NAMESPACE" --request-timeout=15s \
get kafkanodepools.kafka.strimzi.io,strimzipodsets.core.strimzi.io,pods \
-l 'strimzi.io/cluster in (kafka-kafka-meta-src,kafka-kafka-meta-target)' -o wide || true
+93 -2
View File
@@ -4,8 +4,8 @@
# of separate broker + controller KafkaNodePools. This test asserts the KRaft
# topology renders and converges (both node pools present, the Strimzi Kafka CR
# reaches kafkaMetadataState=KRaft) and that no ZooKeeper resources remain.
# Chainsaw's built-in cleanup deletes the applied Kafka CR and fails the test if
# the teardown hangs, so no explicit delete step is needed.
# The last step deletes the app itself and checks that no node pool or Strimzi
# pod of this cluster survives the uninstall.
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
@@ -121,6 +121,51 @@ spec:
metadata:
name: kafka-test-kafka-bootstrap
(subsets[0].addresses[0].ip != null): true
# The node pools are created by the migration hook, outside the release
# manifest, and each owns the StrimziPodSet running its brokers, so only the
# chart's pre-delete hook removes them. Deleting the app explicitly here,
# rather than leaving it to Chainsaw's cleanup, is what lets the test look at
# the namespace afterwards. The pod check selects on strimzi.io/cluster on
# purpose, so it covers every pod Strimzi runs for this cluster (brokers,
# controllers, entity operator), not only the brokers.
- name: verify-uninstall-removes-node-pools
try:
# Waits for the app, and so the HelmRelease uninstall behind it, to finish.
# 8m, the same budget and for the same reason as timeouts.cleanup above:
# this delete is the teardown that cleanup used to perform.
- delete:
timeout: 8m
ref:
apiVersion: apps.cozystack.io/v1alpha1
kind: Kafka
name: test
# The pre-delete hook deleted the pools before the delete above returned;
# the pods then go through garbage collection and their termination grace
# period, which the default error timeout covers.
- error:
resource:
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaNodePool
metadata:
labels:
strimzi.io/cluster: kafka-test
- error:
resource:
apiVersion: v1
kind: Pod
metadata:
labels:
strimzi.io/cluster: kafka-test
catch:
# A failed error op already names each object still matching. This adds
# the StrimziPodSets between the pools and the pods, and the pods' phase.
- script:
timeout: 1m
content: |
kubectl -n "$NAMESPACE" --request-timeout=15s \
get kafkanodepools.kafka.strimzi.io,strimzipodsets.core.strimzi.io,pods \
-l strimzi.io/cluster=kafka-test -o wide || true
---
# Logical topic-data backup -> in-place + to-copy RestoreJob round-trip,
# driving the validated example scripts (examples/backups/kafka) as the harness
@@ -254,3 +299,49 @@ spec:
set -eu
cd ../../..
NAMESPACE="$NAMESPACE" examples/backups/kafka/cleanup.sh
# cleanup.sh above deleted both Kafka apps and waited for them to go, so their
# uninstalls have run, and the node pools the migration hook created outside
# the release manifest must be gone with them. The pod checks select on
# strimzi.io/cluster on purpose, so they cover every pod Strimzi runs for each
# cluster, not only the brokers. What is left is garbage collection and the
# pods' termination grace period, which the default error timeout covers.
- name: verify-uninstall-removes-node-pools
try:
- error:
resource:
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaNodePool
metadata:
labels:
strimzi.io/cluster: kafka-kafka-test
- error:
resource:
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaNodePool
metadata:
labels:
strimzi.io/cluster: kafka-kafka-restore
- error:
resource:
apiVersion: v1
kind: Pod
metadata:
labels:
strimzi.io/cluster: kafka-kafka-test
- error:
resource:
apiVersion: v1
kind: Pod
metadata:
labels:
strimzi.io/cluster: kafka-kafka-restore
catch:
# A failed error op already names each object still matching. This adds
# the StrimziPodSets between the pools and the pods, and the pods' phase.
- script:
timeout: 1m
content: |
kubectl -n "$NAMESPACE" --request-timeout=15s \
get kafkanodepools.kafka.strimzi.io,strimzipodsets.core.strimzi.io,pods \
-l 'strimzi.io/cluster in (kafka-kafka-test,kafka-kafka-restore)' -o wide || true
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bats
# actions/checkout v4.4.0 and later refuse to check out a fork's pull request
# ref from a workflow_run workflow unless the step sets
# allow-unsafe-pr-checkout. e2e-fork.yaml is such a workflow, so a checkout of
# refs/pull/... there without the opt-in fails the publish job for every fork
# PR, and the required "E2E Tests" status never turns green.
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
FORK="$REPO_ROOT/.github/workflows/e2e-fork.yaml"
# Prints one line per actions/checkout step: "<ref> <opt-in>", where <opt-in>
# is the value of allow-unsafe-pr-checkout or "unset".
checkout_steps() {
awk '
function flush() { if (in_step) print ref " " optin; in_step = 0 }
/^[[:space:]]*- (name|uses):/ { flush() }
/uses: actions\/checkout@/ { in_step = 1; ref = "default"; optin = "unset"; next }
in_step && /^[[:space:]]*ref:/ { ref = $2 }
in_step && /^[[:space:]]*allow-unsafe-pr-checkout:/ { optin = $2 }
END { flush() }
' "$FORK"
}
@test "e2e-fork checks out at least one pull request ref" {
checkout_steps | grep -q '^refs/pull/'
}
@test "every pull request checkout in e2e-fork opts in to fork code" {
bad="$(checkout_steps | grep '^refs/pull/' | grep -v ' true$' || true)"
if [ -n "$bad" ]; then
echo "checkout of a pull request ref without allow-unsafe-pr-checkout: true:" >&2
echo "$bad" >&2
exit 1
fi
}
+56 -19
View File
@@ -11,8 +11,8 @@
# A stage compiles when one of its RUN commands is go build/test/install, make,
# gradle, dpkg-buildpackage, a node package manager or a *build.sh script, at
# command position (after RUN and its --flag options, `&&`, `;` or `||`, past
# any VAR=value prefixes). Such a stage must sit on a BUILDPLATFORM FROM and
# use TARGETARCH or TARGETPLATFORM on a RUN or ENV line, unless its output
# any VAR=value prefixes). Such a stage must run on the build platform (see
# stages() below) and use TARGETARCH or TARGETPLATFORM on a RUN or ENV line, unless its output
# carries no architecture at all (ARCH_NEUTRAL below). A bare
# `ARG TARGETARCH` does not count: it declares the variable, and a `go build`
# that never reads it still builds for the host. The check is per stage, not
@@ -25,19 +25,41 @@
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
# Outputs with no architecture: the console builds static JS; piraeus-server
# builds Java .debs that linstor-server's debian/control declares
# `Architecture: all`.
ARCH_NEUTRAL="packages/system/dashboard/images/console/Containerfile
# Outputs with no architecture, one entry per line: a whole file, or
# "<file>:<stage>" when only that stage is neutral and the file's other
# stages still have to target TARGETARCH. The console, the Harbor portal and
# shadowbox's webpack stage build static JS; piraeus-server builds Java .debs
# that linstor-server's debian/control declares `Architecture: all`.
ARCH_NEUTRAL="packages/apps/vpn/images/shadowbox/Dockerfile:app
packages/system/dashboard/images/console/Containerfile
packages/system/harbor/images/harbor-portal/Dockerfile
packages/system/linstor/images/piraeus-server/Dockerfile"
# One line per stage: "<compiles 0|1> <targets 0|1> <FROM line>".
# is_neutral <file> <stage> <list>: 1 when the list names the file or that stage of it.
is_neutral() {
printf '%s\n' "$3" | awk -v f="$1" -v s="$1:$2" '$0 == f || $0 == s { hit = 1 } END { print hit + 0 }'
}
# One line per stage: "<compiles 0|1> <targets 0|1> <native 0|1> <name> <FROM line>".
# A stage is native when its FROM carries --platform=$BUILDPLATFORM, or when it
# builds FROM an earlier native stage: a stage inherits the platform of the
# stage it starts from. An unnamed stage is reported as "-".
# Continuation lines are joined first so a command after `&& \` is seen at
# command position.
stages() {
sed -e ':a' -e '/\\$/N; s/\\\n//; ta' "$1" | awk '
function flush() { if (from != "") printf "%d %d %s\n", compiles, targets, from }
/^FROM[[:space:]]/ { flush(); from = $0; compiles = 0; targets = 0; next }
function flush() { if (from != "") printf "%d %d %d %s %s\n", compiles, targets, native, name, from }
/^FROM[[:space:]]/ {
flush(); from = $0; compiles = 0; targets = 0; native = 0; name = "-"; base = ""
for (i = 2; i <= NF; i++) {
if ($i ~ /^--platform=(\$BUILDPLATFORM|\$\{BUILDPLATFORM\})$/) native = 1
else if ($i !~ /^--/ && base == "") base = $i
else if (tolower($i) == "as" && i < NF) name = $(i + 1)
}
if (base in nativestage) native = 1
if (native && name != "-") nativestage[name] = 1
next
}
/^RUN[[:space:]]/ && /(^RUN([[:space:]]+--[^[:space:]]+)*|&&|;|[|][|])[[:space:]]+([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*(go[[:space:]]+(build|test|install)|make|gradle|[.]\/gradlew|dpkg-buildpackage|pnpm|npm|yarn|[^[:space:]]*build[.]sh)([[:space:]]|$)/ { compiles = 1 }
/^(RUN|ENV)[[:space:]]/ && /TARGETARCH|TARGETPLATFORM/ { targets = 1 }
END { flush() }
@@ -52,18 +74,16 @@ stages() {
# makes), so word-splitting find's output is safe.
for f in $(find "$REPO_ROOT"/packages -path '*/charts' -prune -o \( -name Dockerfile -o -name Containerfile \) -type f -print | sort); do
rel="${f#"$REPO_ROOT"/}"
neutral=0
case "$ARCH_NEUTRAL" in *"$rel"*) neutral=1 ;; esac
stages "$f" >"$stagelist"
while read -r compiles targets from; do
while read -r compiles targets native name from; do
[ "$compiles" = 1 ] || continue
checked=$((checked + 1))
case "$from" in
*'--platform=$BUILDPLATFORM'*|*'--platform=${BUILDPLATFORM}'*) ;;
*) failed="$failed
$rel: $from (not on BUILDPLATFORM)"; continue ;;
esac
if [ "$neutral" = 0 ] && [ "$targets" = 0 ]; then
if [ "$native" = 0 ]; then
failed="$failed
$rel: $from (not on BUILDPLATFORM)"
continue
fi
if [ "$targets" = 0 ] && [ "$(is_neutral "$rel" "$name" "$ARCH_NEUTRAL")" != 1 ]; then
failed="$failed
$rel: $from (compiles without TARGETARCH)"
fi
@@ -83,5 +103,22 @@ stages() {
printf '%s\n' 'FROM golang AS builder' 'RUN --mount=type=cache,target=/root/.cache go build .' >"$fixture"
out="$(stages "$fixture")"
rm -f "$fixture"
[ "$out" = "1 0 FROM golang AS builder" ]
[ "$out" = "1 0 0 builder FROM golang AS builder" ]
}
@test "a stage built FROM an earlier BUILDPLATFORM stage runs on the build platform too" {
fixture="$(mktemp)"
printf '%s\n' 'FROM --platform=$BUILDPLATFORM node AS source' 'RUN true' 'FROM source AS app' 'RUN npm ci' 'FROM node AS emulated' 'RUN npm ci' 'FROM emulated AS child' 'RUN go build .' >"$fixture"
out="$(stages "$fixture")"
rm -f "$fixture"
[ "$(printf '%s\n' "$out" | sed -n 2p)" = "1 0 1 app FROM source AS app" ]
[ "$(printf '%s\n' "$out" | sed -n 3p)" = "1 0 0 emulated FROM node AS emulated" ]
# A stage built from an emulated stage stays emulated.
[ "$(printf '%s\n' "$out" | sed -n 4p)" = "1 0 0 child FROM emulated AS child" ]
}
@test "an arch-neutral stage entry excuses only that stage" {
[ "$(is_neutral packages/x/Dockerfile app 'packages/x/Dockerfile:app')" = 1 ]
[ "$(is_neutral packages/x/Dockerfile ss 'packages/x/Dockerfile:app')" = 0 ]
[ "$(is_neutral packages/y/Dockerfile any 'packages/y/Dockerfile')" = 1 ]
}
+1 -1
View File
@@ -23,7 +23,7 @@
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
PROBE_PLATFORM="linux/riscv64"
PROBE_MARKER="--build-arg=BUILDX_ARGS_PROBE=1"
AMD64_ONLY="packages/core/talos packages/core/testing"
AMD64_ONLY="packages/core/testing"
@test "every package image target passes BUILDX_ARGS and PLATFORM to docker buildx" {
command -v make >/dev/null || { echo "make is required" >&2; exit 1; }
+141 -1
View File
@@ -25,7 +25,14 @@
# must turn this file red;
# * any OTHER kubectl failure exits non-zero, so a Forbidden or an apiserver
# error cannot be mistaken for the timeout case and silently proceed;
# * the selector names this release only.
# * the selector names this release only;
# * the release's KafkaNodePools are deleted after the topics, including on
# the timeout branch. The migration hook creates them outside the release
# manifest, and each owns the StrimziPodSet running its brokers, so a pool
# the hook skips keeps its brokers running after the release is gone;
# * a KafkaNodePool CRD that is absent exits 0, and any other failure of
# that delete exits non-zero, so the uninstall is retried rather than
# finishing with the pools still in place.
#
# Run via hack/cozytest.sh from the repo root (make bats-unit-tests); relative
# paths resolve against that cwd. That runner implements `@test` and little
@@ -59,11 +66,20 @@ render_hook() {
# CRD-absent strings are the two RESTMapper wordings.
# DELETE_RC deliberately has NO default: an unset value would make every
# failure-path test exit 0 and assert nothing. Each test sets it.
# The KafkaNodePool delete answers from POOL_OUT / POOL_RC instead, so a test
# can fail one call without failing the other. POOL_RC defaults to a clean
# delete, because the topic-path tests above are about the topic call; every
# pool-path test sets it explicitly.
# No line below is a bare column-0 `}`, so cozytest.sh's parser leaves it alone.
write_fake_kubectl() {
cat > "$1/kubectl" <<'KEOF'
#!/bin/sh
echo "$*" >> "$KLOG"
case "$*" in
*kafkanodepools*)
printf '%s\n' "${POOL_OUT:-kafkanodepool.kafka.strimzi.io \"b-0\" deleted}"
exit "${POOL_RC:-0}" ;;
esac
if [ -z "${DELETE_RC:-}" ]; then
echo "fake kubectl: DELETE_RC unset; the test would assert nothing" >&2
exit 64
@@ -206,3 +222,127 @@ KEOF
fi
rm -rf "$tmp"
}
@test "kafka pre-delete hook deletes this release's node pools after its topics" {
tmp=$(mktemp -d)
write_fake_kubectl "$tmp"
render_hook "$KAFKA_CHART" kafka-test tenant-test kafka-test-pre-delete "$tmp/hook.sh"
export KLOG="$tmp/calls"
export DELETE_RC=0
export DELETE_OUT='kafkatopic.kafka.strimzi.io "t" deleted'
export POOL_RC=0
export POOL_OUT='kafkanodepool.kafka.strimzi.io "b-0" deleted'
rc=0
PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
if [ "$rc" -ne 0 ]; then
echo "FAIL: hook exited $rc on a clean delete"
cat "$tmp/out" "$tmp/err"
false
fi
# Anchored like the topic selector above: the trailing space keeps the
# sibling release kafka-test2 out.
pool_line=$(grep -n -- "delete kafkanodepools.kafka.strimzi.io -l strimzi.io/cluster=kafka-test " "$tmp/calls" | cut -d: -f1)
topic_line=$(grep -n -- "delete kafkatopics.kafka.strimzi.io -l strimzi.io/cluster=kafka-test " "$tmp/calls" | cut -d: -f1)
if [ -z "$pool_line" ] || [ -z "$topic_line" ]; then
echo "FAIL: expected one release-scoped delete of each kind"
cat "$tmp/calls"
false
fi
# The topic operator clears topic finalizers through the brokers, so the
# brokers have to outlive the topic delete.
if [ "$pool_line" -le "$topic_line" ]; then
echo "FAIL: the node pools were deleted before the topics"
cat "$tmp/calls"
false
fi
if ! grep -q -- "kafkanodepools.* --wait=false" "$tmp/calls"; then
echo "FAIL: the node pool delete waits, which spends the Job's deadline"
cat "$tmp/calls"
false
fi
rm -rf "$tmp"
}
@test "kafka pre-delete hook still deletes the node pools after a topic wait timeout" {
# The timeout branch lets the uninstall finish. Skipping the pools there
# would leave the brokers running with nothing left to remove them.
tmp=$(mktemp -d)
write_fake_kubectl "$tmp"
render_hook "$KAFKA_CHART" kafka-test tenant-test kafka-test-pre-delete "$tmp/hook.sh"
export KLOG="$tmp/calls"
export DELETE_RC=1
export DELETE_OUT='error: timed out waiting for the condition on kafkatopics/kafka-test-orders'
export POOL_RC=0
export POOL_OUT='kafkanodepool.kafka.strimzi.io "b-0" deleted'
rc=0
PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
if [ "$rc" -ne 0 ]; then
echo "FAIL: hook exited $rc on a wait timeout"
cat "$tmp/out" "$tmp/err"
false
fi
if ! grep -q -- "delete kafkanodepools.kafka.strimzi.io -l strimzi.io/cluster=kafka-test " "$tmp/calls"; then
echo "FAIL: the node pools were skipped after the topic wait timed out"
cat "$tmp/calls"
false
fi
rm -rf "$tmp"
}
@test "kafka pre-delete hook exits 0 when the KafkaNodePool CRD is absent" {
for msg in 'error: the server doesn'"'"'t have a resource type "kafkanodepools"' \
'error: no matches for kind "KafkaNodePool" in version "kafka.strimzi.io/v1beta2"'; do
tmp=$(mktemp -d)
write_fake_kubectl "$tmp"
render_hook "$KAFKA_CHART" kafka-test tenant-test kafka-test-pre-delete "$tmp/hook.sh"
export KLOG="$tmp/calls"
export DELETE_RC=0
export DELETE_OUT='kafkatopic.kafka.strimzi.io "t" deleted'
export POOL_RC=1
export POOL_OUT="$msg"
rc=0
PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
if [ "$rc" -ne 0 ]; then
echo "FAIL: hook exited $rc on an absent KafkaNodePool CRD, wedging the uninstall"
cat "$tmp/out" "$tmp/err"
false
fi
if ! grep -q 'KafkaNodePool CRD absent' "$tmp/out"; then
echo "FAIL: the absent CRD was not reported as such"
cat "$tmp/out"
false
fi
rm -rf "$tmp"
done
}
@test "kafka pre-delete hook exits non-zero when the node pool delete fails" {
# Once the uninstall finishes, Helm never runs this hook for the release
# again, so a pool left here is left for good. A non-zero exit stops the
# uninstall before it deletes anything, and helm-controller retries it.
tmp=$(mktemp -d)
write_fake_kubectl "$tmp"
render_hook "$KAFKA_CHART" kafka-test tenant-test kafka-test-pre-delete "$tmp/hook.sh"
export KLOG="$tmp/calls"
export DELETE_RC=0
export DELETE_OUT='kafkatopic.kafka.strimzi.io "t" deleted'
export POOL_RC=1
export POOL_OUT='Error from server (Forbidden): kafkanodepools.kafka.strimzi.io is forbidden'
rc=0
PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
if [ "$rc" -eq 0 ]; then
echo "FAIL: hook exited 0 on a Forbidden, so the uninstall would finish and leave the pools"
cat "$tmp/out" "$tmp/err"
false
fi
if ! grep -q '^ERROR: deleting KafkaNodePools failed' "$tmp/err"; then
echo "FAIL: the failure was not reported as an error"
cat "$tmp/err"
false
fi
rm -rf "$tmp"
}
+1 -1
View File
@@ -97,7 +97,7 @@ image_ref_files() {
# 1. single string <repo>:<tag>@sha256:<digest> (e.g. .cozystackAPI.image)
# 2. split map {[registry,] repository, tag, digest} (e.g. .cilium.image)
# 3. split map {[registry,] repository, tag: <tag>@sha256:<digest>}
# (e.g. .linstorCSI.image; .keycloak-operator.image adds registry)
# (e.g. .linstorCSI.image)
# 4. chart-global global.registry.address + global.images.<n>.{repository, tag}
# (kube-ovn's wrapper chart)
# 5. OCI artifact {platformSourceUrl: oci://<repo>, platformSourceRef: digest=sha256:<digest>}
+3 -5
View File
@@ -159,11 +159,9 @@ else
# `<src>/<repo>@<digest>` ref continues past the host and is left to the first
# expression, so the two cannot both fire on one reference.
#
# It deliberately does NOT fix keycloak-operator, whose host splits at a
# different boundary (`registry: iad.ocir.io` + `repository:
# idyksih5sir9/cozystack/<name>`): neither key holds SRC_REGISTRY as a whole,
# so rewriting it needs the structure-aware pass docs/agents/image-refs.md
# describes. That gap stays dormant while no CI path rebuilds the package.
# Neither expression reaches a host split at any other boundary (`registry:
# iad.ocir.io` + `repository: idyksih5sir9/cozystack/<name>`), where no key
# holds SRC_REGISTRY as a whole, so no package may stamp that layout.
# In-place edit written portably: `sed -i` takes the next argument as the
# backup suffix on BSD but as a flag on GNU, so `sed -i -e` diverges between
# them. Route the output through a temp file and copy it back over the original
+72 -9
View File
@@ -77,7 +77,7 @@ _make_tree() {
printf ' tag: main@sha256:%s\n' "$D"
} > "$t/system/third/values.yaml"
# shapes 2 and 3 with the host split into a sibling `registry` key instead of
# living inside `repository` — the layout keycloak-operator ships. Rejoining
# living inside `repository` — the layout keycloak-operator shipped. Rejoining
# the two is what keeps the ref recognisable: emitting the bare `repository`
# yields a host-less ref that the SRC_REGISTRY filter discards as third-party,
# the same silent drop shape 3 exists to fix. Worse here than in promote-retag,
@@ -259,11 +259,9 @@ _stub_registry() {
#
# Scope, so the name does not overclaim: this pins WHICH FILES the rewrite
# visits, not that every ref inside them is successfully rewritten. The sed
# itself only runs outside --dry-run (it needs skopeo), so it still cannot
# rewrite a host split into a sibling `registry:` key (keycloak-operator).
# That is a known gap recorded in docs/agents/image-refs.md, not something
# this test covers; the whole-value shape (kubeovn) is covered by the
# dedicated test below.
# itself only runs outside --dry-run (it needs skopeo); the whole-value
# shape (kubeovn) and keycloak-operator's empty `registry` are covered by
# the dedicated tests below.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
_make_tree "$tmp/tree"
@@ -284,9 +282,7 @@ _stub_registry() {
# image was built in cozystack/kubeovn-chart; live once it is built here.
#
# Needs a skopeo stub because the sed only runs outside --dry-run. Scope: this
# covers the whole-value shape only. keycloak-operator splits the host at a
# different boundary (`registry: iad.ocir.io`), where no single key holds
# SRC_REGISTRY, and is still unfixed — see docs/agents/image-refs.md.
# covers the whole-value shape only.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
_make_tree "$tmp/tree"
@@ -328,3 +324,70 @@ _stub_registry() {
grep -q "expected 'sha256:$D'" "$tmp/err"
rm -rf "$tmp"
}
@test "the host rewrite reaches a host kept in repository beside an empty registry" {
# keycloak-operator's layout: the upstream chart joins `registry` and
# `repository`, so the package sets `registry: ""` and keeps the whole host in
# `repository`, where the literal "<src>/" replace can reach it. The image
# must be both mirrored and rewritten, or the published tree points at an
# image the dest registry does not have or at the private CI registry.
tmp=$(mktemp -d)
_make_tree "$tmp/tree"
mkdir -p "$tmp/tree/system/emptyreg"
{
echo 'image:'
echo ' registry: ""'
echo ' repository: iad.ocir.io/idyksih5sir9/cozystack/emptyreg'
printf ' tag: main@sha256:%s\n' "$D"
} > "$tmp/tree/system/emptyreg/values.yaml"
_stub_registry "$tmp/bin"
hack/nightly-mirror.sh 0.0.0-nightly.test "$tmp/tree" --dry-run >"$tmp/out"
grep -q 'docker://ghcr.io/cozystack/cozystack/emptyreg:0.0.0-nightly.test' "$tmp/out"
PATH="$tmp/bin:$PATH" hack/nightly-mirror.sh 0.0.0-nightly.test "$tmp/tree"
grep -q '^ repository: ghcr.io/cozystack/cozystack/emptyreg$' "$tmp/tree/system/emptyreg/values.yaml"
grep -q '^ registry: ""$' "$tmp/tree/system/emptyreg/values.yaml"
rm -rf "$tmp"
}
@test "keycloak-operator's stamped image reaches the nightly mirror and its host rewrite" {
# Runs the package's real `image:` recipe against the CI build registry and
# then the mirror over the result, so a regression in either the values
# layout or the Makefile stamp fails here. The chart joins `registry` and
# `repository`; if the host moves back into `registry`, or the recipe stops
# stamping `repository`, the stamped ref no longer carries the build host
# contiguously and is either never mirrored or left pointing at it.
D="$(printf 'c%.0s' $(seq 1 64))"
tmp=$(mktemp -d)
mkdir -p "$tmp/repo/packages/system" "$tmp/bin"
cp -R packages/system/keycloak-operator "$tmp/repo/packages/system/"
ln -s "$PWD/hack" "$tmp/repo/hack"
# docker stub: only writes the buildx metadata file the recipe reads.
{
echo '#!/bin/sh'
echo 'while [ $# -gt 0 ]; do'
echo ' if [ "$1" = --metadata-file ]; then'
printf ' printf %s > "$2"\n' "'{\"containerimage.digest\":\"sha256:$D\"}'"
echo ' fi'
echo ' shift'
echo 'done'
} > "$tmp/bin/docker"
chmod +x "$tmp/bin/docker"
_stub_registry "$tmp/bin"
PATH="$tmp/bin:$PATH" make --no-print-directory -s -C "$tmp/repo/packages/system/keycloak-operator" image \
REGISTRY=iad.ocir.io/idyksih5sir9/cozystack IMAGE_TAG=main COZYSTACK_VERSION=0.0.0 >/dev/null
hack/nightly-mirror.sh 0.0.0-nightly.test "$tmp/repo/packages" --dry-run >"$tmp/out"
grep -q 'docker://ghcr.io/cozystack/cozystack/keycloak-operator:0.0.0-nightly.test' "$tmp/out"
PATH="$tmp/bin:$PATH" hack/nightly-mirror.sh 0.0.0-nightly.test "$tmp/repo/packages"
if grep -q 'iad.ocir.io' "$tmp/repo/packages/system/keycloak-operator/values.yaml"; then
echo "the build registry host survived the nightly rewrite" >&2
false
fi
rm -rf "$tmp"
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
for makefile in ./packages/*/*/Makefile; do
[ -f "$makefile" ] || continue
if awk '/^generate[[:space:]]*:/ { found=1 } END { exit !found }' "$makefile"; then
dir=${makefile%/Makefile}
echo "Running make generate in $dir"
make generate -C "$dir"
fi
done
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bats
# cozytest.sh provides no bats lifecycle or result helpers, so each test owns
# its fixture and uses ordinary shell assertions.
add_generator() {
root="$1"
dir="$2"
mkdir -p "$root/$dir"
printf 'generate:\n\t@printf "%%s\\n" "%s" >> "$${GEN_LOG}"\n' "$dir" > "$root/$dir/Makefile"
}
@test "discovers package generators across categories" {
script="$PWD/hack/pre-commit-generate-packages.sh"
root=$(mktemp -d)
add_generator "$root" packages/apps/app
add_generator "$root" packages/extra/extra
add_generator "$root" packages/library/library
add_generator "$root" packages/system/system
mkdir -p "$root/packages/system/no-generator"
printf '# generate:\n.PHONY: generate\nall:\n\t@:\n' > "$root/packages/system/no-generator/Makefile"
mkdir -p "$root/packages/system/system/charts/vendor"
printf 'generate:\n\t@printf "vendored\\n" >> "$${GEN_LOG}"\n' > "$root/packages/system/system/charts/vendor/Makefile"
(cd "$root" && GEN_LOG="$root/generators.log" "$script")
expected=$(printf '%s\n' packages/apps/app packages/extra/extra packages/library/library packages/system/system)
actual=$(sort "$root/generators.log")
[ "$actual" = "$expected" ]
rm -rf "$root"
}
@test "stops after a package generator fails" {
script="$PWD/hack/pre-commit-generate-packages.sh"
root=$(mktemp -d)
mkdir -p "$root/packages/apps/failing" "$root/packages/system/later"
printf 'generate:\n\t@false\n' > "$root/packages/apps/failing/Makefile"
printf 'generate:\n\t@touch "$${LATER_MARKER}"\n' > "$root/packages/system/later/Makefile"
rc=0
(cd "$root" && LATER_MARKER="$root/later-ran" "$script") || rc=$?
[ "$rc" -ne 0 ]
[ ! -e "$root/later-ran" ]
rm -rf "$root"
}
+6 -8
View File
@@ -131,14 +131,12 @@ EOF
grep -q "docker://ghcr.io/cozystack/cozystack/${owned}@sha256:" "$tmp/out"
done
# Images whose host is NOT inside `repository` must be selected too. Both are
# built and pushed to $REGISTRY by cozystack, both carry the digest in `tag`,
# and both were dropped by the ownership filter for looking host-less — so
# neither has ever received a 1.x release tag (on GHCR keycloak-operator has
# only `latest`, and kubeovn's newest cozystack-versioned tag predates 1.0).
# keycloak-operator splits the host into a sibling `registry` key; kubeovn
# keeps it in the document-level global.registry.address, written by the
# cozystack/kubeovn-chart wrapper's own `make image`.
# Both images are built and pushed to $REGISTRY by cozystack and carry the
# digest in `tag`, and neither has ever received a 1.x release tag (on GHCR
# keycloak-operator has only `latest`, and kubeovn's newest cozystack-versioned
# tag predates 1.0), so they get one only if the ownership filter counts them
# as ours. kubeovn keeps its host in global.registry.address, apart from
# `repository`.
for owned in keycloak-operator kubeovn; do
grep -q "docker://ghcr.io/cozystack/cozystack/${owned}@sha256:" "$tmp/out"
done
+3 -5
View File
@@ -38,11 +38,9 @@
# --exclude='*.md' keeps documentation examples (which carry placeholder or
# historical versions) out of the fixture, matching the postcondition.
# Match 'cozystack/cozystack' WITHOUT a host prefix or trailing slash. The
# host is not always contiguous with the repository: keycloak-operator splits
# it into a sibling `registry: ghcr.io` + `repository:
# cozystack/cozystack/keycloak-operator`, and kubeovn puts it in
# host is not always contiguous with the repository: kubeovn puts it in
# `global.registry.address: ghcr.io/cozystack/cozystack` with `repository:
# kubeovn`. A 'ghcr\.io/cozystack/cozystack/' pattern silently skips both
# kubeovn`. A 'ghcr\.io/cozystack/cozystack/' pattern silently skips such
# files, shrinking the fixture and under-testing the split shapes.
for f in $(grep -rIl --exclude-dir=charts --exclude='*.md' 'cozystack/cozystack' packages/); do
mkdir -p "$tmp/$(dirname "$f")"
@@ -261,7 +259,7 @@
# Parsing every YAML under packages/ costs ~30s; this is the same file set
# for the purpose of finding FIRST-PARTY refs (canon discards everything
# else anyway) and runs in about a second. The marker covers the split forms
# too, so keycloak-operator and kubeovn are not filtered out.
# too, so kubeovn is not filtered out.
grep -rIlE --exclude-dir=charts --exclude='*.md' \
-e 'cozystack/cozystack' packages/ \
| grep -E '\.(yaml|yml|tag)$' \
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bats
# packages/core/talos/hack/installer-index.sh ties the per-arch installer
# manifests skopeo left in an OCI layout into the index the talos image is
# pushed as. A wrong digest or size makes that push fail; a wrong or missing
# platform makes a node pull the other arch's installer.
#
# Written for POSIX sh: hack/cozytest.sh sources this file into /bin/sh.
#
# Requires: jq, sha256sum.
@test "installer-index.sh indexes each arch's manifest under its platform" {
layout="$(mktemp -d)"
mkdir -p "$layout/blobs/sha256"
cat > "$layout/index.json" <<'EOF'
{"schemaVersion":2,"manifests":[
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:aa","size":10,"annotations":{"org.opencontainers.image.ref.name":"amd64"}},
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:bb","size":20,"annotations":{"org.opencontainers.image.ref.name":"arm64"}}]}
EOF
packages/core/talos/hack/installer-index.sh "$layout" amd64 arm64
desc=$(jq -c '.manifests[] | select(.annotations["org.opencontainers.image.ref.name"] == "index")' "$layout/index.json")
digest=$(printf '%s' "$desc" | jq -r .digest)
blob="$layout/blobs/sha256/${digest#sha256:}"
[ "$(sha256sum "$blob" | cut -d' ' -f1)" = "${digest#sha256:}" ]
[ "$(printf '%s' "$desc" | jq .size)" -eq "$(wc -c < "$blob" | tr -d ' ')" ]
[ "$(jq -c '[.manifests[] | [.digest, .platform.os, .platform.architecture]]' "$blob")" = '[["sha256:aa","linux","amd64"],["sha256:bb","linux","arm64"]]' ]
rm -rf "$layout"
}
@test "installer-index.sh refuses an arch the layout has no manifest for" {
layout="$(mktemp -d)"
mkdir -p "$layout/blobs/sha256"
echo '{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:aa","size":10,"annotations":{"org.opencontainers.image.ref.name":"amd64"}}]}' > "$layout/index.json"
if packages/core/talos/hack/installer-index.sh "$layout" amd64 arm64 2>/dev/null; then
echo "FAIL: indexed an arch with no manifest"; rm -rf "$layout"; false
fi
rm -rf "$layout"
}
+27
View File
@@ -0,0 +1,27 @@
# Render fixtures
`hack/check-render-matrix.sh` renders every app chart with its defaults plus these injected values. helm-unittest renders the templates selected by each suite, so a chart with passing unit tests can still have an untested template that fails to render. This sweep checks that Helm accepts the rendered YAML; it does not validate Kubernetes schemas or runtime behavior.
The source of `_cluster` is `packages/core/platform/templates/apps.yaml`; `_namespace` comes from `packages/apps/tenant/templates/namespace.yaml`. These fixtures model selected install states, not a complete copy of every platform setting. Top-level scalar values in these maps are strings, including booleans. Charts such as tenant compare `oidc-enabled` with `"true"`, so a YAML boolean has the wrong type.
`scheduling` and `branding` are maps copied with `toYaml`, which preserves their nested types. The scheduling keys read by app charts have a more specific contract: `globalAppTopologySpreadConstraints` is a string containing YAML (or an empty string), and `dedicatedNodesForWindowsVMs` is compared with the string `"true"`. The configured and wildcard fixtures supply a topology constraint; fresh leaves it empty. The embedded YAML retains numeric fields such as `maxSkew`.
`_namespace.<service>` contains the namespace providing the shared service, or an empty string when unavailable. A non-empty value enables the service-dependent templates in the Kubernetes chart. These states use ingress rather than Gateway API, so `gateway` stays empty: a non-empty gateway would disable Harbor Ingress and leave the certificate modes untested.
| File | State |
|---|---|
| `fresh.yaml` | OIDC off, no certificates or shared services, no topology constraint |
| `configured.yaml` | OIDC on, per-host ACME via http01, shared services and topology constraint |
| `wildcard.yaml` | OIDC on, dns01 with a platform-issued wildcard certificate, shared services and topology constraint |
Each chart is attempted under every fixture. `vm-instance` and `kubernetes-nodes` require a `VirtualMachineClusterInstancetype` lookup on their defaults. The sweep accepts their skip only while every fixture fails with the recorded lookup diagnostic; a successful render or a different error fails the sweep. For kubernetes-nodes it supplies a parent cluster and matching release name first. For tenant it declares the Keycloak API with `--api-versions v1.edp.epam.com/v1`, allowing OIDC-enabled fixtures to render the realm groups.
`lookup` returns an empty result during this check. Tests for existing objects belong in chart-specific helm-unittest suites using `kubernetesProvider`; `packages/apps/clickhouse/tests/backup_api_password_persistence_test.yaml` covers both password generation and preservation that way. Presets, replica counts and other app feature toggles are outside this sweep.
The BATS suite checks fixture types with the small Helm chart in `validator/`, using `--set-file` so explicit nulls are not removed by Helm's values merge. Separate assertions check PostgreSQL topology constraints, tenant OIDC groups, the Kubernetes metrics-server HelmRelease and Harbor per-host ACME versus wildcard TLS. Pairwise comparison of complete tenant renders detects duplicate fixture states, and a second render of each state checks that randomness cannot satisfy that comparison. The branch assertions are needed because tenant also copies injected values into a Secret, so different output alone does not prove a conditional resource was rendered.
When adding an injected value read by an app, update the relevant states and add an assertion for the branch it exercises. Review all read forms, including `index`, `get` and `dig`, before deciding a value is unused:
```sh
grep -rn -e '_cluster' -e '_namespace' packages/apps/*/templates/
```
+30
View File
@@ -0,0 +1,30 @@
# OIDC on, per-host ACME through http01, services exposed. The other side of the
# branches fresh.yaml leaves off.
_cluster:
root-host: "example.org"
bundle-name: "paas-full"
solver: "http01"
issuer-name: "letsencrypt-prod"
wildcard-secret-name: ""
wildcard-issue: "false"
oidc-enabled: "true"
oidc-insecure-skip-verify: "false"
keycloak-internal-url: "https://keycloak.cozy-keycloak.svc.cluster.local"
extra-keycloak-redirect-uri-for-dashboard: ""
expose-ingress: "tenant-root"
expose-services: "tenant-root"
cluster-domain: "cluster.local"
scheduling:
dedicatedNodesForWindowsVMs: "false"
globalAppTopologySpreadConstraints: |
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
_namespace:
etcd: "tenant-test"
ingress: "tenant-test"
gateway: ""
monitoring: "tenant-test"
seaweedfs: "tenant-test"
host: "example.org"
+25
View File
@@ -0,0 +1,25 @@
# A cluster right after install: nothing configured yet.
_cluster:
root-host: "example.org"
bundle-name: "paas-full"
solver: ""
issuer-name: ""
wildcard-secret-name: ""
wildcard-issue: "false"
oidc-enabled: "false"
oidc-insecure-skip-verify: "false"
keycloak-internal-url: ""
extra-keycloak-redirect-uri-for-dashboard: ""
expose-ingress: ""
expose-services: ""
cluster-domain: "cluster.local"
scheduling:
dedicatedNodesForWindowsVMs: "false"
globalAppTopologySpreadConstraints: ""
_namespace:
etcd: ""
ingress: ""
gateway: ""
monitoring: ""
seaweedfs: ""
host: "example.org"
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v2
name: render-fixture-validator
version: 0.0.0
type: application
@@ -0,0 +1,32 @@
{{- /* Parse raw fixture YAML: merging values would silently remove null keys. */ -}}
{{- $fixture := .Values.fixture | fromYaml -}}
{{- if hasKey $fixture "Error" -}}
{{- fail (get $fixture "Error") -}}
{{- end -}}
{{- range $section := list "_cluster" "_namespace" -}}
{{- $values := get $fixture $section -}}
{{- if or (not (kindIs "map" $values)) (empty $values) -}}
{{- fail (printf "%s must be a non-empty map" $section) -}}
{{- end -}}
{{- range $key, $value := $values -}}
{{- if and (eq $section "_cluster") (has $key (list "scheduling" "branding")) -}}
{{- if not (kindIs "map" $value) -}}
{{- fail (printf "%s.%s must be a map" $section $key) -}}
{{- end -}}
{{- else if not (kindIs "string" $value) -}}
{{- fail (printf "%s.%s must be a string" $section $key) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $scheduling := get $fixture._cluster "scheduling" -}}
{{- if not (kindIs "map" $scheduling) -}}
{{- fail "_cluster.scheduling must be a map" -}}
{{- end -}}
{{- range $key := list "globalAppTopologySpreadConstraints" "dedicatedNodesForWindowsVMs" -}}
{{- if not (hasKey $scheduling $key) -}}
{{- fail (printf "_cluster.scheduling.%s is missing" $key) -}}
{{- end -}}
{{- if not (kindIs "string" (get $scheduling $key)) -}}
{{- fail (printf "_cluster.scheduling.%s must be a string" $key) -}}
{{- end -}}
{{- end -}}
+30
View File
@@ -0,0 +1,30 @@
# dns01 with a platform-issued wildcard certificate: the path where charts stop
# doing per-host ACME and use the ingress controller default certificate.
_cluster:
root-host: "example.org"
bundle-name: "paas-full"
solver: "dns01"
issuer-name: "letsencrypt-prod"
wildcard-secret-name: "wildcard-tls"
wildcard-issue: "true"
oidc-enabled: "true"
oidc-insecure-skip-verify: "false"
keycloak-internal-url: "https://keycloak.cozy-keycloak.svc.cluster.local"
extra-keycloak-redirect-uri-for-dashboard: ""
expose-ingress: "tenant-root"
expose-services: "tenant-root"
cluster-domain: "cluster.local"
scheduling:
dedicatedNodesForWindowsVMs: "false"
globalAppTopologySpreadConstraints: |
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
_namespace:
etcd: "tenant-test"
ingress: "tenant-test"
gateway: ""
monitoring: "tenant-test"
seaweedfs: "tenant-test"
host: "example.org"
+7 -5
View File
@@ -7,11 +7,13 @@ gh release upload --clobber $version _out/assets/cozystack-crds.yaml
gh release upload --clobber $version _out/assets/cozystack-operator-talos.yaml
gh release upload --clobber $version _out/assets/cozystack-operator-generic.yaml
gh release upload --clobber $version _out/assets/cozystack-operator-hosted.yaml
gh release upload --clobber $version _out/assets/metal-amd64.iso
gh release upload --clobber $version _out/assets/metal-amd64.raw.xz
gh release upload --clobber $version _out/assets/nocloud-amd64.raw.xz
gh release upload --clobber $version _out/assets/kernel-amd64
gh release upload --clobber $version _out/assets/initramfs-metal-amd64.xz
for arch in amd64 arm64; do
gh release upload --clobber $version _out/assets/metal-$arch.iso
gh release upload --clobber $version _out/assets/metal-$arch.raw.xz
gh release upload --clobber $version _out/assets/nocloud-$arch.raw.xz
gh release upload --clobber $version _out/assets/kernel-$arch
gh release upload --clobber $version _out/assets/initramfs-metal-$arch.xz
done
gh release upload --clobber $version _out/assets/cozypkg-*.tar.gz
gh release upload --clobber $version _out/assets/cozypkg-checksums.txt
gh release upload --clobber $version _out/assets/openapi.json
@@ -0,0 +1,68 @@
suite: clickhouse-backup API password persistence across reconciles
# The chart mints the backup API password once and then preserves it, by looking
# up its own previously-rendered Secret. Both halves matter and they are
# different code paths:
# - no existing Secret -> generate a fresh password;
# - existing Secret -> reuse it, because rotating on every re-render leaves
# a window where the sidecar and the strategy Pod
# disagree and every backup 401s.
#
# The second path is reachable here only because helm-unittest can fake the API
# the chart looks up (kubernetesProvider). Without that, `lookup` returns nothing
# outside a cluster, only the generate path renders, and the preserve path -- the
# one with the failure mode -- is covered by nothing short of a live upgrade.
#
# This is the pattern, not the exception: the same lookup-preserve shape guards
# an immutable PVC storageClass in harbor and the adopt-in-place migrations in
# seaweedfs. Anywhere a chart reads existing cluster state to decide what to
# render, this is how the other branch gets tested.
templates:
- templates/backup-api-auth-secret.yaml
release:
name: ch-test
namespace: tenant-test
set:
_cluster: {}
backup:
enabled: true
tests:
- it: "mints a password when none exists yet"
asserts:
- equal:
path: stringData.username
value: backup
- exists:
path: stringData.password
# Not asserting the value: it is random by design. Asserting its length
# would pin randAlphaNum's argument, which is not the contract.
- notEqual:
path: stringData.password
value: ""
- it: "preserves the existing password rather than rotating it"
kubernetesProvider:
scheme:
"v1/Secret":
gvr:
version: "v1"
resource: "secrets"
namespaced: true
objects:
- kind: Secret
apiVersion: v1
metadata:
name: ch-test-backup-api-auth
namespace: tenant-test
data:
# base64 of "backup" and "preserved-password"
username: YmFja3Vw
password: cHJlc2VydmVkLXBhc3N3b3Jk
asserts:
- equal:
path: stringData.password
value: preserved-password
+1 -1
View File
@@ -1,4 +1,4 @@
{{- $host := .Values._namespace.host }}
{{- $host := include "cozy-lib.ns-host-required" . }}
{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
@@ -1,5 +1,5 @@
{{- $gateway := .Values._namespace.gateway | default "" }}
{{- $host := .Values._namespace.host }}
{{- $host := include "cozy-lib.ns-host-required" . }}
{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }}
{{- if $gateway }}
apiVersion: gateway.networking.k8s.io/v1
+1 -1
View File
@@ -1,6 +1,6 @@
{{- $ingress := .Values._namespace.ingress }}
{{- $gateway := .Values._namespace.gateway | default "" }}
{{- $host := .Values._namespace.host }}
{{- $host := include "cozy-lib.ns-host-required" . }}
{{- $harborHost := .Values.host | default (printf "%s.%s" .Release.Name $host) }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
@@ -0,0 +1,39 @@
suite: harbor CR (harbor.yaml) guards an empty apex in isolation
templates:
- templates/harbor.yaml
release:
name: harbor
namespace: tenant-root
# Isolated to templates/harbor.yaml so reverting its own guard reddens this
# suite even though sibling templates guard the same apex.
tests:
- it: fails when _namespace is injected with an empty host and no host override
set:
_namespace:
host: ""
ingress: tenant-root
_cluster:
issuer-name: letsencrypt-prod
solver: http01
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: renders the credentials Secret URL from the apex when it is set
set:
_namespace:
host: example.org
ingress: tenant-root
_cluster:
issuer-name: letsencrypt-prod
solver: http01
documentSelector:
path: kind
value: Secret
asserts:
- equal:
path: stringData.url
value: https://harbor.example.org
@@ -0,0 +1,29 @@
suite: harbor HTTPRoute guards an empty apex in isolation
templates:
- templates/httproute.yaml
release:
name: harbor
namespace: tenant-root
tests:
- it: fails when _namespace is injected with an empty host and no host override
set:
_namespace:
host: ""
ingress: tenant-root
gateway: tenant-root
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: renders the HTTPRoute hostname from the apex when it is set
set:
_namespace:
host: example.org
ingress: tenant-root
gateway: tenant-root
asserts:
- equal:
path: spec.hostnames[0]
value: harbor.example.org
@@ -0,0 +1,55 @@
suite: harbor fails to render its ingress when the tenant apex is empty
templates:
- templates/ingress.yaml
release:
name: harbor
namespace: tenant-root
# harbor composes its public hostname as <release>.<_namespace.host>. When the
# tenant apex is empty and no per-app host override is set, that used to emit a
# malformed "harbor." host silently. cozy-lib.ns-host-required now fails the
# render instead. Rendering with a valid apex must still succeed.
tests:
- it: fails when _namespace.host is empty and no host override is set
set:
_namespace:
host: ""
ingress: tenant-root
gateway: ""
_cluster:
issuer-name: letsencrypt-prod
solver: http01
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: renders when a host override is set even though the apex is empty
set:
host: harbor.custom.example
_namespace:
host: ""
ingress: tenant-root
gateway: ""
_cluster:
issuer-name: letsencrypt-prod
solver: http01
asserts:
- equal:
path: spec.rules[0].host
value: harbor.custom.example
- it: renders normally when the apex is set
set:
_namespace:
host: example.org
ingress: tenant-root
gateway: ""
_cluster:
issuer-name: letsencrypt-prod
solver: http01
asserts:
- equal:
path: spec.rules[0].host
value: harbor.example.org
@@ -96,6 +96,36 @@ spec:
else
printf '%s\n' "${out}"
fi
# The migration hook creates the node pools with kubectl apply, so
# they are not in the release manifest and the uninstall does not
# remove them, and each pool owns the StrimziPodSet that runs its
# brokers. After the topics, because the topic operator clears
# their finalizers through those brokers; also after the timeout
# branch above, since this is the last chance to remove them.
# --wait=false: the pods go by garbage collection, and waiting
# would spend the headroom the deadline above leaves. No request
# timeout either, for the reason given on the topic call.
echo "Deleting KafkaNodePools for {{ .Release.Name }}"
if ! out=$(kubectl -n {{ .Release.Namespace }} delete kafkanodepools.kafka.strimzi.io \
-l 'strimzi.io/cluster={{ .Release.Name }}' \
--ignore-not-found=true \
--wait=false 2>&1); then
printf '%s\n' "${out}"
case "${out}" in
*"the server doesn't have a resource type"*|*"no matches for kind"*)
echo "KafkaNodePool CRD absent; nothing to delete" ;;
*)
# Non-zero, unlike the topic timeout: once the uninstall
# completes, Helm never runs this hook for the release
# again, so a pool left now keeps its brokers for good.
# Failing stops the uninstall before it deletes anything,
# and helm-controller retries it.
echo "ERROR: deleting KafkaNodePools failed: ${out}" >&2; exit 1 ;;
esac
else
printf '%s\n' "${out}"
fi
volumeMounts:
- name: tmp
mountPath: /tmp
@@ -133,6 +163,9 @@ rules:
- apiGroups: ["kafka.strimzi.io"]
resources: ["kafkatopics"]
verbs: ["get", "list", "watch", "delete"]
- apiGroups: ["kafka.strimzi.io"]
resources: ["kafkanodepools"]
verbs: ["list", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
@@ -127,6 +127,36 @@ tests:
value: kafka-test-pre-delete
documentIndex: 3
- it: deletes release-scoped KafkaNodePools after the KafkaTopics
asserts:
# The migration hook creates the pools with kubectl apply, so they are
# not in the release manifest and helm uninstall never removes them. Each
# pool owns its StrimziPodSet, so a pool left behind keeps its brokers
# running after the release is gone.
#
# One pattern pins three things at once: the pools go after the topics
# (the topic operator needs live brokers to clear its finalizers), the
# selector names this release only (both quotes are part of it, as in the
# topic pin above), and the call does not wait, so the deadline
# arithmetic on the Job is unchanged.
- matchRegex:
path: spec.template.spec.containers[0].command[2]
pattern: "(?s)delete kafkatopics\\.kafka\\.strimzi\\.io .*delete kafkanodepools\\.kafka\\.strimzi\\.io \\\\\\s+-l 'strimzi\\.io/cluster=kafka-test' \\\\\\s+--ignore-not-found=true \\\\\\s+--wait=false"
documentIndex: 0
# list and delete are what a label-selected delete issues; nothing here
# waits on or reads a single pool.
- contains:
path: rules
content:
apiGroups: ["kafka.strimzi.io"]
resources: ["kafkanodepools"]
verbs: ["list", "delete"]
documentIndex: 2
- lengthEqual:
path: rules
count: 2
documentIndex: 2
- it: prefixes the hook image with the cluster images registry when set
set:
_cluster:
@@ -84,7 +84,6 @@
{{- $podCIDR := "10.243.0.0/16" }}
{{- $serviceCIDR := "10.95.0.0/16" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- if not $etcd }}
---
apiVersion: v1
@@ -775,7 +774,7 @@ spec:
ingress:
extraAnnotations:
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
hostname: {{ .Values.host | default (printf "%s.%s" .Release.Name $host) }}:443
hostname: {{ .Values.host | default (printf "%s.%s" .Release.Name (include "cozy-lib.ns-host-required" .)) }}:443
className: "{{ $ingress }}"
additionalServicePorts:
- name: talos-trustd
@@ -0,0 +1,41 @@
suite: kubernetes apiserver endpoint guards an empty tenant apex only when it is emitted
templates:
- templates/cluster.yaml
release:
name: kubernetes
namespace: tenant-test
# cluster.yaml advertises the apiserver endpoint as <release>.<_namespace.host>
# only inside the KamajiControlPlane, which is emitted when the tenant has an
# etcd DataStore. cozy-lib.ns-host-required guards that one hostname, so an empty
# apex fails there. The no-etcd path emits only a ConfigMap beacon and never
# composes the hostname, so it must keep rendering even with an empty apex (the
# parent Tenant chart populates _namespace.etcd asynchronously).
tests:
- it: fails when the tenant has etcd but the apex is empty and no host override
set:
_namespace:
etcd: tenant-test
host: ""
_cluster:
cluster-domain: cozy.local
version: "v1.35"
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: still renders the awaiting-etcd beacon when the apex is empty and there is no etcd
set:
_namespace:
etcd: ""
host: ""
_cluster:
cluster-domain: cozy.local
version: "v1.35"
asserts:
- hasDocuments:
count: 1
- isKind:
of: ConfigMap
@@ -14,6 +14,7 @@ release:
set:
_namespace:
etcd: etcd-test
host: example.org
_cluster:
cluster-domain: cozy.local
version: "v1.35"
@@ -17,6 +17,7 @@ release:
set:
_namespace:
etcd: etcd-test
host: example.org
_cluster:
cluster-domain: cozy.local
version: "v1.35"
@@ -3,6 +3,6 @@ _namespace:
monitoring: ""
ingress: ""
seaweedfs: ""
host: ""
host: "example.org"
_cluster:
cluster-domain: cozy.local
+1 -1
View File
@@ -71,7 +71,7 @@ spec:
# <release>-ca, which holds the CA private key next to ca.crt.
certificates:
serverAltDNSNames:
- {{ printf "%s.%s" .Release.Name .Values._namespace.host }}
- {{ printf "%s.%s" .Release.Name (include "cozy-lib.ns-host-required" .) }}
{{- end }}
{{/*
s3Credentials points at the operator-supplied Secret when
@@ -0,0 +1,63 @@
suite: postgres external cert fails on an empty apex, but only when external
templates:
- templates/db.yaml
release:
name: pg-test
namespace: tenant-test
# db.yaml appends the external SAN <release>.<_namespace.host> only under
# `if and $tlsEnabled .Values.external`. cozy-lib.ns-host-required guards that
# line, so an empty apex fails the render when TLS and external are on, and is a
# non-issue when external is off.
tests:
- it: fails when tls and external are on and the apex is empty
set:
external: true
tls:
enabled: true
_namespace:
host: ""
_cluster:
scheduling: {}
cluster-domain: cozy.local
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: still renders when external is off even though the apex is empty
set:
external: false
tls:
enabled: true
_namespace:
host: ""
_cluster:
scheduling: {}
cluster-domain: cozy.local
documentSelector:
path: kind
value: Cluster
asserts:
- isNull:
path: spec.certificates
- it: renders the external cert SAN from the apex when it is set
set:
external: true
tls:
enabled: true
_namespace:
host: example.org
_cluster:
scheduling: {}
cluster-domain: cozy.local
documentSelector:
path: kind
value: Cluster
asserts:
- equal:
path: spec.certificates.serverAltDNSNames[0]
value: pg-test.example.org
@@ -78,6 +78,6 @@ spec:
- {{ printf "%s-%d.%s-headless.%s.svc.%s" $.Release.Name $i $.Release.Name $.Release.Namespace $clusterDomain | quote }}
{{- end }}
{{- if .Values.external }}
- {{ .Release.Name }}.{{ .Values._namespace.host }}
- {{ .Release.Name }}.{{ include "cozy-lib.ns-host-required" . }}
{{- end }}
{{- end }}
@@ -0,0 +1,46 @@
suite: qdrant external cert fails on an empty apex, but only when external
templates:
- templates/certmanager.yaml
# The external SAN test-qdrant.<_namespace.host> is emitted only under
# `if .Values.external`. cozy-lib.ns-host-required guards that one line, so an
# empty apex fails the render when external is on, and is a non-issue when it is
# off (a tenant with no apex and no external exposure must still render).
tests:
- it: fails when external is true and the apex is empty
release:
name: test-qdrant
namespace: tenant-test
set:
tls:
enabled: true
external: true
_namespace:
host: ""
_cluster:
cluster-domain: cozy.local
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: still renders when external is false even though the apex is empty
release:
name: test-qdrant
namespace: tenant-test
set:
tls:
enabled: true
external: false
_namespace:
host: ""
_cluster:
cluster-domain: cozy.local
asserts:
- hasDocuments:
count: 4
- notContains:
path: spec.dnsNames
content: test-qdrant.
documentIndex: 3
@@ -75,7 +75,7 @@ spec:
{{- end }}
exporter:
enabled: true
image: oliver006/redis_exporter:v1.55.0-alpine
image: oliver006/redis_exporter:v1.55.0@sha256:d82625bda93823fb40de881ffbb45b65aca772a39c30141387f92314c258da5d
args:
- --web.telemetry-path
- /metrics
+12 -12
View File
@@ -74,18 +74,18 @@ tenant-u1
### Common parameters
| Name | Description | Type | Value |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------- |
| `host` | The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host). | `string` | `""` |
| `etcd` | Deploy own Etcd cluster. | `bool` | `false` |
| `monitoring` | Deploy own Monitoring Stack. | `bool` | `false` |
| `ingress` | Deploy own Ingress Controller. | `bool` | `false` |
| `gateway` | Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). When unset (the default), the chart auto-enables the Gateway for tenants whose apex is derived from the parent (i.e. `host` is empty), and leaves it off for tenants with a custom non-derived apex. Set to `true` or `false` explicitly to override that auto-behaviour. Note: leave the key absent (do not write `gateway: null`) — the chart distinguishes "unset" via missing-key, not via null value, to satisfy the JSON schema generated from this comment. | `bool` | `false` |
| `gatewayClass` | GatewayClass for this tenant's own Gateway. Empty uses the platform default (`gateway.className`). Only the classes the operator lists in the platform's `gateway.tenantSelectableClasses`, plus that default, may be named here — anything else fails this tenant's own gateway release and reaches no other tenant. Read only when this tenant owns a Gateway; an inheriting tenant uses its ancestor's. Whether a class terminates TLS itself or at its provider's edge is declared once per class on the platform, not here. | `string` | `""` |
| `seaweedfs` | Deploy own SeaweedFS. | `bool` | `false` |
| `computeplane` | Deploy own ComputePlane — a single-tenant, Cozystack-managed cluster for untrusted-code applications. The tenant receives no admin kubeconfig for it. Automatic routing of catalog applications onto it (placement: ComputePlane) is a planned follow-up and is not available yet; until it lands, external catalogs target the cluster via its computeplane-cluster-admin-kubeconfig Secret. See design-proposals/compute-plane in cozystack/community. | `bool` | `false` |
| `schedulingClass` | The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads. | `string` | `""` |
| `resourceQuotas` | Define resource quotas for the tenant. | `map[string]quantity` | `{}` |
| Name | Description | Type | Value |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------- |
| `host` | The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host). | `string` | `""` |
| `etcd` | Deploy own Etcd cluster. | `bool` | `false` |
| `monitoring` | Deploy own Monitoring Stack. | `bool` | `false` |
| `ingress` | Deploy own Ingress Controller. | `bool` | `false` |
| `gateway` | Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). When unset (the default) or set to `false`, the tenant does not own a Gateway and instead publishes through the nearest ancestor tenant that owns one, regardless of whether `host` is empty or custom (`false` currently behaves the same as unset and does not opt out of that inheritance). Set to `true` to give the tenant its own Gateway with its own LoadBalancer IP and Certificate. A tenant with a custom (non-derived) `host` that needs its apex publicly reachable must set `true` explicitly, because an ancestor's TLS certificate does not cover a custom apex. Note: leave the key absent (do not write `gateway: null`): the chart distinguishes "unset" via missing-key, not via null value, to satisfy the JSON schema generated from this comment. | `bool` | `false` |
| `gatewayClass` | GatewayClass for this tenant's own Gateway. Empty uses the platform default (`gateway.className`). Only the classes the operator lists in the platform's `gateway.tenantSelectableClasses`, plus that default, may be named here — anything else fails this tenant's own gateway release and reaches no other tenant. Read only when this tenant owns a Gateway; an inheriting tenant uses its ancestor's. Whether a class terminates TLS itself or at its provider's edge is declared once per class on the platform, not here. | `string` | `""` |
| `seaweedfs` | Deploy own SeaweedFS. | `bool` | `false` |
| `computeplane` | Deploy own ComputePlane — a single-tenant, Cozystack-managed cluster for untrusted-code applications. The tenant receives no admin kubeconfig for it. Automatic routing of catalog applications onto it (placement: ComputePlane) is a planned follow-up and is not available yet; until it lands, external catalogs target the cluster via its computeplane-cluster-admin-kubeconfig Secret. See design-proposals/compute-plane in cozystack/community. | `bool` | `false` |
| `schedulingClass` | The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads. | `string` | `""` |
| `resourceQuotas` | Define resource quotas for the tenant. | `map[string]quantity` | `{}` |
## Configuration
@@ -43,12 +43,14 @@
owner's Gateway lists this namespace in allowedRoutes via the
matching namespace.cozystack.io/gateway label.
Override fires only when the tenant itself owns a Gateway —
tenant.gatewayEffective returns "true" for explicit
tenant.spec.gateway=true and for tenants with a custom non-derived
host (their apex isn't covered by any ancestor's cert / Issuer).
In those cases the controller renders a separate Gateway in this
tenant's namespace, with its own LB Service / IP / Certificate.
Override fires only when the tenant itself owns a Gateway:
tenant.gatewayEffective returns "true" only for an explicit
tenant.spec.gateway=true, regardless of whether host is derived or
custom. In that case the controller renders a separate Gateway in
this tenant's namespace, with its own LB Service / IP / Certificate.
A tenant with a custom non-derived host that wants its apex routable
must set gateway=true explicitly, because no ancestor's cert / Issuer
covers that apex.
Cross-namespace attach is gated by ReferenceGrants rendered by the
TenantGateway controller in the owner namespace, one per child
@@ -11,7 +11,7 @@ templates:
# Rules under test (see _helpers.tpl::tenant.gatewayEffective):
# - gateway unset → OFF own (inherit from parent)
# - gateway=true explicit → ON own (LB IP / cert burned)
# - gateway=false explicit → OFF own (parked, no inherit)
# - gateway=false explicit → OFF own (same as unset, still inherits)
#
# The inheritance propagation itself happens in namespace.yaml:
# $gateway := $parentNamespace.gateway | default ""
@@ -86,12 +86,12 @@ tests:
path: kind
value: HelmRelease
- it: gateway=false explicit → no own HelmRelease (parked)
# The escape hatch. Operator opts out of any Gateway publishing,
# even inheritance: namespace.yaml writes _namespace.gateway as
# empty string (the cozystack-basics-side label pins this in
# cozystack-values-secret_test.yaml), and the parent Gateway's
# allowedRoutes selector therefore does not match this namespace.
- it: gateway=false explicit → no own HelmRelease (same as unset)
# An explicit false renders no own Gateway HelmRelease, exactly like
# leaving the key unset. It does NOT opt out of inheritance:
# tenant.gatewayEffective returns "false" for both false and unset, so
# namespace.yaml keeps _namespace.gateway pointing at the nearest
# ancestor that owns a Gateway and this tenant still publishes through it.
set:
gateway: false
host: ""
+1 -1
View File
@@ -23,7 +23,7 @@
"default": false
},
"gateway": {
"description": "Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). When unset (the default), the chart auto-enables the Gateway for tenants whose apex is derived from the parent (i.e. `host` is empty), and leaves it off for tenants with a custom non-derived apex. Set to `true` or `false` explicitly to override that auto-behaviour. Note: leave the key absent (do not write `gateway: null`) — the chart distinguishes \"unset\" via missing-key, not via null value, to satisfy the JSON schema generated from this comment.",
"description": "Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). When unset (the default) or set to `false`, the tenant does not own a Gateway and instead publishes through the nearest ancestor tenant that owns one, regardless of whether `host` is empty or custom (`false` currently behaves the same as unset and does not opt out of that inheritance). Set to `true` to give the tenant its own Gateway with its own LoadBalancer IP and Certificate. A tenant with a custom (non-derived) `host` that needs its apex publicly reachable must set `true` explicitly, because an ancestor's TLS certificate does not cover a custom apex. Note: leave the key absent (do not write `gateway: null`): the chart distinguishes \"unset\" via missing-key, not via null value, to satisfy the JSON schema generated from this comment.",
"type": "boolean"
},
"gatewayClass": {
+1 -1
View File
@@ -14,7 +14,7 @@ monitoring: false
## @param {bool} ingress - Deploy own Ingress Controller.
ingress: false
## @param {bool} [gateway] - Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). When unset (the default), the chart auto-enables the Gateway for tenants whose apex is derived from the parent (i.e. `host` is empty), and leaves it off for tenants with a custom non-derived apex. Set to `true` or `false` explicitly to override that auto-behaviour. Note: leave the key absent (do not write `gateway: null`) — the chart distinguishes "unset" via missing-key, not via null value, to satisfy the JSON schema generated from this comment.
## @param {bool} [gateway] - Deploy own Gateway API Gateway (backed by Cilium Gateway API controller). When unset (the default) or set to `false`, the tenant does not own a Gateway and instead publishes through the nearest ancestor tenant that owns one, regardless of whether `host` is empty or custom (`false` currently behaves the same as unset and does not opt out of that inheritance). Set to `true` to give the tenant its own Gateway with its own LoadBalancer IP and Certificate. A tenant with a custom (non-derived) `host` that needs its apex publicly reachable must set `true` explicitly, because an ancestor's TLS certificate does not cover a custom apex. Note: leave the key absent (do not write `gateway: null`): the chart distinguishes "unset" via missing-key, not via null value, to satisfy the JSON schema generated from this comment.
## @param {string} [gatewayClass] - GatewayClass for this tenant's own Gateway. Empty uses the platform default (`gateway.className`). Only the classes the operator lists in the platform's `gateway.tenantSelectableClasses`, plus that default, may be named here — anything else fails this tenant's own gateway release and reaches no other tenant. Read only when this tenant owns a Gateway; an inheriting tenant uses its ancestor's. Whether a class terminates TLS itself or at its provider's edge is declared once per class on the platform, not here.
gatewayClass: ""
@@ -56,7 +56,7 @@ spec:
{{- end }}
exporter:
enabled: true
image: oliver006/redis_exporter:v1.55.0-alpine
image: oliver006/redis_exporter:v1.55.0@sha256:d82625bda93823fb40de881ffbb45b65aca772a39c30141387f92314c258da5d
args:
- --web.telemetry-path
- /metrics
+1 -1
View File
@@ -58,7 +58,7 @@ virtctl ssh <user>@<vm>
| `firmware` | Firmware and boot configuration (UEFI/BIOS selection, Secure Boot, persistent EFI NVRAM). | `object` | `{}` |
| `firmware.bootloader` | Bootloader to boot the VM with: "uefi" (OVMF) or "bios" (SeaBIOS). Empty inherits the instanceProfile default. | `string` | `""` |
| `firmware.secureBoot` | Enable UEFI Secure Boot. Only applies when bootloader is "uefi". | `bool` | `false` |
| `firmware.efiPersistent` | Persist EFI NVRAM (e.g. enrolled Secure Boot keys, such as an updated Microsoft UEFI CA) across reboots. Only applies when bootloader is "uefi". On default RWO storage the VM is node-pinned (no live-migration). | `bool` | `false` |
| `firmware.efiPersistent` | Persist EFI NVRAM (e.g. enrolled Secure Boot keys, such as an updated Microsoft UEFI CA) across reboots. Only applies when bootloader is "uefi". | `bool` | `false` |
| `resources` | Resource configuration for the virtual machine. Set whole-number cpu and sockets together with memory to size the VM directly, which overrides `instanceType`. Alongside an `instanceType`, a block that sizes only part of the VM is rejected; a whole-number cpu or sockets on its own sizes nothing and is ignored. | `object` | `{}` |
| `resources.cpu` | Number of CPU cores allocated, as a positive whole number such as `2`. Millicore and fractional quantities are rejected at render time, because the value becomes the VM's integer `domain.cpu.cores`. | `quantity` | `""` |
| `resources.memory` | Amount of memory allocated, as a positive quantity such as `8Gi`. | `quantity` | `""` |
+1 -1
View File
@@ -152,7 +152,7 @@
"type": "string"
},
"efiPersistent": {
"description": "Persist EFI NVRAM (e.g. enrolled Secure Boot keys, such as an updated Microsoft UEFI CA) across reboots. Only applies when bootloader is \"uefi\". On default RWO storage the VM is node-pinned (no live-migration).",
"description": "Persist EFI NVRAM (e.g. enrolled Secure Boot keys, such as an updated Microsoft UEFI CA) across reboots. Only applies when bootloader is \"uefi\".",
"type": "boolean"
},
"secureBoot": {
+1 -1
View File
@@ -27,7 +27,7 @@
## @typedef {struct} Firmware - Firmware and boot configuration. When left empty, the boot mode is inherited from the instanceProfile.
## @field {string} [bootloader] - Bootloader to boot the VM with: "uefi" (OVMF) or "bios" (SeaBIOS). Empty inherits the instanceProfile default.
## @field {bool} [secureBoot] - Enable UEFI Secure Boot. Only applies when bootloader is "uefi".
## @field {bool} [efiPersistent] - Persist EFI NVRAM (e.g. enrolled Secure Boot keys, such as an updated Microsoft UEFI CA) across reboots. Only applies when bootloader is "uefi". On default RWO storage the VM is node-pinned (no live-migration).
## @field {bool} [efiPersistent] - Persist EFI NVRAM (e.g. enrolled Secure Boot keys, such as an updated Microsoft UEFI CA) across reboots. Only applies when bootloader is "uefi".
## @param {bool} external - Enable external access from outside the cluster.
external: false
+14 -1
View File
@@ -1,9 +1,22 @@
SHADOWBOX_VERSION = $(shell awk -F= '$$1 == "ARG VERSION" {print $$2; exit}' images/shadowbox/Dockerfile)
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
.PHONY: test
.PHONY: test image
test:
helm unittest .
generate:
cozyvalues-gen -m 'vpn' -v values.yaml -s values.schema.json -r README.md -g ../../../api/apps/v1alpha1/vpn/types.go
../../../hack/update-crd.sh
image:
docker buildx build images/shadowbox \
$(call image-tags,shadowbox,$(SHADOWBOX_VERSION)) \
$(call cache-args,shadowbox) \
--metadata-file images/shadowbox.json \
$(BUILDX_ARGS)
echo "$(REGISTRY)/shadowbox:$(IMAGE_TAG)@$$(yq e '."containerimage.digest"' images/shadowbox.json -o json -r)" \
> images/shadowbox.tag
rm -f images/shadowbox.json
+1
View File
@@ -0,0 +1 @@
quay.io/outline/shadowbox:v1.12.3@sha256:545c6f7c7261bb30ae1dffe24a6fca5f8512f5d17c72cfb9e410e7e655444e62
@@ -0,0 +1,64 @@
# Upstream publishes quay.io/outline/shadowbox for amd64 only, although its
# own Taskfile already knows how to target arm64. This rebuilds the same
# release as a multi-arch image; the layout follows upstream's
# src/shadowbox/Taskfile.yml `docker:build`.
ARG VERSION=v1.12.3
ARG COMMIT=067d0d47aedac94a3888a127fa6c65f2f53b9987
FROM --platform=$BUILDPLATFORM node:18.18.0-alpine3.18@sha256:a315556d82ef54561e54fca7d8ee333382de183d4e56841dcefcd05b55310f46 AS source
ARG VERSION
ARG COMMIT
RUN apk add --no-cache git
RUN git clone --branch server-${VERSION} --depth 1 https://github.com/OutlineFoundation/outline-server /src \
&& test "$(git -C /src rev-parse HEAD)" = "${COMMIT}"
# main.js is a single webpack bundle with no native addons, so it is built
# once on the build platform and shared by every target.
FROM source AS app
ARG VERSION
WORKDIR /src
RUN npm ci --ignore-scripts
RUN SB_VERSION=${VERSION} npx webpack --config=src/shadowbox/webpack.config.js \
--output-path=/out/opt/outline-server/app --mode=production \
&& cp src/shadowbox/package.json /out/opt/outline-server/
FROM --platform=$BUILDPLATFORM golang:1.27-alpine@sha256:8a5910f31396cd4d89662f56c68b3ae31d374308270a1c3bd96672ee5ed43414 AS ss-server
ARG TARGETOS
ARG TARGETARCH
COPY --from=source /src /src
WORKDIR /src
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -trimpath \
-ldflags='-s -w -X main.version=embedded' \
-o /out/opt/outline-server/bin/ github.com/Jigsaw-Code/outline-ss-server/cmd/outline-ss-server
# Version and checksums are upstream's, from third_party/Taskfile.yml.
FROM --platform=$BUILDPLATFORM node:18.18.0-alpine3.18@sha256:a315556d82ef54561e54fca7d8ee333382de183d4e56841dcefcd05b55310f46 AS prometheus
ARG TARGETARCH
ARG PROMETHEUS_VERSION=2.37.1
RUN case "${TARGETARCH}" in \
amd64) sum=753f66437597cf52ada98c2f459aa8c03745475c249c9f2b40ac7b3919131ba6 ;; \
arm64) sum=b59a66fb5c7ec5acf6bf426793528a5789a1478a0dad8c64edc2843caf31b1b8 ;; \
*) echo "unsupported TARGETARCH ${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& name=prometheus-${PROMETHEUS_VERSION}.linux-${TARGETARCH} \
&& wget -q -O /tmp/p.tgz https://github.com/prometheus/prometheus/releases/download/v${PROMETHEUS_VERSION}/${name}.tar.gz \
&& echo "${sum} /tmp/p.tgz" | sha256sum -c - \
&& mkdir -p /out/opt/outline-server/bin \
&& tar -xzf /tmp/p.tgz -C /out/opt/outline-server/bin --strip-components=1 ${name}/prometheus
FROM node:18.18.0-alpine3.18@sha256:a315556d82ef54561e54fca7d8ee333382de183d4e56841dcefcd05b55310f46
ARG VERSION
LABEL shadowbox.github.release=${VERSION}
# The user management service doesn't quit with SIGTERM.
STOPSIGNAL SIGKILL
# curl detects the public IP; coreutils gives update_mmdb.sh a `date --date`.
RUN apk add --no-cache --upgrade coreutils curl
COPY --from=app /out /
COPY --from=ss-server /out /
COPY --from=prometheus /out /
COPY --from=source /src/src/shadowbox/scripts /scripts
COPY --from=source /src/src/shadowbox/scripts/update_mmdb.sh /etc/periodic/weekly/update_mmdb.sh
COPY --from=source /src/src/shadowbox/docker/cmd.sh /cmd.sh
RUN mkdir -p /root/shadowbox/persisted-state && /etc/periodic/weekly/update_mmdb.sh
WORKDIR /opt/outline-server
CMD ["/cmd.sh"]
+1 -1
View File
@@ -42,7 +42,7 @@ spec:
path: shadowbox_server_config.json
containers:
- name: outline-vpn
image: quay.io/outline/shadowbox:stable
image: {{ .Files.Get "images/shadowbox.tag" | trim | quote }}
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 12 }}
ports:
- containerPort: 40000
+1 -1
View File
@@ -1,4 +1,4 @@
{{- $host := .Values._namespace.host }}
{{- $host := include "cozy-lib.ns-host-required" . }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-vpn" .Release.Name) }}
{{- $accessKeys := list }}
{{- $passwords := dict }}
@@ -0,0 +1,28 @@
suite: vpn access URL fails on an empty apex
templates:
- templates/secret.yaml
release:
name: vpn-test
namespace: tenant-test
# secret.yaml builds the per-user access URL as <release>.<_namespace.host>.
# cozy-lib.ns-host-required guards the apex, so an empty apex fails the render.
tests:
- it: fails when the apex is empty and no host override
set:
_namespace:
host: ""
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: renders the secret when the apex is set
set:
_namespace:
host: example.com
asserts:
- hasDocuments:
count: 2
+3 -4
View File
@@ -16,11 +16,13 @@ set:
host: example.com
tests:
- it: renders a Deployment with the default replicas and image
- it: renders a Deployment with the default replicas and the image from images/shadowbox.tag
template: templates/deployment.yaml
asserts:
- hasDocuments:
count: 1
- isNotNullOrEmpty:
path: spec.template.spec.containers[0].image
- isKind:
of: Deployment
- equal:
@@ -29,9 +31,6 @@ tests:
- equal:
path: spec.replicas
value: 2
- equal:
path: spec.template.spec.containers[0].image
value: "quay.io/outline/shadowbox:stable"
- equal:
path: spec.template.spec.containers[0].ports[0].containerPort
value: 40000
@@ -22,6 +22,7 @@ spec:
components:
- name: bucket-system
path: system/bucket
libraries: ["cozy-lib"]
- name: bucket
path: apps/bucket
libraries: ["cozy-lib"]
+45 -37
View File
@@ -3,31 +3,39 @@ NAMESPACE=cozy-system
TALOS_VERSION=$(shell awk '/^version:/ {print $$2}' images/talos/profiles/installer.yaml)
# The Talos assets this package builds are amd64 only (profiles, imager
# output), and matchbox bakes them into its image, so both images stay amd64.
# `override` also beats a PLATFORM given on the command line to a whole
# `make build`. It must precede the include: common-envs.mk expands BUILDX_ARGS
# immediately.
override PLATFORM := linux/amd64
include ../../../hack/common-envs.mk
ASSETS := ../../../_out/assets
# The installer image and the release assets are built for the architectures
# in PLATFORM. matchbox carries the kernel and initramfs of both whatever
# PLATFORM says: the node it runs on has nothing to do with the architecture
# of the machines it network-boots.
TALOS_ARCHES := $(patsubst linux/%,%,$(subst $(comma), ,$(or $(PLATFORM),$(DEFAULT_PLATFORMS))))
BOOT_ARCHES := amd64 arm64
# assets <file pattern> <arches> — the asset paths, % standing for the arch.
assets = $(foreach a,$(2),$(ASSETS)/$(subst %,$(a),$(1)))
update:
hack/gen-profiles.sh
image: image-matchbox image-talos
image-talos:
test -f ../../../_out/assets/installer-amd64.tar || make talos-installer
# The installers of every arch go into one OCI layout, where
# hack/installer-index.sh ties them into the index that is pushed.
image-talos: talos-installer
rm -rf $(ASSETS)/installer-oci
set -e; \
SRC=docker-archive:../../../_out/assets/installer-amd64.tar; \
$(if $(strip $(OCI_EXPORT_DIR)),mkdir -p "$(OCI_EXPORT_DIR)"; skopeo copy "$$SRC" "oci-archive:$(OCI_EXPORT_DIR)/talos.oci.tar:$(IMAGE_TAG)",skopeo copy "$$SRC" docker://$(REGISTRY)/talos:$(IMAGE_TAG)); \
if [ -z "$(OCI_EXPORT_DIR)" ] && [ "$(PUBLISH_VERSIONED)" = "1" ]; then skopeo copy "$$SRC" docker://$(REGISTRY)/talos:$(TALOS_VERSION); fi; \
if [ -z "$(OCI_EXPORT_DIR)" ] && [ "$(PUBLISH_FLOATING)" = "1" ]; then skopeo copy "$$SRC" docker://$(REGISTRY)/talos:latest; fi
for a in $(TALOS_ARCHES); do skopeo copy docker-archive:$(ASSETS)/installer-$$a.tar oci:$(ASSETS)/installer-oci:$$a; done; \
hack/installer-index.sh $(ASSETS)/installer-oci $(TALOS_ARCHES); \
SRC=oci:$(ASSETS)/installer-oci:index; \
$(if $(strip $(OCI_EXPORT_DIR)),mkdir -p "$(OCI_EXPORT_DIR)"; skopeo copy --all "$$SRC" "oci-archive:$(OCI_EXPORT_DIR)/talos.oci.tar:$(IMAGE_TAG)",skopeo copy --all "$$SRC" docker://$(REGISTRY)/talos:$(IMAGE_TAG)); \
if [ -z "$(OCI_EXPORT_DIR)" ] && [ "$(PUBLISH_VERSIONED)" = "1" ]; then skopeo copy --all "$$SRC" docker://$(REGISTRY)/talos:$(TALOS_VERSION); fi; \
if [ -z "$(OCI_EXPORT_DIR)" ] && [ "$(PUBLISH_FLOATING)" = "1" ]; then skopeo copy --all "$$SRC" docker://$(REGISTRY)/talos:latest; fi
rm -rf $(ASSETS)/installer-oci
image-matchbox:
test -f ../../../_out/assets/kernel-amd64 || make talos-kernel
test -f ../../../_out/assets/initramfs-metal-amd64.xz || make talos-initramfs
image-matchbox: $(call assets,kernel-%,$(BOOT_ARCHES)) $(call assets,initramfs-metal-%.xz,$(BOOT_ARCHES))
docker buildx build -f images/matchbox/Dockerfile ../../.. \
$(call image-tags,matchbox,$(TALOS_VERSION)) \
$(call cache-args,matchbox) \
@@ -39,27 +47,27 @@ image-matchbox:
assets: talos-iso talos-nocloud talos-metal talos-kernel talos-initramfs
talos-initramfs:
test -f ../../../_out/assets/initramfs-metal-amd64.xz || $(MAKE) build-talos-initramfs
talos-initramfs: $(call assets,initramfs-metal-%.xz,$(TALOS_ARCHES))
talos-kernel: $(call assets,kernel-%,$(TALOS_ARCHES))
talos-installer: $(call assets,installer-%.tar,$(TALOS_ARCHES))
talos-iso: $(call assets,metal-%.iso,$(TALOS_ARCHES))
talos-nocloud: $(call assets,nocloud-%.raw.xz,$(TALOS_ARCHES))
talos-metal: $(call assets,metal-%.raw.xz,$(TALOS_ARCHES))
talos-kernel:
test -f ../../../_out/assets/kernel-amd64 || $(MAKE) build-talos-kernel
.PHONY: update image image-talos image-matchbox assets talos-initramfs talos-kernel talos-installer talos-iso talos-nocloud talos-metal
talos-installer:
test -f ../../../_out/assets/installer-amd64.tar || $(MAKE) build-talos-installer
talos-iso:
test -f ../../../_out/assets/metal-amd64.iso || $(MAKE) build-talos-iso
talos-nocloud:
test -f ../../../_out/assets/nocloud-amd64.raw.xz || $(MAKE) build-talos-nocloud
talos-metal:
test -f ../../../_out/assets/metal-amd64.raw.xz || $(MAKE) build-talos-metal
build-talos-initramfs build-talos-kernel build-talos-installer build-talos-iso build-talos-nocloud build-talos-metal:
mkdir -p ../../../_out/assets
cat images/talos/profiles/$(subst build-talos-,,$@).yaml | \
docker run --rm -i -v /dev:/dev --privileged "ghcr.io/siderolabs/imager:$(TALOS_VERSION)" --tar-to-stdout - | \
tar -C ../../../_out/assets -xzf-
# imager <profile> — run the imager on the profile of that name for the arch
# in $*. amd64 profiles carry no arch suffix (see hack/gen-profiles.sh).
define imager
mkdir -p $(ASSETS)
docker run --rm -i -v /dev:/dev --privileged "ghcr.io/siderolabs/imager:$(TALOS_VERSION)" --tar-to-stdout - \
< images/talos/profiles/$(1)$(if $(filter-out amd64,$*),-$*).yaml | \
tar -C $(ASSETS) -xzf-
endef
$(ASSETS)/initramfs-metal-%.xz: ; $(call imager,initramfs)
$(ASSETS)/kernel-%: ; $(call imager,kernel)
$(ASSETS)/installer-%.tar: ; $(call imager,installer)
$(ASSETS)/metal-%.iso: ; $(call imager,iso)
$(ASSETS)/nocloud-%.raw.xz: ; $(call imager,nocloud)
$(ASSETS)/metal-%.raw.xz: ; $(call imager,metal)
+72 -62
View File
@@ -4,8 +4,20 @@ set -u
TMPDIR=$(mktemp -d)
PROFILES="initramfs kernel iso installer nocloud metal"
FIRMWARES="amd-ucode amdgpu bnx2-bnx2x i915 intel-ice-firmware intel-ucode qlogic-firmware"
EXTENSIONS="drbd zfs"
ARCHES="amd64 arm64"
# Every extension below is published for arm64 too, but not every one is
# worth installing there:
# - amd-ucode and intel-ucode are x86 CPU microcode, which an arm64 kernel
# never loads;
# - the arm64 builds of amdgpu and i915 are empty, carrying a manifest and no
# files at all.
# bnx2-bnx2x, intel-ice-firmware and qlogic-firmware are firmware for PCIe
# NICs whose drivers (bnx2, bnx2x, ice, qed) the arm64 Talos kernel builds as
# modules, so arm64 servers with those cards need them as much as x86 ones.
# drbd and zfs are what Cozystack itself needs on every node.
EXTENSIONS_amd64="amd-ucode amdgpu bnx2-bnx2x intel-ice-firmware i915 intel-ucode qlogic-firmware drbd zfs"
EXTENSIONS_arm64="bnx2-bnx2x intel-ice-firmware qlogic-firmware drbd zfs"
mkdir -p images/talos/profiles
@@ -17,82 +29,80 @@ export "TALOS_VERSION=$talos_version"
crane export ghcr.io/siderolabs/extensions:${TALOS_VERSION} | tar x -O image-digests > $TMPDIR/image-digests
for firmware in $FIRMWARES; do
printf "fetching %s version: " "$firmware"
firmware_var=$(echo "$firmware" | tr '[:lower:]' '[:upper:]' | tr - _)_IMAGE
image=$(grep -F "/$firmware:" $TMPDIR/image-digests)
echo "$image"
export "$firmware_var=$image"
done
for arch in $ARCHES; do
eval "extensions=\$EXTENSIONS_$arch"
# amd64 keeps the unsuffixed names that the Makefile and other tooling
# read the Talos version from.
suffix=""
[ "$arch" = amd64 ] || suffix="-$arch"
for extension in $EXTENSIONS; do
printf "fetching %s version: " "$extension"
extension_var=$(echo "$extension" | tr '[:lower:]' '[:upper:]' | tr - _)_IMAGE
image=$(grep -F "/$extension:" $TMPDIR/image-digests)
echo "$image"
export "$extension_var=$image"
done
for profile in $PROFILES; do
echo "writing profile images/talos/profiles/$profile.yaml"
case "$profile" in
initramfs|kernel|iso)
image_options="{}"
out_format="raw"
platform="metal"
kind="$profile"
;;
installer)
image_options="{}"
out_format="raw"
platform="metal"
kind="installer"
;;
metal)
image_options="{ diskSize: 1306525696, diskFormat: raw }"
out_format=".xz"
platform="metal"
kind="image"
;;
nocloud)
image_options="{ diskSize: 1306525696, diskFormat: raw }"
out_format=".xz"
platform="nocloud"
kind="image"
;;
*)
echo "Unknown profile: $profile" >&2
system_extensions=""
for extension in $extensions; do
printf "fetching %s version for %s: " "$extension" "$arch"
image=$(grep -F "/$extension:" $TMPDIR/image-digests)
echo "$image"
# Fail here rather than in the imager if upstream drops an arch.
crane manifest "$image" | jq -e --arg arch "$arch" '.manifests | any(.platform.architecture == $arch)' >/dev/null || {
echo "$image has no $arch image" >&2
exit 1
;;
esac
}
system_extensions="$system_extensions
- imageRef: $image"
done
cat > images/talos/profiles/$profile.yaml <<EOT
for profile in $PROFILES; do
file="images/talos/profiles/$profile$suffix.yaml"
echo "writing profile $file"
case "$profile" in
initramfs|kernel|iso)
image_options="{}"
out_format="raw"
platform="metal"
kind="$profile"
;;
installer)
image_options="{}"
out_format="raw"
platform="metal"
kind="installer"
;;
metal)
image_options="{ diskSize: 1306525696, diskFormat: raw }"
out_format=".xz"
platform="metal"
kind="image"
;;
nocloud)
image_options="{ diskSize: 1306525696, diskFormat: raw }"
out_format=".xz"
platform="nocloud"
kind="image"
;;
*)
echo "Unknown profile: $profile" >&2
exit 1
;;
esac
cat > "$file" <<EOT
# this file generated by hack/gen-profiles.sh
# do not edit it
arch: amd64
arch: ${arch}
platform: ${platform}
secureboot: false
version: ${TALOS_VERSION}
input:
kernel:
path: /usr/install/amd64/vmlinuz
path: /usr/install/${arch}/vmlinuz
initramfs:
path: /usr/install/amd64/initramfs.xz
path: /usr/install/${arch}/initramfs.xz
baseInstaller:
imageRef: "ghcr.io/siderolabs/installer:${TALOS_VERSION}"
systemExtensions:
- imageRef: ${AMD_UCODE_IMAGE}
- imageRef: ${AMDGPU_IMAGE}
- imageRef: ${BNX2_BNX2X_IMAGE}
- imageRef: ${INTEL_ICE_FIRMWARE_IMAGE}
- imageRef: ${I915_IMAGE}
- imageRef: ${INTEL_UCODE_IMAGE}
- imageRef: ${QLOGIC_FIRMWARE_IMAGE}
- imageRef: ${DRBD_IMAGE}
- imageRef: ${ZFS_IMAGE}
systemExtensions:${system_extensions}
output:
kind: ${kind}
imageOptions: ${image_options}
outFormat: ${out_format}
EOT
done
done
+35
View File
@@ -0,0 +1,35 @@
#!/bin/sh
# installer-index.sh <oci-layout> <arch>...
#
# Adds to an OCI layout an image index, tagged "index", over the manifests
# tagged with each given arch. skopeo copies an index but cannot assemble one,
# and buildx can only take an OCI layout as a source on a builder the release
# job, which builds on the classic docker driver, does not have.
set -eu
layout=$1
shift
manifests=$(for arch in "$@"; do
jq -c --arg arch "$arch" '.manifests[]
| select(.annotations["org.opencontainers.image.ref.name"] == $arch)
| {mediaType, digest, size, platform: {architecture: $arch, os: "linux"}}' "$layout/index.json"
done | jq -s -c .)
found=$(printf '%s' "$manifests" | jq length)
if [ "$found" -ne "$#" ]; then
echo "installer-index.sh: $layout holds $found of the manifests for: $*" >&2
exit 1
fi
index=$(jq -n -c --argjson m "$manifests" \
'{schemaVersion: 2, mediaType: "application/vnd.oci.image.index.v1+json", manifests: $m}')
digest=$(printf '%s' "$index" | sha256sum | cut -d' ' -f1)
size=$(printf '%s' "$index" | wc -c | tr -d ' ')
printf '%s' "$index" > "$layout/blobs/sha256/$digest"
jq --arg d "sha256:$digest" --argjson s "$size" \
'.manifests += [{mediaType: "application/vnd.oci.image.index.v1+json", digest: $d, size: $s,
annotations: {"org.opencontainers.image.ref.name": "index"}}]' \
"$layout/index.json" > "$layout/index.json.new"
mv "$layout/index.json.new" "$layout/index.json"
@@ -1,6 +1,11 @@
FROM quay.io/poseidon/matchbox:v0.11.0@sha256:06bcdae85335fd00e8277b007b55cfb49d96a0114628c0f70db2b92b079d246a
# Every image carries both arches' assets, not only TARGETARCH's: the node
# matchbox runs on says nothing about the machines it boots. The arm64 group
# picks its assets by the arch iPXE reports.
COPY _out/assets/initramfs-metal-amd64.xz /var/lib/matchbox/assets/initramfs.xz
COPY _out/assets/kernel-amd64 /var/lib/matchbox/assets/vmlinuz
COPY _out/assets/initramfs-metal-arm64.xz /var/lib/matchbox/assets/initramfs-arm64.xz
COPY _out/assets/kernel-arm64 /var/lib/matchbox/assets/vmlinuz-arm64
COPY packages/core/talos/images/matchbox/groups /var/lib/matchbox/groups
COPY packages/core/talos/images/matchbox/profiles /var/lib/matchbox/profiles
@@ -0,0 +1,8 @@
{
"id": "arm64",
"name": "arm64",
"profile": "arm64",
"selector": {
"arch": "arm64"
}
}
@@ -0,0 +1,18 @@
{
"id": "arm64",
"name": "arm64",
"boot": {
"kernel": "/assets/vmlinuz-arm64",
"initrd": ["/assets/initramfs-arm64.xz"],
"args": [
"initrd=initramfs-arm64.xz",
"init_on_alloc=1",
"slab_nomerge",
"pti=on",
"console=tty0",
"console=ttyAMA0",
"printk.devkmsg=on",
"talos.platform=metal"
]
}
}
@@ -0,0 +1,23 @@
# this file generated by hack/gen-profiles.sh
# do not edit it
arch: arm64
platform: metal
secureboot: false
version: v1.13.6
input:
kernel:
path: /usr/install/arm64/vmlinuz
initramfs:
path: /usr/install/arm64/initramfs.xz
baseInstaller:
imageRef: "ghcr.io/siderolabs/installer:v1.13.6"
systemExtensions:
- imageRef: ghcr.io/siderolabs/bnx2-bnx2x:20260622@sha256:46dee9e025c972a29cd7e9d0a15f665e71e581627f9372a75607153e1221f44e
- imageRef: ghcr.io/siderolabs/intel-ice-firmware:20260622@sha256:977558ad5f00a4d6058e3f7f0756e68cbb2596621f155fec931261ff2dd330b6
- imageRef: ghcr.io/siderolabs/qlogic-firmware:20260622@sha256:b4e05d55d6d27d57c85a29c33e199444c80aacc72a2e9b5b7344391d46f1eade
- imageRef: ghcr.io/siderolabs/drbd:9.3.2-v1.13.6@sha256:4a6f77f4e3fde41f3656f72d7d68380356afbb289254af8263a154cf7b70598e
- imageRef: ghcr.io/siderolabs/zfs:2.4.3-v1.13.6@sha256:3389808471eb857096cdf47a645b30b3e954bef0a9cb5d13a45b185796f606b6
output:
kind: initramfs
imageOptions: {}
outFormat: raw
@@ -0,0 +1,23 @@
# this file generated by hack/gen-profiles.sh
# do not edit it
arch: arm64
platform: metal
secureboot: false
version: v1.13.6
input:
kernel:
path: /usr/install/arm64/vmlinuz
initramfs:
path: /usr/install/arm64/initramfs.xz
baseInstaller:
imageRef: "ghcr.io/siderolabs/installer:v1.13.6"
systemExtensions:
- imageRef: ghcr.io/siderolabs/bnx2-bnx2x:20260622@sha256:46dee9e025c972a29cd7e9d0a15f665e71e581627f9372a75607153e1221f44e
- imageRef: ghcr.io/siderolabs/intel-ice-firmware:20260622@sha256:977558ad5f00a4d6058e3f7f0756e68cbb2596621f155fec931261ff2dd330b6
- imageRef: ghcr.io/siderolabs/qlogic-firmware:20260622@sha256:b4e05d55d6d27d57c85a29c33e199444c80aacc72a2e9b5b7344391d46f1eade
- imageRef: ghcr.io/siderolabs/drbd:9.3.2-v1.13.6@sha256:4a6f77f4e3fde41f3656f72d7d68380356afbb289254af8263a154cf7b70598e
- imageRef: ghcr.io/siderolabs/zfs:2.4.3-v1.13.6@sha256:3389808471eb857096cdf47a645b30b3e954bef0a9cb5d13a45b185796f606b6
output:
kind: installer
imageOptions: {}
outFormat: raw
@@ -0,0 +1,23 @@
# this file generated by hack/gen-profiles.sh
# do not edit it
arch: arm64
platform: metal
secureboot: false
version: v1.13.6
input:
kernel:
path: /usr/install/arm64/vmlinuz
initramfs:
path: /usr/install/arm64/initramfs.xz
baseInstaller:
imageRef: "ghcr.io/siderolabs/installer:v1.13.6"
systemExtensions:
- imageRef: ghcr.io/siderolabs/bnx2-bnx2x:20260622@sha256:46dee9e025c972a29cd7e9d0a15f665e71e581627f9372a75607153e1221f44e
- imageRef: ghcr.io/siderolabs/intel-ice-firmware:20260622@sha256:977558ad5f00a4d6058e3f7f0756e68cbb2596621f155fec931261ff2dd330b6
- imageRef: ghcr.io/siderolabs/qlogic-firmware:20260622@sha256:b4e05d55d6d27d57c85a29c33e199444c80aacc72a2e9b5b7344391d46f1eade
- imageRef: ghcr.io/siderolabs/drbd:9.3.2-v1.13.6@sha256:4a6f77f4e3fde41f3656f72d7d68380356afbb289254af8263a154cf7b70598e
- imageRef: ghcr.io/siderolabs/zfs:2.4.3-v1.13.6@sha256:3389808471eb857096cdf47a645b30b3e954bef0a9cb5d13a45b185796f606b6
output:
kind: iso
imageOptions: {}
outFormat: raw
@@ -0,0 +1,23 @@
# this file generated by hack/gen-profiles.sh
# do not edit it
arch: arm64
platform: metal
secureboot: false
version: v1.13.6
input:
kernel:
path: /usr/install/arm64/vmlinuz
initramfs:
path: /usr/install/arm64/initramfs.xz
baseInstaller:
imageRef: "ghcr.io/siderolabs/installer:v1.13.6"
systemExtensions:
- imageRef: ghcr.io/siderolabs/bnx2-bnx2x:20260622@sha256:46dee9e025c972a29cd7e9d0a15f665e71e581627f9372a75607153e1221f44e
- imageRef: ghcr.io/siderolabs/intel-ice-firmware:20260622@sha256:977558ad5f00a4d6058e3f7f0756e68cbb2596621f155fec931261ff2dd330b6
- imageRef: ghcr.io/siderolabs/qlogic-firmware:20260622@sha256:b4e05d55d6d27d57c85a29c33e199444c80aacc72a2e9b5b7344391d46f1eade
- imageRef: ghcr.io/siderolabs/drbd:9.3.2-v1.13.6@sha256:4a6f77f4e3fde41f3656f72d7d68380356afbb289254af8263a154cf7b70598e
- imageRef: ghcr.io/siderolabs/zfs:2.4.3-v1.13.6@sha256:3389808471eb857096cdf47a645b30b3e954bef0a9cb5d13a45b185796f606b6
output:
kind: kernel
imageOptions: {}
outFormat: raw
@@ -0,0 +1,23 @@
# this file generated by hack/gen-profiles.sh
# do not edit it
arch: arm64
platform: metal
secureboot: false
version: v1.13.6
input:
kernel:
path: /usr/install/arm64/vmlinuz
initramfs:
path: /usr/install/arm64/initramfs.xz
baseInstaller:
imageRef: "ghcr.io/siderolabs/installer:v1.13.6"
systemExtensions:
- imageRef: ghcr.io/siderolabs/bnx2-bnx2x:20260622@sha256:46dee9e025c972a29cd7e9d0a15f665e71e581627f9372a75607153e1221f44e
- imageRef: ghcr.io/siderolabs/intel-ice-firmware:20260622@sha256:977558ad5f00a4d6058e3f7f0756e68cbb2596621f155fec931261ff2dd330b6
- imageRef: ghcr.io/siderolabs/qlogic-firmware:20260622@sha256:b4e05d55d6d27d57c85a29c33e199444c80aacc72a2e9b5b7344391d46f1eade
- imageRef: ghcr.io/siderolabs/drbd:9.3.2-v1.13.6@sha256:4a6f77f4e3fde41f3656f72d7d68380356afbb289254af8263a154cf7b70598e
- imageRef: ghcr.io/siderolabs/zfs:2.4.3-v1.13.6@sha256:3389808471eb857096cdf47a645b30b3e954bef0a9cb5d13a45b185796f606b6
output:
kind: image
imageOptions: { diskSize: 1306525696, diskFormat: raw }
outFormat: .xz
@@ -0,0 +1,23 @@
# this file generated by hack/gen-profiles.sh
# do not edit it
arch: arm64
platform: nocloud
secureboot: false
version: v1.13.6
input:
kernel:
path: /usr/install/arm64/vmlinuz
initramfs:
path: /usr/install/arm64/initramfs.xz
baseInstaller:
imageRef: "ghcr.io/siderolabs/installer:v1.13.6"
systemExtensions:
- imageRef: ghcr.io/siderolabs/bnx2-bnx2x:20260622@sha256:46dee9e025c972a29cd7e9d0a15f665e71e581627f9372a75607153e1221f44e
- imageRef: ghcr.io/siderolabs/intel-ice-firmware:20260622@sha256:977558ad5f00a4d6058e3f7f0756e68cbb2596621f155fec931261ff2dd330b6
- imageRef: ghcr.io/siderolabs/qlogic-firmware:20260622@sha256:b4e05d55d6d27d57c85a29c33e199444c80aacc72a2e9b5b7344391d46f1eade
- imageRef: ghcr.io/siderolabs/drbd:9.3.2-v1.13.6@sha256:4a6f77f4e3fde41f3656f72d7d68380356afbb289254af8263a154cf7b70598e
- imageRef: ghcr.io/siderolabs/zfs:2.4.3-v1.13.6@sha256:3389808471eb857096cdf47a645b30b3e954bef0a9cb5d13a45b185796f606b6
output:
kind: image
imageOptions: { diskSize: 1306525696, diskFormat: raw }
outFormat: .xz
@@ -7,6 +7,7 @@ ARG COZYHR_VERSION=1.6.1
ARG CRUST_GATHER_VERSION=v0.15.2
ARG CHAINSAW_VERSION=0.2.15
ARG MC_VERSION=RELEASE.2025-08-13T08-35-41Z
ARG FLUX_VERSION=2.8.6
ARG TARGETOS
ARG TARGETARCH
@@ -20,7 +21,9 @@ RUN curl -sSL "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/${TARGETOS}/${T
RUN curl -sSL "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3" | bash -s - --version "v${HELM_VERSION}"
RUN curl -sSL "https://github.com/mikefarah/yq/releases/download/v4.53.3/yq_${TARGETOS}_${TARGETARCH}" -o /usr/local/bin/yq \
&& chmod +x /usr/local/bin/yq
RUN curl -sSL "https://fluxcd.io/install.sh" | bash
RUN curl -fsSL "https://fluxcd.io/install.sh" -o /tmp/flux-install.sh \
&& FLUX_VERSION="${FLUX_VERSION}" bash /tmp/flux-install.sh \
&& rm -f /tmp/flux-install.sh
RUN curl -sSL "https://github.com/cozystack/cozyhr/raw/refs/heads/main/hack/install.sh" | sh -s -- -v "${COZYHR_VERSION}"
# dl.min.io stopped serving community mc builds (HTTP 410) when MinIO archived
# the project; the final release is still published on GitHub.
@@ -22,6 +22,25 @@ data:
]
}
}
arm64.json: |
{
"id": "arm64",
"name": "arm64",
"boot": {
"kernel": "/assets/vmlinuz-arm64",
"initrd": ["/assets/initramfs-arm64.xz"],
"args": [
"initrd=initramfs-arm64.xz",
"init_on_alloc=1",
"slab_nomerge",
"pti=on",
"console=tty0",
"console=ttyAMA0",
"printk.devkmsg=on",
"talos.platform=metal"
]
}
}
---
apiVersion: v1
kind: ConfigMap
@@ -34,6 +53,15 @@ data:
"name": "default",
"profile": "default"
}
arm64.json: |
{
"id": "arm64",
"name": "arm64",
"profile": "arm64",
"selector": {
"arch": "arm64"
}
}
---
apiVersion: v1
kind: ConfigMap
@@ -1,7 +1,7 @@
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- $host := include "cozy-lib.ns-host-required" . }}
{{- $wildcardSecret := (index .Values._cluster "wildcard-secret-name") | default "" }}
apiVersion: networking.k8s.io/v1
kind: Ingress
@@ -1,4 +1,4 @@
{{- $host := .Values._namespace.host }}
{{- $host := include "cozy-lib.ns-host-required" . }}
{{ range $m := .Values.machines }}
---
@@ -0,0 +1,41 @@
suite: bootbox machines guards an empty apex in isolation
templates:
- templates/matchbox/machines.yaml
release:
name: bootbox
namespace: tenant-root
# The guard sits at the top of machines.yaml, before the machines range, so the
# failing case needs no machines (the default [] is schema-valid). The positive
# case supplies a schema-complete machine to emit a Hardware object.
tests:
- it: fails when _namespace is injected with an empty host and no host override
set:
_namespace:
host: ""
ingress: tenant-root
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: renders the iPXE URL from the apex when it is set
set:
_namespace:
host: example.org
ingress: tenant-root
machines:
- arch: x86_64
hostname: node1
mac: ["aa:bb:cc:dd:ee:ff"]
uefi: true
leaseTime: 3600
ip:
address: 192.0.2.10
gateway: 192.0.2.1
netmask: 255.255.255.0
asserts:
- equal:
path: spec.interfaces[0].netboot.ipxe.url
value: "https://bootbox.example.org/boot.ipxe"
@@ -0,0 +1,37 @@
suite: bootbox ingress fails on an empty apex
templates:
- templates/matchbox/ingress.yaml
release:
name: bootbox
namespace: tenant-root
# The matchbox Ingress host is bootbox.<_namespace.host>. cozy-lib.ns-host-required
# guards the apex, so an empty apex fails the render.
tests:
- it: fails when the apex is empty and no host override
set:
_namespace:
host: ""
ingress: tenant-root
_cluster:
issuer-name: letsencrypt-prod
solver: http01
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: renders the ingress when the apex is set
set:
_namespace:
host: example.org
ingress: tenant-root
_cluster:
issuer-name: letsencrypt-prod
solver: http01
asserts:
- equal:
path: kind
value: Ingress
@@ -1,5 +1,5 @@
{{- if eq .Values.topology "Client" }}
{{- $host := .Values._namespace.host }}
{{- $host := include "cozy-lib.ns-host-required" . }}
---
apiVersion: apps/v1
kind: Deployment
@@ -94,7 +94,7 @@
{{- if not (eq .Values.topology "Client") }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- $host := include "cozy-lib.ns-host-required" . }}
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $clusterIssuer := (index .Values._cluster "issuer-name") | default "letsencrypt-prod" }}
{{- $wildcardSecret := (index .Values._cluster "wildcard-secret-name") | default "" }}
@@ -0,0 +1,37 @@
suite: seaweedfs COSI deployment guards an empty apex in isolation
templates:
- templates/client/cosi-deployment.yaml
release:
name: seaweedfs
namespace: tenant-root
tests:
- it: fails when topology is Client and the apex is empty with no host override
set:
topology: Client
_namespace:
host: ""
ingress: tenant-root
filer:
grpcHost: seaweedfs-filer
grpcPort: 18888
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: renders the S3 endpoint from the apex when it is set
set:
topology: Client
_namespace:
host: example.org
ingress: tenant-root
filer:
grpcHost: seaweedfs-filer
grpcPort: 18888
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: ENDPOINT
value: https://s3.example.org
@@ -0,0 +1,44 @@
suite: seaweedfs S3 endpoint fails on an empty apex
templates:
- templates/seaweedfs.yaml
release:
name: seaweedfs
namespace: tenant-root
# The S3 endpoint is s3.<_namespace.host> on the non-Client topology path.
# cozy-lib.ns-host-required guards the apex, so an empty apex fails the render.
tests:
- it: fails when the apex is empty and no host override
set:
topology: Simple
replicationFactor: 2
_namespace:
host: ""
ingress: tenant-root
_cluster:
issuer-name: letsencrypt-prod
solver: http01
asserts:
- failedTemplate:
errorMessage: "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value."
- it: renders the HelmRelease when the apex is set
documentSelector:
path: kind
value: HelmRelease
set:
topology: Simple
replicationFactor: 2
_namespace:
host: example.org
ingress: tenant-root
_cluster:
issuer-name: letsencrypt-prod
solver: http01
asserts:
- equal:
path: kind
value: HelmRelease
@@ -109,6 +109,30 @@ Usage: {{ include "cozy-lib.ns-host" . }}
{{- (.Values._namespace | default dict).host | default "" }}
{{- end }}
{{/*
Get the host for this namespace, requiring a usable apex.
Returns the same value as cozy-lib.ns-host, but fails the render when
the platform has injected _namespace and its host is empty AND this app
sets no host override (.Values.host), instead of silently composing a
malformed "<sub>." hostname into an Ingress, HTTPRoute or Certificate
dnsName. The guard is scoped to the injected state on purpose: a
standalone render (helm lint / helm template with no _namespace) has no
apex by design and must stay renderable, so an absent _namespace never
fails. Call it only at a site that actually emits a hostname, so a
tenant with no apex and no public exposure is unaffected. This mirrors
the gateway path (packages/extra/gateway/templates/tenantgateway.yaml),
which fails on an empty apex only when a Gateway is actually rendered.
Usage: {{ include "cozy-lib.ns-host-required" . }}
*/}}
{{- define "cozy-lib.ns-host-required" -}}
{{- $ns := .Values._namespace -}}
{{- $apex := ($ns | default dict).host | default "" -}}
{{- if and $ns (not (.Values.host | default "")) (not $apex) -}}
{{- fail "the tenant apex (_namespace.host) is empty and this app sets no host override, so its ingress hostname would be malformed. Set tenant.spec.host on this tenant, inherit it from a parent that has host set, or set this app's host value." -}}
{{- end -}}
{{- $apex -}}
{{- end }}
{{/*
Get the etcd namespace reference.
Usage: {{ include "cozy-lib.ns-etcd" . }}
Loaded 100 of 213 files, more files were not shown because too many files have changed in this diff. Show more