provisioning-catalog/components/odoo/cluster/templates/deployment.yaml.j2

403 lines
17 KiB
Text
Raw Normal View History

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ taskserv.name }}
namespace: {{ taskserv.namespace }}
labels:
app.kubernetes.io/name: {{ taskserv.name }}
app.kubernetes.io/instance: {{ taskserv.tenant }}
app.kubernetes.io/managed-by: provisioning
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: {{ taskserv.name }}
app.kubernetes.io/instance: {{ taskserv.tenant }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ taskserv.name }}
app.kubernetes.io/instance: {{ taskserv.tenant }}
app.kubernetes.io/managed-by: provisioning
spec:
{% if taskserv.placement is defined and taskserv.placement.node_class %}
affinity:
nodeAffinity:
{% if taskserv.placement.mode == "require" %}
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-class.{{ taskserv.placement.node_class.0 }}
operator: In
values: ["true"]
{% else %}
preferredDuringSchedulingIgnoredDuringExecution:
{% for c in taskserv.placement.node_class %}
- weight: {{ 100 - loop.index0 * 10 }}
preference:
matchExpressions:
- key: node-class.{{ c }}
operator: In
values: ["true"]
{% endfor %}
{% endif %}
{% endif %}
securityContext:
fsGroup: 101
initContainers:
- name: wait-for-db
image: postgres:18-bookworm
command: ["/bin/bash", "-c"]
args:
- |
for i in $(seq 1 30); do
pg_isready -h {{ taskserv.db_host }} -p {{ taskserv.db_port }} -U {{ taskserv.db_user }} && exit 0
echo "waiting for postgresql (attempt $i/30)..."
sleep 2
done
echo "ERROR: postgresql not ready after 30 attempts" >&2
exit 1
- name: clone-oca-addons
image: alpine/git:latest
command: ["/bin/sh", "-c"]
args:
- |
set -e
CLONEDIR=$(mktemp -d)
: > /mnt/extra-addons/.oca-lock
{% for a in odoo_oca_addons_resolved %}
echo "Fetching OCA/{{ a.repo }} @ {{ a.ref }}"
# fetch by ref works for branch, tag, or commit SHA (GitHub allows
# reachable-SHA fetch), so a pinned commit and a rolling branch
# share one code path.
if git init -q "$CLONEDIR/{{ a.repo }}" \
&& git -C "$CLONEDIR/{{ a.repo }}" remote add origin https://github.com/OCA/{{ a.repo }}.git \
&& git -C "$CLONEDIR/{{ a.repo }}" fetch -q --depth 1 origin "{{ a.ref }}" \
&& git -C "$CLONEDIR/{{ a.repo }}" checkout -q --detach FETCH_HEAD; then
sha=$(git -C "$CLONEDIR/{{ a.repo }}" rev-parse HEAD)
echo "{{ a.repo }} {{ a.ref }} $sha" >> /mnt/extra-addons/.oca-lock
find "$CLONEDIR/{{ a.repo }}" -maxdepth 1 -mindepth 1 -type d \
-exec sh -c '[ -f "$1/__manifest__.py" ] && mv "$1" /mnt/extra-addons/ || true' _ {} \;
rm -rf "$CLONEDIR/{{ a.repo }}"
else
echo "WARN: OCA/{{ a.repo }} fetch failed (ref {{ a.ref }}) — skipping"
fi
{% endfor %}
# fingerprint over the resolved SHAs; changes only when code changes
sort /mnt/extra-addons/.oca-lock | sha256sum | cut -d' ' -f1 \
> /mnt/extra-addons/.oca-fingerprint
echo "OCA fingerprint: $(cat /mnt/extra-addons/.oca-fingerprint)"
chmod -R 755 /mnt/extra-addons 2>/dev/null || true
volumeMounts:
- name: extra-addons
mountPath: /mnt/extra-addons
- name: install-python-deps
image: {{ taskserv.image }}
command: ["/bin/bash", "-c"]
args:
- |
set -e
TARGET=/mnt/extra-addons/python-packages
mkdir -p $TARGET
pkgs=$(python3 - <<'PYEOF'
import os, ast
seen = set()
for entry in os.scandir("/mnt/extra-addons"):
if not entry.is_dir():
continue
manifest = os.path.join(entry.path, "__manifest__.py")
if not os.path.isfile(manifest):
continue
try:
with open(manifest) as f:
data = ast.literal_eval(f.read())
for pkg in data.get("external_dependencies", {}).get("python", []):
seen.add(pkg)
except Exception:
pass
print(" ".join(sorted(seen)))
PYEOF
)
reqs=$(find /mnt/extra-addons -name "requirements.txt" -exec cat {} \; \
| grep -v '^\s*#' | sort -u | tr '\n' ' ')
# `packaging` is needed by Odoo to parse external-dependency
# version specifiers (e.g. schwifty==2024.4.0) during `-u`; the base
# image only vendors it inside pip, not as an importable top-level.
all="$pkgs $reqs packaging{% if odoo_python_packages %} {{ odoo_python_packages }}{% endif %}"
all=$(echo "$all" | tr -s ' ' | sed 's/^ //;s/ $//')
# Pin the system crypto stack to the image's versions. The main
# container puts $TARGET on PYTHONPATH, so an addon-pulled newer
# cryptography (via xmlsig) would shadow and ABI-break the system
# pyOpenSSL ("module 'lib' has no attribute 'GEN_EMAIL'"). Pinning
# to the installed versions keeps the shadow copy ABI-compatible.
CONSTRAINTS=$(mktemp)
python3 - > "$CONSTRAINTS" <<'PYEOF'
import importlib.metadata as m
for pkg in ("cryptography", "cffi", "pyOpenSSL"):
try:
print(f"{pkg}=={m.version(pkg)}")
except Exception:
pass
PYEOF
echo "[deps] system crypto constraints:"; cat "$CONSTRAINTS"
if [ -n "$all" ]; then
pip install --target $TARGET -c "$CONSTRAINTS" $all 2>/dev/null || true
fi
chmod -R 755 $TARGET
securityContext:
runAsUser: 0
volumeMounts:
- name: extra-addons
mountPath: /mnt/extra-addons
- name: init-odoo-db
image: {{ taskserv.image }}
command: ["/bin/bash", "-c"]
args:
- |
set -e
initialized=$(PGPASSWORD="$ODOO_DB_PASSWORD" python3 -c "
import psycopg2, os, sys
try:
conn = psycopg2.connect(
host='{{ taskserv.db_host }}', port={{ taskserv.db_port }},
user='{{ taskserv.db_user }}', password=os.environ['ODOO_DB_PASSWORD'],
dbname='{{ taskserv.db_name }}'
)
cur = conn.cursor()
cur.execute(\"SELECT 1 FROM pg_tables WHERE tablename='ir_module_module'\")
print('yes' if cur.fetchone() else 'no')
except Exception as e:
print('no', file=sys.stderr)
sys.exit(1)
" 2>/dev/null)
if [ "$initialized" = "yes" ]; then
echo "[init-odoo-db] schema present — skipping initialization"
exit 0
fi
echo "[init-odoo-db] empty database — initializing Odoo base (this takes a few minutes)..."
export PGPASSWORD="$ODOO_DB_PASSWORD"
exec odoo \
-d "{{ taskserv.db_name }}" \
--db_host="{{ taskserv.db_host }}" \
--db_port="{{ taskserv.db_port }}" \
--db_user="{{ taskserv.db_user }}" \
--db_password="$ODOO_DB_PASSWORD" \
--addons-path="/usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons" \
--init base \
--stop-after-init \
--without-demo=all \
--log-level=info
envFrom:
- secretRef:
name: {{ taskserv.name }}-credentials
volumeMounts:
- name: extra-addons
mountPath: /mnt/extra-addons
# Reconcile DB schema with the cloned addon code. Odoo only emits
# ALTER TABLE during an explicit module upgrade (-u), never on a plain
# load, so when OCA code advances the schema drifts (missing columns →
# UndefinedColumn at runtime). This runs -u on installed extra-addons
# modules exactly when the resolved-SHA fingerprint differs from the one
# stamped in ir_config_parameter, then re-stamps. No drift → no upgrade.
- name: sync-odoo-schema
image: {{ taskserv.image }}
command: ["/bin/bash", "-c"]
args:
- |
set -e
FP_FILE=/mnt/extra-addons/.oca-fingerprint
if [ ! -f "$FP_FILE" ]; then
echo "[sync] no fingerprint file — skip"; exit 0
fi
FP=$(cat "$FP_FILE")
export PGPASSWORD="$ODOO_DB_PASSWORD"
action=$(python3 - "$FP" <<'PYEOF'
import sys, os, psycopg2
fp = sys.argv[1]
try:
conn = psycopg2.connect(
host="{{ taskserv.db_host }}", port={{ taskserv.db_port }},
user="{{ taskserv.db_user }}",
password=os.environ["ODOO_DB_PASSWORD"],
dbname="{{ taskserv.db_name }}")
except Exception:
print("SKIP"); sys.exit(0)
cur = conn.cursor()
cur.execute("SELECT 1 FROM pg_tables WHERE tablename='ir_config_parameter'")
if not cur.fetchone():
print("SKIP"); sys.exit(0) # DB not yet initialized
cur.execute("SELECT value FROM ir_config_parameter WHERE key='oca_addons.fingerprint'")
row = cur.fetchone()
if row and row[0] == fp:
print("SKIP"); sys.exit(0) # nothing changed
disk = {e.name for e in os.scandir("/mnt/extra-addons")
if e.is_dir() and os.path.isfile(os.path.join(e.path, "__manifest__.py"))}
cur.execute("SELECT name FROM ir_module_module WHERE state='installed'")
installed = {r[0] for r in cur.fetchall()}
mods = sorted(disk & installed)
print(("UPGRADE " + ",".join(mods)) if mods else "STAMP")
PYEOF
)
case "$action" in
SKIP*)
echo "[sync] fingerprint unchanged or DB not ready — skip"; exit 0 ;;
STAMP*)
echo "[sync] no managed modules installed yet — stamping fingerprint" ;;
UPGRADE*)
mods=$(echo "$action" | cut -d' ' -f2)
echo "[sync] OCA addons changed — upgrading installed modules: $mods"
# Non-fatal: a failed reconcile must not block the whole
# instance from starting. Leave the fingerprint unstamped so
# the upgrade is retried on the next start; the server comes
# up on the existing schema (drift surfaces as a runtime error
# to fix, not a total outage).
if ! odoo -d "{{ taskserv.db_name }}" \
--db_host="{{ taskserv.db_host }}" \
--db_port="{{ taskserv.db_port }}" \
--db_user="{{ taskserv.db_user }}" \
--db_password="$ODOO_DB_PASSWORD" \
--addons-path="/usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons" \
-u "$mods" --stop-after-init --no-http \
--max-cron-threads=0 --log-level=info; then
echo "[sync] WARNING: module upgrade failed — starting without re-stamping; will retry next start" >&2
exit 0
fi ;;
esac
python3 - "$FP" <<'PYEOF'
import sys, os, psycopg2
fp = sys.argv[1]
conn = psycopg2.connect(
host="{{ taskserv.db_host }}", port={{ taskserv.db_port }},
user="{{ taskserv.db_user }}",
password=os.environ["ODOO_DB_PASSWORD"],
dbname="{{ taskserv.db_name }}")
conn.autocommit = True
cur = conn.cursor()
cur.execute("UPDATE ir_config_parameter SET value=%s, write_date=now() "
"WHERE key='oca_addons.fingerprint'", (fp,))
if cur.rowcount == 0:
cur.execute("INSERT INTO ir_config_parameter (key, value, create_date, write_date) "
"VALUES ('oca_addons.fingerprint', %s, now(), now())", (fp,))
PYEOF
echo "[sync] fingerprint stamped: $FP"
env:
# match the main container so `-u` can import addon external deps
# (e.g. schwifty/packaging) that install-python-deps placed here.
- name: PYTHONPATH
value: "/mnt/extra-addons/python-packages"
envFrom:
- secretRef:
name: {{ taskserv.name }}-credentials
volumeMounts:
- name: extra-addons
mountPath: /mnt/extra-addons
containers:
- name: odoo
image: {{ taskserv.image }}
args:
- "--db_host={{ taskserv.db_host }}"
- "--db_port={{ taskserv.db_port }}"
- "--db_user={{ taskserv.db_user }}"
- "--db-filter={{ odoo_db_filter }}"
- "--no-database-list"
- "--addons-path=/usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons"
- "--proxy-mode"
- "--workers={{ taskserv.workers }}"
- "--max-cron-threads={{ taskserv.max_cron_threads }}"
- "--limit-memory-hard=536870912"
- "--limit-memory-soft=469762048"
- "--limit-time-cpu=60"
- "--limit-time-real=120"
- "--log-level=info"
ports:
- containerPort: 8069
name: http
protocol: TCP
env:
- name: DB_NAME
value: "{{ taskserv.db_name }}"
- name: HOST
value: "{{ taskserv.db_host }}"
- name: PORT
value: "{{ taskserv.db_port }}"
- name: USER
value: "{{ taskserv.db_user }}"
- name: PYTHONPATH
value: "/mnt/extra-addons/python-packages"
envFrom:
- secretRef:
name: {{ taskserv.name }}-credentials
volumeMounts:
- name: filestore
mountPath: /var/lib/odoo
- name: extra-addons
mountPath: /mnt/extra-addons
{% if taskserv.probes is defined and taskserv.probes.readiness is defined %}
readinessProbe:
httpGet:
path: {{ taskserv.probes.readiness.path }}
port: {{ taskserv.probes.readiness.port | default(value=8069) }}
initialDelaySeconds: {{ taskserv.probes.readiness.initial_delay }}
periodSeconds: {{ taskserv.probes.readiness.period }}
failureThreshold: {{ taskserv.probes.readiness.failure_threshold }}
{% else %}
readinessProbe:
httpGet:
path: /web/login
port: 8069
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 12
{% endif %}
{% if taskserv.probes is defined and taskserv.probes.liveness is defined %}
livenessProbe:
httpGet:
path: {{ taskserv.probes.liveness.path }}
port: {{ taskserv.probes.liveness.port | default(value=8069) }}
initialDelaySeconds: {{ taskserv.probes.liveness.initial_delay }}
periodSeconds: {{ taskserv.probes.liveness.period }}
failureThreshold: {{ taskserv.probes.liveness.failure_threshold }}
{% else %}
livenessProbe:
httpGet:
path: /web/health
port: 8069
initialDelaySeconds: 60
periodSeconds: 30
failureThreshold: 3
{% endif %}
{% if taskserv.resources_effective is defined %}
resources:
requests:
cpu: "{{ taskserv.resources_effective.cpu.request }}"
memory: "{{ taskserv.resources_effective.memory.request }}"
limits:
cpu: "{{ taskserv.resources_effective.cpu.limit }}"
memory: "{{ taskserv.resources_effective.memory.limit }}"
{% else %}
resources:
requests:
cpu: "100m"
memory: "350Mi"
limits:
cpu: "1000m"
memory: "1Gi"
{% endif %}
volumes:
- name: filestore
persistentVolumeClaim:
claimName: {{ taskserv.filestore_pvc }}
- name: extra-addons
emptyDir: {}