Skip to content

refactor(core): compose feature requests into shared host capabilities - #3

Merged
c-wri merged 10 commits into
mainfrom
refactor/ansible-ownership-boundaries
Mar 10, 2026
Merged

refactor(core): compose feature requests into shared host capabilities#3
c-wri merged 10 commits into
mainfrom
refactor/ansible-ownership-boundaries

Conversation

@c-wri

@c-wri c-wri commented Mar 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • replace flat host and service variables with nested host.* and features.* configuration contracts
  • add a site-local composition layer that derives shared ingress, backup, and firewall requests before shared roles run
  • rewire playbooks onto the shared host capability roles introduced by the supporting collections
  • tighten preflight validation so contract failures surface deterministically instead of leaking as undefined-variable errors
  • update repo docs, CI, and tests to match the new ownership model and operator path

Why

This repo is the environment-facing composition layer. It should describe desired host capabilities and feature contracts, not duplicate shared service or firewall implementation details. The refactor makes that boundary explicit and keeps validation close to the site-specific inputs that operators actually edit.

Validation

  • GitHub Actions validate workflow is passing
  • added preflight and CI coverage for cross-repo contract expectations
  • hardened Obsidian ingress contract validation to fail explicitly on missing route configuration

c-wri added 10 commits March 10, 2026 12:31
Why:\nThe core repo needed a single published configuration contract instead of flat per-service variables and wizard output that still emitted the removed restic contribution model. Leaving the builder and scaffold on the old shape would have kept the repo internally inconsistent even if the runtime playbooks had been refactored.\n\nWhat:\n- updated the tracked inventory example to the nested features.* and host.* model\n- rewired bootstrap to consume host.ufw baseline settings from the new contract\n- changed the repo-local wizard builder and inventory template to emit the nested host and feature mappings directly\n- removed flat backup output keys from the builder context and replaced them with host.restic jobs plus nested feature/host settings\n- tightened the wizard profile default so public HTTPS exposure is derived instead of hard-coded into the default firewall baseline\n\nValidation:\n- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/bootstrap.yml\n- bash tests/smoke/test_builder_obsidian_setup_uri_generation.sh\n- bash tests/smoke/test_builder_restic_target_keypair_consistency.sh\n- bash tests/smoke/test_builder_restic_target_name_normalization.sh
…dings

Why:\nFeature roles and shared host capability roles needed an explicit site-owned integration layer. Without a dedicated composition step, the repo would either fall back to runtime self-registration or force shared-service ownership back into feature roles.\n\nWhat:\n- added the platform_bindings role as the site-local composition layer\n- composed platform_ingress_routes from enabled features that require shared ingress\n- composed platform_backup_datasets from host foundation state, workspaces, Obsidian CouchDB data, and Traefik ACME state\n- composed platform_ufw_requests from public HTTPS ingress requirements\n- produced effective platform_obsidian_livesync, platform_host_traefik, platform_host_restic, and platform_host_ufw mappings for downstream role consumption\n\nValidation:\n- bash tests/smoke/test_platform_bindings_public_https.sh\n- bash tests/smoke/test_platform_bindings_private_mesh.sh
Why:\nThe site playbooks still targeted the old single-stack flow even after the role contracts moved to host_traefik, host_restic, and host_ufw. The repo needed explicit orchestration that composes site bindings first, runs feature-local work, and then reconciles shared host capabilities.\n\nWhat:\n- rewired site.yml to run platform_bindings before obsidian_livesync, host_traefik, and host_ufw\n- rewired backup.yml to run platform_bindings before host_restic\n- updated the dedicated Obsidian feature playbook to consume the composed obsidian role contract\n\nValidation:\n- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/site.yml\n- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/backup.yml
Why:\nThe old preflight role validated removed flat variables and restic contribution wiring, which meant it was no longer checking the real contract the repo now executes. Preflight needed to move up to the nested host/features model and the site-composed platform outputs.\n\nWhat:\n- loaded inventory variables before running platform_bindings in playbooks/preflight.yml\n- rewrote preflight validation around nested features.* and host.* contracts\n- added validation for removed legacy variables, public HTTPS ingress requirements, private mesh planning inputs, host.restic job selection, and unsupported feature_owned_jobs\n- validated composed platform_ingress_routes, platform_backup_datasets, and platform_ufw_requests instead of the deleted contribution model\n- kept the aggregated report, warnings, and remote connectivity probe behavior intact\n\nValidation:\n- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/preflight.yml\n- bash tests/smoke/test_preflight_placeholder_scan.sh\n- bash tests/smoke/test_preflight_restic_cache_valid_time_validation.sh\n- bash tests/smoke/test_preflight_restic_target_name_validation.sh\n- bash tests/smoke/test_preflight_tailscale_tag_validation.sh\n- bash tests/smoke/test_preflight_feature_owned_jobs_rejected.sh\n- bash tests/smoke/test_preflight_restic_broad_path_warning.sh
…ooks

