Skip to content

Render Domain routes and serve static files from the release (S-003-2..7, S-003-13..15) - #177

Merged
shipmill-romamo[bot] merged 2 commits into
mainfrom
feat/162-domain-routes-render
Oct 30, 2026
Merged

shipmill-romamo[bot] merged 2 commits into
mainfrom
feat/162-domain-routes-render

Conversation

@shipmill-romamo

Copy link
Copy Markdown
Contributor

Summary

Renders a Domain's validated spec.routes (validation landed in #168) in cloudfall_nginx_site, the last build issue of spec S-003.

  • domain-site.conf.j2: one location per route, longest path first then by path, so the declared order doesn't change the bytes. A proxy route proxies with the full request path (proxy_pass with no URI part); a static route is alias <installRoot>/current/<directory>/; plus try_files $uri <path><spaFallback>; when it declares a fallback. Routes render where the single proxy location did (80 without active TLS, 443 with it); the port 80 redirect and the ACME location are untouched. A Domain with only proxy.upstream goes through the same macro with / and renders byte-identical to origin/main
  • Engine inventory: each static route gains servedDirectory (<installRoot>/current/<directory>) and traverseDirectories (application root down to the install root, plus releases/)
  • Role: accepts routes in its argument spec (each route exactly one of upstream / static), asserts "upstream or routes", and, for a Domain with static routes, installs acl and grants user:www-data:--x on the traverse directories (idempotent through getfacl). The worker joins no group, so shared/env/<component>.env (root:<app> 0640) stays unreadable to it. Because the vhost reads through current, a deploy or rollback switches what is served with no Nginx reload
  • domains.yml: the early check now refuses only a Domain with neither proxy.upstream nor routes (task renamed accordingly)
  • Spec 003 set to built with one Verification line per criterion; engine/README.md and a changelog fragment

Plan

  • Serves: G-3
  • Files: engine/ansible/roles/cloudfall_nginx_site/{templates/domain-site.conf.j2,tasks/main.yml,tasks/static_access.yml,meta/argument_specs.yml,defaults/main.yml}, engine/ansible/playbooks/domains.yml, engine/src/cloudfall_engine/ansible_inventory.py, engine/tests/golden/nginx-site/*.conf (snapshot of origin/main's render), engine/tests/test_nginx_site_routes.py, engine/tests/test_domains_playbook.py, sdk/tests/test_domains_apply.py (the early check's renamed task), engine/README.md, docs/specs/003-domain-routes.md, changelog.d/162-domain-routes-render.md
  • Order: snapshot origin/main's upstream-only vhost (TLS on and off, nginx 1.22.1 and 1.26.3) as golden files before touching the template; resolve each static route's served directory and traverse path in the engine inventory; render routes longest path first in the template, leaving the proxy.upstream path byte-identical; accept routes in the role's argument spec and asserts and in the domains playbook's early check; grant the nginx worker u:www-data:x along each static route's install root; spec status built with its Verification lines
  • Proof: S-003-2, S-003-3, S-003-4, S-003-5, S-003-6, S-003-7, S-003-13, S-003-14, S-003-15, each in a test that names its id; specs.py check and specs.py coverage --spec 003

Test plan

  • uv run ruff check ., uv run mypy, ansible-lint engine/ansible (with ANSIBLE_CONFIG=engine/ansible/ansible.cfg, as CI): clean
  • uv run pytest on the touched and neighbouring files (engine/tests/test_nginx_site_*.py, test_domains_*.py, test_ansible_inventory.py, test_examples_render_unchanged.py, sdk/tests/test_domain_routes.py, sdk/tests/test_domains_apply.py): 122 passed. The full suite is left to CI
  • New engine/tests/test_nginx_site_routes.py renders the vhost with Ansible's own template module on localhost from the engine inventory, compares the upstream-only render with engine/tests/golden/nginx-site/ (captured from origin/main's template before the change), works out request answers with a small model of Nginx's prefix match, alias, root and try_files over a release tree in tmp_path, runs rollback.yml's symlink switch for real, and runs the ACL tasks with stand-in getfacl/setfacl
  • The role's argument spec was checked by including the role on localhost: a routes Domain passes it, a route with both upstream and static is refused
  • specs.py check and specs.py coverage --spec 003: pass
  • Not exercised here (no Nginx, Docker or Linux host available): nginx -t on 1.22.1 and 1.26.3, real GET answers, a real POSIX ACL and www-data reading the files. They are left for a live proving run and said so in the spec's Verification

Fixes #162
Fixes #90

🤖 Generated with Claude Code

…..7, S-003-13..15)

A Domain's validated spec.routes now render in cloudfall_nginx_site: one
location per route, longest path first so the declared order doesn't change
the bytes. A proxy route keeps the full request path; a static route aliases
<installRoot>/current/<directory>, with try_files to its spaFallback, so a
deploy or rollback switches what it serves without an Nginx reload. A Domain
with only proxy.upstream renders byte-identical to origin/main, checked
against golden files captured before the change.

The engine inventory resolves each static route's served directory and the
directories from the application root to releases/ that bootstrap creates
0750; the role grants the Nginx worker traverse-only access there with a
POSIX ACL (user:www-data:--x) and never adds it to the application's group,
so shared/env/<component>.env stays unreadable to it. The domains playbook's
early check refuses only a Domain with neither proxy.upstream nor routes.

Spec 003 is built: every criterion has a Verification line.

Fixes #162
Fixes #90

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a / route, Nginx served its default root for any unmatched path:
on Debian /usr/share/nginx/html, whose welcome page answers 200, so a Domain
health check on / could pass against nothing the Domain declares. The routes
render a closing location / { return 404; } in that case; a Domain with only
proxy.upstream renders unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shipmill-romamo

Copy link
Copy Markdown
Contributor Author

@romamo Decided without waiting: a Domain whose routes have no / route answers 404 for a path no route matches.

Why: spec S-003 doesn't say; without it nginx served Debian's welcome page with 200, so a Domain health check on / could pass against nothing the Domain declares (Q-1). The review added location / { return 404; } in that case only; a Domain with only proxy.upstream still renders byte-identical (S-003-7).
Not taken: leaving the fall-through to nginx's default root.

Reply with a comment starting with veto before 2026-10-31 15:50 UTC to reverse it, adding what you want instead if you like. A later veto still counts and gets a follow-up.

@shipmill-romamo shipmill-romamo Bot added the decided An agent decided at notify; the maintainer may veto label Oct 30, 2026
@shipmill-romamo
shipmill-romamo Bot merged commit 70a167f into main Oct 30, 2026
1 check passed
@shipmill-romamo
shipmill-romamo Bot deleted the feat/162-domain-routes-render branch October 30, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

decided An agent decided at notify; the maintainer may veto

Projects

None yet

0 participants