From 3bb62aa720966271466bc09711ed71fc163bdf36 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 16:38:24 -0700 Subject: [PATCH 01/15] chore: onboard Serena for this repository Index the chart's structure and conventions so future sessions get symbol-aware navigation instead of falling back to plain grep, and gitignore Serena's regenerable cache directory. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b39e122..8c2ea68 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ .iml **/tests/__snapshot__/ + +.serena/cache/ +reviews/ From b9ffaba56f37eec037e732890061ee041e983f1f Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 16:38:31 -0700 Subject: [PATCH 02/15] docs: record reserved-name collision convention for chart extension values Any new chart value that lets a user define a named Kubernetes fragment alongside chart-managed resources must fail the render on a name collision rather than shadowing it silently or letting the Kubernetes API reject it with an opaque error. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d633ea7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,49 @@ +# Gremlin Helm Charts — Repo Conventions + +## Reserved-name collisions must fail fast + +Any *new* chart value that lets a user **define** a named Kubernetes fragment alongside +chart-managed resources of the same kind — `extraVolumes`, `extraVolumeMounts`, `initContainers`, +and any future value of this shape — must fail the template render (via a `_validation.tpl`-style +`fail` check, included at the top of the resource template) when the user-supplied name collides +with a name the chart itself already owns. + +This does not apply retroactively to `extraEnv`. It's pre-existing, and `extraEnv` renders last in +both containers' `env:` lists, so a collision there is Kubernetes' documented last-wins override +behavior — a real, working use case (e.g. overriding `GREMLIN_SERVICE_URL` or `https_proxy`), not +a bug. Retrofitting a hard failure onto it would break existing installs on their next +`helm upgrade` with no opt-in. (Considered and rejected during EN-11853 design — see that +contract's `design.md` for the full reasoning.) + +This also does not apply to `envFrom`: it *references* an existing ConfigMap/Secret by name rather +than defining a new object, and the chart cannot see that object's keys at render time, so there +is nothing to statically collide-check. + +Reserved names as of this writing: + +- Volumes: `gremlin-state`, `gremlin-executions`, `gremlin-logs`, `cgroup-root`, `seccomp-root`, + `seccomp-profile`, `gremlin-cert`, `ssl-cert-file`, `gremlin-tls-identity`, `chao-tls-identity`, + `gremlin-opencl-icd`, plus the container-driver socket volumes (`docker-sock`, + `containerd-sock`, `crio-sock`) and any GPU vendor volume names (`kfd`, `dri`, + `opencl-vendors`, ...). +- Env vars: `GREMLIN_TEAM_ID`, `GREMLIN_TEAM_SECRET`, `GREMLIN_TEAM_CERTIFICATE_OR_FILE`, + `GREMLIN_TEAM_PRIVATE_KEY_OR_FILE`, `GREMLIN_IDENTIFIER`, `GREMLIN_CLIENT_TAGS`, + `GREMLIN_COLLECT_DNS`, `GREMLIN_SERVICE_URL`, `GREMLIN_PUSH_POD_CIDR_TAGS`, + `GREMLIN_PUSH_ZONE_CIDR_TAGS`, `https_proxy`, `no_proxy`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, + `GREMLIN_TLS_IDENTITY_CERTIFICATE`, `GREMLIN_TLS_IDENTITY_PRIVATE_KEY`, + `GREMLIN_CLUSTER_ID`, and any GPU vendor env vars (e.g. `NVIDIA_VISIBLE_DEVICES`, + `NVIDIA_DRIVER_CAPABILITIES`). +- Container names (for `initContainers`): `seccomp-init`, and each workload's own container name + (`gremlin`, `chao`). + +**Why:** a silent name collision either gets rejected by the Kubernetes API with an opaque error, +or silently shadows/duplicates a chart-managed resource — both are worse than a clear failure at +`helm template`/`helm install` time naming the conflicting value path. + +**How to apply:** whenever a new chart value accepts user-supplied names in this shape, add its +reserved list to the collision check and extend this file's list above. + +**Known gap:** the reserved lists above are static. Names introduced by a hand-configured +`gremlin.gpu.` block (a custom vendor's `volumes`/`volumeMounts` entries) are not covered +by the check — a collision there surfaces only as a Kubernetes API rejection at apply time, not a +chart-level failure. Whoever adds a vendor block should add its names to the reserved list above. From feca15e342fd5b07fe849bd03cbea4732ad89409 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 16:38:37 -0700 Subject: [PATCH 03/15] docs(EN-11853): add assented requirements and accepted design Gate 1 assented by Danny Seymour on 2026-09-17 (re-assented same day after dropping the extraEnv reserved-name retrofit, a genuine design fork surfaced during design). Design resolves every template insertion point, the initContainers restructure, the chao volumeMounts gating fix, and the reserved-name validation approach. Co-Authored-By: Claude Sonnet 5 From e7f28dc8d0feb7fefa559a453faa8e8cee210292 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 16:38:43 -0700 Subject: [PATCH 04/15] test(EN-11853): add contract probes for standard Helm config values Helm-unittest specs proving B1-B9 (probes, initContainers, extraVolumes/extraVolumeMounts, lifecycle, envFrom, terminationGracePeriodSeconds, dnsConfig, hostAliases, pod-level securityContext) and the backward-compatibility invariant, ahead of any implementation. Each currently fails (or, for the invariant, passes) for the reason the contract clause requires, verified by running every probe directly. This commit is the contract's base_commit. Co-Authored-By: Claude Sonnet 5 --- gremlin/tests/en11853_b1_probes_test.yaml | 140 ++++++++++++++++++ ...n11853_b2_init_containers_reject_test.yaml | 70 +++++++++ .../en11853_b2_init_containers_test.yaml | 70 +++++++++ .../en11853_b3_extra_volumes_reject_test.yaml | 87 +++++++++++ .../tests/en11853_b3_extra_volumes_test.yaml | 100 +++++++++++++ gremlin/tests/en11853_b4_lifecycle_test.yaml | 52 +++++++ gremlin/tests/en11853_b5_envfrom_test.yaml | 56 +++++++ ...1853_b6_termination_grace_period_test.yaml | 58 ++++++++ gremlin/tests/en11853_b7_dnsconfig_test.yaml | 56 +++++++ .../tests/en11853_b8_hostaliases_test.yaml | 52 +++++++ .../en11853_b9_podsecuritycontext_test.yaml | 62 ++++++++ .../tests/en11853_extraenv_exempt_test.yaml | 40 +++++ .../en11853_backward_compat.chao.yaml | 63 ++++++++ .../en11853_backward_compat.gremlin.yaml | 125 ++++++++++++++++ 14 files changed, 1031 insertions(+) create mode 100644 gremlin/tests/en11853_b1_probes_test.yaml create mode 100644 gremlin/tests/en11853_b2_init_containers_reject_test.yaml create mode 100644 gremlin/tests/en11853_b2_init_containers_test.yaml create mode 100644 gremlin/tests/en11853_b3_extra_volumes_reject_test.yaml create mode 100644 gremlin/tests/en11853_b3_extra_volumes_test.yaml create mode 100644 gremlin/tests/en11853_b4_lifecycle_test.yaml create mode 100644 gremlin/tests/en11853_b5_envfrom_test.yaml create mode 100644 gremlin/tests/en11853_b6_termination_grace_period_test.yaml create mode 100644 gremlin/tests/en11853_b7_dnsconfig_test.yaml create mode 100644 gremlin/tests/en11853_b8_hostaliases_test.yaml create mode 100644 gremlin/tests/en11853_b9_podsecuritycontext_test.yaml create mode 100644 gremlin/tests/en11853_extraenv_exempt_test.yaml create mode 100644 gremlin/tests/fixtures/en11853_backward_compat.chao.yaml create mode 100644 gremlin/tests/fixtures/en11853_backward_compat.gremlin.yaml diff --git a/gremlin/tests/en11853_b1_probes_test.yaml b/gremlin/tests/en11853_b1_probes_test.yaml new file mode 100644 index 0000000..8fc74fe --- /dev/null +++ b/gremlin/tests/en11853_b1_probes_test.yaml @@ -0,0 +1,140 @@ +suite: EN-11853 B1 - probes (liveness/readiness/startup) +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should render a user-supplied livenessProbe on the gremlin DaemonSet + set: + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe + value: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + template: daemonset.yaml + + - it: should render a user-supplied readinessProbe on the gremlin DaemonSet + set: + readinessProbe: + httpGet: + path: /ready + port: 8080 + periodSeconds: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe + value: + httpGet: + path: /ready + port: 8080 + periodSeconds: 5 + template: daemonset.yaml + + - it: should render a user-supplied startupProbe on the gremlin DaemonSet + set: + startupProbe: + exec: + command: ["cat", "/tmp/gremlin-ready"] + failureThreshold: 30 + periodSeconds: 10 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe + value: + exec: + command: ["cat", "/tmp/gremlin-ready"] + failureThreshold: 30 + periodSeconds: 10 + template: daemonset.yaml + + - it: should not render any probe fields on the gremlin DaemonSet by default + asserts: + - notExists: + path: spec.template.spec.containers[0].livenessProbe + template: daemonset.yaml + - notExists: + path: spec.template.spec.containers[0].readinessProbe + template: daemonset.yaml + - notExists: + path: spec.template.spec.containers[0].startupProbe + template: daemonset.yaml + + - it: should render a user-supplied livenessProbe on the chao Deployment + set: + chao.livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe + value: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + template: chao-deployment.yaml + + - it: should render a user-supplied readinessProbe on the chao Deployment + set: + chao.readinessProbe: + httpGet: + path: /ready + port: 8080 + periodSeconds: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe + value: + httpGet: + path: /ready + port: 8080 + periodSeconds: 5 + template: chao-deployment.yaml + + - it: should render a user-supplied startupProbe on the chao Deployment + set: + chao.startupProbe: + exec: + command: ["cat", "/tmp/chao-ready"] + failureThreshold: 30 + periodSeconds: 10 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe + value: + exec: + command: ["cat", "/tmp/chao-ready"] + failureThreshold: 30 + periodSeconds: 10 + template: chao-deployment.yaml + + - it: should not render any probe fields on the chao Deployment by default + asserts: + - notExists: + path: spec.template.spec.containers[0].livenessProbe + template: chao-deployment.yaml + - notExists: + path: spec.template.spec.containers[0].readinessProbe + template: chao-deployment.yaml + - notExists: + path: spec.template.spec.containers[0].startupProbe + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b2_init_containers_reject_test.yaml b/gremlin/tests/en11853_b2_init_containers_reject_test.yaml new file mode 100644 index 0000000..3fbb61f --- /dev/null +++ b/gremlin/tests/en11853_b2_init_containers_reject_test.yaml @@ -0,0 +1,70 @@ +suite: EN-11853 B2 rejects - initContainers reserved-name collision +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should fail the gremlin DaemonSet render when initContainers collides with a reserved container name + set: + initContainers: + - name: seccomp-init + image: busybox:1.36 + asserts: + - failedTemplate: + errorMessage: 'initContainers: "seccomp-init" collides with a container name the gremlin chart manages. Rename it.' + template: daemonset.yaml + + - it: should still fail exactly once and render the fan-out when GPU is enabled and initContainers collides + set: + gremlin.gpu.enabled: true + initContainers: + - name: gremlin + image: busybox:1.36 + asserts: + - failedTemplate: + errorMessage: 'initContainers: "gremlin" collides with a container name the gremlin chart manages. Rename it.' + template: daemonset.yaml + + - it: should not fail the gremlin DaemonSet render when initContainers does not collide + set: + initContainers: + - name: wait-for-node-ready + image: busybox:1.36 + asserts: + - notFailedTemplate: {} + template: daemonset.yaml + + - it: should fail the chao Deployment render when initContainers collides with a reserved container name + set: + chao.initContainers: + - name: chao + image: busybox:1.36 + asserts: + - failedTemplate: + errorMessage: 'chao.initContainers: "chao" collides with a container name the gremlin chart manages. Rename it.' + template: chao-deployment.yaml + + - it: should not fail the chao Deployment render when initContainers does not collide + set: + chao.initContainers: + - name: debug-network-check + image: busybox:1.36 + asserts: + - notFailedTemplate: {} + template: chao-deployment.yaml + + - it: should render nothing for chao when chao.create is false, even with a colliding initContainers name + set: + chao.create: false + chao.initContainers: + - name: chao + image: busybox:1.36 + asserts: + - hasDocuments: + count: 0 + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b2_init_containers_test.yaml b/gremlin/tests/en11853_b2_init_containers_test.yaml new file mode 100644 index 0000000..dc81a25 --- /dev/null +++ b/gremlin/tests/en11853_b2_init_containers_test.yaml @@ -0,0 +1,70 @@ +suite: EN-11853 B2 - user-supplied initContainers +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should append a user-supplied initContainer on the gremlin DaemonSet when seccomp is off + set: + initContainers: + - name: wait-for-node-ready + image: busybox:1.36 + command: ["sh", "-c", "until [ -f /var/run/node-ready ]; do sleep 1; done"] + asserts: + - equal: + path: spec.template.spec.initContainers[0] + value: + name: wait-for-node-ready + image: busybox:1.36 + command: ["sh", "-c", "until [ -f /var/run/node-ready ]; do sleep 1; done"] + template: daemonset.yaml + + - it: should order a user-supplied initContainer after seccomp-init on the gremlin DaemonSet + set: + gremlin.podSecurity.seccomp.enabled: true + gremlin.podSecurity.seccomp.profile: localhost/gremlin + initContainers: + - name: wait-for-node-ready + image: busybox:1.36 + command: ["sh", "-c", "until [ -f /var/run/node-ready ]; do sleep 1; done"] + asserts: + - equal: + path: spec.template.spec.initContainers[0].name + value: seccomp-init + template: daemonset.yaml + - equal: + path: spec.template.spec.initContainers[1].name + value: wait-for-node-ready + template: daemonset.yaml + + - it: should not render initContainers on the gremlin DaemonSet by default + asserts: + - notExists: + path: spec.template.spec.initContainers + template: daemonset.yaml + + - it: should append a user-supplied initContainer on the chao Deployment + set: + chao.initContainers: + - name: debug-network-check + image: busybox:1.36 + command: ["sh", "-c", "nslookup kubernetes.default"] + asserts: + - equal: + path: spec.template.spec.initContainers[0] + value: + name: debug-network-check + image: busybox:1.36 + command: ["sh", "-c", "nslookup kubernetes.default"] + template: chao-deployment.yaml + + - it: should not render initContainers on the chao Deployment by default + asserts: + - notExists: + path: spec.template.spec.initContainers + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b3_extra_volumes_reject_test.yaml b/gremlin/tests/en11853_b3_extra_volumes_reject_test.yaml new file mode 100644 index 0000000..f380606 --- /dev/null +++ b/gremlin/tests/en11853_b3_extra_volumes_reject_test.yaml @@ -0,0 +1,87 @@ +suite: EN-11853 B3 rejects - extraVolumes / extraVolumeMounts reserved-name collision +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should fail the gremlin DaemonSet render when extraVolumes collides with a reserved volume name + set: + extraVolumes: + - name: gremlin-state + emptyDir: {} + asserts: + - failedTemplate: + errorMessage: 'extraVolumes: "gremlin-state" collides with a volume name the gremlin chart manages. Rename it.' + template: daemonset.yaml + + - it: should fail the gremlin DaemonSet render when extraVolumeMounts collides with a reserved volume name + set: + extraVolumeMounts: + - name: cgroup-root + mountPath: /somewhere + asserts: + - failedTemplate: + errorMessage: 'extraVolumeMounts: "cgroup-root" collides with a volume name the gremlin chart manages. Rename it.' + template: daemonset.yaml + + - it: should not fail the gremlin DaemonSet render when extraVolumes/extraVolumeMounts do not collide + set: + extraVolumes: + - name: debug-logs + hostPath: + path: /var/log/debug + extraVolumeMounts: + - name: debug-logs + mountPath: /var/log/debug + asserts: + - notFailedTemplate: {} + template: daemonset.yaml + + - it: should fail the chao Deployment render when extraVolumes collides with a reserved volume name + set: + chao.extraVolumes: + - name: gremlin-cert + emptyDir: {} + asserts: + - failedTemplate: + errorMessage: 'chao.extraVolumes: "gremlin-cert" collides with a volume name the gremlin chart manages. Rename it.' + template: chao-deployment.yaml + + - it: should fail the chao Deployment render when extraVolumeMounts collides with a reserved volume name + set: + chao.extraVolumeMounts: + - name: ssl-cert-file + mountPath: /somewhere + asserts: + - failedTemplate: + errorMessage: 'chao.extraVolumeMounts: "ssl-cert-file" collides with a volume name the gremlin chart manages. Rename it.' + template: chao-deployment.yaml + + - it: should not fail the chao Deployment render when extraVolumes/extraVolumeMounts do not collide + set: + chao.extraVolumes: + - name: debug-logs + hostPath: + path: /var/log/debug + chao.extraVolumeMounts: + - name: debug-logs + mountPath: /var/log/debug + asserts: + - notFailedTemplate: {} + template: chao-deployment.yaml + + - it: should render nothing for chao when chao.create is false, even with a colliding extraVolumes name + set: + chao.create: false + chao.extraVolumes: + - name: gremlin-cert + emptyDir: {} + asserts: + - hasDocuments: + count: 0 + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b3_extra_volumes_test.yaml b/gremlin/tests/en11853_b3_extra_volumes_test.yaml new file mode 100644 index 0000000..b2ab464 --- /dev/null +++ b/gremlin/tests/en11853_b3_extra_volumes_test.yaml @@ -0,0 +1,100 @@ +suite: EN-11853 B3 - extraVolumes / extraVolumeMounts +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should append a user-supplied volume and mount on the gremlin DaemonSet + set: + extraVolumes: + - name: debug-logs + hostPath: + path: /var/log/debug + extraVolumeMounts: + - name: debug-logs + mountPath: /var/log/debug + readOnly: true + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: debug-logs + hostPath: + path: /var/log/debug + template: daemonset.yaml + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: debug-logs + mountPath: /var/log/debug + readOnly: true + template: daemonset.yaml + + - it: should not render the extra volume or mount on the gremlin DaemonSet by default + asserts: + - notContains: + path: spec.template.spec.volumes + content: + name: debug-logs + template: daemonset.yaml + - notContains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: debug-logs + template: daemonset.yaml + + - it: should append a user-supplied volume and mount on the chao Deployment + set: + chao.extraVolumes: + - name: debug-logs + hostPath: + path: /var/log/debug + chao.extraVolumeMounts: + - name: debug-logs + mountPath: /var/log/debug + readOnly: true + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: debug-logs + hostPath: + path: /var/log/debug + template: chao-deployment.yaml + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: debug-logs + mountPath: /var/log/debug + readOnly: true + template: chao-deployment.yaml + + - it: should bring the volumeMounts key into existence on chao when no other volumeMounts gate condition holds + set: + gremlin.secret.managed: true + gremlin.secret.type: secret + gremlin.secret.teamID: test-team-id + gremlin.secret.teamSecret: test-team-secret + chao.extraVolumeMounts: + - name: debug-logs + mountPath: /var/log/debug + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: debug-logs + mountPath: /var/log/debug + template: chao-deployment.yaml + + - it: should not render the extra volume or mount on the chao Deployment by default + asserts: + - notContains: + path: spec.template.spec.volumes + content: + name: debug-logs + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b4_lifecycle_test.yaml b/gremlin/tests/en11853_b4_lifecycle_test.yaml new file mode 100644 index 0000000..171e6b0 --- /dev/null +++ b/gremlin/tests/en11853_b4_lifecycle_test.yaml @@ -0,0 +1,52 @@ +suite: EN-11853 B4 - lifecycle hooks +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should render a user-supplied lifecycle on the gremlin DaemonSet + set: + lifecycle: + preStop: + exec: + command: ["sh", "-c", "sleep 5"] + asserts: + - equal: + path: spec.template.spec.containers[0].lifecycle + value: + preStop: + exec: + command: ["sh", "-c", "sleep 5"] + template: daemonset.yaml + + - it: should not render a lifecycle field on the gremlin DaemonSet by default + asserts: + - notExists: + path: spec.template.spec.containers[0].lifecycle + template: daemonset.yaml + + - it: should render a user-supplied lifecycle on the chao Deployment + set: + chao.lifecycle: + preStop: + exec: + command: ["sh", "-c", "sleep 5"] + asserts: + - equal: + path: spec.template.spec.containers[0].lifecycle + value: + preStop: + exec: + command: ["sh", "-c", "sleep 5"] + template: chao-deployment.yaml + + - it: should not render a lifecycle field on the chao Deployment by default + asserts: + - notExists: + path: spec.template.spec.containers[0].lifecycle + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b5_envfrom_test.yaml b/gremlin/tests/en11853_b5_envfrom_test.yaml new file mode 100644 index 0000000..89bbd55 --- /dev/null +++ b/gremlin/tests/en11853_b5_envfrom_test.yaml @@ -0,0 +1,56 @@ +suite: EN-11853 B5 - envFrom +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should render user-supplied envFrom sources on the gremlin DaemonSet + set: + envFrom: + - configMapRef: + name: gremlin-extra-config + - secretRef: + name: gremlin-extra-secret + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom + value: + - configMapRef: + name: gremlin-extra-config + - secretRef: + name: gremlin-extra-secret + template: daemonset.yaml + + - it: should not render an envFrom field on the gremlin DaemonSet by default + asserts: + - notExists: + path: spec.template.spec.containers[0].envFrom + template: daemonset.yaml + + - it: should render user-supplied envFrom sources on the chao Deployment + set: + chao.envFrom: + - configMapRef: + name: chao-extra-config + - secretRef: + name: chao-extra-secret + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom + value: + - configMapRef: + name: chao-extra-config + - secretRef: + name: chao-extra-secret + template: chao-deployment.yaml + + - it: should not render an envFrom field on the chao Deployment by default + asserts: + - notExists: + path: spec.template.spec.containers[0].envFrom + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b6_termination_grace_period_test.yaml b/gremlin/tests/en11853_b6_termination_grace_period_test.yaml new file mode 100644 index 0000000..255bea8 --- /dev/null +++ b/gremlin/tests/en11853_b6_termination_grace_period_test.yaml @@ -0,0 +1,58 @@ +suite: EN-11853 B6 - terminationGracePeriodSeconds +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should render a user-supplied terminationGracePeriodSeconds on the gremlin DaemonSet + set: + terminationGracePeriodSeconds: 45 + asserts: + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 45 + template: daemonset.yaml + + - it: should render terminationGracePeriodSeconds of 0 on the gremlin DaemonSet without being dropped as falsy + set: + terminationGracePeriodSeconds: 0 + asserts: + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 0 + template: daemonset.yaml + + - it: should not render terminationGracePeriodSeconds on the gremlin DaemonSet by default + asserts: + - notExists: + path: spec.template.spec.terminationGracePeriodSeconds + template: daemonset.yaml + + - it: should render a user-supplied terminationGracePeriodSeconds on the chao Deployment + set: + chao.terminationGracePeriodSeconds: 45 + asserts: + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 45 + template: chao-deployment.yaml + + - it: should render terminationGracePeriodSeconds of 0 on the chao Deployment without being dropped as falsy + set: + chao.terminationGracePeriodSeconds: 0 + asserts: + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 0 + template: chao-deployment.yaml + + - it: should not render terminationGracePeriodSeconds on the chao Deployment by default + asserts: + - notExists: + path: spec.template.spec.terminationGracePeriodSeconds + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b7_dnsconfig_test.yaml b/gremlin/tests/en11853_b7_dnsconfig_test.yaml new file mode 100644 index 0000000..df86cd1 --- /dev/null +++ b/gremlin/tests/en11853_b7_dnsconfig_test.yaml @@ -0,0 +1,56 @@ +suite: EN-11853 B7 - dnsConfig +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should render a user-supplied dnsConfig on the gremlin DaemonSet + set: + dnsConfig: + nameservers: + - 10.0.0.10 + searches: + - svc.cluster.local + asserts: + - equal: + path: spec.template.spec.dnsConfig + value: + nameservers: + - 10.0.0.10 + searches: + - svc.cluster.local + template: daemonset.yaml + + - it: should not render a dnsConfig field on the gremlin DaemonSet by default + asserts: + - notExists: + path: spec.template.spec.dnsConfig + template: daemonset.yaml + + - it: should render a user-supplied dnsConfig on the chao Deployment + set: + chao.dnsConfig: + nameservers: + - 10.0.0.10 + searches: + - svc.cluster.local + asserts: + - equal: + path: spec.template.spec.dnsConfig + value: + nameservers: + - 10.0.0.10 + searches: + - svc.cluster.local + template: chao-deployment.yaml + + - it: should not render a dnsConfig field on the chao Deployment by default + asserts: + - notExists: + path: spec.template.spec.dnsConfig + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b8_hostaliases_test.yaml b/gremlin/tests/en11853_b8_hostaliases_test.yaml new file mode 100644 index 0000000..88fc04a --- /dev/null +++ b/gremlin/tests/en11853_b8_hostaliases_test.yaml @@ -0,0 +1,52 @@ +suite: EN-11853 B8 - hostAliases +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should render user-supplied hostAliases on the gremlin DaemonSet + set: + hostAliases: + - ip: "10.0.0.5" + hostnames: + - internal.gremlin.example + asserts: + - equal: + path: spec.template.spec.hostAliases + value: + - ip: "10.0.0.5" + hostnames: + - internal.gremlin.example + template: daemonset.yaml + + - it: should not render a hostAliases field on the gremlin DaemonSet by default + asserts: + - notExists: + path: spec.template.spec.hostAliases + template: daemonset.yaml + + - it: should render user-supplied hostAliases on the chao Deployment + set: + chao.hostAliases: + - ip: "10.0.0.5" + hostnames: + - internal.gremlin.example + asserts: + - equal: + path: spec.template.spec.hostAliases + value: + - ip: "10.0.0.5" + hostnames: + - internal.gremlin.example + template: chao-deployment.yaml + + - it: should not render a hostAliases field on the chao Deployment by default + asserts: + - notExists: + path: spec.template.spec.hostAliases + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_b9_podsecuritycontext_test.yaml b/gremlin/tests/en11853_b9_podsecuritycontext_test.yaml new file mode 100644 index 0000000..f1575d3 --- /dev/null +++ b/gremlin/tests/en11853_b9_podsecuritycontext_test.yaml @@ -0,0 +1,62 @@ +suite: EN-11853 B9 - pod-level securityContext +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should render a user-supplied pod-level securityContext on the gremlin DaemonSet + set: + podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + asserts: + - equal: + path: spec.template.spec.securityContext + value: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + template: daemonset.yaml + # container-level securityContext is untouched by the pod-level value + - equal: + path: spec.template.spec.containers[0].securityContext.privileged + value: false + template: daemonset.yaml + + - it: should not render a pod-level securityContext on the gremlin DaemonSet by default + asserts: + - notExists: + path: spec.template.spec.securityContext + template: daemonset.yaml + + - it: should render a user-supplied pod-level securityContext on the chao Deployment + set: + chao.podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + asserts: + - equal: + path: spec.template.spec.securityContext + value: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + template: chao-deployment.yaml + # container-level securityContext is untouched by the pod-level value + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: true + template: chao-deployment.yaml + + - it: should not render a pod-level securityContext on the chao Deployment by default + asserts: + - notExists: + path: spec.template.spec.securityContext + template: chao-deployment.yaml diff --git a/gremlin/tests/en11853_extraenv_exempt_test.yaml b/gremlin/tests/en11853_extraenv_exempt_test.yaml new file mode 100644 index 0000000..2e77045 --- /dev/null +++ b/gremlin/tests/en11853_extraenv_exempt_test.yaml @@ -0,0 +1,40 @@ +suite: EN-11853 invariant - extraEnv is exempt from the reserved-name collision check +templates: + - daemonset.yaml + - chao-deployment.yaml +release: + name: my-release + namespace: my-namespace + revision: 1 + upgrade: true + +tests: + - it: should still render successfully when gremlin.extraEnv shadows a chart-managed env var name + set: + gremlin.extraEnv: + - name: GREMLIN_TEAM_ID + value: overridden-team-id + asserts: + - notFailedTemplate: {} + template: daemonset.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: GREMLIN_TEAM_ID + value: overridden-team-id + template: daemonset.yaml + + - it: should still render successfully when chao.extraEnv shadows a chart-managed env var name + set: + chao.extraEnv: + - name: GREMLIN_CLUSTER_ID + value: overridden-cluster-id + asserts: + - notFailedTemplate: {} + template: chao-deployment.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: GREMLIN_CLUSTER_ID + value: overridden-cluster-id + template: chao-deployment.yaml diff --git a/gremlin/tests/fixtures/en11853_backward_compat.chao.yaml b/gremlin/tests/fixtures/en11853_backward_compat.chao.yaml new file mode 100644 index 0000000..0333a6d --- /dev/null +++ b/gremlin/tests/fixtures/en11853_backward_compat.chao.yaml @@ -0,0 +1,63 @@ +--- +# Source: gremlin/templates/chao-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chao + app.kubernetes.io/name: chao + helm.sh/chart: gremlin-0.28.0 + app.kubernetes.io/version: "1" + name: chao + namespace: default +spec: + replicas: 1 + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/instance: chao + app.kubernetes.io/name: chao + app.kubernetes.io/version: "1" + template: + metadata: + labels: + app.kubernetes.io/instance: chao + app.kubernetes.io/name: chao + helm.sh/chart: gremlin-0.28.0 + app.kubernetes.io/version: "1" + spec: + serviceAccountName: chao + containers: + - image: gremlin/chao:latest + securityContext: + readOnlyRootFilesystem: true + env: + - name: GREMLIN_TEAM_ID + valueFrom: + secretKeyRef: + name: gremlin-secret + key: GREMLIN_TEAM_ID + - name: GREMLIN_CLUSTER_ID + valueFrom: + secretKeyRef: + name: gremlin-secret + key: GREMLIN_CLUSTER_ID + - name: GREMLIN_TEAM_SECRET + valueFrom: + secretKeyRef: + name: gremlin-secret + key: GREMLIN_TEAM_SECRET + args: + - "-api_url" + - "https://api.gremlin.com/v1/kubernetes" + imagePullPolicy: Always + name: chao + volumes: + - name: gremlin-cert + secret: + secretName: gremlin-secret + diff --git a/gremlin/tests/fixtures/en11853_backward_compat.gremlin.yaml b/gremlin/tests/fixtures/en11853_backward_compat.gremlin.yaml new file mode 100644 index 0000000..91c5976 --- /dev/null +++ b/gremlin/tests/fixtures/en11853_backward_compat.gremlin.yaml @@ -0,0 +1,125 @@ +--- +# Source: gremlin/templates/daemonset.yaml +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: my-release-gremlin + namespace: default + labels: + app.kubernetes.io/name: gremlin + helm.sh/chart: gremlin-0.28.0 + app.kubernetes.io/instance: my-release + app.kubernetes.io/managed-by: Helm + version: v1 +spec: + selector: + matchLabels: + app.kubernetes.io/name: gremlin + updateStrategy: + rollingUpdate: + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app.kubernetes.io/name: gremlin + helm.sh/chart: gremlin-0.28.0 + app.kubernetes.io/instance: my-release + app.kubernetes.io/managed-by: Helm + version: v1 + spec: + serviceAccountName: gremlin + dnsPolicy: ClusterFirstWithHostNet + hostPID: true + hostNetwork: true + containers: + - name: gremlin + image: gremlin/gremlin:latest + args: [ "daemon" ] + imagePullPolicy: Always + securityContext: + privileged: false + allowPrivilegeEscalation: false + capabilities: + add: + - KILL + - NET_ADMIN + - SYS_BOOT + - SYS_TIME + - DAC_READ_SEARCH + - SYS_RESOURCE + - SYS_ADMIN + - SYS_PTRACE + - NET_RAW + readOnlyRootFilesystem: false + env: + - name: GREMLIN_TEAM_ID + valueFrom: + secretKeyRef: + name: gremlin-secret + key: GREMLIN_TEAM_ID + - name: GREMLIN_TEAM_SECRET + valueFrom: + secretKeyRef: + name: gremlin-secret + key: GREMLIN_TEAM_SECRET + - name: GREMLIN_IDENTIFIER + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: GREMLIN_CLIENT_TAGS + value: + - name: GREMLIN_COLLECT_DNS + value: "true" + - name: GREMLIN_SERVICE_URL + value: https://api.gremlin.com/v1 + volumeMounts: + - name: gremlin-state + mountPath: /var/lib/gremlin + readOnly: false + - name: gremlin-executions + mountPath: /var/lib/gremlin/executions + readOnly: false + - name: gremlin-logs + mountPath: /var/log/gremlin + readOnly: false + - name: cgroup-root + mountPath: /sys/fs/cgroup + readOnly: false + + - name: containerd-sock + mountPath: /run/containerd/containerd.sock + readOnly: true + - name: crio-sock + mountPath: /run/crio/crio.sock + readOnly: true + - name: docker-sock + mountPath: /var/run/docker.sock + readOnly: true + volumes: + - name: cgroup-root + hostPath: + path: /sys/fs/cgroup + + - name: containerd-sock + hostPath: + path: /run/containerd/containerd.sock + - name: crio-sock + hostPath: + path: /run/crio/crio.sock + - name: docker-sock + hostPath: + path: /var/run/docker.sock + # The Gremlin daemon communicates with Gremlin sidecars via its state directory. + - name: gremlin-state + emptyDir: + medium: Memory + - name: gremlin-executions + hostPath: + path: /var/lib/gremlin/executions + # The Gremlin daemon forwards logs from the Gremlin sidecars to the Gremlin control plane + # These logs should be shared with the host + - name: gremlin-logs + hostPath: + path: /var/log/gremlin + From 2169813a7ef56202e46ab41581eb45562f99f5c8 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 17:14:39 -0700 Subject: [PATCH 05/15] feat(EN-11853): seal contract at Gate 2 11 clauses (9 behavior, 2 property) base-verified against 4279bc29262b73b65027302dbeff5978274a9cdc. Approved by Danny Seymour. Co-Authored-By: Claude Sonnet 5 From d21b4836fee6c37e21ff5b28de9cf7c613bbb4aa Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 17:26:28 -0700 Subject: [PATCH 06/15] feat(EN-11853): add extension values and reserved-name validation Adds the 24 new pass-through values (12 root, 12 chao.*) to values.yaml and the reserved-name collision check templates, wired into both daemonset.yaml and chao-deployment.yaml. Satisfies C2 and C3's rejects clauses in full; the additive placements in _daemonset.tpl and chao-deployment.yaml follow in subsequent commits. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ --- gremlin/templates/_validation.tpl | 54 +++++++++ gremlin/templates/chao-deployment.yaml | 1 + gremlin/templates/daemonset.yaml | 1 + gremlin/values.yaml | 161 +++++++++++++++++++++++++ 4 files changed, 217 insertions(+) diff --git a/gremlin/templates/_validation.tpl b/gremlin/templates/_validation.tpl index 48a93ec..ab0e67a 100644 --- a/gremlin/templates/_validation.tpl +++ b/gremlin/templates/_validation.tpl @@ -84,3 +84,57 @@ stray space would silently become something other than what was written. {{- end -}} {{- end -}} {{- end -}} + +{{/* +Reserved volume names the gremlin chart itself manages, space-separated. Callers turn this into a +list with splitList " " (include "gremlin.reservedNames.volumes" .). Keeping this in one named +template means there is exactly one place to edit when a new chart-managed volume name appears. +*/}} +{{- define "gremlin.reservedNames.volumes" -}} +gremlin-state gremlin-executions gremlin-logs cgroup-root seccomp-root seccomp-profile gremlin-cert ssl-cert-file gremlin-tls-identity chao-tls-identity gremlin-opencl-icd docker-sock containerd-sock crio-sock kfd dri opencl-vendors +{{- end -}} + +{{/* +Reserved container names the gremlin chart itself manages, space-separated. See +gremlin.reservedNames.volumes for the calling convention. +*/}} +{{- define "gremlin.reservedNames.containers" -}} +seccomp-init gremlin chao +{{- end -}} + +{{/* +Generic reserved-name collision check. Takes dict "entries" "reserved" "valuePath" + "kind" , and fails on the first entry whose .name is in the reserved list. An +entry with no name field does not match and is left to the Kubernetes API to reject. +*/}} +{{- define "gremlin.validateReservedNames.check" -}} +{{- range $entry := .entries -}} +{{- if $entry.name -}} +{{- if has $entry.name $.reserved -}} +{{- fail (printf "%s: %q collides with a %s name the gremlin chart manages. Rename it." $.valuePath $entry.name $.kind) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Reserved-name collision checks for the Gremlin DaemonSet's user-supplied initContainers, +extraVolumes, and extraVolumeMounts. Calls fail directly rather than contributing to +gremlin.validateValues's aggregate message, so it also fires when a single template +(daemonset.yaml) is rendered on its own. +*/}} +{{- define "gremlin.validateReservedNames.daemonset" -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.initContainers "reserved" (splitList " " (include "gremlin.reservedNames.containers" .)) "valuePath" "initContainers" "kind" "container") -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.extraVolumes "reserved" (splitList " " (include "gremlin.reservedNames.volumes" .)) "valuePath" "extraVolumes" "kind" "volume") -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.extraVolumeMounts "reserved" (splitList " " (include "gremlin.reservedNames.volumes" .)) "valuePath" "extraVolumeMounts" "kind" "volume") -}} +{{- end -}} + +{{/* +Reserved-name collision checks for the Chao deployment's user-supplied initContainers, +extraVolumes, and extraVolumeMounts. See gremlin.validateReservedNames.daemonset. +*/}} +{{- define "gremlin.validateReservedNames.chao" -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.initContainers "reserved" (splitList " " (include "gremlin.reservedNames.containers" .)) "valuePath" "chao.initContainers" "kind" "container") -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.extraVolumes "reserved" (splitList " " (include "gremlin.reservedNames.volumes" .)) "valuePath" "chao.extraVolumes" "kind" "volume") -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.extraVolumeMounts "reserved" (splitList " " (include "gremlin.reservedNames.volumes" .)) "valuePath" "chao.extraVolumeMounts" "kind" "volume") -}} +{{- end -}} diff --git a/gremlin/templates/chao-deployment.yaml b/gremlin/templates/chao-deployment.yaml index 65a561a..63f498e 100644 --- a/gremlin/templates/chao-deployment.yaml +++ b/gremlin/templates/chao-deployment.yaml @@ -1,5 +1,6 @@ {{ if .Values.chao.create }} {{- include "chaoNamespacesValidate" . -}} +{{- include "gremlin.validateReservedNames.chao" . -}} apiVersion: apps/v1 kind: Deployment metadata: diff --git a/gremlin/templates/daemonset.yaml b/gremlin/templates/daemonset.yaml index c1def39..404c0a0 100644 --- a/gremlin/templates/daemonset.yaml +++ b/gremlin/templates/daemonset.yaml @@ -8,6 +8,7 @@ template (see _daemonset.tpl), which knows nothing about GPUs or node mixes. that vendor's nodes and carrying that vendor's GPU config, plus `-gpu-none` for every node that belongs to none of those vendors. */ -}} +{{- include "gremlin.validateReservedNames.daemonset" . -}} {{- $fullname := include "gremlin.fullname" $ -}} {{- if .Values.gremlin.gpu.enabled -}} {{- $vendors := default (list) .Values.gremlin.gpu.vendors -}} diff --git a/gremlin/values.yaml b/gremlin/values.yaml index 5c4c3b3..1078e5b 100644 --- a/gremlin/values.yaml +++ b/gremlin/values.yaml @@ -23,6 +23,86 @@ tolerations: [] affinity: {} +# livenessProbe - +# A Kubernetes Probe for the Gremlin Agent container. Applies to the Gremlin DaemonSet only; +# the Chao deployment has its own chao.livenessProbe. Passed through to the rendered manifest +# as-is. Unset by default, which renders no probe. +livenessProbe: {} + +# readinessProbe - +# A Kubernetes Probe for the Gremlin Agent container. Applies to the Gremlin DaemonSet only; +# the Chao deployment has its own chao.readinessProbe. Passed through to the rendered manifest +# as-is. Unset by default, which renders no probe. +readinessProbe: {} + +# startupProbe - +# A Kubernetes Probe for the Gremlin Agent container. Applies to the Gremlin DaemonSet only; +# the Chao deployment has its own chao.startupProbe. Passed through to the rendered manifest +# as-is. Unset by default, which renders no probe. +startupProbe: {} + +# initContainers - +# Additional init containers for the Gremlin DaemonSet's pod spec. Applies to the Gremlin +# DaemonSet only; the Chao deployment has its own chao.initContainers. Appended after any +# chart-managed init container (e.g. seccomp-init). Passed through to the rendered manifest +# as-is. A container name colliding with a name the gremlin chart manages (e.g. seccomp-init, +# gremlin) fails the install. Unset by default, which renders no additional init containers. +initContainers: [] + +# extraVolumes - +# Additional volumes for the Gremlin DaemonSet's pod spec. Applies to the Gremlin DaemonSet +# only; the Chao deployment has its own chao.extraVolumes. Passed through to the rendered +# manifest as-is. A volume name colliding with a name the gremlin chart manages (e.g. +# gremlin-state, gremlin-cert) fails the install. Unset by default, which renders no additional +# volumes. +extraVolumes: [] + +# extraVolumeMounts - +# Additional volume mounts for the Gremlin Agent container. Applies to the Gremlin DaemonSet +# only; the Chao deployment has its own chao.extraVolumeMounts. Passed through to the rendered +# manifest as-is. A volume name colliding with a name the gremlin chart manages (e.g. +# gremlin-state, gremlin-cert) fails the install. Unset by default, which renders no additional +# volume mounts. +extraVolumeMounts: [] + +# lifecycle - +# A Kubernetes Lifecycle object (preStop/postStart hooks) for the Gremlin Agent container. +# Applies to the Gremlin DaemonSet only; the Chao deployment has its own chao.lifecycle. Passed +# through to the rendered manifest as-is. Unset by default, which renders no lifecycle hooks. +lifecycle: {} + +# envFrom - +# Additional sources (ConfigMaps/Secrets) to populate the Gremlin Agent container's environment +# from. Applies to the Gremlin DaemonSet only; the Chao deployment has its own chao.envFrom. +# Passed through to the rendered manifest as-is. Unset by default, which renders no envFrom. +envFrom: [] + +# terminationGracePeriodSeconds - +# The termination grace period, in seconds, for the Gremlin DaemonSet's pods. Applies to the +# Gremlin DaemonSet only; the Chao deployment has its own chao.terminationGracePeriodSeconds. +# Passed through to the rendered manifest as-is. Unset by default, which leaves Kubernetes' +# default (30 seconds) in effect. +terminationGracePeriodSeconds: + +# dnsConfig - +# A Kubernetes PodDNSConfig object for the Gremlin DaemonSet's pod spec. Applies to the Gremlin +# DaemonSet only; the Chao deployment has its own chao.dnsConfig. Passed through to the rendered +# manifest as-is. Unset by default, which renders no dnsConfig. +dnsConfig: {} + +# hostAliases - +# A list of Kubernetes HostAlias entries for the Gremlin DaemonSet's pod spec. Applies to the +# Gremlin DaemonSet only; the Chao deployment has its own chao.hostAliases. Passed through to +# the rendered manifest as-is. Unset by default, which renders no hostAliases. +hostAliases: [] + +# podSecurityContext - +# A Kubernetes pod-level SecurityContext for the Gremlin DaemonSet's pod spec. Applies to the +# Gremlin DaemonSet only; the Chao deployment has its own chao.podSecurityContext. Renders +# alongside the existing container-level securityContext, which is unaffected. Passed through +# to the rendered manifest as-is. Unset by default, which renders no pod-level securityContext. +podSecurityContext: {} + gremlin: # gremlin.updateStrategy - @@ -528,6 +608,87 @@ chao: # Specify any arbitrary environment variables to pass to the Chao deployment. extraEnv: [] + # chao.livenessProbe - + # A Kubernetes Probe for the Chao deployment's container. Applies to the Chao deployment only; + # the Gremlin DaemonSet has its own livenessProbe. Passed through to the rendered manifest + # as-is. Unset by default, which renders no probe. + livenessProbe: {} + + # chao.readinessProbe - + # A Kubernetes Probe for the Chao deployment's container. Applies to the Chao deployment only; + # the Gremlin DaemonSet has its own readinessProbe. Passed through to the rendered manifest + # as-is. Unset by default, which renders no probe. + readinessProbe: {} + + # chao.startupProbe - + # A Kubernetes Probe for the Chao deployment's container. Applies to the Chao deployment only; + # the Gremlin DaemonSet has its own startupProbe. Passed through to the rendered manifest + # as-is. Unset by default, which renders no probe. + startupProbe: {} + + # chao.initContainers - + # Additional init containers for the Chao deployment's pod spec. Applies to the Chao + # deployment only; the Gremlin DaemonSet has its own initContainers. Passed through to the + # rendered manifest as-is. A container name colliding with a name the gremlin chart manages + # (e.g. gremlin, chao) fails the install. Unset by default, which renders no additional init + # containers. + initContainers: [] + + # chao.extraVolumes - + # Additional volumes for the Chao deployment's pod spec. Applies to the Chao deployment only; + # the Gremlin DaemonSet has its own extraVolumes. Passed through to the rendered manifest + # as-is. A volume name colliding with a name the gremlin chart manages (e.g. gremlin-cert, + # chao-tls-identity) fails the install. Unset by default, which renders no additional volumes. + extraVolumes: [] + + # chao.extraVolumeMounts - + # Additional volume mounts for the Chao deployment's container. Applies to the Chao deployment + # only; the Gremlin DaemonSet has its own extraVolumeMounts. Passed through to the rendered + # manifest as-is. A volume name colliding with a name the gremlin chart manages (e.g. + # gremlin-cert, chao-tls-identity) fails the install. Unset by default, which renders no + # additional volume mounts. + extraVolumeMounts: [] + + # chao.lifecycle - + # A Kubernetes Lifecycle object (preStop/postStart hooks) for the Chao deployment's container. + # Applies to the Chao deployment only; the Gremlin DaemonSet has its own lifecycle. Passed + # through to the rendered manifest as-is. Unset by default, which renders no lifecycle hooks. + lifecycle: {} + + # chao.envFrom - + # Additional sources (ConfigMaps/Secrets) to populate the Chao deployment's container + # environment from. Applies to the Chao deployment only; the Gremlin DaemonSet has its own + # envFrom. Passed through to the rendered manifest as-is. Unset by default, which renders no + # envFrom. + envFrom: [] + + # chao.terminationGracePeriodSeconds - + # The termination grace period, in seconds, for the Chao deployment's pods. Applies to the + # Chao deployment only; the Gremlin DaemonSet has its own terminationGracePeriodSeconds. + # Passed through to the rendered manifest as-is. Unset by default, which leaves Kubernetes' + # default (30 seconds) in effect. + terminationGracePeriodSeconds: + + # chao.dnsConfig - + # A Kubernetes PodDNSConfig object for the Chao deployment's pod spec. Applies to the Chao + # deployment only; the Gremlin DaemonSet has its own dnsConfig. Passed through to the rendered + # manifest as-is. Unset by default, which renders no dnsConfig. + dnsConfig: {} + + # chao.hostAliases - + # A list of Kubernetes HostAlias entries for the Chao deployment's pod spec. Applies to the + # Chao deployment only; the Gremlin DaemonSet has its own hostAliases. Passed through to the + # rendered manifest as-is. Unset by default, which renders no hostAliases. + hostAliases: [] + + # chao.podSecurityContext - + # A Kubernetes pod-level SecurityContext for the Chao deployment's pod spec. Applies to the + # Chao deployment only; the Gremlin DaemonSet has its own podSecurityContext. Renders + # alongside the existing container-level securityContext, which is unaffected. Passed through + # to the rendered manifest as-is. Unset by default, which renders no pod-level + # securityContext. + podSecurityContext: {} + # chao.namespaces # list of namespaces for Gremlin to watch for attacking namespaces: [] From 8f67ca7607657e03490c1fc8d13c9824ed86ef2e Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 17:28:28 -0700 Subject: [PATCH 07/15] feat(EN-11853): implement gremlin DaemonSet config placements Adds pod-spec fields (terminationGracePeriodSeconds, dnsConfig, hostAliases, podSecurityContext), the initContainers restructure (user containers appended after seccomp-init), container fields (envFrom, probes, lifecycle), and extraVolumes/extraVolumeMounts appends to _daemonset.tpl. Satisfies C1, C4-C9, and C2's additive half on the gremlin DaemonSet. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ --- gremlin/templates/_daemonset.tpl | 41 +++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/gremlin/templates/_daemonset.tpl b/gremlin/templates/_daemonset.tpl index 00b52b8..4b8b8bf 100644 --- a/gremlin/templates/_daemonset.tpl +++ b/gremlin/templates/_daemonset.tpl @@ -21,6 +21,7 @@ Context: dict {{- /* CDI device: global gremlin.gpu.cdiDevice, optionally overridden per vendor block */ -}} {{- $cdiDevice := "" -}} {{- with $gpu -}}{{- $cdiDevice = default $root.Values.gremlin.gpu.cdiDevice .cdiDevice -}}{{- end -}} +{{- $seccompInit := and $root.Values.gremlin.podSecurity.seccomp.enabled (eq "localhost/gremlin" $root.Values.gremlin.podSecurity.seccomp.profile) -}} apiVersion: apps/v1 kind: DaemonSet metadata: @@ -100,12 +101,25 @@ spec: dnsPolicy: {{ $root.Values.gremlin.dnsPolicy }} hostPID: {{ $root.Values.gremlin.hostPID }} hostNetwork: {{ $root.Values.gremlin.hostNetwork }} + {{- if not (kindIs "invalid" $root.Values.terminationGracePeriodSeconds) }} + terminationGracePeriodSeconds: {{ $root.Values.terminationGracePeriodSeconds }} + {{- end }} + {{- with $root.Values.dnsConfig }} + dnsConfig: {{ toYaml . | nindent 8 }} + {{- end }} + {{- with $root.Values.hostAliases }} + hostAliases: {{ toYaml . | nindent 8 }} + {{- end }} + {{- with $root.Values.podSecurityContext }} + securityContext: {{ toYaml . | nindent 8 }} + {{- end }} {{- if $root.Values.image.pullSecret }} imagePullSecrets: - name: {{ $root.Values.image.pullSecret }} {{- end }} - {{- if and $root.Values.gremlin.podSecurity.seccomp.enabled (eq "localhost/gremlin" $root.Values.gremlin.podSecurity.seccomp.profile) }} + {{- if or $seccompInit $root.Values.initContainers }} initContainers: + {{- if $seccompInit }} - name: seccomp-init image: {{ $root.Values.image.repository }}:{{ $root.Values.image.tag }} imagePullPolicy: {{ $root.Values.image.pullPolicy }} @@ -118,6 +132,10 @@ spec: - cp - /gremlin/seccomp.json - {{ $root.Values.gremlin.podSecurity.seccomp.root }}/gremlin + {{- end }} + {{- with $root.Values.initContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} containers: - name: {{ $root.Chart.Name }} @@ -207,6 +225,21 @@ spec: {{- with $root.Values.gremlin.extraEnv }} {{- toYaml . | nindent 10 }} {{- end }} + {{- with $root.Values.envFrom }} + envFrom: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with $root.Values.livenessProbe }} + livenessProbe: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with $root.Values.readinessProbe }} + readinessProbe: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with $root.Values.startupProbe }} + startupProbe: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with $root.Values.lifecycle }} + lifecycle: {{ toYaml . | nindent 10 }} + {{- end }} volumeMounts: - name: gremlin-state mountPath: /var/lib/gremlin @@ -245,6 +278,9 @@ spec: subPath: {{ $gpu.openclIcd.filename }} readOnly: true {{- end }} + {{- with $root.Values.extraVolumeMounts }} + {{- toYaml . | nindent 10 }} + {{- end }} volumes: - name: cgroup-root hostPath: @@ -293,6 +329,9 @@ spec: configMap: name: {{ include "gremlin.fullname" $root }}-opencl-icd {{- end }} + {{- with $root.Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- if $root.Values.gremlin.priorityClassName }} priorityClassName: {{ $root.Values.gremlin.priorityClassName }} {{- end }} From 95a5997c6c228cdc235bbd72af86586fdf3651c5 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 17:29:03 -0700 Subject: [PATCH 08/15] feat(EN-11853): implement chao Deployment config placements Adds pod-spec fields, initContainers, container fields (envFrom, probes, lifecycle), the volumeMounts gate fix (extended to fire on chao.extraVolumeMounts alone), and extraVolumes/extraVolumeMounts appends to chao-deployment.yaml. All 11 contract clauses now pass in full; full existing suite (30 suites, 214 tests) is regression-free. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ --- gremlin/templates/chao-deployment.yaml | 38 +++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/gremlin/templates/chao-deployment.yaml b/gremlin/templates/chao-deployment.yaml index 63f498e..59cad1b 100644 --- a/gremlin/templates/chao-deployment.yaml +++ b/gremlin/templates/chao-deployment.yaml @@ -54,6 +54,21 @@ spec: imagePullSecrets: - name: {{ .Values.chaoimage.pullSecret }} {{- end }} + {{- if not (kindIs "invalid" .Values.chao.terminationGracePeriodSeconds) }} + terminationGracePeriodSeconds: {{ .Values.chao.terminationGracePeriodSeconds }} + {{- end }} + {{- with .Values.chao.dnsConfig }} + dnsConfig: {{ toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.chao.hostAliases }} + hostAliases: {{ toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.chao.podSecurityContext }} + securityContext: {{ toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.chao.initContainers }} + initContainers: {{ toYaml . | nindent 8 }} + {{- end }} containers: - image: {{ .Values.chaoimage.repository }}:{{ .Values.chaoimage.tag }} {{- with (default .Values.resources .Values.chao.resources) }} @@ -107,6 +122,21 @@ spec: {{- with .Values.chao.extraEnv }} {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.chao.envFrom }} + envFrom: {{ toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.chao.livenessProbe }} + livenessProbe: {{ toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.chao.readinessProbe }} + readinessProbe: {{ toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.chao.startupProbe }} + startupProbe: {{ toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.chao.lifecycle }} + lifecycle: {{ toYaml . | nindent 12 }} + {{- end }} args: - "-api_url" - "{{ include "gremlinServiceUrl" . }}/kubernetes" @@ -146,7 +176,7 @@ spec: {{- end }} imagePullPolicy: {{ .Values.chaoimage.pullPolicy }} name: chao -{{- if (or ((eq (include "gremlin.secretType" .) "certificate")) .Values.ssl.certFile (include "chaoTlsIdentityVolumeMounts" .)) }} +{{- if (or ((eq (include "gremlin.secretType" .) "certificate")) .Values.ssl.certFile (include "chaoTlsIdentityVolumeMounts" .) .Values.chao.extraVolumeMounts) }} volumeMounts: {{- end }} {{- if (eq (include "gremlin.secretType" .) "certificate") }} @@ -161,6 +191,9 @@ spec: {{- end }} {{- if include "chaoTlsIdentityVolumeMounts" . }} {{- include "chaoTlsIdentityVolumeMounts" . | nindent 10 }} +{{- end }} +{{- with .Values.chao.extraVolumeMounts }} +{{- toYaml . | nindent 10 }} {{- end }} volumes: - name: gremlin-cert @@ -174,6 +207,9 @@ spec: {{- if include "chaoTlsIdentityVolumes" . }} {{- include "chaoTlsIdentityVolumes" . | nindent 6 }} {{- end }} +{{- with .Values.chao.extraVolumes }} +{{- toYaml . | nindent 6 }} +{{- end }} {{- if .Values.chao.priorityClassName }} priorityClassName: {{ .Values.chao.priorityClassName }} {{- end }} From 73fe881548cc3db6acfb461d145cbb7925cdb2a3 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 17:39:51 -0700 Subject: [PATCH 09/15] fix(EN-11853): correct seal.txt format and add validation dossier seal.txt's sha256 line needed the bare "sha256 " form build_dossier.py parses, not "sha256: ". All 11 clauses validated PASS against the implementation; dossier and run records added ahead of Gate 3 signature. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ From f62e40904890c6e05efe106258c6e01e932c05df Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 18:19:42 -0700 Subject: [PATCH 10/15] fix(EN-11853): address review findings - Coerce terminationGracePeriodSeconds with `| int` on both workloads, closing a YAML-injection path a bare-scalar string value could use to inject sibling pod-spec keys (security review, low severity; same shape as the chart's pre-existing dnsPolicy interpolation). - Document that hostAliases has no effect while gremlin.hostNetwork is true (the default) -- correct rendering, no runtime effect. - Document that envFrom does not override chart-managed env vars, unlike the adjacent extraEnv (Kubernetes gives env: precedence). - Document the interaction between extraVolumes/initContainers and the chart's own PodSecurityPolicy/SecurityContextConstraints when enabled. - Add all 24 new values to gremlin/README.md's values table, which was otherwise the only chart doc surface left undocumented. None of these change any tested behavior -- comments and an integer coercion that is a no-op for every value the contract's probes set. Full suite re-run clean: 30 suites, 214 tests, base verification still passes for all 11 clauses. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ --- gremlin/README.md | 24 ++++++++++++++++++++++++ gremlin/templates/_daemonset.tpl | 2 +- gremlin/templates/chao-deployment.yaml | 2 +- gremlin/values.yaml | 19 +++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/gremlin/README.md b/gremlin/README.md index 9226633..dc27f4b 100644 --- a/gremlin/README.md +++ b/gremlin/README.md @@ -34,6 +34,18 @@ their default values. See values.yaml for all available options. | `nodeSelector` | Map of node labels for pod assignment for the `gremlin` container | `{}` | | `tolerations` | List of node taints to tolerate for the `gremlin` container | `[]` | | `affinity` | Map of node/pod affinities for the `gremlin` container | `{}` | +| `livenessProbe` | A Kubernetes `Probe` for the `gremlin` container | `{}` | +| `readinessProbe` | A Kubernetes `Probe` for the `gremlin` container | `{}` | +| `startupProbe` | A Kubernetes `Probe` for the `gremlin` container | `{}` | +| `initContainers` | Additional init containers for the `gremlin` DaemonSet, appended after `seccomp-init` when enabled. A name colliding with one the chart manages fails the install | `[]` | +| `extraVolumes` | Additional volumes for the `gremlin` DaemonSet. A name colliding with one the chart manages fails the install | `[]` | +| `extraVolumeMounts` | Additional volume mounts for the `gremlin` container. A name colliding with one the chart manages fails the install | `[]` | +| `lifecycle` | A Kubernetes `Lifecycle` object (`preStop`/`postStart`) for the `gremlin` container | `{}` | +| `envFrom` | Additional `ConfigMap`/`Secret` sources for the `gremlin` container's environment. Unlike `gremlin.extraEnv`, does not override chart-managed env vars (Kubernetes gives `env:` precedence over `envFrom:`) | `[]` | +| `terminationGracePeriodSeconds` | Termination grace period, in seconds, for the `gremlin` DaemonSet's pods | (Kubernetes default: `30`) | +| `dnsConfig` | A Kubernetes `PodDNSConfig` for the `gremlin` DaemonSet's pod spec | `{}` | +| `hostAliases` | A list of Kubernetes `HostAlias` entries for the `gremlin` DaemonSet's pod spec. No effect while `gremlin.hostNetwork` is `true` (the default) | `[]` | +| `podSecurityContext` | A Kubernetes pod-level `SecurityContext` for the `gremlin` DaemonSet, alongside the existing container-level `gremlin.podSecurity.*` settings | `{}` | | `chao.podLabels` | Kubernetes labels applied to the chao deployment and it's Pods | `{}` | | `chao.priorityClassName` | The name of the priority class to use for the Chao deployment | `""` | | `chao.nodeSelector` | Map of node labels for pod assignment for the `chao` container | `{}` | @@ -42,6 +54,18 @@ their default values. See values.yaml for all available options. | `chao.create` | Enable kubernetes targeting by installing k8s client | true | | `chao.resources` | Set resource requests and limits for the chao deployment | `{}` | | `chao.extraEnv` | Specify any arbitrary environment variables to pass to the Chao deployment. | `[]` | +| `chao.livenessProbe` | A Kubernetes `Probe` for the `chao` container | `{}` | +| `chao.readinessProbe` | A Kubernetes `Probe` for the `chao` container | `{}` | +| `chao.startupProbe` | A Kubernetes `Probe` for the `chao` container | `{}` | +| `chao.initContainers` | Additional init containers for the `chao` deployment. A name colliding with one the chart manages fails the install | `[]` | +| `chao.extraVolumes` | Additional volumes for the `chao` deployment. A name colliding with one the chart manages fails the install | `[]` | +| `chao.extraVolumeMounts` | Additional volume mounts for the `chao` container. A name colliding with one the chart manages fails the install | `[]` | +| `chao.lifecycle` | A Kubernetes `Lifecycle` object (`preStop`/`postStart`) for the `chao` container | `{}` | +| `chao.envFrom` | Additional `ConfigMap`/`Secret` sources for the `chao` container's environment. Unlike `chao.extraEnv`, does not override chart-managed env vars (Kubernetes gives `env:` precedence over `envFrom:`) | `[]` | +| `chao.terminationGracePeriodSeconds` | Termination grace period, in seconds, for the `chao` deployment's pods | (Kubernetes default: `30`) | +| `chao.dnsConfig` | A Kubernetes `PodDNSConfig` for the `chao` deployment's pod spec | `{}` | +| `chao.hostAliases` | A list of Kubernetes `HostAlias` entries for the `chao` deployment's pod spec | `[]` | +| `chao.podSecurityContext` | A Kubernetes pod-level `SecurityContext` for the `chao` deployment's pod spec | `{}` | | `chao.namespaces` | List of namespaces for Gremlin to watch for attacking | `[]` | | `chao.excludedNamespaces` | List of namespaces Gremlin should never report or attack (mutually exclusive with `chao.namespaces`) | `[]` | | `chao.features.dynamicQuery.enabled` | Let Gremlin query Kubernetes resources beyond the fixed set Chao watches by default. [See below](#chao-dynamic-queries) | `false` | diff --git a/gremlin/templates/_daemonset.tpl b/gremlin/templates/_daemonset.tpl index 4b8b8bf..3b66ec4 100644 --- a/gremlin/templates/_daemonset.tpl +++ b/gremlin/templates/_daemonset.tpl @@ -102,7 +102,7 @@ spec: hostPID: {{ $root.Values.gremlin.hostPID }} hostNetwork: {{ $root.Values.gremlin.hostNetwork }} {{- if not (kindIs "invalid" $root.Values.terminationGracePeriodSeconds) }} - terminationGracePeriodSeconds: {{ $root.Values.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ $root.Values.terminationGracePeriodSeconds | int }} {{- end }} {{- with $root.Values.dnsConfig }} dnsConfig: {{ toYaml . | nindent 8 }} diff --git a/gremlin/templates/chao-deployment.yaml b/gremlin/templates/chao-deployment.yaml index 59cad1b..7f5634a 100644 --- a/gremlin/templates/chao-deployment.yaml +++ b/gremlin/templates/chao-deployment.yaml @@ -55,7 +55,7 @@ spec: - name: {{ .Values.chaoimage.pullSecret }} {{- end }} {{- if not (kindIs "invalid" .Values.chao.terminationGracePeriodSeconds) }} - terminationGracePeriodSeconds: {{ .Values.chao.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ .Values.chao.terminationGracePeriodSeconds | int }} {{- end }} {{- with .Values.chao.dnsConfig }} dnsConfig: {{ toYaml . | nindent 8 }} diff --git a/gremlin/values.yaml b/gremlin/values.yaml index 1078e5b..33707fd 100644 --- a/gremlin/values.yaml +++ b/gremlin/values.yaml @@ -47,6 +47,9 @@ startupProbe: {} # chart-managed init container (e.g. seccomp-init). Passed through to the rendered manifest # as-is. A container name colliding with a name the gremlin chart manages (e.g. seccomp-init, # gremlin) fails the install. Unset by default, which renders no additional init containers. +# NOTE: when gremlin.podSecurity.podSecurityPolicy.create (or ...securityContextConstraints.create) +# is enabled, the chart's own PSP/SCC may reject volumes an init container mounts; see +# extraVolumes below. initContainers: [] # extraVolumes - @@ -55,6 +58,9 @@ initContainers: [] # manifest as-is. A volume name colliding with a name the gremlin chart manages (e.g. # gremlin-state, gremlin-cert) fails the install. Unset by default, which renders no additional # volumes. +# NOTE: when gremlin.podSecurity.podSecurityPolicy.create (or ...securityContextConstraints.create) +# is enabled, a volume type or hostPath outside what gremlin.podSecurity.volumes / +# allowedHostPaths permits is rejected at admission, not by this chart. extraVolumes: [] # extraVolumeMounts - @@ -75,6 +81,9 @@ lifecycle: {} # Additional sources (ConfigMaps/Secrets) to populate the Gremlin Agent container's environment # from. Applies to the Gremlin DaemonSet only; the Chao deployment has its own chao.envFrom. # Passed through to the rendered manifest as-is. Unset by default, which renders no envFrom. +# NOTE: unlike extraEnv, envFrom does NOT override chart-managed environment variables -- +# Kubernetes gives a container's env: precedence over its envFrom: for any key set in both. +# Use extraEnv (which renders last in env:) to override values such as GREMLIN_SERVICE_URL. envFrom: [] # terminationGracePeriodSeconds - @@ -94,6 +103,8 @@ dnsConfig: {} # A list of Kubernetes HostAlias entries for the Gremlin DaemonSet's pod spec. Applies to the # Gremlin DaemonSet only; the Chao deployment has its own chao.hostAliases. Passed through to # the rendered manifest as-is. Unset by default, which renders no hostAliases. +# NOTE: has no effect while gremlin.hostNetwork is true (the default), since the kubelet does +# not manage /etc/hosts for host-network pods. hostAliases: [] # podSecurityContext - @@ -632,6 +643,8 @@ chao: # rendered manifest as-is. A container name colliding with a name the gremlin chart manages # (e.g. gremlin, chao) fails the install. Unset by default, which renders no additional init # containers. + # NOTE: when gremlin.podSecurity.podSecurityPolicy.create is enabled, the chart's own chao PSP + # may reject volumes an init container mounts; see chao.extraVolumes below. initContainers: [] # chao.extraVolumes - @@ -639,6 +652,9 @@ chao: # the Gremlin DaemonSet has its own extraVolumes. Passed through to the rendered manifest # as-is. A volume name colliding with a name the gremlin chart manages (e.g. gremlin-cert, # chao-tls-identity) fails the install. Unset by default, which renders no additional volumes. + # NOTE: when gremlin.podSecurity.podSecurityPolicy.create is enabled, the chart's own chao PSP + # only permits the "secret" volume type; any other type is rejected at admission, not by this + # chart. extraVolumes: [] # chao.extraVolumeMounts - @@ -660,6 +676,9 @@ chao: # environment from. Applies to the Chao deployment only; the Gremlin DaemonSet has its own # envFrom. Passed through to the rendered manifest as-is. Unset by default, which renders no # envFrom. + # NOTE: unlike extraEnv, envFrom does NOT override chart-managed environment variables -- + # Kubernetes gives a container's env: precedence over its envFrom: for any key set in both. + # Use extraEnv (which renders last in env:) to override chart-managed values. envFrom: [] # chao.terminationGracePeriodSeconds - From dd122cbdadc5be6003920166fda2f33883cd0257 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Thu, 17 Sep 2026 18:19:58 -0700 Subject: [PATCH 11/15] docs(EN-11853): re-validate against post-review commit Re-run validation and rebuilt the dossier after the review-fix commit; all 11 clauses still pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ From 76afd1f9226199748cf3bea1f4b5b8b7d7b711be Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Tue, 22 Sep 2026 11:02:52 -0700 Subject: [PATCH 12/15] test: rename test specs to generic, ticket-independent names Tests describe behavior, not the ticket that introduced it -- drop the EN-11853/B prefixes from filenames and suite: descriptions, matching the existing repo convention (daemonset_resources_test.yaml, chao_deployment_namespaces_test.yaml, etc). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ --- .../{en11853_b7_dnsconfig_test.yaml => dnsconfig_test.yaml} | 2 +- .../tests/{en11853_b5_envfrom_test.yaml => envfrom_test.yaml} | 2 +- ...853_extraenv_exempt_test.yaml => extra_env_exempt_test.yaml} | 2 +- ..._volumes_reject_test.yaml => extra_volumes_reject_test.yaml} | 2 +- ...11853_b3_extra_volumes_test.yaml => extra_volumes_test.yaml} | 2 +- ...1853_backward_compat.chao.yaml => backward_compat_chao.yaml} | 0 ...kward_compat.gremlin.yaml => backward_compat_daemonset.yaml} | 0 .../{en11853_b8_hostaliases_test.yaml => hostaliases_test.yaml} | 2 +- ...ainers_reject_test.yaml => init_containers_reject_test.yaml} | 2 +- ...3_b2_init_containers_test.yaml => init_containers_test.yaml} | 2 +- .../{en11853_b4_lifecycle_test.yaml => lifecycle_test.yaml} | 2 +- ...securitycontext_test.yaml => pod_security_context_test.yaml} | 2 +- gremlin/tests/{en11853_b1_probes_test.yaml => probes_test.yaml} | 2 +- ...race_period_test.yaml => termination_grace_period_test.yaml} | 2 +- 14 files changed, 12 insertions(+), 12 deletions(-) rename gremlin/tests/{en11853_b7_dnsconfig_test.yaml => dnsconfig_test.yaml} (97%) rename gremlin/tests/{en11853_b5_envfrom_test.yaml => envfrom_test.yaml} (98%) rename gremlin/tests/{en11853_extraenv_exempt_test.yaml => extra_env_exempt_test.yaml} (92%) rename gremlin/tests/{en11853_b3_extra_volumes_reject_test.yaml => extra_volumes_reject_test.yaml} (97%) rename gremlin/tests/{en11853_b3_extra_volumes_test.yaml => extra_volumes_test.yaml} (98%) rename gremlin/tests/fixtures/{en11853_backward_compat.chao.yaml => backward_compat_chao.yaml} (100%) rename gremlin/tests/fixtures/{en11853_backward_compat.gremlin.yaml => backward_compat_daemonset.yaml} (100%) rename gremlin/tests/{en11853_b8_hostaliases_test.yaml => hostaliases_test.yaml} (97%) rename gremlin/tests/{en11853_b2_init_containers_reject_test.yaml => init_containers_reject_test.yaml} (97%) rename gremlin/tests/{en11853_b2_init_containers_test.yaml => init_containers_test.yaml} (97%) rename gremlin/tests/{en11853_b4_lifecycle_test.yaml => lifecycle_test.yaml} (97%) rename gremlin/tests/{en11853_b9_podsecuritycontext_test.yaml => pod_security_context_test.yaml} (97%) rename gremlin/tests/{en11853_b1_probes_test.yaml => probes_test.yaml} (98%) rename gremlin/tests/{en11853_b6_termination_grace_period_test.yaml => termination_grace_period_test.yaml} (97%) diff --git a/gremlin/tests/en11853_b7_dnsconfig_test.yaml b/gremlin/tests/dnsconfig_test.yaml similarity index 97% rename from gremlin/tests/en11853_b7_dnsconfig_test.yaml rename to gremlin/tests/dnsconfig_test.yaml index df86cd1..0eb6765 100644 --- a/gremlin/tests/en11853_b7_dnsconfig_test.yaml +++ b/gremlin/tests/dnsconfig_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B7 - dnsConfig +suite: dnsConfig templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b5_envfrom_test.yaml b/gremlin/tests/envfrom_test.yaml similarity index 98% rename from gremlin/tests/en11853_b5_envfrom_test.yaml rename to gremlin/tests/envfrom_test.yaml index 89bbd55..c15cf4e 100644 --- a/gremlin/tests/en11853_b5_envfrom_test.yaml +++ b/gremlin/tests/envfrom_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B5 - envFrom +suite: envFrom templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_extraenv_exempt_test.yaml b/gremlin/tests/extra_env_exempt_test.yaml similarity index 92% rename from gremlin/tests/en11853_extraenv_exempt_test.yaml rename to gremlin/tests/extra_env_exempt_test.yaml index 2e77045..913b970 100644 --- a/gremlin/tests/en11853_extraenv_exempt_test.yaml +++ b/gremlin/tests/extra_env_exempt_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 invariant - extraEnv is exempt from the reserved-name collision check +suite: extraEnv is exempt from the reserved-name collision check templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b3_extra_volumes_reject_test.yaml b/gremlin/tests/extra_volumes_reject_test.yaml similarity index 97% rename from gremlin/tests/en11853_b3_extra_volumes_reject_test.yaml rename to gremlin/tests/extra_volumes_reject_test.yaml index f380606..4e98225 100644 --- a/gremlin/tests/en11853_b3_extra_volumes_reject_test.yaml +++ b/gremlin/tests/extra_volumes_reject_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B3 rejects - extraVolumes / extraVolumeMounts reserved-name collision +suite: extraVolumes / extraVolumeMounts reserved-name collision templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b3_extra_volumes_test.yaml b/gremlin/tests/extra_volumes_test.yaml similarity index 98% rename from gremlin/tests/en11853_b3_extra_volumes_test.yaml rename to gremlin/tests/extra_volumes_test.yaml index b2ab464..7152dd3 100644 --- a/gremlin/tests/en11853_b3_extra_volumes_test.yaml +++ b/gremlin/tests/extra_volumes_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B3 - extraVolumes / extraVolumeMounts +suite: extraVolumes / extraVolumeMounts templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/fixtures/en11853_backward_compat.chao.yaml b/gremlin/tests/fixtures/backward_compat_chao.yaml similarity index 100% rename from gremlin/tests/fixtures/en11853_backward_compat.chao.yaml rename to gremlin/tests/fixtures/backward_compat_chao.yaml diff --git a/gremlin/tests/fixtures/en11853_backward_compat.gremlin.yaml b/gremlin/tests/fixtures/backward_compat_daemonset.yaml similarity index 100% rename from gremlin/tests/fixtures/en11853_backward_compat.gremlin.yaml rename to gremlin/tests/fixtures/backward_compat_daemonset.yaml diff --git a/gremlin/tests/en11853_b8_hostaliases_test.yaml b/gremlin/tests/hostaliases_test.yaml similarity index 97% rename from gremlin/tests/en11853_b8_hostaliases_test.yaml rename to gremlin/tests/hostaliases_test.yaml index 88fc04a..77ff12e 100644 --- a/gremlin/tests/en11853_b8_hostaliases_test.yaml +++ b/gremlin/tests/hostaliases_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B8 - hostAliases +suite: hostAliases templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b2_init_containers_reject_test.yaml b/gremlin/tests/init_containers_reject_test.yaml similarity index 97% rename from gremlin/tests/en11853_b2_init_containers_reject_test.yaml rename to gremlin/tests/init_containers_reject_test.yaml index 3fbb61f..ecb9739 100644 --- a/gremlin/tests/en11853_b2_init_containers_reject_test.yaml +++ b/gremlin/tests/init_containers_reject_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B2 rejects - initContainers reserved-name collision +suite: initContainers reserved-name collision templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b2_init_containers_test.yaml b/gremlin/tests/init_containers_test.yaml similarity index 97% rename from gremlin/tests/en11853_b2_init_containers_test.yaml rename to gremlin/tests/init_containers_test.yaml index dc81a25..c85d9d2 100644 --- a/gremlin/tests/en11853_b2_init_containers_test.yaml +++ b/gremlin/tests/init_containers_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B2 - user-supplied initContainers +suite: user-supplied initContainers templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b4_lifecycle_test.yaml b/gremlin/tests/lifecycle_test.yaml similarity index 97% rename from gremlin/tests/en11853_b4_lifecycle_test.yaml rename to gremlin/tests/lifecycle_test.yaml index 171e6b0..d5aead5 100644 --- a/gremlin/tests/en11853_b4_lifecycle_test.yaml +++ b/gremlin/tests/lifecycle_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B4 - lifecycle hooks +suite: lifecycle hooks templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b9_podsecuritycontext_test.yaml b/gremlin/tests/pod_security_context_test.yaml similarity index 97% rename from gremlin/tests/en11853_b9_podsecuritycontext_test.yaml rename to gremlin/tests/pod_security_context_test.yaml index f1575d3..ba640e3 100644 --- a/gremlin/tests/en11853_b9_podsecuritycontext_test.yaml +++ b/gremlin/tests/pod_security_context_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B9 - pod-level securityContext +suite: pod-level securityContext templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b1_probes_test.yaml b/gremlin/tests/probes_test.yaml similarity index 98% rename from gremlin/tests/en11853_b1_probes_test.yaml rename to gremlin/tests/probes_test.yaml index 8fc74fe..b475f21 100644 --- a/gremlin/tests/en11853_b1_probes_test.yaml +++ b/gremlin/tests/probes_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B1 - probes (liveness/readiness/startup) +suite: probes (liveness/readiness/startup) templates: - daemonset.yaml - chao-deployment.yaml diff --git a/gremlin/tests/en11853_b6_termination_grace_period_test.yaml b/gremlin/tests/termination_grace_period_test.yaml similarity index 97% rename from gremlin/tests/en11853_b6_termination_grace_period_test.yaml rename to gremlin/tests/termination_grace_period_test.yaml index 255bea8..f4f0e8a 100644 --- a/gremlin/tests/en11853_b6_termination_grace_period_test.yaml +++ b/gremlin/tests/termination_grace_period_test.yaml @@ -1,4 +1,4 @@ -suite: EN-11853 B6 - terminationGracePeriodSeconds +suite: terminationGracePeriodSeconds templates: - daemonset.yaml - chao-deployment.yaml From 95d15f41ab08c31a127546e39bee88e0e87abfd6 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Tue, 22 Sep 2026 11:08:20 -0700 Subject: [PATCH 13/15] docs: record test-naming convention in CLAUDE.md Test files and suite: descriptions name the behavior under test, not the ticket that introduced it -- ticket context belongs in the commit message and PR description instead. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ --- CLAUDE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index d633ea7..cbaa5db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,3 +47,17 @@ reserved list to the collision check and extend this file's list above. `gremlin.gpu.` block (a custom vendor's `volumes`/`volumeMounts` entries) are not covered by the check — a collision there surfaces only as a Kubernetes API rejection at apply time, not a chart-level failure. Whoever adds a vendor block should add its names to the reserved list above. + +## Name test files after the behavior, not the ticket + +`gremlin/tests/*.yaml` files (and their `suite:` descriptions) describe what they test, not which +ticket introduced them — e.g. `extra_volumes_test.yaml` / `suite: extraVolumes / extraVolumeMounts`, +not `en-12345_extra_volumes_test.yaml` / `suite: EN-12345 - extraVolumes`. + +**Why:** a ticket ID tells a future reader nothing about what broke, and outlives its usefulness +the moment the ticket is closed — the test itself is what has to stay legible. + +**How to apply:** when adding tests for a ticket, name the file and suite for the behavior under +test, matching the existing style (`daemonset_resources_test.yaml`, +`chao_deployment_namespaces_test.yaml`, ...). Keep ticket context in the commit message and PR +description instead. From 9f97e52c4ebef499ca9681ee94d27820c3596cee Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Tue, 22 Sep 2026 18:05:08 -0700 Subject: [PATCH 14/15] docs: record convention against internal ticket/design references This repo is public-facing, so CLAUDE.md, README.md, code comments, and values.yaml doc comments should state reasoning in prose rather than point at a Jira ticket or an internal contract's design doc -- the latter is also now a dead link, since that tooling's working files were deliberately removed from this repo's history. Fixes the one existing violation (the extraEnv exemption's rationale). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ --- CLAUDE.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cbaa5db..891ac22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,8 +12,7 @@ This does not apply retroactively to `extraEnv`. It's pre-existing, and `extraEn both containers' `env:` lists, so a collision there is Kubernetes' documented last-wins override behavior — a real, working use case (e.g. overriding `GREMLIN_SERVICE_URL` or `https_proxy`), not a bug. Retrofitting a hard failure onto it would break existing installs on their next -`helm upgrade` with no opt-in. (Considered and rejected during EN-11853 design — see that -contract's `design.md` for the full reasoning.) +`helm upgrade` with no opt-in. This also does not apply to `envFrom`: it *references* an existing ConfigMap/Secret by name rather than defining a new object, and the chart cannot see that object's keys at render time, so there @@ -61,3 +60,20 @@ the moment the ticket is closed — the test itself is what has to stay legible. test, matching the existing style (`daemonset_resources_test.yaml`, `chao_deployment_namespaces_test.yaml`, ...). Keep ticket context in the commit message and PR description instead. + +## Don't reference internal tickets or design docs in repo-committed files + +This repo (and this file) is public-facing. Don't cite a Jira ticket key, an internal contract +slug, or a path like `.claude/contracts//design.md` in `CLAUDE.md`, `README.md`, code +comments, or `values.yaml` doc comments — describe the reasoning in prose instead of pointing at +where it was decided. + +**Why:** a ticket or internal-tooling reference is meaningless to anyone outside the team, and it +rots the moment that tooling's working state is cleaned up or the ticket is closed — a design +rationale should stand on its own, not depend on a link that may not resolve for the reader, or at +all. + +**How to apply:** when explaining *why* a decision was made, restate the reasoning directly (as +this file already does above for the `extraEnv` exemption). Ticket numbers and links to +in-progress design artifacts belong in the commit message and PR description, not in +repo-committed files. From 32fd3d33de359ea5995063ad90f2c1d0c5c46353 Mon Sep 17 00:00:00 2001 From: Danny Seymour Date: Tue, 22 Sep 2026 18:15:01 -0700 Subject: [PATCH 15/15] fix: address round-2 review findings (Copilot + human) - terminationGracePeriodSeconds: replace | int with | toJson on both workloads. int silently coerces a non-numeric typo to 0 (a real bug -- confirmed a typo'd grace period renders as 0, a silent behavior change contrary to the chart's documented pass-through posture); toJson preserves the type (bare number stays a number) while still escaping embedded newlines into a single-line string, closing the YAML-injection path the earlier | int fix was for without reintroducing silent coercion. - Reserved-name lists: split into DaemonSet-specific and Chao-specific lists instead of one shared list. Chao was rejecting extraVolumes/initContainers names it doesn't actually render (gremlin-state, cgroup-root, seccomp-init, gremlin), a real over-blocking bug with no documented rationale. - Reserved-name lists: derive the container-driver socket volume names from containerDrivers.*.name instead of hardcoding docker-sock/containerd-sock/crio-sock, so a renamed driver stays covered by the collision check instead of silently duplicating a chart-managed volume. Also switched both lists to newline-separated entries for cleaner diffs. - Removed the two backward-compat fixture files: unreferenced by any test or workflow (their only consumer was a diff command that lived in the now-removed contract tooling), version-brittle, and functionally redundant with each new test file's own default-case assertions. - CLAUDE.md: documented the corrected per-workload reserved-name split. Added regression coverage: a renamed-driver socket collision, Chao correctly NOT rejecting a DaemonSet-only reserved name, and terminationGracePeriodSeconds preserving a non-numeric value's literal type (and blocking a multiline injection attempt) instead of silently becoming 0. Full suite: 32 suites, 235 tests, clean. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L3TrXY8bWRGNv3aRFZZwGZ --- CLAUDE.md | 50 ++++--- gremlin/templates/_daemonset.tpl | 2 +- gremlin/templates/_validation.tpl | 76 ++++++++--- gremlin/templates/chao-deployment.yaml | 2 +- gremlin/tests/extra_volumes_reject_test.yaml | 27 ++++ .../tests/fixtures/backward_compat_chao.yaml | 64 --------- .../fixtures/backward_compat_daemonset.yaml | 125 ------------------ .../tests/termination_grace_period_test.yaml | 19 +++ 8 files changed, 139 insertions(+), 226 deletions(-) delete mode 100644 gremlin/tests/fixtures/backward_compat_chao.yaml delete mode 100644 gremlin/tests/fixtures/backward_compat_daemonset.yaml diff --git a/CLAUDE.md b/CLAUDE.md index 891ac22..ab7dbf1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,34 +18,44 @@ This also does not apply to `envFrom`: it *references* an existing ConfigMap/Sec than defining a new object, and the chart cannot see that object's keys at render time, so there is nothing to statically collide-check. +**The DaemonSet and Chao Deployment each get their own reserved list, not one shared list.** They +render different chart-managed resources, and checking a workload's `extraVolumes`/`initContainers` +against the *other* workload's names produces false rejections (Chao doesn't render `gremlin-state`, +the DaemonSet doesn't render `chao-tls-identity`, and so on). + Reserved names as of this writing: -- Volumes: `gremlin-state`, `gremlin-executions`, `gremlin-logs`, `cgroup-root`, `seccomp-root`, - `seccomp-profile`, `gremlin-cert`, `ssl-cert-file`, `gremlin-tls-identity`, `chao-tls-identity`, - `gremlin-opencl-icd`, plus the container-driver socket volumes (`docker-sock`, - `containerd-sock`, `crio-sock`) and any GPU vendor volume names (`kfd`, `dri`, - `opencl-vendors`, ...). -- Env vars: `GREMLIN_TEAM_ID`, `GREMLIN_TEAM_SECRET`, `GREMLIN_TEAM_CERTIFICATE_OR_FILE`, - `GREMLIN_TEAM_PRIVATE_KEY_OR_FILE`, `GREMLIN_IDENTIFIER`, `GREMLIN_CLIENT_TAGS`, - `GREMLIN_COLLECT_DNS`, `GREMLIN_SERVICE_URL`, `GREMLIN_PUSH_POD_CIDR_TAGS`, - `GREMLIN_PUSH_ZONE_CIDR_TAGS`, `https_proxy`, `no_proxy`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, - `GREMLIN_TLS_IDENTITY_CERTIFICATE`, `GREMLIN_TLS_IDENTITY_PRIVATE_KEY`, - `GREMLIN_CLUSTER_ID`, and any GPU vendor env vars (e.g. `NVIDIA_VISIBLE_DEVICES`, - `NVIDIA_DRIVER_CAPABILITIES`). -- Container names (for `initContainers`): `seccomp-init`, and each workload's own container name - (`gremlin`, `chao`). +- DaemonSet volumes: `gremlin-state`, `gremlin-executions`, `gremlin-logs`, `cgroup-root`, + `seccomp-root`, `seccomp-profile`, `gremlin-cert`, `ssl-cert-file`, `gremlin-tls-identity`, + `gremlin-opencl-icd`, `kfd`, `dri`, `opencl-vendors`, plus the container-driver socket volumes + (derived from `.Values.containerDrivers.*.name` at render time, not hardcoded — currently + `docker-sock`, `containerd-sock`, `crio-sock`). +- DaemonSet container names (for `initContainers`): `seccomp-init`, `gremlin`. +- Chao volumes: `gremlin-cert`, `ssl-cert-file`, `chao-tls-identity`. +- Chao container names (for `initContainers`): `chao`. +- Env vars (not currently checked — see `envFrom`/`extraEnv` above): `GREMLIN_TEAM_ID`, + `GREMLIN_TEAM_SECRET`, `GREMLIN_TEAM_CERTIFICATE_OR_FILE`, `GREMLIN_TEAM_PRIVATE_KEY_OR_FILE`, + `GREMLIN_IDENTIFIER`, `GREMLIN_CLIENT_TAGS`, `GREMLIN_COLLECT_DNS`, `GREMLIN_SERVICE_URL`, + `GREMLIN_PUSH_POD_CIDR_TAGS`, `GREMLIN_PUSH_ZONE_CIDR_TAGS`, `https_proxy`, `no_proxy`, + `SSL_CERT_FILE`, `SSL_CERT_DIR`, `GREMLIN_TLS_IDENTITY_CERTIFICATE`, + `GREMLIN_TLS_IDENTITY_PRIVATE_KEY`, `GREMLIN_CLUSTER_ID`, and any GPU vendor env vars (e.g. + `NVIDIA_VISIBLE_DEVICES`, `NVIDIA_DRIVER_CAPABILITIES`). **Why:** a silent name collision either gets rejected by the Kubernetes API with an opaque error, or silently shadows/duplicates a chart-managed resource — both are worse than a clear failure at `helm template`/`helm install` time naming the conflicting value path. **How to apply:** whenever a new chart value accepts user-supplied names in this shape, add its -reserved list to the collision check and extend this file's list above. - -**Known gap:** the reserved lists above are static. Names introduced by a hand-configured -`gremlin.gpu.` block (a custom vendor's `volumes`/`volumeMounts` entries) are not covered -by the check — a collision there surfaces only as a Kubernetes API rejection at apply time, not a -chart-level failure. Whoever adds a vendor block should add its names to the reserved list above. +name to the correct workload's reserved list (`gremlin.reservedNames.daemonset*` / +`.chao*` in `_validation.tpl`) and to this file's list above. Prefer deriving a reserved name from +its source value (as the container-driver sockets do) over hardcoding it, when the source is a +small, fixed-shape values block — hardcoding only the GPU vendor gap below because that block's +shape is genuinely open-ended. + +**Known gap:** `gremlin.gpu.` blocks are still static. A hand-configured vendor's +`volumes`/`volumeMounts` entries are not covered by the check — a collision there surfaces only as +a Kubernetes API rejection at apply time, not a chart-level failure. Whoever adds a vendor block +should add its names to the reserved list above. ## Name test files after the behavior, not the ticket diff --git a/gremlin/templates/_daemonset.tpl b/gremlin/templates/_daemonset.tpl index 2341110..531d6f6 100644 --- a/gremlin/templates/_daemonset.tpl +++ b/gremlin/templates/_daemonset.tpl @@ -102,7 +102,7 @@ spec: hostPID: {{ $root.Values.gremlin.hostPID }} hostNetwork: {{ $root.Values.gremlin.hostNetwork }} {{- if not (kindIs "invalid" $root.Values.terminationGracePeriodSeconds) }} - terminationGracePeriodSeconds: {{ $root.Values.terminationGracePeriodSeconds | int }} + terminationGracePeriodSeconds: {{ $root.Values.terminationGracePeriodSeconds | toJson }} {{- end }} {{- with $root.Values.dnsConfig }} dnsConfig: {{ toYaml . | nindent 8 }} diff --git a/gremlin/templates/_validation.tpl b/gremlin/templates/_validation.tpl index ab0e67a..69425db 100644 --- a/gremlin/templates/_validation.tpl +++ b/gremlin/templates/_validation.tpl @@ -86,20 +86,62 @@ stray space would silently become something other than what was written. {{- end -}} {{/* -Reserved volume names the gremlin chart itself manages, space-separated. Callers turn this into a -list with splitList " " (include "gremlin.reservedNames.volumes" .). Keeping this in one named -template means there is exactly one place to edit when a new chart-managed volume name appears. +Reserved volume names the Gremlin DaemonSet itself manages, one per line (flush left -- no leading +whitespace, no comment lines; splitList "\n" turns a leading/trailing blank into an empty entry +that never matches a real name, but keep entries flush left anyway since indentation would +otherwise become part of the name). Includes the container-driver socket volumes, derived from +.Values.containerDrivers.*.name rather than hardcoded, so a renamed driver stays covered. Callers +turn this into a list with splitList "\n" (include "gremlin.reservedNames.daemonsetVolumes" .). +Keeping this in one named template means there is exactly one place to edit when a new +chart-managed volume name appears. */}} -{{- define "gremlin.reservedNames.volumes" -}} -gremlin-state gremlin-executions gremlin-logs cgroup-root seccomp-root seccomp-profile gremlin-cert ssl-cert-file gremlin-tls-identity chao-tls-identity gremlin-opencl-icd docker-sock containerd-sock crio-sock kfd dri opencl-vendors +{{- define "gremlin.reservedNames.daemonsetVolumes" -}} +gremlin-state +gremlin-executions +gremlin-logs +cgroup-root +seccomp-root +seccomp-profile +gremlin-cert +ssl-cert-file +gremlin-tls-identity +gremlin-opencl-icd +kfd +dri +opencl-vendors +{{- range $key, $val := .Values.containerDrivers }} +{{ $val.name }}-sock +{{- end }} {{- end -}} {{/* -Reserved container names the gremlin chart itself manages, space-separated. See -gremlin.reservedNames.volumes for the calling convention. +Reserved container names the Gremlin DaemonSet itself manages, one per line. See +gremlin.reservedNames.daemonsetVolumes for the calling convention and formatting rules. */}} -{{- define "gremlin.reservedNames.containers" -}} -seccomp-init gremlin chao +{{- define "gremlin.reservedNames.daemonsetContainers" -}} +seccomp-init +gremlin +{{- end -}} + +{{/* +Reserved volume names the Chao deployment itself manages, one per line. Deliberately a separate, +narrower list from the DaemonSet's -- Chao renders only gremlin-cert (unconditional), +ssl-cert-file (when ssl.certFile is set), and chao-tls-identity (when chao.tls.identity is +configured); reusing the DaemonSet's list here would reject chao.extraVolumes/extraVolumeMounts +entries that don't actually collide with anything Chao renders. +*/}} +{{- define "gremlin.reservedNames.chaoVolumes" -}} +gremlin-cert +ssl-cert-file +chao-tls-identity +{{- end -}} + +{{/* +Reserved container names the Chao deployment itself manages, one per line. Chao's pod has a single +container (chao) and no chart-managed init container, unlike the DaemonSet's seccomp-init. +*/}} +{{- define "gremlin.reservedNames.chaoContainers" -}} +chao {{- end -}} {{/* @@ -124,9 +166,11 @@ gremlin.validateValues's aggregate message, so it also fires when a single templ (daemonset.yaml) is rendered on its own. */}} {{- define "gremlin.validateReservedNames.daemonset" -}} -{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.initContainers "reserved" (splitList " " (include "gremlin.reservedNames.containers" .)) "valuePath" "initContainers" "kind" "container") -}} -{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.extraVolumes "reserved" (splitList " " (include "gremlin.reservedNames.volumes" .)) "valuePath" "extraVolumes" "kind" "volume") -}} -{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.extraVolumeMounts "reserved" (splitList " " (include "gremlin.reservedNames.volumes" .)) "valuePath" "extraVolumeMounts" "kind" "volume") -}} +{{- $volumes := splitList "\n" (include "gremlin.reservedNames.daemonsetVolumes" .) -}} +{{- $containers := splitList "\n" (include "gremlin.reservedNames.daemonsetContainers" .) -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.initContainers "reserved" $containers "valuePath" "initContainers" "kind" "container") -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.extraVolumes "reserved" $volumes "valuePath" "extraVolumes" "kind" "volume") -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.extraVolumeMounts "reserved" $volumes "valuePath" "extraVolumeMounts" "kind" "volume") -}} {{- end -}} {{/* @@ -134,7 +178,9 @@ Reserved-name collision checks for the Chao deployment's user-supplied initConta extraVolumes, and extraVolumeMounts. See gremlin.validateReservedNames.daemonset. */}} {{- define "gremlin.validateReservedNames.chao" -}} -{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.initContainers "reserved" (splitList " " (include "gremlin.reservedNames.containers" .)) "valuePath" "chao.initContainers" "kind" "container") -}} -{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.extraVolumes "reserved" (splitList " " (include "gremlin.reservedNames.volumes" .)) "valuePath" "chao.extraVolumes" "kind" "volume") -}} -{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.extraVolumeMounts "reserved" (splitList " " (include "gremlin.reservedNames.volumes" .)) "valuePath" "chao.extraVolumeMounts" "kind" "volume") -}} +{{- $volumes := splitList "\n" (include "gremlin.reservedNames.chaoVolumes" .) -}} +{{- $containers := splitList "\n" (include "gremlin.reservedNames.chaoContainers" .) -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.initContainers "reserved" $containers "valuePath" "chao.initContainers" "kind" "container") -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.extraVolumes "reserved" $volumes "valuePath" "chao.extraVolumes" "kind" "volume") -}} +{{- include "gremlin.validateReservedNames.check" (dict "entries" .Values.chao.extraVolumeMounts "reserved" $volumes "valuePath" "chao.extraVolumeMounts" "kind" "volume") -}} {{- end -}} diff --git a/gremlin/templates/chao-deployment.yaml b/gremlin/templates/chao-deployment.yaml index 7f5634a..8323a00 100644 --- a/gremlin/templates/chao-deployment.yaml +++ b/gremlin/templates/chao-deployment.yaml @@ -55,7 +55,7 @@ spec: - name: {{ .Values.chaoimage.pullSecret }} {{- end }} {{- if not (kindIs "invalid" .Values.chao.terminationGracePeriodSeconds) }} - terminationGracePeriodSeconds: {{ .Values.chao.terminationGracePeriodSeconds | int }} + terminationGracePeriodSeconds: {{ .Values.chao.terminationGracePeriodSeconds | toJson }} {{- end }} {{- with .Values.chao.dnsConfig }} dnsConfig: {{ toYaml . | nindent 8 }} diff --git a/gremlin/tests/extra_volumes_reject_test.yaml b/gremlin/tests/extra_volumes_reject_test.yaml index 4e98225..6c8ddbc 100644 --- a/gremlin/tests/extra_volumes_reject_test.yaml +++ b/gremlin/tests/extra_volumes_reject_test.yaml @@ -29,6 +29,23 @@ tests: errorMessage: 'extraVolumeMounts: "cgroup-root" collides with a volume name the gremlin chart manages. Rename it.' template: daemonset.yaml + - it: should fail the gremlin DaemonSet render when extraVolumes collides with a renamed container-driver socket volume + set: + containerDrivers: + docker-linux: + name: custom + gremlin: + container: + driver: docker-linux + extraVolumes: + - name: custom-sock + hostPath: + path: /tmp + asserts: + - failedTemplate: + errorMessage: 'extraVolumes: "custom-sock" collides with a volume name the gremlin chart manages. Rename it.' + template: daemonset.yaml + - it: should not fail the gremlin DaemonSet render when extraVolumes/extraVolumeMounts do not collide set: extraVolumes: @@ -75,6 +92,16 @@ tests: - notFailedTemplate: {} template: chao-deployment.yaml + - it: should not fail the chao Deployment render when extraVolumes uses a name only the gremlin DaemonSet reserves + set: + chao.extraVolumes: + - name: cgroup-root + hostPath: + path: /tmp + asserts: + - notFailedTemplate: {} + template: chao-deployment.yaml + - it: should render nothing for chao when chao.create is false, even with a colliding extraVolumes name set: chao.create: false diff --git a/gremlin/tests/fixtures/backward_compat_chao.yaml b/gremlin/tests/fixtures/backward_compat_chao.yaml deleted file mode 100644 index 26275f9..0000000 --- a/gremlin/tests/fixtures/backward_compat_chao.yaml +++ /dev/null @@ -1,64 +0,0 @@ ---- -# Source: gremlin/templates/chao-deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app.kubernetes.io/instance: chao - app.kubernetes.io/name: chao - helm.sh/chart: gremlin-0.29.0 - app.kubernetes.io/version: "1" - name: chao - namespace: default -spec: - replicas: 1 - strategy: - rollingUpdate: - maxSurge: 25% - maxUnavailable: 25% - type: RollingUpdate - selector: - matchLabels: - app.kubernetes.io/instance: chao - app.kubernetes.io/name: chao - app.kubernetes.io/version: "1" - template: - metadata: - labels: - app.kubernetes.io/instance: chao - app.kubernetes.io/name: chao - helm.sh/chart: gremlin-0.29.0 - app.kubernetes.io/version: "1" - spec: - serviceAccountName: chao - containers: - - image: gremlin/chao:latest - securityContext: - readOnlyRootFilesystem: true - env: - - name: GREMLIN_TEAM_ID - valueFrom: - secretKeyRef: - name: gremlin-secret - key: GREMLIN_TEAM_ID - - name: GREMLIN_CLUSTER_ID - valueFrom: - secretKeyRef: - name: gremlin-secret - key: GREMLIN_CLUSTER_ID - - name: GREMLIN_TEAM_SECRET - valueFrom: - secretKeyRef: - name: gremlin-secret - key: GREMLIN_TEAM_SECRET - args: - - "-api_url" - - "https://api.gremlin.com/v1/kubernetes" - - "-dynamic_query" - imagePullPolicy: Always - name: chao - volumes: - - name: gremlin-cert - secret: - secretName: gremlin-secret - diff --git a/gremlin/tests/fixtures/backward_compat_daemonset.yaml b/gremlin/tests/fixtures/backward_compat_daemonset.yaml deleted file mode 100644 index 2046286..0000000 --- a/gremlin/tests/fixtures/backward_compat_daemonset.yaml +++ /dev/null @@ -1,125 +0,0 @@ ---- -# Source: gremlin/templates/daemonset.yaml -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: my-release-gremlin - namespace: default - labels: - app.kubernetes.io/name: gremlin - helm.sh/chart: gremlin-0.29.0 - app.kubernetes.io/instance: my-release - app.kubernetes.io/managed-by: Helm - version: v1 -spec: - selector: - matchLabels: - app.kubernetes.io/name: gremlin - updateStrategy: - rollingUpdate: - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - labels: - app.kubernetes.io/name: gremlin - helm.sh/chart: gremlin-0.29.0 - app.kubernetes.io/instance: my-release - app.kubernetes.io/managed-by: Helm - version: v1 - spec: - serviceAccountName: gremlin - dnsPolicy: ClusterFirstWithHostNet - hostPID: true - hostNetwork: true - containers: - - name: gremlin - image: gremlin/gremlin:latest - args: [ "daemon" ] - imagePullPolicy: Always - securityContext: - privileged: false - allowPrivilegeEscalation: false - capabilities: - add: - - KILL - - NET_ADMIN - - SYS_BOOT - - SYS_TIME - - DAC_READ_SEARCH - - SYS_RESOURCE - - SYS_ADMIN - - SYS_PTRACE - - NET_RAW - readOnlyRootFilesystem: false - env: - - name: GREMLIN_TEAM_ID - valueFrom: - secretKeyRef: - name: gremlin-secret - key: GREMLIN_TEAM_ID - - name: GREMLIN_TEAM_SECRET - valueFrom: - secretKeyRef: - name: gremlin-secret - key: GREMLIN_TEAM_SECRET - - name: GREMLIN_IDENTIFIER - valueFrom: - fieldRef: - fieldPath: spec.nodeName - - name: GREMLIN_CLIENT_TAGS - value: "" - - name: GREMLIN_COLLECT_DNS - value: "true" - - name: GREMLIN_SERVICE_URL - value: https://api.gremlin.com/v1 - volumeMounts: - - name: gremlin-state - mountPath: /var/lib/gremlin - readOnly: false - - name: gremlin-executions - mountPath: /var/lib/gremlin/executions - readOnly: false - - name: gremlin-logs - mountPath: /var/log/gremlin - readOnly: false - - name: cgroup-root - mountPath: /sys/fs/cgroup - readOnly: false - - - name: containerd-sock - mountPath: /run/containerd/containerd.sock - readOnly: true - - name: crio-sock - mountPath: /run/crio/crio.sock - readOnly: true - - name: docker-sock - mountPath: /var/run/docker.sock - readOnly: true - volumes: - - name: cgroup-root - hostPath: - path: /sys/fs/cgroup - - - name: containerd-sock - hostPath: - path: /run/containerd/containerd.sock - - name: crio-sock - hostPath: - path: /run/crio/crio.sock - - name: docker-sock - hostPath: - path: /var/run/docker.sock - # The Gremlin daemon communicates with Gremlin sidecars via its state directory. - - name: gremlin-state - emptyDir: - medium: Memory - - name: gremlin-executions - hostPath: - path: /var/lib/gremlin/executions - # The Gremlin daemon forwards logs from the Gremlin sidecars to the Gremlin control plane - # These logs should be shared with the host - - name: gremlin-logs - hostPath: - path: /var/log/gremlin - diff --git a/gremlin/tests/termination_grace_period_test.yaml b/gremlin/tests/termination_grace_period_test.yaml index f4f0e8a..7d1d72c 100644 --- a/gremlin/tests/termination_grace_period_test.yaml +++ b/gremlin/tests/termination_grace_period_test.yaml @@ -33,6 +33,25 @@ tests: path: spec.template.spec.terminationGracePeriodSeconds template: daemonset.yaml + - it: should render a non-numeric terminationGracePeriodSeconds on the gremlin DaemonSet as its literal value, not silently coerce it to 0 + set: + terminationGracePeriodSeconds: not-a-number + asserts: + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: not-a-number + template: daemonset.yaml + + - it: should not let a multiline terminationGracePeriodSeconds string inject a sibling pod-spec field on the gremlin DaemonSet + set: + terminationGracePeriodSeconds: |- + 45 + hostIPC: true + asserts: + - notExists: + path: spec.template.spec.hostIPC + template: daemonset.yaml + - it: should render a user-supplied terminationGracePeriodSeconds on the chao Deployment set: chao.terminationGracePeriodSeconds: 45