Why:\nThe repo documentation still described the deleted flat variable surface and the removed restic contribution model. That would have left operators following the wrong ownership boundaries even though the code had already moved on.\n\nWhat:\n- rewrote the top-level README and architecture docs around nested features.* and host.* configuration plus site-local composition\n- updated preflight, quickstart, restore, and implementation status docs to describe host capability ownership and composed backup datasets\n- removed references to restic_backup_contributions and other deleted flat inventory keys from the operator docs\n- refreshed the config wizard spec examples so the documented target paths match the nested contract\n\nValidation:\n- rg -n restic_backup_contributions
Why:
The previous smoke suite still asserted the removed flat backup contribution model and did not exercise the new site-local composition layer. Without updating the tests and CI, the repo would have looked refactored while silently losing the coverage that makes the new ownership boundaries safe to extend.

What:
- updated the builder smoke tests to assert nested host.* and features.* output plus removal of flat backup keys
- updated the preflight smoke tests to load platform_bindings and validate the nested contract messages
- added composition smoke coverage for platform_bindings in public_https and private_mesh modes
- added preflight coverage for unsupported host.restic.feature_owned_jobs
- expanded CI to run the new builder, composition, and preflight smoke suite

Validation:
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/preflight.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/bootstrap.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/site.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/backup.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/lockdown.yml
- bash tests/smoke/test_builder_obsidian_setup_uri_generation.sh
- bash tests/smoke/test_builder_restic_target_keypair_consistency.sh
- bash tests/smoke/test_builder_restic_target_name_normalization.sh
- bash tests/smoke/test_platform_bindings_public_https.sh
- bash tests/smoke/test_platform_bindings_private_mesh.sh
- bash tests/smoke/test_preflight_placeholder_scan.sh
- bash tests/smoke/test_preflight_restic_broad_path_warning.sh
- bash tests/smoke/test_preflight_restic_cache_valid_time_validation.sh
- bash tests/smoke/test_preflight_restic_target_name_validation.sh
- bash tests/smoke/test_preflight_tailscale_tag_validation.sh
- bash tests/smoke/test_preflight_feature_owned_jobs_rejected.sh
Why
The platform composition layer crashed on missing obsidian ingress.route_name before preflight could report a contract error. The builder also still leaked public HTTPS into the host UFW baseline, which violated the refactor's ownership split.

What
Make platform_bindings tolerate a missing route_name without undefined-variable failures.
Add explicit preflight validation for features.obsidian_livesync.ingress.route_name.
Stop the builder from auto-injecting 443 into host.ufw.baseline_tcp_public.
Add smoke coverage and CI wiring for the new contract and ownership checks.

