fadenstack 0.5.0rc4__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/PKG-INFO +1 -1
  2. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/pyproject.toml +1 -1
  3. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/__init__.py +1 -1
  4. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/docker-compose.yaml +6 -1
  5. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/nginx/rate-limits.conf +12 -3
  6. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/deploy.py +5 -0
  7. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/restore.py +32 -7
  8. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/upgrade.py +4 -0
  9. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/bundle.py +10 -0
  10. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/compose.py +49 -1
  11. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/registry.py +63 -1
  12. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/rmq.py +8 -1
  13. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/secret_files.py +3 -1
  14. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/conftest.py +20 -2
  15. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_inactive_modules.py +37 -9
  16. fadenstack-0.6.0/tests/test_install_extensions.py +158 -0
  17. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_release_bundle.py +5 -1
  18. fadenstack-0.6.0/tests/test_restore_rabbitmq.py +96 -0
  19. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_tune_memory.py +12 -1
  20. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/.gitignore +0 -0
  21. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/LICENSE +0 -0
  22. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/NOTICE +0 -0
  23. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/README.md +0 -0
  24. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/hatch_build.py +0 -0
  25. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/__main__.py +0 -0
  26. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/pii-catalog.json +0 -0
  27. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/.gitignore +0 -0
  28. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/LICENSE +0 -0
  29. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/NOTICE +0 -0
  30. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/README.md +0 -0
  31. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/agent-binaries/.gitignore +0 -0
  32. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/alloy/config.alloy +0 -0
  33. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/clickhouse/config.d/system-logs.xml +0 -0
  34. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/docker-compose.gpu.yaml +0 -0
  35. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/docker-compose.pii-gpu.yaml +0 -0
  36. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/grafana/dashboards/templates/vllm-runtime.json +0 -0
  37. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/grafana/provisioning/dashboards/dashboard.yml +0 -0
  38. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/grafana/provisioning/dashboards/json/faden-overview.json +0 -0
  39. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/grafana/provisioning/datasources/ds.yaml +0 -0
  40. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/grafana/provisioning/datasources/prometheus.yaml +0 -0
  41. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/initdb/01-init.sql +0 -0
  42. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/loki/local-config.yaml +0 -0
  43. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/nginx/locations.conf +0 -0
  44. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/nginx/maintenance.html +0 -0
  45. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/nginx/nginx-tls.conf +0 -0
  46. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/nginx/nginx.conf +0 -0
  47. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/nginx/security-headers.conf +0 -0
  48. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/nginx/select-config.sh +0 -0
  49. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/prometheus/prometheus.yml +0 -0
  50. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/rabbitmq/definitions.json.example +0 -0
  51. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/bundle/shared/tls/README.md +0 -0
  52. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/cli.py +0 -0
  53. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/__init__.py +0 -0
  54. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/admin.py +0 -0
  55. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/audit.py +0 -0
  56. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/backup.py +0 -0
  57. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/chats.py +0 -0
  58. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/config.py +0 -0
  59. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/dev/__init__.py +0 -0
  60. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/dev/dev_doctor.py +0 -0
  61. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/dev/dev_down.py +0 -0
  62. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/dev/dev_group.py +0 -0
  63. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/dev/dev_init.py +0 -0
  64. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/dev/dev_pull.py +0 -0
  65. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/dev/dev_status.py +0 -0
  66. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/dev/dev_up.py +0 -0
  67. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/doctor.py +0 -0
  68. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/down.py +0 -0
  69. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/logs_cmd.py +0 -0
  70. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/maintenance.py +0 -0
  71. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/module.py +0 -0
  72. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/observe.py +0 -0
  73. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/ports.py +0 -0
  74. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/rebuild.py +0 -0
  75. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/runtime_images.py +0 -0
  76. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/secrets.py +0 -0
  77. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/status.py +0 -0
  78. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/tls.py +0 -0
  79. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/tune.py +0 -0
  80. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/up.py +0 -0
  81. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/commands/version.py +0 -0
  82. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/__init__.py +0 -0
  83. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/api_client.py +0 -0
  84. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/backend_tool.py +0 -0
  85. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/backup.py +0 -0
  86. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/backup_crypto.py +0 -0
  87. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/backup_key.py +0 -0
  88. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/bootstrap.py +0 -0
  89. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/chat_retention.py +0 -0
  90. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/clickhouse.py +0 -0
  91. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/console.py +0 -0
  92. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/detect.py +0 -0
  93. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/dev_modules.py +0 -0
  94. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/docker_env.py +0 -0
  95. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/edge.py +0 -0
  96. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/env_gen.py +0 -0
  97. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/git.py +0 -0
  98. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/install.py +0 -0
  99. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/installation.py +0 -0
  100. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/local_node.py +0 -0
  101. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/maintenance.py +0 -0
  102. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/pii_artifacts.py +0 -0
  103. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/pii_catalog.py +0 -0
  104. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/pii_hardware.py +0 -0
  105. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/pii_runtime.py +0 -0
  106. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/private_files.py +0 -0
  107. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/provenance_keys.py +0 -0
  108. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/release_feed.py +0 -0
  109. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/release_notice.py +0 -0
  110. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/runtime_images.py +0 -0
  111. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/schedule.py +0 -0
  112. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/sealing.py +0 -0
  113. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/secret_rotation.py +0 -0
  114. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/settings.py +0 -0
  115. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/sysinfo.py +0 -0
  116. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/tls.py +0 -0
  117. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/tune.py +0 -0
  118. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/update_check.py +0 -0
  119. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/core/workspace.py +0 -0
  120. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/templates/env.j2 +0 -0
  121. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/tui/__init__.py +0 -0
  122. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/tui/widgets/__init__.py +0 -0
  123. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/src/faden_cli/tui/wizard/__init__.py +0 -0
  124. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_backup_encryption.py +0 -0
  125. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_backup_migration_heads.py +0 -0
  126. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_backup_retention.py +0 -0
  127. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_backup_schedule.py +0 -0
  128. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_backup_tls.py +0 -0
  129. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_backup_volumes.py +0 -0
  130. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_binary_notices.py +0 -0
  131. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_bootstrap_token.py +0 -0
  132. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_chat_retention.py +0 -0
  133. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_cli_output_encoding.py +0 -0
  134. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_data_dir_move.py +0 -0
  135. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_data_volumes_logs.py +0 -0
  136. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_deploy_summary.py +0 -0
  137. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_dev_env_loopback.py +0 -0
  138. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_dev_modules.py +0 -0
  139. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_dev_up_port_holders.py +0 -0
  140. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_dev_up_reclaim.py +0 -0
  141. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_dev_up_service_hosts.py +0 -0
  142. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_dev_up_terminal.py +0 -0
  143. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_docker_env.py +0 -0
  144. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_doctor_ports.py +0 -0
  145. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_edge.py +0 -0
  146. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_generated_env_reaches_compose.py +0 -0
  147. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_installation.py +0 -0
  148. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_internal_token_env.py +0 -0
  149. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_local_node.py +0 -0
  150. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_main.py +0 -0
  151. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_maintenance.py +0 -0
  152. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_module_env.py +0 -0
  153. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_module_secrets.py +0 -0
  154. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_pii_artifacts.py +0 -0
  155. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_pii_catalog.py +0 -0
  156. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_pii_hardware.py +0 -0
  157. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_pii_runtime.py +0 -0
  158. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_pii_upgrade.py +0 -0
  159. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_postgres_logins.py +0 -0
  160. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_private_files.py +0 -0
  161. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_provenance_keys.py +0 -0
  162. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_provenance_module.py +0 -0
  163. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_provenance_on_by_default.py +0 -0
  164. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_rebuild.py +0 -0
  165. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_redis_limits.py +0 -0
  166. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_registry_gateway_env.py +0 -0
  167. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_release_feed.py +0 -0
  168. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_release_notice.py +0 -0
  169. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_release_versions.py +0 -0
  170. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_required_env.py +0 -0
  171. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_restore_maintenance.py +0 -0
  172. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_runtime_images.py +0 -0
  173. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_secret_files.py +0 -0
  174. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_secret_rotation.py +0 -0
  175. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_service_logins.py +0 -0
  176. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_shared_compose_ports.py +0 -0
  177. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_tls.py +0 -0
  178. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_tls_renew.py +0 -0
  179. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_trusted_keys.py +0 -0
  180. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_tune.py +0 -0
  181. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_update_check.py +0 -0
  182. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_update_feed.py +0 -0
  183. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_upgrade_housekeeping.py +0 -0
  184. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_upgrade_rabbitmq.py +0 -0
  185. {fadenstack-0.5.0rc4 → fadenstack-0.6.0}/tests/test_workspace_detection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fadenstack
