Task · SKT-0014

Ship a Helm chart for synthkit

Description

synthkit has no Helm chart. The only supported deployment is docker-compose.yml, plus deploy/skcapture/ for the capture tool. Kubernetes is the deployment mode most users will reach for — synthkit exists to model Kubernetes estates — so the gap is conspicuous.

The chart is also the thing that makes an always-on deployment possible, which is the point: today nothing exercises synthkit between waves, so a defect of the class this audit keeps finding (plausible-but-wrong rendering that works against synthkit and fails against production) surfaces only when someone goes looking.

What the chart has to get right, beyond rendering:

Out of scope here: the ArgoCD application that consumes the chart, and the end-to-end validation of what it emits — both tracked separately.

Acceptance Criteria

Definition of Done

Final Summary

charts/synthkit ships, and the two hard parts are enforced STRUCTURALLY rather than documented.

CREDENTIAL SEPARATION is three mechanisms, not an assertion. A fixed table in _helpers.tpl maps each env var to exactly one destination-stack group, and projection is per-group — so listing a self-obs key under the data group FAILS the render, as does GC_TOKEN under selfObs. The self-obs Secret is compared against all seven other groups and equality fails the render, which is the specific way GC_TOKEN would have ended up authenticating self-observability. And extraEnv cannot launder around either check, because any name in synthkit own config surface is rejected. Verified by the wiring pass: the negative fixture fails with a precise message naming the conflicting Secret. There is no values-file credential field at all, and no optional: true anywhere — a missing key holds the pod in CreateContainerConfigError rather than starting with a blank credential.

EXPOSURE defaults closed: loopback bind, no Service or Ingress, default-deny NetworkPolicy, access by port-forward. Opening needs both an explicit acknowledgement value AND a Secret projecting CONTROL_TOKEN, with five negative fixtures covering the subsets. Setting the acknowledgement also flips the bind, so the BINARY own gate re-validates independently — chart guard and binary guard both fail closed.

One subtlety the lane caught and worked around rather than depending on: the binary inContainer() keys on /.dockerenv, which CRI runtimes do not create. The chart mirrors SYNTHKIT_BIND to the JSON_HTTP_ADDR host so both branches of the exposure check reach the same verdict, and blocks SYNTHKIT_IN_CONTAINER from extraEnv — setting it alongside a loopback SYNTHKIT_BIND would otherwise satisfy the gate while listening on all interfaces.

RESOURCES were MEASURED, not habitual. Native binary, dry-run, heap sampled every 15s with the first three minutes discarded: one blueprint 253 MiB floor / 461 MiB peak; four blueprints 289 / 522; the whole catalogue 1716 / 3135. Defaults are requests 100m + 768Mi and a 1Gi memory limit — request above the four-blueprint peak RSS so it is not chronically over-request and first to be evicted, limit about twice peak heap because Go GC targets ~2x live and the sawtooth peak is what a limit absorbs. No CPU limit, since throttling a fixed-cadence tick loop only lengthens ticks. Two cross-checks: the CPU figure independently reproduces what the Compose deployment recorded in August, and the flat sawtooth floor rules out a leak. Estate cost is NOT linear — one to four blueprints moved the floor 36 MiB, twenty-six moved it sixfold.

Volumes: PVC retained by default, fsGroup set because without it control-state saves fail permission denied and every blueprint selection is silently lost, and Recreate strategy because RWO plus RollingUpdate deadlocks.

Wiring applied by the root: make helm-test (103 render assertions, 0 failures), added to ci and to the ci-success aggregator, a GitHub job using preinstalled helm so no third-party action is needed, and the docs nav entry that make docs-check was already failing without.

ACCEPTED DECISION: no values.schema.json. JSON Schema cannot express the two rules that actually matter — self-obs Secret must differ from data Secret, and this var belongs to that group — the template failures name the exact conflicting values, and carrying both would mean two places to keep in sync.

Deploying it into a cluster and validating what it EMITS is SKT-0014.01, deliberately not this task.

View the source file on GitHub