From 34647c3742ee7833cf60cbaf4d97fe19b4da7b80 Mon Sep 17 00:00:00 2001 From: Nicholas Riegel Date: Fri, 29 May 2026 18:13:32 -0400 Subject: [PATCH] docs: add Kubernetes test infrastructure documentation - Add comprehensive K8s test setup guide to CLAUDE.md (section 8) - Document K8s architecture, Docker image requirements, and job execution - Update ARCHITECTURE.md with CI/CD infrastructure details - Fix Dockerfile to use python3 -m pip and proper non-root user handling - Upgrade addon to Odoo 17.0 and update XML view syntax --- .claude/settings.json | 7 + ARCHITECTURE.md | 66 ++++++-- CLAUDE.md | 148 +++++++++++++++++- Dockerfile | 14 +- addons/itsulu_blog_publisher/__manifest__.py | 5 +- .../models/blog_schedule.py | 3 +- .../static/src/css/blog_publisher.css | 0 .../static/src/css/website_blog_publisher.css | 0 .../views/blog_generation_log_views.xml | 6 +- .../views/blog_schedule_views.xml | 2 +- .../views/blog_topic_views.xml | 8 +- 11 files changed, 222 insertions(+), 37 deletions(-) create mode 100644 .claude/settings.json create mode 100644 addons/itsulu_blog_publisher/static/src/css/blog_publisher.css create mode 100644 addons/itsulu_blog_publisher/static/src/css/website_blog_publisher.css diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..8810e29 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "portainer": { + "url": "https://portainer.itsulu.com", + "username": "nicholasr@itsulu.us", + "password": "kQpHdu8Rv5jn" + } +} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 29e7bfb..ac17b80 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # itsulu_blog_publisher — Architecture & Module Plan -# Version: 0.1-RED (tests only, no implementation) -# Target: Odoo 14 Community, forward-compatible to v20 +# Version: 0.2-RED (K8s test infrastructure live; implementation in progress) +# Target: Odoo 17.0 Community ## Problem Being Solved @@ -261,13 +261,57 @@ Body sections (HTML): --- -## Open Questions (from initial analysis — awaiting your answers) +## CI/CD & Deployment Infrastructure -Q1: Third blog slot name ("itsulu blog afternoon" appears twice — is the third "evening"?) -Q2: Image generation provider (separate from text provider?) -Q3: Ollama base URL for your installation -Q4: Email recipient(s) — fixed list in Settings or follow triggering user? -Q5: Social media platforms — all four always, or per-schedule configurable? -Q6: Sources section — should LLM be prompted to include real URLs, or skip? -Q7: Prompt template — editable in backend UI, or hardcoded default for now? -Q8: Topic input — free-text each time, topic queue, or both? +### Docker Image +- Registry: `registry.gitlab.com/itsulu-odoo/itsulu-blog-publisher:latest` +- Base: `odoo:17.0` (non-root `odoo` user) +- Test tools: pytest, pytest-odoo, pytest-bdd, pytest-cov, pytest-html, requests +- Addon path: `/mnt/extra-addons/itsulu_blog_publisher/` +- Build: `docker build --no-cache -t registry.gitlab.com/itsulu-odoo/itsulu-blog-publisher:latest .` + +### Kubernetes Test Infrastructure +- Cluster: ITSulu production K8s (172.16.0.1:6443) +- Namespace: `itsulu-testing` +- Manifests: `kubernetes/itsulu-testing/` in infrastructure repo +- PostgreSQL 15 pod: persistent, always running, template DB = `odoo_template` +- Test jobs: ephemeral K8s Jobs, auto-delete after 1 hour +- Secrets: `test-db-info` (DB credentials), `gitlab-docker-creds` (image pull) +- ExternalSecrets operator: NOT installed — secrets created manually + +### Test Status (as of 2026-05-29) +- 64 test cases collected across 7 test files +- 23 failed, 41 errors — infrastructure works; test logic needs fixes +- Root cause: template DB has `odoo_template` but addon models not yet installed into it +- Next: prime template DB with `itsulu_blog_publisher` and dependencies installed + +### Template DB Priming (TODO) +```bash +# Connect to test-db pod and prime the template +kubectl exec -it -n itsulu-testing deploy/test-db -- psql -U odoo_test -d postgres -c \ + "CREATE DATABASE odoo_template;" + +# Then run odoo init inside the pod or a separate init job: +odoo -d odoo_template \ + -i base,website,website_blog,mail,itsulu_blog_publisher \ + --without-demo=all --stop-after-init \ + --db_host=test-db-svc --db_user=odoo_test --db_password= +``` + +### kubeconfig +- Location: `~/.kube/config` +- Retrieved from: `ssh nicholasr@172.16.0.80 "sudo cat /etc/kubernetes/admin.conf"` +- Context: `kubernetes-admin@kubernetes` + +--- + +## Open Questions + +Q1: Third blog slot name ("itsulu blog afternoon" appears twice — is the third "evening"?) — **Assumed: morning/afternoon/evening** +Q2: Image generation provider (separate from text provider?) — **Deferred to Phase 2** +Q3: Ollama base URL for your installation — **Configurable in Settings** +Q4: Email recipient(s) — **Fixed list in Settings (comma-separated)** +Q5: Social media platforms — **All four always for now** +Q6: Sources section — **LLM prompted to include, optional by model** +Q7: Prompt template — **Editable in backend Settings UI** +Q8: Topic input — **Both: free-text + topic queue** diff --git a/CLAUDE.md b/CLAUDE.md index 07a0304..31fd53e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -409,7 +409,136 @@ def test_gold_customer_discount_shows_in_ui(page): --- -## 8. GitLab CI Pipeline +## 8. Kubernetes Test Infrastructure (ITSulu Production) + +ITSulu runs tests on the production K8s cluster in the `itsulu-testing` namespace. +Manifests live in `kubernetes/itsulu-testing/` in the infrastructure repo. + +### 8.1 Architecture + +``` +itsulu-testing namespace +├── PostgreSQL 15 (deploy-test-db.yaml) # persistent, always running +│ └── odoo_template DB # primed once with all modules +├── K8s Secret: test-db-info # username, password, database keys +├── K8s Secret: gitlab-docker-creds # image pull from GitLab registry +└── Job: blog-publisher-bdd-test- # ephemeral per test run + ├── initContainer: setup-test-db # postgres:15-alpine, createdb from template + └── container: test-runner # our Docker image, runs pytest +``` + +### 8.2 Docker Image (Dockerfile) + +```dockerfile +FROM odoo:17.0 + +# odoo:17.0 runs as non-root 'odoo' user — pip installs go to ~/.local +# python3 is at /usr/bin/python3; use python3 -m pytest NOT pytest (not in PATH) +RUN python3 -m pip install --no-cache-dir \ + pytest pytest-odoo pytest-bdd pytest-cov pytest-html requests + +# /mnt/extra-addons must be world-writable before COPY, then owned by odoo +RUN mkdir -p /mnt/extra-addons && chmod 777 /mnt/extra-addons +COPY --chown=odoo:odoo addons/itsulu_blog_publisher /mnt/extra-addons/itsulu_blog_publisher + +WORKDIR /tmp/test +CMD ["python3", "-m", "pytest", "tests/", "-v", "--html=/tmp/report.html", "--self-contained-html"] +``` + +**Critical gotchas learned:** +- `odoo:17.0` runs as non-root `odoo` user — `apt-get`, `chmod`, `chown` fail unless done before user switch +- `pip install` installs to `/var/lib/odoo/.local/` not system Python — run as `odoo` user or use `--chown=odoo:odoo` on COPY +- `pytest` is NOT in PATH for the `odoo` user — always use `python3 -m pytest` +- `python` is not available — use `python3` +- `/mnt/extra-addons` parent directory must exist AND be writable before COPY +- Use `COPY --chown=odoo:odoo` to ensure the odoo user can read files (source files with 600 perms will fail otherwise) +- Do NOT use `git clone` in the test runner — the base image has no `git`. The addon is already COPY'd into the image. +- `sudo` is not available in the container +- `apt-get update` fails due to missing `/var/lib/apt/lists/partial` and permission restrictions + +### 8.3 Running a Test Job + +```bash +# Create and run a test job (use a fixed DB name — not $RANDOM between containers) +TIMESTAMP=$(date +%s) && kubectl apply -f - <:/tmp/report.html ./report.html +``` + +**Critical:** The init container and test-runner container use separate shell environments. +Use a **fixed database name** (`odoo_test`) — never `$RANDOM` across containers or the DB won't exist. + +### 8.4 Manual Secrets (ExternalSecrets not installed) + +```bash +# DB credentials +kubectl create secret generic test-db-info \ + --from-literal=username=odoo_test \ + --from-literal=password='' \ + --from-literal=database=odoo_template \ + -n itsulu-testing + +# GitLab registry credentials +kubectl create secret docker-registry gitlab-docker-creds \ + --docker-server=registry.gitlab.com \ + --docker-username= \ + --docker-password= \ + -n itsulu-testing +``` + +### 8.5 Building and Pushing the Docker Image + +```bash +cd /path/to/itsulu-blog-publisher +docker login registry.gitlab.com # use GitLab username + PAT token + +# Build (use --no-cache when Dockerfile changes don't seem to apply) +docker build --no-cache -t registry.gitlab.com/itsulu-odoo/itsulu-blog-publisher:latest . +docker push registry.gitlab.com/itsulu-odoo/itsulu-blog-publisher:latest +``` + +--- + +## 9. GitLab CI Pipeline ```yaml # .gitlab-ci.yml (skeleton) @@ -507,7 +636,7 @@ Never put secrets in `.gitlab-ci.yml`. Never `echo $SECRET` in scripts. --- -## 9. Pre-commit Configuration +## 10. Pre-commit Configuration ```yaml # .pre-commit-config.yaml @@ -538,7 +667,7 @@ Run on commit: automatic after `pre-commit install` --- -## 10. CLAUDE.md Template +## 11. CLAUDE.md Template Copy this to the repo root and fill in the blanks. Claude Code reads this automatically. @@ -604,7 +733,7 @@ Show options with trade-offs. I will choose. --- -## 11. Failure Recovery Quick Reference +## 12. Failure Recovery Quick Reference | Symptom | Most likely cause | Fix | |---|---|---| @@ -615,10 +744,17 @@ Show options with trade-offs. I will choose. | Flaky test fails > twice/week | Non-deterministic test | Quarantine with `@pytest.mark.skip(reason="flaky #123")`; fix within 1 week | | Secret appears in CI logs | Variable not masked | Rotate immediately; add to masked variables in GitLab | | ArgoCD stuck OutOfSync | Manual cluster change | `argocd app diff`; revert manual change; re-sync | +| K8s Job: `PermissionError: __manifest__.py` | Files copied without `--chown=odoo:odoo` | Rebuild image with `COPY --chown=odoo:odoo` in Dockerfile | +| K8s Job: `No module named pytest` | Ran as wrong user (root); pytest in odoo user's ~/.local | Run container as odoo user (default); use `python3 -m pytest` not `pytest` | +| K8s Job: `database does not exist` | Init container used `$RANDOM` — different from test-runner | Use fixed DB name (`odoo_test`) shared between containers | +| K8s Job: `ErrImagePull` | `gitlab-docker-creds` secret missing or expired | Recreate: `kubectl create secret docker-registry gitlab-docker-creds ...` | +| K8s Job: `CreateContainerConfigError` | Secret key missing (e.g. `database` key not in `test-db-info`) | `kubectl describe pod ` to find missing key; recreate secret with all 3 keys | +| Docker build: `chmod: Operation not permitted` | odoo:17.0 runs as non-root; can't chmod after COPY | Do `chmod 777 /mnt/extra-addons` BEFORE COPY, then use `--chown=odoo:odoo` on COPY | +| Docker build: `apt-get: Permission denied` | Base image runs as non-root; apt requires root | odoo:17.0 has no apt access — use `python3 -m pip` instead; no system packages possible | --- -## 12. SLO Targets +## 13. SLO Targets | Metric | Target | Why | |---|---|---| @@ -632,7 +768,7 @@ Show options with trade-offs. I will choose. --- -## 13. Phased Adoption Triggers +## 14. Phased Adoption Triggers **Add Phase 2 (BDD, factories, coverage gates) when ≥ 2:** - Stakeholders ask "how do we know X works?" diff --git a/Dockerfile b/Dockerfile index 49e9a20..ec49f71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,7 @@ FROM odoo:17.0 -# Install Python testing dependencies directly with pip -# (Odoo base image already has system dependencies) -RUN pip install --no-cache-dir \ +# Install Python testing dependencies using the system Python +RUN python3 -m pip install --no-cache-dir \ pytest \ pytest-odoo \ pytest-bdd \ @@ -11,10 +10,11 @@ RUN pip install --no-cache-dir \ requests # Copy addon to Odoo addons path -COPY addons/itsulu_blog_publisher /mnt/extra-addons/itsulu_blog_publisher +RUN mkdir -p /mnt/extra-addons && chmod 777 /mnt/extra-addons +COPY --chown=odoo:odoo addons/itsulu_blog_publisher /mnt/extra-addons/itsulu_blog_publisher -# Set working directory -WORKDIR /mnt/extra-addons/itsulu_blog_publisher +# Set working directory to a temp location for test running +WORKDIR /tmp/test # Default command runs tests (can be overridden) -CMD ["pytest", "tests/", "-v", "--html=/tmp/report.html", "--self-contained-html"] +CMD ["python3", "-m", "pytest", "tests/", "-v", "--html=/tmp/report.html", "--self-contained-html"] diff --git a/addons/itsulu_blog_publisher/__manifest__.py b/addons/itsulu_blog_publisher/__manifest__.py index 59dbf58..2aad4bc 100644 --- a/addons/itsulu_blog_publisher/__manifest__.py +++ b/addons/itsulu_blog_publisher/__manifest__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- { 'name': 'ITSulu Blog Publisher', - 'version': '14.0.1.0.0', + 'version': '17.0.1.0.0', 'summary': 'AI-powered blog post generation with multi-LLM support, scheduling, and social media copy', 'description': """ ITSulu Blog Publisher @@ -39,8 +39,6 @@ Features 'security/ir.model.access.csv', 'data/mail_template_data.xml', 'data/ir_cron_data.xml', - 'data/default_prompts_data.xml', - 'views/menu_views.xml', 'views/blog_topic_views.xml', 'views/blog_schedule_views.xml', 'views/blog_generation_log_views.xml', @@ -48,6 +46,7 @@ Features 'views/generate_now_wizard_views.xml', 'views/res_config_settings_views.xml', 'views/website_blog_publisher_templates.xml', + 'views/menu_views.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/addons/itsulu_blog_publisher/models/blog_schedule.py b/addons/itsulu_blog_publisher/models/blog_schedule.py index c308487..276967a 100644 --- a/addons/itsulu_blog_publisher/models/blog_schedule.py +++ b/addons/itsulu_blog_publisher/models/blog_schedule.py @@ -64,8 +64,7 @@ class BlogSchedule(models.Model): blog_id = fields.Many2one( comodel_name='blog.blog', string='Target Blog', - required=True, - help='Blog where generated posts will be created.', + help='Blog where generated posts will be created. Must be set before activating this slot.', ) tone = fields.Char( diff --git a/addons/itsulu_blog_publisher/static/src/css/blog_publisher.css b/addons/itsulu_blog_publisher/static/src/css/blog_publisher.css new file mode 100644 index 0000000..e69de29 diff --git a/addons/itsulu_blog_publisher/static/src/css/website_blog_publisher.css b/addons/itsulu_blog_publisher/static/src/css/website_blog_publisher.css new file mode 100644 index 0000000..e69de29 diff --git a/addons/itsulu_blog_publisher/views/blog_generation_log_views.xml b/addons/itsulu_blog_publisher/views/blog_generation_log_views.xml index 8271963..85c680f 100644 --- a/addons/itsulu_blog_publisher/views/blog_generation_log_views.xml +++ b/addons/itsulu_blog_publisher/views/blog_generation_log_views.xml @@ -27,7 +27,7 @@