Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ spec:
backoffLimit: 0
template:
spec:
# Runs under the namespace default SA (as a pre-install hook it cannot reference the
# chart ServiceAccount, which does not exist yet); it never talks to the API, so the
# token is not mounted.
automountServiceAccountToken: false
volumes:
- name: domains-urls
configMap:
Expand Down
3 changes: 3 additions & 0 deletions charts/shopsys-app/templates/hooks/job-post-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ spec:
backoffLimit: 0
template:
spec:
# Runs under the namespace default SA (kept consistent with the migration hook);
# it never talks to the API, so the token is not mounted.
automountServiceAccountToken: false
volumes:
- name: domains-urls
configMap:
Expand Down
9 changes: 9 additions & 0 deletions charts/shopsys-app/templates/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "shopsys.serviceAccountName" . }}
labels:
{{- include "shopsys.labels" $ | nindent 4 }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountToken }}
{{- end }}
90 changes: 90 additions & 0 deletions charts/shopsys-app/tests/serviceaccount_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
suite: dedicated service account
values:
- ./values/required.yaml
templates:
- templates/serviceaccount.yaml
- templates/deployment-webserver-php-fpm.yaml
- templates/hooks/job-migrate-application.yaml
tests:
- it: creates the chart ServiceAccount without token automount
template: templates/serviceaccount.yaml
asserts:
- equal:
path: metadata.name
value: shopsys-app
- equal:
path: automountServiceAccountToken
value: false

- it: assigns the ServiceAccount to workload pods
template: templates/deployment-webserver-php-fpm.yaml
asserts:
- equal:
path: spec.template.spec.serviceAccountName
value: shopsys-app
- equal:
path: spec.template.spec.automountServiceAccountToken
value: false

- it: honors a custom name
template: templates/serviceaccount.yaml
set:
serviceAccount:
name: my-account
asserts:
- equal:
path: metadata.name
value: my-account

- it: references an externally managed account when create is false
template: templates/deployment-webserver-php-fpm.yaml
set:
serviceAccount:
create: false
name: external-account
asserts:
- equal:
path: spec.template.spec.serviceAccountName
value: external-account

- it: renders no ServiceAccount when create is false
template: templates/serviceaccount.yaml
set:
serviceAccount:
create: false
asserts:
- hasDocuments:
count: 0

- it: falls back to the default SA when create is false and no name is given
template: templates/deployment-webserver-php-fpm.yaml
set:
serviceAccount:
create: false
asserts:
- equal:
path: spec.template.spec.serviceAccountName
value: default
# pod-level automount still guarantees no token, whatever the SA itself mounts
- equal:
path: spec.template.spec.automountServiceAccountToken
value: false

- it: mounts the token at pod level when automountToken is enabled
template: templates/deployment-webserver-php-fpm.yaml
set:
serviceAccount:
automountToken: true
asserts:
- equal:
path: spec.template.spec.automountServiceAccountToken
value: true

