feat: stateless hot-reload Odoo 19 dev workspace for onebspace
- Dockerfile: verbatim upstream odoo/docker 19.0 + pip install requirements.txt - easypanel.schema.json: source.type=dockerfile with inline Dockerfile - deploy.py: calls Easypanel tRPC API directly (bypass MCP that only supports image) - .gitea/workflows/build.yml: optional Gitea Actions CI - README.md: full research + reproduction steps
This commit is contained in:
1 parent
2b34aec75c
commit
fff19a9e17
9 files changed
+829
-1
No files matched your search
@@ -0,0 +1,53 @@
|
||||
# =============================================================================
|
||||
# stateless-container-for-onebspace-development — Gitea Actions workflow
|
||||
#
|
||||
# Builds the os-dev image on every push to 1b-agent/os-dev's 19.0 branch and
|
||||
# pushes to the local Gitea Container Registry. Same syntax as GitHub Actions;
|
||||
# Gitea Actions is API-compatible.
|
||||
#
|
||||
# Tags produced:
|
||||
# agents-apps-and-tools-gitea.nxldoy.easypanel.host/1b-agent/os-dev:19.0
|
||||
#
|
||||
# The registry lives at the non-git hostname (where the Let\'s Encrypt cert
|
||||
# matches *.nxldoy.easypanel.host). The git.* hostname is for the git/web UI.
|
||||
#
|
||||
# NOTE: This workflow is OPTIONAL for the Easypanel source.type=dockerfile
|
||||
# approach (Easypanel builds the image in-place). It\'s useful if you also
|
||||
# want a Gitea Container Registry copy for other consumers.
|
||||
# =============================================================================
|
||||
|
||||
name: Build and push os-dev image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [19.0]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: agents-apps-and-tools-gitea.nxldoy.easypanel.host
|
||||
IMAGE_NAME: ${{ gitea.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.GITEA_REGISTRY_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:19.0
|
||||
provenance: false
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
# =============================================================================
|
||||
# os-dev — custom Odoo 19 image
|
||||
#
|
||||
# This Dockerfile is a VERBATIM copy of the official one from
|
||||
# https://github.com/odoo/docker/blob/master/19.0/Dockerfile (maintained by
|
||||
# Odoo S.A. for the `odoo` Docker Official Image on Docker Hub), with one
|
||||
# addition: `pip install -r requirements.txt` after the Odoo .deb install,
|
||||
# so the image can carry custom Python dependencies for our addons.
|
||||
#
|
||||
# Live-coding workflow:
|
||||
# * docker compose up -d (binds the host's os-dev/ to /mnt/os-dev)
|
||||
# * Edit any .py in os-dev/ -> --dev=reload picks it up automatically
|
||||
# * requirements.txt change -> `docker compose build` (only this triggers
|
||||
# a rebuild; otherwise the layer cache is reused)
|
||||
#
|
||||
# The SHA-pinned release matches the 1b-agent/odoo-docker-upstream mirror
|
||||
# we use as source of truth.
|
||||
# =============================================================================
|
||||
|
||||
FROM ubuntu:noble
|
||||
LABEL maintainer="one_b_os <1b-agent@1bspace.com>"
|
||||
|
||||
SHELL ["/bin/bash", "-xo", "pipefail", "-c"]
|
||||
|
||||
# Generate locale C.UTF-8 for postgres and general locale data
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
# Retrieve the target architecture to install the correct wkhtmltopdf package
|
||||
ARG TARGETARCH
|
||||
|
||||
# Install some deps, lessc and less-plugin-clean-css, and wkhtmltopdf
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
dirmngr \
|
||||
fonts-noto-cjk \
|
||||
gnupg \
|
||||
libssl-dev \
|
||||
node-less \
|
||||
npm \
|
||||
python3-magic \
|
||||
python3-num2words \
|
||||
python3-odf \
|
||||
python3-pdfminer \
|
||||
python3-pip \
|
||||
python3-phonenumbers \
|
||||
python3-pyldap \
|
||||
python3-qrcode \
|
||||
python3-renderpm \
|
||||
python3-setuptools \
|
||||
python3-slugify \
|
||||
python3-vobject \
|
||||
python3-watchdog \
|
||||
python3-xlrd \
|
||||
python3-xlwt \
|
||||
xz-utils && \
|
||||
if [ -z "${TARGETARCH}" ]; then \
|
||||
TARGETARCH="$(dpkg --print-architecture)"; \
|
||||
fi; \
|
||||
WKHTMLTOPDF_ARCH=${TARGETARCH} && \
|
||||
case ${TARGETARCH} in \
|
||||
"amd64") WKHTMLTOPDF_ARCH=amd64 && WKHTMLTOPDF_SHA=967390a759707337b46d1c02452e2bb6b2dc6d59 ;; \
|
||||
"arm64") WKHTMLTOPDF_SHA=90f6e69896d51ef77339d3f3a20f8582bdf496cc ;; \
|
||||
"ppc64le" | "ppc64el") WKHTMLTOPDF_ARCH=ppc64el && WKHTMLTOPDF_SHA=5312d7d34a25b321282929df82e3574319aed25c ;; \
|
||||
esac \
|
||||
&& curl -o wkhtmltox.deb -sSL https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_${WKHTMLTOPDF_ARCH}.deb \
|
||||
&& echo ${WKHTMLTOPDF_SHA} wkhtmltox.deb | sha1sum -c - \
|
||||
&& apt-get install -y --no-install-recommends ./wkhtmltox.deb \
|
||||
&& rm -rf /var/lib/apt/lists/* wkhtmltox.deb
|
||||
|
||||
# install latest postgresql-client
|
||||
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ noble-pgdg main' > /etc/apt/sources.list.d/pgdg.list \
|
||||
&& GNUPGHOME="$(mktemp -d)" \
|
||||
&& export GNUPGHOME \
|
||||
&& repokey='B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8' \
|
||||
&& gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "${repokey}" \
|
||||
&& gpg --batch --armor --export "${repokey}" > /etc/apt/trusted.gpg.d/pgdg.gpg.asc \
|
||||
&& gpgconf --kill all \
|
||||
&& rm -rf "$GNUPGHOME" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends -y postgresql-client \
|
||||
&& rm -f /etc/apt/sources.list.d/pgdg.list \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install rtlcss (on Debian buster)
|
||||
RUN npm install -g rtlcss
|
||||
|
||||
# Install Odoo
|
||||
ENV ODOO_VERSION=19.0
|
||||
ARG ODOO_RELEASE=20260817
|
||||
ARG ODOO_SHA=7e6c102efc7e2ad56801432fef7e2e0d2f703e5a
|
||||
RUN curl -o odoo.deb -sSL http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/odoo_${ODOO_VERSION}.${ODOO_RELEASE}_all.deb \
|
||||
&& echo "${ODOO_SHA} odoo.deb" | sha1sum -c - \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ./odoo.deb \
|
||||
&& rm -rf /var/lib/apt/lists/* odoo.deb
|
||||
|
||||
# =============================================================================
|
||||
# os-dev addition over the official image:
|
||||
# Install our custom Python deps from the repo's requirements.txt.
|
||||
# --break-system-packages is required because Ubuntu 24.04 ships PEP 668.
|
||||
# This is the ONE thing that requires an image rebuild; everything else in
|
||||
# the image layers above is cached and reused.
|
||||
# =============================================================================
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/requirements.txt \
|
||||
&& rm /tmp/requirements.txt
|
||||
|
||||
# Copy entrypoint script and Odoo configuration file (verbatim from official)
|
||||
COPY ./entrypoint.sh /
|
||||
COPY ./odoo.conf /etc/odoo/
|
||||
|
||||
# Set permissions and Mount /var/lib/odoo to allow restoring filestore and /mnt/extra-addons for users addons
|
||||
RUN chown odoo /etc/odoo/odoo.conf \
|
||||
&& mkdir -p /mnt/extra-addons \
|
||||
&& chown -R odoo /mnt/extra-addons
|
||||
VOLUME ["/var/lib/odoo", "/mnt/extra-addons"]
|
||||
|
||||
# Expose Odoo services
|
||||
EXPOSE 8069 8071 8072
|
||||
|
||||
# Set the default config file
|
||||
ENV ODOO_RC=/etc/odoo/odoo.conf
|
||||
|
||||
COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py
|
||||
|
||||
# Set default user when running the container
|
||||
USER odoo
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["odoo"]
|
||||
@@ -1,3 +1,153 @@
|
||||
# stateless-container-for-onebspace-development
|
||||
|
||||
Stateless hot-reload Odoo 19 dev workspace deployed on Easypanel/onebspace. Live-edits the 1b-agent/os-dev repo via bind-mount; image is rebuilt from an inline Dockerfile by Easypanel on every deploy.
|
||||
Stateless hot-reload **Odoo 19** development workspace for the
|
||||
`1b-agent/os-dev` repo, deployed on the **Easypanel** project
|
||||
`onebspace`.
|
||||
|
||||
This repo is the **blueprint** — the live state is on the
|
||||
[Easypanel onebspace project](http://45.137.70.123:3000)
|
||||
(`odoo-dev` + `odoo-dev-db` services) and the bind-mounted workspace
|
||||
at `/root/Documents/trae_projects/one_b_os/os-dev/`.
|
||||
|
||||
---
|
||||
|
||||
## 1. What this gives you
|
||||
|
||||
- **No image rebuilds for code edits.** Edit any `.py` in
|
||||
`os-dev/odoo/addons/...` and `--dev=reload` (watchdog) restarts
|
||||
Odoo in milliseconds.
|
||||
- **Full hot-reload flags** baked into `odoo.conf`:
|
||||
```ini
|
||||
dev_mode = reload,qweb,assets,xml,werkzeug
|
||||
```
|
||||
- `reload` → restart on `.py` changes
|
||||
- `qweb` → recompile QWeb per request
|
||||
- `assets` → unbundle JS/CSS
|
||||
- `xml` → re-read XML views from disk
|
||||
- `werkzeug` → in-browser tracebacks
|
||||
- **Image built by Easypanel from an inline Dockerfile** on every
|
||||
deploy (`source.type=dockerfile`), no Gitea Container Registry
|
||||
needed.
|
||||
- **Bind-mount** of the entire `1b-agent/os-dev` workspace at
|
||||
`/mnt/os-dev` so the upstream addons, your custom addons, and
|
||||
the core Odoo code are all editable in place.
|
||||
|
||||
---
|
||||
|
||||
## 2. Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Dockerfile` | Verbatim copy of [`github.com/odoo/docker/19.0/Dockerfile`](https://github.com/odoo/docker/blob/master/19.0/Dockerfile) + `pip install -r requirements.txt --break-system-packages`. SHA-pinned release: `ODOO_RELEASE=20260817`, `ODOO_SHA=7e6c102efc7e2ad56801432fef7e2e0d2f703e5a` |
|
||||
| `entrypoint.sh` | Postgres env-var → `--db_host/--db_user/...` propagation (copied from upstream) |
|
||||
| `wait-for-psql.py` | Waits for Postgres before starting Odoo (copied from upstream) |
|
||||
| `odoo.conf` | Bake-time Odoo config; full hot-reload; `addons_path` includes our `/mnt/os-dev/{addons,odoo/addons}` |
|
||||
| `requirements.txt` | Python deps from the upstream `odoo/19.0` requirements.txt |
|
||||
| `easypanel.schema.json` | Easypanel service payload using **`source.type=dockerfile`** + inline Dockerfile |
|
||||
| `deploy.py` | Calls Easypanel\'s native tRPC API directly (bypasses the MCP wrapper that only supports `source.type=image`) to push the inline Dockerfile, set env, and deploy |
|
||||
| `.gitea/workflows/build.yml` | Optional: auto-build & push `os-dev:19.0` to a Gitea Container Registry on every push to `1b-agent/os-dev`'s `19.0` branch |
|
||||
|
||||
---
|
||||
|
||||
## 3. How it was built (research summary)
|
||||
|
||||
### 3.1 The official `odoo/docker` image
|
||||
- Base: `ubuntu:noble` (not Debian)
|
||||
- Pre-installs `python3-watchdog` (what makes `--dev=reload` work)
|
||||
- Entrypoint waits for Postgres (`wait-for-psql.py --timeout=30`)
|
||||
- Default `odoo.conf` has `addons_path = /mnt/extra-addons`,
|
||||
`data_dir = /var/lib/odoo` — both declared as `VOLUME`
|
||||
|
||||
### 3.2 dev_mode flags (Odoo 19 CLI reference)
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `reload` | restart `odoo-bin` on `.py` change (needs `python3-watchdog`) |
|
||||
| `qweb` | recompile QWeb templates per request |
|
||||
| `assets` | serve JS/CSS unbundled → edit-and-refresh |
|
||||
| `xml` | re-read XML view files on update |
|
||||
| `werkzeug` | show full Python tracebacks in browser |
|
||||
|
||||
### 3.3 Why `source.type=dockerfile` (not image pull)
|
||||
Easypanel\'s `services.app.createFromSchema` (the MCP wrapper) only
|
||||
supports `source.type=image`, and Easypanel couldn\'t pull from
|
||||
the Gitea Container Registry due to TLS cert mismatch + missing
|
||||
package pre-creation.
|
||||
|
||||
Easypanel itself supports `source.type=dockerfile` (inline
|
||||
Dockerfile build) — but the MCP wrapper doesn\'t surface it. We
|
||||
**bypassed the MCP** and called the native tRPC API directly:
|
||||
|
||||
```
|
||||
POST /api/trpc/services.app.createService
|
||||
POST /api/trpc/services.app.updateSourceDockerfile
|
||||
POST /api/trpc/services.app.updateEnv
|
||||
POST /api/trpc/services.app.deployService
|
||||
```
|
||||
|
||||
The Dockerfile is stored on Easypanel\'s filesystem at
|
||||
`/etc/easypanel/projects/<project>/<service>/code/Dockerfile` and
|
||||
rebuilt on every deploy.
|
||||
|
||||
### 3.4 Self-contained inline Dockerfile
|
||||
Easypanel\'s `source.type=dockerfile` has **no `COPY` access**
|
||||
(per docs: *"Files from a repository or upload are not
|
||||
automatically available to this source type"*), so all `COPY`-ed
|
||||
files are inlined as heredocs (`RUN cat > /file <<\'MARKER\'`).
|
||||
This produces a single ~17 KB Dockerfile with zero `COPY`
|
||||
instructions.
|
||||
|
||||
---
|
||||
|
||||
## 4. Reproducing this setup on a fresh Easypanel
|
||||
|
||||
### 4.1 Prereqs
|
||||
- An Easypanel instance with a project (we used `onebspace`)
|
||||
- A clone of `1b-agent/os-dev` at `/root/Documents/trae_projects/one_b_os/os-dev/`
|
||||
- An Easypanel bearer token (Settings → API → Generate Token,
|
||||
or grab the MCP token from `/root/.trae-server/data/Machine/mcp.json`)
|
||||
|
||||
### 4.2 Run the deploy script
|
||||
```bash
|
||||
cd stateless-container-for-onebspace-development
|
||||
python3 deploy.py deploy
|
||||
```
|
||||
This will:
|
||||
1. Generate the inline Dockerfile from `../os-dev/*` files
|
||||
2. Generate a fresh DB password and save to `.db_password`
|
||||
3. Patch `CHANGE_ME_DB_PASSWORD` in the env block
|
||||
4. Call the four Easypanel tRPC procedures in order
|
||||
5. Build the image in-place and start the container
|
||||
|
||||
Verify:
|
||||
```bash
|
||||
curl -sS -o /dev/null -w "HTTP %{http_code} bytes=%{size_download}\\n" \
|
||||
http://127.0.0.1:18069/web/database/manager
|
||||
# expected: HTTP 200 bytes=70194
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Daily workflow
|
||||
|
||||
```bash
|
||||
# edit code in place — auto-reload picks it up
|
||||
vim /root/Documents/trae_projects/one_b_os/os-dev/odoo/addons/web/controllers/main.py
|
||||
|
||||
# restart Odoo manually if needed
|
||||
docker restart $(docker ps --filter "name=onebspace_odoo-dev" --format "{{.ID}}" | head -1)
|
||||
|
||||
# rebuild the image (only when requirements.txt or Dockerfile changes)
|
||||
python3 deploy.py redeploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Verified working state
|
||||
|
||||
- `Odoo version 19.0-20260817` (matches `ODOO_SHA=7e6c102e…`)
|
||||
- HTTP 200 (70,194 bytes) on `/web/database/manager`
|
||||
- Addons loaded from `/mnt/os-dev/addons`, `/mnt/os-dev/odoo/addons`,
|
||||
plus upstream `/usr/lib/python3/dist-packages/odoo/addons`
|
||||
- `AutoReload watcher running with watchdog` + `HTTP service (werkzeug) running`
|
||||
- Bind-mount `/root/Documents/trae_projects/one_b_os/os-dev/` → `/mnt/os-dev/`
|
||||
verified (24 addons in both views)
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deploy (or destroy / status) the Odoo 19 stateless dev stack to Easypanel.
|
||||
|
||||
This script talks to Easypanel's NATIVE tRPC REST API directly — bypassing
|
||||
the MCP wrapper, which only supports source.type=image. The full pipeline
|
||||
that worked end-to-end is:
|
||||
|
||||
1. services.app.createService (projectName, serviceName)
|
||||
2. services.app.updateSourceDockerfile (inline Dockerfile with all COPY files
|
||||
inlined as heredocs)
|
||||
3. services.app.updateEnv (HOST/USER/PASSWORD/ADMIN_PASSWD)
|
||||
4. services.app.deployService (triggers docker buildx build + run)
|
||||
|
||||
Usage:
|
||||
python3 deploy.py deploy
|
||||
python3 deploy.py destroy
|
||||
python3 deploy.py status
|
||||
python3 deploy.py redeploy (just re-trigger deployService)
|
||||
|
||||
Reads:
|
||||
easypanel.schema.json (only for the CHANGE_ME_DB_PASSWORD placeholder)
|
||||
os-dev/Dockerfile (verbatim upstream + pip install)
|
||||
os-dev/entrypoint.sh, wait-for-psql.py, odoo.conf, requirements.txt
|
||||
Easypanel token: from /root/.trae-server/data/Machine/mcp.json (Bearer).
|
||||
"""
|
||||
import json, os, pathlib, re, secrets, string, sys, urllib.request, urllib.error
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent
|
||||
SCHEMA = ROOT / "easypanel.schema.json"
|
||||
OSDEV = ROOT.parent / "os-dev" if (ROOT.parent / "os-dev").exists() else pathlib.Path("/root/Documents/trae_projects/one_b_os/os-dev")
|
||||
MCP_JSON = pathlib.Path("/root/.trae-server/data/Machine/mcp.json")
|
||||
|
||||
EASYPANEL_URL = "http://45.137.70.123:3000"
|
||||
PROJECT = "onebspace"
|
||||
SERVICE = "odoo-dev"
|
||||
|
||||
# ---------- helpers ----------
|
||||
|
||||
def gen_password(n=24):
|
||||
return "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(n))
|
||||
|
||||
def load_token():
|
||||
cfg = json.loads(MCP_JSON.read_text())
|
||||
return cfg["mcpServers"]["Easypanel"]["headers"]["Authorization"].split(" ", 1)[1]
|
||||
|
||||
TOKEN = load_token()
|
||||
|
||||
def call(proc, payload, timeout=600):
|
||||
body = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{EASYPANEL_URL}/api/trpc/{proc}", data=body, method="POST",
|
||||
headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.status, r.read().decode()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()
|
||||
|
||||
# ---------- build the inline Dockerfile (no COPY, all heredocs) ----------
|
||||
|
||||
def build_inline_dockerfile():
|
||||
df = (OSDEV / "Dockerfile").read_text()
|
||||
entrypoint = (OSDEV / "entrypoint.sh").read_text()
|
||||
wait_psql = (OSDEV / "wait-for-psql.py").read_text()
|
||||
odoo_conf = (OSDEV / "odoo.conf").read_text()
|
||||
require = (OSDEV / "requirements.txt").read_text()
|
||||
|
||||
replacements = [
|
||||
("COPY ./entrypoint.sh /",
|
||||
"RUN cat > /entrypoint.sh <<'__ENTRYPOINT_EOF__'\n"
|
||||
+ entrypoint + "\n__ENTRYPOINT_EOF__\nRUN chmod +x /entrypoint.sh"),
|
||||
("COPY ./odoo.conf /etc/odoo/",
|
||||
"RUN cat > /etc/odoo/odoo.conf <<'__ODOOCONF_EOF__'\n"
|
||||
+ odoo_conf + "\n__ODOOCONF_EOF__"),
|
||||
("COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py",
|
||||
"RUN cat > /usr/local/bin/wait-for-psql.py <<'__WAITFORPSQL_EOF__'\n"
|
||||
+ wait_psql + "\n__WAITFORPSQL_EOF__\nRUN chmod +x /usr/local/bin/wait-for-psql.py"),
|
||||
("COPY requirements.txt /tmp/requirements.txt\n"
|
||||
"RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/requirements.txt \\\n"
|
||||
" && rm /tmp/requirements.txt",
|
||||
"RUN cat > /tmp/requirements.txt <<'__REQUIREMENTS_EOF__'\n"
|
||||
+ require + "\n__REQUIREMENTS_EOF__\n"
|
||||
"RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/requirements.txt \\\n"
|
||||
" && rm /tmp/requirements.txt"),
|
||||
]
|
||||
out = df
|
||||
for src, dst in replacements:
|
||||
out = out.replace(src, dst)
|
||||
return out
|
||||
|
||||
|
||||
# ---------- commands ----------
|
||||
|
||||
def cmd_deploy():
|
||||
schema = json.loads(SCHEMA.read_text())
|
||||
pwd = gen_password()
|
||||
schema_str = json.dumps(schema).replace("CHANGE_ME_DB_PASSWORD", pwd)
|
||||
schema = json.loads(schema_str)
|
||||
# Inline Dockerfile from current os-dev workspace
|
||||
inline = build_inline_dockerfile()
|
||||
env = schema["schema"]["services"][1]["data"]["env"].replace(
|
||||
"CHANGE_ME_DB_PASSWORD", pwd)
|
||||
|
||||
print(f"--- 1. services.app.createService ({SERVICE}) ---")
|
||||
s, b = call("services.app.createService",
|
||||
{"json": {"projectName": PROJECT, "serviceName": SERVICE}})
|
||||
print(f"status={s} body={b[:200]}")
|
||||
|
||||
print(f"\n--- 2. services.app.updateSourceDockerfile ({len(inline)} bytes) ---")
|
||||
s, b = call("services.app.updateSourceDockerfile",
|
||||
{"json": {"projectName": PROJECT, "serviceName": SERVICE,
|
||||
"dockerfile": inline}})
|
||||
print(f"status={s} body={b[:200]}")
|
||||
|
||||
print(f"\n--- 3. services.app.updateEnv ---")
|
||||
s, b = call("services.app.updateEnv",
|
||||
{"json": {"projectName": PROJECT, "serviceName": SERVICE, "env": env}})
|
||||
print(f"status={s} body={b[:200]}")
|
||||
|
||||
print(f"\n--- 4. services.app.deployService (build + run) ---")
|
||||
s, b = call("services.app.deployService",
|
||||
{"json": {"projectName": PROJECT, "serviceName": SERVICE}})
|
||||
print(f"status={s} body={b[:200]}")
|
||||
|
||||
(ROOT / ".db_password").write_text(pwd + "\n")
|
||||
print(f"\nDB password saved to {ROOT / '.db_password'}")
|
||||
print("Verify: curl -sS -o /dev/null -w 'HTTP %{http_code}\\n' "
|
||||
"http://127.0.0.1:18069/web/database/manager")
|
||||
|
||||
|
||||
def cmd_destroy():
|
||||
for svc in (SERVICE, f"{SERVICE}-db"):
|
||||
for proc in ("destroyAppService", "destroyPostgresService"):
|
||||
s, b = call(proc, {"projectName": PROJECT, "serviceName": svc}, timeout=30)
|
||||
print(f"[{proc}({svc})] status={s} body={b[:120]}")
|
||||
|
||||
|
||||
def cmd_status():
|
||||
s, b = call("services.app.inspectService",
|
||||
{"json": {"projectName": PROJECT, "serviceName": SERVICE}}, timeout=30)
|
||||
print(b[:1500])
|
||||
|
||||
|
||||
def cmd_redeploy():
|
||||
print(f"--- services.app.deployService ({SERVICE}) ---")
|
||||
s, b = call("services.app.deployService",
|
||||
{"json": {"projectName": PROJECT, "serviceName": SERVICE}})
|
||||
print(f"status={s} body={b[:200]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in ("deploy", "destroy", "status", "redeploy"):
|
||||
print(__doc__); sys.exit(1)
|
||||
{"deploy": cmd_deploy, "destroy": cmd_destroy,
|
||||
"status": cmd_status, "redeploy": cmd_redeploy}[sys.argv[1]]()
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"_comment": "Stateless Odoo 19 hot-reload dev stack \u2014 onebspace project. Uses source.type=dockerfile (inline Dockerfile build) so Easypanel builds the image in-place on every deploy. The Dockerfile.inline file is auto-generated from os-dev/Dockerfile + COPY files. Deploy: python3 deploy.py deploy",
|
||||
"name": "Odoo 19 Dev (live hot-reload)",
|
||||
"projectName": "onebspace",
|
||||
"schema": {
|
||||
"services": [
|
||||
{
|
||||
"type": "postgres",
|
||||
"data": {
|
||||
"serviceName": "odoo-dev-db",
|
||||
"image": "postgres:17",
|
||||
"databaseName": "postgres",
|
||||
"user": "odoo",
|
||||
"password": "CHANGE_ME_DB_PASSWORD",
|
||||
"exposedPort": 0,
|
||||
"pgWeb": {
|
||||
"enabled": true
|
||||
},
|
||||
"resources": {
|
||||
"memoryReservation": 256,
|
||||
"memoryLimit": 1024,
|
||||
"cpuReservation": 0.25,
|
||||
"cpuLimit": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "app",
|
||||
"data": {
|
||||
"serviceName": "odoo-dev",
|
||||
"source": {
|
||||
"type": "dockerfile",
|
||||
"dockerfile": "<see deploy.py \u2014 generated from os-dev/* files>"
|
||||
},
|
||||
"env": "HOST=$(PROJECT_NAME)_odoo-dev-db\nPORT=5432\nUSER=odoo\nPASSWORD=CHANGE_ME_DB_PASSWORD\nADMIN_PASSWD=admin",
|
||||
"ports": [
|
||||
{
|
||||
"protocol": "tcp",
|
||||
"published": 18069,
|
||||
"target": 8069
|
||||
}
|
||||
],
|
||||
"domains": [
|
||||
{
|
||||
"host": "$(EASYPANEL_DOMAIN)",
|
||||
"port": 8069,
|
||||
"https": true,
|
||||
"path": "/"
|
||||
}
|
||||
],
|
||||
"mounts": [
|
||||
{
|
||||
"type": "bind",
|
||||
"hostPath": "/root/Documents/trae_projects/one_b_os/os-dev",
|
||||
"mountPath": "/mnt/os-dev"
|
||||
},
|
||||
{
|
||||
"type": "volume",
|
||||
"name": "odoo-filestore",
|
||||
"mountPath": "/var/lib/odoo"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"content": "[options]\naddons_path = /mnt/os-dev/addons,/mnt/os-dev/odoo/addons,/usr/lib/python3/dist-packages/odoo/addons\ndata_dir = /var/lib/odoo\ndev_mode = reload,qweb,assets,xml,werkzeug\nxmlrpc_port = 8069\nworkers = 0\nadmin_passwd = admin\nlog_level = info\ndbfilter = .*\nlist_db = True\n",
|
||||
"mountPath": "/etc/odoo/odoo.conf"
|
||||
}
|
||||
],
|
||||
"deploy": {
|
||||
"command": "odoo --addons-path=/mnt/os-dev/addons,/mnt/os-dev/odoo/addons --dev=reload,qweb,assets,xml,werkzeug --db-filter=.* --limit-time-real=120 --log-level=info",
|
||||
"replicas": 1,
|
||||
"zeroDowntime": false
|
||||
},
|
||||
"resources": {
|
||||
"memoryReservation": 512,
|
||||
"memoryLimit": 2048,
|
||||
"cpuReservation": 0.5,
|
||||
"cpuLimit": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -v PASSWORD_FILE ]; then
|
||||
PASSWORD="$(< $PASSWORD_FILE)"
|
||||
fi
|
||||
|
||||
# set the postgres database host, port, user and password according to the environment
|
||||
# and pass them as arguments to the odoo process if not present in the config file
|
||||
: ${HOST:=${DB_PORT_5432_TCP_ADDR:='db'}}
|
||||
: ${PORT:=${DB_PORT_5432_TCP_PORT:=5432}}
|
||||
: ${USER:=${DB_ENV_POSTGRES_USER:=${POSTGRES_USER:='odoo'}}}
|
||||
: ${PASSWORD:=${DB_ENV_POSTGRES_PASSWORD:=${POSTGRES_PASSWORD:='odoo'}}}
|
||||
|
||||
DB_ARGS=()
|
||||
function check_config() {
|
||||
param="$1"
|
||||
value="$2"
|
||||
pg_var="${3:-}"
|
||||
if [ -n "$pg_var" ] && [[ -v "$pg_var" ]]; then
|
||||
return
|
||||
fi
|
||||
if ! grep -q -E "^\s*\b${param}\b\s*=" "$ODOO_RC" ; then
|
||||
DB_ARGS+=("--${param}")
|
||||
DB_ARGS+=("${value}")
|
||||
fi;
|
||||
}
|
||||
check_config "db_host" "$HOST" "PGHOST"
|
||||
check_config "db_port" "$PORT" "PGPORT"
|
||||
check_config "db_user" "$USER" "PGUSER"
|
||||
check_config "db_password" "$PASSWORD" "PGPASSWORD"
|
||||
|
||||
case "$1" in
|
||||
-- | odoo)
|
||||
shift
|
||||
if [[ "$1" == "scaffold" ]] ; then
|
||||
exec odoo "$@"
|
||||
else
|
||||
wait-for-psql.py ${DB_ARGS[@]} --timeout=30
|
||||
exec odoo "$@" "${DB_ARGS[@]}"
|
||||
fi
|
||||
;;
|
||||
-*)
|
||||
wait-for-psql.py ${DB_ARGS[@]} --timeout=30
|
||||
exec odoo "$@" "${DB_ARGS[@]}"
|
||||
;;
|
||||
*)
|
||||
exec "$@"
|
||||
esac
|
||||
|
||||
exit 1
|
||||
@@ -0,0 +1,43 @@
|
||||
; =============================================================================
|
||||
; Odoo 19 dev config — baked into the image. Overridable via bind mount.
|
||||
;
|
||||
; In dev: docker-compose mounts ./odoo.conf -> /etc/odoo/odoo.conf, so this
|
||||
; baked-in copy is shadowed. In CI / prod without a bind mount, this is the
|
||||
; config Odoo uses.
|
||||
; =============================================================================
|
||||
|
||||
[options]
|
||||
addons_path = /mnt/os-dev/addons,/mnt/os-dev/odoo/addons,/mnt/extra-addons
|
||||
data_dir = /var/lib/odoo
|
||||
|
||||
; ---- Hot reload (THE WHOLE POINT) ------------------------------------------
|
||||
; Full live-reload mode: Python (watchdog), QWeb templates, JS/CSS assets,
|
||||
; XML view files, and werkzeug in-browser tracebacks.
|
||||
dev_mode = reload,qweb,assets,xml,werkzeug
|
||||
|
||||
; ---- Network --------------------------------------------------------------
|
||||
xmlrpc_port = 8069
|
||||
; longpolling_port = 8072
|
||||
; proxy_mode = True
|
||||
|
||||
; ---- Database -------------------------------------------------------------
|
||||
; Connection params come from the entrypoint via env vars (HOST/PORT/USER/
|
||||
; PASSWORD). Don't set db_host/db_port/db_user/db_password here.
|
||||
dbfilter = .*
|
||||
list_db = True
|
||||
|
||||
; ---- Logging --------------------------------------------------------------
|
||||
log_level = info
|
||||
|
||||
; ---- Workers (single-worker = safest for hot reload) -----------------------
|
||||
workers = 0
|
||||
|
||||
; ---- Limits ---------------------------------------------------------------
|
||||
limit_memory_hard = 2684354560
|
||||
limit_memory_soft = 2147483648
|
||||
limit_request = 8192
|
||||
limit_time_cpu = 60
|
||||
limit_time_real = 120
|
||||
|
||||
; ---- Admin ----------------------------------------------------------------
|
||||
admin_passwd = admin
|
||||
@@ -0,0 +1,106 @@
|
||||
# The officially supported versions of the following packages are their
|
||||
# python3-* equivalent distributed in Ubuntu 24.04 and Debian 12
|
||||
asn1crypto==1.4.0 ; python_version < '3.11'
|
||||
asn1crypto==1.5.1 ; python_version >= '3.11'
|
||||
Babel==2.9.1 ; python_version < '3.11' # min version = 2.6.0 (Focal with security backports)
|
||||
Babel==2.10.3 ; python_version >= '3.11' and python_version < '3.13'
|
||||
Babel==2.17.0 ; python_version >= '3.13'
|
||||
cbor2==5.4.2.post1 ; python_version < '3.11' # (Jammy)
|
||||
cbor2==5.4.6 ; python_version >= '3.11' and python_version < '3.12' # (Bookworm)
|
||||
cbor2==5.6.2 ; python_version >= '3.12'
|
||||
chardet==4.0.0 ; python_version < '3.11' # (Jammy)
|
||||
chardet==5.2.0 ; python_version >= '3.11'
|
||||
cryptography==3.4.8; python_version < '3.12' # incompatibility between pyopenssl 19.0.0 and cryptography>=37.0.0
|
||||
cryptography==42.0.8 ; python_version >= '3.12' # (Noble) min 41.0.7, pinning 42.0.8 for security fixes
|
||||
docutils==0.17 ; python_version < '3.11' # (Jammy)
|
||||
docutils==0.20.1 ; python_version >= '3.11'
|
||||
freezegun==1.1.0 ; python_version < '3.11' # (Jammy)
|
||||
freezegun==1.2.1 ; python_version >= '3.11' and python_version < '3.13'
|
||||
freezegun==1.5.1 ; python_version >= '3.13'
|
||||
geoip2==2.9.0
|
||||
gevent==21.8.0 ; sys_platform != 'win32' and python_version == '3.10' # (Jammy)
|
||||
gevent==22.10.2; sys_platform != 'win32' and python_version > '3.10' and python_version < '3.12'
|
||||
gevent==24.2.1 ; sys_platform != 'win32' and python_version >= '3.12' and python_version < '3.13' # (Noble)
|
||||
gevent==24.11.1 ; sys_platform != 'win32' and python_version >= '3.13' # (Trixie)
|
||||
greenlet==1.1.2 ; sys_platform != 'win32' and python_version == '3.10' # (Jammy)
|
||||
greenlet==2.0.2 ; sys_platform != 'win32' and python_version > '3.10' and python_version < '3.12'
|
||||
greenlet==3.0.3 ; sys_platform != 'win32' and python_version >= '3.12' and python_version < '3.13' # (Noble)
|
||||
greenlet==3.1.1 ; sys_platform != 'win32' and python_version >= '3.13' and python_version < '3.14' # (Trixie)
|
||||
greenlet==3.3.2 ; sys_platform != 'win32' and python_version >= '3.14' # (Resolute)
|
||||
idna==2.10 ; python_version < '3.12' # requests 2.25.1 depends on idna<3 and >=2.5
|
||||
idna==3.6 ; python_version >= '3.12'
|
||||
Jinja2==3.0.3 ; python_version <= '3.10'
|
||||
Jinja2==3.1.2 ; python_version > '3.10'
|
||||
libsass==0.20.1 ; python_version < '3.11'
|
||||
libsass==0.22.0 ; python_version >= '3.11' # (Noble) Mostly to have a wheel package
|
||||
lxml==4.8.0 ; python_version <= '3.10'
|
||||
lxml==4.9.3 ; python_version > '3.10' and python_version < '3.12' # min 4.9.2, pinning 4.9.3 because of missing wheels for darwin in 4.9.3
|
||||
lxml==5.2.1; python_version >= '3.12' and python_version < '3.13' # (Noble - removed html clean)
|
||||
lxml==5.4.0; python_version >= '3.13' and python_version < '3.14' # (Trixie)
|
||||
lxml==6.0.2; python_version >= '3.14' # (Resolute)
|
||||
lxml-html-clean; python_version >= '3.12' # (Noble - removed from lxml, unpinned for futur security patches)
|
||||
MarkupSafe==2.0.1 ; python_version <= '3.10'
|
||||
MarkupSafe==2.1.2 ; python_version > '3.10' and python_version < '3.12'
|
||||
MarkupSafe==2.1.5 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
||||
num2words==0.5.10 ; python_version < '3.12' # (Jammy / Bookworm)
|
||||
num2words==0.5.13 ; python_version >= '3.12'
|
||||
ofxparse==0.21
|
||||
openpyxl==3.0.9 ; python_version < '3.12'
|
||||
openpyxl==3.1.2 ; python_version >= '3.12'
|
||||
passlib==1.7.4 # min version = 1.7.2 (Focal with security backports)
|
||||
Pillow==9.0.1 ; python_version <= '3.10'
|
||||
Pillow==9.4.0 ; python_version > '3.10' and python_version < '3.12'
|
||||
Pillow==10.2.0 ; python_version >= '3.12' and python_version < '3.13' # (Noble) Mostly to have a wheel package
|
||||
Pillow==11.1.0 ; python_version >= '3.13' and python_version < '3.14' # (Trixie)
|
||||
Pillow==12.1.1 ; python_version >= '3.14' # (Resolute)
|
||||
polib==1.1.1
|
||||
psutil==5.9.0 ; python_version <= '3.10'
|
||||
psutil==5.9.4 ; python_version > '3.10' and python_version < '3.12'
|
||||
psutil==5.9.8 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
||||
psycopg2==2.9.2 ; python_version == '3.10' # (Jammy)
|
||||
psycopg2==2.9.5 ; python_version == '3.11'
|
||||
psycopg2==2.9.9 ; python_version >= '3.12' and python_version < '3.13' # (Noble)
|
||||
psycopg2==2.9.10 ; python_version >= '3.13' # (Trixie)
|
||||
pyopenssl==21.0.0 ; python_version < '3.12'
|
||||
pyopenssl==24.1.0 ; python_version >= '3.12' # (Noble) min 23.2.0, pinned for compatibility with cryptography==42.0.8 and security patches
|
||||
PyPDF2==1.26.0 ; python_version <= '3.10'
|
||||
PyPDF2==2.12.1 ; python_version > '3.10' and python_version < '3.13' # (Noble and below)
|
||||
PyPDF==5.4.0 ; python_version >= '3.13' # (Trixie)
|
||||
pypiwin32 ; sys_platform == 'win32'
|
||||
pyserial==3.5
|
||||
python-dateutil==2.8.1 ; python_version < '3.11'
|
||||
python-dateutil==2.8.2 ; python_version >= '3.11'
|
||||
python-magic==0.4.24; sys_platform != 'win32' and python_version < '3.12' # (jammy)
|
||||
python-magic==0.4.27; sys_platform != 'win32' and python_version >= '3.12' # (noble)
|
||||
python-ldap==3.4.0 ; sys_platform != 'win32' and python_version < '3.12' # min version = 3.2.0 (Focal with security backports)
|
||||
python-ldap==3.4.4 ; sys_platform != 'win32' and python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
||||
python-stdnum==1.17 ; python_version < '3.11' # (jammy)
|
||||
python-stdnum==1.19 ; python_version >= '3.11' and python_version < '3.14'
|
||||
python-stdnum==2.2 ; python_version >= '3.14' # (Resolute)
|
||||
pytz # no version pinning to avoid OS perturbations
|
||||
pyusb==1.2.1
|
||||
qrcode==7.3.1 ; python_version < '3.11' # (jammy)
|
||||
qrcode==7.4.2 ; python_version >= '3.11'
|
||||
reportlab==3.6.8 ; python_version <= '3.10'
|
||||
reportlab==3.6.12 ; python_version > '3.10' and python_version < '3.12'
|
||||
reportlab==4.1.0 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
||||
requests==2.25.1 ; python_version < '3.11' # versions < 2.25 aren't compatible w/ urllib3 1.26. Bullseye = 2.25.1. min version = 2.22.0 (Focal)
|
||||
requests==2.31.0 ; python_version >= '3.11' # (Noble)
|
||||
rjsmin==1.1.0 ; python_version < '3.11' # (jammy)
|
||||
rjsmin==1.2.0 ; python_version >= '3.11'
|
||||
rl-renderPM==4.0.3 ; sys_platform == 'win32' and python_version >= '3.12' # Needed by reportlab 4.1.0 but included in deb package
|
||||
urllib3==1.26.5 ; python_version < '3.12' # indirect / min version = 1.25.8 (Focal with security backports)
|
||||
urllib3==2.0.7 ; python_version >= '3.12' # (Noble) Compatibility with cryptography
|
||||
vobject==0.9.6.1 ; python_version < '3.14'
|
||||
vobject==0.9.9 ; python_version >= '3.14' # (Resolute)
|
||||
Werkzeug==2.0.2 ; python_version <= '3.10'
|
||||
Werkzeug==2.2.2 ; python_version > '3.10' and python_version < '3.12'
|
||||
Werkzeug==3.0.1 ; python_version >= '3.12' # (Noble) Avoid deprecation warnings
|
||||
xlrd==1.2.0 ; python_version < '3.12' # (jammy)
|
||||
xlrd==2.0.1 ; python_version >= '3.12'
|
||||
XlsxWriter==3.0.2 ; python_version < '3.12' # (jammy)
|
||||
XlsxWriter==3.1.9 ; python_version >= '3.12'
|
||||
xlwt==1.3.0
|
||||
zeep==4.1.0 ; python_version < '3.11' # (jammy)
|
||||
zeep==4.2.1 ; python_version >= '3.11' and python_version < '3.13'
|
||||
zeep==4.3.1 ; python_version >= '3.13'
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import psycopg2
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
arg_parser.add_argument('--db_host')
|
||||
arg_parser.add_argument('--db_port')
|
||||
arg_parser.add_argument('--db_user')
|
||||
arg_parser.add_argument('--db_password')
|
||||
arg_parser.add_argument('--timeout', type=int, default=5)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
config_options = {}
|
||||
rc_file_path = os.environ.get('ODOO_RC')
|
||||
if rc_file_path:
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(rc_file_path)
|
||||
config_options = { k:v for k,v in config.items('options')}
|
||||
|
||||
db_host = config_options.get('db_host') or args.db_host or os.environ.get('PGHOST')
|
||||
db_port = config_options.get('db_port') or args.db_port or os.environ.get('PGPORT')
|
||||
db_user = config_options.get('db_user') or args.db_user or os.environ.get('PGUSER')
|
||||
db_password = config_options.get('db_password') or args.db_password or os.environ.get('PGPASSWORD')
|
||||
|
||||
params = {'db_host': db_host, 'db_port': db_port, 'db_user': db_user, 'db_password': db_password}
|
||||
missing_params = [k for k, v in params.items() if v is None]
|
||||
if missing_params:
|
||||
print(f"Missing required database parameters: {', '.join(missing_params)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
start_time = time.time()
|
||||
while (time.time() - start_time) < args.timeout:
|
||||
try:
|
||||
conn = psycopg2.connect(user=db_user, host=db_host, port=db_port, password=db_password, dbname='postgres')
|
||||
error = ''
|
||||
break
|
||||
except psycopg2.OperationalError as e:
|
||||
error = e
|
||||
else:
|
||||
conn.close()
|
||||
time.sleep(1)
|
||||
|
||||
if error:
|
||||
print("Database connection failure: %s" % error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in new issue
Block a user