cloudfall 0.5.0__tar.gz → 0.5.1__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.5.0 → cloudfall-0.5.1}/CHANGELOG.md +21 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/PKG-INFO +1 -1
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/operation-decision.schema.json +6 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/pyproject.toml +1 -1
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/decision.py +84 -17
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_decision.py +91 -6
- {cloudfall-0.5.0 → cloudfall-0.5.1}/uv.lock +1 -1
- {cloudfall-0.5.0 → cloudfall-0.5.1}/.ansible-lint +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/.github/workflows/ci.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/.github/workflows/release.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/.gitignore +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/ARCHITECTURE.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/CONTRIBUTING.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/LICENSE +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/MANIFESTO.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/README.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/ROADMAP.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/SECURITY.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/Taskfile.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/README.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/alert-rules/postgresql-down.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/applications/crm.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/components/crm-backend.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/domains/crm-site.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/logging-stacks/operations.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/operator-policies/production.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/server-types/debian-application.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/servers/h1.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/servers/h2.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/services/postgresql-main.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/services/redis-cache.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/alert-rule.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/application.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/artifact.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/backup-receipt.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/common.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/component.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/deployment-receipt.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/domain.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/environment-receipt.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/logging-stack.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/observed-domain.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/observed-server.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/operation.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/operator-policy.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/operator-proposal.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/release-receipt.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/server-type.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/server.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/service.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/schemas/v1/ssh-public-key.schema.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/observed/compliant/h1.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/observed/compliant/h2.json +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/ansible-context.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/availability-design.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/brownfield-design.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/fleet-goals.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/hybrid-storage-design.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/landscape.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/logging-service-guide.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/neon-migration-guide.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/new-server-storage-guide.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/operator-guide.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/render-migration-guide.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/docs/secrets-guide.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/README.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/ansible.cfg +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/backup.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/baseline.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/bootstrap.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/data.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/deploy.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/domains.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/health.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/inspect.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/logging.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/restart.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/rollback.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/services.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/playbooks/time.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/__init__.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/__main__.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/artifact.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/cli.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/playbook.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/src/cloudfall_engine/py.typed +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_ansible_inventory.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_artifact_builder.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_deploy_templates.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_firewall_templates.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_logging_templates.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_nginx_site_templates.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_playbook.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/engine/tests/test_postgresql_templates.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/README.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/environment.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/findings.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/issues.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/linkedin.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/readiness.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-agent-dev.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-dev.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-index.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-issues.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/report-runtime.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/trace.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/evaluations/cloudfall/x.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/README.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/pyproject.toml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/render.yaml +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/src/demo/__init__.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/src/demo/asgi.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/examples/render-demo/uv.lock +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/README.md +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/__init__.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/__main__.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/agent_tools.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/ansible_api.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/ansible_reader.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/arguments.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/audit.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/authoring.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/catalog.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/cli.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/commands.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/cutover.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/dashboard.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/dashboard_server.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/domain.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/fleet_tools.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/importer.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/inventory.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/lifecycle.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/mcp_server.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/migrate.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/observation.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/observe.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/operations.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/operator.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/project.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/py.typed +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/render_api.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/resources.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/secrets.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/service_evidence.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/src/cloudfall/validation.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_agent_tools.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_ansible_reader.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_arguments.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_audit.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_authoring.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_catalog.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_commands.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_cutover.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_dashboard_server.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_data_migration.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_fleet_tools.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_importer.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_inventory.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_lifecycle.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_migrate.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_observe.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_operations_dashboard.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_operator.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_project.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_render_api.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_resources.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_secrets.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_service_evidence.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/sdk/tests/test_validation.py +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/apple-touch-icon.png +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/favicon-32.png +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/favicon.svg +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/icon-192.png +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/icon-512.png +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/index.html +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/manifesto.html +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/og.png +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/public/site.webmanifest +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/src/worker.js +0 -0
- {cloudfall-0.5.0 → cloudfall-0.5.1}/site/wrangler.jsonc +0 -0
|
@@ -6,6 +6,27 @@ Notable changes to Cloudfall. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.1] — 2026-09-22
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Decision records default to `decisions/` beside the operations rather
|
|
14
|
+
than to `tmp/decisions`: the record is what a team keeps, and `tmp/` is
|
|
15
|
+
what they throw away. The diffs and logs beside each record are raw
|
|
16
|
+
Ansible output, so a repository that commits them wants `no_log` on the
|
|
17
|
+
tasks that handle secrets
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A decision is `verified` only when its verify run changed nothing. A
|
|
22
|
+
verify step that exits zero while changing a host found the fleet not as
|
|
23
|
+
the run left it and converged it further, which verifies nothing, so it
|
|
24
|
+
is recorded `failed`. Every approved decision now carries a `verdict`
|
|
25
|
+
saying why it ended where it did
|
|
26
|
+
- A record cites its artifacts and its observation basis as the repository
|
|
27
|
+
sees them rather than by absolute path, because a record is committed
|
|
28
|
+
and one machine's home directory means nothing in anyone else's checkout
|
|
29
|
+
|
|
9
30
|
## [0.5.0] — 2026-09-21
|
|
10
31
|
|
|
11
32
|
## [0.4.0] — 2026-09-21
|
|
@@ -337,6 +337,12 @@
|
|
|
337
337
|
],
|
|
338
338
|
"additionalProperties": false,
|
|
339
339
|
"description": "What the operation's verify playbook reported afterwards."
|
|
340
|
+
},
|
|
341
|
+
"verdict": {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"description": "Why the decision ended in its status.",
|
|
344
|
+
"minLength": 1,
|
|
345
|
+
"maxLength": 300
|
|
340
346
|
}
|
|
341
347
|
},
|
|
342
348
|
"required": [
|
|
@@ -42,8 +42,15 @@ type CheckRunner = Callable[[Sequence[str], Mapping[str, str], Path], int]
|
|
|
42
42
|
API_VERSION = "cloudfall/v1"
|
|
43
43
|
DECISION_KIND = "OperationDecision"
|
|
44
44
|
DECISION_SCHEMA = "operation-decision.schema.json"
|
|
45
|
-
DECISION_DIRECTORY = "
|
|
46
|
-
"""Where decision records and their
|
|
45
|
+
DECISION_DIRECTORY = "decisions"
|
|
46
|
+
"""Where decision records and their artifacts land inside the repository.
|
|
47
|
+
|
|
48
|
+
Beside the operations rather than under ``tmp/``: the record is what the
|
|
49
|
+
team keeps, and the runtime directory is what they throw away. Commit the
|
|
50
|
+
records; the diffs and logs beside them are raw Ansible output, so they
|
|
51
|
+
belong in git only in a repository whose tasks set ``no_log`` where it
|
|
52
|
+
matters.
|
|
53
|
+
"""
|
|
47
54
|
|
|
48
55
|
_TREE_DIRECTORY = "tree"
|
|
49
56
|
_DIFF_SUFFIX = ".diff"
|
|
@@ -248,6 +255,7 @@ class Decision:
|
|
|
248
255
|
approval: Approval | None = None
|
|
249
256
|
execution: RunRecord | None = None
|
|
250
257
|
verification: RunRecord | None = None
|
|
258
|
+
verdict: str | None = None
|
|
251
259
|
|
|
252
260
|
def as_document(self) -> dict[str, object]:
|
|
253
261
|
"""Serialize the decision as a schema-valid record document."""
|
|
@@ -274,6 +282,8 @@ class Decision:
|
|
|
274
282
|
spec["execution"] = self.execution.as_dict()
|
|
275
283
|
if self.verification is not None:
|
|
276
284
|
spec["verify"] = self.verification.as_dict()
|
|
285
|
+
if self.verdict is not None:
|
|
286
|
+
spec["verdict"] = self.verdict
|
|
277
287
|
return {
|
|
278
288
|
"apiVersion": API_VERSION,
|
|
279
289
|
"kind": DECISION_KIND,
|
|
@@ -399,8 +409,8 @@ def propose(
|
|
|
399
409
|
inputs=declared,
|
|
400
410
|
requirement=requirement,
|
|
401
411
|
reason=reason,
|
|
402
|
-
basis=read_basis(request.observations),
|
|
403
|
-
check=_preview(exit_code, tree, diff_path),
|
|
412
|
+
basis=read_basis(request.observations, request.repository),
|
|
413
|
+
check=_preview(exit_code, tree, diff_path, request.repository),
|
|
404
414
|
status=DecisionStatus.PROPOSED,
|
|
405
415
|
)
|
|
406
416
|
store.save(decision)
|
|
@@ -468,6 +478,7 @@ def approve(
|
|
|
468
478
|
if decision.verify_playbook is not None and execution.exit_code == 0
|
|
469
479
|
else None
|
|
470
480
|
)
|
|
481
|
+
status, verdict = _outcome(execution, verification, decision.verify_playbook)
|
|
471
482
|
approved = replace(
|
|
472
483
|
decision,
|
|
473
484
|
approval=Approval(
|
|
@@ -475,7 +486,8 @@ def approve(
|
|
|
475
486
|
),
|
|
476
487
|
execution=execution,
|
|
477
488
|
verification=verification,
|
|
478
|
-
status=
|
|
489
|
+
status=status,
|
|
490
|
+
verdict=verdict,
|
|
479
491
|
)
|
|
480
492
|
store.update(approved)
|
|
481
493
|
return approved
|
|
@@ -485,15 +497,42 @@ def _outcome(
|
|
|
485
497
|
execution: RunRecord,
|
|
486
498
|
verification: RunRecord | None,
|
|
487
499
|
verify_playbook: Path | None,
|
|
488
|
-
) -> DecisionStatus:
|
|
489
|
-
"""Return the status one run earned
|
|
500
|
+
) -> tuple[DecisionStatus, str]:
|
|
501
|
+
"""Return the status one run earned, and why, for the record.
|
|
502
|
+
|
|
503
|
+
Verification is a claim about the fleet's state, so a verify run that
|
|
504
|
+
changed something did not verify anything: it found the fleet not as
|
|
505
|
+
the operation left it and converged it further. Exiting zero is not
|
|
506
|
+
enough.
|
|
507
|
+
"""
|
|
490
508
|
if execution.exit_code != 0:
|
|
491
|
-
return
|
|
509
|
+
return (
|
|
510
|
+
DecisionStatus.FAILED,
|
|
511
|
+
f"the run exited {execution.exit_code}",
|
|
512
|
+
)
|
|
492
513
|
if verify_playbook is None:
|
|
493
|
-
return
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
514
|
+
return (
|
|
515
|
+
DecisionStatus.EXECUTED,
|
|
516
|
+
"the run succeeded; the operation declares no verify step",
|
|
517
|
+
)
|
|
518
|
+
if verification is None: # pragma: no cover - callers verify after a clean run
|
|
519
|
+
return (DecisionStatus.FAILED, "the verify step did not run")
|
|
520
|
+
if verification.exit_code != 0:
|
|
521
|
+
return (
|
|
522
|
+
DecisionStatus.FAILED,
|
|
523
|
+
f"the verify step exited {verification.exit_code}",
|
|
524
|
+
)
|
|
525
|
+
if verification.changed:
|
|
526
|
+
changed = ", ".join(verification.changed)
|
|
527
|
+
return (
|
|
528
|
+
DecisionStatus.FAILED,
|
|
529
|
+
"the verify step changed "
|
|
530
|
+
f"{changed}, so the fleet was not in the state the run claimed",
|
|
531
|
+
)
|
|
532
|
+
return (
|
|
533
|
+
DecisionStatus.VERIFIED,
|
|
534
|
+
"the run succeeded and the verify step changed nothing",
|
|
535
|
+
)
|
|
497
536
|
|
|
498
537
|
|
|
499
538
|
def _run_stage(
|
|
@@ -513,7 +552,9 @@ def _run_stage(
|
|
|
513
552
|
argv, environment = run_command(invocation)
|
|
514
553
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
515
554
|
exit_code = stage.runner(argv, environment, log_path)
|
|
516
|
-
preview = _preview(
|
|
555
|
+
preview = _preview(
|
|
556
|
+
exit_code, invocation.tree, log_path, stage.request.repository
|
|
557
|
+
)
|
|
517
558
|
return RunRecord(
|
|
518
559
|
ran_at=_timestamp(moment),
|
|
519
560
|
exit_code=exit_code,
|
|
@@ -554,7 +595,19 @@ def run_command(
|
|
|
554
595
|
return tuple(argv), environment
|
|
555
596
|
|
|
556
597
|
|
|
557
|
-
def
|
|
598
|
+
def repository_relative(path: Path, repository: Path) -> Path:
|
|
599
|
+
"""Return the path as the repository sees it, when it lies inside it.
|
|
600
|
+
|
|
601
|
+
Records are committed, so a path that names one machine's home
|
|
602
|
+
directory is worse than useless to everyone else reading the trail.
|
|
603
|
+
"""
|
|
604
|
+
try:
|
|
605
|
+
return path.resolve().relative_to(repository.resolve())
|
|
606
|
+
except ValueError:
|
|
607
|
+
return path
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def read_basis(observations: Path, repository: Path | None = None) -> Basis:
|
|
558
611
|
"""Return the observation evidence a proposal cites.
|
|
559
612
|
|
|
560
613
|
A proposal made against no evidence says so, rather than implying it
|
|
@@ -571,10 +624,19 @@ def read_basis(observations: Path) -> Basis:
|
|
|
571
624
|
spec = document.get("spec")
|
|
572
625
|
if isinstance(spec, dict) and isinstance(spec.get("observedAt"), str):
|
|
573
626
|
observed_at[path.stem] = str(spec["observedAt"])
|
|
574
|
-
return Basis(
|
|
627
|
+
return Basis(
|
|
628
|
+
observations=(
|
|
629
|
+
repository_relative(observations, repository)
|
|
630
|
+
if repository is not None
|
|
631
|
+
else observations
|
|
632
|
+
),
|
|
633
|
+
observed_at=observed_at,
|
|
634
|
+
)
|
|
575
635
|
|
|
576
636
|
|
|
577
|
-
def _preview(
|
|
637
|
+
def _preview(
|
|
638
|
+
exit_code: int, tree: Path, diff_path: Path, repository: Path | None = None
|
|
639
|
+
) -> CheckPreview:
|
|
578
640
|
changed: list[str] = []
|
|
579
641
|
unchanged: list[str] = []
|
|
580
642
|
if tree.is_dir():
|
|
@@ -596,7 +658,11 @@ def _preview(exit_code: int, tree: Path, diff_path: Path) -> CheckPreview:
|
|
|
596
658
|
changed=tuple(changed),
|
|
597
659
|
unchanged=tuple(unchanged),
|
|
598
660
|
diff=DiffArtifact(
|
|
599
|
-
path=
|
|
661
|
+
path=(
|
|
662
|
+
repository_relative(diff_path, repository)
|
|
663
|
+
if repository is not None
|
|
664
|
+
else diff_path
|
|
665
|
+
),
|
|
600
666
|
sha256=hashlib.sha256(content).hexdigest(),
|
|
601
667
|
size=len(content),
|
|
602
668
|
),
|
|
@@ -768,6 +834,7 @@ def _decision_from_document(document: Mapping[str, object]) -> Decision:
|
|
|
768
834
|
),
|
|
769
835
|
),
|
|
770
836
|
status=DecisionStatus(str(spec["status"])),
|
|
837
|
+
verdict=str(spec["verdict"]) if "verdict" in spec else None,
|
|
771
838
|
execution=_run_from_document(spec.get("execution")),
|
|
772
839
|
verification=_run_from_document(spec.get("verify")),
|
|
773
840
|
approval=(
|
|
@@ -11,6 +11,7 @@ import pytest
|
|
|
11
11
|
from cloudfall.catalog import RiskLevel, TargetScope, load_catalog
|
|
12
12
|
from cloudfall.cli import main
|
|
13
13
|
from cloudfall.decision import (
|
|
14
|
+
DECISION_DIRECTORY,
|
|
14
15
|
ApprovalRequest,
|
|
15
16
|
DecisionError,
|
|
16
17
|
DecisionStatus,
|
|
@@ -49,7 +50,7 @@ verify:
|
|
|
49
50
|
|
|
50
51
|
def _store(repository: Path) -> DecisionStore:
|
|
51
52
|
return DecisionStore(
|
|
52
|
-
directory=repository /
|
|
53
|
+
directory=repository / DECISION_DIRECTORY,
|
|
53
54
|
catalog=SchemaCatalog(SCHEMAS),
|
|
54
55
|
)
|
|
55
56
|
|
|
@@ -84,13 +85,21 @@ def _proposal(
|
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
class _Runner:
|
|
87
|
-
"""A
|
|
88
|
+
"""A runner that writes what Ansible would write, stage by stage.
|
|
89
|
+
|
|
90
|
+
An idempotent fleet changes on the run and changes nothing on the
|
|
91
|
+
verify that follows, so the two stages are modelled separately.
|
|
92
|
+
"""
|
|
88
93
|
|
|
89
94
|
def __init__(
|
|
90
|
-
self,
|
|
95
|
+
self,
|
|
96
|
+
exit_code: int = 0,
|
|
97
|
+
changed: tuple[str, ...] = ("web-1",),
|
|
98
|
+
verify_changed: tuple[str, ...] = (),
|
|
91
99
|
) -> None:
|
|
92
100
|
self.exit_code = exit_code
|
|
93
101
|
self.changed = changed
|
|
102
|
+
self.verify_changed = verify_changed
|
|
94
103
|
self.recorded: list[tuple[str, ...]] = []
|
|
95
104
|
|
|
96
105
|
def __call__(
|
|
@@ -99,15 +108,20 @@ class _Runner:
|
|
|
99
108
|
environment: Mapping[str, str],
|
|
100
109
|
diff_path: Path,
|
|
101
110
|
) -> int:
|
|
102
|
-
self.recorded.append(tuple(argv))
|
|
103
111
|
tree = Path(environment["ANSIBLE_CALLBACK_TREE_DIR"])
|
|
112
|
+
changed = self.verify_changed if self._verifying(tree) else self.changed
|
|
113
|
+
self.recorded.append(tuple(argv))
|
|
104
114
|
tree.mkdir(parents=True, exist_ok=True)
|
|
105
|
-
for host in
|
|
115
|
+
for host in changed:
|
|
106
116
|
(tree / host).write_text('{"changed": true}', encoding="utf-8")
|
|
107
117
|
(tree / "web-2").write_text('{"changed": false}', encoding="utf-8")
|
|
108
118
|
diff_path.write_text("--- before\n+++ after\n", encoding="utf-8")
|
|
109
119
|
return self.exit_code
|
|
110
120
|
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _verifying(tree: Path) -> bool:
|
|
123
|
+
return tree.name.endswith("-verify")
|
|
124
|
+
|
|
111
125
|
|
|
112
126
|
def test_the_gate_follows_the_risk_level() -> None:
|
|
113
127
|
"""The table is the product's: a human cannot be skipped by policy."""
|
|
@@ -131,7 +145,11 @@ def test_a_proposal_records_what_check_mode_would_change(tmp_path: Path) -> None
|
|
|
131
145
|
assert decision.check.changed == ("web-1",)
|
|
132
146
|
assert decision.check.unchanged == ("web-2",)
|
|
133
147
|
assert decision.check.diff.size == len(b"--- before\n+++ after\n")
|
|
134
|
-
|
|
148
|
+
# A committed record names paths as the repository sees them.
|
|
149
|
+
assert not decision.check.diff.path.is_absolute()
|
|
150
|
+
artifact = repository / decision.check.diff.path
|
|
151
|
+
assert artifact.read_text(encoding="utf-8").startswith("---")
|
|
152
|
+
assert not decision.basis.observations.is_absolute()
|
|
135
153
|
|
|
136
154
|
|
|
137
155
|
def test_a_proposal_cites_the_evidence_it_was_made_against(
|
|
@@ -442,6 +460,10 @@ def test_an_approval_runs_the_recorded_proposal_and_verifies_it(
|
|
|
442
460
|
assert approved.execution is not None
|
|
443
461
|
assert approved.execution.changed == ("web-1",)
|
|
444
462
|
assert approved.verification is not None
|
|
463
|
+
assert approved.verification.changed == ()
|
|
464
|
+
assert approved.verdict == (
|
|
465
|
+
"the run succeeded and the verify step changed nothing"
|
|
466
|
+
)
|
|
445
467
|
assert [argv[1] for argv in run.recorded] == ["--diff", "--diff"]
|
|
446
468
|
assert run.recorded[0][-1].endswith("playbooks/deploy.yml")
|
|
447
469
|
assert run.recorded[1][-1].endswith("playbooks/health.yml")
|
|
@@ -630,3 +652,66 @@ def test_an_approved_decision_round_trips_through_the_store(
|
|
|
630
652
|
assert loaded.execution.exit_code == 0
|
|
631
653
|
assert loaded.verification is not None
|
|
632
654
|
assert loaded.status is DecisionStatus.VERIFIED
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def test_a_verify_step_that_changes_the_fleet_did_not_verify_it(
|
|
658
|
+
tmp_path: Path,
|
|
659
|
+
) -> None:
|
|
660
|
+
"""Exiting zero is not enough: verification is a claim about state."""
|
|
661
|
+
repository = _repository(tmp_path)
|
|
662
|
+
store = _store(repository)
|
|
663
|
+
proposed = propose(_proposal(repository), store, lambda: MOMENT, _Runner())
|
|
664
|
+
|
|
665
|
+
approved = approve(
|
|
666
|
+
ApprovalRequest(
|
|
667
|
+
decision=proposed, approver="roman", repository=repository
|
|
668
|
+
),
|
|
669
|
+
store,
|
|
670
|
+
lambda: MOMENT,
|
|
671
|
+
_Runner(verify_changed=("web-1",)),
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
assert approved.status is DecisionStatus.FAILED
|
|
675
|
+
assert approved.verification is not None
|
|
676
|
+
assert approved.verification.exit_code == 0
|
|
677
|
+
assert approved.verification.changed == ("web-1",)
|
|
678
|
+
assert approved.verdict is not None
|
|
679
|
+
assert "the verify step changed web-1" in approved.verdict
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def test_every_outcome_says_why_in_the_record(tmp_path: Path) -> None:
|
|
683
|
+
repository = _repository(tmp_path, deploy=DEPLOY, facts=FACTS)
|
|
684
|
+
store = _store(repository)
|
|
685
|
+
|
|
686
|
+
failed = approve(
|
|
687
|
+
ApprovalRequest(
|
|
688
|
+
decision=propose(
|
|
689
|
+
_proposal(repository), store, lambda: MOMENT, _Runner()
|
|
690
|
+
),
|
|
691
|
+
approver="roman",
|
|
692
|
+
repository=repository,
|
|
693
|
+
),
|
|
694
|
+
store,
|
|
695
|
+
lambda: MOMENT,
|
|
696
|
+
_Runner(exit_code=2),
|
|
697
|
+
)
|
|
698
|
+
executed = approve(
|
|
699
|
+
ApprovalRequest(
|
|
700
|
+
decision=propose(
|
|
701
|
+
_proposal(repository, "facts", target=None, inputs={}),
|
|
702
|
+
store,
|
|
703
|
+
lambda: MOMENT,
|
|
704
|
+
_Runner(),
|
|
705
|
+
),
|
|
706
|
+
approver="roman",
|
|
707
|
+
repository=repository,
|
|
708
|
+
),
|
|
709
|
+
store,
|
|
710
|
+
lambda: MOMENT,
|
|
711
|
+
_Runner(),
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
assert failed.verdict == "the run exited 2"
|
|
715
|
+
assert executed.verdict == (
|
|
716
|
+
"the run succeeded; the operation declares no verify step"
|
|
717
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/applications/crm.yaml
RENAMED
|
File without changes
|
{cloudfall-0.5.0 → cloudfall-0.5.1}/config/tests/invalid/missing-server/components/crm-backend.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|