- it: does not mount the default SA token into the migration hook
template: templates/hooks/job-migrate-application.yaml
asserts:
- equal:
path: spec.template.spec.automountServiceAccountToken
value: false
- notExists:
path: spec.template.spec.serviceAccountName
9 changes: 9 additions & 0 deletions charts/shopsys-app/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@
}
}
},
"serviceAccount": {
"type": "object",
"additionalProperties": false,
"properties": {
"create": { "type": "boolean" },
"name": { "type": "string" },
"automountToken": { "type": "boolean" }
}
},
"registry": {
"type": "object",
"properties": {
Expand Down
13 changes: 13 additions & 0 deletions charts/shopsys-app/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ registry:
imagePullSecrets:
- dockerregistry

# ServiceAccount the workload pods run under (the hook Jobs keep the deploy-hooks SA).
# None of the workloads talk to the Kubernetes API, so the token is not mounted:
# automountToken is applied both on the chart-created ServiceAccount and at pod level on
# every workload pod, so it holds even with create=false (external SA or the namespace
# `default` fallback when `name` is left empty) - pod-level overrides the SA's own setting.
# NOTE: the helmfile passes these values to both charts - leave `name` empty (each chart
# defaults to its own chart name) or set create=false with an externally managed account;
# an explicit name with create=true would make both releases fight over one ServiceAccount.
serviceAccount:
create: true
name: "" # generated from the chart name when empty
automountToken: false

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info: worth a note here (or in docs/values.md) that automountToken only takes effect when create: true — with create: false it is silently ignored and the external SA's own automount setting wins. Also consider adding the serviceAccount block to values.schema.json in a follow-up (create: boolean, name: string, automountToken: boolean) so typos fail at render time. — Claude Agent (CR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "only takes effect when create=true" caveat no longer exists after ccedfde: automountToken now also drives the pod-level automountServiceAccountToken on every workload pod, so it is effective with create: false too — documented in both values.yaml comments, docs/values.md, and deviation 23. The values.schema.json addition is deferred to a follow-up so it can cover the pdb/topologySpreadConstraints blocks from #48 in the same change and keep the stack consistent — flagged to the maintainer.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: per the maintainer's decision the schema coverage now ships inside the PRs themselves instead of a separate follow-up — serviceAccount was added to both charts' values.schema.json in this PR (34f3da6, additionalProperties: false so typos like automountServiceAccountToken fail the render), and pdb + topologySpreadConstraints were added in #48 (f93671c).


app:
# Backend environment variables (webserver, cron, consumers, migration job, cron shell).
# Values MUST be strings - quote values like "479411e7" in YAML.
Expand Down
13 changes: 13 additions & 0 deletions charts/shopsys-common/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,23 @@ checksum/php-fpm: {{ printf "%s%s" ($root.Values.webserver.phpFpm.config | defau
{{- end }}
{{- end }}

{{/* Name of the chart-scoped ServiceAccount workload pods run under.
Defaults to the chart name; with create=false an explicitly named (externally
managed) ServiceAccount is referenced, falling back to "default". */}}
{{- define "shopsys.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- .Values.serviceAccount.name | default .Chart.Name -}}
{{- else -}}
{{- .Values.serviceAccount.name | default "default" -}}
Comment on lines +257 to +258

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: with create: false and an empty name, this falls back to the namespace default SA — which on most clusters automounts its API token, silently reintroducing the exposure this PR removes. The values comment says to pair create: false with an externally managed account, but nothing enforces or warns about the forgotten-name case. Consider either a fail ("serviceAccount.name is required when create=false") or at least calling the fallback's token consequence out in docs/values.md. — Claude Agent (CR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the silent fallback was a real gap, but a fail would also break the legitimate "run under the namespace default SA" configuration, so I removed the token consequence instead: as of ccedfde podSettings emits pod-level automountServiceAccountToken: {{ .Values.serviceAccount.automountToken }} (per your other suggestion), so the default-SA fallback no longer mounts a token regardless of the SA's own automount setting. The fallback and its semantics are now called out explicitly in docs/values.md and the values comments.

{{- end -}}
{{- end }}

{{/* Standard scheduling/security pod fields shared by every component.
ctx: (dict "root" $ "component" <component values>)
Rendered at zero indent — use `| nindent N` at the call site. */}}
{{- define "shopsys.podSettings" -}}
serviceAccountName: {{ include "shopsys.serviceAccountName" .root }}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (Low): the no-token guarantee currently lives only on the chart-created SA object. Emitting the pod-level field here as well, e.g.

serviceAccountName: {{ include "shopsys.serviceAccountName" .root }}
automountServiceAccountToken: {{ .root.Values.serviceAccount.automountToken }}

would make it hold regardless of SA-level configuration (pod-level overrides SA-level), covering both the create: false external-SA path and the default-SA fallback. Since the value drives it, a user who genuinely needs a token can still set automountToken: true. Fine as a follow-up — would touch all golden snapshots again. — Claude Agent (CR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in ccedfde exactly as suggested: shopsys.podSettings now emits automountServiceAccountToken: {{ .root.Values.serviceAccount.automountToken }} right after serviceAccountName, making the no-token guarantee hold for the create: false external-SA path and the default-SA fallback alike (pod-level overrides SA-level). Hook Jobs are unaffected — they do not use podSettings (cron-suspend keeps its mounted deploy-hooks token). Golden snapshots regenerated in the same commit; unit tests extended (pod-level false by default, still false with create: false, true when automountToken: true). Full suite green: 78/78 unittest, 15/15 golden, lint clean.

automountServiceAccountToken: {{ .root.Values.serviceAccount.automountToken }}
{{- with .component.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 2 }}
Expand Down
9 changes: 9 additions & 0 deletions charts/shopsys-infra/templates/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "shopsys.serviceAccountName" . }}
labels:
{{- include "shopsys.labels" $ | nindent 4 }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountToken }}
{{- end }}
21 changes: 21 additions & 0 deletions charts/shopsys-infra/tests/infra_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ templates:
- templates/ingress-rabbitmq.yaml
- templates/rbac-deploy-hooks.yaml
- templates/secret-rabbitmq.yaml
- templates/serviceaccount.yaml
tests:
- it: runs redis as the first container with the exporter sidecar and health probes
template: templates/deployment-redis.yaml
Expand Down Expand Up @@ -92,3 +93,23 @@ tests:
- equal:
path: stringData.password
value: secret

- it: runs infra pods under the chart ServiceAccount without token automount
template: templates/serviceaccount.yaml
asserts:
- equal:
path: metadata.name
value: shopsys-infra
- equal:
path: automountServiceAccountToken
value: false

- it: assigns the ServiceAccount to the rabbitmq pod
template: templates/statefulset-rabbitmq.yaml
asserts:
- equal:
path: spec.template.spec.serviceAccountName
value: shopsys-infra
- equal:
path: spec.template.spec.automountServiceAccountToken
value: false
9 changes: 9 additions & 0 deletions charts/shopsys-infra/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
"environment": { "type": "string" }
}
},
"serviceAccount": {
"type": "object",
"additionalProperties": false,
"properties": {
"create": { "type": "boolean" },
"name": { "type": "string" },
"automountToken": { "type": "boolean" }
}
},
"redis": {
"type": "object",
"properties": {
Expand Down
8 changes: 8 additions & 0 deletions charts/shopsys-infra/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ registry:
# when set, pods reference this name instead of "dockerregistry".
existingSecret: ""

# ServiceAccount the redis/rabbitmq pods run under; see the app chart for the shared-values
# caveat (leave `name` empty - it defaults to the chart name per chart) and for the
# automountToken semantics (applied on the SA and at pod level on every workload pod).
serviceAccount:
create: true
name: ""
automountToken: false

redis:
enabled: true
image:
Expand Down
10 changes: 10 additions & 0 deletions docs/migrating-from-shopsys-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,13 @@ Intentional differences of the phase-1 rewrite; everything else is a 1:1 port.
via `webserver.pdb.enabled` / `storefront.pdb.enabled`. `topologySpreadConstraints`
is also available as a standard component key (empty by default — the legacy
anti-affinity defaults are kept untouched).
23. **Dedicated ServiceAccounts**: workload pods previously ran under the namespace
`default` ServiceAccount with its API token mounted. Each chart now creates its own
ServiceAccount (`shopsys-app` / `shopsys-infra`) with
`automountServiceAccountToken: false` — none of the workloads talk to the Kubernetes
API. The same setting is also emitted at pod level on every workload pod, so the
no-token guarantee holds even when `serviceAccount.create=false` points at an external
account (or falls back to the namespace `default` SA). The hook Jobs are unchanged: `cron-suspend` keeps the `deploy-hooks` SA (needs the
API), and the migration/post-deploy Jobs stay on the default SA (a `pre-install` hook
cannot reference the chart SA, which does not exist yet) but no longer mount its token.
Configure via `serviceAccount: {create, name, automountToken}`.
8 changes: 8 additions & 0 deletions docs/values.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ security:
registry: # image pull secret; credentials sensitive → env vars
existingSecret: "" # OR reference an externally managed pull secret

serviceAccount: # per-chart SA the workload pods run under (no API token mounted)
create: true
name: "" # empty = chart name per chart; leave empty (shared values -
# an explicit name would collide between the two releases);
# with create=false an empty name falls back to `default`
automountToken: false # applied on the chart SA AND at pod level on every workload
# pod - effective even with create=false (pod-level wins)

app: # shared backend configuration
env: {} # non-sensitive backend env vars (webserver, cron, consumers, migration)
secretEnv: {} # sensitive backend env vars → app-secret-env Secret + envFrom;
Expand Down
Loading