Validation
bash tests/smoke/test_preflight_obsidian_route_name_validation.sh
bash tests/smoke/test_builder_restic_target_name_normalization.sh
for t in tests/smoke/*.sh; do bash "" || exit 1; done
./scripts/init-local-config.sh >/dev/null
umask 177 && printf 'ci-test-only\n' > .vault_pass
./scripts/install-collections.sh >/dev/null
ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/preflight.yml
ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/bootstrap.yml
ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/site.yml
ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/backup.yml
ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/lockdown.yml
ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/features/obsidian.yml
Why\nThe core PR depends on new host capability roles that only exist on matching provider branches until the services and base refactors merge. CI was installing provider collections from their default remote branches, so syntax checks on the core PR could not resolve the new roles. The collection installer also inherited a brittle user-level Ansible temp path, which made local validation less reliable.\n\nWhat\n- stage branch-matched provider repo checkouts during pull_request validation when the same branch exists in crownops-deploy-base and crownops-deploy-services\n- point collection installation at those staged checkouts only when they are present, while preserving the default source fallback for normal validation\n- harden scripts/install-collections.sh to create and use a repo-local ANSIBLE_LOCAL_TEMP by default\n\nValidation\n- python3 -m pip install "ansible-core==2.17.*"\n- TMPDIR=/dev/shm/crownops-core-tmp ANSIBLE_LOCAL_TEMP=/dev/shm/crownops-core-tmp/ansible ./scripts/install-collections.sh\n- TMPDIR=/dev/shm/crownops-core-tmp ANSIBLE_LOCAL_TEMP=/dev/shm/crownops-core-tmp/ansible ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/preflight.yml\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_preflight_placeholder_scan.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_preflight_tailscale_tag_validation.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_platform_bindings_public_https.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_platform_bindings_private_mesh.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_builder_restic_target_name_normalization.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_builder_obsidian_setup_uri_generation.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_builder_restic_target_keypair_consistency.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_preflight_restic_target_name_validation.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_preflight_restic_cache_valid_time_validation.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_preflight_restic_broad_path_warning.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_preflight_feature_owned_jobs_rejected.sh\n- TMPDIR=/dev/shm/crownops-core-tmp bash tests/smoke/test_preflight_obsidian_route_name_validation.sh\n- TMPDIR=/dev/shm/crownops-core-tmp ANSIBLE_LOCAL_TEMP=/dev/shm/crownops-core-tmp/ansible ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/bootstrap.yml\n- TMPDIR=/dev/shm/crownops-core-tmp ANSIBLE_LOCAL_TEMP=/dev/shm/crownops-core-tmp/ansible ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/site.yml\n- TMPDIR=/dev/shm/crownops-core-tmp ANSIBLE_LOCAL_TEMP=/dev/shm/crownops-core-tmp/ansible ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/backup.yml\n- TMPDIR=/dev/shm/crownops-core-tmp ANSIBLE_LOCAL_TEMP=/dev/shm/crownops-core-tmp/ansible ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/lockdown.yml
Why
- platform_bindings was overwriting host-owned Traefik routes and restic datasets instead of composing additive effective contracts, which blocked extension and would have forced follow-up refactors.
- preflight could crash with undefined-variable errors when nested host/features roots were missing, so operators would not get the aggregated contract validation the refactor promised.
- the smoke harness depended on ambient tempdir and umask behavior, which made the new validation surface brittle outside the GitHub runner defaults.

What
- normalize host and feature roots inside platform_bindings, preserve host-owned Traefik routes/restic datasets/UFW requests, and avoid crashing when optional roots are absent during validation.
- harden preflight validation with safe nested-contract aliases, additive contract collision checks, missing root/path checks, and effective-dataset validation against the fully composed restic contract.
- add smoke coverage for missing nested contract roots and additive host extension merges, switch temp fixture helpers to a controlled helper, and wire the new smoke tests into CI.
- document that host-owned routes, datasets, and requests remain additive extension points after site composition.

Validation
- ./scripts/install-collections.sh
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/preflight.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/bootstrap.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/site.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/backup.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/lockdown.yml
- bash tests/smoke/test_*.sh
Why
- the refactor published host.traefik as an additive extension point, but preflight was still validating only part of that public contract before remote execution
- that gap would let incomplete custom Traefik host capability configs survive preflight and fail later during deployment, which is the wrong failure boundary for an operator-facing repo

What
- validate the required host.traefik contract fields whenever Traefik is enabled, even when Obsidian is not the ingress source
- validate effective Traefik route definitions against the same contract shape the shared role expects, while still allowing implicit entrypoint defaults from the services collection
- add a smoke test that exercises host-only Traefik preflight validation and wire it into CI

Validation
- ./scripts/install-collections.sh with CROWNOPS_BASE_COLLECTION_SOURCE=/home/user/Code/vps/crownops-deploy-base and CROWNOPS_SERVICES_COLLECTION_SOURCE=/home/user/Code/vps/crownops-deploy-services
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/preflight.yml
- bash tests/smoke/test_preflight_placeholder_scan.sh
- bash tests/smoke/test_preflight_missing_contract_roots.sh
- bash tests/smoke/test_preflight_tailscale_tag_validation.sh
- bash tests/smoke/test_platform_bindings_public_https.sh
- bash tests/smoke/test_platform_bindings_private_mesh.sh
- bash tests/smoke/test_platform_bindings_host_extensions.sh
- bash tests/smoke/test_preflight_host_traefik_contract_validation.sh
- bash tests/smoke/test_builder_restic_target_name_normalization.sh
- bash tests/smoke/test_builder_obsidian_setup_uri_generation.sh
- bash tests/smoke/test_builder_restic_target_keypair_consistency.sh
- bash tests/smoke/test_preflight_restic_target_name_validation.sh
- bash tests/smoke/test_preflight_restic_cache_valid_time_validation.sh
- bash tests/smoke/test_preflight_restic_broad_path_warning.sh
- bash tests/smoke/test_preflight_feature_owned_jobs_rejected.sh
- bash tests/smoke/test_preflight_obsidian_route_name_validation.sh
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/bootstrap.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/site.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/backup.yml
- ansible-playbook --syntax-check -i inventories/prod/hosts.yml playbooks/lockdown.yml
@c-wri
c-wri marked this pull request as ready for review March 10, 2026 22:23
@c-wri
c-wri merged commit ea873e7 into main Mar 10, 2026
1 check passed
@c-wri
c-wri deleted the refactor/ansible-ownership-boundaries branch March 10, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant