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.
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/PKG-INFO +1 -1
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/pyproject.toml +1 -1
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/__init__.py +1 -1
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/docker-compose.yaml +4 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/deploy.py +3 -2
- fadenstack-0.7.0rc2/src/faden_cli/commands/restore.py +428 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/up.py +50 -4
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/backup.py +323 -89
- fadenstack-0.7.0rc2/src/faden_cli/core/release_feed.py +755 -0
- fadenstack-0.7.0rc2/src/faden_cli/core/restore_swap.py +406 -0
- fadenstack-0.7.0rc2/tests/test_app_catalogue.py +308 -0
- fadenstack-0.7.0rc2/tests/test_backup_release.py +52 -0
- fadenstack-0.7.0rc2/tests/test_gateway_dns_cache.py +25 -0
- fadenstack-0.7.0rc2/tests/test_image_labels.py +121 -0
- fadenstack-0.7.0rc2/tests/test_restore_beside.py +530 -0
- fadenstack-0.7.0rc2/tests/test_restore_maintenance.py +111 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_restore_rabbitmq.py +37 -14
- fadenstack-0.7.0rc2/tests/test_up_waits.py +76 -0
- fadenstack-0.7.0rc2/tests/test_update_feed.py +193 -0
- fadenstack-0.6.0rc4/src/faden_cli/commands/restore.py +0 -255
- fadenstack-0.6.0rc4/src/faden_cli/core/release_feed.py +0 -348
- fadenstack-0.6.0rc4/tests/test_restore_maintenance.py +0 -96
- fadenstack-0.6.0rc4/tests/test_update_feed.py +0 -86
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/.gitignore +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/LICENSE +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/NOTICE +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/README.md +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/hatch_build.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/__main__.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/pii-catalog.json +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/.gitignore +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/LICENSE +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/NOTICE +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/README.md +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/agent-binaries/.gitignore +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/alloy/config.alloy +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/clickhouse/config.d/system-logs.xml +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/docker-compose.gpu.yaml +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/docker-compose.pii-gpu.yaml +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/dashboards/templates/vllm-runtime.json +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/provisioning/dashboards/dashboard.yml +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/provisioning/dashboards/json/faden-overview.json +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/provisioning/datasources/ds.yaml +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/grafana/provisioning/datasources/prometheus.yaml +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/initdb/01-init.sql +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/loki/local-config.yaml +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/locations.conf +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/maintenance.html +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/nginx-tls.conf +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/nginx.conf +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/rate-limits.conf +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/security-headers.conf +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/nginx/select-config.sh +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/prometheus/prometheus.yml +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/rabbitmq/definitions.json.example +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/bundle/shared/tls/README.md +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/cli.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/__init__.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/admin.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/audit.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/backup.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/chats.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/config.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/__init__.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_doctor.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_down.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_group.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_init.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_pull.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_status.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/dev/dev_up.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/doctor.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/down.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/logs_cmd.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/maintenance.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/module.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/observe.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/ports.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/rebuild.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/runtime_images.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/secrets.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/status.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/tls.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/tune.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/upgrade.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/commands/version.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/__init__.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/api_client.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/backend_tool.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/backup_crypto.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/backup_key.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/bootstrap.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/bundle.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/chat_retention.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/clickhouse.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/compose.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/console.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/detect.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/dev_modules.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/docker_env.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/edge.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/env_gen.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/git.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/install.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/installation.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/local_node.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/maintenance.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/pii_artifacts.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/pii_catalog.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/pii_hardware.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/pii_runtime.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/private_files.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/provenance_keys.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/registry.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/release_notice.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/rmq.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/runtime_images.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/schedule.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/sealing.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/secret_files.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/secret_rotation.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/settings.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/sysinfo.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/tls.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/tune.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/update_check.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/core/workspace.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/templates/env.j2 +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/tui/__init__.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/tui/widgets/__init__.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/src/faden_cli/tui/wizard/__init__.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/conftest.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_encryption.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_migration_heads.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_retention.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_schedule.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_tls.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_backup_volumes.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_binary_notices.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_bootstrap_token.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_chat_retention.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_cli_output_encoding.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_data_dir_move.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_data_volumes_logs.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_deploy_summary.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_env_loopback.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_modules.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_up_port_holders.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_up_reclaim.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_up_service_hosts.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_dev_up_terminal.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_docker_env.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_doctor_ports.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_edge.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_generated_env_reaches_compose.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_inactive_modules.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_install_extensions.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_installation.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_internal_token_env.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_local_node.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_main.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_maintenance.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_module_env.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_module_secrets.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_artifacts.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_catalog.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_hardware.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_runtime.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_pii_upgrade.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_postgres_logins.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_private_files.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_provenance_keys.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_provenance_module.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_provenance_on_by_default.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_rebuild.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_redis_limits.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_registry_gateway_env.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_release_bundle.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_release_feed.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_release_notice.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_release_versions.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_required_env.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_runtime_images.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_secret_files.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_secret_rotation.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_service_logins.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_shared_compose_ports.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_tls.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_tls_renew.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_trusted_keys.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_tune.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_tune_memory.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_update_check.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_upgrade_housekeeping.py +0 -0
- {fadenstack-0.6.0rc4 → fadenstack-0.7.0rc2}/tests/test_upgrade_rabbitmq.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
178
|
-
"
|
|
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
|
|
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)
|