cloudfall 0.8.0rc4__tar.gz → 0.8.0rc5__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.0rc4 → cloudfall-0.8.0rc5}/PKG-INFO +1 -1
- cloudfall-0.8.0rc5/docs/specs/002-component-build-steps.md +137 -0
- cloudfall-0.8.0rc5/docs/specs/004-domains-apply.md +125 -0
- cloudfall-0.8.0rc5/docs/specs/005-release-command.md +128 -0
- cloudfall-0.8.0rc5/docs/specs/006-standalone-postgresql.md +140 -0
- cloudfall-0.8.0rc5/docs/specs/007-data-copy-schemas.md +129 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/pyproject.toml +1 -1
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/.ansible-lint +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/.gitignore +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/ARCHITECTURE.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/CHANGELOG.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/CODE_OF_CONDUCT.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/CONTRIBUTING.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/LICENSE +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/MANIFESTO.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/README.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/ROADMAP.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/SECURITY.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/Taskfile.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/changelog.d/89-ci-deploy-guide.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/changelog.d/96-healthcheck-none.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/changelog.d/97-stale-proposals.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/changelog.d/README.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/README.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/alert-rules/postgresql-down.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/applications/crm.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/components/crm-backend.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/domains/crm-site.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/logging-stacks/operations.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/operator-policies/production.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/server-types/debian-application.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/servers/h1.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/servers/h2.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/services/postgresql-main.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/services/redis-cache.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/agent-investigation.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/alert-rule.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/application.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/artifact.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/backup-receipt.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/common.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/component.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/deployment-receipt.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/domain.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/environment-receipt.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/logging-stack.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/observed-domain.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/observed-server.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/operation-decision.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/operation.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/operator-policy.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/operator-proposal.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/release-receipt.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/server-type.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/server.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/service.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/schemas/v1/ssh-public-key.schema.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/tests/observed/compliant/h1.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/config/tests/observed/compliant/h2.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/README.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/.gitignore +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/ansible.cfg +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/inventories/production/hosts.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/operations/converge.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/operations/disk-report.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/operations/journal-vacuum.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/operations/postgresql-reinit.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/operations/postgresql-restart.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/operations/service-logs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/operations/shop-logrotate.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/playbooks/disk-report.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/playbooks/journal-vacuum.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/playbooks/postgresql-reinit.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/playbooks/postgresql-restart.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/playbooks/postgresql.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/playbooks/service-logs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/playbooks/shop-logrotate.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/playbooks/shop.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/disk-report-20261008170744.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/disk-report-20261008170744.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/journal-vacuum-20261008171016.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/journal-vacuum-20261008171016.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/postgresql-reinit-20261008171010.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/postgresql-reinit-20261008171010.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/postgresql-restart-20261008170624.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/postgresql-restart-20261008170624.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170612.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170612.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170740.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170740.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170742.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170742.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170838.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170838.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170920.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/service-logs-20261008170920.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/shop-logrotate-20261008170752.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full/shop-logrotate-20261008170752.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full-approved/disk-report-20261008204348.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full-approved/disk-report-20261008204348.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full-approved/disk-report-20261008204421.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full-approved/disk-report-20261008204421.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full-approved/postgresql-restart-20261008204358.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full-approved/postgresql-restart-20261008204358.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full-approved/shop-logrotate-20261008204351.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/recordings/disk-full-approved/shop-logrotate-20261008204351.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/scenario/break.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/incident/site.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/pyproject.toml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/src/cloudfall_demo/__init__.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/src/cloudfall_demo/server.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/src/cloudfall_demo/static/evaluation.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/src/cloudfall_demo/static/index.html +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/tests/test_demo.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/demo/uv.lock +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/ansible-context.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/assets/logo.svg +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/availability-design.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/brownfield-design.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/ci-deploy-guide.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/decisions.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/fleet-goals.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/getting-started.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/hybrid-storage-design.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/images/demo.png +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/landscape.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/logging-service-guide.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/neon-migration-guide.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/new-server-storage-guide.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/operator-guide.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/proving-runs/2026-10-08-disk-full-agent.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/render-migration-guide.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/secrets-guide.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/docs/specs/001-stale-proposals.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/README.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/ansible.cfg +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/backup.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/baseline.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/bootstrap.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/data.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/deploy.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/domains.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/health.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/inspect.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/logging.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/restart.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/rollback.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/services.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/playbooks/time.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_nginx_site/tasks/apply_virtual_host.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/src/cloudfall_engine/__init__.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/src/cloudfall_engine/__main__.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/src/cloudfall_engine/artifact.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/src/cloudfall_engine/cli.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/src/cloudfall_engine/playbook.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/src/cloudfall_engine/py.typed +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_ansible_inventory.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_artifact_builder.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_deploy_role.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_deploy_templates.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_firewall_templates.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_logging_templates.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_nginx_site_role.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_nginx_site_templates.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_playbook.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_postgresql_templates.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/engine/tests/test_time_role.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/.gitignore +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/README.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/ansible.cfg +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/inventories/production/hosts.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/operations/converge.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/operations/disk-report.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/operations/journal-vacuum.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/operations/postgresql-reinit.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/operations/postgresql-restart.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/operations/service-logs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/operations/shop-logrotate.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/playbooks/disk-report.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/playbooks/journal-vacuum.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/playbooks/postgresql-reinit.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/playbooks/postgresql-restart.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/playbooks/postgresql.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/playbooks/service-logs.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/playbooks/shop-logrotate.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/playbooks/shop.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/disk-report-20261008170744.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/disk-report-20261008170744.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/journal-vacuum-20261008171016.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/journal-vacuum-20261008171016.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/postgresql-reinit-20261008171010.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/postgresql-reinit-20261008171010.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/postgresql-restart-20261008170624.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/postgresql-restart-20261008170624.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170612.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170612.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170740.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170740.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170742.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170742.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170838.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170838.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170920.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/service-logs-20261008170920.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/shop-logrotate-20261008170752.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full/shop-logrotate-20261008170752.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204348.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204348.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204421.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full-approved/disk-report-20261008204421.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full-approved/postgresql-restart-20261008204358.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full-approved/postgresql-restart-20261008204358.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full-approved/shop-logrotate-20261008204351.diff +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/recordings/disk-full-approved/shop-logrotate-20261008204351.json +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/scenario/break.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/disk-full-incident/site.yml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/render-demo/README.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/render-demo/pyproject.toml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/render-demo/render.yaml +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/render-demo/src/demo/__init__.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/render-demo/src/demo/asgi.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/examples/render-demo/uv.lock +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/README.md +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/__init__.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/__main__.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/agent_server.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/agent_tools.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/ansible_api.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/ansible_reader.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/app.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/audit.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/authoring.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/callback_plugins/__init__.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/callback_plugins/cloudfall_host_stats.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/catalog.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/cli.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/commands.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/cutover.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/dashboard.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/dashboard_server.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/decision.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/domain.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/importer.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/inventory.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/investigation.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/lifecycle.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/migrate.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/observation.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/observe.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/operations.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/operator.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/project.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/py.typed +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/recording.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/render_api.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/resources.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/secrets.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/service_evidence.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/validation.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/src/cloudfall/why.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_agent_server.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_ansible_reader.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_app.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_arguments.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_audit.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_authoring.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_catalog.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_commands.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_cutover.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_dashboard_server.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_data_migration.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_decision.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_host_stats.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_importer.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_inventory.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_investigation.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_lifecycle.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_migrate.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_observe.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_operations_dashboard.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_operator.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_project.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_render_api.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_resources.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_secrets.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_service_evidence.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_stale.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/sdk/tests/test_validation.py +0 -0
- {cloudfall-0.8.0rc4 → cloudfall-0.8.0rc5}/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.0rc5
|
|
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
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# S-002: Component build steps
|
|
2
|
+
|
|
3
|
+
status: approved
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
A component can't ship a frontend build. `artifact build`
|
|
8
|
+
(`engine/src/cloudfall_engine/artifact.py`) clones the repository at a ref and tars the tree
|
|
9
|
+
(or `repository.subdirectory`) with no build step, so a Vite app's `web/dist/` never reaches
|
|
10
|
+
the artifact. `cloudfall_deploy` (`engine/ansible/roles/cloudfall_deploy/tasks/main.yml`)
|
|
11
|
+
asserts `runtime.type == "python"` and `runtime.packageManager == "uv"` and runs only
|
|
12
|
+
`uv sync` on the server. Abecra (Cloudfall's customer zero) works around it with a side
|
|
13
|
+
script that builds and rsyncs the web app into its own release tree: no digest check, no
|
|
14
|
+
receipt, and no rollback together with the API release (cloudfall-dev/cloudfall#86).
|
|
15
|
+
|
|
16
|
+
The same issue shows the schema promising more than the engine does: `component.schema.json`
|
|
17
|
+
accepts `runtime.type: node | pm2` and `packageManager: npm | pnpm | yarn`, which pass
|
|
18
|
+
`cloudfall config validate` and then fail in the role.
|
|
19
|
+
|
|
20
|
+
S-003 (Domain routes) is the pair to this spec: it serves the build output from the
|
|
21
|
+
component's release.
|
|
22
|
+
|
|
23
|
+
## Behaviour
|
|
24
|
+
|
|
25
|
+
A Component may declare build steps that run on the controller before packaging:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
spec:
|
|
29
|
+
buildTools:
|
|
30
|
+
node: "22.0.0"
|
|
31
|
+
pnpm: "9.0.0"
|
|
32
|
+
build:
|
|
33
|
+
- workingDirectory: web
|
|
34
|
+
command: [pnpm, install, --frozen-lockfile]
|
|
35
|
+
- workingDirectory: web
|
|
36
|
+
command: [pnpm, build]
|
|
37
|
+
artifact:
|
|
38
|
+
include: [web/dist]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `spec.build` is an ordered list of steps, each with `workingDirectory` (relative, no `..`,
|
|
42
|
+
same rule as `repository.subdirectory`), `command` (an argv list, never a shell string) and
|
|
43
|
+
an optional `timeoutSeconds` (1 to 3600, default 900)
|
|
44
|
+
- `spec.buildTools` maps each tool a step runs to its minimum version, like a command's
|
|
45
|
+
`required_tools`. Every step's `command[0]` must be a key of `buildTools`; it is looked up
|
|
46
|
+
on the controller's `PATH`
|
|
47
|
+
- `spec.artifact.include` lists paths (relative, no `..`) that the build produces and the
|
|
48
|
+
archive must keep, even when the repository's `.gitignore` names them
|
|
49
|
+
- `buildTools`, `build` and `artifact` are optional. `artifact` and `buildTools` require
|
|
50
|
+
`build`. A Component without `build` validates, builds and deploys exactly as today
|
|
51
|
+
|
|
52
|
+
`artifact build` with declared steps works in this order:
|
|
53
|
+
|
|
54
|
+
1. Before cloning, each `buildTools` entry is checked: the tool must be on `PATH`
|
|
55
|
+
(`artifact_build_tool_missing` otherwise) and `<tool> --version` must print a version at
|
|
56
|
+
least the declared one (`artifact_build_tool_unsupported` otherwise, also when no version
|
|
57
|
+
can be read). Both exit `ARTIFACT_BUILD_FAILED` with the code in the error context
|
|
58
|
+
2. The clean checkout is made as today, at the resolved commit
|
|
59
|
+
3. Each step runs in order, in `<packaged root>/<workingDirectory>`, where the packaged root is
|
|
60
|
+
`repository.subdirectory` when set and the checkout otherwise. A step that exits non-zero
|
|
61
|
+
or outruns its timeout stops the build with `artifact_build_step_failed`, naming the step
|
|
62
|
+
index, its argv and its exit code, with the tail of its stderr
|
|
63
|
+
4. Each `artifact.include` path must exist after the last step (`artifact_include_missing`
|
|
64
|
+
otherwise)
|
|
65
|
+
5. The archive holds the files git tracks at the commit under the packaged root, plus every
|
|
66
|
+
`artifact.include` path. Other files the build left behind (`node_modules/`, caches) are
|
|
67
|
+
not packaged. Without `build`, the archive is the tree as today
|
|
68
|
+
|
|
69
|
+
Any failure in steps 1 to 4 writes no archive and no metadata file. Step output never goes to
|
|
70
|
+
stdout, so `artifact build` still answers one JSON envelope there; it goes to stderr.
|
|
71
|
+
|
|
72
|
+
Build steps run on the controller (the operator's machine or CI), not on the server, with the
|
|
73
|
+
operator's user and file access. A step runs code from the component's repository
|
|
74
|
+
(`package.json` scripts, install hooks). To limit what that code sees, a step gets only
|
|
75
|
+
`PATH`, `HOME`, `LANG`, `TMPDIR` from the controller's environment plus `CI=true`; cloud
|
|
76
|
+
tokens, SOPS keys and other variables are not passed. The `build_artifact` MCP tool stays
|
|
77
|
+
`mutating`, so an agent can only start a build where the operator's policy allows mutating
|
|
78
|
+
tools.
|
|
79
|
+
|
|
80
|
+
The Artifact metadata file (`config/schemas/v1/artifact.schema.json`, written by
|
|
81
|
+
`build_artifact`) gains an optional `spec.build` object: `steps` (each step's
|
|
82
|
+
`workingDirectory` and `command` as run) and `tools` (each tool's version as read in step 1).
|
|
83
|
+
Metadata without `build` validates as before, and a build without steps writes none.
|
|
84
|
+
|
|
85
|
+
The archive is then deployed by `cloudfall_deploy` unchanged: the digest check, the
|
|
86
|
+
`releases/<release>/` unpack, the `current` symlink switch, the receipt, and
|
|
87
|
+
`cloudfall rollback` cover the build output with the rest of the release.
|
|
88
|
+
|
|
89
|
+
`cloudfall config validate` refuses a Component whose runtime the deploy role can't run: any
|
|
90
|
+
`runtime.type` other than `python` or any `runtime.packageManager` other than `uv`, with
|
|
91
|
+
`component_runtime_unsupported` naming the field and saying only Python with uv deploys
|
|
92
|
+
today. The schema enums keep the other values as reserved. This refuses configs that validate
|
|
93
|
+
today but could never deploy. The Render importer (`sdk/src/cloudfall/importer.py`) records a
|
|
94
|
+
Node service as an unsupported gap and writes no Component for it, instead of writing one
|
|
95
|
+
that `config validate` now refuses.
|
|
96
|
+
|
|
97
|
+
Touches `engine/src/cloudfall_engine/artifact.py`, `engine/src/cloudfall_engine/cli.py`
|
|
98
|
+
(`artifact build`), `config/schemas/v1/component.schema.json`,
|
|
99
|
+
`config/schemas/v1/artifact.schema.json`, `sdk/src/cloudfall/inventory.py`,
|
|
100
|
+
`sdk/src/cloudfall/validation.py`, `sdk/src/cloudfall/importer.py`, `cloudfall config
|
|
101
|
+
validate`, and the `build_artifact` MCP tool.
|
|
102
|
+
|
|
103
|
+
## Acceptance criteria
|
|
104
|
+
|
|
105
|
+
- S-002-1: A Component with `buildTools`, a `pnpm install` and a `pnpm build` step in `web`, and `artifact.include: [web/dist]` passes `cloudfall config validate`; every existing Component without `build` still validates
|
|
106
|
+
- S-002-2: `artifact build --ref <sha>` for that Component produces a tarball containing `web/dist/index.html`, even though the repository's `.gitignore` lists `web/dist`
|
|
107
|
+
- S-002-3: The same tarball contains no `web/node_modules/` entry and contains every file `git ls-files` lists at that commit
|
|
108
|
+
- S-002-4: The metadata file next to the tarball has `spec.build.steps` listing both steps' `workingDirectory` and `command` in order, and `spec.build.tools` with the versions `node --version` and `pnpm --version` printed, and validates against `artifact.schema.json`
|
|
109
|
+
- S-002-5: When a declared build tool is not on `PATH`, `artifact build` exits `ARTIFACT_BUILD_FAILED` with code `artifact_build_tool_missing` naming the tool, and no git clone runs
|
|
110
|
+
- S-002-6: When a declared build tool prints a version older than its `buildTools` minimum, `artifact build` exits `ARTIFACT_BUILD_FAILED` with code `artifact_build_tool_unsupported` naming the tool and both versions
|
|
111
|
+
- S-002-7: A build step that exits non-zero makes `artifact build` exit `ARTIFACT_BUILD_FAILED` with code `artifact_build_step_failed` naming the step and its exit code, and the output directory holds no new `.tar.gz` or `.json` file
|
|
112
|
+
- S-002-8: A step that finishes but leaves an `artifact.include` path missing makes `artifact build` exit `ARTIFACT_BUILD_FAILED` with code `artifact_include_missing` and write no artifact
|
|
113
|
+
- S-002-9: A step that prints to stdout leaves the stdout of `artifact build` a single valid JSON envelope
|
|
114
|
+
- S-002-10: A build step sees `CI=true` and does not see a variable such as `HCLOUD_TOKEN` set in the controller's environment
|
|
115
|
+
- S-002-11: `cloudfall config validate` refuses a step whose `command[0]` is not a key of `buildTools`, a `workingDirectory` or `include` path that is absolute or contains `..`, and `artifact` or `buildTools` declared without `build`
|
|
116
|
+
- S-002-12: `cloudfall deploy` of a built release puts `web/dist/index.html` under `<installRoot>/releases/<release>/`, and `cloudfall rollback` to the previous release makes `<installRoot>/current/web/dist/` that release's files
|
|
117
|
+
- S-002-13: For a Component without `build`, `artifact build` packages the same file list as before this change and writes metadata with no `spec.build` key
|
|
118
|
+
- S-002-14: `cloudfall config validate` on a Component with `runtime.type: node`, `runtime.type: pm2`, or `runtime.packageManager` of `npm`, `pnpm` or `yarn` exits `CONFIG_INVALID` with code `component_runtime_unsupported` naming the field
|
|
119
|
+
- S-002-15: Importing a Render blueprint with a Node web service writes no Component for it and lists the service as an unsupported gap
|
|
120
|
+
|
|
121
|
+
## Out of scope
|
|
122
|
+
|
|
123
|
+
- Node runtimes on the server (`runtime.type: node | pm2`, npm, pnpm or yarn installs and a Node process under systemd): a separate feature. Until it ships, S-002-14 refuses those runtimes at validate time rather than in the role
|
|
124
|
+
- Building on the server: the server keeps running only `uv sync`, so build toolchains stay off production hosts
|
|
125
|
+
- Pinning exact tool versions or installing tools: `buildTools` checks minimums like `required_tools`; the operator or CI image provides the tools, and the metadata records the versions that ran
|
|
126
|
+
- Sandboxing build steps (containers, network off): the reduced environment limits what a step sees, not what it can do. Revisit with container runtimes
|
|
127
|
+
- Build steps outside the packaged root (a monorepo root lockfile when `repository.subdirectory` is set): `workingDirectory` stays inside the packaged root so the archive and the build see the same tree
|
|
128
|
+
- A separate `static` component kind with its own artifact: two artifacts for one commit would still need a joint rollback, which one artifact gives for free
|
|
129
|
+
- Build caching between runs: each build starts from a clean checkout
|
|
130
|
+
|
|
131
|
+
## Decisions relied on
|
|
132
|
+
|
|
133
|
+
- none
|
|
134
|
+
|
|
135
|
+
## Issues
|
|
136
|
+
|
|
137
|
+
## Verification
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# S-004: Domains apply
|
|
2
|
+
|
|
3
|
+
status: approved
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
A declared Domain reaches its server only through `domains.yml`. The CLI runs that playbook
|
|
8
|
+
only inside `cloudfall migrate` (`sdk/src/cloudfall/migrate.py`, steps `domains-http`,
|
|
9
|
+
`dns-verify` and `domains-tls`), and the same plan converges `baseline` and `services` on
|
|
10
|
+
every server first. The `converge_domains` MCP tool runs `domains.yml` without the baseline,
|
|
11
|
+
but it takes no domain argument and connects to every server in `cloudfall_servers`. So an
|
|
12
|
+
operator who may deploy to a server but must not converge its baseline (a server lent from
|
|
13
|
+
another project, cloudfall-dev/cloudfall#87) has no safe way to apply one Domain. The vhost
|
|
14
|
+
and the certificate get written by hand, outside the receipts, and the next `domains.yml`
|
|
15
|
+
run overwrites that vhost with no record of what it replaced.
|
|
16
|
+
|
|
17
|
+
## Behaviour
|
|
18
|
+
|
|
19
|
+
Two commands in a new `domains` group:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
cloudfall domains apply <domain> [--issue-certificate] [--yes]
|
|
23
|
+
cloudfall domains observe <domain>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`<domain>` is a Domain's `metadata.id`. Both commands validate the project first, as
|
|
27
|
+
`deploy` does. Invalid config exits `CONFIG_INVALID`. A valid config with no Domain of that
|
|
28
|
+
id exits `PRECONDITION` with context code `lifecycle_domain_missing`, the way `deploy`
|
|
29
|
+
refuses an unknown component (`lifecycle_component_missing`). Both refusals happen before
|
|
30
|
+
the inventory render and before any SSH connection.
|
|
31
|
+
|
|
32
|
+
**domains apply**
|
|
33
|
+
|
|
34
|
+
- Runs `domains.yml` limited to the Domain's `proxy.server` and to that one Domain. No other host is contacted, and the proxy server renders no other Domain
|
|
35
|
+
- The play runs only the `cloudfall_nginx_site` role, so no baseline, firewall, time, access or service task runs. It is safe on a server whose baseline another project owns
|
|
36
|
+
- `origin.server` gets nothing, even when it differs from the proxy server: `domains.yml` has no origin-side task today, and no playbook reads the rendered `cloudfall_origin_domains` variable
|
|
37
|
+
- Without `--yes` it changes nothing and exits 0 with a plan, like `deploy`: `status: "plan"`, `action: "domains-apply"`, `domain`, `servers` (the proxy server), `wouldRun`, `instruction` and `effect: "would_update"`. The plan runs every controller-side check the run does, the DNS check included
|
|
38
|
+
- With `--yes` it answers `status: "ok"`, `action`, `domain`, `servers`, `receipt` (the receipt path) and `effect: "updated"`
|
|
39
|
+
- `--issue-certificate` checks DNS on the controller first, then runs the role with `cloudfall_domains_issue_certificates` true. The role's own sequence is the one `migrate` relies on: the vhost goes live over HTTP to serve the ACME challenge, certbot issues the certificate by webroot, the vhost is rendered again with TLS, and `certbot.timer` is enabled
|
|
40
|
+
- The DNS check covers `primaryName` and every alias. Each name must resolve to at least one address of the proxy server, computed as `migrate`'s `dns-verify` does (the server address, the names it resolves to, and `network.ipv4`). Otherwise the command exits `PRECONDITION` with context code `domains_dns_unverified`, naming each failing name and what it resolved to
|
|
41
|
+
- A Domain with `edge.mode: proxied` skips the DNS check, as `dns-verify` does, and the answer says `dnsCheck: "skipped-proxied"`. A checked Domain answers `dnsCheck: "verified"`. Without `--issue-certificate` there is no DNS check and no `dnsCheck` value
|
|
42
|
+
- `--issue-certificate` on a Domain with `tls.mode: disabled` exits `PRECONDITION` before any engine step
|
|
43
|
+
- A failed playbook run, such as a vhost that `nginx -t` rejects (the role rolls it back) or a failed issuance, exits `ENGINE_STEP_FAILED`
|
|
44
|
+
- Receipts go to `--deployments` (default `tmp/deployments`), the folder `migrate` and `converge_domains` use
|
|
45
|
+
|
|
46
|
+
**Receipt**
|
|
47
|
+
|
|
48
|
+
Every `domains.yml` run keeps writing one `DeploymentReceipt` per Domain at
|
|
49
|
+
`<deployments>/<domain>.json` (`config/schemas/v1/deployment-receipt.schema.json`). Its
|
|
50
|
+
`spec` gains an optional `previousConfigurationSha256`: the sha256 of the file at
|
|
51
|
+
`proxy.configurationPath` before this run rendered it, or `null` when no file was there. The
|
|
52
|
+
role already reads that file before rendering
|
|
53
|
+
(`engine/ansible/roles/cloudfall_nginx_site/tasks/apply_virtual_host.yml`), so it hashes it
|
|
54
|
+
there. The role writes the receipt, so runs from `migrate` and `converge_domains` record the
|
|
55
|
+
field too. A receipt without the field still loads.
|
|
56
|
+
|
|
57
|
+
**domains observe**
|
|
58
|
+
|
|
59
|
+
Read-only. It runs a new read-only playbook against the proxy server only, and writes
|
|
60
|
+
nothing to the server or to the project. It reports:
|
|
61
|
+
|
|
62
|
+
- `server`: the proxy server
|
|
63
|
+
- `vhost`: `path` (`proxy.configurationPath`), `present`, and `sha256` when present
|
|
64
|
+
- `receipt`: `configurationSha256` and `deployedAt` from `<deployments>/<domain>.json`, and `matches` saying whether the vhost on disk has that hash; `null` when there is no receipt
|
|
65
|
+
- `certificate`: `path` (`/etc/letsencrypt/live/<primaryName>/fullchain.pem`), `present`, and `notAfter` when present
|
|
66
|
+
- `certbotTimer`: `enabled` and `active`
|
|
67
|
+
|
|
68
|
+
It exits 0 whatever it finds: a missing vhost or an expired certificate is a reading, not an
|
|
69
|
+
error. An unreachable server exits `ENGINE_STEP_FAILED`. The playbook hands its reading back
|
|
70
|
+
through a controller-side file, as `backup.yml` does with its receipt. `services observe`
|
|
71
|
+
stays the public-network view of the route; `domains observe` is the server-side view.
|
|
72
|
+
|
|
73
|
+
**MCP**
|
|
74
|
+
|
|
75
|
+
`domains.apply` and `domains.observe` join `PROJECT_COMMANDS` in
|
|
76
|
+
`sdk/src/cloudfall/agent_server.py`, so `cloudfall mcp serve` serves them as tools generated
|
|
77
|
+
from the commands. The provided `converge_domains` tool (`_engine_tools` in
|
|
78
|
+
`sdk/src/cloudfall/app.py`) gains an optional `domain` string. Given, the tool runs on that
|
|
79
|
+
Domain's proxy server only and renders only that Domain, with `issue_certificates` working
|
|
80
|
+
as today (no DNS check). An unknown id exits `PRECONDITION`. Omitted, the tool behaves as
|
|
81
|
+
today.
|
|
82
|
+
|
|
83
|
+
Touches `sdk/src/cloudfall/app.py`, `sdk/src/cloudfall/commands.py`,
|
|
84
|
+
`sdk/src/cloudfall/agent_server.py`, `engine/ansible/playbooks/domains.yml`, a new read-only
|
|
85
|
+
playbook under `engine/ansible/playbooks/`, `engine/ansible/roles/cloudfall_nginx_site`,
|
|
86
|
+
`config/schemas/v1/deployment-receipt.schema.json`, `domains apply`, `domains observe` and
|
|
87
|
+
the `converge_domains` MCP tool.
|
|
88
|
+
|
|
89
|
+
## Acceptance criteria
|
|
90
|
+
|
|
91
|
+
- S-004-1: In a project with servers `a` and `b` and a Domain whose `proxy.server` is `a`, `cloudfall domains apply <d> --yes` runs `domains.yml` with only `a` in its play recap, and no task from `baseline.yml`, `services.yml` or the `cloudfall_firewall` role runs
|
|
92
|
+
- S-004-2: With Domains `d1` and `d2` both proxied on `a`, `domains apply d1 --yes` renders only `d1`'s `proxy.configurationPath`, and `d2`'s vhost file and receipt are unchanged
|
|
93
|
+
- S-004-3: Without `--yes`, `domains apply <d>` exits 0 with `status: "plan"` and `effect: "would_update"`, runs no `ansible-playbook`, and writes no receipt
|
|
94
|
+
- S-004-4: `domains apply nope` and `domains observe nope` in a valid project with no Domain `nope` exit `PRECONDITION` with context code `lifecycle_domain_missing`, and neither the inventory render nor `ansible-playbook` runs
|
|
95
|
+
- S-004-5: `domains apply <d>` in a project whose config fails validation exits `CONFIG_INVALID` before any engine step
|
|
96
|
+
- S-004-6: `--issue-certificate` on a `dns-only` Domain with an alias that resolves to no address of the proxy server exits `PRECONDITION` with context code `domains_dns_unverified` naming that alias, with and without `--yes`, and runs no `ansible-playbook`
|
|
97
|
+
- S-004-7: `--issue-certificate` on a `proxied` Domain runs no DNS check and answers `dnsCheck: "skipped-proxied"`
|
|
98
|
+
- S-004-8: `--issue-certificate` on a Domain with `tls.mode: disabled` exits `PRECONDITION` before any engine step
|
|
99
|
+
- S-004-9: `domains apply <d> --issue-certificate --yes` on a fresh `dns-only` Domain whose names resolve to the proxy server passes `cloudfall_domains_issue_certificates: true` to `domains.yml`, ends with `/etc/letsencrypt/live/<primaryName>/fullchain.pem` present and `certbot.timer` enabled and active, answers `dnsCheck: "verified"`, and writes `<deployments>/<d>.json`
|
|
100
|
+
- S-004-10: A second `domains apply <d> --issue-certificate --yes` skips the `Issue the declared certificate` task, and its receipt has `configurationSha256` equal to `previousConfigurationSha256`
|
|
101
|
+
- S-004-11: A `domains.yml` run on a proxy server with a hand-written file at `proxy.configurationPath` writes a receipt whose `previousConfigurationSha256` is that file's sha256, and a run with no file there writes `null`
|
|
102
|
+
- S-004-12: A `DeploymentReceipt` without `previousConfigurationSha256` still loads in `services status`
|
|
103
|
+
- S-004-13: A vhost that `nginx -t` rejects makes `domains apply <d> --yes` exit `ENGINE_STEP_FAILED`
|
|
104
|
+
- S-004-14: `domains observe <d>` reports `vhost.present`, `vhost.sha256`, `certificate.notAfter`, `certbotTimer.enabled` and `certbotTimer.active` as read on the proxy server, exits 0 when the vhost file is missing, and changes no file on the server or in the project
|
|
105
|
+
- S-004-15: `domains observe <d>` reports `receipt.matches: false` once the vhost file was edited after the last receipt, `true` while it is unchanged, and `receipt: null` when no receipt exists
|
|
106
|
+
- S-004-16: `cloudfall mcp serve` on a project lists a tool for `domains apply` and one for `domains observe`
|
|
107
|
+
- S-004-17: The `converge_domains` tool called with `domain: <d>` runs only on `d`'s proxy server and renders only `d`, called without `domain` runs on every server in `cloudfall_servers` as before, and called with an unknown `domain` exits `PRECONDITION`
|
|
108
|
+
|
|
109
|
+
## Out of scope
|
|
110
|
+
|
|
111
|
+
- Origin-side tasks: nothing in `domains.yml` acts on `origin.server` today. A spec that adds an origin allowlist or a private upstream can extend `domains apply` to the origin then
|
|
112
|
+
- Opening ports 80 and 443: the firewall belongs to the baseline, which this command must not touch on a lent server. If the firewall blocks them, issuance fails with `ENGINE_STEP_FAILED`
|
|
113
|
+
- Removing a Domain's vhost or certificate: deletion needs its own plan and refusal rules
|
|
114
|
+
- A `--server` flag or several domains per call: one Domain has one proxy server, and a loop over ids covers several
|
|
115
|
+
- Storing `domains observe` readings as evidence files: `services observe` already stores the public view, and the server view can join it later if the dashboard needs it
|
|
116
|
+
- Compatibility of new receipts with an older `cloudfall`: the receipt schema sets `additionalProperties: false`, so an older `services status` refuses a receipt that carries `previousConfigurationSha256`. Upgrading `cloudfall` first is the path, as for any new receipt field
|
|
117
|
+
- `migrate --only domains-http,domains-tls`: a step filter on a resumable plan is easy to get wrong and still reads as a migration
|
|
118
|
+
|
|
119
|
+
## Decisions relied on
|
|
120
|
+
|
|
121
|
+
- none
|
|
122
|
+
|
|
123
|
+
## Issues
|
|
124
|
+
|
|
125
|
+
## Verification
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# S-005: Release command
|
|
2
|
+
|
|
3
|
+
status: approved
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
`cloudfall deploy` runs the `cloudfall_deploy` role, which unpacks the release, runs
|
|
8
|
+
`uv sync --frozen --no-dev`, switches `current`, restarts the unit and gates on the health
|
|
9
|
+
check (`engine/ansible/roles/cloudfall_deploy/tasks/main.yml`). Nothing runs a declared
|
|
10
|
+
command against the new release before traffic moves to it, and
|
|
11
|
+
`config/schemas/v1/component.schema.json` has no field for one: `service.command` is the
|
|
12
|
+
only command a component declares. Database migrations therefore run by hand over SSH after
|
|
13
|
+
the health gate (cloudfall-dev/cloudfall#88). The gate checks code against the old schema, a
|
|
14
|
+
failed migration leaves no receipt and stops nothing, and the migration's environment is
|
|
15
|
+
assembled by hand instead of from the unit's `EnvironmentFile`. This spec pairs only loosely
|
|
16
|
+
with S-004: the two share the plan and receipt shape, nothing else.
|
|
17
|
+
|
|
18
|
+
## Behaviour
|
|
19
|
+
|
|
20
|
+
A component may declare a release command:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
spec:
|
|
24
|
+
release:
|
|
25
|
+
command: [.venv/bin/alembic, upgrade, head]
|
|
26
|
+
workingDirectory: services/api
|
|
27
|
+
timeoutSeconds: 300
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `command` is required, a non-empty list of non-empty strings like `service.command`. A relative first element resolves against the new release directory, as the unit resolves `service.command` against `current`
|
|
31
|
+
- `workingDirectory` is optional, a relative path inside the release directory with no `..` segment (the rule `repository.subdirectory` follows). It defaults to the release directory itself
|
|
32
|
+
- `timeoutSeconds` is optional, an integer from 1 to 1800, default 300. The bound stays well under `STEP_TIMEOUT_SECONDS` (3600), the limit on the whole playbook run
|
|
33
|
+
- A component without `release` deploys exactly as today
|
|
34
|
+
|
|
35
|
+
**When it runs**
|
|
36
|
+
|
|
37
|
+
The `cloudfall_deploy` role runs the command after `Materialize locked dependencies for the
|
|
38
|
+
release` and before `Install the managed service unit`, so before `current` moves and before
|
|
39
|
+
the unit file changes. It runs:
|
|
40
|
+
|
|
41
|
+
- as the application's `linuxUser` (`application_user` in the rendered inventory)
|
|
42
|
+
- in `<installRoot>/releases/<release>`, or in `workingDirectory` under it
|
|
43
|
+
- with the environment the unit gets: `<installRoot>/shared/env/<component>.env`, the file the unit names in `EnvironmentFile=-` and that `deploy --env-file` installs. The file is parsed by systemd's own rules (for example a transient `systemd-run` unit with the same `EnvironmentFile=`), so quoting means what it means for the service. A missing file gives no extra variables, as for the unit. The variables from `environment` and `secretRefs` are there when the operator rendered them with `secrets render` and passed the file to this or an earlier deploy
|
|
44
|
+
|
|
45
|
+
**Once per deploy**
|
|
46
|
+
|
|
47
|
+
`deploy.yml` walks `cloudfall_servers` one host at a time (`serial: 1`, `any_errors_fatal`)
|
|
48
|
+
in inventory order, which need not be `deployment.servers` order. The release server is the
|
|
49
|
+
first entry of `deployment.servers`. The deploy runs that server first, and the command runs
|
|
50
|
+
there only. The other servers deploy afterwards, one at a time as today, and skip the
|
|
51
|
+
command. So the command has run before any server's `current` moves.
|
|
52
|
+
|
|
53
|
+
The command must be safe while the old release still serves traffic: the other servers, and
|
|
54
|
+
the release server itself until its switch, run old code against whatever the command
|
|
55
|
+
changed. Migrations should be expand and contract: add first, remove in a later release. A
|
|
56
|
+
later failure (a health gate on the second server, say) does not undo what the command did.
|
|
57
|
+
|
|
58
|
+
**Failure**
|
|
59
|
+
|
|
60
|
+
A non-zero exit, or a run still going after `timeoutSeconds` (it is then stopped), fails the
|
|
61
|
+
release server's play. Because the play is `any_errors_fatal`, no server switches `current`,
|
|
62
|
+
no unit file changes, and no service restarts. `deploy --yes` exits `ENGINE_STEP_FAILED`, and
|
|
63
|
+
its `error.message` names the release command, its exit code or the timeout, and the end of
|
|
64
|
+
its output. The unpacked release directory stays, as any unpacked release does. No release
|
|
65
|
+
receipt is written: a `ReleaseReceipt` means a release went live (`healthCheckPassed` is
|
|
66
|
+
`const: true`).
|
|
67
|
+
|
|
68
|
+
**Receipt**
|
|
69
|
+
|
|
70
|
+
`ReleaseReceipt` (`config/schemas/v1/release-receipt.schema.json`) gains an optional
|
|
71
|
+
`spec.releaseCommand` object:
|
|
72
|
+
|
|
73
|
+
- `server`: the server the command ran on
|
|
74
|
+
- `command` and `workingDirectory`, as declared
|
|
75
|
+
- `exitCode`: 0
|
|
76
|
+
- `outputTail`: the last 2000 characters of its combined stdout and stderr, the bound `lifecycle.py` already uses for step output, with `maxLength: 2000` in the schema
|
|
77
|
+
|
|
78
|
+
Every receipt the deploy writes carries the same `releaseCommand`, the one from the release
|
|
79
|
+
server, so the receipt left at `<releases>/<component>.json` names where the command ran
|
|
80
|
+
whichever server wrote it last. A deploy of a component without `release` writes no
|
|
81
|
+
`releaseCommand`. Nothing in the SDK loads release receipts today, so an older `cloudfall`
|
|
82
|
+
is not affected.
|
|
83
|
+
|
|
84
|
+
**Plan and rollback**
|
|
85
|
+
|
|
86
|
+
- `deploy` without `--yes` adds `releaseCommand` to its plan: `command`, `workingDirectory`, `timeoutSeconds` and `server` (the release server), or `null` when the component declares none. Its `wouldRun` says the command runs once on that server before any switch
|
|
87
|
+
- `rollback` never runs the release command: downgrades stay a human decision. For a component that declares one, the `rollback` plan's `wouldRun` says the release command is not run and adds `releaseCommandSkipped: true`
|
|
88
|
+
- `restart` never runs it either
|
|
89
|
+
- `migrate`'s `deploy:<component>` steps go through the same deploy, so they run the command
|
|
90
|
+
|
|
91
|
+
Touches `config/schemas/v1/component.schema.json`,
|
|
92
|
+
`config/schemas/v1/release-receipt.schema.json`, `sdk/src/cloudfall/inventory.py`,
|
|
93
|
+
`sdk/src/cloudfall/validation.py`, `sdk/src/cloudfall/lifecycle.py`,
|
|
94
|
+
`sdk/src/cloudfall/commands.py`, `engine/src/cloudfall_engine/ansible_inventory.py`,
|
|
95
|
+
`engine/ansible/playbooks/deploy.yml`, `engine/ansible/roles/cloudfall_deploy`, `deploy`,
|
|
96
|
+
`rollback` and the MCP tools generated from them.
|
|
97
|
+
|
|
98
|
+
## Acceptance criteria
|
|
99
|
+
|
|
100
|
+
- S-005-1: A component with `release.command` that creates a table deploys with `cloudfall deploy --yes`, the table exists afterwards, and the release receipt has `releaseCommand` with the command, `exitCode: 0` and an `outputTail`
|
|
101
|
+
- S-005-2: The command runs after the `Materialize locked dependencies for the release` task and before the `Install the managed service unit` and `Switch the current release symlink` tasks, as the application's `linuxUser`, with the new release directory (or its `workingDirectory`) as its working directory
|
|
102
|
+
- S-005-3: A release command that prints every variable sees each key of the installed `<installRoot>/shared/env/<component>.env`, values with the same quoting rules the unit applies
|
|
103
|
+
- S-005-4: A release command that exits 1 makes `deploy --yes` exit `ENGINE_STEP_FAILED` with an `error.message` naming the release command and exit code 1; `current` still points at the previous release on every server, the unit file is unchanged, the service was not restarted, and no release receipt was written
|
|
104
|
+
- S-005-5: A release command still running after `timeoutSeconds` is stopped and fails the deploy the same way, with an `error.message` naming the timeout
|
|
105
|
+
- S-005-6: With `deployment.servers: [b, a]`, the command runs once, on `b`, before either server's `current` moves, and every release receipt from that deploy names `b` in `releaseCommand.server`
|
|
106
|
+
- S-005-7: `releaseCommand.outputTail` holds at most the last 2000 characters of a command that prints more
|
|
107
|
+
- S-005-8: `deploy` without `--yes` exits 0, shows `releaseCommand` with `command`, `workingDirectory`, `timeoutSeconds` and `server`, and runs no `ansible-playbook`; for a component without `release` it shows `releaseCommand: null`
|
|
108
|
+
- S-005-9: `rollback` of a component with `release` runs no release command, and its plan without `--yes` shows `releaseCommandSkipped: true`
|
|
109
|
+
- S-005-10: A component without `release` deploys with the same Ansible tasks as before and a receipt with no `releaseCommand`
|
|
110
|
+
- S-005-11: `config validate` exits `CONFIG_INVALID` for a `release.workingDirectory` that is absolute or contains `..`, an empty `release.command`, or a `timeoutSeconds` outside 1 to 1800
|
|
111
|
+
|
|
112
|
+
## Out of scope
|
|
113
|
+
|
|
114
|
+
- A post-switch hook: the issue's case needs the schema before the gate, and a hook after it can be its own spec
|
|
115
|
+
- Running the command on rollback, or a down command: downgrades stay a human decision, as the issue asks
|
|
116
|
+
- A failure receipt: a `ReleaseReceipt` means a live release, and its schema says so with `healthCheckPassed: true`. The failure's evidence is the `ENGINE_STEP_FAILED` message. A spec for failed-run records can add one for every engine step at once
|
|
117
|
+
- Redacting `outputTail`: the command is the operator's own and runs with secrets in its environment. A command that prints secrets puts them in a receipt under the project, so it should not print them
|
|
118
|
+
- `cloudfall run <component> -- <command>` for one-off commands: useful on its own, but it leaves ordering with the deploy to the operator
|
|
119
|
+
- Runtimes other than Python with uv: the deploy role supports only those today
|
|
120
|
+
- Choosing the release server by anything but its place in `deployment.servers`: the first entry is explicit and needs no new field
|
|
121
|
+
|
|
122
|
+
## Decisions relied on
|
|
123
|
+
|
|
124
|
+
- none
|
|
125
|
+
|
|
126
|
+
## Issues
|
|
127
|
+
|
|
128
|
+
## Verification
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# S-006: Standalone PostgreSQL
|
|
2
|
+
|
|
3
|
+
status: approved
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
A PostgreSQL `Service` can only hold databases that belong to an application Cloudfall
|
|
8
|
+
deploys. `config/schemas/v1/service.schema.json` requires
|
|
9
|
+
`spec.postgresql.databases[].application`, `cloudfall config validate` refuses a name with
|
|
10
|
+
no `Application` behind it (`sdk/src/cloudfall/validation.py`), and an `Application` needs
|
|
11
|
+
at least one component. The database role is the application's Linux user
|
|
12
|
+
(`_postgres_database` in `sdk/src/cloudfall/inventory.py`), it has no password, and the
|
|
13
|
+
server listens on `127.0.0.1` only (`bind.address` is `const` in the schema and asserted by
|
|
14
|
+
`cloudfall_postgresql`). So a team that wants Cloudfall to run only its database, with the
|
|
15
|
+
app staying elsewhere, has no way to declare it (cloudfall-dev/cloudfall#81).
|
|
16
|
+
|
|
17
|
+
## Behaviour
|
|
18
|
+
|
|
19
|
+
**A database with an owner instead of an application.** Each entry of
|
|
20
|
+
`spec.postgresql.databases` names exactly one of `application` (today's form) or `owner`,
|
|
21
|
+
a PostgreSQL role Cloudfall creates. `owner` uses the database-name pattern
|
|
22
|
+
(`^[a-z][a-z0-9_]*$`, at most 63 characters). An entry with `owner` also names
|
|
23
|
+
`passwordRef`, the role's password in the secrets store:
|
|
24
|
+
`{environment, path, key}`, read from `<secrets-dir>/<environment><path>.env` as the line
|
|
25
|
+
`<key>=<value>`, the same sops and age layout `cloudfall secrets render` reads
|
|
26
|
+
(`sdk/src/cloudfall/secrets.py`). An entry with `application` can't carry `passwordRef`.
|
|
27
|
+
A project with a server and a PostgreSQL `Service` whose databases all name an `owner`
|
|
28
|
+
needs no `Application` or `Component`.
|
|
29
|
+
|
|
30
|
+
`cloudfall config validate` refuses with `CONFIG_INVALID`:
|
|
31
|
+
|
|
32
|
+
- an entry with both `application` and `owner`, or with neither
|
|
33
|
+
- `owner` without `passwordRef`, or `passwordRef` next to `application`
|
|
34
|
+
- an `owner` of `postgres`, of the metrics role (`alloy`), or equal to the Linux user of an
|
|
35
|
+
`Application` that has a database on the same service, with code
|
|
36
|
+
`service_database_owner_reserved`
|
|
37
|
+
|
|
38
|
+
**The role and its password.** `cloudfall_postgresql` creates the `owner` role with
|
|
39
|
+
`LOGIN` like an application role today, then sets its password on every converge, so a
|
|
40
|
+
rotation is a commit in the secrets repository and a services converge. The value is
|
|
41
|
+
decrypted on the controller by the path that runs `services.yml` (the `converge_services`
|
|
42
|
+
MCP tool, the `services` step of `cloudfall migrate`, the operator's `converge-services`
|
|
43
|
+
operation, and `cloudfall-engine playbook run services.yml`), each reading the secrets
|
|
44
|
+
directory from a `--secrets-dir` flag (default `secrets`, as `secrets render` and
|
|
45
|
+
`mcp serve` use). It reaches the host in a `0600` file removed after the run, the role
|
|
46
|
+
sends it to `psql` on stdin with `no_log`, and PostgreSQL stores it as SCRAM. The value
|
|
47
|
+
never appears in the rendered inventory, a command line, Ansible output, a command's
|
|
48
|
+
answer, or a receipt; the answer names the reference, the key, and the value's SHA-256,
|
|
49
|
+
as `EnvironmentReceipt` does for environment files. A missing file or key stops the run
|
|
50
|
+
with `PRECONDITION` naming the expected path and key before any playbook starts.
|
|
51
|
+
|
|
52
|
+
**A way in for an outside app.** `spec.postgresql.remoteAccess` is optional:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
remoteAccess:
|
|
56
|
+
listenAddress: 10.0.0.2
|
|
57
|
+
allowedSources:
|
|
58
|
+
- 10.0.0.0/24
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`listenAddress` is one IPv4 or IPv6 address on the server, never a wildcard.
|
|
62
|
+
`allowedSources` is a non-empty list of CIDRs. `bind` keeps its loopback contract, and the
|
|
63
|
+
same `bind.port` serves both addresses. With `remoteAccess` declared:
|
|
64
|
+
|
|
65
|
+
- `cloudfall.conf` sets `listen_addresses = '127.0.0.1,<listenAddress>'` and `ssl = on`
|
|
66
|
+
with the Debian `ssl-cert` snakeoil pair, which the role installs
|
|
67
|
+
- `pg_hba.conf` gains a block between Cloudfall markers with one
|
|
68
|
+
`hostssl <database> <owner> <cidr> scram-sha-256` line per `owner` database and source.
|
|
69
|
+
`hostssl` means a remote client must use TLS; a plain connection matches no line and is
|
|
70
|
+
refused. Databases named by `application` get no line and stay socket-only with peer
|
|
71
|
+
authentication
|
|
72
|
+
- the server's nftables ruleset gains one rule per source, `ip saddr <cidr> tcp dport
|
|
73
|
+
<port> accept` (`ip6 saddr` for an IPv6 CIDR), rendered by
|
|
74
|
+
`engine/src/cloudfall_engine/ansible_inventory.py` into `cloudfall_firewall.allowedInbound`
|
|
75
|
+
as entries with an optional `sources` list, and applied when `baseline.yml` runs. Entries
|
|
76
|
+
without `sources` render as today
|
|
77
|
+
|
|
78
|
+
`cloudfall config validate` refuses `remoteAccess` with `CONFIG_INVALID` when no database
|
|
79
|
+
on the service names an `owner` (`service_remote_access_unused`), when the server's server
|
|
80
|
+
type declares no `firewall` (`service_remote_access_unfirewalled`), when that firewall's
|
|
81
|
+
`allowedInbound` already opens the service port to everyone
|
|
82
|
+
(`service_remote_access_port_open`), or when a source is `0.0.0.0/0` or `::/0`
|
|
83
|
+
(`service_remote_access_open`). With `remoteAccess` absent, PostgreSQL listens on
|
|
84
|
+
`127.0.0.1` only, `pg_hba.conf` keeps no Cloudfall block (a block left by an earlier
|
|
85
|
+
declaration is removed), and no PostgreSQL rule is added to nftables. Closed is the default.
|
|
86
|
+
|
|
87
|
+
**Backups, the restore drill, and `data copy`.** They don't change. `backup.sh.j2` and
|
|
88
|
+
`restore-check.sh.j2` loop over `postgresql.databases` by `name` only, and
|
|
89
|
+
`cloudfall_data_migration` selects the entry by name and restores with
|
|
90
|
+
`--role <owner>`; its receipt records `service`, `database`, and `owner`, never an
|
|
91
|
+
application. An `owner` database is backed up, drilled, and copied into the same way.
|
|
92
|
+
|
|
93
|
+
**Compatibility.** Every config that validates today validates the same. A database entry
|
|
94
|
+
with `application` renders exactly as before; one with `owner` renders `name` and `owner`
|
|
95
|
+
with no `application` key. The role's argument spec
|
|
96
|
+
(`engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml`) makes `application`
|
|
97
|
+
optional and adds `remoteAccess`. The rendered service carries `passwordRef`, never the value;
|
|
98
|
+
the password travels as its own role variable.
|
|
99
|
+
|
|
100
|
+
Touches `config/schemas/v1/service.schema.json`, `sdk/src/cloudfall/validation.py`,
|
|
101
|
+
`sdk/src/cloudfall/inventory.py` (`PostgresDatabase`, `_postgres_database`,
|
|
102
|
+
`ServiceInventory`), `sdk/src/cloudfall/secrets.py`, `sdk/src/cloudfall/migrate.py`,
|
|
103
|
+
`sdk/src/cloudfall/app.py` (`converge_services`, `cloudfall migrate`),
|
|
104
|
+
`sdk/src/cloudfall/operator.py`, `engine/src/cloudfall_engine/ansible_inventory.py`,
|
|
105
|
+
`engine/src/cloudfall_engine/cli.py` (`playbook run`),
|
|
106
|
+
`engine/ansible/roles/cloudfall_postgresql`, `engine/ansible/roles/cloudfall_firewall`,
|
|
107
|
+
`engine/ansible/playbooks/services.yml`, and `docs/secrets-guide.md`.
|
|
108
|
+
|
|
109
|
+
## Acceptance criteria
|
|
110
|
+
|
|
111
|
+
- S-006-1: A project with one `Server`, its server type, and one PostgreSQL `Service` whose only database declares `owner: billing` and a `passwordRef`, and no `Application` or `Component`, passes `cloudfall config validate` with exit 0
|
|
112
|
+
- S-006-2: A database entry with both `application` and `owner` fails `cloudfall config validate` with `CONFIG_INVALID` naming `spec.postgresql.databases[0]`, and an entry with neither fails the same way
|
|
113
|
+
- S-006-3: `owner` without `passwordRef` fails `cloudfall config validate` with `CONFIG_INVALID`, and so does `passwordRef` next to `application`
|
|
114
|
+
- S-006-4: An `owner` of `postgres`, of `alloy`, or equal to the Linux user of an `Application` with a database on the same service fails with `CONFIG_INVALID` code `service_database_owner_reserved`
|
|
115
|
+
- S-006-5: After `services.yml` converges the service of S-006-1, `pg_roles` holds `billing` with `rolcanlogin` and a `SCRAM-SHA-256` password, and database `billing` is owned by `billing`
|
|
116
|
+
- S-006-6: The password value appears in none of the rendered inventory, the playbook output, the command's answer, or any receipt, and the answer names the reference, the key, and the value's SHA-256
|
|
117
|
+
- S-006-7: A `passwordRef` whose file or key is missing stops the services converge with `PRECONDITION` naming the expected path and key, and no playbook runs
|
|
118
|
+
- S-006-8: With `remoteAccess` of `listenAddress: 10.0.0.2` and `allowedSources: [10.0.0.0/24]`, the converged `cloudfall.conf` reads `listen_addresses = '127.0.0.1,10.0.0.2'` and `ssl = on`, the Cloudfall block in `pg_hba.conf` holds exactly `hostssl billing billing 10.0.0.0/24 scram-sha-256`, and the rendered nftables ruleset holds `ip saddr 10.0.0.0/24 tcp dport 5432 accept` and no unrestricted rule for port 5432
|
|
119
|
+
- S-006-9: On a proving pair of hosts, `psql` from an address in `allowedSources` as `billing` with `sslmode=require` connects, from an address outside it gets no connection, and from an allowed address with `sslmode=disable` is refused with `no pg_hba.conf entry`
|
|
120
|
+
- S-006-10: Removing `remoteAccess` and converging baseline and services leaves `listen_addresses = '127.0.0.1'`, no Cloudfall block in `pg_hba.conf`, and no rule for the service port in nftables
|
|
121
|
+
- S-006-11: `remoteAccess` fails `cloudfall config validate` with `CONFIG_INVALID` and code `service_remote_access_unused` when no database names an `owner`, `service_remote_access_unfirewalled` when the server type declares no `firewall`, `service_remote_access_port_open` when its `allowedInbound` opens the service port, and `service_remote_access_open` for a source of `0.0.0.0/0` or `::/0`
|
|
122
|
+
- S-006-12: For the project in `config/examples`, `cloudfall-engine inventory render` writes a byte-identical inventory before and after the change, and the rendered `cloudfall.conf`, `nftables.conf`, backup script, and restore-check script are identical
|
|
123
|
+
- S-006-13: For the service of S-006-1, the backup script writes `billing-<timestamp>.dump`, the restore drill restores it and passes, and `cloudfall data copy <service> --database billing --source-url-file <file> --yes` restores as role `billing` and writes a receipt with `spec.owner` of `billing`
|
|
124
|
+
|
|
125
|
+
## Out of scope
|
|
126
|
+
|
|
127
|
+
- Managed connection pooling (PgBouncer): a separate service with its own auth file, and nothing in the issue needs it yet
|
|
128
|
+
- Replication and read replicas: a different durability contract from the dump-based backup and drill
|
|
129
|
+
- A CA-verified server certificate: the snakeoil pair gives encryption with `sslmode=require`, not server identity with `verify-full`. A later spec can take a certificate from the domain's ACME issuance
|
|
130
|
+
- A tunnel (WireGuard or SSH) as the way in: the listen address plus source list covers a private network and a fixed egress address, which is what the proving run will use
|
|
131
|
+
- Remote access for databases named by `application`: their role has no password and peer authentication is the point of that form
|
|
132
|
+
- Restoring the "for an app elsewhere" path in `docs/neon-migration-guide.md`: it follows a proving run of S-006-9, not this spec
|
|
133
|
+
|
|
134
|
+
## Decisions relied on
|
|
135
|
+
|
|
136
|
+
- none
|
|
137
|
+
|
|
138
|
+
## Issues
|
|
139
|
+
|
|
140
|
+
## Verification
|