feat(clickhouse): let the backup sidecar trust a private S3 CA, unblocking the backup e2e round-trip - #3385
Conversation
📝 WalkthroughWalkthroughAdds optional S3 endpoint CA support to ClickHouse backups. The change updates API and chart configuration, mounts a Secret in the backup sidecar, propagates CA settings through the example harness, improves waits and cleanup, and adds Chainsaw validation. ChangesClickHouse backup endpoint CA
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Harness as ClickHouse example harness
participant Kubernetes as Kubernetes Secret
participant ClickHouse as ClickHouse resource
participant Sidecar as clickhouse-backup sidecar
participant S3 as S3 endpoint
Harness->>Kubernetes: Copy endpoint CA bundle
Harness->>ClickHouse: Set backup.endpointCA
ClickHouse->>Sidecar: Mount CA Secret and set SSL_CERT_DIR
Sidecar->>S3: Connect using extended trust store
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
48d7300 to
0b7b444
Compare
Port the intended ClickHouse Altinity backup/restore e2e coverage from #2600 (never-merged hack/e2e-apps/backup-clickhouse.bats) to the current Chainsaw approach, reworked on origin/main where the Altinity backup feature and examples/backups/clickhouse already landed. Two Tests, etcd-suite shaped: - clickhouse-backup-1-contracts (CI): a backup-enabled ClickHouse, asserting the chart<->Altinity-strategy contract against the running cluster — the clickhouse-backup sidecar on the ch-backup-api :7171 port and the populated <release>-backup-api-auth Secret the strategy Pod authenticates with. Needs no reachable object store. - clickhouse-backup-2-roundtrip: the full Bucket -> BackupJob -> Backup -> in-place + to-copy RestoreJob round-trip, driving examples/backups/clickhouse/run-all.sh. Gated out of CI (CLICKHOUSE_E2E_S3_ROUNDTRIP=1) because kind's self-signed SeaweedFS S3 endpoint cannot be TLS-validated by the in-Pod sidecar. Wire clickhouse-application -> "clickhouse clickhouse-backup" in hack/select-e2e.sh so ClickHouse chart edits exercise the backup suite too. Assisted-By: Claude <[email protected]> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
hack/select-install.sh --validate (run in the unit-test job) requires every Chainsaw suite dir to resolve to a PackageSource via suite_to_source(); the new clickhouse-backup suite has no <suite>-application fallback, so map it to cozystack.clickhouse-application, in lockstep with the select-e2e.sh override. Assisted-By: Claude <[email protected]> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
- Add a select-e2e_test.bats case asserting a packages/apps/clickhouse diff selects BOTH the clickhouse and clickhouse-backup suites (mirrors the kubernetes-application multi-suite test). Fails without the src_to_suites override that makes the backup contracts suite run on ClickHouse chart PRs. - Correct the backup-api-auth Secret assertion's prose to match what it proves (present with both keys, not "populated"). - Capture the to-copy restore target's Pod logs in the round-trip catch so a failure isolated to step 07 keeps its diagnostics. Assisted-By: Claude <[email protected]> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
Extend verify-backup-sidecar to assert the sidecar's API_USERNAME/API_PASSWORD env resolve valueFrom the backup-api-auth Secret's username/password keys, not just that the Secret and the :7171 port exist independently. A chart change that repointed or dropped those secretKeyRefs while keeping both would slip through and surface only as 401s at BackupJob time — the silent break this suite exists to catch. Assisted-By: Claude <[email protected]> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
The clickhouse-backup sidecar had no TLS/CA surface at all: bucket coordinates
and credentials arrive as S3_* env, and nothing let it verify an endpoint whose
certificate is signed by a private CA. Cozystack's own in-cluster SeaweedFS is
exactly that case — it serves :8333 behind the self-signed "SeaweedFS CA" — so a
ClickHouse release could only ever back up to a publicly-trusted endpoint, while
MariaDB (storage.s3.tls.caSecretKeyRef) and Postgres (barman endpointCA) have had
a CA field all along.
Add backup.endpointCA {name, key}. When set, the chart mounts that Secret into
the sidecar and points SSL_CERT_DIR at it. Empty (the default) renders no env, no
mount and no volume, so existing releases are untouched.
SSL_CERT_DIR rather than AWS_CA_BUNDLE, measured against
altinity/clickhouse-backup:2.7.4 with a self-signed endpoint and public AWS S3 in
turn: Go reads SSL_CERT_DIR in addition to the system bundle, so both verify;
AWS_CA_BUNDLE replaces the SDK trust pool, so the private endpoint verifies and
public S3 starts failing `x509: certificate signed by unknown authority`. That
would silently break a release that also talks to a public endpoint.
S3_DISABLE_CERT_VERIFICATION was rejected outright — it drops verification
instead of trusting one more CA.
Covered by helm-unittest: the unset default, the mount/env/volume wiring, a
custom key still projected as ca.crt, composition with useSystemBucket, and that
backup.enabled=false renders nothing.
Assisted-By: Claude <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: Andrey Kolkov <[email protected]>
The round-trip Test was skipping: it exited 0 behind CLICKHOUSE_E2E_S3_ROUNDTRIP, so "PASS (1.01s)" meant the gate fired, not that backup and restore worked. Only the contracts Test ran, which proves the chart still exposes the sidecar and its auth Secret — not that a byte ever reaches S3 and comes back. The gate existed because the sidecar could not verify the in-cluster SeaweedFS certificate. backup.endpointCA closes that, so adopt the shape mariadb and postgres already use to run their round-trips un-gated: - examples: resolve the S3 endpoint from S3_ENDPOINT when set (BucketInfo advertises the external ingress URL, an unroutable placeholder in CI), copy the seaweedfs CA (auto-discovered from its cert-manager Certificate when the fullname default is absent) into a per-release Secret, and reference it from backup.endpointCA on both the source and the to-copy target. NAMESPACE now defaults to tenant-root, matching the other backup demos. - suite: drop the gate, run in tenant-root — an isolated tenant's Cilium egress allowlist blocks its Pods from reaching tenant-root's seaweedfs — and pre-clean with cleanup.sh, because tenant-root is shared and a leftover Succeeded BackupJob would satisfy the harness's wait without a backup running. cleanup.sh also drops the copied CA Secret; the platform's own is never touched. Assisted-By: Claude <[email protected]> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
Review findings on the previous two commits. 1. TIA no longer selected the suite that runs the harness. select-e2e.sh maps examples/backups/<app>/** to suite <app>, so with the round-trip living in a separate clickhouse-backup/ dir an edit to the harness selected `clickhouse` — which did not execute those scripts — while the suite that does was left out. Fixed the way postgres/mariadb/etcd already do it: both Tests move into hack/e2e-chainsaw/clickhouse/ as clickhouse-1-backup-contracts and clickhouse-2-backup-roundtrip. That also deletes every special case the earlier commits added (src_to_suites, suite_to_source, and the bats case that guarded them — both scripts are now byte-identical to main), and the suite list in hack/e2e-chainsaw/README.md stays correct because no dir is added. The bats case is replaced by one asserting the invariant that actually matters: an examples/backups/clickhouse edit selects the suite that runs it. 2. The docs claimed a fail-fast the harness did not have, and the budget was sized as if it did. wait_for_field polled to the full timeout with no terminal-value escape, and the HelmRelease/StatefulSet waits were bare `kubectl wait` with no existence backstop and no Stalled fail-fast. Ported wait_for_field's fail_value parameter and wait_hr_ready from the postgres helpers, added wait_sts_ready, and routed all eight waits through them. Verified against a fake kubectl: a Failed BackupJob now bails in 3s instead of 600s, a Stalled HelmRelease immediately. The inner budgets sum to 3900s, which the old 30m op timeout did not cover — a hang would have been SIGKILLed with no diagnostics, the failure mode .chainsaw.yaml calls strictly worse — so the timeout is now 65m with the arithmetic written out. 3. The strategy Pod ran `apk add --no-cache curl jq` on every backup and restore. Harmless while nothing in CI ran it; a per-PR dependency on the Alpine CDN once it does, and broken outright air-gapped — which is exactly why the shipped strategy bakes curl+jq into its image. Step 01 now reads that image off cozy-default-altinity and drops the apk add, falling back to alpine only when the platform strategy is absent. Also bounded the two polling curls with --max-time; a hung connection previously stalled the loop until the outer timeout. 4. 91-scenario-user-backup.md still documented the old tenant-test default. 5. .chainsaw.yaml claimed all app tests run in tenant-test, which the postgres and mariadb round-trips already contradicted before this branch. The round-trip header now also states its scope plainly: it exercises the example's strategy, not the shipped cozy-default-altinity. Assisted-By: Claude <[email protected]> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
Second review pass on this branch.
1. The op timeout equalled the inner sum instead of exceeding it: the comment
enumerated 3900s of inner waits and then set 65m, and 3900s IS 65m — so there
was zero headroom for the applies and SQL calls between those waits, and the
claim of "room" was false. Sized the inner budgets against what CI actually
takes instead (the bucket suite completes in ~37s and a backup-enabled
ClickHouse plus Keeper reaches Ready in ~98s, measured on run 30353894332):
bucket claim/access 300->180 and the three job waits 600->480, summing to
3300s, under a 60m op timeout. Shrinking the inner budgets rather than
raising the ceiling also keeps this suite closer to its postgres and mariadb
siblings on a 180-minute job cap.
2. The strategy Pod's securityContext comment still justified root with "Pod
runs apk add at startup", which the previous commit made conditional — the
default path now runs the platform's pre-baked image and installs nothing.
Scoped the note to the alpine fallback and said explicitly that the field set
matches cozy-default-altinity, runAsNonRoot deliberately unset in both.
3. clickhouse-backup.yaml still referenced clickhouse-backup-{1-contracts,
2-roundtrip}: the file moved unchanged, so the Test rename never reached its
own header and a reader following either name found nothing.
4. wait_sts_ready's timeout diagnostic could not print anything. It selected
clickhouse.altinity.com/chi=${name#chi-}, which yields
clickhouse-<app>-clickhouse-0-0 while the label carries the CHI name
(clickhouse-<app>), and 2>/dev/null then swallowed kubectl's "No resources
found". Ask for the StatefulSet's own first replica by name instead and let
both streams through; verified it now surfaces the Pod events that explain
why the replica never came up.
Also dropped the clickhouse bats case added in the previous commit: it asserted
what the generic examples/backups/<app> -> <app> mapping already guarantees, so
it did not lock the invariant its comment claimed. That invariant is now
recorded where it belongs — in the mapping's own comment in select-e2e.sh.
The dedicated ch-backup-client image this example now inherits is tracked in
#3420; noted there that the command: override is load-bearing in two files.
Assisted-By: Claude <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: Andrey Kolkov <[email protected]>
Recommendations from the third review pass (which returned LGTM; none of these blocked, all three are cheap and make the failure paths honest). 1. The round-trip's catch named nothing. An unnamed `describe HelmRelease` in tenant-root dumps every release on the root tenant of a live cluster and buries the two that matter, so name them (mariadb-2-backup-roundtrip names its release for the same reason). Raised podLogs tail 100 -> 200, since the sidecar logs every action and an S3 error sits well back from the tail by the time a step fails, and added the Keeper Pods: chkeeper.yaml labels them `app: <release>-keeper`, so the existing app.kubernetes.io/instance selectors missed them entirely — and a source ClickHouse that never reaches a ready replica is usually Keeper failing to form a quorum, which explains itself only there. 2. SSL_CERT_DIR is now `/etc/ssl/cozy-s3-ca:/etc/ssl/certs`. The single-directory form works — Go reads the default bundle FILE regardless, which is what the earlier measurement showed — but that left the public-CA half of the contract resting on a file this image happens to ship. Naming the system directory costs nothing and survives a base-image bump that moves the bundle; verified against public AWS S3 that both forms still verify (403 auth, no x509). The mount path stays the bare directory, and the unit test now pins both halves separately — it caught the over-broad edit that had briefly made mountPath a colon list. 3. cleanup.sh returned while the uninstalls it triggered were still running, so the next suite (etcd) could start with helm-controller still draining this one, and the pre-clean could hand step 04 an application whose predecessor was still going away. It now waits for both ClickHouse releases and the Bucket to actually disappear, and — since the script deliberately runs without `set -e` so one absent resource cannot abandon the rest — records those failures and exits non-zero, rather than reporting a teardown that did not settle as success. wait_deleted polls `get` instead of using `kubectl wait --for=delete`, which errors on an already-absent resource: the normal case here, and indistinguishable from a real failure. Sized the two cleanup step budgets to 12m for the same reason the run step is 60m: cleanup.sh's waits now sum to 540s, which the previous 8m did not clear. The Keeper-sized-from-the-server-resources bug the review found outside this diff is filed as #3487. Assisted-By: Claude <[email protected]> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
b89a18b to
0c14d6c
Compare
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/backups/clickhouse/00-helpers.sh`:
- Around line 46-47: The generated CA Secret currently uses a namespace-wide
static name, allowing collisions, overwrites, and unsafe cleanup. In
examples/backups/clickhouse/00-helpers.sh lines 46-47, derive the default
CH_CA_SECRET_NAME from CLICKHOUSE_NAME; in
examples/backups/clickhouse/03-create-bucket.sh lines 90-92, label the generated
Secret and refuse to overwrite an existing Secret without the demo ownership
label; in examples/backups/clickhouse/cleanup.sh lines 34-36, delete the Secret
only when that ownership label is present; update
examples/backups/clickhouse/README.md lines 30 and 60 to document and reference
the release-scoped generated name.
- Around line 188-215: The wait_deleted function must distinguish a missing
resource from kubectl retrieval failures. Update its polling logic to use
kubectl get with --ignore-not-found, treating only a successful empty result as
deletion and returning an error for RBAC, API-server, transport, or other
command failures; preserve the existing timeout and success logging behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 8286c769-d5b1-4639-abb2-3f7f030065de
📒 Files selected for processing (23)
api/apps/v1alpha1/clickhouse/types.goapi/apps/v1alpha1/clickhouse/zz_generated.deepcopy.godocs/operations/backup-classes.mdexamples/backups/clickhouse/00-helpers.shexamples/backups/clickhouse/01-create-strategy.shexamples/backups/clickhouse/03-create-bucket.shexamples/backups/clickhouse/04-create-clickhouse.shexamples/backups/clickhouse/05-create-backupjob.shexamples/backups/clickhouse/06-restore-in-place.shexamples/backups/clickhouse/07-restore-to-copy.shexamples/backups/clickhouse/91-scenario-user-backup.mdexamples/backups/clickhouse/README.mdexamples/backups/clickhouse/cleanup.shhack/e2e-chainsaw/.chainsaw.yamlhack/e2e-chainsaw/clickhouse/chainsaw-test.yamlhack/e2e-chainsaw/clickhouse/clickhouse-backup.yamlhack/select-e2e.shpackages/apps/clickhouse/README.mdpackages/apps/clickhouse/templates/clickhouse.yamlpackages/apps/clickhouse/tests/backup_test.yamlpackages/apps/clickhouse/values.schema.jsonpackages/apps/clickhouse/values.yamlpackages/system/clickhouse-rd/cozyrds/clickhouse.yaml
| # Wait until a namespaced resource is really gone. | ||
| # | ||
| # `kubectl wait --for=delete` is not used: it errors out when the resource is | ||
| # already absent, which is the normal case on a clean namespace (cleanup.sh is | ||
| # idempotent and runs as a pre-clean too), and swallowing that error would also | ||
| # swallow a genuine failure. Polling `get` treats "already gone" and "gone now" | ||
| # as the same success. | ||
| wait_deleted() { | ||
| local resource_type="$1" | ||
| local resource_name="$2" | ||
| local timeout="${3:-300}" | ||
| local elapsed=0 | ||
|
|
||
| while true; do | ||
| if ! kubectl -n "$NAMESPACE" get "$resource_type" "$resource_name" >/dev/null 2>&1; then | ||
| [[ $elapsed -gt 0 ]] && log_success "$resource_type/$resource_name is gone" | ||
| return 0 | ||
| fi | ||
| if [[ $elapsed -ge $timeout ]]; then | ||
| log_error "Timeout waiting for $resource_type/$resource_name to be deleted; it is still present after ${timeout}s:" | ||
| kubectl -n "$NAMESPACE" get "$resource_type" "$resource_name" -o wide >&2 || true | ||
| return 1 | ||
| fi | ||
| [[ $elapsed -eq 0 ]] && log_substep "Waiting for $resource_type/$resource_name to be deleted..." | ||
| sleep 5 | ||
| elapsed=$((elapsed + 5)) | ||
| done | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file line count and context =="
wc -l examples/backups/clickhouse/00-helpers.sh
sed -n '180,225p' examples/backups/clickhouse/00-helpers.sh
echo
echo "== wait_deleted usages =="
rg -n "wait_deleted|\$wait_deleted|wait_deleted\(" examples/backups/clickhouse -S
echo
echo "== log functions =="
rg -n "log_error|log_success|log_substep" examples/backups/clickhouse/00-helpers.sh -SRepository: cozystack/cozystack
Length of output: 3963
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== cleanup context =="
cat -n examples/backups/clickhouse/cleanup.sh | sed -n '1,60p'
echo
echo "== kubectl version/config if available =="
kubectl version --client --short 2>/dev/null || true
kubectl config current-context 2>/dev/null || trueRepository: cozystack/cozystack
Length of output: 3024
🌐 Web query:
kubectl get resource missing NotFound --ignore-not-found documentation
💡 Result:
The --ignore-not-found flag in kubectl get is used to suppress the "NotFound" error and return an exit code of 0 when a specific, requested object does not exist [1][2][3]. Key behaviors and details: - Purpose: When set to true, it prevents kubectl from returning an error if the specific resource you are trying to retrieve cannot be found [1][3]. - Scope: The flag specifically applies when requesting a specific resource by name [1][3]. It has no effect when querying collections of resources (e.g., listing all pods in a namespace or using label selectors), as these commands do not produce a "NotFound" error when no resources are returned; they simply return an empty list [1][3]. - Exit Codes: When used with a specific resource name, setting --ignore-not-found=true ensures the command exits with code 0 even if the resource is missing [2][4][5]. Conversely, if the flag is set to false (the default) and the resource is missing, the command will exit with a non-zero status and display a "NotFound" error [3][5]. For more detailed technical documentation, you can refer to the official Kubernetes kubectl get command reference [1].
Citations:
- 1: https://kubernetes.io/docs/reference/kubectl/generated/kubectl_get/
- 2: Improve ignore-not-found behavior kubernetes/kubernetes#132542
- 3: https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/get/get.go
- 4: https://manpages.debian.org/testing/kubernetes-client/kubectl-get.1.en.html
- 5:
kubectl get --ignore-not-foundbehavior unclear kubernetes/kubectl#1596
Do not treat every kubectl get error as successful deletion.
This returns success for RBAC, API-server, and transport failures, not just NotFound; the cleanup then settles on a false success and can leave half-uninstalled resources for later runs. Use --ignore-not-found for missing resources and fail when any other retrieval query fails.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/backups/clickhouse/00-helpers.sh` around lines 188 - 215, The
wait_deleted function must distinguish a missing resource from kubectl retrieval
failures. Update its polling logic to use kubectl get with --ignore-not-found,
treating only a successful empty result as deletion and returning an error for
RBAC, API-server, transport, or other command failures; preserve the existing
timeout and success logging behavior.
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM with non-blocking notes
backup.endpointCA is opt-in and default-empty: the unset case renders no env/mount/volume (verified by render + a non-vacuous negative unit test), so existing releases are unchanged and no migration is needed. All schema-valid corners render admittable objects; generated artifacts (types.go, values.schema.json, README, cozyrds openAPISchema/keysOrder) are mutually consistent. Notes below are MINOR/observational and none blocks merge.
Findings
[MINOR] packages/apps/clickhouse/templates/clickhouse.yaml (the backup-s3-ca volume) — the projected CA Secret volume carries no optional: true, unlike the useSystemBucket credential env refs which are deliberately optional: true so the Pod starts before projection. If a tenant sets backup.endpointCA.name to a Secret that does not exist yet (or it is later deleted), the whole ClickHouse Pod (not just the sidecar) is stuck in ContainerCreating with MountVolume.SetUp failed ... secret not found on the next rollout. Fail-closed for trust material is a defensible choice and the failure is legible via kubectl describe, but the asymmetry with the adjacent credential design is undocumented — an inline comment stating the intent (or optional for symmetry) would prevent a surprised operator.
[MINOR] examples/backups/clickhouse/03-create-bucket.sh + examples/backups/clickhouse/README.md (step-order table) — the CA-copy probe reads the platform CA Secret cross-namespace in tenant-root with stderr swallowed, so an RBAC denial is misdiagnosed as "CA not found" and the script exits with advice ("set S3_CA_SECRET explicitly") that cannot fix a permissions problem. The README labels step 03 "tenant", but copying the CA requires reading seaweedfs-ca-cert in tenant-root, i.e. an admin/platform capability. Either relabel step 03 or distinguish NotFound from can't-ask in the probe.
Claim mismatches
[PARTIAL] PR body: "hack/select-e2e.sh maps clickhouse-application to clickhouse clickhouse-backup, and hack/select-install.sh maps the new suite to its PackageSource; both mappings have unit tests." The diff does not touch hack/select-install.sh, and the hack/select-e2e.sh change is comment-only. There is no separate clickhouse-backup suite dir — the two backup Tests (clickhouse-1-backup-contracts, clickhouse-2-backup-roundtrip) live in the existing hack/e2e-chainsaw/clickhouse/ suite, so the pre-existing examples/backups/<app>/ path derivation already selects them. The design is coherent; only the body text is stale.
Caveats
- SSL_CERT_DIR additive-trust ("adds to, does not replace, the system bundle") rests on
altinity/clickhouse-backup:2.7.4's Go runtime and is not re-verifiable in a hermetic review. Risk is low: opt-in, and the template appends/etc/ssl/certsexplicitly. - Existing-customer upgrade verified by render only (unset → zero new fields on the running sidecar, so SSA field-ownership is not engaged); a live upgrade replay was out of scope for this static review.
- On a cluster with a publicly-trusted S3 endpoint the demo now fails by default (
S3_CA_SECRETdefaults toseaweedfs-ca-cert, step 03 hard-exits when discovery is empty). TheS3_CA_SECRET=""opt-out is documented, but a "no seaweedfs present → warn and skip" path would be safer for a legal input. backup.endpointCAis wired only into the sidecar, not the legacybackup.scheduleCronJob; combiningschedulewith a private-CA endpoint still fails TLS. The CronJob is deprecated, so note-level.
Recommended follow-ups
clickhouse-2-backup-roundtripbecomes CI-live only with this push and per the PR body has not had a green CI run. Confirm a green CI (or acozystack-pr-testrun) before merge — this gates the PR's own e2e, not customer clusters. Note01-create-strategy.shfalls back toalpine:3.19+apk add(needs internet) whencozy-default-altinityis absent — a possible constrained-CI flake.- Latent / pre-existing (not introduced here):
packages/core/platform/sources/clickhouse-application.yamldependsOnlists only clickhouse-operator + cozystack-engine, while the round-trip needs seaweedfs / bucket / backup-controller. Masked today becausehack/select-install.shis not wired into CI (full platform installed); the deferred "test-minimal" work would expose it for clickhouse and equally mariadb/postgres.
Review notes from IvanHunters on #3385 (LGTM with non-blocking findings). 1. The backup-s3-ca volume carries no `optional: true` while the adjacent useSystemBucket credential refs deliberately do, and the asymmetry was undocumented — an operator naming a Secret that does not exist gets the whole ClickHouse Pod stuck in ContainerCreating, not just the sidecar. Kept the fail-closed behaviour and wrote down why the two cases differ: credentials are projected by the platform after install, so a strict ref would wedge every fresh release until the first BackupJob, whereas this CA is named by the tenant, must already exist, and is trust material — optional would mount an empty dir, the Pod would look healthy, and the first backup would fail deep inside clickhouse-backup with an x509 error naming neither the Secret nor the chart. The comment also states the blast radius plainly. 2. The CA probe swallowed stderr, so an RBAC denial on the cross-namespace read was reported as "CA not found" followed by advice (set S3_CA_SECRET) that cannot fix a permissions problem. It now separates present / absent / not-allowed-to-look and says so, and the README no longer labels step 03 simply "tenant": copying the CA reads a Secret in tenant-root, which is a platform capability. Added a section spelling out the three ways to satisfy that (admin credentials, a one-off admin copy, or skipping the CA entirely on a publicly-trusted endpoint). 3. Caveat, and a usability regression this branch introduced: on a cluster with a publicly-trusted endpoint and no seaweedfs, step 03 hard-exited on a default the user never chose. It now distinguishes an explicitly named Secret (still an error when missing — a typo should not be guessed past) from the unset default, where it warns and continues with backup.endpointCA unset. Verified all four paths against a fake kubectl: Forbidden, absent+default, absent+explicit, and present. 4. Caveat: endpointCA reaches the clickhouse-backup sidecar only. The legacy `schedule` CronJob writes to S3 through restic and does not consume it, so a private-CA endpoint combined with `schedule` still fails TLS. Documented on the field itself, so it lands in values.schema.json, the README and the cozyrds openAPISchema. The PR body's claim about select-e2e.sh/select-install.sh mappings was stale after the restructure that removed them; corrected there rather than in code. Assisted-By: Claude <[email protected]> Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/backups/clickhouse/README.md`:
- Line 72: Update the admin CA override instructions in the backup demo README
to specify copying the CA into <NAMESPACE>/<CH_CA_SECRET_NAME>, running with
S3_CA_SECRET="", and exporting CH_BACKUP_CA_SECRET="<application-secrets-name>"
before Step 04 and Step 07.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c26d4e99-e134-41b4-a279-482705d667a8
📒 Files selected for processing (9)
api/apps/v1alpha1/clickhouse/types.goexamples/backups/clickhouse/00-helpers.shexamples/backups/clickhouse/03-create-bucket.shexamples/backups/clickhouse/README.mdpackages/apps/clickhouse/README.mdpackages/apps/clickhouse/templates/clickhouse.yamlpackages/apps/clickhouse/values.schema.jsonpackages/apps/clickhouse/values.yamlpackages/system/clickhouse-rd/cozyrds/clickhouse.yaml
🚧 Files skipped from review as they are similar to previous changes (8)
- packages/apps/clickhouse/templates/clickhouse.yaml
- packages/apps/clickhouse/values.schema.json
- packages/apps/clickhouse/values.yaml
- packages/system/clickhouse-rd/cozyrds/clickhouse.yaml
- packages/apps/clickhouse/README.md
- api/apps/v1alpha1/clickhouse/types.go
- examples/backups/clickhouse/00-helpers.sh
- examples/backups/clickhouse/03-create-bucket.sh
E2E: all ClickHouse tests pass ✅The ClickHouse backup tests this PR adds/enables all passed in E2E — the backup/restore roundtrip that
The only failures in that run were E2E run (ClickHouse tests green): https://github.com/cozystack/cozystack/actions/runs/30810480620/job/92274960158 |
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM with non-blocking notes.
Reviewed at 9379729 against merge-base 10554f9. Static review found nothing blocking: backup.endpointCA is opt-in and empty by default (existing releases render byte-identical), all five config corners render to admittable objects, the unit tests are non-vacuous, generated artifacts are in sync, and the shell harness is fail-closed. The remaining items are runtime facts a static review cannot settle.
Claim mismatches
[PARTIAL] PR body §2 describes a suite hack/e2e-chainsaw/clickhouse-backup/ with Tests clickhouse-backup-1-contracts / clickhouse-backup-2-roundtrip. The code folds both Tests into the existing hack/e2e-chainsaw/clickhouse/chainsaw-test.yaml with names clickhouse-1-backup-contracts (:119) / clickhouse-2-backup-roundtrip (:255). A later paragraph of the body corrects the directory to clickhouse/, so this is an internal wording inconsistency, not a shipped-artifact defect. Worth tidying the description before squash-merge.
[UNVERIFIABLE] The SSL_CERT_DIR-vs-AWS_CA_BUNDLE behaviour is documented as "measured against altinity/clickhouse-backup:2.7.4" (packages/apps/clickhouse/templates/clickhouse.yaml:319), but the chart ships ghcr.io/cozystack/cozystack/altinity-clickhouse-backup:v1.6.0 (templates/clickhouse.yaml:205). Whether that image honours SSL_CERT_DIR the same way at runtime cannot be checked statically. Risk is low (SSL_CERT_DIR-in-addition-to-the-system-store is a Go crypto/x509 property and clickhouse-backup is Go), but the load-bearing proof is the CI round-trip, not the code.
Caveats
- Runtime CA trust (does the shipped v1.6.0 sidecar actually verify the SeaweedFS endpoint via the mounted bundle) is out of scope for a static review. Per the PR body the
clickhouse-2-backup-roundtripTest only became CI-live with this branch and still needs a green E2E run — that green run is the gating evidence. - Non-
optionalCA volume, blast radius (templates/clickhouse.yaml:411-417). The stated failure mode is accurate: a named-but-absent Secret holds the Pod inContainerCreatingwithMountVolume.SetUp failed ... secret not found, and because the CA lives on the single Altinity CHI Pod the blast radius is the whole ClickHouse Pod, not just the sidecar. This is inherent to the single-Pod-with-sidecar CHI shape, it is opt-in and empty by default, and the fail-closed choice overoptional: trueis explicitly reasoned. Acceptable as designed; noted so operators know that deleting/renaming a referenced CA Secret will wedge the serving Pod on next (re)start. - Applying
endpointCAto a running release is an additivevolume+volumeMount+SSL_CERT_DIRchange reconciled into the StatefulSet by the Altinity clickhouse-operator; the rolling recreation was not exercised here (additive on an opt-in toggle, low risk).
Verified negatives (so they are not re-litigated): corner endpointCA unset renders zero CA env/mount/volume; backup.enabled=false with endpointCA.name set renders no sidecar and no volume; cozyrds secrets/services.include and templates/dashboard-resourcemap.yaml untouched (no dashboard-RBAC drift); values.schema.json, keysOrder, types.go and zz_generated.deepcopy.go all consistent (make generate idempotent, go build/go vet clean); helm unittest 17/17 and mutation of the SSL_CERT_DIR gate reddens exactly the 3 endpointCA assertions (non-vacuous); hack/select-e2e.sh diff is comment-only; the CA-copy gate in examples/backups/clickhouse/03-create-bucket.sh is fail-closed (separates Forbidden from NotFound).
Recommended follow-ups
- Land the green
clickhouse-2-backup-roundtripCI run (or a dev-cluster round-trip) before merge; it is the only thing that proves the shipped v1.6.0 sidecar honoursSSL_CERT_DIRagainst SeaweedFS. - Track the already-opened
terraform-provider-cozystackschema follow-up for the new nestedendpointCAobject. - Reconcile the PR description's suite-directory / Test-name wording with the code.
…SSL_CERT_DIR note The SSL_CERT_DIR rationale said the behaviour was "verified against altinity/clickhouse-backup:2.7.4" while the chart ships altinity-clickhouse-backup:v1.6.0, which reads as a version mismatch. They are the same binary: the cozystack image is built FROM upstream altinity/clickhouse-backup:2.7.4. Say so, and point at the Dockerfile, so the measurement provably applies to the shipped image. Comment-only change inside a Go-template comment; render is unchanged and helm unittest stays 17/17. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
| {{- /* | ||
| SSL_CERT_DIR points clickhouse-backup at the mounted CA | ||
| bundle so it can verify an S3 endpoint signed by a private | ||
| CA — Cozystack's in-cluster SeaweedFS | ||
| (seaweedfs-s3.<tenant>:8333) serves the self-signed | ||
| "SeaweedFS CA", which no system trust store carries. | ||
|
|
||
| SSL_CERT_DIR and NOT AWS_CA_BUNDLE, deliberately. Both are | ||
| honoured by this image — the shipped | ||
| altinity-clickhouse-backup:v1.6.0 is built FROM upstream | ||
| altinity/clickhouse-backup:2.7.4 (see | ||
| images/altinity-clickhouse-backup/Dockerfile), which is the | ||
| binary the behaviour below was measured against — but they | ||
| differ in one | ||
| way that matters: Go reads SSL_CERT_DIR *in addition to* | ||
| the default system bundle file, whereas AWS_CA_BUNDLE | ||
| REPLACES the SDK's trust pool. Measured against a private | ||
| CA and public AWS S3 in turn: with SSL_CERT_DIR both | ||
| verify; with AWS_CA_BUNDLE the private endpoint verifies | ||
| and public S3 fails `x509: certificate signed by unknown | ||
| authority`. So AWS_CA_BUNDLE would silently break a | ||
| release that also talks to a publicly-trusted endpoint. | ||
| S3_DISABLE_CERT_VERIFICATION is the third option and is | ||
| rejected on purpose: it drops verification entirely rather | ||
| than trusting one more CA. | ||
|
|
||
| /etc/ssl/certs is listed explicitly alongside our directory. | ||
| Go would still read the default bundle FILE | ||
| (/etc/ssl/certs/ca-certificates.crt) even if SSL_CERT_DIR | ||
| named only our dir — that is what the measurement above | ||
| showed — but that leaves the public-CA half of the contract | ||
| resting on a file this image happens to ship. Naming the | ||
| system directory costs nothing and survives a base-image | ||
| bump that moves the bundle. | ||
| */}} |
There was a problem hiding this comment.
Guys, a Helm template is not the right place for documentation.
| {{- /* | ||
| The CA bundle the sidecar's SSL_CERT_DIR points at. Projected | ||
| under the fixed filename ca.crt regardless of the key it occupies | ||
| in the Secret, because SSL_CERT_DIR reads whole directories and | ||
| the filename is what lands in the trust set. | ||
|
|
||
| Deliberately NOT `optional: true`, unlike the credential | ||
| secretKeyRefs above on the useSystemBucket path. The asymmetry is | ||
| intended and the two cases are not alike: | ||
|
|
||
| - credentials are projected by the platform AFTER the release | ||
| installs, so a strict ref would wedge every fresh | ||
| useSystemBucket release in CreateContainerConfigError until | ||
| the first BackupJob fired. Optional lets the Pod start and | ||
| surfaces the problem later, when a backup is actually run. | ||
| - this CA is named by the tenant, must already exist when they | ||
| name it, and is trust material. Optional would mount an empty | ||
| directory, the Pod would come up looking healthy, and the | ||
| first backup would fail deep inside clickhouse-backup with an | ||
| x509 error naming neither the Secret nor this chart. | ||
|
|
||
| So it fails closed: a missing Secret holds the Pod in | ||
| ContainerCreating with `MountVolume.SetUp failed ... secret not | ||
| found` on it, which names the exact missing object in | ||
| `kubectl describe pod`. Note the blast radius is the whole | ||
| ClickHouse Pod, not just the sidecar — that is the cost of the | ||
| legible failure, and the reason the field is opt-in and empty by | ||
| default. | ||
| */}} |
There was a problem hiding this comment.
Same comments-as-docs issue here.
…emo CA secret Review feedback on #3385: - Move the endpointCA rationale out of the Helm template (lllamnyp): the SSL_CERT_DIR and CA-volume blocks were multi-paragraph essays. Trim each to a few lines stating only the non-obvious decision (SSL_CERT_DIR vs AWS_CA_BUNDLE; the deliberate non-optional mount) and point at the values.yaml endpointCA field for the full description. - wait_deleted (00-helpers.sh): poll with 'get --ignore-not-found' and treat only an empty result as deletion, so an RBAC/API-server/transport failure is no longer read as success and cleanup can't settle on a false positive. - Backup demo CA Secret: scope the default name to CLICKHOUSE_NAME, stamp an ownership label on the copy, refuse to overwrite a Secret the demo doesn't own, and delete on cleanup only when that label is present — so a foreign Secret sharing the name is never clobbered or removed. - README: document CH_BACKUP_CA_SECRET and the release-scoped, demo-owned CA Secret; spell out the admin-copy path (export CH_BACKUP_CA_SECRET before steps 04 and 07). Assisted-By: Claude <[email protected]> Signed-off-by: Andrey Kolkov <[email protected]>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
examples/backups/clickhouse/00-helpers.sh (1)
219-225: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winBound every
kubectl getin the deletion waiter.
kubectl --request-timeoutdefaults to0, so requests do not time out.wait_deletedalso updateselapsedonly afterkubectlreturns, so a stalled API call can leavewait_deletedpast its loop timeout. Add a bounded request timeout to bothkubectl getinvocations in this function, or wrap each call with an explicit process timeout.Proposed fix
- if got=$(kubectl -n "$NAMESPACE" get "$resource_type" "$resource_name" --ignore-not-found 2>/dev/null) && [[ -z "$got" ]]; then + if got=$(kubectl --request-timeout=30s -n "$NAMESPACE" get "$resource_type" "$resource_name" --ignore-not-found 2>/dev/null) && [[ -z "$got" ]]; then ... - kubectl -n "$NAMESPACE" get "$resource_type" "$resource_name" -o wide >&粗 or true + kubectl --request-timeout=30s -n "$NAMESPACE" get "$resource_type" "$resource_name" -o wide >&2 || true🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/backups/clickhouse/00-helpers.sh` around lines 219 - 225, Update the kubectl invocations in wait_deleted, including the initial get used to assign got and the timeout-diagnostic get, to enforce a bounded request duration via --request-timeout or an equivalent process timeout. Keep the existing deletion detection and timeout logging behavior unchanged.Source: MCP tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@examples/backups/clickhouse/00-helpers.sh`:
- Around line 219-225: Update the kubectl invocations in wait_deleted, including
the initial get used to assign got and the timeout-diagnostic get, to enforce a
bounded request duration via --request-timeout or an equivalent process timeout.
Keep the existing deletion detection and timeout logging behavior unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: aff84ad3-400d-431a-9037-ef3ba26d41df
📒 Files selected for processing (5)
examples/backups/clickhouse/00-helpers.shexamples/backups/clickhouse/03-create-bucket.shexamples/backups/clickhouse/README.mdexamples/backups/clickhouse/cleanup.shpackages/apps/clickhouse/templates/clickhouse.yaml
🚧 Files skipped from review as they are similar to previous changes (4)
- examples/backups/clickhouse/cleanup.sh
- packages/apps/clickhouse/templates/clickhouse.yaml
- examples/backups/clickhouse/03-create-bucket.sh
- examples/backups/clickhouse/README.md
What this PR does
Gives the ClickHouse
clickhouse-backupsidecar a way to trust an S3 endpoint whose certificate is signed by a private CA, and uses that to make the ClickHouse backup/restore e2e round-trip actually run in CI. This actualizes #2600, whose feature code and example scripts have since landed onmainindependently, and whose remaining contribution — the e2e test — was still in the retired BATS format. #2600 can be closed once this merges.1.
backup.endpointCAon the ClickHouse chartThe sidecar had no TLS/CA surface at all: bucket coordinates and credentials arrive as
S3_*env, and nothing let it verify an endpoint signed by a private CA. Cozystack's own in-cluster SeaweedFS is exactly that case — it serves:8333behind the self-signed "SeaweedFS CA" — so a ClickHouse release could only ever back up to a publicly-trusted endpoint. MariaDB (storage.s3.tls.caSecretKeyRef) and Postgres (barmanendpointCA) have had a CA field all along; this closes the gap for ClickHouse.backup.endpointCAtakes{name, key}. When set, the chart mounts that Secret into the sidecar and pointsSSL_CERT_DIRat it. Empty — the default — renders no env, no mount and no volume, so existing releases are byte-for-byte unchanged.Why
SSL_CERT_DIRand notAWS_CA_BUNDLE. Both are honoured byaltinity/clickhouse-backup:2.7.4; I measured them against a self-signed endpoint and public AWS S3 in turn. Go readsSSL_CERT_DIRin addition to the system bundle, so both endpoints verify.AWS_CA_BUNDLEreplaces the SDK's trust pool: the private endpoint verifies, and public S3 then failsx509: certificate signed by unknown authority— it would silently break a release that also talks to a public endpoint.S3_DISABLE_CERT_VERIFICATIONwas rejected outright, since it drops verification rather than trusting one more CA.2. The e2e suite, and what it was really testing
Two Tests added to the existing
hack/e2e-chainsaw/clickhouse/suite (in name order, mirroring theetcd/mariadb/postgresshape — see the layout rationale below for why they live here rather than in a new directory):clickhouse-1-backup-contracts(CI,tenant-test, ~98s): a backup-enabled ClickHouse asserted against the running cluster — theclickhouse-backupsidecar on thech-backup-api:7171port, and the<release>-backup-api-authSecret whose keys the sidecar'sAPI_USERNAME/API_PASSWORDactually resolvevalueFrom. Each is a silent break if a chart bump renames it. Needs no object store.clickhouse-2-backup-roundtrip(CI,tenant-root):Bucket→Altinitystrategy +BackupClass→ source ClickHouse with a sentinel row →BackupJob→ in-placeRestoreJob→ to-copyRestoreJobinto a second instance, drivingexamples/backups/clickhouse/run-all.shso test and docs cannot drift.The round-trip was originally gated behind
CLICKHOUSE_E2E_S3_ROUNDTRIPand skipped in CI — itsPASS (1.01s)meant the gate fired, not that a backup happened.backup.endpointCAremoves the reason for the gate, so the suite now adopts the same three preconditions mariadb/postgres rely on: run intenant-root(an isolated tenant's Cilium egress allowlist blocks its Pods from reachingtenant-root's SeaweedFS), target the in-cluster endpoint instead of theBucket's external ingress URL (an unroutable placeholder in CI), and trust its certificate via the copied CA. It also pre-cleans withcleanup.sh, becausetenant-rootis shared and a leftoverSucceededBackupJobwould satisfy the harness's wait without a backup ever running.Both backup Tests live in the existing
hack/e2e-chainsaw/clickhouse/suite rather than a new suite directory, so no selector mapping is added:hack/select-e2e.shalready derivesclickhouseboth frompackages/apps/clickhouse/**(the*-applicationrule) and fromexamples/backups/clickhouse/**(the harness path), and both paths therefore select the suite that actually runs the harness. That alignment is the reason for the layout — a separateclickhouse-backup/directory would have made an edit to the harness select a suite that cannot exercise it.hack/select-e2e.shandhack/select-install.share otherwise untouched (the only change is a comment recording that invariant), and the existingbackup example harness edit selects its app suitecase inhack/select-e2e_test.batscovers the mapping.Verification
helm unittest— 17 pass, including 5 newendpointCAcases: the unset default renders nothing, the env/mount/volume wiring, a custom key still projected asca.crt, composition withuseSystemBucket, andbackup.enabled=falserenders nothing.x509: certificate signed by unknown authority; withSSL_CERT_DIRthe handshake succeeds.hack/select-e2e_test.bats(16) andhack/select-install_test.bats(17) pass;select-install.sh --validatereports graph OK.make generateis idempotent, so the pre-commit job'sgit diff --exit-codestays clean.mainto get one.Screenshots
Not applicable — no UI changes.
Downstream repositories
Walked the trigger map against the diff, file by file. The load-bearing change is the new
backup.endpointCAfield inpackages/apps/clickhouse/values.yaml/values.schema.json; everything else ishack/e2e-chainsaw/,examples/backups/, one docs paragraph, and regenerated artifacts, none of which appear in the map.values.schema.json" reaches the provider's hand-maintained schema, model and expand/flatten pair, so the new nested object is not surfaced until it is added there. Opened as an issue rather than a PR because the natural moment is alongside that repo's nextapi/apps/v1alpha1pin bump, which is a maintainer call.Not ticked, with reasons rather than silence: cozystack/website — the trigger is adding, renaming or removing a package, and this adds a field to an existing one.
clickhouseis already in the websiteMakefile's app list and its reference page is generated from this package'sREADME.md, so the regeneratedbackup.endpointCArows are carried by the existing release-tag automation with no manual PR. cozystack/external-apps-example — vendorshack/package.mkandupdate-crd.sh, neither of which this PR touches. cozystack/ansible-cozystack, ccp, talm, cozyhr, cozy-proxy, cozystack-telemetry-server, examples — no installer values,hack/layout, namespace, variant, node-prerequisite, annotation, label or metric in their trigger rows is touched here.Release note
Summary by CodeRabbit
New Features
backup.endpointCA.Documentation
Tests