Skip to content

Read one Domain on its proxy server with cloudfall domains observe (#118) - #142

Merged
shipmill-romamo[bot] merged 1 commit into
mainfrom
feat/118-domains-observe
Oct 27, 2026
Merged

shipmill-romamo[bot] merged 1 commit into
mainfrom
feat/118-domains-observe

Conversation

@shipmill-romamo

Copy link
Copy Markdown
Contributor

Summary

Implements the last open part of S-004 (docs/specs/004-domains-apply.md): criteria S-004-4, 14, 15 and 16. With #116 and #117 merged, the spec is now status: built, and its Issues and Verification sections are filled in.

  • New cloudfall domains observe <domain> [--deployments DIR]. It validates the project, refuses an unknown id with PRECONDITION (lifecycle_domain_missing) before the inventory render, loads <deployments>/<domain>.json when there is one (strict schema validation, CONFIG_INVALID when malformed), then runs the new engine/ansible/playbooks/domains-observe.yml with --limit <proxy.server>
  • It answers status, action: "domains-observe", domain, server, vhost (path, present, sha256), receipt (configuration_sha256, deployed_at, matches, or null when there is no receipt), certificate (path, present, not_after as UTC RFC 3339), and certbot_timer (enabled, active). Keys come out snake_case. A missing vhost or certificate is a reading and exits 0. An unreachable server exits ENGINE_STEP_FAILED, and so does a reading that breaks the engine contract (wrong domain or server, a non-boolean flag, a hash with no file, an unreadable date)
  • domains-observe.yml only reads on the server: set_fact, assert, two stats (the vhost hashed with sha256 through links, as the site role hashes what it replaces), openssl x509 -noout -enddate, and systemctl is-enabled|is-active certbot.timer. Each command has changed_when: false and check_mode: false, so the playbook reads the same under --check. Its one write is the reading itself, handed back to the controller at cloudfall_domains_observe_reading (delegate_to: localhost, become: false), as backup.yml does with its receipt. gather_facts: false
  • No file changes in the project: the CLI renders the inventory and receives the reading in a tempfile.TemporaryDirectory outside the project. So domains observe takes no --inventory-file
  • MCP: domains.apply and domains.observe join PROJECT_COMMANDS, so cloudfall mcp serve lists domains_apply and domains_observe. domains_apply keeps the CLI's yes gate (the same Flag(confirm=True) as deploy): without yes it answers the plan. The project instructions name both tools
  • domains apply already refused an unknown Domain (Add cloudfall domains apply (S-004-1, 3, 5..10) #117). Its test is renamed test_s004_4_...
  • Catalog: a domains observe read entry, the keys server, vhost, certificate and certbotTimer, and a fixed shape for an object receipt, so null stays null instead of {}. _key_schema now applies a fixed shape only where the key is an object, so receipt stays a path string in every other command. The playbook run summary names domains-observe as read-only beside inspect
  • service_evidence.load_deployment_receipt loads one receipt file. load_deployment_receipts now uses the same per-file builder, and its behaviour is unchanged
  • Docs: sdk/README.md and engine/README.md describe the command and the playbook. Changelog fragment changelog.d/118-domains-observe.md

Fixes #118
Part of #87

Test plan

  • sdk/tests/test_domains_observe.py:
    • S-004-14: the real domains-observe.yml runs under ansible-playbook against the rendered inventory of the examples, with every host on a local connection, the proxy server's files under tmp_path, and stand-in openssl/systemctl scripts first on PATH. Only h2 appears in the recap. The vhost hash, the certificate expiry and the timer state come back as read. Project and server files are unchanged by content and mtime. A missing vhost and certificate read as present: false with exit 0. A static check holds every server task to stat/set_fact/assert or a command with changed_when: false, and the one copy to the controller reading path. Through cloudfall.cli.main, with a stand-in engine playbook, the payload comes out snake_case and the project tree is unchanged, because the inventory is rendered outside it
    • S-004-15: matches: true while the vhost is unchanged, false once it is edited or removed, and receipt: null with no receipt. Through the CLI, the receipt object's keys are snake_case
    • S-004-4: the CLI exits PRECONDITION/lifecycle_domain_missing and creates no tmp/. At the lifecycle level, no step starts
    • The engine contract: render, then domains-observe.yml with --limit h2 and the two extra vars. Malformed readings exit lifecycle_execution_failed. A server address under .invalid exits ENGINE_STEP_FAILED
  • sdk/tests/test_domains_apply.py: S-004-4 for apply (renamed, plan and --yes)
  • sdk/tests/test_agent_server.py: S-004-16 over the real stdio transport. Both tools are listed with domain and no bound path properties, domains_observe is read_only_hint, domains_apply has yes, and a domains_apply call without yes answers the plan (dry_run)
  • specs.py check passes, and specs.py coverage --spec 004 lists tests for every S-004 criterion
  • Ran locally: uv run ruff check ., uv run mypy, uv run cloudfall config validate --project config/examples, and ansible-lint on engine/ansible with ANSIBLE_CONFIG=engine/ansible/ansible.cfg (production profile, passed). Tests: test_app, test_commands, test_agent_server, test_lifecycle, test_domains_apply, test_domains_observe, test_domains_converge, test_service_evidence, test_operations_dashboard, test_dashboard_server, test_cutover, test_project, test_catalog, test_arguments, test_migrate, and engine test_playbook, test_domains_one_domain, test_domains_certificate, test_domains_playbook all pass. The full suite is left to CI
  • Not run live: domains-observe.yml over SSH on a Debian proxy server, real openssl on a Let's Encrypt fullchain.pem, the exit codes of real systemctl is-enabled/is-active for a missing unit, and reading /etc/letsencrypt/live under become

Plan

  • Serves: quality bar
  • Files: engine/ansible/playbooks/domains-observe.yml, sdk/src/cloudfall/lifecycle.py, sdk/src/cloudfall/service_evidence.py, sdk/src/cloudfall/app.py, sdk/src/cloudfall/commands.py, sdk/src/cloudfall/agent_server.py, sdk/README.md, engine/README.md, docs/specs/004-domains-apply.md, changelog.d/118-domains-observe.md, tests
  • Order: read-only playbook, one-receipt loader, observe_domain in lifecycle, domains observe command and catalog entry, MCP tools, tests per criterion, spec built (see the spec's Behaviour)
  • Proof: S-004-4, S-004-14, S-004-15, S-004-16 (specs.py coverage --spec 004 lists every S-004 criterion)

🤖 Generated with Claude Code

cloudfall domains observe <domain> runs the new read-only
domains-observe.yml on the Domain's proxy server alone and reports the
vhost file (present, sha256), the certificate's not_after, whether
certbot.timer is enabled and active, and whether the vhost still has the
configurationSha256 of the last receipt (receipt: null before one).
Every server task is a stat or a command with changed_when: false; the
reading comes back through a controller file, and the inventory is
rendered in a temporary directory, so the project is left as it was. A
missing vhost exits 0, an unknown Domain exits PRECONDITION
(lifecycle_domain_missing) before the inventory render. domains apply
and domains observe join the MCP project tools; domains_apply keeps its
yes gate. Spec S-004 is built.

Fixes #118

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

Copy link
Copy Markdown
Contributor Author

@romamo Decided without waiting: domains observe always writes vhost.sha256 and certificate.not_after (null when absent) instead of omitting them, renders its inventory into a system temp dir and so takes no --inventory-file, and a certificate openssl can't read fails the run with ENGINE_STEP_FAILED.

Why: S-004-14 says observe changes no file in the project, so its inventory can't go to tmp/ there; a stable output shape is easier for agents to read; an unreadable certificate is a fault to see, not a reading.
Not taken: omitting absent keys (the shape changes with the host's state), and reporting a corrupt certificate as null (it would read like "no certificate").

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

@shipmill-romamo

Copy link
Copy Markdown
Contributor Author

@romamo Decided without waiting: cloudfall domains observe is a new read-only command with its own read-only playbook (domains-observe.yml), domains_apply and domains_observe become MCP tools (domains_apply keeps the yes gate as deploy does), and cloudfall.lifecycle and cloudfall.service_evidence gain public names for them.

Why: S-004-4, 14, 15, 16 ask for these; every existing command and tool answers as before.
Not taken: holding the MCP tools back, which would leave S-004-16 unbuilt.

Reply with a comment starting with veto before 2026-10-28 11:40 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 27, 2026
@shipmill-romamo
shipmill-romamo Bot merged commit 5bdb46d into main Oct 27, 2026
1 check passed
@shipmill-romamo
shipmill-romamo Bot deleted the feat/118-domains-observe branch October 27, 2026 10:55
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

Development

Successfully merging this pull request may close these issues.

Add cloudfall domains observe and the domains MCP tools (S-004-4, 14, 15, 16)

0 participants