feat(garage): new chart for the Garage object store #82

Merged
jklein merged 4 commits from feat/garage-chart into main 2026-10-04 15:09:20 +02:00
Member

Implements the approved plan for #81 (plan comment and the two decisions that corrected it: #issuecomment-4456, #issuecomment-4457, #issuecomment-4458).

One Garage node, replication_factor = 1, db_engine = "lmdb", built from template-chart/ to the style guide — upstream's in-tree chart was read as a source for what the application needs, not copied as a form.

What is in it

Chart garage 1.0.0+upv2.4.1, appVersion v2.4.1, image docker.io/dxflrs/garage (multi-arch, not upstream's amd64_garage)
Objects StatefulSet, ConfigMap (garage.toml, secret-free), two PVCs, ClusterIP Service, two OnePasswordItems, Ingress (off by default, built to the convention), NOTES.txt
Mandatory durability settings metadata_fsync = true (off in Garage's defaults), metadata_auto_snapshot_interval = "6h", metadata_snapshots_dir on a second volume
Storage layout PVC 1 carries metadata_dir and data_dir as two subdirectories; PVC 2 carries the snapshots
CI ci/garage/ fixtures (no exclusion), publish-garage.yml, garage in change-app-version.yml, a customManagers block with a version constraint
Wiki new garage page, row in Charts, entry in _Sidebar.md, a Lessons-Learned entry — see below

The two plan corrections, and why they were needed

Both were found by reading the source at tag v2.4.1, posted on the issue before any code was written, and decided there.

1. rpc_secret_file cannot work in a hardened pod. read_secret_file rejects any file whose mode carries a group or other bit (src/garage/secrets.rs:144, mask 0o077 — the message says "world-readable", the mask tests the group as well), while the kubelet OR-s the group-read bit onto every file of a secret volume as soon as fsGroup is set (pkg/volume/volume_linux.go, Chmod(info.Mode() | 0440) for a read-only volume). Without fsGroup the file stays root-owned and uid 20000 cannot read it. Kubernetes can set the group of such a file, never its owner, so no combination satisfies both checks. The secret therefore arrives as GARAGE_RPC_SECRET through secretKeyRef — nothing in the ConfigMap, no safety check switched off. The mechanism is written at the value, because without it the next round "fixes" this back to the file form and the pod stops starting.

2. The first start is done by the server. garage server --single-node creates the cluster layout in-process (initial_config in src/garage/server.rs) before the S3, web and admin listeners are spawned, is idempotent on the layout version (0 creates, 1 does nothing, >1 refuses to start, a node that sees peers refuses as well) and has no --version argument that could be reused — the trap of the CLI path does not exist here. --default-access-key / --default-bucket import key and bucket from the vault item, so the credential travels into Garage and the item is the source of truth instead of a copy of what the server generated. Both premises of the original "manual first start" decision fell with that; NOTES.txt keeps the manual path for singleNode: false.

The readiness decision, measured against the source

The question was whether the S3 API answers before the layout is applied. It does: the S3 server is spawned independently of the layout state, so the socket binds and answers — with errors. A tcpSocket probe on 3900 would therefore have been exactly the lie the plan warned about.

  • Readiness: GET /health on the admin port. Token-free (@special in router_v2.rs), and it maps cluster health onto the status code: 200 for Healthy/Degraded, 503 for Unavailable — and an unconfigured layout is a special case that yields Unavailable (src/rpc/system.rs, "Layout not yet configured, special case" → src/api/admin/special.rs).
  • Liveness and startup: TCP connect to the admin port, deliberately not /health. A node waiting for its layout would otherwise be killed and restarted forever, and a restart is precisely what cannot fix it — the same lesson as zipline (README 12.3), in its sharpest form.

Fixtures rather than an exclusion

With --single-node the pod reaches Ready by itself, so the install-test is an assertion instead of a formality. A green run proves: Garage accepts metadata_dir/data_dir as two subdirectories of one PVC and creates them itself (the image has no mkdir), readOnlyRootFilesystem: true holds with no emptyDir anywhere, the layout really is applied in-process and /health answers 200 afterwards (Ready is that 200), the rendered garage.toml parses as Garage reads it, and the access key and bucket are imported from the vault fields — Garage validates all three and refuses to start on a malformed one.

What was verified locally, and what could not be

No cluster in this workspace (no k3d, no usable container runtime), so nothing here is a runtime claim; the install-test in this PR is the first time the chart runs.

  • helm lint --strict garage — 0 failed, only the expected [INFO] Missing required value lines for the two required() secret refs (same shape as valkey).
  • helm template against chart defaults and against ci/garage/test-values.yaml; the rendered garage.toml parsed with tomllib in both cases (top-level keys, [s3_api], [s3_web], [admin] all as intended).
  • Null-safety: --set X=null for all eleven chart-owned sub-blocks (ports, s3, web, resources, the three probes, securityContext and both of its halves, env) — every one falls back to the chart's defaults, checked on the rendered text rather than on the exit code.
  • Fail paths, 14 of them, each aborting as intended: both required() secrets, defaultAccessKey without singleNode (the render fails where Garage would otherwise exit at start), unknown keys at both levels, replicas: 2, a dbEngine outside the enum, an empty snapshot interval, replicationFactor: 0, a port out of range, scalars where a mapping belongs, and the Ingress without domain/issuer.
  • ci/run-quantity-render-test.sh garage — both cases exercised the resource helper, no exponential notation.
  • NOTES.txt rendered through the ConfigMap-wrap trick (README 12.2, since helm template does not render it) in all three shapes: defaults, manual path, CI values.
  • helm package garage — garage-1.0.0+upv2.4.1.tgz.
  • Not run: trivy is not installed in this workspace, so the config scan runs for the first time in this PR.

What stays open — hence Part of #81, not Fixes

The plan's measurement points are not all closed, and one of them is about this chart's central durability claim:

  1. Does the metadata auto-snapshot really land in metadata_snapshots_dir? Not closed, and not closable in CI: the first snapshot is due after interval/2, which is longer than the run — and shortening the interval to seconds would test the timer rather than the thing. This needs one look at the running service after rollout.
  2. Longhorn's snapshot semantics, on which the whole one-PVC layout rests, are a property of the storage layer; the runner has local-path.
  3. Durability of metadata_fsync is not observable in a container at all.
  4. Whether readOnlyRootFilesystem and the subdirectory layout hold is closed — by the install-test in this PR, and only if it goes green.

Part of #81

Implements the approved plan for #81 (plan comment and the two decisions that corrected it: #issuecomment-4456, #issuecomment-4457, #issuecomment-4458). One Garage node, `replication_factor = 1`, `db_engine = "lmdb"`, built from `template-chart/` to the style guide — upstream's in-tree chart was read as a source for *what the application needs*, not copied as a form. ## What is in it | | | | --- | --- | | Chart | `garage` 1.0.0+upv2.4.1, appVersion `v2.4.1`, image `docker.io/dxflrs/garage` (multi-arch, not upstream's `amd64_garage`) | | Objects | StatefulSet, ConfigMap (`garage.toml`, secret-free), two PVCs, ClusterIP Service, two `OnePasswordItem`s, Ingress (off by default, built to the convention), `NOTES.txt` | | Mandatory durability settings | `metadata_fsync = true` (**off** in Garage's defaults), `metadata_auto_snapshot_interval = "6h"`, `metadata_snapshots_dir` on a **second** volume | | Storage layout | PVC 1 carries `metadata_dir` **and** `data_dir` as two subdirectories; PVC 2 carries the snapshots | | CI | `ci/garage/` fixtures (no exclusion), `publish-garage.yml`, `garage` in `change-app-version.yml`, a `customManagers` block with a version constraint | | Wiki | new `garage` page, row in `Charts`, entry in `_Sidebar.md`, a `Lessons-Learned` entry — see below | ## The two plan corrections, and why they were needed Both were found by reading the source at tag `v2.4.1`, posted on the issue before any code was written, and decided there. **1. `rpc_secret_file` cannot work in a hardened pod.** `read_secret_file` rejects any file whose mode carries a group *or* other bit (`src/garage/secrets.rs:144`, mask `0o077` — the message says "world-readable", the mask tests the group as well), while the kubelet OR-s the group-read bit onto every file of a secret volume as soon as `fsGroup` is set (`pkg/volume/volume_linux.go`, `Chmod(info.Mode() | 0440)` for a read-only volume). Without `fsGroup` the file stays root-owned and uid 20000 cannot read it. Kubernetes can set the **group** of such a file, never its **owner**, so no combination satisfies both checks. The secret therefore arrives as `GARAGE_RPC_SECRET` through `secretKeyRef` — nothing in the ConfigMap, no safety check switched off. The mechanism is written at the value, because without it the next round "fixes" this back to the file form and the pod stops starting. **2. The first start is done by the server.** `garage server --single-node` creates the cluster layout in-process (`initial_config` in `src/garage/server.rs`) **before** the S3, web and admin listeners are spawned, is idempotent on the layout version (0 creates, 1 does nothing, >1 refuses to start, a node that sees peers refuses as well) and has no `--version` argument that could be reused — the trap of the CLI path does not exist here. `--default-access-key` / `--default-bucket` **import** key and bucket from the vault item, so the credential travels *into* Garage and the item is the source of truth instead of a copy of what the server generated. Both premises of the original "manual first start" decision fell with that; `NOTES.txt` keeps the manual path for `singleNode: false`. ## The readiness decision, measured against the source The question was whether the S3 API answers before the layout is applied. It does: the S3 server is spawned independently of the layout state, so the socket binds and answers — with errors. A `tcpSocket` probe on 3900 would therefore have been exactly the lie the plan warned about. - **Readiness**: `GET /health` on the admin port. Token-free (`@special` in `router_v2.rs`), and it maps cluster health onto the status code: 200 for `Healthy`/`Degraded`, **503** for `Unavailable` — and an unconfigured layout is a special case that yields `Unavailable` (`src/rpc/system.rs`, "Layout not yet configured, special case" → `src/api/admin/special.rs`). - **Liveness and startup**: TCP connect to the admin port, deliberately **not** `/health`. A node waiting for its layout would otherwise be killed and restarted forever, and a restart is precisely what cannot fix it — the same lesson as `zipline` (README 12.3), in its sharpest form. ## Fixtures rather than an exclusion With `--single-node` the pod reaches Ready by itself, so the install-test is an assertion instead of a formality. A green run proves: Garage accepts `metadata_dir`/`data_dir` as two subdirectories of **one** PVC and creates them itself (the image has no `mkdir`), `readOnlyRootFilesystem: true` holds with no emptyDir anywhere, the layout really is applied in-process and `/health` answers 200 afterwards (Ready *is* that 200), the rendered `garage.toml` parses as Garage reads it, and the access key and bucket are imported from the vault fields — Garage validates all three and refuses to start on a malformed one. ## What was verified locally, and what could not be No cluster in this workspace (no k3d, no usable container runtime), so **nothing here is a runtime claim**; the install-test in this PR is the first time the chart runs. - `helm lint --strict garage` — 0 failed, only the expected `[INFO] Missing required value` lines for the two `required()` secret refs (same shape as `valkey`). - `helm template` against chart defaults **and** against `ci/garage/test-values.yaml`; the rendered `garage.toml` parsed with `tomllib` in both cases (top-level keys, `[s3_api]`, `[s3_web]`, `[admin]` all as intended). - Null-safety: `--set X=null` for all eleven chart-owned sub-blocks (`ports`, `s3`, `web`, `resources`, the three probes, `securityContext` and both of its halves, `env`) — every one falls back to the chart's defaults, checked on the rendered text rather than on the exit code. - Fail paths, 14 of them, each aborting as intended: both `required()` secrets, `defaultAccessKey` without `singleNode` (the render fails where Garage would otherwise exit at start), unknown keys at both levels, `replicas: 2`, a `dbEngine` outside the enum, an empty snapshot interval, `replicationFactor: 0`, a port out of range, scalars where a mapping belongs, and the Ingress without domain/issuer. - `ci/run-quantity-render-test.sh garage` — both cases exercised the resource helper, no exponential notation. - `NOTES.txt` rendered through the ConfigMap-wrap trick (README 12.2, since `helm template` does not render it) in all three shapes: defaults, manual path, CI values. - `helm package garage` — `garage-1.0.0+upv2.4.1.tgz`. - **Not run**: `trivy` is not installed in this workspace, so the config scan runs for the first time in this PR. ## What stays open — hence `Part of #81`, not `Fixes` The plan's measurement points are not all closed, and one of them is about this chart's central durability claim: 1. **Does the metadata auto-snapshot really land in `metadata_snapshots_dir`?** Not closed, and not closable in CI: the first snapshot is due after `interval/2`, which is longer than the run — and shortening the interval to seconds would test the timer rather than the thing. This needs one look at the running service after rollout. 2. **Longhorn's snapshot semantics**, on which the whole one-PVC layout rests, are a property of the storage layer; the runner has `local-path`. 3. **Durability of `metadata_fsync`** is not observable in a container at all. 4. Whether `readOnlyRootFilesystem` and the subdirectory layout hold *is* closed — by the install-test in this PR, and only if it goes green. Part of #81
jlab-bot added 1 commit 2026-10-03 22:30:40 +02:00
feat(garage): new chart for the Garage object store
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
Validate Helm Charts / Build garage (pull_request) Successful in 6s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 16s
Install-test / Install-test garage (pull_request) Successful in 1m15s
Validate Helm Charts / Install-test (pull_request) Successful in 1m15s
79b7e44fdc
One Garage node with replication_factor = 1, built from template-chart to the
style guide rather than copied from upstream's in-tree chart (Gitea #81). The
single-node trade is deliberate and its price is named at the value: the factor
applies to the metadata as well, where upstream calls a corrupted database
"generally not recoverable" and a replicated volume replicates the corruption
faithfully. The three countermeasures that work at factor 1 are therefore
mandatory, not optional - metadata_fsync = true (off in Garage's defaults), the
automatic metadata snapshots, and those snapshots on a second PVC.

Storage: PVC 1 carries metadata_dir AND data_dir as two subdirectories, because
volume snapshots are atomic per volume and that is what keeps index and blocks
consistent with each other; PVC 2 carries metadata_snapshots_dir, because a
recovery point must not die with the volume whose corruption it should repair.
Garage creates all three directories itself, which matters because the image is
FROM scratch and has no mkdir.

Two corrections to the approved plan, both verified against the source at tag
v2.4.1 and decided on the issue before implementation:

- The RPC secret arrives as GARAGE_RPC_SECRET via secretKeyRef, not through
  rpc_secret_file. The file form cannot work in a non-root pod:
  read_secret_file rejects any mode with a group or other bit set
  (src/garage/secrets.rs:144, mask 0o077) while the kubelet OR-s the group-read
  bit onto every file of a secret volume once fsGroup is set
  (pkg/volume/volume_linux.go), and Kubernetes can set the group of such a file
  but never its owner. The mechanism is documented at the value so the next
  round does not "fix" it back.
- The first start is done by the server, not by hand: garage server
  --single-node creates the layout in-process before any listener opens, is
  idempotent on the layout version and has no --version argument that could be
  reused, and --default-access-key/--default-bucket import key and bucket FROM
  the vault item. Both premises of the original "manual first start" decision
  fell with that.

Probes follow from the same source reading: readiness is GET /health on the
admin port, which answers 503 while no layout exists; liveness and startup are
a TCP connect, because a probe that can only fail would restart a node waiting
for its layout forever (same lesson as zipline, README 12.3). The S3 listener
binds independently of the layout, so a TCP probe on 3900 would have been the
lie the plan warned about.

CI: fixtures under ci/garage/ rather than an exclusion - with --single-node the
pod reaches Ready on its own, which makes the install-test an assertion about
the layout, the one-PVC subdirectory layout and readOnlyRootFilesystem instead
of a formality. The Renovate block carries a regex versioning anchored to
^v<major>.<minor>.<patch>$, because the registry receives 40-character
commit-SHA tags almost daily (measured 2026-10-03: the newest 22 tags are all
of that kind) and the four v1.99.x-internal builds sort above the v1.3.x line.

Part of #81
Author
Member

CI result, with the output rather than the tick

Run 444, all six contexts green:

Job Output
Build garage 1 chart(s) linted, 0 chart(s) failed · Successfully packaged chart and saved it to: .../garage-1.0.0+upv2.4.1.tgz
Resource quantities both cases exercised the helper, no exponential notation
Trivy config garage Rendered 8 manifests. · garage.yaml kubernetes 0 misconfigurations · garage: 0 to act on, 3 excepted (repo-wide documented exceptions; the per-finding list lives in the job summary, not in the log, so I am not naming the three)
Install-test garage Waiting for statefulset.apps/ci-garage-sfs... → ci-garage-sfs-0 1/1 Running 0 34s → OK: chart 'garage' installed and all workloads are Ready. on k3s v1.35.5, Ready about 33s after the install

Why those 33 seconds are the interesting part of this run. Ready on this chart is an HTTP 200 from /health, and /health answers 503 while no cluster layout exists. So the line above is not "the container did not crash" — it is, in one assertion:

  • garage server --single-node created and applied the cluster layout in-process;
  • Garage accepted metadata_dir and data_dir as two subdirectories of one PVC and created them itself (the image has no mkdir, so a wrong assumption here could not have started at all);
  • readOnlyRootFilesystem: true held with no emptyDir anywhere;
  • the rendered garage.toml parsed as Garage reads it (it opened LMDB and validated replication_factor against --single-node);
  • and the four vault fields passed Garage's own validation — access-key, secret-key and bucket are imported on start and a malformed one aborts it.

Three of the plan's four measurement points are closed by that, on every future PR as well.

What the run says nothing about, unchanged from the PR description: the metadata snapshot is not observed (the first one is due after half the interval, longer than the run — the CI values set 30m so the worker is scheduled, and that is all), Longhorn is not involved (the runner has local-path), and durability is not testable in a container. The snapshot check is step 6 of the wiki page's checklist, to be done once after rollout.

Nothing in this PR was run against a cluster before this job: there is no k3d and no usable container runtime in the authoring workspace, so every local check was a render, a lint, a schema validation or a parse.

## CI result, with the output rather than the tick Run [444](/jlab-cluster/helm-charts/actions/runs/444), all six contexts green: | Job | Output | | --- | --- | | Build garage | `1 chart(s) linted, 0 chart(s) failed` · `Successfully packaged chart and saved it to: .../garage-1.0.0+upv2.4.1.tgz` | | Resource quantities | both cases exercised the helper, no exponential notation | | Trivy config garage | `Rendered 8 manifests.` · `garage.yaml kubernetes 0` misconfigurations · `garage: 0 to act on, 3 excepted` (repo-wide documented exceptions; the per-finding list lives in the job summary, not in the log, so I am not naming the three) | | Install-test garage | `Waiting for statefulset.apps/ci-garage-sfs...` → `ci-garage-sfs-0 1/1 Running 0 34s` → `OK: chart 'garage' installed and all workloads are Ready.` on k3s v1.35.5, Ready about 33s after the install | **Why those 33 seconds are the interesting part of this run.** Ready on this chart *is* an HTTP 200 from `/health`, and `/health` answers 503 while no cluster layout exists. So the line above is not "the container did not crash" — it is, in one assertion: - `garage server --single-node` created and applied the cluster layout in-process; - Garage accepted `metadata_dir` and `data_dir` as two **subdirectories of one PVC** and created them itself (the image has no `mkdir`, so a wrong assumption here could not have started at all); - `readOnlyRootFilesystem: true` held with **no** emptyDir anywhere; - the rendered `garage.toml` parsed as Garage reads it (it opened LMDB and validated `replication_factor` against `--single-node`); - and the four vault fields passed Garage's own validation — `access-key`, `secret-key` and `bucket` are imported on start and a malformed one aborts it. Three of the plan's four measurement points are closed by that, on every future PR as well. **What the run says nothing about**, unchanged from the PR description: the metadata snapshot is not observed (the first one is due after half the interval, longer than the run — the CI values set `30m` so the worker is scheduled, and that is all), Longhorn is not involved (the runner has `local-path`), and durability is not testable in a container. The snapshot check is step 6 of the wiki page's checklist, to be done once after rollout. Nothing in this PR was run against a cluster before this job: there is no k3d and no usable container runtime in the authoring workspace, so every local check was a render, a lint, a schema validation or a parse.
jklein added 1 commit 2026-10-03 22:40:49 +02:00
fix(garage): compressionLevel disables with "none", never with 0
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 12s
Validate Helm Charts / Build garage (pull_request) Successful in 7s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 16s
Install-test / Install-test garage (pull_request) Successful in 1m17s
Validate Helm Charts / Install-test (pull_request) Successful in 1m17s
5d10b02458
My own values comment claimed 0 switches compression off. It does not:
Garage's deserializer takes an integer OR the literal string "none"
(src/util/config.rs, deserialize_compression), and 0 is a valid integer that
zstd reads as its default level - 3, i.e. MORE compression than the chart's 1,
not none. A comment that invites an operator to write 0 for "off" is the
values-that-must-not-lie shape with the lie in the documentation instead of in
the template.

The schema now accepts an integer (negative included, those are zstd's fast
modes) or exactly "none", and the ConfigMap quotes the string form so the
rendered TOML stays valid. Verified: 1, 7, -3 render unquoted, "none" renders
quoted and the result still parses as TOML, and any other string is rejected by
the schema.

No version bump: the chart is not published yet.
jklein added 1 commit 2026-10-03 22:42:21 +02:00
docs(garage): state the admin surface precisely, including open /metrics
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
Validate Helm Charts / Build garage (pull_request) Successful in 7s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 15s
Install-test / Install-test garage (pull_request) Successful in 1m22s
Validate Helm Charts / Install-test (pull_request) Successful in 1m23s
01336af953
"No admin_token is set" read like "nothing is open", and two parts of that
were wrong. /metrics needs no authentication either, because
metrics_require_token defaults to false - internal metrics on a ClusterIP port.
And a config token is not the only way to reach the authenticated API: Garage
2.x keeps admin tokens in the cluster as well ("garage admin-token create"),
which work without any entry in garage.toml. Both now stated at the ConfigMap
and the Service, plus the way to close /metrics.
jklein added 1 commit 2026-10-03 22:45:58 +02:00
fix(garage): a comment in the ConfigMap tripped Trivy's KSV-0109
Validate Helm Charts / Detect changed charts (pull_request) Successful in 2s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
Validate Helm Charts / Build garage (pull_request) Successful in 5s
Install-test / Install-test garage (pull_request) Successful in 1m20s
Validate Helm Charts / Install-test (pull_request) Successful in 1m20s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 16s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Garage Helm Chart / Publish Garage Helm Chart (push) Successful in 10s
SonarQube Analysis / SonarQube Analysis (push) Successful in 36s
b19da4e580
Run 446 reported one HIGH finding, and it was prose, not data: the line
"metrics_require_token = true plus a token to close it" sits inside the
ConfigMap's data block, and KSV-0109 reads a <key> = <value> assignment with a
credential-ish key name as a stored secret - comments included. The ConfigMap
holds no credential at all; the RPC secret arrives as an environment variable
precisely so that it cannot.

Reworded rather than excepted: an entry in ci/trivy/trivyignore.yaml would
carry a permanent exception for one sentence, and the next reader of the report
would have to re-establish that the ConfigMap really is secret-free. The
replacement names the mechanism at the line, so the shape does not come back.
Author
Member

Three follow-up commits, and what the middle run found

The head is now b19da4e, run 447, all six contexts green:

Job Output
Build garage 1 chart(s) linted, 0 chart(s) failed · New chart (does not exist on main), version: 1.0.0+upv2.4.1 · Successfully packaged chart …/garage-1.0.0+upv2.4.1.tgz
Trivy config garage garage.yaml kubernetes 0 · garage: 0 to act on, 3 excepted
Install-test garage ci-garage-sfs-0 1/1 Running 0 41s · OK: chart 'garage' installed and all workloads are Ready.
Resource quantities both cases exercised the helper, no exponential notation

1. compressionLevel does not disable with 0 (5d10b02). My own values comment said it did. Garage's deserializer takes an integer or the literal string "none" (src/util/config.rs, deserialize_compression), and 0 is a perfectly valid integer that zstd reads as its default level — 3, i.e. more compression than the chart's 1, not none. So the comment invited an operator to turn compression up while believing they had switched it off: the "values that must not lie" shape with the lie in the documentation rather than in the template. The schema now takes an integer (negatives included, those are zstd's fast modes) or exactly "none", and the ConfigMap quotes the string form. Verified: 1, 7, -3 render unquoted, "none" renders quoted and the result still parses as TOML, any other string is rejected by the schema.

2. The admin surface was stated too favourably (01336af). "No admin_token is set" reads as "nothing is open", and two parts of that were wrong: /metrics needs no authentication either, because the require-token switch defaults to false — internal metrics on a ClusterIP port — and a config token is not the only route to the authenticated API, since Garage 2.x also keeps admin tokens in the cluster (garage admin-token create), which work without any entry in garage.toml. Both are now named at the ConfigMap and the Service, together with the way to close /metrics.

3. And run 446 found something worth recording (b19da4e). Commit 2 turned the Trivy scan red-ish — one HIGH finding, KSV-0109, ConfigMap stores secrets:

KSV-0109 (HIGH): ConfigMap 'scan-garage-configmap' stores secrets in key(s) or value(s)
'{"#             metrics_require_token "}'

It was prose, not data: a comment line inside the ConfigMap's data block, shaped like <key> = <value> with a credential-ish key name. The ConfigMap holds no credential at all — the RPC secret arrives as an environment variable specifically so that it cannot.

Reworded rather than excepted. An entry in ci/trivy/trivyignore.yaml would park a permanent exception for one sentence, and the next reader of that report would have to re-establish that this ConfigMap really is secret-free — on the one chart whose design point is that it is. The replacement names the mechanism at the line so the shape does not come back. Note for other charts: the check reads comments inside data: as data, and /metrics and admin_token_file in prose were fine — the = assignment was not.

Nothing else changed; the open measurement points in the description are unchanged.

## Three follow-up commits, and what the middle run found The head is now `b19da4e`, run [447](/jlab-cluster/helm-charts/actions/runs/447), all six contexts green: | Job | Output | | --- | --- | | Build garage | `1 chart(s) linted, 0 chart(s) failed` · `New chart (does not exist on main), version: 1.0.0+upv2.4.1` · `Successfully packaged chart …/garage-1.0.0+upv2.4.1.tgz` | | Trivy config garage | `garage.yaml kubernetes 0` · `garage: 0 to act on, 3 excepted` | | Install-test garage | `ci-garage-sfs-0 1/1 Running 0 41s` · `OK: chart 'garage' installed and all workloads are Ready.` | | Resource quantities | both cases exercised the helper, no exponential notation | **1. `compressionLevel` does not disable with `0` (`5d10b02`).** My own values comment said it did. Garage's deserializer takes an integer **or** the literal string `"none"` (`src/util/config.rs`, `deserialize_compression`), and `0` is a perfectly valid integer that zstd reads as *its* default level — 3, i.e. **more** compression than the chart's `1`, not none. So the comment invited an operator to turn compression up while believing they had switched it off: the "values that must not lie" shape with the lie in the documentation rather than in the template. The schema now takes an integer (negatives included, those are zstd's fast modes) or exactly `"none"`, and the ConfigMap quotes the string form. Verified: `1`, `7`, `-3` render unquoted, `"none"` renders quoted and the result still parses as TOML, any other string is rejected by the schema. **2. The admin surface was stated too favourably (`01336af`).** "No `admin_token` is set" reads as "nothing is open", and two parts of that were wrong: `/metrics` needs no authentication either, because the require-token switch defaults to `false` — internal metrics on a ClusterIP port — and a config token is not the only route to the authenticated API, since Garage 2.x also keeps admin tokens in the cluster (`garage admin-token create`), which work without any entry in `garage.toml`. Both are now named at the ConfigMap and the Service, together with the way to close `/metrics`. **3. And run [446](/jlab-cluster/helm-charts/actions/runs/446) found something worth recording (`b19da4e`).** Commit 2 turned the Trivy scan red-ish — one HIGH finding, `KSV-0109`, *ConfigMap stores secrets*: ``` KSV-0109 (HIGH): ConfigMap 'scan-garage-configmap' stores secrets in key(s) or value(s) '{"# metrics_require_token "}' ``` It was **prose, not data**: a comment line inside the ConfigMap's data block, shaped like `<key> = <value>` with a credential-ish key name. The ConfigMap holds no credential at all — the RPC secret arrives as an environment variable specifically so that it cannot. Reworded rather than excepted. An entry in `ci/trivy/trivyignore.yaml` would park a permanent exception for one sentence, and the next reader of that report would have to re-establish that this ConfigMap really is secret-free — on the one chart whose design point is that it is. The replacement names the mechanism at the line so the shape does not come back. Note for other charts: the check reads **comments inside `data:`** as data, and `/metrics` and `admin_token_file` in prose were fine — the `=` assignment was not. Nothing else changed; the open measurement points in the description are unchanged.
jklein approved these changes 2026-10-04 15:09:15 +02:00
jklein merged commit b19da4e580 into main 2026-10-04 15:09:20 +02:00
jklein deleted branch feat/garage-chart 2026-10-04 15:09:21 +02:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jlab-cluster/helm-charts#82