Skip to content
Merged
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
39 changes: 24 additions & 15 deletions .github/workflows/k8s.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Kubernetes

on:
pull_request:
# labeled and unlabeled for the ci:odoo-* labels, see the deploy matrix
# labeled and unlabeled for the ci:odoo-* and ci:postgres-* labels, see the deploy matrix
types: [opened, synchronize, reopened, labeled, unlabeled]
push:
branches:
Expand All @@ -13,13 +13,14 @@ on:
- cron: "0 4 * * 1"
workflow_dispatch:

# A label other than ci:odoo-* gets a group of its own, so it does not cancel
# the running tests only to skip everything itself
# A label other than ci:odoo-* and ci:postgres-* gets a group of its own, so it
# does not cancel the running tests only to skip everything itself
concurrency:
group: >-
${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}${{
contains(fromJSON('["labeled", "unlabeled"]'), github.event.action) &&
!startsWith(github.event.label.name, 'ci:odoo-') && format('-{0}', github.run_id) || '' }}
!startsWith(github.event.label.name, 'ci:odoo-') && !startsWith(github.event.label.name, 'ci:postgres-') &&
format('-{0}', github.run_id) || '' }}
cancel-in-progress: true

permissions:
Expand All @@ -35,38 +36,45 @@ jobs:
# Other labels do not change what runs
if: >-
!contains(fromJSON('["labeled", "unlabeled"]'), github.event.action) ||
startsWith(github.event.label.name, 'ci:odoo-')
startsWith(github.event.label.name, 'ci:odoo-') || startsWith(github.event.label.name, 'ci:postgres-')
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
- run: make validate

versions:
name: Pick the Odoo versions
name: Pick the Odoo and PostgreSQL versions
runs-on: ubuntu-latest
needs: validate
outputs:
versions: ${{ steps.pick.outputs.versions }}
matrix: ${{ steps.pick.outputs.matrix }}
steps:
# Odoo 20 on every change, or the versions of the ci:odoo-19,
# ci:odoo-20 and ci:odoo-master labels on a pull request. Every build
# weekly and on demand
# Odoo 20 on the default PostgreSQL on every change. On a pull request
# the versions of the ci:odoo-19, ci:odoo-20 and ci:odoo-master labels,
# each on the PostgreSQL of every ci:postgres-<major> label. Weekly and on
# demand every build on the default, the oldest supported (min) and the
# newest PostgreSQL
- id: pick
env:
EVENT: ${{ github.event_name }}
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
run: |
if [ "$EVENT" = schedule ] || [ "$EVENT" = workflow_dispatch ]; then
versions='["19", "20", "19-nightly", "20-nightly", "master"]'
postgres='["", "min", "18"]'
else
versions=$(jq -c '[.[]? | select(IN("ci:odoo-19", "ci:odoo-20", "ci:odoo-master"))
| ltrimstr("ci:odoo-")] | if . == [] then ["20"] else . end' <<<"$LABELS")
postgres=$(jq -c '[.[]? | select(test("^ci:postgres-[0-9]+$"))
| ltrimstr("ci:postgres-")] | if . == [] then [""] else . end' <<<"$LABELS")
fi
echo "Odoo versions: $versions"
echo "versions=$versions" >>"$GITHUB_OUTPUT"
matrix=$(jq -nc --argjson v "$versions" --argjson p "$postgres" \
'[$v[] as $version | $p[] as $postgres | {$version, $postgres}]')
echo "Odoo and PostgreSQL versions: $matrix"
echo "matrix=$matrix" >>"$GITHUB_OUTPUT"

