#!/bin/bash # Methods library for odoo — sourced by install-odoo.sh and generated run-*.sh scripts. # SCRIPT_DIR must be set by the caller before sourcing. _kubectl() { if command -v kubectl >/dev/null 2>&1; then command kubectl "$@" elif command -v k0s >/dev/null 2>&1; then k0s kubectl "$@" else echo "ERROR: no kubectl or k0s binary found" >&2; exit 127 fi } _require_env() { local var="$1" if [ -z "${!var:-}" ]; then echo "ERROR: required variable $var is not set" >&2; exit 1 fi } _pod_name() { _kubectl get pod \ --namespace "$ODOO_NAMESPACE" \ --selector "app.kubernetes.io/name=odoo,app.kubernetes.io/instance=${ODOO_TENANT}" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null } _wait_for_pod() { local timeout="${1:-300}" _kubectl wait pod \ --namespace "$ODOO_NAMESPACE" \ --selector "app.kubernetes.io/name=odoo,app.kubernetes.io/instance=${ODOO_TENANT}" \ --for=condition=Ready \ --timeout="${timeout}s" } # ── Manifest plan helpers ───────────────────────────────────────────────────── _plan_apply() { local file="$1" local path="$SCRIPT_DIR/manifests/${file}.yaml" [ -f "$path" ] || { echo " [skip] ${file}.yaml not in manifests/"; return 0; } _kubectl apply -f "$path" } _plan_apply_skip() { local file="$1" local path="$SCRIPT_DIR/manifests/${file}.yaml" [ -f "$path" ] || { echo " [skip] ${file}.yaml not in manifests/"; return 0; } if _kubectl get -f "$path" >/dev/null 2>&1; then echo " [skip] ${file} already exists in cluster" return 0 fi _kubectl apply -f "$path" } _plan_rollout_restart() { local file="$1" local path="$SCRIPT_DIR/manifests/${file}.yaml" [ -f "$path" ] || { echo " [skip] ${file}.yaml not in manifests/"; return 0; } _kubectl rollout restart -f "$path" } _plan_delete() { local file="$1" local path="$SCRIPT_DIR/manifests/${file}.yaml" [ -f "$path" ] || { echo " [skip] ${file}.yaml not in manifests/"; return 0; } _kubectl delete -f "$path" --ignore-not-found } # shellcheck source=lib/gateway-tls.sh source "${SCRIPT_DIR}/lib/gateway-tls.sh" # ── Component methods ───────────────────────────────────────────────────────── _pg_namespace() { # postgresql.core-data.svc.cluster.local → core-data awk -F'.' '{print $2}' <<< "${ODOO_DB_HOST:-postgresql.core-data.svc.cluster.local}" } _pg_pod() { local ns ns=$(_pg_namespace) _kubectl get pod --namespace "$ns" \ --selector "app.kubernetes.io/name=postgresql" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null } _method_create-credentials() { [ -f "${SCRIPT_DIR}/_credentials.env" ] && set -a && source "${SCRIPT_DIR}/_credentials.env" && set +a _require_env ODOO_DB_PASSWORD _require_env ODOO_ADMIN_PASSWORD _kubectl create secret generic odoo-credentials \ --namespace "$ODOO_NAMESPACE" \ --from-literal=ODOO_DB_PASSWORD="$ODOO_DB_PASSWORD" \ --from-literal=PASSWORD="$ODOO_DB_PASSWORD" \ --from-literal=ODOO_ADMIN_PASSWORD="$ODOO_ADMIN_PASSWORD" \ --from-literal=ADMIN_PASSWD="$ODOO_ADMIN_PASSWORD" \ --dry-run=client -o yaml | _kubectl apply -f - } _method_init-db() { [ -f "${SCRIPT_DIR}/_credentials.env" ] && set -a && source "${SCRIPT_DIR}/_credentials.env" && set +a _require_env ODOO_DB_PASSWORD _require_env ODOO_DB_NAME local pg_ns pg_pod db_user safe_pw safe_usr safe_db pg_ns=$(_pg_namespace) pg_pod=$(_pg_pod) [ -z "$pg_pod" ] && { echo " [error] no postgresql pod found in namespace $pg_ns" >&2; exit 1; } db_user="${ODOO_DB_USER:-odoo}" # single-quote escape for embedding in PL/pgSQL string literal safe_pw="${ODOO_DB_PASSWORD//\'/\'\'}" safe_usr="${db_user//\'/\'\'}" safe_db="${ODOO_DB_NAME//\'/\'\'}" # Role: can use DO block (DDL inside function is fine for ROLE, not DATABASE) _kubectl exec -i --namespace "$pg_ns" "$pg_pod" -- psql -U postgres </dev/null) if [ "${db_exists:-}" = "1" ]; then echo " [skip] database '${safe_db}' already exists (migration restore expected)" else _kubectl exec --namespace "$pg_ns" "$pg_pod" -- \ psql -U postgres -c \ "CREATE DATABASE \"${safe_db}\" WITH OWNER \"${safe_usr}\" ENCODING 'UTF8' LC_COLLATE 'C' TEMPLATE template0" echo " [ok] database '${safe_db}' created" fi echo " [ok] role '${db_user}' and database '${ODOO_DB_NAME}' ready in $pg_ns" } _method_wait-ready() { _wait_for_pod 300 } _method_post-install() { echo " [odoo] tenant ${ODOO_TENANT:-unknown} deployed in namespace ${ODOO_NAMESPACE}" } _method_dns-check() { local domain="${ODOO_DOMAIN:-}" local expected_ip="${ODOO_GATEWAY_IP:-}" if [ -z "$domain" ]; then echo " [skip] ODOO_DOMAIN not set — skipping DNS check" return 0 fi local resolved resolved=$(dig +short "$domain" A 2>/dev/null | head -1) if [ -z "$resolved" ]; then echo " [warn] DNS: $domain does not resolve yet — expected $expected_ip" return 0 fi if [ -n "$expected_ip" ] && [ "$resolved" != "$expected_ip" ]; then echo " [warn] DNS: $domain → $resolved (expected $expected_ip)" else echo " [ok] DNS: $domain → $resolved" fi } _method_protect-volume() { local pvc="${PLAN_PARAM_PVC:-odoo-filestore}" echo " waiting for PVC '$pvc' to bind..." _kubectl wait pvc "$pvc" \ --namespace "$ODOO_NAMESPACE" \ --for=jsonpath='{.status.phase}'=Bound \ --timeout=120s local pv_name pv_name=$(_kubectl get pvc "$pvc" \ --namespace "$ODOO_NAMESPACE" \ -o jsonpath='{.spec.volumeName}') if [ -z "$pv_name" ]; then echo " [warn] could not resolve PV name for PVC '$pvc' — skipping volume protection" return 0 fi echo " enabling delete-protection on volume '$pv_name'..." if command -v hcloud >/dev/null 2>&1; then hcloud volume enable-protection "$pv_name" delete 2>/dev/null \ && echo " [ok] volume '$pv_name' protected" \ || echo " [warn] hcloud protect failed — enable manually: hcloud volume enable-protection $pv_name delete" else echo " [warn] hcloud CLI not found — skip volume protection for '$pv_name'" fi }