apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "instance.fullname" . }}-odoo labels: {{- include "instance.labels" . | nindent 4 }} spec: replicas: 1 # ReadWriteOnce filestore volume + Odoo's session locks rule out # rolling deploys with two pods overlapping. Recreate is safe and # only causes brief downtime. strategy: type: Recreate selector: matchLabels: app.kubernetes.io/instance: {{ .Values.instance.code | quote }} odoosky.io/role: odoo template: metadata: labels: {{- include "instance.labels" . | nindent 8 }} odoosky.io/role: odoo annotations: # Bumping this hash whenever the addons list changes forces # Helm/ArgoCD to roll the Deployment, which re-runs the init # containers and re-materializes the shared volume from the # current image set. Without this, changing only `addons:` in # values.yaml would leave the existing pod alone. odoosky.io/addons-hash: {{ .Values.addons | toJson | sha256sum | trunc 16 }} spec: {{- if .Values.addons }} # One initContainer per selected addon. Each addon image's # ENTRYPOINT/CMD copies its bundled content into /target/, # which is the shared volume the Odoo container reads from. # Init containers run in order, but each writes to its own # subdir, so order doesn't matter. initContainers: {{- range $i, $a := .Values.addons }} - name: addon-{{ $a.code | replace "_" "-" | lower }} image: {{ $a.image }}:{{ $a.version }} imagePullPolicy: IfNotPresent volumeMounts: - name: addons mountPath: /target {{- end }} {{- end }} containers: - name: odoo image: "{{ .Values.odoo.image }}:{{ .Values.odoo.tag }}" imagePullPolicy: IfNotPresent {{- if .Values.addons }} # Pass --addons-path explicitly so the official Odoo image's # entrypoint appends our extra-addons dir to the default # addons_path. Args after the entrypoint that start with -- # are passed through to `odoo` directly. We list our path # AFTER the default so built-in modules still take priority # on name collisions. args: - "--addons-path=/usr/lib/python3/dist-packages/odoo/addons,{{ .Values.addonsMountPath }}" {{- end }} ports: - name: http containerPort: 8069 env: - name: HOST value: {{ include "instance.fullname" . }}-pg - name: PORT value: "5432" - name: USER valueFrom: secretKeyRef: name: {{ include "instance.fullname" . }}-pg key: POSTGRES_USER - name: PASSWORD valueFrom: secretKeyRef: name: {{ include "instance.fullname" . }}-pg key: POSTGRES_PASSWORD volumeMounts: - name: filestore mountPath: /var/lib/odoo {{- if .Values.addons }} - name: addons mountPath: {{ .Values.addonsMountPath }} readOnly: true {{- end }} resources: {{- include "instance.resources" (dict "Values" .Values "role" "odoo") | nindent 12 }} # /web/login is the most stable health endpoint across Odoo # 16/17/18/19 — /web/health is 17+. Use login HTTP 200 as # readiness signal. readinessProbe: httpGet: path: /web/login port: 8069 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 livenessProbe: tcpSocket: port: 8069 initialDelaySeconds: 60 periodSeconds: 30 volumes: - name: filestore persistentVolumeClaim: claimName: {{ include "instance.fullname" . }}-odoo {{- if .Values.addons }} # Shared scratch volume that init containers populate with # addon content. emptyDir is fine — the source of truth is # the registry's images; if the volume is wiped (pod # recreate) the init containers re-materialize from the # cached images. - name: addons emptyDir: {} {{- end }}