deploy:
name: Deploy and test Odoo ${{ matrix.version }} in k3d
name: Deploy and test Odoo ${{ matrix.version }} on PostgreSQL ${{ matrix.postgres || 'default' }} in k3d
runs-on: ubuntu-latest
needs: [validate, versions]
timeout-minutes: 60
Expand All @@ -75,9 +83,10 @@ jobs:
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.versions.outputs.versions) }}
include: ${{ fromJSON(needs.versions.outputs.matrix) }}
env:
VERSION: ${{ matrix.version }}
PG: ${{ matrix.postgres }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand Down Expand Up @@ -114,7 +123,7 @@ jobs:
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: k8s-diagnostics-${{ matrix.version }}
name: k8s-diagnostics-${{ matrix.version }}-postgres-${{ matrix.postgres || 'default' }}
path: diagnostics/

- name: Delete cluster
Expand Down
12 changes: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ container setup, the init and seed scripts, and custom modules in `addons/`.
```bash
make up # create/reuse the k3d cluster, build, deploy, follow logs
make up VERSION=19 # 20 (default), 19, 19-nightly, 20-nightly, master (remembered in .k8s/version)
make up PG=18 # PostgreSQL major version, 17 (default) or min (remembered in .k8s/postgres)
make down # stop, keep data
make reset # delete the noodle namespace and its data
make destroy # delete the shared cluster (FORCE=1 if other apps run)
Expand Down Expand Up @@ -55,12 +56,14 @@ This runs shellcheck, ruff (check and format) for `odoo/seed`, yamllint and tapl

- `k8s/cluster/k3d.yaml` - shared k3d cluster `dynamist` (Traefik on 127.0.0.1:80/443), identical in every repo
that uses it
- `k8s/base` - namespace `noodle`: `postgres` StatefulSet (postgres:17), `odoo` Deployment, Ingress `odoo.localhost`,
NetworkPolicies, quota. Settings in `config.env`/`secret.env`. Overlays `local` and `ci`
- `k8s/base` - namespace `noodle`: `postgres` StatefulSet (postgres:17, `PG` overrides the tag), `odoo` Deployment,
Ingress `odoo.localhost`, NetworkPolicies, quota. Settings in `config.env`/`secret.env`. Overlays `local` and `ci`
- `tests/k8s` - smoke, seed data and isolation tests against the deployed instance
- `Dockerfile` - one stage per Odoo source (`release-19`, `release-20`, `nightly`, `master`), picked by the `BASE`
build arg, then adds `odoo/` and `addons/`
- `scripts/build-args.sh` - maps `VERSION` to the build args, resolving the newest nightly deb or master commit
- `scripts/postgres-version.sh` - resolves `PG` to a PostgreSQL major version, at least the image's
`MIN_PG_VERSION`
- `odoo/entrypoint.sh` - runs `init-odoo.sh` and the banner, then the official `/entrypoint.sh`
- `odoo/init-odoo.sh` - orchestrator: database setup, then seeding
- `odoo/lib/` - `common.sh` (defaults, `TEST_USERS`), `setup-database.sh`, `banner.sh`
Expand All @@ -78,7 +81,8 @@ This runs shellcheck, ruff (check and format) for `odoo/seed`, yamllint and tapl

- Conventional commits (`feat:`, `fix:`, `docs:`, `chore:`), default branch `main`
- Work on a branch and open a pull request, do not push to `main`. CI tests only Odoo 20 on a pull request, add the
labels `ci:odoo-19`, `ci:odoo-20` and `ci:odoo-master` to test exactly those versions instead
labels `ci:odoo-19`, `ci:odoo-20` and `ci:odoo-master` to test exactly those versions instead, and
`ci:postgres-<major>` to run them on those PostgreSQL versions
- Init and seed steps must be idempotent (check, then create or update) since they run on every start. Shell modules
in `odoo/lib/` must also run on their own.
- Sample data goes in a dataset file in `odoo/seed/datasets/`, declaring its Odoo `modules` and the datasets it
Expand All @@ -95,6 +99,8 @@ This runs shellcheck, ruff (check and format) for `odoo/seed`, yamllint and tapl
- Seed code runs on 19, 20 and master: detect features (`hasattr`) instead of checking versions, and share the
helpers in `seed_common.py`
- A database only works with the series that created it, `setup-database.sh` refuses others, `make reset` to switch
- The PostgreSQL data only works with the major version that created it, `make deploy` and the `check-version` init
container refuse others, `make reset` to switch. Odoo only warns below `MIN_PG_VERSION`, `postgres-version.sh` refuses
- Odoo 20: `ir.config_parameter.set_param()` is gone, use typed `set_str()`/`set_bool()`/`set_int()` (see
`seed_common.set_param()`)
- Odoo 20: bearer API keys must have the route's scope (`rpc`), keys without a scope no longer work there
Expand Down
37 changes: 27 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ OVERLAY ?= local
IMAGE := dynamist/odoo
BUILD_DIR := .k8s

# Odoo to build: 19, 20, 20-nightly or master, see scripts/build-args.sh.
# Odoo to build: 19, 20, 19-nightly, 20-nightly or master, see scripts/build-args.sh.
# Defaults to the last one built, switching needs `make reset`.
VERSION ?= $(shell cat $(BUILD_DIR)/version 2>/dev/null || echo 20)
# PostgreSQL major version to run, or min for the oldest Odoo supports, see
# scripts/postgres-version.sh. Defaults to the last one deployed, else the tag
# in k8s/base/postgres.yaml. Switching needs `make reset`.
PG ?= $(shell cat $(BUILD_DIR)/postgres 2>/dev/null)

# odooly.ini section used by `make console` and `make sample`
ODOOLY_ENV ?= dev
Expand Down Expand Up @@ -79,18 +83,30 @@ odoo-image: check-tools ## build the odoo image of VERSION (19, 20, 20-nightly,
$(K3D) image import -c $(CLUSTER) $(IMAGE):$$tag; \
echo $$tag > $(BUILD_DIR)/image-tag

deploy: check-tools ## apply the manifests of OVERLAY (local or ci) with the imported image
deploy: check-tools ## apply the manifests of OVERLAY (local or ci) with the imported image and PostgreSQL PG
@test -f $(BUILD_DIR)/image-tag || { echo "Error: no image imported yet, run make odoo-image"; exit 1; }
@touch k8s/overlays/local/config.local.env
@printf '%s\n' \
@tag=$$(cat $(BUILD_DIR)/image-tag) && \
pg=$$(scripts/postgres-version.sh $(IMAGE):$$tag $(PG)) && \
data=$$($(KUBECTL) exec statefulset/postgres -c postgres -- sh -c 'cat "$$PGDATA/PG_VERSION"' 2>/dev/null); \
if [ -n "$$data" ] && [ "$$data" != "$$pg" ]; then \
echo "Error: the database was created with PostgreSQL $$data, PG asks for $$pg"; \
echo "Error: run 'make reset' to delete the data, then 'make up PG=$$pg' again (or PG=$$data to keep it)"; \
exit 1; \
fi; \
printf '%s\n' \
'apiVersion: kustomize.config.k8s.io/v1beta1' \
'kind: Kustomization' \
'resources: [../k8s/overlays/$(OVERLAY)]' \
'images: [{name: $(IMAGE), newTag: '"$$(cat $(BUILD_DIR)/image-tag)"'}]' \
> $(BUILD_DIR)/kustomization.yaml
mise exec -- kubectl --context $(KUBE_CONTEXT) apply -k $(BUILD_DIR)

up: cluster odoo-image deploy ## start odoo of VERSION in the cluster and follow its logs until it is ready
'images:' \
" - {name: $(IMAGE), newTag: $$tag}" \
" - {name: postgres, newTag: \"$$pg\"}" \
> $(BUILD_DIR)/kustomization.yaml && \
echo "Deploying PostgreSQL $$pg" && \
mise exec -- kubectl --context $(KUBE_CONTEXT) apply -k $(BUILD_DIR) && \
echo $$pg > $(BUILD_DIR)/postgres

up: cluster odoo-image deploy ## start odoo of VERSION on PostgreSQL PG in the cluster and follow its logs until it is ready
@$(KUBECTL) rollout status statefulset/postgres --timeout=5m
@$(KUBECTL) logs -f deploy/odoo --pod-running-timeout=5m & logs=$$!; \
$(KUBECTL) rollout status deploy/odoo --timeout=30m; status=$$?; \
Expand All @@ -105,9 +121,10 @@ reset: check-tools ## DELETE the noodle namespace with all its data (other apps
logs: check-tools ## follow odoo logs
$(KUBECTL) logs -f deploy/odoo

ps: check-tools ## show pods, services, ingress, volumes and the running Odoo version
ps: check-tools ## show pods, services, ingress, volumes and the running Odoo and PostgreSQL versions
$(KUBECTL) get pods,svc,ingress,pvc
@$(KUBECTL) exec deploy/odoo -- odoo --version 2>/dev/null || true
@$(KUBECTL) exec statefulset/postgres -c postgres -- postgres --version 2>/dev/null || true

shell: check-tools ## open shell in the odoo pod
$(KUBECTL) exec -it deploy/odoo -- /bin/bash
Expand Down Expand Up @@ -142,7 +159,7 @@ validate: ## validate the rendered manifests of all overlays
done

test-k8s: ## run the smoke, seed data and isolation tests in tests/k8s against the deployed odoo (PYTEST_ARGS="-m 'not slow'" for pytest)
NOODLE_VERSION=$(VERSION) mise exec -- uv run --no-project --with pytest --with requests --with pyyaml \
NOODLE_VERSION=$(VERSION) NOODLE_POSTGRES=$$(cat $(BUILD_DIR)/postgres 2>/dev/null) mise exec -- uv run --no-project --with pytest --with requests --with pyyaml \
pytest tests/k8s -p no:cacheprovider $(PYTEST_ARGS)

##@ CI
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ The last `VERSION` built is kept in `.k8s/version`, a plain `make up` reuses
it. A database only works with the Odoo series that created it: Odoo refuses to
start on another one and asks for `make reset`.

`PG` picks the PostgreSQL major version next to it, PostgreSQL 17 by default.
`PG=min` picks the oldest version the Odoo series supports, and a version below
that is refused before deploying:

```bash
make reset && make up VERSION=19 PG=min
make reset && make up PG=18
```

The last `PG` is kept in `.k8s/postgres`. The data only works with the
PostgreSQL major version that created it, switching needs `make reset` as well.

```bash
make console # odooly console logged in as admin with the API key
make test-k8s # run the smoke, seed data and isolation tests
Expand Down
39 changes: 35 additions & 4 deletions docs/odoo-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,41 @@ Differences between the versions are handled in the seed code by feature
detection rather than version checks, e.g. `set_param()` in
`seed_common.py`.

CI tests `20` on every change. A pull request with the label `ci:odoo-19`,
`ci:odoo-20` or `ci:odoo-master` tests exactly the labelled versions instead.
The weekly and manual runs test `19`, `20`, `19-nightly`, `20-nightly` and
`master` (`master` may fail without failing the workflow).
## PostgreSQL Versions

`make up PG=...` runs the official `postgres:<PG>` image next to Odoo, the
deploy replaces the tag of `k8s/base/postgres.yaml` in the generated
`.k8s/kustomization.yaml`. `scripts/postgres-version.sh` resolves `PG`:

| `PG` | PostgreSQL |
|------|------------|
| empty (default) | the tag in `k8s/base/postgres.yaml`, `17` |
| `min` | the oldest the Odoo in the image supports, `odoo.release.MIN_PG_VERSION` |
| `16`, `18`, ... | that major version |

Odoo itself only warns when PostgreSQL is older than `MIN_PG_VERSION`, so
`make deploy` reads it from the built image and refuses an older `PG`.

The last `PG` is kept in `.k8s/postgres` and reused by a plain `make up`, so a
new default in `k8s/base/postgres.yaml` does not break existing data. The data
belongs to the major version that created it (`PG_VERSION` in the data
directory). `make deploy` compares it with `PG` while postgres runs, and the
`check-version` init container of the StatefulSet when it starts. Either stops
with an error, run `make reset` to switch.

The volume is mounted at `/var/lib/postgresql` with the data in `pgdata/`,
which is the volume of `postgres:18` and later images and works for the older
ones as well.

## Versions in CI

CI tests `20` on the default PostgreSQL on every change. A pull request with
the label `ci:odoo-19`, `ci:odoo-20` or `ci:odoo-master` tests exactly the
labelled versions instead, and the labels `ci:postgres-<major>` (e.g.
`ci:postgres-18`) run each of them on those PostgreSQL versions. The weekly
and manual runs test `19`, `20`, `19-nightly`, `20-nightly` and `master`, each
on the default, `min` and `18` (`master` may fail without failing the
workflow).

## Kubernetes Setup

Expand Down
34 changes: 32 additions & 2 deletions k8s/base/postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,45 @@ spec:
app.kubernetes.io/name: postgres
spec:
enableServiceLinks: false
initContainers:
# A major version cannot read the data of another, say so instead of
# failing on start. `make deploy` replaces both images with PG
- name: check-version
image: postgres:17
command:
- sh
- -c
- |
data=$(cat "$PGDATA/PG_VERSION" 2>/dev/null) || exit 0
if [ "$data" != "$PG_MAJOR" ]; then
echo "ERROR: the database was created with PostgreSQL $data, this image runs PostgreSQL $PG_MAJOR"
echo "ERROR: run 'make reset' to delete the data, then 'make up PG=$PG_MAJOR' again (or PG=$data to keep it)"
exit 1
fi
env:
- name: PGDATA
value: /var/lib/postgresql/pgdata
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
memory: 64Mi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql
containers:
- name: postgres
# The default PostgreSQL, `make deploy` replaces the tag with PG
image: postgres:17
env:
# Do not pre-create the Odoo database, init-odoo.sh creates it
- name: POSTGRES_DB
value: postgres
# The volume of the postgres:18 and later images, a subdirectory
# so initdb gets an empty directory
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
value: /var/lib/postgresql/pgdata
envFrom:
- secretRef:
name: odoo-secret
Expand All @@ -52,7 +82,7 @@ spec:
memory: 1Gi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
mountPath: /var/lib/postgresql
volumeClaimTemplates:
- metadata:
name: data
Expand Down
12 changes: 12 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@
"matchCurrentValue": "/^20\\./",
"allowedVersions": "/^20\\./"
},
{
"description": "The postgres tag in k8s/base/postgres.yaml is the default PostgreSQL for new data only",
"matchManagers": [
"kubernetes"
],
"matchDepNames": [
"postgres"
],
"prBodyNotes": [
":warning: Existing setups keep the PostgreSQL in `.k8s/postgres`. A new major version only applies after `make reset` (the data belongs to the major version that created it). Check that it is not newer than the PostgreSQL client in the Odoo images."
]
},
{
"description": "dynamist/odoo is built from this repo and tagged at deploy time, there is nothing upstream to track",
"matchDepNames": [
Expand Down
Loading
Loading