cloudfall 0.2.0__tar.gz → 0.3.0__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.2.0 → cloudfall-0.3.0}/CHANGELOG.md +43 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/PKG-INFO +24 -14
- {cloudfall-0.2.0 → cloudfall-0.3.0}/README.md +23 -13
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/render-migration-guide.md +1 -1
- {cloudfall-0.2.0 → cloudfall-0.3.0}/pyproject.toml +1 -1
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/cli.py +2 -25
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/project.py +48 -163
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_agent_tools.py +8 -3
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_authoring.py +8 -3
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_project.py +57 -131
- {cloudfall-0.2.0 → cloudfall-0.3.0}/uv.lock +1 -1
- {cloudfall-0.2.0 → cloudfall-0.3.0}/.ansible-lint +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/.github/workflows/ci.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/.github/workflows/release.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/.gitignore +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/ARCHITECTURE.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/CONTRIBUTING.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/LICENSE +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/MANIFESTO.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/ROADMAP.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/SECURITY.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/Taskfile.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/alert-rules/postgresql-down.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/applications/crm.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/components/crm-backend.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/domains/crm-site.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/logging-stacks/operations.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/operator-policies/production.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/server-types/debian-application.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/servers/h1.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/servers/h2.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/services/postgresql-main.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/services/redis-cache.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/alert-rule.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/application.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/artifact.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/backup-receipt.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/common.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/component.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/deployment-receipt.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/domain.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/environment-receipt.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/logging-stack.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/observed-domain.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/observed-server.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/operator-policy.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/operator-proposal.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/release-receipt.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/server-type.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/server.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/service.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/schemas/v1/ssh-public-key.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/observed/compliant/h1.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/config/tests/observed/compliant/h2.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/ansible-context.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/availability-design.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/brownfield-design.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/fleet-goals.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/hybrid-storage-design.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/landscape.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/logging-service-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/neon-migration-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/new-server-storage-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/operator-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/docs/secrets-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/ansible.cfg +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/backup.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/baseline.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/bootstrap.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/data.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/deploy.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/domains.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/health.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/inspect.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/logging.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/restart.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/rollback.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/services.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/playbooks/time.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/__init__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/__main__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/artifact.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/cli.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/playbook.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/src/cloudfall_engine/py.typed +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_ansible_inventory.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_artifact_builder.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_deploy_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_firewall_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_logging_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_nginx_site_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_playbook.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/engine/tests/test_postgresql_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/environment.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/findings.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/issues.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/linkedin.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/readiness.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-agent-dev.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-dev.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-index.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-issues.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/report-runtime.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/trace.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/evaluations/cloudfall/x.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/pyproject.toml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/render.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/src/demo/__init__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/src/demo/asgi.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/examples/render-demo/uv.lock +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/__init__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/__main__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/agent_tools.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/arguments.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/audit.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/authoring.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/commands.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/cutover.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/dashboard.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/dashboard_server.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/domain.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/importer.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/inventory.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/lifecycle.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/mcp_server.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/migrate.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/observation.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/operations.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/operator.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/py.typed +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/render_api.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/resources.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/secrets.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/service_evidence.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/src/cloudfall/validation.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_arguments.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_audit.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_commands.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_cutover.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_dashboard_server.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_data_migration.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_importer.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_inventory.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_lifecycle.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_migrate.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_operations_dashboard.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_operator.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_render_api.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_resources.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_secrets.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_service_evidence.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/sdk/tests/test_validation.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/apple-touch-icon.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/favicon-32.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/favicon.svg +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/icon-192.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/icon-512.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/index.html +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/manifesto.html +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/og.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/public/site.webmanifest +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/src/worker.js +0 -0
- {cloudfall-0.2.0 → cloudfall-0.3.0}/site/wrangler.jsonc +0 -0
|
@@ -6,6 +6,49 @@ Notable changes to Cloudfall. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] — 2026-09-20
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- `cloudfall init --rev` and `--source`, and the machinery behind them:
|
|
14
|
+
`GitRevision`, `GitSourceUrl`, `GitPin`, `IndexPin`, `CheckoutState`, the
|
|
15
|
+
checkout inspection, and the `project_revision_unresolved`,
|
|
16
|
+
`project_revision_uncommitted` and `project_revision_unpublished` errors.
|
|
17
|
+
Cloudfall is on PyPI, so a project pins a release and resolves it like any
|
|
18
|
+
other dependency; `resolve_installed_version` replaces
|
|
19
|
+
`resolve_installed_pin`, and the `init` envelope reports `version` rather
|
|
20
|
+
than a `pin` object. To run an unreleased Cloudfall in a project, point the
|
|
21
|
+
dependency at a checkout with uv's own `[tool.uv.sources]`
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- The quickstart and the Render migration guide install Cloudfall from the
|
|
26
|
+
index rather than from git
|
|
27
|
+
|
|
28
|
+
## [0.2.1] — 2026-09-20
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- `cloudfall init` works from a PyPI install. An install from the package
|
|
33
|
+
index records no origin to read a commit from, so `init` failed with
|
|
34
|
+
`project_revision_unresolved` on the first command a new user runs, and
|
|
35
|
+
`--rev` only worked around it by writing a project that installed
|
|
36
|
+
Cloudfall from git instead of from the index. A project now pins whatever
|
|
37
|
+
ran `init`: `cloudfall==<version>` with no `[tool.uv.sources]` table for a
|
|
38
|
+
released install, the commit and repository for a git or source install
|
|
39
|
+
- The project README links the secrets guide and the reference examples at
|
|
40
|
+
the release tag when the pin is a released version, rather than at a
|
|
41
|
+
commit the install does not know
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- `InitOptions` and the `cloudfall init` envelope carry one `pin` instead of
|
|
46
|
+
a `revision` and a `source`: `{"kind": "index", "version": ...}` or
|
|
47
|
+
`{"kind": "git", "revision": ..., "source": ...}`. `resolve_installed_pin`
|
|
48
|
+
replaces `resolve_installed_revision`
|
|
49
|
+
- `--rev` documents that it pins a commit from `--source`, which is now the
|
|
50
|
+
explicit alternative to the default rather than the only mechanism
|
|
51
|
+
|
|
9
52
|
## [0.2.0] — 2026-09-20
|
|
10
53
|
|
|
11
54
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cloudfall
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: AI-native control plane for dedicated Debian application fleets
|
|
5
5
|
Project-URL: Homepage, https://cloudfall.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/romamo/cloudfall
|
|
@@ -160,10 +160,18 @@ is implemented yet; everything below this line is.
|
|
|
160
160
|
|
|
161
161
|
## Quickstart
|
|
162
162
|
|
|
163
|
-
Cloudfall is
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
Cloudfall is on PyPI. The only prerequisite is
|
|
164
|
+
[`uv`](https://docs.astral.sh/uv/): it provisions the required Python 3.14
|
|
165
|
+
interpreter and all dependencies automatically, so you do not need Python
|
|
166
|
+
3.14 preinstalled.
|
|
167
|
+
|
|
168
|
+
```console
|
|
169
|
+
uv tool install cloudfall
|
|
170
|
+
cloudfall --help
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The reference resource set lives in this repository, so validating it means
|
|
174
|
+
a clone:
|
|
167
175
|
|
|
168
176
|
```console
|
|
169
177
|
git clone https://github.com/romamo/cloudfall.git
|
|
@@ -189,13 +197,12 @@ uv run cloudfall-engine inventory render --project config/examples
|
|
|
189
197
|
Your servers, applications, evidence, and any playbooks of your own belong in
|
|
190
198
|
a project: a directory of its own, kept private. Cloudfall's wheel bundles the
|
|
191
199
|
schema catalog and the Ansible engine, so the project needs no checkout of
|
|
192
|
-
this repository. `cloudfall init` is the first command
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
argument it initializes the current directory, which must be empty:
|
|
200
|
+
this repository. `cloudfall init` is the first command, and it pins the
|
|
201
|
+
project to the released Cloudfall that ran it. Without a directory argument
|
|
202
|
+
it initializes the current directory, which must be empty:
|
|
196
203
|
|
|
197
204
|
```console
|
|
198
|
-
uvx
|
|
205
|
+
uvx cloudfall init my-project
|
|
199
206
|
cd my-project
|
|
200
207
|
uv sync
|
|
201
208
|
```
|
|
@@ -215,15 +222,18 @@ repository, and a `pyproject.toml`:
|
|
|
215
222
|
name = "my-project"
|
|
216
223
|
version = "0"
|
|
217
224
|
requires-python = ">=3.14"
|
|
218
|
-
dependencies = ["cloudfall"]
|
|
225
|
+
dependencies = ["cloudfall==<version>"]
|
|
219
226
|
|
|
220
227
|
[tool.uv]
|
|
221
228
|
package = false
|
|
222
|
-
|
|
223
|
-
[tool.uv.sources]
|
|
224
|
-
cloudfall = { git = "https://github.com/romamo/cloudfall.git", rev = "<commit>" }
|
|
225
229
|
```
|
|
226
230
|
|
|
231
|
+
Bump that version and run `uv sync` to move the project to a newer
|
|
232
|
+
Cloudfall. To run an unreleased Cloudfall in a project, point the dependency
|
|
233
|
+
at a checkout or a commit with uv's own
|
|
234
|
+
[`[tool.uv.sources]`](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-sources);
|
|
235
|
+
Cloudfall itself only ever pins a release.
|
|
236
|
+
|
|
227
237
|
Only the kind directories are read as resources, so playbooks, roles, docs,
|
|
228
238
|
and tooling files may live anywhere else in the project. Commands find the
|
|
229
239
|
project on their own: the current directory when it is one, else
|
|
@@ -134,10 +134,18 @@ is implemented yet; everything below this line is.
|
|
|
134
134
|
|
|
135
135
|
## Quickstart
|
|
136
136
|
|
|
137
|
-
Cloudfall is
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
Cloudfall is on PyPI. The only prerequisite is
|
|
138
|
+
[`uv`](https://docs.astral.sh/uv/): it provisions the required Python 3.14
|
|
139
|
+
interpreter and all dependencies automatically, so you do not need Python
|
|
140
|
+
3.14 preinstalled.
|
|
141
|
+
|
|
142
|
+
```console
|
|
143
|
+
uv tool install cloudfall
|
|
144
|
+
cloudfall --help
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The reference resource set lives in this repository, so validating it means
|
|
148
|
+
a clone:
|
|
141
149
|
|
|
142
150
|
```console
|
|
143
151
|
git clone https://github.com/romamo/cloudfall.git
|
|
@@ -163,13 +171,12 @@ uv run cloudfall-engine inventory render --project config/examples
|
|
|
163
171
|
Your servers, applications, evidence, and any playbooks of your own belong in
|
|
164
172
|
a project: a directory of its own, kept private. Cloudfall's wheel bundles the
|
|
165
173
|
schema catalog and the Ansible engine, so the project needs no checkout of
|
|
166
|
-
this repository. `cloudfall init` is the first command
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
argument it initializes the current directory, which must be empty:
|
|
174
|
+
this repository. `cloudfall init` is the first command, and it pins the
|
|
175
|
+
project to the released Cloudfall that ran it. Without a directory argument
|
|
176
|
+
it initializes the current directory, which must be empty:
|
|
170
177
|
|
|
171
178
|
```console
|
|
172
|
-
uvx
|
|
179
|
+
uvx cloudfall init my-project
|
|
173
180
|
cd my-project
|
|
174
181
|
uv sync
|
|
175
182
|
```
|
|
@@ -189,15 +196,18 @@ repository, and a `pyproject.toml`:
|
|
|
189
196
|
name = "my-project"
|
|
190
197
|
version = "0"
|
|
191
198
|
requires-python = ">=3.14"
|
|
192
|
-
dependencies = ["cloudfall"]
|
|
199
|
+
dependencies = ["cloudfall==<version>"]
|
|
193
200
|
|
|
194
201
|
[tool.uv]
|
|
195
202
|
package = false
|
|
196
|
-
|
|
197
|
-
[tool.uv.sources]
|
|
198
|
-
cloudfall = { git = "https://github.com/romamo/cloudfall.git", rev = "<commit>" }
|
|
199
203
|
```
|
|
200
204
|
|
|
205
|
+
Bump that version and run `uv sync` to move the project to a newer
|
|
206
|
+
Cloudfall. To run an unreleased Cloudfall in a project, point the dependency
|
|
207
|
+
at a checkout or a commit with uv's own
|
|
208
|
+
[`[tool.uv.sources]`](https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-sources);
|
|
209
|
+
Cloudfall itself only ever pins a release.
|
|
210
|
+
|
|
201
211
|
Only the kind directories are read as resources, so playbooks, roles, docs,
|
|
202
212
|
and tooling files may live anywhere else in the project. Commands find the
|
|
203
213
|
project on their own: the current directory when it is one, else
|
|
@@ -55,7 +55,7 @@ Not imported today; each lands in the gap report instead of being guessed at:
|
|
|
55
55
|
- Your `render.yaml` blueprint and access to your application repositories
|
|
56
56
|
- Python 3.14 and [`uv`](https://docs.astral.sh/uv/) on the machine you run
|
|
57
57
|
Cloudfall from (your workstation works; no management server is required)
|
|
58
|
-
- A project: `uvx
|
|
58
|
+
- A project: `uvx cloudfall init my-project`,
|
|
59
59
|
then `cd my-project && uv sync`. Every command below runs from inside it
|
|
60
60
|
|
|
61
61
|
## Step 1: import the blueprint
|
|
@@ -96,17 +96,14 @@ from cloudfall.operator import (
|
|
|
96
96
|
run_once as operator_run_once,
|
|
97
97
|
)
|
|
98
98
|
from cloudfall.project import (
|
|
99
|
-
DEFAULT_SOURCE_URL,
|
|
100
99
|
PROJECT_DIRECTORY_VARIABLE,
|
|
101
|
-
GitRevision,
|
|
102
|
-
GitSourceUrl,
|
|
103
100
|
InitOptions,
|
|
104
101
|
ProjectDescription,
|
|
105
102
|
ProjectError,
|
|
106
103
|
ProjectName,
|
|
107
104
|
init_project,
|
|
108
105
|
project_context,
|
|
109
|
-
|
|
106
|
+
resolve_installed_version,
|
|
110
107
|
)
|
|
111
108
|
from cloudfall.render_api import (
|
|
112
109
|
HttpRenderApiClient,
|
|
@@ -199,13 +196,6 @@ def _add_init_parser(
|
|
|
199
196
|
"--name",
|
|
200
197
|
help="package name written to pyproject.toml (default: the directory name)",
|
|
201
198
|
)
|
|
202
|
-
init_parser.add_argument(
|
|
203
|
-
"--rev",
|
|
204
|
-
help=(
|
|
205
|
-
"Cloudfall commit to pin (default: the commit the running "
|
|
206
|
-
"cloudfall was installed from)"
|
|
207
|
-
),
|
|
208
|
-
)
|
|
209
199
|
init_parser.add_argument(
|
|
210
200
|
"--description",
|
|
211
201
|
help=(
|
|
@@ -213,13 +203,6 @@ def _add_init_parser(
|
|
|
213
203
|
"and pyproject.toml"
|
|
214
204
|
),
|
|
215
205
|
)
|
|
216
|
-
init_parser.add_argument(
|
|
217
|
-
"--source",
|
|
218
|
-
default=DEFAULT_SOURCE_URL,
|
|
219
|
-
help=(
|
|
220
|
-
f"git location of Cloudfall to install from (default: {DEFAULT_SOURCE_URL})"
|
|
221
|
-
),
|
|
222
|
-
)
|
|
223
206
|
|
|
224
207
|
|
|
225
208
|
def _add_add_parsers(
|
|
@@ -1030,16 +1013,10 @@ def _run_init(arguments: Namespace) -> int:
|
|
|
1030
1013
|
if arguments.name is not None
|
|
1031
1014
|
else ProjectName.from_directory(directory)
|
|
1032
1015
|
)
|
|
1033
|
-
revision = (
|
|
1034
|
-
GitRevision.from_boundary(arguments.rev)
|
|
1035
|
-
if arguments.rev is not None
|
|
1036
|
-
else resolve_installed_revision()
|
|
1037
|
-
)
|
|
1038
1016
|
options = InitOptions(
|
|
1039
1017
|
directory=directory,
|
|
1040
1018
|
name=name,
|
|
1041
|
-
|
|
1042
|
-
source=GitSourceUrl.from_boundary(arguments.source),
|
|
1019
|
+
version=resolve_installed_version(),
|
|
1043
1020
|
description=(
|
|
1044
1021
|
ProjectDescription.from_boundary(arguments.description)
|
|
1045
1022
|
if arguments.description is not None
|
|
@@ -4,7 +4,7 @@ A project is one directory holding everything Cloudfall needs to run one
|
|
|
4
4
|
user's stack: the fleet (servers, server types, SSH public keys), the
|
|
5
5
|
applications (applications, components, services, domains), the operating
|
|
6
6
|
declarations (alert rules, operator policies, logging stacks), and the
|
|
7
|
-
``pyproject.toml`` that pins the Cloudfall
|
|
7
|
+
``pyproject.toml`` that pins the released Cloudfall the project runs with.
|
|
8
8
|
Every ``cloudfall`` command runs inside a project: the current directory
|
|
9
9
|
when it is one, else ``--project``, else ``CLOUDFALL_PROJECT``.
|
|
10
10
|
Evidence, receipts, and rendered files land under ``tmp/`` inside the
|
|
@@ -13,7 +13,6 @@ project and are never committed.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
import json
|
|
17
16
|
import os
|
|
18
17
|
import re
|
|
19
18
|
import shutil
|
|
@@ -25,7 +24,6 @@ from enum import Enum
|
|
|
25
24
|
from importlib import metadata
|
|
26
25
|
from pathlib import Path
|
|
27
26
|
from typing import TYPE_CHECKING
|
|
28
|
-
from urllib.parse import urlparse
|
|
29
27
|
|
|
30
28
|
from cloudfall.commands import CommandEffect, commands_with_effect
|
|
31
29
|
from cloudfall.domain import ResourceKind
|
|
@@ -33,17 +31,17 @@ from cloudfall.domain import ResourceKind
|
|
|
33
31
|
if TYPE_CHECKING:
|
|
34
32
|
from collections.abc import Callable, Iterator, Mapping
|
|
35
33
|
|
|
36
|
-
_GIT_REVISION_PATTERN = re.compile(r"^[0-9a-f]{40}$")
|
|
37
34
|
_PROJECT_NAME_PATTERN = re.compile(r"^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$")
|
|
38
|
-
|
|
39
|
-
r"
|
|
35
|
+
_RELEASE_VERSION_PATTERN = re.compile(
|
|
36
|
+
r"^\d+(?:\.\d+)*(?:(?:a|b|rc)\d+)?(?:\.post\d+)?(?:\.dev\d+)?$"
|
|
40
37
|
)
|
|
41
38
|
_PROJECT_NAME_MAX_LENGTH = 64
|
|
42
39
|
_PROJECT_DESCRIPTION_MAX_LENGTH = 512
|
|
43
40
|
_GIT_TIMEOUT_SECONDS = 30
|
|
44
41
|
_GIT_NOT_A_REPOSITORY_EXIT_CODE = 128
|
|
45
42
|
|
|
46
|
-
|
|
43
|
+
REPOSITORY_URL = "https://github.com/romamo/cloudfall"
|
|
44
|
+
"""Location of the Cloudfall repository, for links to its files."""
|
|
47
45
|
|
|
48
46
|
SECRETS_DIRECTORY = "secrets"
|
|
49
47
|
"""Project directory holding sops-encrypted secret fragments."""
|
|
@@ -66,9 +64,7 @@ CLAUDE_POINTER_FILE = "CLAUDE.md"
|
|
|
66
64
|
_ERROR_NAME_UNDERIVED = "project_name_underived"
|
|
67
65
|
_ERROR_DIRECTORY_MISSING = "project_directory_missing"
|
|
68
66
|
_ERROR_DIRECTORY_UNRESOLVED = "project_directory_unresolved"
|
|
69
|
-
|
|
70
|
-
_ERROR_REVISION_UNCOMMITTED = "project_revision_uncommitted"
|
|
71
|
-
_ERROR_REVISION_UNPUBLISHED = "project_revision_unpublished"
|
|
67
|
+
_ERROR_VERSION_UNRESOLVED = "project_version_unresolved"
|
|
72
68
|
_ERROR_NOT_A_DIRECTORY = "project_directory_not_a_directory"
|
|
73
69
|
_ERROR_NOT_EMPTY = "project_directory_not_empty"
|
|
74
70
|
_ERROR_GIT_UNAVAILABLE = "project_git_unavailable"
|
|
@@ -100,72 +96,27 @@ class ProjectError(RuntimeError):
|
|
|
100
96
|
|
|
101
97
|
|
|
102
98
|
@dataclass(frozen=True, slots=True)
|
|
103
|
-
class
|
|
104
|
-
"""
|
|
99
|
+
class ReleaseVersion:
|
|
100
|
+
"""Released Cloudfall version a project pins from the package index."""
|
|
105
101
|
|
|
106
102
|
value: str
|
|
107
103
|
|
|
108
104
|
def __post_init__(self) -> None:
|
|
109
|
-
"""Enforce the
|
|
110
|
-
if not
|
|
111
|
-
message = f"
|
|
105
|
+
"""Enforce the release-version invariant at construction time."""
|
|
106
|
+
if not _RELEASE_VERSION_PATTERN.fullmatch(self.value):
|
|
107
|
+
message = f"release version must be a PEP 440 release: {self.value!r}"
|
|
112
108
|
raise ValueError(message)
|
|
113
109
|
|
|
114
110
|
@classmethod
|
|
115
|
-
def from_boundary(cls, value: object) ->
|
|
111
|
+
def from_boundary(cls, value: object) -> ReleaseVersion:
|
|
116
112
|
"""Coerce a boundary value while keeping internal APIs strictly typed."""
|
|
117
113
|
if not isinstance(value, str):
|
|
118
|
-
message = f"
|
|
119
|
-
raise TypeError(message)
|
|
120
|
-
return cls(value.strip().lower())
|
|
121
|
-
|
|
122
|
-
def __str__(self) -> str:
|
|
123
|
-
"""Return the serialized hash."""
|
|
124
|
-
return self.value
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@dataclass(frozen=True, slots=True)
|
|
128
|
-
class GitSourceUrl:
|
|
129
|
-
"""Location of the Cloudfall repository a project installs from."""
|
|
130
|
-
|
|
131
|
-
value: str
|
|
132
|
-
|
|
133
|
-
def __post_init__(self) -> None:
|
|
134
|
-
"""Accept https and ssh git locations only."""
|
|
135
|
-
parsed = urlparse(self.value)
|
|
136
|
-
https = parsed.scheme == "https" and bool(parsed.netloc)
|
|
137
|
-
ssh = parsed.scheme == "ssh" and bool(parsed.netloc)
|
|
138
|
-
scp_like = _SCP_LIKE_SOURCE_PATTERN.fullmatch(self.value) is not None
|
|
139
|
-
if not (https or ssh or scp_like):
|
|
140
|
-
message = f"git source must be an https or ssh location: {self.value!r}"
|
|
141
|
-
raise ValueError(message)
|
|
142
|
-
|
|
143
|
-
@classmethod
|
|
144
|
-
def from_boundary(cls, value: object) -> GitSourceUrl:
|
|
145
|
-
"""Coerce a boundary value while keeping internal APIs strictly typed."""
|
|
146
|
-
if not isinstance(value, str):
|
|
147
|
-
message = f"git source must be a string, got {type(value).__name__}"
|
|
114
|
+
message = f"release version must be a string, got {type(value).__name__}"
|
|
148
115
|
raise TypeError(message)
|
|
149
116
|
return cls(value.strip())
|
|
150
117
|
|
|
151
|
-
@property
|
|
152
|
-
def browse_url(self) -> str:
|
|
153
|
-
"""Return the https location of the repository for links to its files.
|
|
154
|
-
|
|
155
|
-
``https://host/owner/repo.git``, ``ssh://git@host/owner/repo.git``, and
|
|
156
|
-
``git@host:owner/repo.git`` all browse at ``https://host/owner/repo``.
|
|
157
|
-
"""
|
|
158
|
-
scp_like = _SCP_LIKE_SOURCE_PATTERN.fullmatch(self.value)
|
|
159
|
-
if scp_like is not None:
|
|
160
|
-
host, path = scp_like.group("host"), scp_like.group("path")
|
|
161
|
-
else:
|
|
162
|
-
parsed = urlparse(self.value)
|
|
163
|
-
host, path = parsed.hostname or "", parsed.path
|
|
164
|
-
path = path.strip("/").removesuffix(".git")
|
|
165
|
-
return f"https://{host}/{path}"
|
|
166
|
-
|
|
167
118
|
def __str__(self) -> str:
|
|
168
|
-
"""Return the serialized
|
|
119
|
+
"""Return the serialized version."""
|
|
169
120
|
return self.value
|
|
170
121
|
|
|
171
122
|
|
|
@@ -255,8 +206,7 @@ class InitOptions:
|
|
|
255
206
|
|
|
256
207
|
directory: Path
|
|
257
208
|
name: ProjectName
|
|
258
|
-
|
|
259
|
-
source: GitSourceUrl = GitSourceUrl(DEFAULT_SOURCE_URL)
|
|
209
|
+
version: ReleaseVersion
|
|
260
210
|
description: ProjectDescription | None = None
|
|
261
211
|
|
|
262
212
|
|
|
@@ -276,8 +226,7 @@ class ProjectScaffold:
|
|
|
276
226
|
|
|
277
227
|
directory: Path
|
|
278
228
|
name: ProjectName
|
|
279
|
-
|
|
280
|
-
source: GitSourceUrl
|
|
229
|
+
version: ReleaseVersion
|
|
281
230
|
files: tuple[str, ...]
|
|
282
231
|
git: GitSetup
|
|
283
232
|
|
|
@@ -288,8 +237,7 @@ class ProjectScaffold:
|
|
|
288
237
|
"project": {
|
|
289
238
|
"directory": str(self.directory),
|
|
290
239
|
"name": str(self.name),
|
|
291
|
-
"
|
|
292
|
-
"source": str(self.source),
|
|
240
|
+
"version": str(self.version),
|
|
293
241
|
"git": self.git.value,
|
|
294
242
|
},
|
|
295
243
|
"files": list(self.files),
|
|
@@ -302,17 +250,6 @@ class ProjectScaffold:
|
|
|
302
250
|
}
|
|
303
251
|
|
|
304
252
|
|
|
305
|
-
@dataclass(frozen=True, slots=True)
|
|
306
|
-
class CheckoutState:
|
|
307
|
-
"""What a source checkout says about the commit it is running."""
|
|
308
|
-
|
|
309
|
-
head: str
|
|
310
|
-
clean: bool
|
|
311
|
-
"""No tracked file differs from ``HEAD``, so ``HEAD`` is the running code."""
|
|
312
|
-
published: bool
|
|
313
|
-
"""``HEAD`` is reachable from a remote-tracking branch, so it can be fetched."""
|
|
314
|
-
|
|
315
|
-
|
|
316
253
|
def is_project(directory: Path) -> bool:
|
|
317
254
|
"""Return whether a directory holds at least one resource kind directory."""
|
|
318
255
|
return directory.is_dir() and any(
|
|
@@ -388,69 +325,30 @@ def project_path(value: str) -> Path:
|
|
|
388
325
|
return path
|
|
389
326
|
|
|
390
327
|
|
|
391
|
-
def
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
"""Return the commit the running Cloudfall was installed from.
|
|
328
|
+
def resolve_installed_version(
|
|
329
|
+
read_version: Callable[[], str] | None = None,
|
|
330
|
+
) -> ReleaseVersion:
|
|
331
|
+
"""Return the released version of the running Cloudfall.
|
|
396
332
|
|
|
397
|
-
A
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
cannot fetch it). Anything else cannot be pinned and must be given
|
|
402
|
-
explicitly.
|
|
333
|
+
A project depends on ``cloudfall==<version>`` and resolves it from the
|
|
334
|
+
package index like any other dependency. An install whose version is not
|
|
335
|
+
a release cannot be pinned that way, which is a checkout of this
|
|
336
|
+
repository between releases.
|
|
403
337
|
"""
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
"pass --rev with the commit to pin"
|
|
409
|
-
)
|
|
410
|
-
raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
|
|
411
|
-
record = json.loads(direct_url)
|
|
412
|
-
if not isinstance(record, dict):
|
|
413
|
-
message = f"direct_url.json is not an object: {direct_url!r}"
|
|
414
|
-
raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
|
|
415
|
-
vcs_info = record.get("vcs_info")
|
|
416
|
-
if isinstance(vcs_info, dict) and "commit_id" in vcs_info:
|
|
417
|
-
return GitRevision.from_boundary(vcs_info["commit_id"])
|
|
418
|
-
dir_info = record.get("dir_info")
|
|
419
|
-
url = record.get("url")
|
|
420
|
-
if (
|
|
421
|
-
isinstance(dir_info, dict)
|
|
422
|
-
and isinstance(url, str)
|
|
423
|
-
and url.startswith("file://")
|
|
424
|
-
):
|
|
425
|
-
checkout = Path(urlparse(url).path)
|
|
426
|
-
inspect = inspect_checkout or _inspect_checkout
|
|
427
|
-
return _pinnable_head(checkout, inspect(checkout))
|
|
428
|
-
message = (
|
|
429
|
-
"the installed cloudfall distribution is neither a git install nor a "
|
|
430
|
-
f"source checkout ({url!r}); pass --rev with the commit to pin"
|
|
431
|
-
)
|
|
432
|
-
raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
def _pinnable_head(checkout: Path, state: CheckoutState) -> GitRevision:
|
|
436
|
-
head = GitRevision.from_boundary(state.head)
|
|
437
|
-
if not state.clean:
|
|
438
|
-
message = (
|
|
439
|
-
f"the source checkout {checkout} has uncommitted changes, so HEAD "
|
|
440
|
-
f"{head} is not the code that is running; commit them or pass --rev"
|
|
441
|
-
)
|
|
442
|
-
raise ProjectError(_ERROR_REVISION_UNCOMMITTED, message)
|
|
443
|
-
if not state.published:
|
|
338
|
+
raw = (read_version or _installed_version)()
|
|
339
|
+
try:
|
|
340
|
+
return ReleaseVersion.from_boundary(raw)
|
|
341
|
+
except ValueError as error:
|
|
444
342
|
message = (
|
|
445
|
-
f"
|
|
446
|
-
"
|
|
343
|
+
f"the installed cloudfall version {raw!r} is not a release, so a "
|
|
344
|
+
"project cannot pin it; install a released cloudfall, or set the "
|
|
345
|
+
"dependency in the project's pyproject.toml by hand"
|
|
447
346
|
)
|
|
448
|
-
raise ProjectError(
|
|
449
|
-
return head
|
|
347
|
+
raise ProjectError(_ERROR_VERSION_UNRESOLVED, message) from error
|
|
450
348
|
|
|
451
349
|
|
|
452
|
-
def
|
|
453
|
-
return metadata.
|
|
350
|
+
def _installed_version() -> str:
|
|
351
|
+
return metadata.version("cloudfall")
|
|
454
352
|
|
|
455
353
|
|
|
456
354
|
def _git_binary() -> str:
|
|
@@ -486,15 +384,6 @@ def _git_or_fail(directory: Path, *arguments: str) -> str:
|
|
|
486
384
|
return completed.stdout
|
|
487
385
|
|
|
488
386
|
|
|
489
|
-
def _inspect_checkout(checkout: Path) -> CheckoutState:
|
|
490
|
-
head = _git_or_fail(checkout, "rev-parse", "HEAD").strip()
|
|
491
|
-
changes = _git_or_fail(checkout, "status", "--porcelain", "--untracked-files=no")
|
|
492
|
-
remote_branches = _git_or_fail(checkout, "branch", "--remotes", "--contains", head)
|
|
493
|
-
return CheckoutState(
|
|
494
|
-
head=head, clean=not changes.strip(), published=bool(remote_branches.strip())
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
|
|
498
387
|
def _initialize_git(directory: Path) -> GitSetup:
|
|
499
388
|
"""Create a repository in the directory unless one already encloses it."""
|
|
500
389
|
enclosing = _run_git(directory, "rev-parse", "--is-inside-work-tree")
|
|
@@ -543,8 +432,7 @@ def init_project(
|
|
|
543
432
|
return ProjectScaffold(
|
|
544
433
|
directory=directory,
|
|
545
434
|
name=options.name,
|
|
546
|
-
|
|
547
|
-
source=options.source,
|
|
435
|
+
version=options.version,
|
|
548
436
|
files=tuple(files),
|
|
549
437
|
git=git,
|
|
550
438
|
)
|
|
@@ -561,17 +449,14 @@ name = "{options.name}"
|
|
|
561
449
|
version = "0"
|
|
562
450
|
description = "{description}"
|
|
563
451
|
requires-python = ">=3.14"
|
|
564
|
-
|
|
452
|
+
# Cloudfall is an installed package: the schema catalog and the Ansible engine
|
|
453
|
+
# ship inside the wheel. Bump this version to move the project to a newer
|
|
454
|
+
# Cloudfall, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
|
|
455
|
+
# the `cloudfall-mcp` agent server.
|
|
456
|
+
dependencies = ["cloudfall=={options.version}"]
|
|
565
457
|
|
|
566
458
|
[tool.uv]
|
|
567
459
|
package = false
|
|
568
|
-
|
|
569
|
-
# Cloudfall is an installed package: the schema catalog and the Ansible engine
|
|
570
|
-
# ship inside the wheel. Bump `rev` to move this project to a newer Cloudfall
|
|
571
|
-
# commit, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
|
|
572
|
-
# the `cloudfall-mcp` agent server.
|
|
573
|
-
[tool.uv.sources]
|
|
574
|
-
cloudfall = {{ git = "{options.source}", rev = "{options.revision}" }}
|
|
575
460
|
"""
|
|
576
461
|
|
|
577
462
|
|
|
@@ -603,10 +488,10 @@ creation_rules:
|
|
|
603
488
|
def _readme(options: InitOptions, git: GitSetup) -> str:
|
|
604
489
|
tmp = RUNTIME_DIRECTORY
|
|
605
490
|
secrets = SECRETS_DIRECTORY
|
|
606
|
-
browse =
|
|
607
|
-
|
|
608
|
-
secrets_guide = f"{browse}/blob/{
|
|
609
|
-
examples = f"{browse}/tree/{
|
|
491
|
+
browse = REPOSITORY_URL
|
|
492
|
+
ref = f"v{options.version}"
|
|
493
|
+
secrets_guide = f"{browse}/blob/{ref}/{SECRETS_GUIDE_PATH}"
|
|
494
|
+
examples = f"{browse}/tree/{ref}/{EXAMPLES_PATH}"
|
|
610
495
|
about = (
|
|
611
496
|
str(options.description)
|
|
612
497
|
if options.description is not None
|
|
@@ -689,9 +574,9 @@ uv run cloudfall migrate --build <component>=main
|
|
|
689
574
|
uv run cloudfall migrate --build <component>=main --yes
|
|
690
575
|
```
|
|
691
576
|
|
|
692
|
-
Cloudfall is pinned to one
|
|
693
|
-
`uv sync` to move the project to a newer release. The links below
|
|
694
|
-
the pinned
|
|
577
|
+
Cloudfall is pinned to one released version in `pyproject.toml`; bump it
|
|
578
|
+
and run `uv sync` to move the project to a newer release. The links below
|
|
579
|
+
point at the pinned release.
|
|
695
580
|
|
|
696
581
|
[cloudfall]: {browse}
|
|
697
582
|
[secrets-guide]: {secrets_guide}
|
|
@@ -8,7 +8,12 @@ from pathlib import Path
|
|
|
8
8
|
import pytest
|
|
9
9
|
import yaml
|
|
10
10
|
from cloudfall.agent_tools import AgentConfig, AgentToolset
|
|
11
|
-
from cloudfall.project import
|
|
11
|
+
from cloudfall.project import (
|
|
12
|
+
InitOptions,
|
|
13
|
+
ProjectName,
|
|
14
|
+
ReleaseVersion,
|
|
15
|
+
init_project,
|
|
16
|
+
)
|
|
12
17
|
|
|
13
18
|
ROOT = Path(__file__).parents[2]
|
|
14
19
|
SCHEMAS = ROOT / "config" / "schemas" / "v1"
|
|
@@ -26,7 +31,7 @@ services:
|
|
|
26
31
|
"""
|
|
27
32
|
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
VERSION = "0.3.0"
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
def _config(
|
|
@@ -141,7 +146,7 @@ def _fresh_project(tmp_path: Path) -> AgentToolset:
|
|
|
141
146
|
InitOptions(
|
|
142
147
|
directory=directory,
|
|
143
148
|
name=ProjectName("project"),
|
|
144
|
-
|
|
149
|
+
version=ReleaseVersion(VERSION),
|
|
145
150
|
)
|
|
146
151
|
)
|
|
147
152
|
return AgentToolset(_config(tmp_path, project_directory=directory))
|