cloudfall 0.8.0rc13__tar.gz → 0.8.0rc15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/PKG-INFO +8 -3
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/README.md +7 -2
- cloudfall-0.8.0rc15/changelog.d/106-enforce-manual-approval.md +3 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/160-release-approve.md +1 -1
- cloudfall-0.8.0rc15/changelog.d/165-migrate-approval-pause.md +3 -0
- cloudfall-0.8.0rc15/changelog.d/174-no-templating-config-values.md +3 -0
- cloudfall-0.8.0rc15/changelog.d/181-catalog-inputs-as-data.md +3 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/ci-deploy-guide.md +86 -19
- cloudfall-0.8.0rc15/docs/decisions/2026-11-02-isolation-without-containers.md +155 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/decisions.md +8 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/operator-guide.md +5 -2
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/render-migration-guide.md +15 -1
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/008-enforce-manual-approval.md +20 -1
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/cli.py +14 -3
- cloudfall-0.8.0rc15/engine/tests/golden/examples/ansible-inventory.json +1 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_ansible_inventory.py +4 -4
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_examples_render_unchanged.py +6 -1
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_services_passwords.py +10 -3
- cloudfall-0.8.0rc15/engine/tests/test_unsafe_config_values.py +348 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/pyproject.toml +1 -1
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/README.md +11 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/agent_server.py +16 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/agent_tools.py +3 -0
- cloudfall-0.8.0rc15/sdk/src/cloudfall/ansible_data.py +45 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/app.py +101 -6
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/commands.py +28 -7
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/decision.py +5 -3
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/lifecycle.py +9 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/migrate.py +88 -20
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/observe.py +5 -1
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/recording.py +8 -1
- cloudfall-0.8.0rc15/sdk/src/cloudfall/release_approval.py +609 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_agent_server.py +85 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_decision.py +67 -1
- cloudfall-0.8.0rc15/sdk/tests/test_deploy_approval.py +556 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_host_stats.py +24 -0
- cloudfall-0.8.0rc15/sdk/tests/test_migrate_approval.py +320 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_observe.py +3 -2
- cloudfall-0.8.0rc13/engine/tests/golden/examples/ansible-inventory.json +0 -1
- cloudfall-0.8.0rc13/sdk/src/cloudfall/release_approval.py +0 -286
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/.ansible-lint +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/.gitignore +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/ARCHITECTURE.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/CHANGELOG.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/CODE_OF_CONDUCT.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/CONTRIBUTING.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/LICENSE +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/MANIFESTO.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/ROADMAP.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/SECURITY.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/Taskfile.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/113-component-build-validation.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/114-refuse-node-runtimes.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/115-component-build-steps.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/116-converge-one-domain.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/117-domains-apply.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/118-domains-observe.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/119-release-command.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/120-release-command-plan.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/121-owned-postgresql-validation.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/122-owned-postgresql-converge.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/123-postgresql-remote-access.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/124-data-copy-schema.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/125-data-copy-preflight.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/126-source-schema-checks.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/140-scram-verifier.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/143-audit-remote-access.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/144-pg-hba-reload-and-names.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/151-keep-hand-made-unit.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/159-domain-routes-validation.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/161-command-health-check-schema.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/162-domain-routes-render.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/164-command-health-gate.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/166-command-check-restart-rollback-health.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/173-preflight-extension-arrays.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/183-migrate-observations-missing.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/89-ci-deploy-guide.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/90-domain-upstream-early.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/96-healthcheck-none.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/97-stale-proposals.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/changelog.d/README.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/README.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/alert-rules/postgresql-down.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/applications/crm.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/components/crm-backend.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/domains/crm-site.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/logging-stacks/operations.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/operator-policies/production.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/server-types/debian-application.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/servers/h1.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/servers/h2.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/services/postgresql-main.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/services/redis-cache.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/agent-investigation.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/alert-rule.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/application.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/artifact.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/backup-receipt.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/common.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/component.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/deployment-receipt.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/domain.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/environment-receipt.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/logging-stack.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/observed-domain.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/observed-server.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/operation-decision.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/operation.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/operator-policy.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/operator-proposal.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/release-approval.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/release-receipt.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/server-type.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/server.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/service.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/schemas/v1/ssh-public-key.schema.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/observed/compliant/h1.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/config/tests/observed/compliant/h2.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/README.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/.gitignore +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/ansible.cfg +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/inventories/production/hosts.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/converge.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/disk-report.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/journal-vacuum.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/postgresql-reinit.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/postgresql-restart.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/service-logs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/operations/shop-logrotate.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/disk-report.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/journal-vacuum.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/postgresql-reinit.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/postgresql-restart.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/postgresql.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/service-logs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/shop-logrotate.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/playbooks/shop.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/disk-report-20261008170744.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/disk-report-20261008170744.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/journal-vacuum-20261008171016.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/journal-vacuum-20261008171016.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/postgresql-reinit-20261008171010.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/postgresql-reinit-20261008171010.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/postgresql-restart-20261008170624.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/postgresql-restart-20261008170624.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170612.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170612.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170740.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170740.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170742.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170742.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170838.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170838.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170920.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/service-logs-20261008170920.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/shop-logrotate-20261008170752.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full/shop-logrotate-20261008170752.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/disk-report-20261008204348.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/disk-report-20261008204348.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/disk-report-20261008204421.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/disk-report-20261008204421.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/postgresql-restart-20261008204358.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/postgresql-restart-20261008204358.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/shop-logrotate-20261008204351.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/recordings/disk-full-approved/shop-logrotate-20261008204351.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/scenario/break.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/incident/site.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/pyproject.toml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/src/cloudfall_demo/__init__.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/src/cloudfall_demo/server.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/src/cloudfall_demo/static/evaluation.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/src/cloudfall_demo/static/index.html +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/tests/test_demo.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/demo/uv.lock +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/ansible-context.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/assets/logo.svg +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/availability-design.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/brownfield-design.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/fleet-goals.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/getting-started.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/hybrid-storage-design.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/images/demo.png +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/landscape.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/logging-service-guide.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/neon-migration-guide.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/new-server-storage-guide.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/product.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/proving-runs/2026-10-08-disk-full-agent.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/secrets-guide.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/001-stale-proposals.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/002-component-build-steps.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/003-domain-routes.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/004-domains-apply.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/005-release-command.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/006-standalone-postgresql.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/007-data-copy-schemas.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/docs/specs/009-command-health-check.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/README.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/ansible.cfg +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/backup.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/baseline.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/bootstrap.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/data.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/deploy.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/domains-observe.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/domains.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/health.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/inspect.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/logging.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/restart.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/rollback.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/services.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/playbooks/time.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/command_health_check.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/release_command.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/release_receipt.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/tasks/apply_virtual_host.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/tasks/static_access.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/tasks/owner_password.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/pg_hba-remote-access.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/__init__.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/__main__.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/artifact.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/playbook.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/src/cloudfall_engine/py.typed +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h1/nftables.conf +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h1/postgresql-main/backup.sh +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h1/postgresql-main/cloudfall.conf +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h1/postgresql-main/restore-check.sh +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/examples/h2/nftables.conf +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/nginx-site/http-1.22.1.conf +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/nginx-site/http-1.26.3.conf +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/nginx-site/tls-1.22.1.conf +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/golden/nginx-site/tls-1.26.3.conf +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_artifact_build_steps.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_artifact_builder.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_command_health_gate.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_data_migration_role.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_deploy_role.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_deploy_templates.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_domains_certificate.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_domains_one_domain.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_domains_playbook.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_firewall_templates.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_logging_templates.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_nginx_site_role.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_nginx_site_routes.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_nginx_site_templates.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_playbook.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_postgresql_owner_role.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_postgresql_remote_access.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_postgresql_templates.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_release_command.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/engine/tests/test_time_role.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/.gitignore +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/README.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/ansible.cfg +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/inventories/production/hosts.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/converge.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/disk-report.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/journal-vacuum.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/postgresql-reinit.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/postgresql-restart.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/service-logs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/operations/shop-logrotate.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/disk-report.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/journal-vacuum.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/postgresql-reinit.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/postgresql-restart.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/postgresql.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/service-logs.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/shop-logrotate.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/playbooks/shop.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/disk-report-20261008170744.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/disk-report-20261008170744.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/journal-vacuum-20261008171016.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/journal-vacuum-20261008171016.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/postgresql-reinit-20261008171010.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/postgresql-reinit-20261008171010.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/postgresql-restart-20261008170624.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/postgresql-restart-20261008170624.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170612.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170612.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170740.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170740.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170742.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170742.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170838.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170838.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170920.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170920.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/shop-logrotate-20261008170752.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full/shop-logrotate-20261008170752.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204348.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204348.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204421.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204421.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/postgresql-restart-20261008204358.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/postgresql-restart-20261008204358.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/shop-logrotate-20261008204351.diff +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/recordings/disk-full-approved/shop-logrotate-20261008204351.json +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/scenario/break.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/disk-full-incident/site.yml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/README.md +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/pyproject.toml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/render.yaml +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/src/demo/__init__.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/src/demo/asgi.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/examples/render-demo/uv.lock +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/__init__.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/__main__.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/ansible_api.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/ansible_reader.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/audit.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/authoring.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/callback_plugins/__init__.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/callback_plugins/cloudfall_host_stats.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/catalog.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/cli.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/cutover.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/dashboard.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/dashboard_server.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/domain.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/importer.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/inventory.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/investigation.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/observation.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/operations.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/operator.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/project.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/py.typed +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/render_api.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/resources.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/secrets.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/service_evidence.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/validation.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/src/cloudfall/why.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_ansible_reader.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_app.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_arguments.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_audit.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_authoring.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_catalog.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_command_health_check.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_commands.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_component_build_validation.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_cutover.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_dashboard_server.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_data_copy_preflight.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_data_migration.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_domain_routes.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_domains_apply.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_domains_converge.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_domains_observe.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_importer.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_inventory.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_investigation.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_lifecycle.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_migrate.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_operations_dashboard.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_operator.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_owned_postgresql.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_owned_postgresql_converge.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_project.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_release_approval.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_release_command_plan.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_render_api.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_resources.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_secrets.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_service_evidence.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_stale.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_validation.py +0 -0
- {cloudfall-0.8.0rc13 → cloudfall-0.8.0rc15}/sdk/tests/test_why.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cloudfall
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.0rc15
|
|
4
4
|
Summary: The operator's record for self-hosted servers run by an AI agent: Ansible executes, Cloudfall gates, verifies and keeps the receipts
|
|
5
5
|
Project-URL: Homepage, https://cloudfall.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/cloudfall-dev/cloudfall
|
|
@@ -252,14 +252,19 @@ and drift audit to the live dashboard, is in
|
|
|
252
252
|
```console
|
|
253
253
|
uv run cloudfall import render render.yaml --application acme --server h1
|
|
254
254
|
uv run cloudfall migrate --build web=main # show the plan
|
|
255
|
-
uv run cloudfall migrate --build web=main --yes # run it
|
|
255
|
+
uv run cloudfall migrate --build web=main --yes # run it; pauses at deploy:web
|
|
256
|
+
uv run cloudfall release approve web --release <release-id> # a person, at a terminal
|
|
257
|
+
uv run cloudfall migrate --yes # resume: deploy, routes, TLS
|
|
256
258
|
```
|
|
257
259
|
|
|
258
260
|
The importer never guesses silently: `IMPORT-REPORT.md` records what was not
|
|
259
261
|
imported, every assumption and every step required before cutover. The
|
|
260
262
|
migration runs baseline, services, builds, health-gated deploys, routes, a
|
|
261
263
|
DNS-verification pause, TLS and a final evidence pass, and resumes exactly
|
|
262
|
-
where it stopped.
|
|
264
|
+
where it stopped. An imported application declares `approval: manual`, so
|
|
265
|
+
the run pauses before its deploy until a person approves the built release
|
|
266
|
+
with `cloudfall release approve`; the resumed run deploys that release
|
|
267
|
+
without building again. Databases move with `cloudfall data copy`, which verifies
|
|
263
268
|
per-table row counts and writes a receipt. See the
|
|
264
269
|
[Render migration guide](docs/render-migration-guide.md) and the
|
|
265
270
|
[Neon migration guide](docs/neon-migration-guide.md).
|
|
@@ -224,14 +224,19 @@ and drift audit to the live dashboard, is in
|
|
|
224
224
|
```console
|
|
225
225
|
uv run cloudfall import render render.yaml --application acme --server h1
|
|
226
226
|
uv run cloudfall migrate --build web=main # show the plan
|
|
227
|
-
uv run cloudfall migrate --build web=main --yes # run it
|
|
227
|
+
uv run cloudfall migrate --build web=main --yes # run it; pauses at deploy:web
|
|
228
|
+
uv run cloudfall release approve web --release <release-id> # a person, at a terminal
|
|
229
|
+
uv run cloudfall migrate --yes # resume: deploy, routes, TLS
|
|
228
230
|
```
|
|
229
231
|
|
|
230
232
|
The importer never guesses silently: `IMPORT-REPORT.md` records what was not
|
|
231
233
|
imported, every assumption and every step required before cutover. The
|
|
232
234
|
migration runs baseline, services, builds, health-gated deploys, routes, a
|
|
233
235
|
DNS-verification pause, TLS and a final evidence pass, and resumes exactly
|
|
234
|
-
where it stopped.
|
|
236
|
+
where it stopped. An imported application declares `approval: manual`, so
|
|
237
|
+
the run pauses before its deploy until a person approves the built release
|
|
238
|
+
with `cloudfall release approve`; the resumed run deploys that release
|
|
239
|
+
without building again. Databases move with `cloudfall data copy`, which verifies
|
|
235
240
|
per-table row counts and writes a receipt. See the
|
|
236
241
|
[Render migration guide](docs/render-migration-guide.md) and the
|
|
237
242
|
[Neon migration guide](docs/neon-migration-guide.md).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
### Breaking
|
|
2
|
+
|
|
3
|
+
- `deploy --yes` and `migrate --yes` on an application that declares `approval: manual` now need a person's approval of the release: without a `cloudfall release approve` record that counts (the same archive SHA-256, exactly the component's declared servers, not expired), `deploy --yes` exits 4 `PRECONDITION` with `deployment_approval_missing` before any playbook runs, naming the `release approve` command, and lists each record that doesn't count with `digest_mismatch`, `servers_changed` or `expired`. This hits every project that declares `manual` and deploys today, the example `crm.yaml` and every Render import included. Two ways out: keep `manual` and approve each release first (build it, run `cloudfall release approve` at a terminal, commit the record under `decisions/releases/` when CI deploys, then deploy), or switch the application to `approval: automatic` when another gate, such as GitHub's required reviewers, is the real one. The `deploy` plan and result gain `approval` (`policy`, and for `manual` the `status`, the counting record's `path`, `approver` and `expires_at` or `approved_at`, the records that don't count and the `command` to run), `deploy` gains `--approvals` (default `decisions/releases`) and the `RECORD_INVALID` exit for an approvals file that isn't a valid `ReleaseApproval`, and `cloudfall mcp serve` binds the approvals folder (its own `--approvals`) so a `deploy` call can't name its own; an `automatic` application's deploy never reads the folder (#163)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
### Added
|
|
2
2
|
|
|
3
|
-
- `cloudfall release approve <component> --release <release>` records a person's approval of one built release as a `ReleaseApproval` (new `release-approval.schema.json`) under `decisions/releases/`: the application, component, release, archive SHA-256, git commit, servers, approver, `approvedAt`, `expiresAt` (`--valid-for` hours, 1 to 168, default 24) and `via: terminal`. It verifies the artifact first, refuses a component of an `automatic` application with `release_approval_not_required`, and writes the record only once a person typed the release id at a terminal; `--yes` never answers it, and it is no MCP tool. It deploys nothing
|
|
3
|
+
- `cloudfall release approve <component> --release <release>` records a person's approval of one built release as a `ReleaseApproval` (new `release-approval.schema.json`) under `decisions/releases/`: the application, component, release, archive SHA-256, git commit, servers, approver, `approvedAt`, `expiresAt` (`--valid-for` hours, 1 to 168, default 24) and `via: terminal`. It verifies the artifact first, refuses a component of an `automatic` application with `release_approval_not_required`, and writes the record only once a person typed the release id at a terminal; `--yes` never answers it, and it is no MCP tool. It deploys nothing (#160)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
### Changed
|
|
2
|
+
|
|
3
|
+
- `migrate --yes` checks a `deploy:<component>` step's approval as `deploy --yes` does: for an application that declares `approval: manual` and no approval that counts, it pauses at that step with `PARTIAL_FAILURE`, `context.code: deployment_approval_missing`, the built `release` and the `cloudfall release approve` `command`, and once a person approved it, `migrate --yes` resumes there with the release the completed `build:<component>` step recorded, without building again and without needing `--build` again. The deploy step's detail gains `approval` (the record it went on), `migrate` gains `--approvals` (default `decisions/releases`, bound by `cloudfall mcp serve`) and the `RECORD_INVALID` exit for an approvals file that isn't a valid `ReleaseApproval`. `rollback` needs no approval: its plan gains `approval`, `{"policy": "manual", "required": false, "reason": ...}` for a `manual` application and `{"policy": "automatic"}` otherwise (#165)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
### Security
|
|
2
|
+
|
|
3
|
+
- A declared config value is never run as an Ansible template: `cloudfall-engine inventory render` and the overlay `cloudfall observe` writes mark every string unsafe (`{"__ansible_unsafe": ...}`), so a value holding `{{ ... }}` reaches the unit file, the release command and the health check as written. Before, a `service.command`, `healthCheck.command` or `release.command` argument such as `{{ lookup('pipe', '...') }}` ran on the management host during `cloudfall health` or `cloudfall deploy`, and an honest one such as `docker inspect --format {{.State.Health.Status}} app` failed with a template syntax error. A declared value that relied on Ansible evaluating `{{ ... }}` is now literal, and the rendered inventory's strings read as `{"__ansible_unsafe": "..."}` in its JSON and in `ansible-inventory --list` (#174)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
### Security
|
|
2
|
+
|
|
3
|
+
- A catalog operation's inputs are never run as an Ansible template: `cloudfall operations propose`, `cloudfall decisions approve` and `cloudfall agent investigate` hand them to the team's playbook in `--extra-vars` with every string marked unsafe (`{"__ansible_unsafe": ...}`), so an input holding `{{ ... }}` reaches the playbook as written, and integers and booleans keep their types. Before, an input such as `{{ lookup('pipe', '...') }}` ran on the management host with the operator's rights when the playbook started, and an honest input holding `{{` failed with a template syntax error. A team playbook that relied on an input being evaluated as Jinja now gets the literal text. The decision record's `inputs` are unchanged, and recordings made before this change replay as before (#181)
|
|
@@ -325,25 +325,92 @@ naming `production` still waits for its reviewers.
|
|
|
325
325
|
|
|
326
326
|
## Approval: who presses the button
|
|
327
327
|
|
|
328
|
-
Every `Application` declares `spec.approval: manual` or `automatic
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
328
|
+
Every `Application` declares `spec.approval: manual` or `automatic`, and
|
|
329
|
+
`cloudfall deploy` enforces it:
|
|
330
|
+
|
|
331
|
+
- `automatic`: `deploy --yes` deploys, as the workflow above does. Keep
|
|
332
|
+
staging's applications `automatic` and its one job stays as it is
|
|
333
|
+
- `manual`: `deploy --yes` deploys a release only when a person approved
|
|
334
|
+
that release with `cloudfall release approve`. Without an approval that
|
|
335
|
+
counts it exits 4 `PRECONDITION` with `deployment_approval_missing`, before
|
|
336
|
+
any playbook runs, and names the command to run. `deploy` without `--yes`
|
|
337
|
+
shows the approval in its plan: `approval.status` is `approved` or
|
|
338
|
+
`missing`
|
|
339
|
+
|
|
340
|
+
An approval is a record under `decisions/releases/` in the project. It names
|
|
341
|
+
the archive's SHA-256, the component's servers and when it expires (24 hours
|
|
342
|
+
by default, `--valid-for` up to a week). It counts only while the archive
|
|
343
|
+
being deployed has that digest, the component is declared on exactly those
|
|
344
|
+
servers, and it has not expired. A failed health gate does not use it up, so
|
|
345
|
+
the same release can be retried until it expires.
|
|
346
|
+
|
|
347
|
+
A person approves at a terminal by typing the release id back; no flag, and
|
|
348
|
+
no CI job, answers that prompt. So a production deploy of a `manual`
|
|
349
|
+
application is two jobs with a person between them, and approval needs the
|
|
350
|
+
exact archive the deploy ships:
|
|
351
|
+
|
|
352
|
+
1. A build job builds the release and uploads `tmp/artifacts/<component>/`
|
|
353
|
+
as a workflow artifact
|
|
354
|
+
2. The person downloads that artifact into `tmp/artifacts/<component>/` of
|
|
355
|
+
their checkout and approves it:
|
|
356
|
+
|
|
357
|
+
```sh
|
|
358
|
+
gh run download <run-id> --name cloudfall-artifact-crm-backend --dir tmp/artifacts/crm-backend
|
|
359
|
+
uv run cloudfall release approve crm-backend --release 20260101T000000Z-abcdef0
|
|
360
|
+
git add decisions/releases
|
|
361
|
+
git commit -m "Approve crm-backend 20260101T000000Z-abcdef0"
|
|
362
|
+
git push
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
3. They dispatch the deploy job with the build's run id and the release. It
|
|
366
|
+
checks out the commit holding the record, downloads the same artifact and
|
|
367
|
+
deploys it:
|
|
368
|
+
|
|
369
|
+
```yaml
|
|
370
|
+
build:
|
|
371
|
+
runs-on: ubuntu-24.04
|
|
372
|
+
steps:
|
|
373
|
+
# checkout, uv, uv sync --locked, as in the workflow above
|
|
374
|
+
- name: Build the artifact
|
|
375
|
+
id: build
|
|
376
|
+
run: |
|
|
377
|
+
uv run cloudfall-engine artifact build "$COMPONENT" --ref "$REF" --format json > "$RUNNER_TEMP/build.json"
|
|
378
|
+
echo "release=$(jq -r '.data.release' "$RUNNER_TEMP/build.json")" >> "$GITHUB_OUTPUT"
|
|
379
|
+
- uses: actions/upload-artifact@v7
|
|
380
|
+
with:
|
|
381
|
+
name: cloudfall-artifact-${{ env.COMPONENT }}
|
|
382
|
+
path: tmp/artifacts/${{ env.COMPONENT }}/
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
deploy:
|
|
387
|
+
runs-on: ubuntu-24.04
|
|
388
|
+
environment: production
|
|
389
|
+
steps:
|
|
390
|
+
# checkout, uv, sops, the SSH and age keys, secrets render, as above
|
|
391
|
+
- uses: actions/download-artifact@v7
|
|
392
|
+
with:
|
|
393
|
+
name: cloudfall-artifact-${{ env.COMPONENT }}
|
|
394
|
+
path: tmp/artifacts/${{ env.COMPONENT }}/
|
|
395
|
+
run-id: ${{ inputs.build_run_id }}
|
|
396
|
+
github-token: ${{ github.token }}
|
|
397
|
+
- name: Deploy the approved release
|
|
398
|
+
run: uv run cloudfall deploy "$COMPONENT" --release "$RELEASE" --env-file "tmp/env/$COMPONENT.env" --yes
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
The deploy job reads the record from its checkout, so dispatch it on the
|
|
402
|
+
commit that holds the approval. A rebuilt release has a new archive, and
|
|
403
|
+
its old approval no longer counts.
|
|
404
|
+
|
|
405
|
+
GitHub's required reviewers remain a useful outer gate: give the
|
|
406
|
+
`production` environment reviewers and turn on "Prevent self-review", and
|
|
407
|
+
the deploy job never sees that environment's SSH key or age key until a
|
|
408
|
+
reviewer lets it start. Their approval lands in GitHub's deployment
|
|
409
|
+
history; the `release approve` record is the one in Cloudfall's.
|
|
410
|
+
|
|
411
|
+
When the reviewers are the real gate and nobody will run `release approve`,
|
|
412
|
+
switch the application to `approval: automatic`, so the project no longer
|
|
413
|
+
claims a gate Cloudfall does not hold.
|
|
347
414
|
|
|
348
415
|
## Keeping the receipts
|
|
349
416
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Decision: isolation without containers or self-run VMs
|
|
2
|
+
|
|
3
|
+
Date: 2026-11-02. Status: decided, revisit on the triggers listed below.
|
|
4
|
+
|
|
5
|
+
## Question
|
|
6
|
+
|
|
7
|
+
How does Cloudfall keep components, runners and environments apart, and does
|
|
8
|
+
that need containers or virtual machines it runs itself?
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
No containers and no hypervisor of Cloudfall's own. Isolation has two
|
|
13
|
+
boundaries:
|
|
14
|
+
|
|
15
|
+
- **Inside a server**, the boundary is the systemd sandbox of each unit, a
|
|
16
|
+
database role per component, and cgroup limits. This is required on every
|
|
17
|
+
server
|
|
18
|
+
- **Between trust levels**, the stronger boundary is a separate server: its
|
|
19
|
+
own kernel. A VPS counts as a server; its hypervisor belongs to the
|
|
20
|
+
provider, not to Cloudfall. This is recommended, not required
|
|
21
|
+
|
|
22
|
+
A team may run everything on one server, several independent projects
|
|
23
|
+
included, when it accepts the shared-kernel risk. The acceptance is declared
|
|
24
|
+
in the plan, never implied: Cloudfall warns about a placement that puts two
|
|
25
|
+
trust levels on one server unless the plan declares it.
|
|
26
|
+
|
|
27
|
+
A separate Unix user per component is necessary but is not a boundary on its
|
|
28
|
+
own.
|
|
29
|
+
|
|
30
|
+
## Why a Unix user is not enough
|
|
31
|
+
|
|
32
|
+
A user keeps files and signals apart. It does not stop:
|
|
33
|
+
|
|
34
|
+
- a local privilege escalation in the shared kernel, which reaches every
|
|
35
|
+
component on the host
|
|
36
|
+
- a database, cache or queue on localhost that trusts every local user, or
|
|
37
|
+
that components share one account of
|
|
38
|
+
- membership in the `docker` group or a broad sudo rule, either of which is
|
|
39
|
+
root
|
|
40
|
+
- a runner that executes code from a pull request or a compromised
|
|
41
|
+
dependency, which gets every key the runner holds; the keys, not the uid,
|
|
42
|
+
set the blast radius
|
|
43
|
+
- reading other processes' command lines in `/proc`, world-readable home
|
|
44
|
+
directories, and one component exhausting memory, disk or processes for
|
|
45
|
+
the rest
|
|
46
|
+
|
|
47
|
+
## Why not containers
|
|
48
|
+
|
|
49
|
+
Docker and Podman isolate with the same kernel mechanisms systemd exposes
|
|
50
|
+
directly: namespaces, cgroups, seccomp and capabilities. A container shares
|
|
51
|
+
the kernel exactly as a sandboxed unit does, so it adds no boundary. What it
|
|
52
|
+
adds is packaging (images, independent library versions) and a runtime to
|
|
53
|
+
operate. Cloudfall's base is Debian and systemd
|
|
54
|
+
([`2026-09-14-no-nixos-base.md`](2026-09-14-no-nixos-base.md)), container
|
|
55
|
+
orchestration is a non-goal ([`../product.md`](../product.md), N-1), and the
|
|
56
|
+
exit test is that any Linux admin can take over a server with Debian
|
|
57
|
+
knowledge.
|
|
58
|
+
|
|
59
|
+
## Why not self-run VMs
|
|
60
|
+
|
|
61
|
+
A hypervisor on Cloudfall's hosts (Proxmox, KVM, Firecracker) is a second
|
|
62
|
+
infrastructure layer to provision, patch, back up and explain to whoever
|
|
63
|
+
takes over. A separate small server gives the same kernel boundary for a
|
|
64
|
+
few euros a month and fits the model Cloudfall already manages.
|
|
65
|
+
|
|
66
|
+
## The target model
|
|
67
|
+
|
|
68
|
+
The first four parts are required on every server, one server or many. The
|
|
69
|
+
last is a recommendation.
|
|
70
|
+
|
|
71
|
+
**Each unit is sandboxed by systemd.** The component unit gets, by default:
|
|
72
|
+
|
|
73
|
+
- its own user (or `DynamicUser=`), `NoNewPrivileges=` and an empty
|
|
74
|
+
`CapabilityBoundingSet=`
|
|
75
|
+
- `ProtectSystem=strict`, `ProtectHome=`, `PrivateTmp=`, `PrivateDevices=`,
|
|
76
|
+
writes only to `ReadWritePaths=` and `StateDirectory=`
|
|
77
|
+
- `ProtectProc=invisible`, `SystemCallFilter=@system-service`,
|
|
78
|
+
`RestrictNamespaces=`, `RestrictAddressFamilies=`
|
|
79
|
+
- `IPAddressAllow=` and `IPAddressDeny=` where the component's peers are
|
|
80
|
+
known
|
|
81
|
+
- `MemoryMax=`, `CPUQuota=`, `TasksMax=` from the component's declared
|
|
82
|
+
envelope ([`../fleet-goals.md`](../fleet-goals.md), goal 8)
|
|
83
|
+
- secrets through `LoadCredentialEncrypted=` rather than `EnvironmentFile=`,
|
|
84
|
+
so a value is readable by that unit only and never sits in the process
|
|
85
|
+
environment
|
|
86
|
+
|
|
87
|
+
`systemd-analyze security` scores a unit; a score threshold can become a
|
|
88
|
+
deploy gate.
|
|
89
|
+
|
|
90
|
+
**Shared services authenticate per component.** Each component gets its own
|
|
91
|
+
PostgreSQL role, preferably through `peer` authentication over the Unix
|
|
92
|
+
socket, so the unit's user is the role and no password exists. A service
|
|
93
|
+
without authentication is never shared between components of different
|
|
94
|
+
owners.
|
|
95
|
+
|
|
96
|
+
**Runners are ephemeral and sandboxed.** A runner executes untrusted code.
|
|
97
|
+
It runs one job per transient unit started with `systemd-run` under the same
|
|
98
|
+
sandbox and `DynamicUser=`, its state removed after the job, and its network
|
|
99
|
+
open only for the dependency step. On a server that also runs production
|
|
100
|
+
components, the runner's unit is also denied the production components'
|
|
101
|
+
directories and network peers (`InaccessiblePaths=`, `IPAddressDeny=`) and
|
|
102
|
+
holds no production keys. This is the most exposed point of a one-server
|
|
103
|
+
placement.
|
|
104
|
+
|
|
105
|
+
**Access is scoped by environment.** A runner holds the SSH and age keys of
|
|
106
|
+
one environment ([`../ci-deploy-guide.md`](../ci-deploy-guide.md)). Deploys
|
|
107
|
+
log in as a dedicated user with sudo limited to the commands a deploy runs,
|
|
108
|
+
not as root.
|
|
109
|
+
|
|
110
|
+
**Separate servers separate trust levels (recommended).** Where the budget
|
|
111
|
+
allows, these do not share a kernel:
|
|
112
|
+
|
|
113
|
+
- production and staging
|
|
114
|
+
- build and CI, and production; production servers then receive built
|
|
115
|
+
artifacts only
|
|
116
|
+
- components of different customers or owners
|
|
117
|
+
|
|
118
|
+
A team can only accept risk for its own projects. Placing components of
|
|
119
|
+
different owners on one server puts the risk on owners who did not accept
|
|
120
|
+
it, so Cloudfall keeps a separate, stronger warning for it. Cloudfall cannot
|
|
121
|
+
verify ownership, so this stays a warning, not a refusal.
|
|
122
|
+
|
|
123
|
+
## Where Cloudfall is today
|
|
124
|
+
|
|
125
|
+
- Each component already runs as its own application user with
|
|
126
|
+
`NoNewPrivileges=true` and `PrivateTmp=true`
|
|
127
|
+
(`engine/ansible/roles/cloudfall_deploy/templates/component.service.j2`);
|
|
128
|
+
the rest of the sandbox is missing
|
|
129
|
+
- SSH keys are scoped by environment, but the baseline installs them into
|
|
130
|
+
root's `authorized_keys`, so a runner's key is root on every server of its
|
|
131
|
+
environment
|
|
132
|
+
- Secrets reach units through `EnvironmentFile=`
|
|
133
|
+
- Resource envelopes are not declared or enforced yet
|
|
134
|
+
|
|
135
|
+
## Triggers to revisit
|
|
136
|
+
|
|
137
|
+
- **A customer needs untrusted code from several tenants on one server**,
|
|
138
|
+
where a separate server per tenant does not pay; then a stronger sandbox
|
|
139
|
+
(gVisor, microVMs) is the question, not containers
|
|
140
|
+
- **A required workload ships only as a container image** with no reasonable
|
|
141
|
+
Debian install path
|
|
142
|
+
- **systemd sandboxing proves insufficient** for a concrete attack that a
|
|
143
|
+
container runtime would have stopped
|
|
144
|
+
|
|
145
|
+
## Consequences
|
|
146
|
+
|
|
147
|
+
- The component unit template gains the sandbox directives above as
|
|
148
|
+
defaults, with per-component opt-outs that are declared, not silent
|
|
149
|
+
- No container runtime, image build or registry appears in Cloudfall's
|
|
150
|
+
engine or docs as the way to isolate components
|
|
151
|
+
- One server is a supported placement, not a degraded one; the required
|
|
152
|
+
parts above carry the whole isolation there
|
|
153
|
+
- A plan that puts two trust levels on one server declares it as an
|
|
154
|
+
accepted risk; Cloudfall warns when it is undeclared, and warns again,
|
|
155
|
+
more strongly, when the trust levels belong to different owners
|
|
@@ -75,3 +75,11 @@ entry that supersedes it, never by editing an old one.
|
|
|
75
75
|
- Why: psql substitutes a variable into the statement text, so ALTER ROLE ... PASSWORD with the plaintext puts the password in the server's log under log_statement ddl or all, or on a failing statement; PostgreSQL stores a pre-hashed verifier as it is
|
|
76
76
|
- Applies to: engine/ansible/roles/cloudfall_postgresql/tasks/owner_password.yml, sdk/src/cloudfall/secrets.py, engine/src/cloudfall_engine/cli.py
|
|
77
77
|
- Enforced by: a regression test that the staged file holds a verifier and never the password
|
|
78
|
+
|
|
79
|
+
## D-10: Components are isolated by the systemd sandbox, and separate servers are recommended, not required
|
|
80
|
+
|
|
81
|
+
- Decided: 2026-11-02, by the maintainer; reasoning in docs/decisions/2026-11-02-isolation-without-containers.md
|
|
82
|
+
- Rule: On every server, components are kept apart by each unit's systemd sandbox (own user, ProtectSystem, ProtectHome, PrivateTmp, ProtectProc, SystemCallFilter, capability and address-family limits, cgroup limits), a database role per component, secrets through systemd credentials, keys scoped by environment and a non-root deploy user; runners are ephemeral sandboxed units, denied production directories, network peers and keys when they share a server with production; separate servers for different trust levels (production and staging, build or CI and production, different owners) are recommended, and a team may place everything on one server, several independent projects included, by declaring the accepted risk in the plan; Cloudfall warns about an undeclared placement of two trust levels on one server, and warns more strongly when they belong to different owners; Cloudfall adds no container runtime and no hypervisor of its own
|
|
83
|
+
- Why: A Unix user alone keeps only files and signals apart; containers use the same kernel mechanisms systemd exposes and add no boundary, only a runtime to operate; a separate kernel is the one stronger boundary and comes from a separate server, but requiring it would rule out the one-server teams Cloudfall serves, and a team can accept that risk for its own projects, not for other owners
|
|
84
|
+
- Applies to: engine/ansible/roles/cloudfall_deploy/templates/component.service.j2, engine/ansible/roles/cloudfall_access, server placement in plans, runner and CI setup
|
|
85
|
+
- Enforced by: review
|
|
@@ -192,8 +192,11 @@ proposals it executed and which it withheld, with reasons.
|
|
|
192
192
|
Approval is not a tool. A person approves a proposal by running
|
|
193
193
|
`cloudfall operator approve` from a shell, as in
|
|
194
194
|
[Reviewing and approving](#reviewing-and-approving); an agent that could
|
|
195
|
-
call it would run and verify the change it proposed itself. The
|
|
196
|
-
|
|
195
|
+
call it would run and verify the change it proposed itself. The same holds
|
|
196
|
+
for `cloudfall release approve`: the `deploy` tool deploys a release of an
|
|
197
|
+
application that declares `approval: manual` only once a person approved
|
|
198
|
+
it at a terminal, and no tool writes that approval. The watch loop is not a
|
|
199
|
+
tool either: it runs until stopped, as the service above.
|
|
197
200
|
|
|
198
201
|
The agent is the brain here and the operator is the record. The agent can
|
|
199
202
|
read the proposals, relay the diagnosis and recommend one; a person
|
|
@@ -127,6 +127,8 @@ and validation rejects them if they do.
|
|
|
127
127
|
```console
|
|
128
128
|
uv run cloudfall migrate --build acme-api=main
|
|
129
129
|
uv run cloudfall migrate --build acme-api=main --yes
|
|
130
|
+
uv run cloudfall release approve acme-api --release <release-id>
|
|
131
|
+
uv run cloudfall migrate --yes
|
|
130
132
|
```
|
|
131
133
|
|
|
132
134
|
Without `--yes` the command shows the plan. With it, the plan executes step
|
|
@@ -134,7 +136,19 @@ by step: server baseline and firewall, infrastructure services, artifact
|
|
|
134
136
|
builds, health-gated deployments, HTTP routes, a DNS-verification pause at
|
|
135
137
|
the cutover moment, TLS issuance, and a final evidence pass requiring a
|
|
136
138
|
compliant audit and healthy routes. Progress persists after every step, so
|
|
137
|
-
a failure or
|
|
139
|
+
a failure or a pause resumes exactly where it stopped.
|
|
140
|
+
|
|
141
|
+
The importer writes `approval: manual` for every application, so the run
|
|
142
|
+
pauses at `deploy:acme-api` (exit 3, `PARTIAL_FAILURE`, with
|
|
143
|
+
`context.code: deployment_approval_missing`) once the release is built. The
|
|
144
|
+
answer names the built release and the exact `cloudfall release approve`
|
|
145
|
+
command. A person runs it at a terminal: it shows the git commit, the archive
|
|
146
|
+
SHA-256 and the servers, and records the approval under
|
|
147
|
+
`decisions/releases/` once the release id is typed back. `migrate --yes` then
|
|
148
|
+
resumes at the deploy step with the release the build step recorded,
|
|
149
|
+
without building again. When another gate, such as GitHub's required
|
|
150
|
+
reviewers, is the real one, switch the application to `approval: automatic`
|
|
151
|
+
and the run deploys without stopping.
|
|
138
152
|
|
|
139
153
|
## Step 6: migrate the data
|
|
140
154
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# S-008: Enforce manual approval
|
|
2
2
|
|
|
3
|
-
status:
|
|
3
|
+
status: built
|
|
4
4
|
serves: G-2
|
|
5
5
|
|
|
6
6
|
## Problem
|
|
@@ -186,3 +186,22 @@ Touches `sdk/src/cloudfall/app.py` (`deploy`, `rollback`, `migrate`, new `releas
|
|
|
186
186
|
- cloudfall-dev/cloudfall#165: S-008-13, S-008-14
|
|
187
187
|
|
|
188
188
|
## Verification
|
|
189
|
+
|
|
190
|
+
Every criterion ran through the CLI (or `cloudfall mcp serve`'s tool call) against a copy of the example project, with an engine whose playbooks stand in for the real ones: they run `ansible-playbook` on localhost and leave a marker and a release receipt, so no host was contacted. The S-008-14 artifact was built for real by `cloudfall-engine artifact build` from a local git repository. The terminal attestation ran with a stream that reports itself a terminal; no person typed at a real one, and no deploy reached a real server.
|
|
191
|
+
|
|
192
|
+
- S-008-1: `test_s008_1_a_manual_application_without_approval_is_refused`, exit 4 `PRECONDITION` with `deployment_approval_missing`, the message named `crm`, `crm-backend`, the release and the command, `context.command` was the exact `release approve` command, and no marker, inventory or receipt was written
|
|
193
|
+
- S-008-2: `test_s008_2_a_person_approves_a_release_by_typing_its_id` and `test_s008_2_the_record_id_is_an_approval_time_and_16_hex`, exit 0 and one file under `decisions/releases/` that validated against `release-approval.schema.json` with every named field, `expiresAt` 24 hours after `approvedAt` and `via: terminal`
|
|
194
|
+
- S-008-3: `test_s008_3_an_approved_release_deploys_and_names_its_approval`, the deploy playbook ran and the result's `approval` carried the record's `path`, `approver` and `approved_at`
|
|
195
|
+
- S-008-4: `test_s008_4_off_a_terminal_nothing_is_approved` and `test_s008_4_a_mistyped_release_id_approves_nothing`, exit 4 `PERSON_REQUIRED` and `ATTESTATION_MISMATCH` with and without `--yes`, and the approvals folder stayed empty
|
|
196
|
+
- S-008-5: `test_s008_5_the_deploy_tool_refuses_an_unapproved_manual_release` and `test_s008_5_the_project_instructions_name_release_approve`, no `release_approve` tool was listed and the `deploy` tool with `yes: true` returned `PRECONDITION` with `deployment_approval_missing`
|
|
197
|
+
- S-008-6: `test_s008_6_a_record_for_another_archive_does_not_count`, `test_s008_6_a_record_for_other_servers_does_not_count`, `test_s008_6_an_expired_record_does_not_count` and `test_s008_6_the_servers_are_compared_as_a_set`, each refusal listed the record with `digest_mismatch`, `servers_changed` or `expired`, and servers in another order still counted
|
|
198
|
+
- S-008-7: `test_s008_7_a_record_for_another_release_or_component_does_not_count`, refused with `deployment_approval_missing` and neither record listed
|
|
199
|
+
- S-008-8: `test_s008_8_the_plan_shows_the_approval_before_and_after_it_is_given` and `test_s008_8_the_latest_counting_approval_is_shown`, exit 0 with no playbook run, `status: missing` with `command` before the approval and `status: approved` with `path`, `approver` and `expires_at` after it, the latest of two counting records shown
|
|
200
|
+
- S-008-9: `test_s008_9_an_automatic_application_needs_no_record`, plan and `--yes` both exit 0 with `approval: {"policy": "automatic"}` beside an approvals file that isn't JSON, and the deploy ran
|
|
201
|
+
- S-008-10: `test_s008_10_a_broken_record_stops_a_manual_deploy` and `test_s008_10_deploy_declares_record_invalid`, exit 86 `RECORD_INVALID` naming the file for invalid JSON, a schema mismatch and a non-object, plan and `--yes` alike, and `deploy --schema` declares the exit
|
|
202
|
+
- S-008-11: `test_s008_11_an_automatic_application_needs_no_approval`, `test_s008_11_a_missing_artifact_is_refused_before_asking` and `test_s008_11_a_tampered_artifact_is_refused_before_asking`, `PRECONDITION` with `release_approval_not_required`, `lifecycle_artifact_missing` and `lifecycle_artifact_digest_mismatch`, no prompt and no file
|
|
203
|
+
- S-008-12: `test_s008_12_a_validity_out_of_range_is_a_usage_error` and `test_s008_12_a_validity_of_two_hours_expires_two_hours_later`, `--valid-for 0` and `169` were usage errors naming `--valid-for`, and `2` wrote `expiresAt` two hours after `approvedAt`
|
|
204
|
+
- S-008-13: `test_s008_13_rollback_needs_no_approval`, for a `manual` application the plan showed `approval` with `policy: manual`, `required: false` and a reason, and `rollback --yes` ran the rollback playbook with no approvals folder at all; an `automatic` application's plan showed `{"policy": "automatic"}`
|
|
205
|
+
- S-008-14: `test_s008_14_migrate_pauses_for_an_approval_and_resumes_without_a_rebuild`, `migrate --build crm-backend=main --yes` exited 3 `PARTIAL_FAILURE` at `deploy:crm-backend` with `deployment_approval_missing`, the release the completed build step recorded and the `release approve` command, no deploy playbook ran; after `release approve`, with the source repository deleted so a second build could not succeed, `migrate --yes` completed `deploy:crm-backend` with that release and the record as its `approval`, and the artifacts folder held the same files. `test_s008_14_a_broken_approval_record_stops_the_deploy_step` (exit 86 `RECORD_INVALID`) and `test_s008_14_an_automatic_application_migrates_without_a_record` cover the step's other answers
|
|
206
|
+
- S-008-15: `test_s008_15_a_failed_health_gate_keeps_the_approval_for_a_retry`, the failed deploy exited `ENGINE_STEP_FAILED`, the record's bytes were unchanged and the retry deployed on the same record
|
|
207
|
+
- S-008-16: `test_s008_16_the_changelog_and_ci_guide_say_deploy_enforces_manual`, the fragment's `### Breaking` entry names `deploy --yes`, `migrate --yes`, `approval: manual`, `cloudfall release approve` and `approval: automatic`, and the CI guide names `cloudfall release approve` and no longer says no command acts on `approval`
|
|
@@ -17,6 +17,7 @@ from importlib.metadata import version
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import TYPE_CHECKING, Self, cast
|
|
19
19
|
|
|
20
|
+
from cloudfall.ansible_data import as_ansible_data, unmarked
|
|
20
21
|
from cloudfall.commands import snake_case_keys
|
|
21
22
|
from cloudfall.domain import (
|
|
22
23
|
ARTIFACT_CLONE_TIMEOUT_SECONDS,
|
|
@@ -223,8 +224,14 @@ class RenderedInventory:
|
|
|
223
224
|
],
|
|
224
225
|
)
|
|
225
226
|
def render(args: RenderArgs, _ctx: Ctx, project: Project) -> RenderedInventory:
|
|
226
|
-
"""Render the inventory, and write it when ``--output`` names a file.
|
|
227
|
-
|
|
227
|
+
"""Render the inventory, and write it when ``--output`` names a file.
|
|
228
|
+
|
|
229
|
+
Every declared string is marked unsafe, so Ansible never templates it.
|
|
230
|
+
"""
|
|
231
|
+
rendered = cast(
|
|
232
|
+
"dict[str, object]",
|
|
233
|
+
as_ansible_data(render_ansible_inventory(project.inventory)),
|
|
234
|
+
)
|
|
228
235
|
if args.output is None:
|
|
229
236
|
return RenderedInventory(rendered)
|
|
230
237
|
output = project.path(args.output)
|
|
@@ -595,8 +602,12 @@ def _rendered_owned_databases(
|
|
|
595
602
|
if not inventory_file.is_file():
|
|
596
603
|
return ()
|
|
597
604
|
try:
|
|
605
|
+
# The rendered strings are marked unsafe for Ansible (#174).
|
|
598
606
|
document = cast(
|
|
599
|
-
"object",
|
|
607
|
+
"object",
|
|
608
|
+
json.loads(
|
|
609
|
+
inventory_file.read_text(encoding="utf-8"), object_hook=unmarked
|
|
610
|
+
),
|
|
600
611
|
)
|
|
601
612
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
602
613
|
return ()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"all": {"children": {"cloudfall_applications": {"children": {"application_crm": {"hosts": {"h1": {}, "h2": {}}}}}, "cloudfall_components": {"children": {"component_crm_backend": {"hosts": {"h1": {}, "h2": {}}}}}, "cloudfall_domain_origins": {"hosts": {"h1": {}}}, "cloudfall_environments": {"children": {"environment_production": {"hosts": {"h1": {}, "h2": {}}}}}, "cloudfall_logging_backends": {"hosts": {"h1": {}}}, "cloudfall_logging_collectors": {"hosts": {"h1": {}, "h2": {}}}, "cloudfall_release_servers": {"hosts": {}}, "cloudfall_servers": {"hosts": {"h1": {"ansible_host": {"__ansible_unsafe": "h1.example.internal"}, "ansible_port": 22, "ansible_user": {"__ansible_unsafe": "cloudfall"}, "cloudfall_alert_rules": [{"environment": {"__ansible_unsafe": "production"}, "expr": {"__ansible_unsafe": "pg_up{service=\"postgresql-main\"} == 0"}, "for": {"__ansible_unsafe": "1m"}, "id": {"__ansible_unsafe": "postgresql-down"}, "severity": {"__ansible_unsafe": "critical"}, "summary": {"__ansible_unsafe": "PostgreSQL postgresql-main is not answering its exporter"}}], "cloudfall_applications": [{"approval": {"__ansible_unsafe": "manual"}, "id": {"__ansible_unsafe": "crm"}, "user": {"__ansible_unsafe": "crm"}}], "cloudfall_components": [{"application": {"__ansible_unsafe": "crm"}, "application_user": {"__ansible_unsafe": "crm"}, "healthCheck": {"attempts": 5, "expectedStatuses": [200], "path": {"__ansible_unsafe": "/health"}, "port": 8100, "scheme": {"__ansible_unsafe": "http"}, "timeoutSeconds": 5, "type": {"__ansible_unsafe": "http"}}, "id": {"__ansible_unsafe": "crm-backend"}, "install_root": {"__ansible_unsafe": "/srv/apps/crm/backend"}, "repository": {"url": {"__ansible_unsafe": "https://github.com/example/crm-backend.git"}}, "retainUntilCleanup": true, "runtime": {"packageManager": {"__ansible_unsafe": "uv"}, "type": {"__ansible_unsafe": "python"}, "version": {"__ansible_unsafe": "3.14"}}, "service": {"command": [{"__ansible_unsafe": ".venv/bin/uvicorn"}, {"__ansible_unsafe": "crm.asgi:application"}, {"__ansible_unsafe": "--host"}, {"__ansible_unsafe": "127.0.0.1"}, {"__ansible_unsafe": "--port"}, {"__ansible_unsafe": "8100"}], "manager": {"__ansible_unsafe": "systemd"}, "name": {"__ansible_unsafe": "crm-backend"}}}], "cloudfall_domains": [], "cloudfall_environment": {"__ansible_unsafe": "production"}, "cloudfall_firewall": {"allowedInbound": [{"description": {"__ansible_unsafe": "ssh"}, "port": 22, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "http"}, "port": 80, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "https"}, "port": 443, "protocol": {"__ansible_unsafe": "tcp"}}], "policy": {"__ansible_unsafe": "default-deny"}}, "cloudfall_hostname": {"__ansible_unsafe": "h1.example.internal"}, "cloudfall_labels": {"region": {"__ansible_unsafe": "eu-central"}, "workload": {"__ansible_unsafe": "application"}}, "cloudfall_lifecycle": {"__ansible_unsafe": "active"}, "cloudfall_logging_backend": {"alerting": {"alertmanager": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9093}, "receivers": [{"id": {"__ansible_unsafe": "operations-webhook"}, "webhook": {"url": {"__ansible_unsafe": "https://hooks.example.internal/cloudfall"}}}, {"email": {"authPasswordFile": {"__ansible_unsafe": "/etc/cloudfall/logging/smtp-password"}, "authUsername": {"__ansible_unsafe": "alerts@example.internal"}, "from": {"__ansible_unsafe": "alerts@example.internal"}, "smarthost": {"__ansible_unsafe": "smtp.example.internal:587"}, "to": {"__ansible_unsafe": "oncall@example.internal"}}, "id": {"__ansible_unsafe": "operations-email"}}]}, "backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3100, "retentionHours": 720, "server": {"__ansible_unsafe": "h1"}, "storage": {"path": {"__ansible_unsafe": "/var/lib/loki-cloudfall"}, "type": {"__ansible_unsafe": "filesystem"}}}, "collectors": {"clientTls": {"caPath": {"__ansible_unsafe": "/etc/cloudfall/logging/ca.crt"}, "certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.key"}}, "files": [{"application": {"__ansible_unsafe": "crm"}, "component": {"__ansible_unsafe": "crm-backend"}, "id": {"__ansible_unsafe": "crm-backend"}, "path": {"__ansible_unsafe": "/var/log/crm/backend/*.log"}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}], "journal": {"enabled": true, "maxAgeHours": 24}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}, "environment": {"__ansible_unsafe": "production"}, "gateway": {"listenAddress": {"__ansible_unsafe": "0.0.0.0"}, "port": 3101, "serverName": {"__ansible_unsafe": "logs.example.internal"}, "tls": {"certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.crt"}, "clientCaPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client-ca.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.key"}}}, "grafana": {"adminPasswordPath": {"__ansible_unsafe": "/etc/cloudfall/logging/grafana-admin-password"}, "listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3000}, "id": {"__ansible_unsafe": "operations"}, "metrics": {"backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9090, "retentionHours": 360, "storagePath": {"__ansible_unsafe": "/var/lib/prometheus-cloudfall"}}, "collection": {"intervalSeconds": 60}}, "migration": {"mode": {"__ansible_unsafe": "parallel"}, "preserveLegacyAgents": true}, "software": {"alloyPackageVersion": {"__ansible_unsafe": "1.17.1-1"}, "grafanaPackageVersion": {"__ansible_unsafe": "13.1.0"}, "lokiPackageVersion": {"__ansible_unsafe": "3.7.2"}}}, "cloudfall_logging_collector": {"alerting": {"alertmanager": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9093}, "receivers": [{"id": {"__ansible_unsafe": "operations-webhook"}, "webhook": {"url": {"__ansible_unsafe": "https://hooks.example.internal/cloudfall"}}}, {"email": {"authPasswordFile": {"__ansible_unsafe": "/etc/cloudfall/logging/smtp-password"}, "authUsername": {"__ansible_unsafe": "alerts@example.internal"}, "from": {"__ansible_unsafe": "alerts@example.internal"}, "smarthost": {"__ansible_unsafe": "smtp.example.internal:587"}, "to": {"__ansible_unsafe": "oncall@example.internal"}}, "id": {"__ansible_unsafe": "operations-email"}}]}, "backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3100, "retentionHours": 720, "server": {"__ansible_unsafe": "h1"}, "storage": {"path": {"__ansible_unsafe": "/var/lib/loki-cloudfall"}, "type": {"__ansible_unsafe": "filesystem"}}}, "collectors": {"clientTls": {"caPath": {"__ansible_unsafe": "/etc/cloudfall/logging/ca.crt"}, "certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.key"}}, "files": [{"application": {"__ansible_unsafe": "crm"}, "component": {"__ansible_unsafe": "crm-backend"}, "id": {"__ansible_unsafe": "crm-backend"}, "path": {"__ansible_unsafe": "/var/log/crm/backend/*.log"}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}], "journal": {"enabled": true, "maxAgeHours": 24}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}, "environment": {"__ansible_unsafe": "production"}, "gateway": {"listenAddress": {"__ansible_unsafe": "0.0.0.0"}, "port": 3101, "serverName": {"__ansible_unsafe": "logs.example.internal"}, "tls": {"certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.crt"}, "clientCaPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client-ca.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.key"}}}, "grafana": {"adminPasswordPath": {"__ansible_unsafe": "/etc/cloudfall/logging/grafana-admin-password"}, "listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3000}, "id": {"__ansible_unsafe": "operations"}, "metrics": {"backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9090, "retentionHours": 360, "storagePath": {"__ansible_unsafe": "/var/lib/prometheus-cloudfall"}}, "collection": {"intervalSeconds": 60}}, "migration": {"mode": {"__ansible_unsafe": "parallel"}, "preserveLegacyAgents": true}, "software": {"alloyPackageVersion": {"__ansible_unsafe": "1.17.1-1"}, "grafanaPackageVersion": {"__ansible_unsafe": "13.1.0"}, "lokiPackageVersion": {"__ansible_unsafe": "3.7.2"}}}, "cloudfall_origin_domains": [{"aliases": [{"__ansible_unsafe": "www.crm.example.test"}], "edge": {"mode": {"__ansible_unsafe": "dns-only"}, "provider": {"__ansible_unsafe": "cloudflare"}}, "healthCheck": {"expectedStatuses": [200], "path": {"__ansible_unsafe": "/health"}, "scheme": {"__ansible_unsafe": "https"}, "timeoutSeconds": 5}, "id": {"__ansible_unsafe": "crm-site"}, "origin": {"configurationPath": {"__ansible_unsafe": "/etc/nginx/sites-available/crm-origin.conf"}, "port": 8100, "scheme": {"__ansible_unsafe": "http"}, "server": {"__ansible_unsafe": "h1"}, "serverName": {"__ansible_unsafe": "crm.example.test"}, "service": {"__ansible_unsafe": "nginx.service"}}, "primaryName": {"__ansible_unsafe": "crm.example.test"}, "proxy": {"configurationPath": {"__ansible_unsafe": "/etc/nginx/sites-available/crm.example.test.conf"}, "server": {"__ansible_unsafe": "h2"}, "service": {"__ansible_unsafe": "nginx.service"}, "upstream": {"address": {"__ansible_unsafe": "127.0.0.1"}, "port": 8100}}, "tls": {"mode": {"__ansible_unsafe": "required"}}}], "cloudfall_server_id": {"__ansible_unsafe": "h1"}, "cloudfall_server_type": {"configuration": {"files": [{"capture": {"__ansible_unsafe": "hash"}, "group": {"__ansible_unsafe": "root"}, "mode": {"__ansible_unsafe": "0644"}, "owner": {"__ansible_unsafe": "root"}, "path": {"__ansible_unsafe": "/etc/ssh/sshd_config"}}]}, "firewall": {"allowedInbound": [{"description": {"__ansible_unsafe": "http"}, "port": 80, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "https"}, "port": 443, "protocol": {"__ansible_unsafe": "tcp"}}], "policy": {"__ansible_unsafe": "default-deny"}}, "id": {"__ansible_unsafe": "debian-application"}, "os": {"distribution": {"__ansible_unsafe": "Debian"}, "serviceManager": {"__ansible_unsafe": "systemd"}, "versions": [{"__ansible_unsafe": "12"}, {"__ansible_unsafe": "13"}]}, "packages": {"forbidden": [{"__ansible_unsafe": "telnetd"}], "required": [{"name": {"__ansible_unsafe": "acl"}}, {"name": {"__ansible_unsafe": "ca-certificates"}}, {"name": {"__ansible_unsafe": "curl"}}, {"name": {"__ansible_unsafe": "git"}}, {"name": {"__ansible_unsafe": "rsync"}}, {"name": {"__ansible_unsafe": "unzip"}}]}, "services": {"required": [{"name": {"__ansible_unsafe": "ssh.service"}, "state": {"__ansible_unsafe": "running"}, "status": {"__ansible_unsafe": "enabled"}}, {"name": {"__ansible_unsafe": "apt-daily.timer"}, "state": {"__ansible_unsafe": "running"}, "status": {"__ansible_unsafe": "enabled"}}]}, "storage": {"mounts": [{"filesystem": {"__ansible_unsafe": "ext4"}, "minimumBytes": 850000000000, "path": {"__ansible_unsafe": "/"}}], "softwareRaid": {"level": {"__ansible_unsafe": "raid1"}, "minimumActiveDevices": 2, "minimumUsableBytes": 900000000000}}}, "cloudfall_services": [{"backup": {"directory": {"__ansible_unsafe": "/var/backups/cloudfall/postgresql-main"}, "onCalendar": {"__ansible_unsafe": "*-*-* 02:00:00 UTC"}, "restoreCheckOnCalendar": {"__ansible_unsafe": "*-*-* 05:00:00 UTC"}, "retentionDays": 14}, "bind": {"address": {"__ansible_unsafe": "127.0.0.1"}, "port": 5432}, "environment": {"__ansible_unsafe": "production"}, "id": {"__ansible_unsafe": "postgresql-main"}, "metrics": {"enabled": true}, "postgresql": {"databases": [{"application": {"__ansible_unsafe": "crm"}, "name": {"__ansible_unsafe": "crm"}, "owner": {"__ansible_unsafe": "crm"}}], "majorVersion": {"__ansible_unsafe": "17"}}, "server": {"__ansible_unsafe": "h1"}, "serviceKind": {"__ansible_unsafe": "postgresql"}}, {"backup": {"directory": {"__ansible_unsafe": "/var/backups/cloudfall/redis-cache"}, "onCalendar": {"__ansible_unsafe": "*-*-* 03:00:00 UTC"}, "restoreCheckOnCalendar": {"__ansible_unsafe": "*-*-* 05:30:00 UTC"}, "retentionDays": 7}, "bind": {"address": {"__ansible_unsafe": "127.0.0.1"}, "port": 6379}, "environment": {"__ansible_unsafe": "production"}, "id": {"__ansible_unsafe": "redis-cache"}, "metrics": {"enabled": true}, "redis": {"appendOnly": true, "maxmemoryMb": 256}, "server": {"__ansible_unsafe": "h1"}, "serviceKind": {"__ansible_unsafe": "redis"}}], "cloudfall_ssh_public_keys": [{"algorithm": {"__ansible_unsafe": "ssh-rsa"}, "environment": {"__ansible_unsafe": "production"}, "id": {"__ansible_unsafe": "example-admin"}, "lifecycle": {"__ansible_unsafe": "active"}, "owner": {"__ansible_unsafe": "example-admin"}, "publicKey": {"__ansible_unsafe": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqmOqPPapyG6hL5FlR/KOK2ENEvi/2qlhoRtrgK7JfafHXb1i4WJwf5FMGrPE4DBRfDQDwokABdvUPITZOymFMgiGMmFKGnp0xq+/B0wTTOtdnr8GC3sCmSRwuFDrroVWwfjIqyC/wlpoPXpSvyc1VKBuXOe/gLv1tTzDH3Jc+3fUYaUCj+uIVO65n0N2ajNg9ibBsCjiUp9CrWEoprqxR4lz3TaiPuRtMxWQlQ2NKPSTdUZWqg9KVXm3TS8xgf4Ox2dFwedVMcucC8UBBAwetgylXNZMv1yAMYD3QDokLi0AC+XnNR+UhtFXr9Y3VveQfH4YKnv0Fhr2opCyN99Lr example"}}]}, "h2": {"ansible_host": {"__ansible_unsafe": "h2.example.internal"}, "ansible_port": 22, "ansible_user": {"__ansible_unsafe": "cloudfall"}, "cloudfall_applications": [{"approval": {"__ansible_unsafe": "manual"}, "id": {"__ansible_unsafe": "crm"}, "user": {"__ansible_unsafe": "crm"}}], "cloudfall_components": [{"application": {"__ansible_unsafe": "crm"}, "application_user": {"__ansible_unsafe": "crm"}, "healthCheck": {"attempts": 5, "expectedStatuses": [200], "path": {"__ansible_unsafe": "/health"}, "port": 8100, "scheme": {"__ansible_unsafe": "http"}, "timeoutSeconds": 5, "type": {"__ansible_unsafe": "http"}}, "id": {"__ansible_unsafe": "crm-backend"}, "install_root": {"__ansible_unsafe": "/srv/apps/crm/backend"}, "repository": {"url": {"__ansible_unsafe": "https://github.com/example/crm-backend.git"}}, "retainUntilCleanup": true, "runtime": {"packageManager": {"__ansible_unsafe": "uv"}, "type": {"__ansible_unsafe": "python"}, "version": {"__ansible_unsafe": "3.14"}}, "service": {"command": [{"__ansible_unsafe": ".venv/bin/uvicorn"}, {"__ansible_unsafe": "crm.asgi:application"}, {"__ansible_unsafe": "--host"}, {"__ansible_unsafe": "127.0.0.1"}, {"__ansible_unsafe": "--port"}, {"__ansible_unsafe": "8100"}], "manager": {"__ansible_unsafe": "systemd"}, "name": {"__ansible_unsafe": "crm-backend"}}}], "cloudfall_domains": [{"aliases": [{"__ansible_unsafe": "www.crm.example.test"}], "edge": {"mode": {"__ansible_unsafe": "dns-only"}, "provider": {"__ansible_unsafe": "cloudflare"}}, "healthCheck": {"expectedStatuses": [200], "path": {"__ansible_unsafe": "/health"}, "scheme": {"__ansible_unsafe": "https"}, "timeoutSeconds": 5}, "id": {"__ansible_unsafe": "crm-site"}, "origin": {"configurationPath": {"__ansible_unsafe": "/etc/nginx/sites-available/crm-origin.conf"}, "port": 8100, "scheme": {"__ansible_unsafe": "http"}, "server": {"__ansible_unsafe": "h1"}, "serverName": {"__ansible_unsafe": "crm.example.test"}, "service": {"__ansible_unsafe": "nginx.service"}}, "primaryName": {"__ansible_unsafe": "crm.example.test"}, "proxy": {"configurationPath": {"__ansible_unsafe": "/etc/nginx/sites-available/crm.example.test.conf"}, "server": {"__ansible_unsafe": "h2"}, "service": {"__ansible_unsafe": "nginx.service"}, "upstream": {"address": {"__ansible_unsafe": "127.0.0.1"}, "port": 8100}}, "tls": {"mode": {"__ansible_unsafe": "required"}}}], "cloudfall_environment": {"__ansible_unsafe": "production"}, "cloudfall_firewall": {"allowedInbound": [{"description": {"__ansible_unsafe": "ssh"}, "port": 22, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "http"}, "port": 80, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "https"}, "port": 443, "protocol": {"__ansible_unsafe": "tcp"}}], "policy": {"__ansible_unsafe": "default-deny"}}, "cloudfall_hostname": {"__ansible_unsafe": "h2.example.internal"}, "cloudfall_labels": {"region": {"__ansible_unsafe": "eu-central"}, "workload": {"__ansible_unsafe": "application"}}, "cloudfall_lifecycle": {"__ansible_unsafe": "active"}, "cloudfall_logging_collector": {"alerting": {"alertmanager": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9093}, "receivers": [{"id": {"__ansible_unsafe": "operations-webhook"}, "webhook": {"url": {"__ansible_unsafe": "https://hooks.example.internal/cloudfall"}}}, {"email": {"authPasswordFile": {"__ansible_unsafe": "/etc/cloudfall/logging/smtp-password"}, "authUsername": {"__ansible_unsafe": "alerts@example.internal"}, "from": {"__ansible_unsafe": "alerts@example.internal"}, "smarthost": {"__ansible_unsafe": "smtp.example.internal:587"}, "to": {"__ansible_unsafe": "oncall@example.internal"}}, "id": {"__ansible_unsafe": "operations-email"}}]}, "backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3100, "retentionHours": 720, "server": {"__ansible_unsafe": "h1"}, "storage": {"path": {"__ansible_unsafe": "/var/lib/loki-cloudfall"}, "type": {"__ansible_unsafe": "filesystem"}}}, "collectors": {"clientTls": {"caPath": {"__ansible_unsafe": "/etc/cloudfall/logging/ca.crt"}, "certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client.key"}}, "files": [{"application": {"__ansible_unsafe": "crm"}, "component": {"__ansible_unsafe": "crm-backend"}, "id": {"__ansible_unsafe": "crm-backend"}, "path": {"__ansible_unsafe": "/var/log/crm/backend/*.log"}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}], "journal": {"enabled": true, "maxAgeHours": 24}, "servers": [{"__ansible_unsafe": "h1"}, {"__ansible_unsafe": "h2"}]}, "environment": {"__ansible_unsafe": "production"}, "gateway": {"listenAddress": {"__ansible_unsafe": "0.0.0.0"}, "port": 3101, "serverName": {"__ansible_unsafe": "logs.example.internal"}, "tls": {"certificatePath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.crt"}, "clientCaPath": {"__ansible_unsafe": "/etc/cloudfall/logging/client-ca.crt"}, "keyPath": {"__ansible_unsafe": "/etc/cloudfall/logging/server.key"}}}, "grafana": {"adminPasswordPath": {"__ansible_unsafe": "/etc/cloudfall/logging/grafana-admin-password"}, "listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 3000}, "id": {"__ansible_unsafe": "operations"}, "metrics": {"backend": {"listenAddress": {"__ansible_unsafe": "127.0.0.1"}, "port": 9090, "retentionHours": 360, "storagePath": {"__ansible_unsafe": "/var/lib/prometheus-cloudfall"}}, "collection": {"intervalSeconds": 60}}, "migration": {"mode": {"__ansible_unsafe": "parallel"}, "preserveLegacyAgents": true}, "software": {"alloyPackageVersion": {"__ansible_unsafe": "1.17.1-1"}, "grafanaPackageVersion": {"__ansible_unsafe": "13.1.0"}, "lokiPackageVersion": {"__ansible_unsafe": "3.7.2"}}}, "cloudfall_origin_domains": [], "cloudfall_server_id": {"__ansible_unsafe": "h2"}, "cloudfall_server_type": {"configuration": {"files": [{"capture": {"__ansible_unsafe": "hash"}, "group": {"__ansible_unsafe": "root"}, "mode": {"__ansible_unsafe": "0644"}, "owner": {"__ansible_unsafe": "root"}, "path": {"__ansible_unsafe": "/etc/ssh/sshd_config"}}]}, "firewall": {"allowedInbound": [{"description": {"__ansible_unsafe": "http"}, "port": 80, "protocol": {"__ansible_unsafe": "tcp"}}, {"description": {"__ansible_unsafe": "https"}, "port": 443, "protocol": {"__ansible_unsafe": "tcp"}}], "policy": {"__ansible_unsafe": "default-deny"}}, "id": {"__ansible_unsafe": "debian-application"}, "os": {"distribution": {"__ansible_unsafe": "Debian"}, "serviceManager": {"__ansible_unsafe": "systemd"}, "versions": [{"__ansible_unsafe": "12"}, {"__ansible_unsafe": "13"}]}, "packages": {"forbidden": [{"__ansible_unsafe": "telnetd"}], "required": [{"name": {"__ansible_unsafe": "acl"}}, {"name": {"__ansible_unsafe": "ca-certificates"}}, {"name": {"__ansible_unsafe": "curl"}}, {"name": {"__ansible_unsafe": "git"}}, {"name": {"__ansible_unsafe": "rsync"}}, {"name": {"__ansible_unsafe": "unzip"}}]}, "services": {"required": [{"name": {"__ansible_unsafe": "ssh.service"}, "state": {"__ansible_unsafe": "running"}, "status": {"__ansible_unsafe": "enabled"}}, {"name": {"__ansible_unsafe": "apt-daily.timer"}, "state": {"__ansible_unsafe": "running"}, "status": {"__ansible_unsafe": "enabled"}}]}, "storage": {"mounts": [{"filesystem": {"__ansible_unsafe": "ext4"}, "minimumBytes": 850000000000, "path": {"__ansible_unsafe": "/"}}], "softwareRaid": {"level": {"__ansible_unsafe": "raid1"}, "minimumActiveDevices": 2, "minimumUsableBytes": 900000000000}}}, "cloudfall_services": [], "cloudfall_ssh_public_keys": [{"algorithm": {"__ansible_unsafe": "ssh-rsa"}, "environment": {"__ansible_unsafe": "production"}, "id": {"__ansible_unsafe": "example-admin"}, "lifecycle": {"__ansible_unsafe": "active"}, "owner": {"__ansible_unsafe": "example-admin"}, "publicKey": {"__ansible_unsafe": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqmOqPPapyG6hL5FlR/KOK2ENEvi/2qlhoRtrgK7JfafHXb1i4WJwf5FMGrPE4DBRfDQDwokABdvUPITZOymFMgiGMmFKGnp0xq+/B0wTTOtdnr8GC3sCmSRwuFDrroVWwfjIqyC/wlpoPXpSvyc1VKBuXOe/gLv1tTzDH3Jc+3fUYaUCj+uIVO65n0N2ajNg9ibBsCjiUp9CrWEoprqxR4lz3TaiPuRtMxWQlQ2NKPSTdUZWqg9KVXm3TS8xgf4Ox2dFwedVMcucC8UBBAwetgylXNZMv1yAMYD3QDokLi0AC+XnNR+UhtFXr9Y3VveQfH4YKnv0Fhr2opCyN99Lr example"}}]}}}, "ungrouped": {"hosts": {}}}}}
|
|
@@ -359,10 +359,10 @@ def test_engine_cli_emits_ansible_json(
|
|
|
359
359
|
assert document["ok"] is True
|
|
360
360
|
assert document["data"]["output"] is None
|
|
361
361
|
inventory = document["data"]["inventory"]
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
362
|
+
h1 = inventory["all"]["children"]["cloudfall_servers"]["hosts"]["h1"]
|
|
363
|
+
# Declared strings are marked unsafe so Ansible never templates them (#174).
|
|
364
|
+
assert h1["ansible_user"] == {"__ansible_unsafe": "cloudfall"}
|
|
365
|
+
assert h1["ansible_port"] == 22
|
|
366
366
|
assert (
|
|
367
367
|
"component_crm_backend"
|
|
368
368
|
in inventory["all"]["children"]["cloudfall_components"]["children"]
|
|
@@ -11,6 +11,7 @@ import json
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import TYPE_CHECKING, cast
|
|
13
13
|
|
|
14
|
+
from cloudfall.ansible_data import unmarked
|
|
14
15
|
from cloudfall_engine.cli import main
|
|
15
16
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
16
17
|
|
|
@@ -58,7 +59,11 @@ def render_examples(output: Path) -> dict[str, str]:
|
|
|
58
59
|
assert exit_code == 0
|
|
59
60
|
inventory_text = inventory_path.read_text(encoding="utf-8")
|
|
60
61
|
rendered = {"ansible-inventory.json": inventory_text}
|
|
61
|
-
|
|
62
|
+
# Read the values back as Ansible's loader does: an unsafe string is text.
|
|
63
|
+
document = cast(
|
|
64
|
+
"dict[str, dict[str, object]]",
|
|
65
|
+
json.loads(inventory_text, object_hook=unmarked),
|
|
66
|
+
)
|
|
62
67
|
servers = cast(
|
|
63
68
|
"dict[str, dict[str, dict[str, dict[str, object]]]]",
|
|
64
69
|
document["all"]["children"],
|