fadenstack 0.6.0rc4__tar.gz → 0.7.0rc2__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 (196) hide show
  1. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/PKG-INFO +1 -1
  2. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/pyproject.toml +1 -1
  3. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/__init__.py +1 -1
  4. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/docker-compose.yaml +4 -0
  5. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/deploy.py +3 -2
  6. fadenstack-0.7.0rc2/src/faden_cli/commands/restore.py +428 -0
  7. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/up.py +50 -4
  8. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/backup.py +323 -89
  9. fadenstack-0.7.0rc2/src/faden_cli/core/release_feed.py +755 -0
  10. fadenstack-0.7.0rc2/src/faden_cli/core/restore_swap.py +406 -0
  11. fadenstack-0.7.0rc2/tests/test_app_catalogue.py +308 -0
  12. fadenstack-0.7.0rc2/tests/test_backup_release.py +52 -0
  13. fadenstack-0.7.0rc2/tests/test_gateway_dns_cache.py +25 -0
  14. fadenstack-0.7.0rc2/tests/test_image_labels.py +121 -0
  15. fadenstack-0.7.0rc2/tests/test_restore_beside.py +530 -0
  16. fadenstack-0.7.0rc2/tests/test_restore_maintenance.py +111 -0
  17. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_restore_rabbitmq.py +37 -14
  18. fadenstack-0.7.0rc2/tests/test_up_waits.py +76 -0
  19. fadenstack-0.7.0rc2/tests/test_update_feed.py +193 -0
  20. fadenstack-0.6.0rc4/src/faden_cli/commands/restore.py +0 -255
  21. fadenstack-0.6.0rc4/src/faden_cli/core/release_feed.py +0 -348
  22. fadenstack-0.6.0rc4/tests/test_restore_maintenance.py +0 -96
  23. fadenstack-0.6.0rc4/tests/test_update_feed.py +0 -86
  24. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/.gitignore +0 -0
  25. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/LICENSE +0 -0
  26. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/NOTICE +0 -0
  27. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/README.md +0 -0
  28. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/hatch_build.py +0 -0
  29. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/__main__.py +0 -0
  30. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/pii-catalog.json +0 -0
  31. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/.gitignore +0 -0
  32. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/LICENSE +0 -0
  33. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/NOTICE +0 -0
  34. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/README.md +0 -0
  35. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/agent-binaries/.gitignore +0 -0
  36. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/alloy/config.alloy +0 -0
  37. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/clickhouse/config.d/system-logs.xml +0 -0
  38. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/docker-compose.gpu.yaml +0 -0
  39. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/docker-compose.pii-gpu.yaml +0 -0
  40. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/dashboards/templates/vllm-runtime.json +0 -0
  41. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/provisioning/dashboards/dashboard.yml +0 -0
  42. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/provisioning/dashboards/json/faden-overview.json +0 -0
  43. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/provisioning/datasources/ds.yaml +0 -0
  44. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/provisioning/datasources/prometheus.yaml +0 -0
  45. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/initdb/01-init.sql +0 -0
  46. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/loki/local-config.yaml +0 -0
  47. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/locations.conf +0 -0
  48. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/maintenance.html +0 -0
  49. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/nginx-tls.conf +0 -0
  50. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/nginx.conf +0 -0
  51. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/rate-limits.conf +0 -0
  52. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/security-headers.conf +0 -0
  53. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/select-config.sh +0 -0
  54. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/prometheus/prometheus.yml +0 -0
  55. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/rabbitmq/definitions.json.example +0 -0
  56. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/tls/README.md +0 -0
  57. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/cli.py +0 -0
  58. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/__init__.py +0 -0
  59. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/admin.py +0 -0
  60. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/audit.py +0 -0
  61. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/backup.py +0 -0
  62. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/chats.py +0 -0
  63. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/config.py +0 -0
  64. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/__init__.py +0 -0
  65. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_doctor.py +0 -0
  66. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_down.py +0 -0
  67. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_group.py +0 -0
  68. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_init.py +0 -0
  69. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_pull.py +0 -0
  70. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_status.py +0 -0
  71. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_up.py +0 -0
  72. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/doctor.py +0 -0
  73. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/down.py +0 -0
  74. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/logs_cmd.py +0 -0
  75. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/maintenance.py +0 -0
  76. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/module.py +0 -0
  77. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/observe.py +0 -0
  78. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/ports.py +0 -0
  79. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/rebuild.py +0 -0
  80. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/runtime_images.py +0 -0
  81. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/secrets.py +0 -0
  82. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/status.py +0 -0
  83. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/tls.py +0 -0
  84. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/tune.py +0 -0
  85. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/upgrade.py +0 -0
  86. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/version.py +0 -0
  87. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/__init__.py +0 -0
  88. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/api_client.py +0 -0
  89. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/backend_tool.py +0 -0
  90. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/backup_crypto.py +0 -0
  91. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/backup_key.py +0 -0
  92. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/bootstrap.py +0 -0
  93. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/bundle.py +0 -0
  94. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/chat_retention.py +0 -0
  95. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/clickhouse.py +0 -0
  96. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/compose.py +0 -0
  97. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/console.py +0 -0
  98. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/detect.py +0 -0
  99. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/dev_modules.py +0 -0
  100. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/docker_env.py +0 -0
  101. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/edge.py +0 -0
  102. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/env_gen.py +0 -0
  103. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/git.py +0 -0
  104. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/install.py +0 -0
  105. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/installation.py +0 -0
  106. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/local_node.py +0 -0
  107. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/maintenance.py +0 -0
  108. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/pii_artifacts.py +0 -0
  109. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/pii_catalog.py +0 -0
  110. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/pii_hardware.py +0 -0
  111. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/pii_runtime.py +0 -0
  112. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/private_files.py +0 -0
  113. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/provenance_keys.py +0 -0
  114. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/registry.py +0 -0
  115. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/release_notice.py +0 -0
  116. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/rmq.py +0 -0
  117. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/runtime_images.py +0 -0
  118. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/schedule.py +0 -0
  119. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/sealing.py +0 -0
  120. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/secret_files.py +0 -0
  121. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/secret_rotation.py +0 -0
  122. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/settings.py +0 -0
  123. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/sysinfo.py +0 -0
  124. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/tls.py +0 -0
  125. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/tune.py +0 -0
  126. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/update_check.py +0 -0
  127. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/workspace.py +0 -0
  128. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/templates/env.j2 +0 -0
  129. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/tui/__init__.py +0 -0
  130. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/tui/widgets/__init__.py +0 -0
  131. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/tui/wizard/__init__.py +0 -0
  132. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/conftest.py +0 -0
  133. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_encryption.py +0 -0
  134. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_migration_heads.py +0 -0
  135. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_retention.py +0 -0
  136. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_schedule.py +0 -0
  137. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_tls.py +0 -0
  138. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_volumes.py +0 -0
  139. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_binary_notices.py +0 -0
  140. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_bootstrap_token.py +0 -0
  141. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_chat_retention.py +0 -0
  142. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_cli_output_encoding.py +0 -0
  143. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_data_dir_move.py +0 -0
  144. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_data_volumes_logs.py +0 -0
  145. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_deploy_summary.py +0 -0
  146. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_env_loopback.py +0 -0
  147. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_modules.py +0 -0
  148. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_up_port_holders.py +0 -0
  149. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_up_reclaim.py +0 -0
  150. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_up_service_hosts.py +0 -0
  151. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_up_terminal.py +0 -0
  152. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_docker_env.py +0 -0
  153. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_doctor_ports.py +0 -0
  154. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_edge.py +0 -0
  155. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_generated_env_reaches_compose.py +0 -0
  156. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_inactive_modules.py +0 -0
  157. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_install_extensions.py +0 -0
  158. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_installation.py +0 -0
  159. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_internal_token_env.py +0 -0
  160. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_local_node.py +0 -0
  161. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_main.py +0 -0
  162. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_maintenance.py +0 -0
  163. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_module_env.py +0 -0
  164. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_module_secrets.py +0 -0
  165. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_artifacts.py +0 -0
  166. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_catalog.py +0 -0
  167. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_hardware.py +0 -0
  168. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_runtime.py +0 -0
  169. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_upgrade.py +0 -0
  170. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_postgres_logins.py +0 -0
  171. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_private_files.py +0 -0
  172. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_provenance_keys.py +0 -0
  173. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_provenance_module.py +0 -0
  174. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_provenance_on_by_default.py +0 -0
  175. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_rebuild.py +0 -0
  176. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_redis_limits.py +0 -0
  177. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_registry_gateway_env.py +0 -0
  178. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_release_bundle.py +0 -0
  179. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_release_feed.py +0 -0
  180. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_release_notice.py +0 -0
  181. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_release_versions.py +0 -0
  182. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_required_env.py +0 -0
  183. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_runtime_images.py +0 -0
  184. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_secret_files.py +0 -0
  185. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_secret_rotation.py +0 -0
  186. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_service_logins.py +0 -0
  187. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_shared_compose_ports.py +0 -0
  188. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_tls.py +0 -0
  189. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_tls_renew.py +0 -0
  190. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_trusted_keys.py +0 -0
  191. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_tune.py +0 -0
  192. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_tune_memory.py +0 -0
  193. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_update_check.py +0 -0
  194. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_upgrade_housekeeping.py +0 -0
  195. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_upgrade_rabbitmq.py +0 -0
  196. {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_workspace_detection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fadenstack
3
- Version: 0.6.0rc4
3
+ Version: 0.7.0rc2
4
4
  Summary: CLI installer and management tool for the Fadenstack platform
5
5
  Project-URL: Homepage, https://fadenstack.com
6
6
  Project-URL: Repository, https://github.com/fadenstack/fadenstack
@@ -10,7 +10,7 @@ packages = ["src/faden_cli"]
10
10
 
11
11
  [project]
12
12
  name = "fadenstack"
13
- version = "0.6.0rc4"
13
+ version = "0.7.0rc2"
14
14
  requires-python = ">=3.12,<4.0"
15
15
  description = "CLI installer and management tool for the Fadenstack platform"
16
16
  authors = [
@@ -13,4 +13,4 @@ Module activation via Docker Compose profiles.
13
13
  All secrets generated securely during init phase.
14
14
  """
15
15
 
16
- __version__ = "0.6.0rc4"
16
+ __version__ = "0.7.0rc2"
@@ -421,6 +421,10 @@ services:
421
421
  FORWARDED_ALLOW_IPS: "*"
422
422
  FADEN_GATEWAY_RELOAD: "false"
423
423
  FADEN_GATEWAY_ENVIRONMENT: ${FADEN_GATEWAY_ENVIRONMENT:-production}
424
+ # Seconds each worker's LiteLLM keeps a model server's address (aiohttp's DNS cache; LiteLLM reads this
425
+ # name once at start, default 300). A provider container that restarts may get another address: with
426
+ # 300 every chat to it failed for up to five minutes ("The model failed to answer").
427
+ AIOHTTP_TTL_DNS_CACHE: "30"
424
428
  # JWT secret is read from backend DB at startup (faden_gateway.jwt_secret system setting).
425
429
  # These two env vars enable that path; no static JWT_SECRET env var is needed.
426
430
  FADEN_GATEWAY_BACKEND_DB_BASE: faden_backend
@@ -174,8 +174,9 @@ def update_check_step(env_path: Path, cfg: FadenConfig, *, choice: bool | None,
174
174
  console.print("\n[bold cyan]Step 3e: New releases…[/bold cyan]")
175
175
  console.print(
176
176
  f" Once a day the server and this CLI read {FEED_URL},\n"
177
- " the public list of releases. They send nothing about this server: the list is\n"
178
- " compared here, and administrators see in the console when an update is out."
177
+ " the public list of releases, and the server reads the public list of Fadenstack apps\n"
178
+ " too. They send nothing about this server: the lists are compared here, and\n"
179
+ " administrators see in the console when an update is out."
179
180
  )
180
181
  choice = click.confirm(" Check for new releases?", default=True)
181
182
  if env_vars.get("FADEN_UPDATE_CHECK") != ("true" if choice else "false"):
@@ -0,0 +1,428 @@
1
+ """``faden restore`` — restore from a backup directory.
2
+
3
+ Examples:
4
+ faden restore backups/20250115T120000Z
5
+ faden restore backups/20250115T120000Z --verify-only
6
+ faden restore backups/20250115T120000Z --db-only
7
+ faden restore backups/20250115T120000Z --skip-env
8
+ faden restore backups/20250115T120000Z --key-file /media/usb/fadenstack-recovery-key.txt
9
+
10
+ Each database becomes exactly the backup's. While the install keeps running, every database of the backup is
11
+ restored beside the live one (``<db>__restore``); a pg_restore error stops the restore there, with the live
12
+ databases untouched (core/restore_swap.py). Then, behind the maintenance page, the services stop, one
13
+ transaction swaps the names (live → ``<db>__old``, restored → live), the backup's ``.env``, volumes and
14
+ certificate material go in -- what they replace kept first -- and everything starts again. Healthy: the old
15
+ databases are dropped, "Restore complete". Not healthy: everything goes back as it was, and the restore exits 1.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import shutil
21
+ import sys
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+
25
+ import click
26
+
27
+ from faden_cli.core import backup_crypto, backup_key, maintenance, restore_swap
28
+ from faden_cli.core.backup import (
29
+ FilesNotKept,
30
+ Kept,
31
+ Prepared,
32
+ RestoreResult,
33
+ describe_release,
34
+ keep_before_restore,
35
+ prepare_restore,
36
+ put_back,
37
+ read_manifest,
38
+ restore_env,
39
+ restore_log_path,
40
+ restore_stamp,
41
+ restore_tls,
42
+ restore_volumes,
43
+ verify_backup,
44
+ )
45
+ from faden_cli.core.backup import data_dir_of
46
+ from faden_cli.core.compose import (
47
+ ComposeContext,
48
+ build_context_from_config,
49
+ ps as compose_ps,
50
+ restart as compose_restart,
51
+ service_names,
52
+ stop as compose_stop,
53
+ up as compose_up,
54
+ )
55
+ from faden_cli.core.console import console, error, info, success, warning
56
+ from faden_cli.core.private_files import open_private
57
+ from faden_cli.core.settings import FadenConfig, load_config
58
+ from faden_cli.commands.deploy import _regenerate_rmq_definitions, _sync_postgres_password
59
+
60
+
61
+ @click.command("restore")
62
+ @click.argument("backup_dir", type=click.Path(exists=True, file_okay=False))
63
+ @click.option("--db-only", is_flag=True, default=False, help="Restore databases only.")
64
+ @click.option("--skip-env", is_flag=True, default=False, help="Don't overwrite the current .env.")
65
+ @click.option(
66
+ "--verify-only",
67
+ is_flag=True,
68
+ default=False,
69
+ help="Validate checksums without restoring.",
70
+ )
71
+ @click.option(
72
+ "--key-file",
73
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
74
+ default=None,
75
+ help="The recovery key, for an encrypted backup; otherwise it is asked for.",
76
+ )
77
+ @click.option("-y", "--yes", is_flag=True, default=False, help="Skip confirmation prompt.")
78
+ def restore_cmd(
79
+ backup_dir: str,
80
+ *,
81
+ db_only: bool,
82
+ skip_env: bool,
83
+ verify_only: bool,
84
+ key_file: Path | None,
85
+ yes: bool,
86
+ ) -> None:
87
+ """Restore Fadenstack from a backup directory.
88
+
89
+ BACKUP_DIR is the path to a timestamped backup folder containing
90
+ a manifest.json file created by ``faden backup``. An encrypted backup
91
+ is opened with the recovery key first.
92
+ """
93
+ console.print("\n[bold magenta]fadenstack — Restore[/bold magenta]\n")
94
+
95
+ backup_path = Path(backup_dir).resolve()
96
+ if not backup_crypto.is_encrypted(backup_path):
97
+ _restore_cmd(backup_path, backup_path, db_only=db_only, skip_env=skip_env, verify_only=verify_only, yes=yes)
98
+ return
99
+ opened = _open(backup_path, key_file)
100
+ try:
101
+ _restore_cmd(backup_path, opened, db_only=db_only, skip_env=skip_env, verify_only=verify_only, yes=yes)
102
+ finally:
103
+ backup_crypto.close_backup(backup_path)
104
+
105
+
106
+ #: Recovery keys typed wrong before the restore stops.
107
+ _KEY_ATTEMPTS = 3
108
+
109
+
110
+ def _open(backup_path: Path, key_file: Path | None) -> Path:
111
+ """Decrypt the encrypted backup with the recovery key: from --key-file, still on the server, or asked."""
112
+ from faden_cli.commands.backup import key_text # noqa: PLC0415
113
+
114
+ index = backup_crypto.read_index(backup_path) or {}
115
+ info(f"This backup is encrypted to the backup key {index.get('key_id') or 'unknown'}.")
116
+ cfg = load_config()
117
+ left = backup_key.pending(cfg.install_path) if cfg.install_dir else None
118
+ given = key_file or left
119
+ if left is not None and key_file is None:
120
+ info(f"Using the recovery key still on this server ({left}).")
121
+ for attempt in range(1, _KEY_ATTEMPTS + 1):
122
+ text = key_text(given.read_text(encoding="utf-8")) if given else click.prompt("Recovery key", hide_input=True)
123
+ try:
124
+ key = backup_crypto.RecoveryKey.from_text(text)
125
+ info("Opening the backup…")
126
+ return backup_crypto.open_backup(backup_path, key)
127
+ except (backup_crypto.KeyTextError, backup_crypto.OpenError) as exc:
128
+ error(f"The backup does not open with that key: {exc}")
129
+ if given or attempt == _KEY_ATTEMPTS:
130
+ sys.exit(1)
131
+ sys.exit(1)
132
+
133
+
134
+ def _restore_cmd(
135
+ backup_path: Path, source: Path, *, db_only: bool, skip_env: bool, verify_only: bool, yes: bool,
136
+ ) -> None:
137
+ """Check the backup in *source* (the backup itself, or its decrypted copy) and restore it."""
138
+ manifest = read_manifest(source)
139
+ if manifest is None:
140
+ error(f"No manifest.json found in {source}")
141
+ sys.exit(1)
142
+
143
+ # ── Show backup info ──────────────────────────────────────
144
+ console.print(f" Backup date: {manifest.get('created_at', 'unknown')}")
145
+ console.print(f" Release: {describe_release(manifest)}")
146
+ db_names = list(manifest.get("databases", {}).keys())
147
+ console.print(f" Databases: {', '.join(db_names)}")
148
+ if manifest.get("env_snapshot"):
149
+ console.print(f" .env snapshot: {manifest['env_snapshot']}")
150
+ if manifest.get("volumes"):
151
+ console.print(f" Volumes: {', '.join(manifest['volumes'])}")
152
+ console.print()
153
+
154
+ # ── Verify checksums ──────────────────────────────────────
155
+ info("Verifying backup checksums…")
156
+ check_errors = verify_backup(source, manifest)
157
+ if check_errors:
158
+ for msg in check_errors:
159
+ error(f" {msg}")
160
+ sys.exit(1)
161
+ success("Checksums OK.")
162
+
163
+ if verify_only:
164
+ return
165
+
166
+ # ── Confirm ───────────────────────────────────────────────
167
+ if not yes:
168
+ console.print()
169
+ warning("This replaces every database the backup holds with the backup's, exactly.")
170
+ click.confirm("Proceed with restore?", default=False, abort=True)
171
+
172
+ cfg = load_config()
173
+ ctx = build_context_from_config(cfg)
174
+ install = Path(str(ctx.project_dir)) if ctx.project_dir else Path.cwd()
175
+ stamp = restore_stamp()
176
+ log_path = restore_log_path(install, stamp)
177
+ info(f"pg_restore's whole output goes to {log_path}")
178
+
179
+ # ── 1. Beside the running install: nothing it runs on changes ──
180
+ console.print("\n[bold cyan]Restoring the databases beside the live ones…[/bold cyan]")
181
+ compose_up(ctx, services=["postgres"], detach=True, wait=True, timeout=60)
182
+ with open_private(log_path) as log:
183
+ try:
184
+ prepared = prepare_restore(ctx, source, manifest, log, db_only=db_only, work_parent=install)
185
+ except restore_swap.RestoreRefused as exc:
186
+ error(str(exc))
187
+ sys.exit(1)
188
+ except (restore_swap.RestoreFailed, restore_swap.PostgresError) as exc:
189
+ error(f"The restore failed: {exc}.")
190
+ error(f" Nothing was changed: the live databases and the services are as they were. "
191
+ f"pg_restore's whole output: {log_path}")
192
+ sys.exit(1)
193
+ success(f"Restored beside the live databases: {', '.join(prepared.names)}.")
194
+ if prepared.not_in_backup:
195
+ info(f"Not in the backup, left as they are: {', '.join(prepared.not_in_backup)}.")
196
+
197
+ # ── 2. Swap in, behind the maintenance page ───────────────
198
+ # The front door stays up and tells users the platform is being worked on, instead of refusing connections
199
+ # until the end. Lifted however this ends. nginx holds none of the volumes a restore writes.
200
+ with maintenance.during(install, reason="restore"):
201
+ outcome = _swap_in(ctx, cfg, source, manifest, prepared, install / f".restore-{stamp}",
202
+ db_only=db_only, skip_env=skip_env)
203
+
204
+ console.print()
205
+ if not outcome.ok:
206
+ for line in outcome.messages:
207
+ error(line)
208
+ error(f" pg_restore's whole output: {log_path}")
209
+ sys.exit(1)
210
+ from faden_cli.core.backend_tool import record_event # noqa: PLC0415
211
+
212
+ result = outcome.result
213
+ record_event(
214
+ "backup.restore", "install", Path(str(backup_path)).name,
215
+ metadata={
216
+ "databases": list(result.databases_restored or []), "db_only": db_only,
217
+ "encrypted": source != backup_path,
218
+ },
219
+ )
220
+ if outcome.left:
221
+ error("The backup is restored and every service is healthy, but these old databases could not be dropped:")
222
+ for line in outcome.left:
223
+ error(f" {line}")
224
+ error(f" Drop them with: docker exec {restore_swap.container()} psql -U postgres "
225
+ + " ".join(f'-c "DROP DATABASE IF EXISTS {restore_swap.quote(name.split(" ", 1)[0])};"'
226
+ for name in outcome.left))
227
+ sys.exit(1)
228
+ success("Restore complete.")
229
+ console.print(f" Databases: {', '.join(result.databases_restored)} (exactly the backup's; the ones before dropped)")
230
+ if result.env_restored:
231
+ console.print(" .env: restored")
232
+ if result.volumes_restored:
233
+ console.print(f" Volumes: {', '.join(result.volumes_restored)}")
234
+ if result.tls_restored:
235
+ console.print(f" Certificate material: {len(result.tls_restored)} file(s); the machines still trust it")
236
+ console.print(f" Log: {log_path}")
237
+
238
+
239
+ #: The front door, which keeps running (and serving the maintenance page), and the database server, which
240
+ #: renames its databases: neither is stopped for the swap.
241
+ _FRONT = "nginx"
242
+ _POSTGRES = "postgres"
243
+ #: How long the services may take to become healthy after the swap, as for an upgrade.
244
+ _HEALTHY_WITHIN = 180
245
+
246
+
247
+ @dataclass
248
+ class Outcome:
249
+ """How phase 2 ended: *ok* when the restored install is healthy; *left* the old databases that would not drop;
250
+ *messages* what to tell the operator when it is not ok."""
251
+
252
+ ok: bool
253
+ result: RestoreResult = field(default_factory=RestoreResult)
254
+ left: list[str] = field(default_factory=list)
255
+ messages: list[str] = field(default_factory=list)
256
+
257
+
258
+ def _stop_services(ctx: ComposeContext, names: list[str]) -> int:
259
+ """Stop everything but nginx and Postgres; the containers keep their volumes and networks."""
260
+ running = [name for name in names if name not in (_FRONT, _POSTGRES)]
261
+ return compose_stop(ctx, services=running) if running else 0
262
+
263
+
264
+ def _not_healthy(ctx: ComposeContext) -> list[str]:
265
+ """The services that are not running and healthy (one-off migrators that ended with 0 aside)."""
266
+ found: list[str] = []
267
+ for row in compose_ps(ctx):
268
+ state, health = (row.state or row.status or "").lower(), (row.health or "").lower()
269
+ if state == "exited" and row.exit_code == 0:
270
+ continue
271
+ if state != "running" or health in ("unhealthy", "starting"):
272
+ found.append(f"{row.service or row.name} ({health or state})")
273
+ return found
274
+
275
+
276
+ def _after_env(ctx: ComposeContext, cfg: FadenConfig, env_path: Path) -> None:
277
+ """What follows a new .env: Postgres' password and RabbitMQ's users written from it (both read .env)."""
278
+ from faden_cli.core.env_gen import read_env_file # noqa: PLC0415
279
+
280
+ info("Syncing Postgres password with the .env…")
281
+ _sync_postgres_password(ctx, env_path)
282
+ # RabbitMQ makes its users from definitions.json on every start; the services log in with the passwords in
283
+ # .env. The file still held those of the .env the restore replaced: RabbitMQ refused the backend and the
284
+ # gateway (ACCESS_REFUSED), and the install did not come back. `faden upgrade` writes it the same way.
285
+ _regenerate_rmq_definitions(Path(str(ctx.project_dir)), read_env_file(env_path), cfg.profiles)
286
+
287
+
288
+ def _swap_in(
289
+ ctx: ComposeContext,
290
+ cfg: FadenConfig,
291
+ source: Path,
292
+ manifest: dict,
293
+ prepared: Prepared,
294
+ work_dir: Path,
295
+ *,
296
+ db_only: bool,
297
+ skip_env: bool,
298
+ ) -> Outcome:
299
+ """Phase 2: stop, swap, put the backup's files in, start; healthy → drop the old databases, else go back."""
300
+ console.print("\n[bold cyan]Stopping application services…[/bold cyan]")
301
+ names = service_names(ctx)
302
+ copies = [name + restore_swap.RESTORE_SUFFIX for name in prepared.names]
303
+ if _stop_services(ctx, names) != 0:
304
+ left = restore_swap.drop(copies)
305
+ started = compose_up(ctx, detach=True, wait=True, timeout=_HEALTHY_WITHIN)
306
+ return Outcome(False, messages=[
307
+ "The services could not be stopped, so nothing was swapped: the live databases are as they were.",
308
+ *([f" These copies could not be dropped: {'; '.join(left)}"] if left else []),
309
+ " The services were started again" + ("." if started == 0 else f", not all of them (exit {started})."),
310
+ ])
311
+ compose_up(ctx, services=[_POSTGRES], detach=True, wait=True, timeout=60)
312
+
313
+ console.print("\n[bold cyan]Swapping the restored databases in…[/bold cyan]")
314
+ try:
315
+ swapped = restore_swap.swap_in(prepared.names, prepared.live)
316
+ except restore_swap.SwapFailed as exc:
317
+ left = restore_swap.drop(copies)
318
+ started = compose_up(ctx, detach=True, wait=True, timeout=_HEALTHY_WITHIN)
319
+ return Outcome(False, messages=[
320
+ f"The databases could not be swapped ({exc}); nothing was renamed, the live databases are as they were.",
321
+ *([f" These copies could not be dropped: {'; '.join(left)}"] if left else []),
322
+ " The services were started again" + ("." if started == 0 else f", not all of them (exit {started})."),
323
+ ])
324
+ success(f"Swapped in: {', '.join(prepared.names)} (the ones before kept as <name>{restore_swap.OLD_SUFFIX}).")
325
+ result = RestoreResult(databases_restored=list(prepared.names))
326
+
327
+ try:
328
+ kept = keep_before_restore(ctx, manifest, work_dir, db_only=db_only, skip_env=skip_env,
329
+ volumes=prepared.volumes)
330
+ except (FilesNotKept, OSError) as exc:
331
+ shutil.rmtree(work_dir, ignore_errors=True) # partial copies; nothing of the install was replaced yet
332
+ return _go_back(ctx, cfg, names, swapped, None, result, f"What the restore replaces could not be kept: {exc}")
333
+
334
+ # The backup's files: .env, the volumes, the certificate material.
335
+ if kept.env_path is not None:
336
+ console.print("\n[bold cyan]Restoring .env…[/bold cyan]")
337
+ result.env_restored = restore_env(source, kept.env_path)
338
+ failed: list[str] = []
339
+ if not db_only and manifest.get("volumes"):
340
+ console.print("\n[bold cyan]Restoring Docker volumes…[/bold cyan]")
341
+ result.volumes_restored = restore_volumes(source, manifest, data_dir=data_dir_of(ctx.env_file), ctx=ctx,
342
+ failed=failed)
343
+ if not db_only and manifest.get("tls_material") and ctx.project_dir:
344
+ console.print("\n[bold cyan]Restoring the certificate material…[/bold cyan]")
345
+ result.tls_restored = restore_tls(source, Path(str(ctx.project_dir)), list(manifest["tls_material"]))
346
+ if kept.env_path is not None and not result.env_restored:
347
+ failed.append(".env")
348
+ if failed:
349
+ return _go_back(ctx, cfg, names, swapped, kept, result, f"Not restored: {', '.join(failed)}")
350
+
351
+ if result.env_restored:
352
+ env_path = kept.env_path or Path(str(ctx.env_file))
353
+ # A .env backed up before a release required it may lack a key the compose file now requires.
354
+ from faden_cli.core.env_gen import fill_required_env # noqa: PLC0415
355
+
356
+ for note in fill_required_env(env_path, profiles=list(cfg.profiles)):
357
+ warning(note)
358
+ _after_env(ctx, cfg, env_path)
359
+ success("RabbitMQ users written from the restored .env.")
360
+ # The provenance signer of a .env from before the module, or issued by an authority the restore replaced.
361
+ if (result.env_restored or result.tls_restored) and ctx.env_file and ctx.project_dir:
362
+ from faden_cli.core.provenance_keys import ensure_step as provenance_keys_step # noqa: PLC0415
363
+
364
+ provenance_keys_step(Path(ctx.project_dir), Path(str(ctx.env_file)))
365
+
366
+ console.print("\n[bold cyan]Starting all services…[/bold cyan]")
367
+ started = compose_up(ctx, detach=True, wait=True, timeout=_HEALTHY_WITHIN)
368
+ if started != 0:
369
+ sick = _not_healthy(ctx)
370
+ why = f"The restored install did not become healthy (docker compose up exited with {started})"
371
+ return _go_back(ctx, cfg, names, swapped, kept, result, why + (f": {', '.join(sick)}" if sick else ""))
372
+ # nginx read the certificate and picked plain HTTP or HTTPS when it started; a restored one is only served
373
+ # after it starts again.
374
+ if result.tls_restored and _FRONT in names:
375
+ compose_restart(ctx, services=[_FRONT])
376
+ left = restore_swap.drop_old(swapped)
377
+ kept.discard()
378
+ return Outcome(True, result=result, left=left)
379
+
380
+
381
+ def _go_back(
382
+ ctx: ComposeContext,
383
+ cfg: FadenConfig,
384
+ names: list[str],
385
+ swapped: list[restore_swap.Swapped],
386
+ kept: Kept | None,
387
+ result: RestoreResult,
388
+ why: str,
389
+ ) -> Outcome:
390
+ """Put everything back as before the restore and start it; says exactly what did not go back."""
391
+ console.print(f"\n[bold red]{why}.[/bold red] [bold cyan]Putting everything back as it was…[/bold cyan]")
392
+ problems: list[str] = []
393
+ if _stop_services(ctx, names) != 0:
394
+ problems.append("the services could not all be stopped first")
395
+ compose_up(ctx, services=[_POSTGRES], detach=True, wait=True, timeout=60)
396
+ try:
397
+ left = restore_swap.swap_back(swapped)
398
+ problems.extend(f"the restored copy {line} could not be dropped" for line in left)
399
+ except restore_swap.SwapFailed as exc:
400
+ problems.append(
401
+ f"the databases: still the backup's ({exc}); the ones before are "
402
+ + ", ".join(s.name + restore_swap.OLD_SUFFIX for s in swapped if s.had_live)
403
+ )
404
+ if kept is not None:
405
+ problems.extend(put_back(kept, ctx))
406
+ if kept.env_path is not None and kept.env is not None:
407
+ _after_env(ctx, cfg, kept.env_path)
408
+ started = compose_up(ctx, detach=True, wait=True, timeout=_HEALTHY_WITHIN)
409
+ if result.tls_restored and _FRONT in names:
410
+ compose_restart(ctx, services=[_FRONT])
411
+ messages = [f"{why}."]
412
+ if problems:
413
+ messages.append("Not everything went back as it was before the restore:")
414
+ messages.extend(f" - {line}" for line in problems)
415
+ if kept is not None:
416
+ messages.append(f" What the restore replaced is kept in {kept.work_dir}.")
417
+ else:
418
+ what = "the databases" + (", .env, the volumes and the certificate material" if kept is not None else "")
419
+ messages.append(f"Everything the restore changed went back: {what} are as they were before the restore.")
420
+ if kept is not None:
421
+ kept.discard()
422
+ messages.append(
423
+ "The install as it was before the restore is running again."
424
+ if started == 0
425
+ else f"The install as it was before the restore did not start either (docker compose up exited with "
426
+ f"{started}): see `faden status`, and why with `faden logs <service>`."
427
+ )
428
+ return Outcome(False, result=result, messages=messages)
@@ -6,17 +6,50 @@ import sys
6
6
 
7
7
  import click
8
8
 
9
- from faden_cli.core.compose import build_context_from_config, report_inactive_modules, up as compose_up
9
+ from faden_cli.core.compose import (
10
+ ComposeContext,
11
+ build_context_from_config,
12
+ ps as compose_ps,
13
+ report_inactive_modules,
14
+ up as compose_up,
15
+ )
10
16
  from faden_cli.core.console import console, success, error
11
17
  from faden_cli.core.env_gen import write_env_file, default_env_vars
12
18
  from faden_cli.core.settings import load_config
13
19
 
20
+ #: How long `faden up` waits for the services to be healthy: as `faden upgrade` waits after starting them.
21
+ WAIT_SECONDS = 180
22
+
23
+
24
+ def _not_healthy(ctx: ComposeContext) -> list[str]:
25
+ """The services not running and healthy (one-off migrators that ended with 0 aside), as `faden restore` reads them."""
26
+ found: list[str] = []
27
+ for row in compose_ps(ctx):
28
+ state, health = (row.state or row.status or "").lower(), (row.health or "").lower()
29
+ if state == "exited" and row.exit_code == 0:
30
+ continue
31
+ if state != "running" or health in ("unhealthy", "starting"):
32
+ found.append(f"{row.service or row.name} ({health or state})")
33
+ return found
34
+
14
35
 
15
36
  @click.command("up")
16
37
  @click.option("-d", "--detach/--no-detach", default=True, help="Run in detached mode (default).")
17
38
  @click.option("--build", "do_build", is_flag=True, help="Build images before starting.")
18
39
  @click.option("--pull/--no-pull", default=False, help="Pull images before starting.")
19
40
  @click.option("--env-gen/--no-env-gen", "gen_env", default=False, help="Regenerate .env file before starting.")
41
+ @click.option(
42
+ "--wait/--no-wait",
43
+ default=True,
44
+ help="Return once the services are running and healthy (default), as `faden upgrade` and `faden restore` do.",
45
+ )
46
+ @click.option(
47
+ "--wait-timeout",
48
+ type=click.IntRange(min=10),
49
+ default=WAIT_SECONDS,
50
+ show_default=True,
51
+ help="Seconds to wait for the services to be healthy.",
52
+ )
20
53
  @click.argument("services", nargs=-1)
21
54
  def up_cmd(
22
55
  *,
@@ -24,11 +57,16 @@ def up_cmd(
24
57
  do_build: bool,
25
58
  pull: bool,
26
59
  gen_env: bool,
60
+ wait: bool,
61
+ wait_timeout: int,
27
62
  services: tuple[str, ...],
28
63
  ) -> None:
29
- """Start Fadenstack infrastructure and services.
64
+ """Start Fadenstack infrastructure and services, and wait until they are healthy.
30
65
 
31
66
  Optionally pass SERVICE names to start only specific containers.
67
+
68
+ It waits because a service that has just started may not answer yet: a module just switched on
69
+ (`faden module enable pii`) loads its model first, and a chat sent in that time was refused.
32
70
  """
33
71
  cfg = load_config()
34
72
 
@@ -61,19 +99,27 @@ def up_cmd(
61
99
 
62
100
  keep_company_files(ctx.env_file, ctx)
63
101
 
102
+ waiting = wait and detach # compose waits for detached services only
64
103
  returncode = compose_up(
65
104
  ctx,
66
105
  detach=detach,
67
106
  build=do_build,
68
- pull=pull,
107
+ pull="always" if pull else "",
69
108
  services=list(services) if services else None,
109
+ wait=waiting,
110
+ timeout=wait_timeout if waiting else 0,
70
111
  )
71
112
 
72
113
  if returncode == 0:
73
- success("Services started successfully.")
114
+ success("Services started and healthy." if waiting else "Services started successfully.")
74
115
  # Only when everything was started: naming services is a partial start.
75
116
  if not services:
76
117
  report_inactive_modules(ctx)
77
118
  else:
119
+ if waiting and (sick := _not_healthy(ctx)):
120
+ error(
121
+ f"Not running and healthy within {wait_timeout} s: {', '.join(sick)}. `faden status` shows the "
122
+ "services, `faden logs <service>` why."
123
+ )
78
124
  error(f"docker compose up exited with code {returncode}.")
79
125
  sys.exit(returncode)