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
18 changes: 18 additions & 0 deletions charts/shopsys-app/templates/pdb-storefront.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{{/* Rendered only when the storefront is guaranteed to run with 2+ replicas — a PDB with
minAvailable: 1 on a single-replica Deployment would block node drains forever.
Autoscaling counts only with minReplicas > 1 (the HPA may scale down to minReplicas). */}}
{{- $s := .Values.storefront }}
{{- if and ($s.pdb).enabled (or (and $s.autoscaling.enabled (gt (int $s.autoscaling.minReplicas) 1)) (gt (int $s.replicas) 1)) }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: storefront
labels:
{{- include "shopsys.labels" $ | nindent 4 }}
app: storefront
spec:
minAvailable: {{ $s.pdb.minAvailable | default 1 }}
selector:
matchLabels:
app: storefront
{{- end }}
18 changes: 18 additions & 0 deletions charts/shopsys-app/templates/pdb-webserver.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{{/* Rendered only when the webserver is guaranteed to run with 2+ replicas — a PDB with
minAvailable: 1 on a single-replica Deployment would block node drains forever.
Autoscaling counts only with minReplicas > 1 (the HPA may scale down to minReplicas). */}}
{{- $w := .Values.webserver }}
{{- if and ($w.pdb).enabled (or (and $w.autoscaling.enabled (gt (int $w.autoscaling.minReplicas) 1)) (gt (int $w.replicas) 1)) }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: webserver-php-fpm
labels:
{{- include "shopsys.labels" $ | nindent 4 }}
app: webserver-php-fpm
spec:
minAvailable: {{ $w.pdb.minAvailable | default 1 }}
selector:
matchLabels:
app: webserver-php-fpm
{{- end }}
19 changes: 19 additions & 0 deletions charts/shopsys-app/tests/component_schema_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ tests:
path: spec.template.spec.containers[0].env[-1].name
value: FROM_FIELD

- it: propagates topologySpreadConstraints into the pod spec
template: templates/deployment-storefront.yaml
set:
storefront:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: storefront
asserts:
- equal:
path: spec.template.spec.topologySpreadConstraints[0].topologyKey
value: topology.kubernetes.io/zone
- equal:
path: spec.template.spec.topologySpreadConstraints[0].maxSkew
value: 1

- it: allows probe overrides
template: templates/deployment-storefront.yaml
set:
Expand Down
86 changes: 86 additions & 0 deletions charts/shopsys-app/tests/pdb_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
suite: pod disruption budgets
values:
- ./values/required.yaml
templates:
- templates/pdb-webserver.yaml
- templates/pdb-storefront.yaml
tests:
- it: renders no PDB for single-replica components without autoscaling
asserts:
- hasDocuments:
count: 0

- it: renders the PDB when autoscaling is enabled
template: templates/pdb-webserver.yaml
set:
webserver:
autoscaling:
enabled: true
asserts:
- hasDocuments:
count: 1
- equal:
path: spec.minAvailable
value: 1
- equal:
path: spec.selector.matchLabels.app
value: webserver-php-fpm

- it: renders no PDB when autoscaling can scale down to a single replica
template: templates/pdb-webserver.yaml
set:
webserver:
autoscaling:
enabled: true
minReplicas: 1
asserts:
- hasDocuments:
count: 0

- it: renders no PDB when the pdb map is nulled by an overlay
template: templates/pdb-storefront.yaml
set:
storefront:
autoscaling:
enabled: true
pdb: null
asserts:
- hasDocuments:
count: 0

- it: renders the PDB when running multiple fixed replicas
template: templates/pdb-storefront.yaml
set:
storefront:
replicas: 3
asserts:
- hasDocuments:
count: 1
- equal:
path: spec.selector.matchLabels.app
value: storefront

- it: honors a minAvailable override
template: templates/pdb-webserver.yaml
set:
webserver:
autoscaling:
enabled: true
pdb:
minAvailable: 2
asserts:
- equal:
path: spec.minAvailable
value: 2

- it: can be disabled explicitly
template: templates/pdb-webserver.yaml
set:
webserver:
autoscaling:
enabled: true
pdb:
enabled: false
asserts:
- hasDocuments:
count: 0
28 changes: 27 additions & 1 deletion charts/shopsys-app/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@
}
}
},
"webserver": {
"type": "object",
"properties": {
"pdb": { "$ref": "#/$defs/pdb" },
"topologySpreadConstraints": { "type": "array" }
}
},
"storefront": {
"type": "object",
"properties": {
Expand All @@ -110,13 +117,16 @@
"secretEnv": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"pdb": { "$ref": "#/$defs/pdb" },
"topologySpreadConstraints": { "type": "array" }
}
},
"cron": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"topologySpreadConstraints": { "type": "array" },
"instances": {
"type": "array",
"items": {
Expand All @@ -134,6 +144,12 @@
"consumers": {
"type": "object",
"properties": {
"defaults": {
"type": "object",
"properties": {
"topologySpreadConstraints": { "type": "array" }
}
},
"instances": {
"type": "array",
"items": {
Expand Down Expand Up @@ -162,5 +178,15 @@
}
},
"extraManifests": { "type": "array" }
},
"$defs": {
"pdb": {
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"minAvailable": { "type": ["integer", "string"] }
}
}
}
}
18 changes: 17 additions & 1 deletion charts/shopsys-app/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
#
# Every workload component (webserver, storefront, cron, consumers) exposes the same
# standard keys: image {repository, tag, pullPolicy}, replicas, resources, podAnnotations,
# podLabels, nodeSelector, tolerations, affinity, extraEnv, extraVolumes, extraVolumeMounts,
# podLabels, nodeSelector, tolerations, affinity, topologySpreadConstraints, extraEnv,
# extraVolumes, extraVolumeMounts,
# livenessProbe/readinessProbe, securityContext/podSecurityContext, lifecycle,
# terminationGracePeriodSeconds, priorityClassName. Webserver and storefront additionally
# expose independent `autoscaling` sections.
Expand Down Expand Up @@ -96,6 +97,13 @@ webserver:
minReplicas: 2
maxReplicas: 3
targetCPUUtilization: 120
# PodDisruptionBudget protecting against voluntary disruptions (node drains). Rendered
# only when the component is guaranteed to run 2+ replicas (autoscaling enabled with
# minReplicas > 1, or fixed replicas > 1) - a minAvailable: 1 budget on a single
# replica would block drains forever.
pdb:
enabled: true
minAvailable: 1
progressDeadlineSeconds: 1500
terminationGracePeriodSeconds: 120
strategy:
Expand Down Expand Up @@ -189,6 +197,7 @@ webserver:
operator: In
values:
- redis
topologySpreadConstraints: []
podSecurityContext: {}
extraEnv: []
extraVolumes: []
Expand All @@ -208,6 +217,10 @@ storefront:
minReplicas: 2
maxReplicas: 3
targetCPUUtilization: 120
# See webserver.pdb - same gating (autoscaling with minReplicas > 1, or replicas > 1).
pdb:
enabled: true
minAvailable: 1
terminationGracePeriodSeconds: 60
resources:
limits:
Expand Down Expand Up @@ -251,6 +264,7 @@ storefront:
operator: In
values:
- storefront
topologySpreadConstraints: []
podSecurityContext: {}
securityContext: {}
extraEnv: []
Expand Down Expand Up @@ -302,6 +316,7 @@ cron:
operator: In
values:
- background
topologySpreadConstraints: []
podSecurityContext: {}
extraEnv: []
extraVolumes: []
Expand Down Expand Up @@ -341,6 +356,7 @@ consumers:
operator: In
values:
- background
topologySpreadConstraints: []
podSecurityContext: {}
securityContext: {}
extraEnv: []
Expand Down
4 changes: 4 additions & 0 deletions charts/shopsys-common/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ tolerations:
affinity:
{{ toYaml . | indent 2 }}
{{- end }}
{{- with .component.topologySpreadConstraints }}
topologySpreadConstraints:
{{ toYaml . | indent 2 }}
{{- end }}
{{- with .component.podSecurityContext }}
securityContext:
{{ toYaml . | indent 2 }}
Expand Down
4 changes: 3 additions & 1 deletion charts/shopsys-infra/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
"properties": {
"enabled": { "type": "boolean" },
"config": { "type": "string" },
"replicas": { "type": "integer", "minimum": 0 }
"replicas": { "type": "integer", "minimum": 0 },
"topologySpreadConstraints": { "type": "array" }
}
},
"rabbitmq": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"replicas": { "type": "integer", "minimum": 0 },
"topologySpreadConstraints": { "type": "array" },
"auth": {
"type": "object",
"properties": {
Expand Down
2 changes: 2 additions & 0 deletions charts/shopsys-infra/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ redis:
nodeSelector: {}
tolerations: []
affinity: {}
topologySpreadConstraints: []
podSecurityContext: {}
securityContext: {}
extraEnv: []
Expand Down Expand Up @@ -126,6 +127,7 @@ rabbitmq:
operator: In
values:
- rabbitmq
topologySpreadConstraints: []
podSecurityContext: {}
securityContext: {}
extraEnv: []
Expand Down
8 changes: 8 additions & 0 deletions docs/migrating-from-shopsys-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,11 @@ Intentional differences of the phase-1 rewrite; everything else is a 1:1 port.
overcommit, and anything bursting past `1Gi` is now OOMKilled instead of merely
evictable — check your crons'/consumers' peak memory usage before relying on the
defaults, and set `resources: null` on a component to restore the legacy behavior.
22. **PodDisruptionBudgets for webserver and storefront**: the legacy package had none — a
node drain could evict all replicas at once. The chart now renders a
`minAvailable: 1` PDB per component whenever it is guaranteed to run 2+ replicas
(autoscaling enabled with `minReplicas > 1`, or fixed `replicas > 1`); setups that can
run a single replica get no PDB (it would block drains). Opt out
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).
15 changes: 12 additions & 3 deletions docs/values.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,18 @@ Every workload component (`webserver`, `storefront`, `cron`, `consumers.defaults
| `autoscaling: {enabled, minReplicas, maxReplicas, targetCPUUtilization}` | per-component HPA (webserver + storefront) |
| `resources` | container resources |
| `podAnnotations` / `podLabels` | extra pod metadata |
| `nodeSelector` / `tolerations` / `affinity` / `priorityClassName` | scheduling |
| `nodeSelector` / `tolerations` / `affinity` / `topologySpreadConstraints` / `priorityClassName` | scheduling |

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.

Since topologySpreadConstraints is a raw passthrough, consider a one-line usage hint here: prefer whenUnsatisfiable: ScheduleAnyway unless spread is a hard requirement (DoNotSchedule can leave pods Pending on small/dev clusters), and add matchLabelKeys: [pod-template-hash] (beta, enabled by default since K8s 1.27; the chart is validated against 1.31) so the spread does not count old-ReplicaSet pods during rolling updates. Purely a docs suggestion — the passthrough design itself is right and consistent with affinity. — 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.

Added in c2aeb51: a short note under the standard-keys table in docs/values.md recommending whenUnsatisfiable: ScheduleAnyway unless spread is a hard requirement, and matchLabelKeys: [pod-template-hash] so the spread ignores old-ReplicaSet pods during rolling updates. Kept it to one paragraph since the key is a raw passthrough by design.

| `extraEnv` | extra env entries (raw list, supports `valueFrom`) |
| `extraVolumes` / `extraVolumeMounts` | additional volumes |
| `livenessProbe` / `readinessProbe` | probe overrides |
| `securityContext` / `podSecurityContext` | security contexts |
| `terminationGracePeriodSeconds`, `lifecycle` | shutdown behavior |

`topologySpreadConstraints` is a raw passthrough — supply complete constraints. Prefer
`whenUnsatisfiable: ScheduleAnyway` unless spread is a hard requirement (`DoNotSchedule`
can leave pods Pending on small clusters), and add `matchLabelKeys: [pod-template-hash]`
so the spread ignores old-ReplicaSet pods during rolling updates.

## Top-level structure

```yaml
Expand Down Expand Up @@ -74,8 +79,12 @@ app: # shared backend configuration
adminUrl: admin
s3Endpoint: ""

webserver: # component (see standard keys) + phpFpm/nginx sub-containers
storefront: # component + its own `env` / `secretEnv` (storefront-secret-env Secret)
webserver: # component (see standard keys) + phpFpm/nginx sub-containers;
# `pdb: {enabled: true, minAvailable: 1}` - rendered only with
# guaranteed 2+ replicas (autoscaling with minReplicas > 1,
# or replicas > 1)
storefront: # component + its own `env` / `secretEnv` (storefront-secret-env Secret);
# `pdb` - same as webserver
cron: # component + `instances: [{name, schedule}]`;
# default resources: requests 100m/300Mi, limits 1Gi memory
consumers: # `defaults` + `instances: [{name, transports, replicas, ...}]`;
Expand Down
Loading