3
- Version: 0.5.0rc4
3
+ Version: 0.6.0
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.5.0rc4"
13
+ version = "0.6.0"
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.5.0rc4"
16
+ __version__ = "0.6.0"
@@ -340,7 +340,10 @@ services:
340
340
  resources:
341
341
  limits:
342
342
  cpus: "1"
343
- memory: 512M
343
+ # RabbitMQ is told this same amount (rabbitmq/10-faden.conf, written by the CLI's core/rmq.py): it reads
344
+ # the host's memory otherwise and slows publishers only at 60 % of that, long after the kernel has killed
345
+ # it here. 512M was too little at start-up after a reboot (recovering its data, every service starting).
346
+ memory: 1G
344
347
  healthcheck:
345
348
  test: rabbitmq-diagnostics check_running -q
346
349
  interval: 3s
@@ -610,6 +613,7 @@ services:
610
613
  FADEN_BACKEND_SKILLS_ENABLED: ${FADEN_BACKEND_SKILLS_ENABLED:-false}
611
614
  FADEN_BACKEND_MAILER_SERVICE_URL: ${FADEN_BACKEND_MAILER_SERVICE_URL:-http://faden-mailer:8000}
612
615
  FADEN_BACKEND_AUTH_SERVICE_URL: ${FADEN_BACKEND_AUTH_SERVICE_URL:-http://faden-auth:8000}
616
+ FADEN_BACKEND_AUTH_SERVICE_TOKEN: ${FADEN_BACKEND_AUTH_SERVICE_TOKEN:-}
613
617
  FADEN_BACKEND_DOCLING_SERVICE_URL: ${FADEN_BACKEND_DOCLING_SERVICE_URL:-http://faden-docling:8000}
614
618
  FADEN_BACKEND_SKILLS_SERVICE_URL: ${FADEN_BACKEND_SKILLS_SERVICE_URL:-http://faden-skills:8000}
615
619
  FADEN_BACKEND_SKILLS_SERVICE_TOKEN: ${FADEN_BACKEND_SKILLS_SERVICE_TOKEN:?set it in .env, as faden deploy and faden upgrade do (Help, Installing, Secrets in .env)}
@@ -731,6 +735,7 @@ services:
731
735
  FADEN_BACKEND_MCP_SERVICE_TOKEN: ${FADEN_BACKEND_MCP_SERVICE_TOKEN:?set it in .env, as faden deploy and faden upgrade do (Help, Installing, Secrets in .env)}
732
736
  FADEN_BACKEND_MAILER_SERVICE_URL: ${FADEN_BACKEND_MAILER_SERVICE_URL:-http://faden-mailer:8000}
733
737
  FADEN_BACKEND_AUTH_SERVICE_URL: ${FADEN_BACKEND_AUTH_SERVICE_URL:-http://faden-auth:8000}
738
+ FADEN_BACKEND_AUTH_SERVICE_TOKEN: ${FADEN_BACKEND_AUTH_SERVICE_TOKEN:-}
734
739
  FADEN_BACKEND_DOCLING_SERVICE_URL: ${FADEN_BACKEND_DOCLING_SERVICE_URL:-http://faden-docling:8000}
735
740
  FADEN_BACKEND_SKILLS_SERVICE_URL: ${FADEN_BACKEND_SKILLS_SERVICE_URL:-http://faden-skills:8000}
736
741
  FADEN_BACKEND_SKILLS_SERVICE_TOKEN: ${FADEN_BACKEND_SKILLS_SERVICE_TOKEN:?set it in .env, as faden deploy and faden upgrade do (Help, Installing, Secrets in .env)}
@@ -11,9 +11,13 @@
11
11
  # for everyone, shares one count. Over the limit nginx answers 429 at
12
12
  # once (nodelay) and the next try a few seconds later goes through.
13
13
  #
14
- # faden_signin console and API sign-in: 20 a minute, 20 at once.
15
- # faden_account forgotten and reset passwords, e-mail verification and
16
- # bootstrap: 10 a minute, 10 at once.
14
+ # faden_signin console and API sign-in, every step of a sign-in with an
15
+ # outside provider, and the password or code that links one
16
+ # to an existing account: 20 a minute, 20 at once.
17
+ # faden_account forgotten and reset passwords (by mail or by a link an
18
+ # administrator made), e-mail verification, the code mailed
19
+ # to link an outside sign-in, and bootstrap: 10 a minute,
20
+ # 10 at once.
17
21
  # faden_refresh agents renewing their tokens: 300 a minute, 200 at once.
18
22
  # Each running agent renews about every 10 minutes
19
23
  # (FADEN_BACKEND_AGENT_ACCESS_TOKEN_TTL_S); the SDKs keep
@@ -26,11 +30,16 @@
26
30
  map "$request_method $uri" $faden_signin_key {
27
31
  default "";
28
32
  "~^POST /api/auth/(jwt|cookie)/login$" $binary_remote_addr;
33
+ "~^GET /api/auth/sso/[a-z0-9-]+/(start|callback)$" $binary_remote_addr;
34
+ "~^POST /api/auth/sso/[a-z0-9-]+/(acs|password)$" $binary_remote_addr;
35
+ "~^POST /api/auth/sso/link/with-(password|code)$" $binary_remote_addr;
29
36
  }
30
37
 
31
38
  map "$request_method $uri" $faden_account_key {
32
39
  default "";
33
40
  "~^POST /api/auth/(forgot-password|reset-password|request-verify-token|verify)$" $binary_remote_addr;
41
+ "~^POST /api/auth/sso/link/code$" $binary_remote_addr;
42
+ "~^POST /api/auth/reset-link/(preview|use)$" $binary_remote_addr;
34
43
  "~^POST /api/bootstrap$" $binary_remote_addr;
35
44
  }
36
45
 
@@ -840,6 +840,11 @@ def deploy_cmd(
840
840
  env_path, days=chat_retention_days, yes=yes, heading="Step 3f: Deleted chats…",
841
841
  )
842
842
 
843
+ # ── 3g. Add-ons' steps (registry.extend_install_hooks): their settings, secrets, registry login ──
844
+ from faden_cli.core.registry import run_install_hooks # noqa: PLC0415
845
+
846
+ run_install_hooks(shared_dir)
847
+
843
848
  # ── 4. Save config ────────────────────────────────────────────
844
849
  cfg.install_dir = str(shared_dir)
845
850
  cfg.compose_file = compose_file.name
@@ -32,7 +32,7 @@ from faden_cli.core.compose import (
32
32
  )
33
33
  from faden_cli.core.console import console, error, info, success, warning
34
34
  from faden_cli.core.settings import FadenConfig, load_config
35
- from faden_cli.commands.deploy import _sync_postgres_password
35
+ from faden_cli.commands.deploy import _regenerate_rmq_definitions, _sync_postgres_password
36
36
 
37
37
 
38
38
  @click.command("restore")
@@ -154,10 +154,10 @@ def _restore_cmd(
154
154
  # however this ends. nginx holds none of the volumes a restore writes (it mounts the install's nginx/
155
155
  # and tls/ directories, read-only), so it can run through it.
156
156
  with maintenance.during(Path(str(ctx.project_dir)), reason="restore"):
157
- result = _restore(ctx, cfg, source, db_only=db_only, skip_env=skip_env)
157
+ result, started = _restore(ctx, cfg, source, db_only=db_only, skip_env=skip_env)
158
158
 
159
159
  console.print()
160
- success("Restore complete.")
160
+ _report_start(started)
161
161
  from faden_cli.core.backend_tool import record_event # noqa: PLC0415
162
162
 
163
163
  record_event(
@@ -175,6 +175,20 @@ def _restore_cmd(
175
175
  console.print(f" Volumes: {', '.join(result.volumes_restored)}")
176
176
  if result.tls_restored:
177
177
  console.print(f" Certificate material: {len(result.tls_restored)} file(s); the machines still trust it")
178
+ if started != 0:
179
+ sys.exit(1)
180
+
181
+
182
+ def _report_start(started: int) -> None:
183
+ """Whether everything came back: *started* is the exit code of starting it all again.
184
+
185
+ The data is back either way; `faden status` names what did not start. It used to say "Restore complete."
186
+ """
187
+ if started == 0:
188
+ success("Restore complete.")
189
+ return
190
+ error(f"The backup is restored, but not every service started (docker compose up exited with {started}).")
191
+ error(" See which with `faden status`, and why with `faden logs <service>`.")
178
192
 
179
193
 
180
194
  #: The front door, which keeps running (and serving the maintenance page) while everything else is restored.
@@ -183,8 +197,11 @@ _FRONT = "nginx"
183
197
 
184
198
  def _restore(
185
199
  ctx: ComposeContext, cfg: FadenConfig, backup_path: Path, *, db_only: bool, skip_env: bool
186
- ) -> RestoreResult:
187
- """Stop everything but nginx, restore, and start everything again; exits when the restore fails."""
200
+ ) -> tuple[RestoreResult, int]:
201
+ """Stop everything but nginx, restore, and start everything again; exits when the restore fails.
202
+
203
+ Returns what was restored and the exit code of starting everything again (0: all started).
204
+ """
188
205
  console.print("\n[bold cyan]Stopping application services…[/bold cyan]")
189
206
  names = service_names(ctx)
190
207
  # Stopped, not taken down: nginx is left alone, and the containers keep their volumes and networks.
@@ -214,6 +231,14 @@ def _restore(
214
231
  warning(note)
215
232
  info("Syncing Postgres password with restored .env…")
216
233
  _sync_postgres_password(ctx, env_path)
234
+ # RabbitMQ makes its users from definitions.json on every start; the services log in with the
235
+ # passwords in .env. The file still held those of the .env the restore replaced: RabbitMQ refused
236
+ # the backend and the gateway (ACCESS_REFUSED), and the install did not come back. `faden upgrade`
237
+ # writes it the same way.
238
+ from faden_cli.core.env_gen import read_env_file # noqa: PLC0415
239
+
240
+ _regenerate_rmq_definitions(Path(str(ctx.project_dir)), read_env_file(env_path), cfg.profiles)
241
+ success("RabbitMQ users written from the restored .env.")
217
242
  # The provenance signer of a .env from before the module, or issued by an authority the restore replaced.
218
243
  if (result.env_restored or result.tls_restored) and ctx.env_file and ctx.project_dir:
219
244
  from faden_cli.core.provenance_keys import ensure_step as provenance_keys_step # noqa: PLC0415
@@ -222,9 +247,9 @@ def _restore(
222
247
 
223
248
  # ── Restart all services ──────────────────────────────────
224
249
  console.print("\n[bold cyan]Restarting all services…[/bold cyan]")
225
- compose_up(ctx, detach=True, wait=True, timeout=120)
250
+ started = compose_up(ctx, detach=True, wait=True, timeout=120)
226
251
  # nginx read the certificate and picked plain HTTP or HTTPS when it started; a restored one is
227
252
  # only served after it starts again.
228
253
  if result.tls_restored and _FRONT in names:
229
254
  compose_restart(ctx, services=[_FRONT])
230
- return result
255
+ return result, started
@@ -386,6 +386,10 @@ def upgrade_cmd(
386
386
  from faden_cli.commands.tls import refresh_edge_info # noqa: PLC0415
387
387
 
388
388
  refresh_edge_info(shared_dir)
389
+ # Add-ons' steps (registry.extend_install_hooks): their settings, secrets, registry login.
390
+ from faden_cli.core.registry import run_install_hooks # noqa: PLC0415
391
+
392
+ run_install_hooks(shared_dir)
389
393
 
390
394
  # ── 4. Images ─────────────────────────────────────────────
391
395
  if release is not None:
@@ -96,6 +96,16 @@ def unpack(install_dir: Path) -> list[str]:
96
96
  if not target.is_file() or target.read_bytes() != data:
97
97
  target.write_bytes(data)
98
98
  written.append(rel)
99
+ # What an add-on ships (registry.extend_bundle_files), replaced like the bundle's own files.
100
+ from faden_cli.core.registry import BUNDLE_FILES # noqa: PLC0415
101
+
102
+ for rel, source in BUNDLE_FILES.items():
103
+ target = install_dir / rel
104
+ data = Path(source).read_bytes()
105
+ if not target.is_file() or target.read_bytes() != data:
106
+ target.parent.mkdir(parents=True, exist_ok=True)
107
+ target.write_bytes(data)
108
+ written.append(rel)
99
109
  for rel, content in _STARTING_CONTENT.items():
100
110
  target = install_dir / rel
101
111
  if not target.exists():
@@ -121,6 +121,15 @@ class ComposeContext:
121
121
  error(f"The keys' files could not be written: {exc}")
122
122
  sys.exit(1)
123
123
 
124
+ def extension_overlays(self) -> list[Path]:
125
+ """The add-ons' compose files beside the main one that exist (``registry.extend_compose_files``)."""
126
+ from faden_cli.core.registry import COMPOSE_OVERLAYS # noqa: PLC0415
127
+
128
+ if not self.compose_files:
129
+ return []
130
+ folder = Path(str(self.compose_files[0])).parent
131
+ return [folder / name for name in COMPOSE_OVERLAYS if (folder / name).is_file()]
132
+
124
133
  def base_cmd(self) -> list[str]:
125
134
  """Build the ``docker compose`` prefix with all file/profile flags."""
126
135
  docker = shutil.which("docker")
@@ -135,6 +144,7 @@ class ComposeContext:
135
144
  overlay = gpu_overlay(self.env_file, self.project_dir)
136
145
  if overlay is not None and overlay not in files:
137
146
  files.append(overlay)
147
+ files.extend(p for p in self.extension_overlays() if p not in files)
138
148
  for f in files:
139
149
  cmd.extend(["-f", str(f)])
140
150
  if self.env_file and Path(str(self.env_file)).exists():
@@ -336,11 +346,49 @@ def stop_inactive_modules(ctx: ComposeContext) -> list[tuple[str, list[str]]]:
336
346
  return sorted(cleaned.items())
337
347
 
338
348
 
349
+ #: Services that start only when their profile is asked for, which ``faden`` never does, by the profile. An
350
+ #: earlier release started them with a module: up to 0.4.x ``faden-pii-worker`` was in profile ``pii``, since
351
+ #: 0.5.0 it is in ``pii-tasks``. Compose leaves a container of a service whose profile is off alone, so after
352
+ #: an upgrade from 0.4.x the old worker kept running on its 0.4.x image, through every restart and
353
+ #: ``faden down``.
354
+ ON_REQUEST_SERVICES: dict[str, str] = {"faden-pii-worker": "pii-tasks"}
355
+
356
+
357
+ def stop_unrequested_services(ctx: ComposeContext) -> list[str]:
358
+ """Stop and remove the containers of :data:`ON_REQUEST_SERVICES` whose profile is off; their names.
359
+
360
+ Volumes stay. Like :func:`stop_inactive_modules`, a failure is said and not fatal.
361
+ """
362
+ from dataclasses import replace # noqa: PLC0415
363
+
364
+ wanted = [name for name, profile in ON_REQUEST_SERVICES.items() if profile not in set(ctx.profiles)]
365
+ if not wanted:
366
+ return []
367
+ everything = replace(ctx, profiles=["*"])
368
+ present = {row.service: row.name for row in ps(everything) if row.service in wanted}
369
+ if not present:
370
+ return []
371
+ names = sorted(present)
372
+ failed = stop(everything, services=names)
373
+ failed = failed or _run(everything.base_cmd() + ["rm", "-f", *names], stream=True).returncode
374
+ if failed:
375
+ containers = " ".join(sorted(present.values()))
376
+ warning(f"Containers this release no longer runs could not be stopped and removed: {containers}. "
377
+ f"Run `faden up` again, or remove them with `docker rm -f {containers}`.")
378
+ return []
379
+ return sorted(name or service for service, name in present.items())
380
+
381
+
339
382
  def report_inactive_modules(ctx: ComposeContext) -> None:
340
- """Run :func:`stop_inactive_modules` and print one line per module it stopped."""
383
+ """Run :func:`stop_inactive_modules` and print one line per module it stopped.
384
+
385
+ Then :func:`stop_unrequested_services`, with a line of its own: those containers are an earlier release's.
386
+ """
341
387
  for module, containers in stop_inactive_modules(ctx):
342
388
  label = module.upper() if len(module) <= 3 else module.capitalize()
343
389
  info(f"{label} is switched off: stopped and removed {', '.join(containers)} (their data is kept).")
390
+ for container in stop_unrequested_services(ctx):
391
+ info(f"Stopped and removed {container}: an earlier release started it, this one does not.")
344
392
 
345
393
 
346
394
  def down(
@@ -7,8 +7,9 @@ instead of hard-coding values.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from collections.abc import Callable
10
11
  from dataclasses import dataclass
11
- from pathlib import Path
12
+ from pathlib import Path, PurePosixPath
12
13
 
13
14
 
14
15
  # ── GitHub organisation ───────────────────────────────────────────
@@ -543,6 +544,67 @@ def extend_dev_processes(extra: list[DevProcess]) -> None:
543
544
  existing.add(proc.name)
544
545
 
545
546
 
547
+ # ── What an add-on adds to an install ────────────────────────────
548
+
549
+ #: Compose files beside the install's ``docker-compose.yaml`` that every compose command of the install
550
+ #: includes, after core's own, when they exist (``compose.ComposeContext.base_cmd``).
551
+ COMPOSE_OVERLAYS: list[str] = []
552
+ #: Files an add-on ships, by their name in the install: ``faden deploy`` and ``faden upgrade`` write them
553
+ #: with the bundled deployment files (``bundle.unpack``), so an upgrade brings the add-on's copy too.
554
+ BUNDLE_FILES: dict[str, Path] = {}
555
+ #: Keys of ``.env`` written to ``secrets/<KEY>`` before every compose command, like core's own keys
556
+ #: (``secret_files.SECRET_FILES``): an add-on's compose file mounts them under ``/run/secrets``.
557
+ SECRET_FILE_KEYS: list[str] = []
558
+ #: Called by ``faden deploy`` and ``faden upgrade`` with the install's directory once its ``.env`` is
559
+ #: written and before any image is pulled or started: an add-on writes its settings and secrets, or logs in
560
+ #: to the registry its images come from. One that raises stops the deploy or the upgrade.
561
+ INSTALL_HOOKS: list[Callable[[Path], None]] = []
562
+
563
+
564
+ def extend_compose_files(names: list[str]) -> None:
565
+ """Add compose files every command of an install includes when they exist (idempotent)."""
566
+ for name in names:
567
+ if Path(name).name != name or not name.endswith((".yaml", ".yml")):
568
+ msg = f"Not a compose file beside the install's own: {name!r}"
569
+ raise ValueError(msg)
570
+ if name not in COMPOSE_OVERLAYS:
571
+ COMPOSE_OVERLAYS.append(name)
572
+
573
+
574
+ def extend_bundle_files(files: dict[str, Path]) -> None:
575
+ """Add files written into an install with the bundled deployment files (a later add wins)."""
576
+ for name, source in files.items():
577
+ # Either separator: an install on Windows reads "/etc/x" as relative to its drive.
578
+ rel = PurePosixPath(name.replace("\\", "/"))
579
+ if rel.is_absolute() or Path(name).is_absolute() or ".." in rel.parts:
580
+ msg = f"Not a path inside the install: {name!r}"
581
+ raise ValueError(msg)
582
+ BUNDLE_FILES[name] = source
583
+
584
+
585
+ def extend_secret_files(keys: list[str]) -> None:
586
+ """Add ``.env`` keys written as files under ``secrets/`` before every compose command (idempotent)."""
587
+ for key in keys:
588
+ if not key.isidentifier() or key.upper() != key:
589
+ msg = f"Not an .env key: {key!r}"
590
+ raise ValueError(msg)
591
+ if key not in SECRET_FILE_KEYS:
592
+ SECRET_FILE_KEYS.append(key)
593
+
594
+
595
+ def extend_install_hooks(hooks: list[Callable[[Path], None]]) -> None:
596
+ """Add steps ``faden deploy`` and ``faden upgrade`` run before pulling (idempotent)."""
597
+ for hook in hooks:
598
+ if hook not in INSTALL_HOOKS:
599
+ INSTALL_HOOKS.append(hook)
600
+
601
+
602
+ def run_install_hooks(install_dir: Path) -> None:
603
+ """Run the add-ons' install steps, in the order they were added."""
604
+ for hook in INSTALL_HOOKS:
605
+ hook(install_dir)
606
+
607
+
546
608
  def _read_env_values(path: Path) -> dict[str, str]:
547
609
  """Parse a simple ``KEY=VALUE`` env file into a dict."""
548
610
  path = Path(path)
@@ -94,6 +94,12 @@ def generate_definitions(
94
94
  #: still running and leaves it be (backend ``services/llm/tasks.py``).
95
95
  CONSUMER_TIMEOUT_MS = 2 * 24 * 3600 * 1000
96
96
 
97
+ #: The memory RabbitMQ's container may use (``faden-rmq``'s limit in shared/docker-compose.yaml), which RabbitMQ is
98
+ #: told (``total_memory_available_override_value``): it reads the host's memory otherwise, and its memory alarm
99
+ #: (60 % of what it believes it has: 40 GB on a 64 GB host) never came before the kernel killed it at the
100
+ #: container's limit. Killed at start-up after a reboot, in a loop, while it recovered its data.
101
+ MEMORY_BYTES = 1024**3
102
+
97
103
 
98
104
  def write_definitions(
99
105
  output_dir: Path,
@@ -128,7 +134,8 @@ def write_definitions(
128
134
  conf_path.write_text(
129
135
  "# Auto-generated by faden deploy — do not edit.\n"
130
136
  "load_definitions = /etc/rabbitmq/definitions.json\n"
131
- f"consumer_timeout = {CONSUMER_TIMEOUT_MS}\n",
137
+ f"consumer_timeout = {CONSUMER_TIMEOUT_MS}\n"
138
+ f"total_memory_available_override_value = {MEMORY_BYTES}\n",
132
139
  encoding="utf-8",
133
140
  )
134
141
 
@@ -75,9 +75,11 @@ def _clear_docker_folder(path: Path) -> None:
75
75
 
76
76
  def write(project_dir: Path, env_file: Path) -> list[str]:
77
77
  """Write every key's file from *env_file* into ``<project_dir>/secrets/``; the required keys .env lacks."""
78
+ from faden_cli.core.registry import SECRET_FILE_KEYS # noqa: PLC0415 -- an add-on's keys, added at start
79
+
78
80
  values = read_env_file(env_file) if env_file.is_file() else {}
79
81
  folder = private_dir(project_dir / FOLDER)
80
- for name in SECRET_FILES:
82
+ for name in (*SECRET_FILES, *(key for key in SECRET_FILE_KEYS if key not in SECRET_FILES)):
81
83
  path = folder / name
82
84
  if path.is_dir():
83
85
  _clear_docker_folder(path)
@@ -6,7 +6,12 @@ tests drive that code, and three of them mocked only some of the helpers
6
6
  and killed the development stack the developer was running at the time.
7
7
  A test run should never be able to do that. The same went for a deploy step
8
8
  that ran `docker exec faden-postgres` for real and switched off logins of the
9
- developer's database, so a real `docker exec` fails the test too.
9
+ developer's database, so a real `docker exec` fails the test too. And for the
10
+ runtime images `deploy` and `upgrade` fetch at the end: the upgrade tests read
11
+ the pins out of the real backend image and pulled every runtime image it names
12
+ (about 10 GB each), which filled CI's disk; a machine that already had them
13
+ passed. So that step's Docker is answered "not here", and a real `docker pull`
14
+ fails the test.
10
15
 
11
16
  The guard patches the primitives rather than the functions, so the tests that
12
17
  exist to exercise the matching logic still call the real thing; they just
@@ -17,10 +22,12 @@ this fixture and therefore wins.
17
22
 
18
23
  from __future__ import annotations
19
24
 
25
+ import subprocess
26
+
20
27
  import pytest
21
28
 
22
29
  from faden_cli.commands.dev import dev_up
23
- from faden_cli.core import backend_tool, backup, sealing
30
+ from faden_cli.core import backend_tool, backup, runtime_images, sealing
24
31
 
25
32
 
26
33
  @pytest.fixture(autouse=True)
@@ -33,6 +40,12 @@ def never_touch_real_processes(monkeypatch: pytest.MonkeyPatch) -> None:
33
40
  # The upgrade, deploy and rebuild steps that carry chat files out of the gateway container: none here.
34
41
  monkeypatch.setattr(backup, "keep_chat_files", lambda *_a, **_k: None)
35
42
  monkeypatch.setattr(backup, "keep_company_files", lambda *_a, **_k: None)
43
+ # The runtime images step's Docker (reading the pins out of the backend image, inspecting, pulling): it
44
+ # finds nothing, says so and goes on, as it does on a server without Docker. Its own tests fake `_run`.
45
+ monkeypatch.setattr(
46
+ runtime_images, "_run",
47
+ lambda cmd, **_k: subprocess.CompletedProcess(cmd, 1, "", "no Docker in the tests"),
48
+ )
36
49
 
37
50
  # Nothing to find...
38
51
  monkeypatch.setattr(dev_up.psutil, "process_iter", lambda *_a, **_k: [])
@@ -60,6 +73,11 @@ def never_touch_real_processes(monkeypatch: pytest.MonkeyPatch) -> None:
60
73
  "cp" in args[1:2] or list(args[1:3]) == ["volume", "create"]
61
74
  ):
62
75
  pytest.fail(f"a test reached a real `docker cp` or `docker volume create`: {args!r}")
76
+ # Nor a pull: the images are gigabytes, and a test passes only where they happen to be present.
77
+ if isinstance(args, (list, tuple)) and isinstance(first, str) and "docker" in first.lower() and (
78
+ "pull" in args[1:2]
79
+ ):
80
+ pytest.fail(f"a test reached a real `docker pull`: {args!r}")
63
81
  return real_run(args, *rest, **kwargs)
64
82
 
65
83
  monkeypatch.setattr(dev_up.subprocess, "run", guarded_run)
@@ -1,4 +1,8 @@
1
- """A module that is switched off has its containers stopped and removed (data kept) by ``faden up``."""
1
+ """A module that is switched off has its containers stopped and removed (data kept) by ``faden up``.
2
+
3
+ So has a service that starts only on request when it was not asked for: the PII worker an install from 0.4.x
4
+ still ran on its 0.4.x image (its profile is ``pii-tasks`` since 0.5.0, ``pii`` before).
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -18,7 +22,7 @@ SERVICES = {
18
22
  "postgres": {},
19
23
  "backend": {},
20
24
  "faden-pii": {"profiles": ["pii"]},
21
- "faden-pii-worker": {"profiles": ["pii"]},
25
+ "faden-pii-worker": {"profiles": ["pii-tasks"]},
22
26
  "faden-mcp": {"profiles": ["mcp"]},
23
27
  "faden-base": {"profiles": ["_build"]},
24
28
  }
@@ -56,17 +60,17 @@ def fake(monkeypatch: pytest.MonkeyPatch):
56
60
 
57
61
 
58
62
  def test_a_disabled_module_is_stopped_and_removed(fake) -> None:
59
- f = fake(["postgres", "backend", "faden-pii", "faden-pii-worker"])
63
+ f = fake(["postgres", "backend", "faden-pii", "faden-mcp"])
60
64
  done = compose.stop_inactive_modules(ComposeContext(profiles=[]))
61
- assert done == [("pii", ["faden-pii", "faden-pii-worker"])]
65
+ assert done == [("mcp", ["faden-mcp"]), ("pii", ["faden-pii"])]
62
66
  stop, rm = f.calls[-2], f.calls[-1]
63
- assert stop[-3:] == ["stop", "faden-pii", "faden-pii-worker"]
64
- assert rm[-4:] == ["rm", "-f", "faden-pii", "faden-pii-worker"]
67
+ assert stop[-3:] == ["stop", "faden-mcp", "faden-pii"]
68
+ assert rm[-4:] == ["rm", "-f", "faden-mcp", "faden-pii"]
65
69
  assert all(w not in c for c in f.calls for w in ("down", "-v", "--volumes"))
66
70
 
67
71
 
68
72
  def test_an_enabled_module_is_left_running(fake) -> None:
69
- f = fake(["postgres", "faden-pii", "faden-pii-worker", "faden-mcp"])
73
+ f = fake(["postgres", "faden-pii", "faden-mcp"])
70
74
  done = compose.stop_inactive_modules(ComposeContext(profiles=["pii"]))
71
75
  assert done == [("mcp", ["faden-mcp"])]
72
76
  assert "faden-pii" not in f.calls[-1] and "faden-pii" not in f.calls[-2]
@@ -96,10 +100,12 @@ def test_up_reports_each_stopped_module(monkeypatch: pytest.MonkeyPatch, tmp_pat
96
100
  monkeypatch.setattr(up_command, "build_context_from_config", lambda _c: ctx)
97
101
  monkeypatch.setattr(up_command, "compose_up", lambda *_a, **_k: 0)
98
102
  monkeypatch.setattr("faden_cli.core.backup.keep_company_files", lambda *_a, **_k: None)
99
- monkeypatch.setattr(compose, "stop_inactive_modules", lambda _c: [("pii", ["faden-pii", "faden-pii-worker"])])
103
+ monkeypatch.setattr(compose, "stop_inactive_modules", lambda _c: [("pii", ["faden-pii"])])
104
+ monkeypatch.setattr(compose, "stop_unrequested_services", lambda _c: ["faden-pii-worker"])
100
105
  result = CliRunner().invoke(up_command.up_cmd, [])
101
106
  assert result.exit_code == 0, result.output
102
- assert "PII is switched off: stopped and removed faden-pii, faden-pii-worker" in result.output
107
+ assert "PII is switched off: stopped and removed faden-pii" in result.output
108
+ assert "Stopped and removed faden-pii-worker: an earlier release" in result.output
103
109
 
104
110
 
105
111
  def test_up_with_named_services_does_not_stop_modules(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
@@ -129,3 +135,25 @@ def test_a_failed_stop_is_said_not_swallowed(fake, monkeypatch: pytest.MonkeyPat
129
135
  monkeypatch.setattr(compose, "warning", said.append)
130
136
  assert compose.stop_inactive_modules(ComposeContext(profiles=[])) == []
131
137
  assert len(said) == 1 and "faden-pii" in said[0] and "docker rm -f faden-pii" in said[0]
138
+
139
+
140
+ def test_the_old_pii_worker_is_removed_also_with_pii_on(fake) -> None:
141
+ """What an install from 0.4.x still ran: the worker, outside every profile `faden` switches on."""
142
+ f = fake(["postgres", "faden-pii", "faden-pii-worker"])
143
+ assert compose.stop_unrequested_services(ComposeContext(profiles=["pii"])) == ["faden-pii-worker"]
144
+ stop, rm = f.calls[-2], f.calls[-1]
145
+ assert stop[-2:] == ["stop", "faden-pii-worker"]
146
+ assert rm[-3:] == ["rm", "-f", "faden-pii-worker"]
147
+ assert all("faden-pii" not in c for c in (stop, rm)), "the PII service itself keeps running"
148
+
149
+
150
+ def test_a_service_asked_for_stays(fake) -> None:
151
+ f = fake(["faden-pii", "faden-pii-worker"])
152
+ assert compose.stop_unrequested_services(ComposeContext(profiles=["pii", "pii-tasks"])) == []
153
+ assert f.calls == [], "nothing to look up when every on-request service was asked for"
154
+
155
+
156
+ def test_no_old_worker_means_one_lookup(fake) -> None:
157
+ f = fake(["postgres", "faden-pii"])
158
+ assert compose.stop_unrequested_services(ComposeContext(profiles=["pii"])) == []
159
+ assert f.verbs() == ["ps"]