cloudfall 0.8.0rc4__tar.gz → 0.8.0rc5__tar.gz

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