helm: verify the chart actually deploys, in CI and on a real cluster - #279
Conversation
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
1 issue found across 16 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="helm/vtop/templates/statefulset.yaml">
<violation number="1" location="helm/vtop/templates/statefulset.yaml:44">
P2: The 60-second termination grace period does not provide a clean vtop-node shutdown because the binary has no SIGTERM handling, so StatefulSet updates and pod deletions take the crash-recovery path immediately; signal-aware shutdown or an equivalent pre-stop mechanism would make this grace period effective.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| # vtop-node never calls the Kubernetes API; a mounted token would be | ||
| # pure attack surface on a pod that also serves unauthenticated HTTP. | ||
| automountServiceAccountToken: false | ||
| terminationGracePeriodSeconds: {{ int .Values.terminationGracePeriodSeconds }} |
There was a problem hiding this comment.
P2: The 60-second termination grace period does not provide a clean vtop-node shutdown because the binary has no SIGTERM handling, so StatefulSet updates and pod deletions take the crash-recovery path immediately; signal-aware shutdown or an equivalent pre-stop mechanism would make this grace period effective.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helm/vtop/templates/statefulset.yaml, line 44:
<comment>The 60-second termination grace period does not provide a clean vtop-node shutdown because the binary has no SIGTERM handling, so StatefulSet updates and pod deletions take the crash-recovery path immediately; signal-aware shutdown or an equivalent pre-stop mechanism would make this grace period effective.</comment>
<file context>
@@ -0,0 +1,220 @@
+ # vtop-node never calls the Kubernetes API; a mounted token would be
+ # pure attack surface on a pod that also serves unauthenticated HTTP.
+ automountServiceAccountToken: false
+ terminationGracePeriodSeconds: {{ int .Values.terminationGracePeriodSeconds }}
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
</file context>
|
All seven fixed across P1 — the admin Service could not bootstrap the clusterYou are right, and this was the one that would have wasted somebody's afternoon. A load-balanced endpoint and per-pod certificate identity are simply incompatible: every pod presents a certificate whose SAN is that pod's own headless FQDN, so a client routed to an arbitrary pod has no name it can verify. I did not add a shared SAN contract, because the chart cannot inspect the certificates an operator minted and must not assume what is in them. Instead:
P1 — names collided at length
Verified rather than reasoned about — an 80-character
P2 — DNS egress was unrestrictedCorrect. No P2 —
|
There was a problem hiding this comment.
All reported issues were addressed across 18 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
All four fixed in The CI step I wrote did not test what it claimedYou are exactly right, and the reasoning is precise: I added that check one commit earlier, in a message arguing that a refusal which quietly stopped refusing "would look exactly like success". Then I wrote one that looks exactly like success. A regression removing the TLS requirement would have sailed straight through the check written to catch it. Fixed as suggested — every other identity supplied so rendering reaches the TLS path — and verified by confirming the refusal message now actually cites TLS rather than trusting the exit code: I also added a fourth refusal for the duplicate-UUID case below, and left a comment on the step explaining why the extra Duplicate
|
There was a problem hiding this comment.
6 issues found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="scripts/k8s-smoke.sh">
<violation number="1" location="scripts/k8s-smoke.sh:17">
P2: The smoke test instructs building the image with `docker build -f docker/Dockerfile`, but that Dockerfile only builds and copies the `vtopctl` binary — it never builds `vtop-node` (the chart's own README 'Image caveat' states exactly this). The script's first check waits for the StatefulSet pods to become Ready, and those pods exec `vtop-node node`; in an image lacking `vtop-node` every pod would CrashLoopBackOff and the test would fail at its very first assertion — the same #281 bug the test exists to detect. Building the image as documented therefore cannot produce a working run. Worth aligning the recipe with reality: either extend `docker/Dockerfile` to also build and copy `vtop-node`, or make the smoke test build/push an image that includes both binaries (e.g. `cargo build --release --bin vtopctl --bin vtop-node` and copy both, or a dedicated Dockerfile target) before the script runs.</violation>
<violation number="2" location="scripts/k8s-smoke.sh:25">
P1: Setting `CLUSTER_DOMAIN` to a non-default domain makes the certificates use one FQDN while the rendered peer configs still dial `cluster.local`, causing peer TLS/name validation to fail and the pods never to become Ready. The selected domain should also be passed to Helm as `clusterDomain` (or the script should reject the override).</violation>
<violation number="3" location="scripts/k8s-smoke.sh:28">
P1: Using a release name that does not contain `vtop` makes the smoke test target a non-existent pod and mint certificates for the wrong DNS names, so it cannot bootstrap. The script should resolve the chart’s actual fullname (including overrides/truncation) and use that for pod names, Service names, and SANs.</violation>
<violation number="4" location="scripts/k8s-smoke.sh:45">
P0: A normal completion or any failure can delete unrelated workloads in the caller-supplied namespace and uninstall an existing release with the same name. The smoke test should use an isolated, uniquely owned namespace/release or track ownership and clean up only resources it created.</violation>
<violation number="5" location="scripts/k8s-smoke.sh:187">
P2: The documented invocation fails before exercising the cluster unless `vtopctl`, `vtop-node`, and `curl` happen to be installed separately on the host; building the documented image does not provide those commands, especially `vtop-node`. The smoke test should preflight or build the host binaries and require `curl`, or invoke known binary paths and document that setup.</violation>
<violation number="6" location="scripts/k8s-smoke.sh:211">
P2: The independent-range check can pass without observing pod 1's offset metric, leaving a missing or renamed metric indistinguishable from zero records. Require a non-empty metric value before asserting it equals zero.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| for pid in ${FORWARDS:-}; do kill "$pid" 2>/dev/null || true; done | ||
| if [ "${KEEP:-0}" != "1" ]; then | ||
| helm uninstall "$REL" -n "$NS" >/dev/null 2>&1 || true | ||
| kubectl delete namespace "$NS" --wait=false >/dev/null 2>&1 || true |
There was a problem hiding this comment.
P0: A normal completion or any failure can delete unrelated workloads in the caller-supplied namespace and uninstall an existing release with the same name. The smoke test should use an isolated, uniquely owned namespace/release or track ownership and clean up only resources it created.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/k8s-smoke.sh, line 45:
<comment>A normal completion or any failure can delete unrelated workloads in the caller-supplied namespace and uninstall an existing release with the same name. The smoke test should use an isolated, uniquely owned namespace/release or track ownership and clean up only resources it created.</comment>
<file context>
@@ -0,0 +1,229 @@
+ for pid in ${FORWARDS:-}; do kill "$pid" 2>/dev/null || true; done
+ if [ "${KEEP:-0}" != "1" ]; then
+ helm uninstall "$REL" -n "$NS" >/dev/null 2>&1 || true
+ kubectl delete namespace "$NS" --wait=false >/dev/null 2>&1 || true
+ fi
+ rm -rf "$WORK"
</file context>
| REL="${2:-vtop}" | ||
| IMAGE_REPO="${IMAGE_REPO:-vtop-engine}" | ||
| IMAGE_TAG="${IMAGE_TAG:-local}" | ||
| DOMAIN="${CLUSTER_DOMAIN:-cluster.local}" |
There was a problem hiding this comment.
P1: Setting CLUSTER_DOMAIN to a non-default domain makes the certificates use one FQDN while the rendered peer configs still dial cluster.local, causing peer TLS/name validation to fail and the pods never to become Ready. The selected domain should also be passed to Helm as clusterDomain (or the script should reject the override).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/k8s-smoke.sh, line 25:
<comment>Setting `CLUSTER_DOMAIN` to a non-default domain makes the certificates use one FQDN while the rendered peer configs still dial `cluster.local`, causing peer TLS/name validation to fail and the pods never to become Ready. The selected domain should also be passed to Helm as `clusterDomain` (or the script should reject the override).</comment>
<file context>
@@ -0,0 +1,229 @@
+REL="${2:-vtop}"
+IMAGE_REPO="${IMAGE_REPO:-vtop-engine}"
+IMAGE_TAG="${IMAGE_TAG:-local}"
+DOMAIN="${CLUSTER_DOMAIN:-cluster.local}"
+WORK="$(mktemp -d)"
+CERTS="$WORK/certs"
</file context>
| DOMAIN="${CLUSTER_DOMAIN:-cluster.local}" | ||
| WORK="$(mktemp -d)" | ||
| CERTS="$WORK/certs" | ||
| HEADLESS="${REL}-headless" |
There was a problem hiding this comment.
P1: Using a release name that does not contain vtop makes the smoke test target a non-existent pod and mint certificates for the wrong DNS names, so it cannot bootstrap. The script should resolve the chart’s actual fullname (including overrides/truncation) and use that for pod names, Service names, and SANs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/k8s-smoke.sh, line 28:
<comment>Using a release name that does not contain `vtop` makes the smoke test target a non-existent pod and mint certificates for the wrong DNS names, so it cannot bootstrap. The script should resolve the chart’s actual fullname (including overrides/truncation) and use that for pod names, Service names, and SANs.</comment>
<file context>
@@ -0,0 +1,229 @@
+DOMAIN="${CLUSTER_DOMAIN:-cluster.local}"
+WORK="$(mktemp -d)"
+CERTS="$WORK/certs"
+HEADLESS="${REL}-headless"
+
+CLUSTER_ID=11111111-2222-3333-4444-555555555555
</file context>
| # deployment is not the shape the chart documents. | ||
| forward "${REL}-1" 19501 9500 | ||
| other="$(curl -sf localhost:19501/metrics | awk '/^vtop_broker_next_offset\{/ { print $NF }' | head -1)" | ||
| [ "${other:-0}" = "0" ] || fail "pod 1 holds $other records; ranges are not independent as documented" |
There was a problem hiding this comment.
P2: The independent-range check can pass without observing pod 1's offset metric, leaving a missing or renamed metric indistinguishable from zero records. Require a non-empty metric value before asserting it equals zero.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/k8s-smoke.sh, line 211:
<comment>The independent-range check can pass without observing pod 1's offset metric, leaving a missing or renamed metric indistinguishable from zero records. Require a non-empty metric value before asserting it equals zero.</comment>
<file context>
@@ -0,0 +1,229 @@
+# deployment is not the shape the chart documents.
+forward "${REL}-1" 19501 9500
+other="$(curl -sf localhost:19501/metrics | awk '/^vtop_broker_next_offset\{/ { print $NF }' | head -1)"
+[ "${other:-0}" = "0" ] || fail "pod 1 holds $other records; ranges are not independent as documented"
+log "pod 1 is empty, confirming per-pod independent ranges"
+
</file context>
| PER_ROUND=50 | ||
| for epoch in $(seq 1 "$ROUNDS"); do | ||
| sed -i.bak "s/^producer_epoch: .*/producer_epoch: $epoch/" "$WORK/client.yaml" | ||
| vtop-node produce --client-config "$WORK/client.yaml" \ |
There was a problem hiding this comment.
P2: The documented invocation fails before exercising the cluster unless vtopctl, vtop-node, and curl happen to be installed separately on the host; building the documented image does not provide those commands, especially vtop-node. The smoke test should preflight or build the host binaries and require curl, or invoke known binary paths and document that setup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/k8s-smoke.sh, line 187:
<comment>The documented invocation fails before exercising the cluster unless `vtopctl`, `vtop-node`, and `curl` happen to be installed separately on the host; building the documented image does not provide those commands, especially `vtop-node`. The smoke test should preflight or build the host binaries and require `curl`, or invoke known binary paths and document that setup.</comment>
<file context>
@@ -0,0 +1,229 @@
+PER_ROUND=50
+for epoch in $(seq 1 "$ROUNDS"); do
+ sed -i.bak "s/^producer_epoch: .*/producer_epoch: $epoch/" "$WORK/client.yaml"
+ vtop-node produce --client-config "$WORK/client.yaml" \
+ --addr "127.0.0.1:19400" --records "$PER_ROUND" --batch 10 \
+ --durability local-fsync >/dev/null || fail "produce round $epoch failed"
</file context>
| # Usage: scripts/k8s-smoke.sh [namespace] [release] | ||
| # | ||
| # Requires: a reachable cluster, kubectl, helm, openssl, and a locally built | ||
| # image tagged vtop-engine:local (docker build -f docker/Dockerfile). |
There was a problem hiding this comment.
P2: The smoke test instructs building the image with docker build -f docker/Dockerfile, but that Dockerfile only builds and copies the vtopctl binary — it never builds vtop-node (the chart's own README 'Image caveat' states exactly this). The script's first check waits for the StatefulSet pods to become Ready, and those pods exec vtop-node node; in an image lacking vtop-node every pod would CrashLoopBackOff and the test would fail at its very first assertion — the same #281 bug the test exists to detect. Building the image as documented therefore cannot produce a working run. Worth aligning the recipe with reality: either extend docker/Dockerfile to also build and copy vtop-node, or make the smoke test build/push an image that includes both binaries (e.g. cargo build --release --bin vtopctl --bin vtop-node and copy both, or a dedicated Dockerfile target) before the script runs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/k8s-smoke.sh, line 17:
<comment>The smoke test instructs building the image with `docker build -f docker/Dockerfile`, but that Dockerfile only builds and copies the `vtopctl` binary — it never builds `vtop-node` (the chart's own README 'Image caveat' states exactly this). The script's first check waits for the StatefulSet pods to become Ready, and those pods exec `vtop-node node`; in an image lacking `vtop-node` every pod would CrashLoopBackOff and the test would fail at its very first assertion — the same #281 bug the test exists to detect. Building the image as documented therefore cannot produce a working run. Worth aligning the recipe with reality: either extend `docker/Dockerfile` to also build and copy `vtop-node`, or make the smoke test build/push an image that includes both binaries (e.g. `cargo build --release --bin vtopctl --bin vtop-node` and copy both, or a dedicated Dockerfile target) before the script runs.</comment>
<file context>
@@ -0,0 +1,229 @@
+# Usage: scripts/k8s-smoke.sh [namespace] [release]
+#
+# Requires: a reachable cluster, kubectl, helm, openssl, and a locally built
+# image tagged vtop-engine:local (docker build -f docker/Dockerfile).
+
+set -euo pipefail
</file context>
There was a problem hiding this comment.
3 issues found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".github/workflows/ci.yml">
<violation number="1" location=".github/workflows/ci.yml:301">
P2: Changes to `scripts/k8s-smoke.sh` do not rerun this deployment gate: paths-filter reports them as `scripts`, while this condition ignores that output. Gating on the smoke-script path as part of the Helm filter, or including the scripts output here, would prevent a broken harness from bypassing live validation.</violation>
<violation number="2" location=".github/workflows/ci.yml:328">
P1: The new deploy gate fails before the smoke test because the image under test does not contain `/usr/local/bin/vtop-node`. The Dockerfile needs to build and copy `vtop-node` (or this extraction must use an image that contains it) before this job can pass.</violation>
<violation number="3" location=".github/workflows/ci.yml:340">
P1: The kind smoke deployment cannot reach Ready with the chart defaults because its two PVCs require a default StorageClass, while this job creates no storage provisioner or test storage class. Installing a kind-compatible provisioner or configuring the smoke deployment with bound test volumes would make the live gate runnable.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| # The half the static checks cannot reach: pods Ready, Raft bootstrapped, | ||
| # records streamed and counted, and every record surviving a killed pod. | ||
| - name: Deploy the chart and verify it works | ||
| run: scripts/k8s-smoke.sh vtop-ci vtop |
There was a problem hiding this comment.
P1: The kind smoke deployment cannot reach Ready with the chart defaults because its two PVCs require a default StorageClass, while this job creates no storage provisioner or test storage class. Installing a kind-compatible provisioner or configuring the smoke deployment with bound test volumes would make the live gate runnable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 340:
<comment>The kind smoke deployment cannot reach Ready with the chart defaults because its two PVCs require a default StorageClass, while this job creates no storage provisioner or test storage class. Installing a kind-compatible provisioner or configuring the smoke deployment with bound test volumes would make the live gate runnable.</comment>
<file context>
@@ -295,6 +295,57 @@ jobs:
+ # The half the static checks cannot reach: pods Ready, Raft bootstrapped,
+ # records streamed and counted, and every record surviving a killed pod.
+ - name: Deploy the chart and verify it works
+ run: scripts/k8s-smoke.sh vtop-ci vtop
+
+ - name: Cluster state on failure
</file context>
| mkdir -p "$RUNNER_TEMP/bin" | ||
| container="$(docker create vtop-engine:local)" | ||
| for binary in vtopctl vtop-node; do | ||
| docker cp "$container:/usr/local/bin/$binary" "$RUNNER_TEMP/bin/$binary" |
There was a problem hiding this comment.
P1: The new deploy gate fails before the smoke test because the image under test does not contain /usr/local/bin/vtop-node. The Dockerfile needs to build and copy vtop-node (or this extraction must use an image that contains it) before this job can pass.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 328:
<comment>The new deploy gate fails before the smoke test because the image under test does not contain `/usr/local/bin/vtop-node`. The Dockerfile needs to build and copy `vtop-node` (or this extraction must use an image that contains it) before this job can pass.</comment>
<file context>
@@ -295,6 +295,57 @@ jobs:
+ mkdir -p "$RUNNER_TEMP/bin"
+ container="$(docker create vtop-engine:local)"
+ for binary in vtopctl vtop-node; do
+ docker cp "$container:/usr/local/bin/$binary" "$RUNNER_TEMP/bin/$binary"
+ done
+ docker rm "$container" >/dev/null
</file context>
| helm-deploy: | ||
| name: helm (deploy to kind) | ||
| needs: changes | ||
| if: needs.changes.outputs.helm == 'true' || needs.changes.outputs.rust == 'true' |
There was a problem hiding this comment.
P2: Changes to scripts/k8s-smoke.sh do not rerun this deployment gate: paths-filter reports them as scripts, while this condition ignores that output. Gating on the smoke-script path as part of the Helm filter, or including the scripts output here, would prevent a broken harness from bypassing live validation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 301:
<comment>Changes to `scripts/k8s-smoke.sh` do not rerun this deployment gate: paths-filter reports them as `scripts`, while this condition ignores that output. Gating on the smoke-script path as part of the Helm filter, or including the scripts output here, would prevent a broken harness from bypassing live validation.</comment>
<file context>
@@ -295,6 +295,57 @@ jobs:
+ helm-deploy:
+ name: helm (deploy to kind)
+ needs: changes
+ if: needs.changes.outputs.helm == 'true' || needs.changes.outputs.rust == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
</file context>
A StatefulSet of vtop-node node processes — the co-located shape #237 built — with a headless Service for stable peer DNS, per-plane volume claims, meta node ids derived from the pod ordinal, and a config-checksum annotation so config changes roll the set. Readiness is /readyz and the chart documents that a fenced node going unready is correct behaviour to route around, not a probe to loosen; liveness is /healthz; a startup probe covers cold recovery. Nothing identity-shaped has a default: TLS secret names, the cluster id, per-replica broker UUIDs, range identities, and the client principal are all required, refuse rendering with the exact contract in the message, and are shape-checked by values.schema.json — a chart that defaulted any of them would be shipping credentials, which is issue #81's lesson. The PodDisruptionBudget holds maxUnavailable at 1 because three voters are a quorum; pods run non-root with a read-only rootfs; the ServiceMonitor is gated behind both a value and the CRD's presence.
Two of them made the chart unusable in the shapes it claimed to support. THE ADMIN SERVICE COULD NOT BOOTSTRAP THE CLUSTER. The client Service published the metadata admin port and NOTES told operators to port-forward it, but every pod presents a certificate whose SAN is that pod's OWN headless FQDN. A client routed to an arbitrary pod has no name it can verify, so TLS fails before `vtopctl meta init` sends a byte. A load-balanced endpoint and per-pod certificate identity are incompatible unless the operator also mints a shared SAN, which the chart cannot inspect and must not assume. The port is now off by default behind `service.exposeMetaAdmin`, documented with the exact condition that makes it safe, and NOTES forwards a SPECIFIC POD and prints the `server_name` to set — it defaults to "localhost", which never matches. NAMES COLLIDED AT LENGTH. `fullname` truncated to 63, then the headless Service appended "-headless" and truncated to 63 again — which for a long name yields the SAME string as the client Service, so the chart renders two Services with one name. Verified: an 80-character fullnameOverride produced a collision before this change and distinct 54/63-character names after. The reserved 9 characters also keep StatefulSet pod names and the statefulset.kubernetes.io/pod-name label inside the DNS limit. DNS EGRESS WAS UNRESTRICTED. The rule carried no `to` selector, so enabling the NetworkPolicy admitted port 53 to every destination — a standing hole in a default-deny policy, on the classic exfiltration channel. Scoped to kube-dns in kube-system, overridable via `networkPolicy.dnsTo`. `vtopctl node status` WAS BLOCKED BY THE POLICY THAT DOCUMENTS IT. That command goes replica-to-replica over the replication listener, not over the admin endpoint, and the client rule admitted only admin and native. Replica port added. podLabels and podAnnotations could override chart-managed keys. A user label redefining a selector label leaves the StatefulSet unable to match its own pods; a user annotation replacing checksum/config silently disables config-triggered rollouts, so pods keep running against a config that no longer exists. Both reserved with `omit`. THE TERMINATION GRACE PERIOD WAS DESCRIBED AS "long enough for a clean shutdown fsync". vtop-node installs no signal handler at all, so SIGTERM is ignored and every stop ends in SIGKILL. That is safe — acknowledgement follows fsync, recovery truncates a torn tail, and chaos scenario 04 verifies it — but it is the crash path, and the chart now says so instead of implying a graceful stop it never gets. Signal handling is tracked as #280, including the part that actually matters: a departing leader could release its range lease rather than making failover wait out a timer. Verified with helm v4.2.3: lint clean, full render 6 manifests, identity refusals still fire with their contracts intact, both omit guards drop hijacking keys, the opt-in restores the admin port, and the long-name cases render distinct in-limit names.
The chart had NO CI. Every check on its pull request skipped, so `helm lint` never ran, nothing rendered it, and the two real bugs review found — a Service that could not bootstrap the cluster, and names that collided at length — reached review with no automated signal at all. A branch existed that added the `helm` path filter and a comment referring to "the helm job below". There is no such job. The filter output was wired to nothing, which is the failure mode where CI looks configured and gates nothing. The job checks the two contracts the chart actually makes: RENDERING. Lint, render the documented happy path, and count the manifests — a chart that renders NOTHING also exits zero, so the exit code is not the signal. Every manifest must carry a kind and an apiVersion. REFUSING. Identities and credentials are never defaulted, which is the chart's security property: #81 exists because a lab compose shipped default credentials. A refusal that quietly stopped refusing would render a cluster whose nodes all share whatever the chart invented, and it would look exactly like success. Three refusals are asserted: no identities at all, no TLS secret names, and a nodeUuids list that does not match replicaCount. NAMES. Rendered at helm's maximum release-name length, asserting Service names are distinct and every name fits inside 63 characters — including room for the ordinal StatefulSet pods append. This is the check that would have caught the collision fixed alongside it. Stdlib only, no YAML library. Installing one adds a step that can quietly fail, and a check that stops checking while still reporting success is worse than no check. Helm's output is predictable enough to scan honestly. Every step was run locally against the chart before being written down: lint clean, 6 manifests, all three refusals refuse, and the long-name render produces distinct in-limit names.
… checked nothing MY OWN CI STEP DID NOT TEST WHAT IT CLAIMED. "refuses TLS secret names" set only cluster.id, so rendering aborted at the nodeUuids length check inside the ConfigMap — which renders before the StatefulSet whose volumes reference the Secrets. Helm exits non-zero either way, so the step PASSED while never once reaching the TLS refusal it names. That is the chart's core security property (#81 exists because a lab compose shipped default credentials), and a regression that silently stopped refusing TLS would have sailed straight through the check written to catch it. It now supplies every other identity so rendering reaches the TLS path, verified by confirming the refusal message actually cites TLS. A fourth refusal covers duplicate broker UUIDs. DUPLICATE nodeUuids WERE ACCEPTED. Two pods sharing a broker UUID share an IDENTITY: with data.lease.enabled they present the same one to the metadata plane and race each other for the same range lease, so the StatefulSet is not a set of replicas but two claimants wearing one name. The CN convention makes it worse — one certificate would be valid for both. Refused, naming the duplicates. STRING-VALUED NAMES RENDERED UNQUOTED. A Secret or storage class named `true` (or `no`, or a bare number) is emitted as a YAML boolean, and the manifest is rejected on a type error rather than anything meaningful. The schema keeps the INPUT a string; only quoting keeps the OUTPUT one. Verified: with --set-string tls.metaSecretName=true the rendered field is now "true". THE README STILL SENT PEOPLE TO THE ADMIN SERVICE. I fixed NOTES.txt in the previous commit and left the README pointing at a port that is now off by default — so the documented default workflow could not work. It now shows the per-pod port-forward, the server_name that must accompany it, and why the Service does not publish that port.
The chart's CI lints, renders, and checks its refusal contracts. All of that is STATIC — it proves the chart emits valid YAML, not that a cluster comes up. That gap is not theoretical: the published image shipped without `vtop-node` entirely (#281), so every pod would have CrashLoopBackOff'd, and nothing in the static checks could have noticed. `scripts/k8s-smoke.sh` installs the chart against a live cluster and asserts what rendering cannot: - all three pods reach Ready - the metadata Raft group bootstraps and elects a leader - records stream in, and the committed offset equals what was produced - a pod nobody produced to is still EMPTY, which is what makes the chart's "each pod is an independent standalone range" claim a checked fact rather than a sentence in a README - every record survives a force-deleted pod with no grace period — the SIGKILL path every stop takes today (#280), so this is ordinary shutdown behaviour rather than an extreme Verified by running it, not by writing it: three consecutive passes against Docker Desktop's Kubernetes, and the earlier manual run streamed 600 records and recovered all of them from a kill. TWO THINGS THE RUN TAUGHT THE SCRIPT. A fresh producer epoch per round, because sequence state is keyed on (producer_id, producer_epoch) and replaying epoch 1 is correctly DEDUPLICATED — the offset stops moving, which reads as a stall and is actually idempotency working. And status output is captured rather than piped into `grep -q`, which short-circuits, closes the pipe, and kills vtopctl on SIGPIPE — reported as a panic that hides the real status. The README now also states that a cold install typically restarts once or twice: nodes resolve peer DNS at startup and exit if it is not ready yet, so `RESTARTS 2` on a fresh deploy is startup ordering settling, not a broken deployment — and not a reason to loosen the probes.
The helm job lints, renders, and checks refusal contracts. All static. It cannot tell whether a cluster comes up, and that gap let a real bug through: the image shipped without `vtop-node` (#281), so every pod would have CrashLoopBackOff'd while every check stayed green. `helm-deploy` stands up a kind cluster, installs the chart, and runs scripts/k8s-smoke.sh: pods Ready, Raft bootstrapped with a leader, records streamed and the committed offset counted, a pod nobody produced to still empty, and every record surviving a force-deleted pod. THE BINARIES COME OUT OF THE IMAGE, not from a second cargo build. The image already contains both — that is exactly what #281 was about — so extracting them means the test drives the artifact that would actually ship, rather than a locally compiled pair that might differ from it. It also avoids a second full Rust build in a job that has already paid for one. Gated on `helm` OR `rust`, because the chart's behaviour depends on the image as much as on the templates: a chart change and a binary change can each break the deployment, and the earlier failure was on the binary side. Cluster state is dumped on failure — pods, describe, and logs — because a deployment failure that reports only a timeout tells you nothing about which of the twenty things went wrong.
597bfc4 to
47802ed
Compare
|
Correcting the record: #282 merged far more than it said it did, and that was my mistake. Its title and description were about shipping The cause: I created that branch with I flagged this exact failure earlier in this session, on #258, and wrote that flagging it beat letting it pass unnoticed. Then I did it again, and worse — #258's description at least described the code, while #282's did not mention the chart at all. What that means in practice:
The one thing worth taking from it beyond the apology: |
A patch release for one reason: v0.2.0's container image cannot run a node. docker/Dockerfile built only --bin vtopctl, so the published image contained the operator CLI and not vtop-node, the process a deployment actually runs. Anyone pulling ghcr.io/allamiro/vtop-engine:0.2.0 for Kubernetes got a steering wheel and no car, and the Helm chart could not start a single pod against it. The failure is silent to whoever trusts the published artifact, which is why this is a release rather than a line in the next one: the fix is worthless sitting on main while the tag people actually pull stays broken. Fixed in #282, with a CI check that runs both binaries with --version and was verified by control — it fails against 0.2.0 and passes against the rebuild. CI had been building that image for two releases while asserting nothing about its contents. Also here, from making the chart real rather than rendered: #279 verifies the chart by DEPLOYING it. scripts/k8s-smoke.sh installs against a live cluster and asserts pods reach Ready, the metadata Raft group bootstraps and elects a leader, the committed offset equals what was produced, ranges really are independent per pod, and every record survives a force-deleted pod. A helm-deploy CI job runs it on kind, and it earned its place on its first run by catching the missing binary above. The eleven chart review findings and the helm lint/render/refusal job merged earlier, carried to main by #282 — whose description did not mention them. That was a branching mistake, recorded on #279 rather than quietly left in the history. No engine behaviour changed. Every crate version moves together because the workspace shares one version, not because each crate changed.
Scope changed after #282 merged. That PR — titled as a Dockerfile fix — carried the entire chart to main, so everything except the deployment testing is already there. See the comment below; the mistake was mine and worth reading before the diff.
What is left here is the half the chart never had: proof it deploys.
The gap
The
helmCI job lints, renders, checks every manifest has kind/apiVersion, asserts four identity refusals, and checks name limits. All of it static. None of it can tell whether a cluster comes up.That is not a hypothetical gap. The image shipped without
vtop-nodeentirely (#281), so every pod would have CrashLoopBackOff'd — and every static check stayed green.scripts/k8s-smoke.shInstalls the chart against a live cluster and asserts what rendering cannot:
helm-deployCI jobStands up kind, builds the image, and runs the smoke test.
The binaries come out of the image, not from a second
cargo build. The image already contains both — that is exactly what #281 was about — so extracting them means the test drives the artifact that would actually ship. It also skips a second full Rust build in a job that has already paid for one.Gated on
helmorrust, because the chart's behaviour depends on the image as much as on the templates.Verified by running it
Three consecutive passes against Docker Desktop's Kubernetes, plus an earlier manual run that streamed 600 records and recovered all of them from a kill:
The
helm-deployjob also already earned its place: on the pre-rebase branch it failed withCould not find /usr/local/bin/vtop-node, which is precisely the bug it was written to catch.Two things the run taught the script
A fresh producer epoch per round — sequence state is keyed on
(producer_id, producer_epoch), so replaying epoch 1 is correctly deduplicated, the offset stops moving, and that reads as a stall when it is idempotency working.Status output is captured, never piped into
grep -q— the short-circuit closes the pipe and killsvtopctlon SIGPIPE, reported as a panic that hides the real status.README
Now states that a cold install typically restarts once or twice: nodes resolve peer DNS at startup and exit if it is not ready yet, so
RESTARTS 2on a fresh deploy is startup ordering settling — not a broken deployment, and not a reason to loosen the probes.