cloudfall 0.2.0__tar.gz → 0.2.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.2.0 → cloudfall-0.2.1}/CHANGELOG.md +24 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/PKG-INFO +25 -14
- {cloudfall-0.2.0 → cloudfall-0.2.1}/README.md +24 -13
- {cloudfall-0.2.0 → cloudfall-0.2.1}/pyproject.toml +1 -1
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/cli.py +14 -9
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/project.py +158 -39
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_agent_tools.py +8 -2
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_authoring.py +8 -2
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_project.py +69 -11
- {cloudfall-0.2.0 → cloudfall-0.2.1}/uv.lock +1 -1
- {cloudfall-0.2.0 → cloudfall-0.2.1}/.ansible-lint +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/.github/workflows/ci.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/.github/workflows/release.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/.gitignore +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/ARCHITECTURE.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/CONTRIBUTING.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/LICENSE +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/MANIFESTO.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/ROADMAP.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/SECURITY.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/Taskfile.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/alert-rules/postgresql-down.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/applications/crm.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/components/crm-backend.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/domains/crm-site.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/logging-stacks/operations.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/operator-policies/production.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/server-types/debian-application.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/servers/h1.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/servers/h2.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/services/postgresql-main.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/services/redis-cache.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/examples/ssh-public-keys/example-admin.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/alert-rule.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/application.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/artifact.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/backup-receipt.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/common.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/component.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/deployment-receipt.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/domain.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/environment-receipt.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/logging-stack.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/observed-domain.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/observed-server.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/operator-policy.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/operator-proposal.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/release-receipt.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/server-type.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/server.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/service.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/schemas/v1/ssh-public-key.schema.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/missing-server/applications/crm.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/missing-server/components/crm-backend.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/missing-server/server-types/debian-application.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/missing-server/servers/h1.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/invalid/schema/components/component-unknown-field.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/observed/compliant/h1.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/config/tests/observed/compliant/h2.json +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/ansible-context.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/availability-design.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/brownfield-design.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/decisions/2026-09-14-no-ansible-replacement.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/decisions/2026-09-14-no-nixos-base.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/decisions/2026-09-14-no-rust-rewrite.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/fleet-goals.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/hybrid-storage-design.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/landscape.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/logging-service-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/neon-migration-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/new-server-storage-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/operator-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-debian-container-m1.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-dns-cutover.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-guided-data-migration.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-hetzner-m1.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-hetzner-m2-m3.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-hetzner-m4-m5.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-08-render-cutover.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-10-hetzner-m7.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-10-hetzner-m8.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-10-hetzner-m9.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/proving-runs/2026-09-11-hetzner-m10.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/render-migration-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/docs/secrets-guide.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/ansible.cfg +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/backup.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/baseline.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/bootstrap.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/data.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/deploy.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/domains.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/health.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/inspect.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/logging.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/restart.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/rollback.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/services.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/playbooks/time.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_access/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_bootstrap/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_bootstrap/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_bootstrap/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_bootstrap/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_data_migration/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_data_migration/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_data_migration/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_data_migration/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/health.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/health_gate.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/restart.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/tasks/rollback.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_deploy/templates/component.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_firewall/templates/nftables.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_inspect/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_inspect/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_inspect/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_inspect/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager-defaults.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/alertmanager.yaml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/grafana.ini.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-datasource.yml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki-systemd-override.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/loki.yaml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/nginx-loki-gateway.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-datasource.yml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-defaults.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus-rules.yaml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_backend/templates/prometheus.yaml.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/templates/alloy-systemd-override.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_logging_collector/templates/config.alloy.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_nginx_site/templates/domain-site.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.sh.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/backup.timer.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_postgresql/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/handlers/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/backup.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/backup.sh.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/backup.timer.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/cloudfall.conf.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.service.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.sh.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_redis/templates/restore-check.timer.j2 +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_time/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_time/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_time/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_time/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_unattended_upgrades/defaults/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/argument_specs.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_unattended_upgrades/meta/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/ansible/roles/cloudfall_unattended_upgrades/tasks/main.yml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/__init__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/__main__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/ansible_inventory.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/artifact.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/cli.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/playbook.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/src/cloudfall_engine/py.typed +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_ansible_inventory.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_artifact_builder.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_deploy_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_firewall_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_logging_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_nginx_site_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_playbook.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/engine/tests/test_postgresql_templates.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/environment.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/findings.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/issues.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/linkedin.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/readiness.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-agent-dev.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-dev.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-index.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-issues.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/report-runtime.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/trace.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/evaluations/cloudfall/x.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/pyproject.toml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/render.yaml +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/src/demo/__init__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/src/demo/asgi.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/examples/render-demo/uv.lock +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/README.md +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/__init__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/__main__.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/agent_tools.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/arguments.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/audit.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/authoring.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/commands.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/cutover.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/dashboard.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/dashboard_server.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/domain.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/importer.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/inventory.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/lifecycle.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/mcp_server.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/migrate.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/observation.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/operations.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/operator.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/py.typed +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/render_api.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/resources.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/secrets.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/service_evidence.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/src/cloudfall/validation.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_arguments.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_audit.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_commands.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_cutover.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_dashboard_server.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_data_migration.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_importer.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_inventory.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_lifecycle.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_migrate.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_operations_dashboard.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_operator.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_render_api.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_resources.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_secrets.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_service_evidence.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/sdk/tests/test_validation.py +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/apple-touch-icon.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/favicon-32.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/favicon.svg +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/icon-192.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/icon-512.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/index.html +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/manifesto.html +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/og.png +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/public/site.webmanifest +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/src/worker.js +0 -0
- {cloudfall-0.2.0 → cloudfall-0.2.1}/site/wrangler.jsonc +0 -0
|
@@ -6,6 +6,30 @@ Notable changes to Cloudfall. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.1] — 2026-09-20
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `cloudfall init` works from a PyPI install. An install from the package
|
|
14
|
+
index records no origin to read a commit from, so `init` failed with
|
|
15
|
+
`project_revision_unresolved` on the first command a new user runs, and
|
|
16
|
+
`--rev` only worked around it by writing a project that installed
|
|
17
|
+
Cloudfall from git instead of from the index. A project now pins whatever
|
|
18
|
+
ran `init`: `cloudfall==<version>` with no `[tool.uv.sources]` table for a
|
|
19
|
+
released install, the commit and repository for a git or source install
|
|
20
|
+
- The project README links the secrets guide and the reference examples at
|
|
21
|
+
the release tag when the pin is a released version, rather than at a
|
|
22
|
+
commit the install does not know
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `InitOptions` and the `cloudfall init` envelope carry one `pin` instead of
|
|
27
|
+
a `revision` and a `source`: `{"kind": "index", "version": ...}` or
|
|
28
|
+
`{"kind": "git", "revision": ..., "source": ...}`. `resolve_installed_pin`
|
|
29
|
+
replaces `resolve_installed_revision`
|
|
30
|
+
- `--rev` documents that it pins a commit from `--source`, which is now the
|
|
31
|
+
explicit alternative to the default rather than the only mechanism
|
|
32
|
+
|
|
9
33
|
## [0.2.0] — 2026-09-20
|
|
10
34
|
|
|
11
35
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cloudfall
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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,14 @@ 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
|
|
200
|
+
this repository. `cloudfall init` is the first command, and it pins the
|
|
201
|
+
project to the Cloudfall that ran it: the released version when that came
|
|
202
|
+
from PyPI, the exact commit when it came from git or a clone. Without a
|
|
203
|
+
directory argument it initializes the current directory, which must be
|
|
204
|
+
empty:
|
|
196
205
|
|
|
197
206
|
```console
|
|
198
|
-
uvx
|
|
207
|
+
uvx cloudfall init my-project
|
|
199
208
|
cd my-project
|
|
200
209
|
uv sync
|
|
201
210
|
```
|
|
@@ -215,15 +224,17 @@ repository, and a `pyproject.toml`:
|
|
|
215
224
|
name = "my-project"
|
|
216
225
|
version = "0"
|
|
217
226
|
requires-python = ">=3.14"
|
|
218
|
-
dependencies = ["cloudfall"]
|
|
227
|
+
dependencies = ["cloudfall==<version>"]
|
|
219
228
|
|
|
220
229
|
[tool.uv]
|
|
221
230
|
package = false
|
|
222
|
-
|
|
223
|
-
[tool.uv.sources]
|
|
224
|
-
cloudfall = { git = "https://github.com/romamo/cloudfall.git", rev = "<commit>" }
|
|
225
231
|
```
|
|
226
232
|
|
|
233
|
+
Bump that pin and run `uv sync` to move the project to a newer Cloudfall. A
|
|
234
|
+
project initialized from a git or source install pins a commit instead, with
|
|
235
|
+
a `[tool.uv.sources]` table naming the repository and revision; `--rev` asks
|
|
236
|
+
for that explicitly.
|
|
237
|
+
|
|
227
238
|
Only the kind directories are read as resources, so playbooks, roles, docs,
|
|
228
239
|
and tooling files may live anywhere else in the project. Commands find the
|
|
229
240
|
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,14 @@ 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
|
|
174
|
+
this repository. `cloudfall init` is the first command, and it pins the
|
|
175
|
+
project to the Cloudfall that ran it: the released version when that came
|
|
176
|
+
from PyPI, the exact commit when it came from git or a clone. Without a
|
|
177
|
+
directory argument it initializes the current directory, which must be
|
|
178
|
+
empty:
|
|
170
179
|
|
|
171
180
|
```console
|
|
172
|
-
uvx
|
|
181
|
+
uvx cloudfall init my-project
|
|
173
182
|
cd my-project
|
|
174
183
|
uv sync
|
|
175
184
|
```
|
|
@@ -189,15 +198,17 @@ repository, and a `pyproject.toml`:
|
|
|
189
198
|
name = "my-project"
|
|
190
199
|
version = "0"
|
|
191
200
|
requires-python = ">=3.14"
|
|
192
|
-
dependencies = ["cloudfall"]
|
|
201
|
+
dependencies = ["cloudfall==<version>"]
|
|
193
202
|
|
|
194
203
|
[tool.uv]
|
|
195
204
|
package = false
|
|
196
|
-
|
|
197
|
-
[tool.uv.sources]
|
|
198
|
-
cloudfall = { git = "https://github.com/romamo/cloudfall.git", rev = "<commit>" }
|
|
199
205
|
```
|
|
200
206
|
|
|
207
|
+
Bump that pin and run `uv sync` to move the project to a newer Cloudfall. A
|
|
208
|
+
project initialized from a git or source install pins a commit instead, with
|
|
209
|
+
a `[tool.uv.sources]` table naming the repository and revision; `--rev` asks
|
|
210
|
+
for that explicitly.
|
|
211
|
+
|
|
201
212
|
Only the kind directories are read as resources, so playbooks, roles, docs,
|
|
202
213
|
and tooling files may live anywhere else in the project. Commands find the
|
|
203
214
|
project on their own: the current directory when it is one, else
|
|
@@ -98,6 +98,7 @@ from cloudfall.operator import (
|
|
|
98
98
|
from cloudfall.project import (
|
|
99
99
|
DEFAULT_SOURCE_URL,
|
|
100
100
|
PROJECT_DIRECTORY_VARIABLE,
|
|
101
|
+
GitPin,
|
|
101
102
|
GitRevision,
|
|
102
103
|
GitSourceUrl,
|
|
103
104
|
InitOptions,
|
|
@@ -106,7 +107,7 @@ from cloudfall.project import (
|
|
|
106
107
|
ProjectName,
|
|
107
108
|
init_project,
|
|
108
109
|
project_context,
|
|
109
|
-
|
|
110
|
+
resolve_installed_pin,
|
|
110
111
|
)
|
|
111
112
|
from cloudfall.render_api import (
|
|
112
113
|
HttpRenderApiClient,
|
|
@@ -202,8 +203,9 @@ def _add_init_parser(
|
|
|
202
203
|
init_parser.add_argument(
|
|
203
204
|
"--rev",
|
|
204
205
|
help=(
|
|
205
|
-
"Cloudfall commit to pin (default: the
|
|
206
|
-
"cloudfall was
|
|
206
|
+
"Cloudfall commit to pin, installed from --source (default: the "
|
|
207
|
+
"released version of the running cloudfall, or the commit it was "
|
|
208
|
+
"installed from when that was a git or source install)"
|
|
207
209
|
),
|
|
208
210
|
)
|
|
209
211
|
init_parser.add_argument(
|
|
@@ -217,7 +219,8 @@ def _add_init_parser(
|
|
|
217
219
|
"--source",
|
|
218
220
|
default=DEFAULT_SOURCE_URL,
|
|
219
221
|
help=(
|
|
220
|
-
|
|
222
|
+
"git location of Cloudfall to install from, with --rev "
|
|
223
|
+
f"(default: {DEFAULT_SOURCE_URL})"
|
|
221
224
|
),
|
|
222
225
|
)
|
|
223
226
|
|
|
@@ -1030,16 +1033,18 @@ def _run_init(arguments: Namespace) -> int:
|
|
|
1030
1033
|
if arguments.name is not None
|
|
1031
1034
|
else ProjectName.from_directory(directory)
|
|
1032
1035
|
)
|
|
1033
|
-
|
|
1034
|
-
|
|
1036
|
+
pin = (
|
|
1037
|
+
GitPin(
|
|
1038
|
+
GitRevision.from_boundary(arguments.rev),
|
|
1039
|
+
GitSourceUrl.from_boundary(arguments.source),
|
|
1040
|
+
)
|
|
1035
1041
|
if arguments.rev is not None
|
|
1036
|
-
else
|
|
1042
|
+
else resolve_installed_pin()
|
|
1037
1043
|
)
|
|
1038
1044
|
options = InitOptions(
|
|
1039
1045
|
directory=directory,
|
|
1040
1046
|
name=name,
|
|
1041
|
-
|
|
1042
|
-
source=GitSourceUrl.from_boundary(arguments.source),
|
|
1047
|
+
pin=pin,
|
|
1043
1048
|
description=(
|
|
1044
1049
|
ProjectDescription.from_boundary(arguments.description)
|
|
1045
1050
|
if arguments.description is not None
|
|
@@ -4,7 +4,8 @@ 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 Cloudfall the project runs with: a
|
|
8
|
+
released version from the package index, or a commit in the repository.
|
|
8
9
|
Every ``cloudfall`` command runs inside a project: the current directory
|
|
9
10
|
when it is one, else ``--project``, else ``CLOUDFALL_PROJECT``.
|
|
10
11
|
Evidence, receipts, and rendered files land under ``tmp/`` inside the
|
|
@@ -35,6 +36,9 @@ if TYPE_CHECKING:
|
|
|
35
36
|
|
|
36
37
|
_GIT_REVISION_PATTERN = re.compile(r"^[0-9a-f]{40}$")
|
|
37
38
|
_PROJECT_NAME_PATTERN = re.compile(r"^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$")
|
|
39
|
+
_RELEASE_VERSION_PATTERN = re.compile(
|
|
40
|
+
r"^\d+(?:\.\d+)*(?:(?:a|b|rc)\d+)?(?:\.post\d+)?(?:\.dev\d+)?$"
|
|
41
|
+
)
|
|
38
42
|
_SCP_LIKE_SOURCE_PATTERN = re.compile(
|
|
39
43
|
r"^(?P<user>[\w.-]+)@(?P<host>[\w.-]+):(?P<path>[\w./-]+)$"
|
|
40
44
|
)
|
|
@@ -69,6 +73,7 @@ _ERROR_DIRECTORY_UNRESOLVED = "project_directory_unresolved"
|
|
|
69
73
|
_ERROR_REVISION_UNRESOLVED = "project_revision_unresolved"
|
|
70
74
|
_ERROR_REVISION_UNCOMMITTED = "project_revision_uncommitted"
|
|
71
75
|
_ERROR_REVISION_UNPUBLISHED = "project_revision_unpublished"
|
|
76
|
+
_ERROR_VERSION_UNRESOLVED = "project_version_unresolved"
|
|
72
77
|
_ERROR_NOT_A_DIRECTORY = "project_directory_not_a_directory"
|
|
73
78
|
_ERROR_NOT_EMPTY = "project_directory_not_empty"
|
|
74
79
|
_ERROR_GIT_UNAVAILABLE = "project_git_unavailable"
|
|
@@ -169,6 +174,109 @@ class GitSourceUrl:
|
|
|
169
174
|
return self.value
|
|
170
175
|
|
|
171
176
|
|
|
177
|
+
@dataclass(frozen=True, slots=True)
|
|
178
|
+
class ReleaseVersion:
|
|
179
|
+
"""Released Cloudfall version a project pins from the package index."""
|
|
180
|
+
|
|
181
|
+
value: str
|
|
182
|
+
|
|
183
|
+
def __post_init__(self) -> None:
|
|
184
|
+
"""Enforce the release-version invariant at construction time."""
|
|
185
|
+
if not _RELEASE_VERSION_PATTERN.fullmatch(self.value):
|
|
186
|
+
message = f"release version must be a PEP 440 release: {self.value!r}"
|
|
187
|
+
raise ValueError(message)
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def from_boundary(cls, value: object) -> ReleaseVersion:
|
|
191
|
+
"""Coerce a boundary value while keeping internal APIs strictly typed."""
|
|
192
|
+
if not isinstance(value, str):
|
|
193
|
+
message = f"release version must be a string, got {type(value).__name__}"
|
|
194
|
+
raise TypeError(message)
|
|
195
|
+
return cls(value.strip())
|
|
196
|
+
|
|
197
|
+
def __str__(self) -> str:
|
|
198
|
+
"""Return the serialized version."""
|
|
199
|
+
return self.value
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True, slots=True)
|
|
203
|
+
class GitPin:
|
|
204
|
+
"""A project that installs Cloudfall from a commit in the repository."""
|
|
205
|
+
|
|
206
|
+
revision: GitRevision
|
|
207
|
+
source: GitSourceUrl = GitSourceUrl(DEFAULT_SOURCE_URL)
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def requirement(self) -> str:
|
|
211
|
+
"""Return the dependency specifier for the project's ``pyproject``."""
|
|
212
|
+
return "cloudfall"
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def sources_table(self) -> str:
|
|
216
|
+
"""Return the ``[tool.uv.sources]`` table that resolves the commit."""
|
|
217
|
+
return f"""
|
|
218
|
+
# Cloudfall is an installed package: the schema catalog and the Ansible engine
|
|
219
|
+
# ship inside the wheel. Bump `rev` to move this project to a newer Cloudfall
|
|
220
|
+
# commit, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
|
|
221
|
+
# the `cloudfall-mcp` agent server.
|
|
222
|
+
[tool.uv.sources]
|
|
223
|
+
cloudfall = {{ git = "{self.source}", rev = "{self.revision}" }}
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def browse_url(self) -> str:
|
|
228
|
+
"""Return the https location of the repository the pin reads from."""
|
|
229
|
+
return self.source.browse_url
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def browse_ref(self) -> str:
|
|
233
|
+
"""Return the git ref whose files match the pinned Cloudfall."""
|
|
234
|
+
return str(self.revision)
|
|
235
|
+
|
|
236
|
+
def as_dict(self) -> dict[str, object]:
|
|
237
|
+
"""Serialize the pin for system boundaries."""
|
|
238
|
+
return {
|
|
239
|
+
"kind": "git",
|
|
240
|
+
"revision": str(self.revision),
|
|
241
|
+
"source": str(self.source),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass(frozen=True, slots=True)
|
|
246
|
+
class IndexPin:
|
|
247
|
+
"""A project that installs a released Cloudfall from the package index."""
|
|
248
|
+
|
|
249
|
+
version: ReleaseVersion
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def requirement(self) -> str:
|
|
253
|
+
"""Return the dependency specifier for the project's ``pyproject``."""
|
|
254
|
+
return f"cloudfall=={self.version}"
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def sources_table(self) -> str:
|
|
258
|
+
"""Return nothing: a released version needs no source override."""
|
|
259
|
+
return ""
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def browse_url(self) -> str:
|
|
263
|
+
"""Return the https location of the repository the release came from."""
|
|
264
|
+
return GitSourceUrl(DEFAULT_SOURCE_URL).browse_url
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def browse_ref(self) -> str:
|
|
268
|
+
"""Return the release tag whose files match the pinned Cloudfall."""
|
|
269
|
+
return f"v{self.version}"
|
|
270
|
+
|
|
271
|
+
def as_dict(self) -> dict[str, object]:
|
|
272
|
+
"""Serialize the pin for system boundaries."""
|
|
273
|
+
return {"kind": "index", "version": str(self.version)}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
type ProjectPin = GitPin | IndexPin
|
|
277
|
+
"""What a project pins Cloudfall to: a released version, or a commit."""
|
|
278
|
+
|
|
279
|
+
|
|
172
280
|
@dataclass(frozen=True, slots=True)
|
|
173
281
|
class ProjectName:
|
|
174
282
|
"""Package name of a project, as written into its ``pyproject.toml``."""
|
|
@@ -255,8 +363,7 @@ class InitOptions:
|
|
|
255
363
|
|
|
256
364
|
directory: Path
|
|
257
365
|
name: ProjectName
|
|
258
|
-
|
|
259
|
-
source: GitSourceUrl = GitSourceUrl(DEFAULT_SOURCE_URL)
|
|
366
|
+
pin: ProjectPin
|
|
260
367
|
description: ProjectDescription | None = None
|
|
261
368
|
|
|
262
369
|
|
|
@@ -276,8 +383,7 @@ class ProjectScaffold:
|
|
|
276
383
|
|
|
277
384
|
directory: Path
|
|
278
385
|
name: ProjectName
|
|
279
|
-
|
|
280
|
-
source: GitSourceUrl
|
|
386
|
+
pin: ProjectPin
|
|
281
387
|
files: tuple[str, ...]
|
|
282
388
|
git: GitSetup
|
|
283
389
|
|
|
@@ -288,8 +394,7 @@ class ProjectScaffold:
|
|
|
288
394
|
"project": {
|
|
289
395
|
"directory": str(self.directory),
|
|
290
396
|
"name": str(self.name),
|
|
291
|
-
"
|
|
292
|
-
"source": str(self.source),
|
|
397
|
+
"pin": self.pin.as_dict(),
|
|
293
398
|
"git": self.git.value,
|
|
294
399
|
},
|
|
295
400
|
"files": list(self.files),
|
|
@@ -388,33 +493,32 @@ def project_path(value: str) -> Path:
|
|
|
388
493
|
return path
|
|
389
494
|
|
|
390
495
|
|
|
391
|
-
def
|
|
496
|
+
def resolve_installed_pin(
|
|
392
497
|
read_direct_url: Callable[[], str | None] | None = None,
|
|
393
498
|
inspect_checkout: Callable[[Path], CheckoutState] | None = None,
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
the
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
499
|
+
read_version: Callable[[], str] | None = None,
|
|
500
|
+
) -> ProjectPin:
|
|
501
|
+
"""Return what the running Cloudfall pins a new project to.
|
|
502
|
+
|
|
503
|
+
An install from the package index records no origin, and its version is
|
|
504
|
+
the pin: the project depends on ``cloudfall==<version>`` and resolves it
|
|
505
|
+
the ordinary way. A wheel installed from git records its commit in
|
|
506
|
+
``direct_url.json``; an editable source checkout records the checkout
|
|
507
|
+
location, whose ``HEAD`` is the commit, provided the checkout is clean
|
|
508
|
+
(else ``HEAD`` is not the code that is running) and ``HEAD`` is on a
|
|
509
|
+
remote branch (else ``uv sync`` cannot fetch it). Anything else cannot be
|
|
510
|
+
pinned and must be given explicitly.
|
|
403
511
|
"""
|
|
404
512
|
direct_url = (read_direct_url or _installed_direct_url)()
|
|
405
513
|
if direct_url is None:
|
|
406
|
-
|
|
407
|
-
"the installed cloudfall distribution records no origin; "
|
|
408
|
-
"pass --rev with the commit to pin"
|
|
409
|
-
)
|
|
410
|
-
raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
|
|
514
|
+
return IndexPin(_installed_release_version(read_version))
|
|
411
515
|
record = json.loads(direct_url)
|
|
412
516
|
if not isinstance(record, dict):
|
|
413
517
|
message = f"direct_url.json is not an object: {direct_url!r}"
|
|
414
518
|
raise ProjectError(_ERROR_REVISION_UNRESOLVED, message)
|
|
415
519
|
vcs_info = record.get("vcs_info")
|
|
416
520
|
if isinstance(vcs_info, dict) and "commit_id" in vcs_info:
|
|
417
|
-
return GitRevision.from_boundary(vcs_info["commit_id"])
|
|
521
|
+
return GitPin(GitRevision.from_boundary(vcs_info["commit_id"]))
|
|
418
522
|
dir_info = record.get("dir_info")
|
|
419
523
|
url = record.get("url")
|
|
420
524
|
if (
|
|
@@ -424,7 +528,7 @@ def resolve_installed_revision(
|
|
|
424
528
|
):
|
|
425
529
|
checkout = Path(urlparse(url).path)
|
|
426
530
|
inspect = inspect_checkout or _inspect_checkout
|
|
427
|
-
return _pinnable_head(checkout, inspect(checkout))
|
|
531
|
+
return GitPin(_pinnable_head(checkout, inspect(checkout)))
|
|
428
532
|
message = (
|
|
429
533
|
"the installed cloudfall distribution is neither a git install nor a "
|
|
430
534
|
f"source checkout ({url!r}); pass --rev with the commit to pin"
|
|
@@ -449,6 +553,25 @@ def _pinnable_head(checkout: Path, state: CheckoutState) -> GitRevision:
|
|
|
449
553
|
return head
|
|
450
554
|
|
|
451
555
|
|
|
556
|
+
def _installed_release_version(
|
|
557
|
+
read_version: Callable[[], str] | None = None,
|
|
558
|
+
) -> ReleaseVersion:
|
|
559
|
+
raw = (read_version or _installed_version)()
|
|
560
|
+
try:
|
|
561
|
+
return ReleaseVersion.from_boundary(raw)
|
|
562
|
+
except ValueError as error:
|
|
563
|
+
message = (
|
|
564
|
+
"the installed cloudfall distribution records no origin and its "
|
|
565
|
+
f"version {raw!r} is not a release, so there is nothing to pin; "
|
|
566
|
+
"pass --rev with the commit to pin"
|
|
567
|
+
)
|
|
568
|
+
raise ProjectError(_ERROR_VERSION_UNRESOLVED, message) from error
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _installed_version() -> str:
|
|
572
|
+
return metadata.version("cloudfall")
|
|
573
|
+
|
|
574
|
+
|
|
452
575
|
def _installed_direct_url() -> str | None:
|
|
453
576
|
return metadata.distribution("cloudfall").read_text("direct_url.json")
|
|
454
577
|
|
|
@@ -543,8 +666,7 @@ def init_project(
|
|
|
543
666
|
return ProjectScaffold(
|
|
544
667
|
directory=directory,
|
|
545
668
|
name=options.name,
|
|
546
|
-
|
|
547
|
-
source=options.source,
|
|
669
|
+
pin=options.pin,
|
|
548
670
|
files=tuple(files),
|
|
549
671
|
git=git,
|
|
550
672
|
)
|
|
@@ -561,18 +683,15 @@ name = "{options.name}"
|
|
|
561
683
|
version = "0"
|
|
562
684
|
description = "{description}"
|
|
563
685
|
requires-python = ">=3.14"
|
|
564
|
-
|
|
686
|
+
# Cloudfall is an installed package: the schema catalog and the Ansible engine
|
|
687
|
+
# ship inside the wheel. Bump this pin to move the project to a newer
|
|
688
|
+
# Cloudfall, then run `uv sync`. Add the `mcp` extra (`cloudfall[mcp]`) to run
|
|
689
|
+
# the `cloudfall-mcp` agent server.
|
|
690
|
+
dependencies = ["{options.pin.requirement}"]
|
|
565
691
|
|
|
566
692
|
[tool.uv]
|
|
567
693
|
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
|
-
"""
|
|
694
|
+
{options.pin.sources_table}"""
|
|
576
695
|
|
|
577
696
|
|
|
578
697
|
_GITIGNORE = f"""# Runtime state: evidence, receipts, rendered inventory, built
|
|
@@ -603,10 +722,10 @@ creation_rules:
|
|
|
603
722
|
def _readme(options: InitOptions, git: GitSetup) -> str:
|
|
604
723
|
tmp = RUNTIME_DIRECTORY
|
|
605
724
|
secrets = SECRETS_DIRECTORY
|
|
606
|
-
browse = options.
|
|
607
|
-
|
|
608
|
-
secrets_guide = f"{browse}/blob/{
|
|
609
|
-
examples = f"{browse}/tree/{
|
|
725
|
+
browse = options.pin.browse_url
|
|
726
|
+
ref = options.pin.browse_ref
|
|
727
|
+
secrets_guide = f"{browse}/blob/{ref}/{SECRETS_GUIDE_PATH}"
|
|
728
|
+
examples = f"{browse}/tree/{ref}/{EXAMPLES_PATH}"
|
|
610
729
|
about = (
|
|
611
730
|
str(options.description)
|
|
612
731
|
if options.description is not None
|
|
@@ -8,7 +8,13 @@ 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
|
+
GitPin,
|
|
13
|
+
GitRevision,
|
|
14
|
+
InitOptions,
|
|
15
|
+
ProjectName,
|
|
16
|
+
init_project,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
ROOT = Path(__file__).parents[2]
|
|
14
20
|
SCHEMAS = ROOT / "config" / "schemas" / "v1"
|
|
@@ -141,7 +147,7 @@ def _fresh_project(tmp_path: Path) -> AgentToolset:
|
|
|
141
147
|
InitOptions(
|
|
142
148
|
directory=directory,
|
|
143
149
|
name=ProjectName("project"),
|
|
144
|
-
|
|
150
|
+
pin=GitPin(GitRevision(REVISION)),
|
|
145
151
|
)
|
|
146
152
|
)
|
|
147
153
|
return AgentToolset(_config(tmp_path, project_directory=directory))
|
|
@@ -17,7 +17,13 @@ from cloudfall.authoring import (
|
|
|
17
17
|
)
|
|
18
18
|
from cloudfall.cli import main
|
|
19
19
|
from cloudfall.domain import ConnectionAddress, LinuxUser, ResourceId, TcpPort
|
|
20
|
-
from cloudfall.project import
|
|
20
|
+
from cloudfall.project import (
|
|
21
|
+
GitPin,
|
|
22
|
+
GitRevision,
|
|
23
|
+
InitOptions,
|
|
24
|
+
ProjectName,
|
|
25
|
+
init_project,
|
|
26
|
+
)
|
|
21
27
|
from cloudfall.validation import ConfigValidationError, validate_config
|
|
22
28
|
|
|
23
29
|
ROOT = Path(__file__).parents[2]
|
|
@@ -46,7 +52,7 @@ def _project(tmp_path: Path) -> Path:
|
|
|
46
52
|
InitOptions(
|
|
47
53
|
directory=directory,
|
|
48
54
|
name=ProjectName("project"),
|
|
49
|
-
|
|
55
|
+
pin=GitPin(GitRevision(REVISION)),
|
|
50
56
|
)
|
|
51
57
|
)
|
|
52
58
|
return directory
|