redeploy 0.2.54__tar.gz → 0.2.56__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.
- {redeploy-0.2.54 → redeploy-0.2.56}/PKG-INFO +6 -6
- {redeploy-0.2.54 → redeploy-0.2.56}/README.md +5 -5
- {redeploy-0.2.54 → redeploy-0.2.56}/pyproject.toml +1 -1
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/__init__.py +1 -1
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/runner.py +1 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/docker_compose.py +74 -49
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/parsers/compose.py +21 -13
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_heal.py +5 -5
- redeploy-0.2.56/redeploy/tests/test_heal_decider.py +91 -0
- redeploy-0.2.56/redeploy/tests/test_iac_compose_contract.py +181 -0
- redeploy-0.2.56/redeploy/tests/test_iac_docker_compose_contract.py +172 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/SOURCES.txt +5 -0
- redeploy-0.2.56/tests/test_heal_decider.py +63 -0
- redeploy-0.2.56/tests/test_heal_loop_detector.py +47 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/handlers.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/audit.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/compose.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/hardware.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/infra.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/blueprint/sources/migration.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/bump_fix.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/mcp_cmd.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/prompt_cmd.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli/query.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/cli.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/discovery.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/fleet.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/decider.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/hint_provider.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/log_writer.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal/loop_detector.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/heal.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/config_hints.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/integrations/op3_bridge.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/mcp_server.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/models.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/observe.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/parse.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/patterns.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/schema.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/ssh.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/steps/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/steps/builtins.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/steps/kiosk.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_hooks.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/verify.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy/version.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/setup.cfg +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_blueprint_sources.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_blueprint_sources_compose.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_cli_query.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_example_argocd_flux_plugin.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_example_gitops_ci_plugin.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_example_helm_kustomize_plugin.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_iac.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_iac_config_hints.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_iac_plugin_loading.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_import_plugin_template.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_parse.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_prompt_llm.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_redeploy.py +0 -0
- {redeploy-0.2.54 → redeploy-0.2.56}/tests/test_steps_kiosk.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.56
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -32,17 +32,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
|
32
32
|
|
|
33
33
|
## AI Cost Tracking
|
|
34
34
|
|
|
35
|
-
     
|
|
36
|
+
  
|
|
37
37
|
|
|
38
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
39
|
-
- 👤 **Human dev:** ~$
|
|
38
|
+
- 🤖 **LLM usage:** $7.5000 (76 commits)
|
|
39
|
+
- 👤 **Human dev:** ~$2862 (28.6h @ $100/h, 30min dedup)
|
|
40
40
|
|
|
41
41
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
    
|
|
46
46
|
|
|
47
47
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
48
48
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.5000 (76 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2862 (28.6h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
|
|
|
7
7
|
|
|
8
8
|
Everything not listed in ``__all__`` is internal and may change without notice.
|
|
9
9
|
"""
|
|
10
|
-
__version__ = "0.2.
|
|
10
|
+
__version__ = "0.2.56"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -291,14 +291,12 @@ class DockerComposeParser:
|
|
|
291
291
|
pass
|
|
292
292
|
return env
|
|
293
293
|
|
|
294
|
-
def
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
# image / build
|
|
301
|
-
image = resolve(cfg.get("image")) or None
|
|
294
|
+
def _resolve_image_and_build(
|
|
295
|
+
self,
|
|
296
|
+
cfg: dict,
|
|
297
|
+
env_context: dict[str, str],
|
|
298
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
299
|
+
image = _resolve_var(str(cfg.get("image")), env_context) if cfg.get("image") is not None else None
|
|
302
300
|
build = cfg.get("build")
|
|
303
301
|
build_context: Optional[str] = None
|
|
304
302
|
if build:
|
|
@@ -308,8 +306,15 @@ class DockerComposeParser:
|
|
|
308
306
|
build_context = build.get("context", ".")
|
|
309
307
|
if not image:
|
|
310
308
|
image = f"<build:{build_context or '.'}>"
|
|
311
|
-
|
|
312
|
-
|
|
309
|
+
return image, build_context
|
|
310
|
+
|
|
311
|
+
def _parse_service_ports(
|
|
312
|
+
self,
|
|
313
|
+
name: str,
|
|
314
|
+
cfg: dict,
|
|
315
|
+
env_context: dict[str, str],
|
|
316
|
+
spec: ParsedSpec,
|
|
317
|
+
) -> list[PortInfo]:
|
|
313
318
|
ports: list[PortInfo] = []
|
|
314
319
|
for raw in (cfg.get("ports") or []):
|
|
315
320
|
if isinstance(raw, str):
|
|
@@ -318,75 +323,95 @@ class DockerComposeParser:
|
|
|
318
323
|
if p:
|
|
319
324
|
ports.append(p)
|
|
320
325
|
else:
|
|
321
|
-
spec.add_warning(f"[{name}] Could not parse port: {raw!r}",
|
|
322
|
-
|
|
326
|
+
spec.add_warning(f"[{name}] Could not parse port: {raw!r}", severity="warn")
|
|
327
|
+
return ports
|
|
323
328
|
|
|
324
|
-
|
|
329
|
+
def _parse_service_volumes(self, cfg: dict) -> list[VolumeInfo]:
|
|
325
330
|
volumes: list[VolumeInfo] = []
|
|
326
331
|
for raw in (cfg.get("volumes") or []):
|
|
327
332
|
v = _parse_volume(raw)
|
|
328
333
|
if v:
|
|
329
334
|
volumes.append(v)
|
|
335
|
+
return volumes
|
|
330
336
|
|
|
331
|
-
|
|
337
|
+
def _parse_service_env(
|
|
338
|
+
self,
|
|
339
|
+
cfg: dict,
|
|
340
|
+
env_context: dict[str, str],
|
|
341
|
+
) -> dict[str, str]:
|
|
332
342
|
env = _env_dict(cfg.get("environment"))
|
|
333
|
-
|
|
334
|
-
env = {k: _resolve_var(v, env_context) for k, v in env.items()}
|
|
343
|
+
return {k: _resolve_var(v, env_context) for k, v in env.items()}
|
|
335
344
|
|
|
336
|
-
|
|
337
|
-
env_files: list[str] = []
|
|
345
|
+
def _parse_service_env_files(self, cfg: dict) -> list[str]:
|
|
338
346
|
ef = cfg.get("env_file")
|
|
339
|
-
if ef:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
347
|
+
if not ef:
|
|
348
|
+
return []
|
|
349
|
+
if isinstance(ef, str):
|
|
350
|
+
return [ef]
|
|
351
|
+
if isinstance(ef, list):
|
|
352
|
+
return [str(x) for x in ef]
|
|
353
|
+
return []
|
|
354
|
+
|
|
355
|
+
def _parse_service_networks(self, cfg: dict) -> list[str]:
|
|
346
356
|
svc_nets = cfg.get("networks")
|
|
347
357
|
if isinstance(svc_nets, dict):
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
svc_networks = []
|
|
358
|
+
return list(svc_nets.keys())
|
|
359
|
+
if isinstance(svc_nets, list):
|
|
360
|
+
return [str(n) for n in svc_nets]
|
|
361
|
+
return []
|
|
353
362
|
|
|
354
|
-
|
|
363
|
+
def _parse_service_depends_on(self, cfg: dict) -> list[str]:
|
|
355
364
|
dep = cfg.get("depends_on")
|
|
356
365
|
if isinstance(dep, list):
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
depends_on = []
|
|
366
|
+
return [str(d) for d in dep]
|
|
367
|
+
if isinstance(dep, dict):
|
|
368
|
+
return list(dep.keys())
|
|
369
|
+
return []
|
|
362
370
|
|
|
363
|
-
|
|
371
|
+
def _parse_service_healthcheck(self, cfg: dict) -> Optional[str]:
|
|
364
372
|
hc = cfg.get("healthcheck")
|
|
365
|
-
healthcheck: Optional[str] = None
|
|
366
373
|
if isinstance(hc, dict):
|
|
367
374
|
test = hc.get("test")
|
|
368
375
|
if isinstance(test, list):
|
|
369
376
|
test = " ".join(str(t) for t in test)
|
|
370
|
-
|
|
377
|
+
return str(test) if test else None
|
|
378
|
+
return None
|
|
371
379
|
|
|
372
|
-
|
|
380
|
+
def _parse_service_replicas(self, cfg: dict) -> int:
|
|
373
381
|
deploy = cfg.get("deploy") or {}
|
|
374
|
-
replicas = 1
|
|
375
382
|
if isinstance(deploy, dict):
|
|
376
|
-
|
|
383
|
+
return int(deploy.get("replicas", 1))
|
|
384
|
+
return 1
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
def _parse_service_labels(self, cfg: dict) -> dict[str, str]:
|
|
379
387
|
labels_raw = cfg.get("labels")
|
|
380
388
|
if isinstance(labels_raw, dict):
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
labels = {}
|
|
389
|
+
return {str(k): str(v) for k, v in labels_raw.items()}
|
|
390
|
+
if isinstance(labels_raw, list):
|
|
391
|
+
labels: dict[str, str] = {}
|
|
384
392
|
for item in labels_raw:
|
|
385
393
|
if "=" in str(item):
|
|
386
394
|
k, v = str(item).split("=", 1)
|
|
387
395
|
labels[k] = v
|
|
388
|
-
|
|
389
|
-
|
|
396
|
+
return labels
|
|
397
|
+
return {}
|
|
398
|
+
|
|
399
|
+
def _parse_service_command(self, cfg: dict) -> Optional[str]:
|
|
400
|
+
return str(cfg["command"]) if cfg.get("command") else None
|
|
401
|
+
|
|
402
|
+
def _parse_service(self, name: str, cfg: dict,
|
|
403
|
+
env_context: dict[str, str],
|
|
404
|
+
spec: ParsedSpec) -> ServiceInfo:
|
|
405
|
+
image, build_context = self._resolve_image_and_build(cfg, env_context)
|
|
406
|
+
ports = self._parse_service_ports(name, cfg, env_context, spec)
|
|
407
|
+
volumes = self._parse_service_volumes(cfg)
|
|
408
|
+
env = self._parse_service_env(cfg, env_context)
|
|
409
|
+
env_files = self._parse_service_env_files(cfg)
|
|
410
|
+
svc_networks = self._parse_service_networks(cfg)
|
|
411
|
+
depends_on = self._parse_service_depends_on(cfg)
|
|
412
|
+
healthcheck = self._parse_service_healthcheck(cfg)
|
|
413
|
+
replicas = self._parse_service_replicas(cfg)
|
|
414
|
+
labels = self._parse_service_labels(cfg)
|
|
390
415
|
|
|
391
416
|
return ServiceInfo(
|
|
392
417
|
name=name,
|
|
@@ -399,7 +424,7 @@ class DockerComposeParser:
|
|
|
399
424
|
depends_on=depends_on,
|
|
400
425
|
healthcheck=healthcheck,
|
|
401
426
|
restart=cfg.get("restart"),
|
|
402
|
-
command=
|
|
427
|
+
command=self._parse_service_command(cfg),
|
|
403
428
|
build_context=build_context,
|
|
404
429
|
replicas=replicas,
|
|
405
430
|
labels=labels,
|
|
@@ -113,13 +113,29 @@ class DockerComposeParser:
|
|
|
113
113
|
|
|
114
114
|
# ── service parsing ───────────────────────────────────────────────────────
|
|
115
115
|
|
|
116
|
+
def _collect_service_secrets(self, d: dict, spec: ParsedSpec) -> None:
|
|
117
|
+
for secret in (d.get("secrets") or []):
|
|
118
|
+
secret_name = secret if isinstance(secret, str) else secret.get("source", "")
|
|
119
|
+
if secret_name and secret_name not in spec.secrets_referenced:
|
|
120
|
+
spec.secrets_referenced.append(secret_name)
|
|
121
|
+
|
|
122
|
+
def _merge_service_env_files(self, d: dict, svc: ServiceInfo, spec: ParsedSpec) -> None:
|
|
123
|
+
for ef in (d.get("env_file") or []):
|
|
124
|
+
ef = ef if isinstance(ef, str) else ef.get("path", "")
|
|
125
|
+
if ef and ef not in spec.env_files:
|
|
126
|
+
spec.env_files.append(ef)
|
|
127
|
+
if ef and ef not in svc.env_files:
|
|
128
|
+
svc.env_files.append(ef)
|
|
129
|
+
|
|
130
|
+
def _collect_service_image(self, svc: ServiceInfo, spec: ParsedSpec) -> None:
|
|
131
|
+
if svc.image and svc.image not in spec.images:
|
|
132
|
+
spec.images.append(svc.image)
|
|
133
|
+
|
|
116
134
|
def _parse_service(self, name: str, d: dict, spec: ParsedSpec) -> ServiceInfo:
|
|
117
135
|
svc = ServiceInfo(name=name)
|
|
118
136
|
|
|
119
137
|
svc.image = d.get("image") or None
|
|
120
|
-
|
|
121
|
-
if svc.image not in spec.images:
|
|
122
|
-
spec.images.append(svc.image)
|
|
138
|
+
self._collect_service_image(svc, spec)
|
|
123
139
|
|
|
124
140
|
svc.build_context = self._parse_build(d.get("build"))
|
|
125
141
|
svc.command = self._parse_command(d.get("command"))
|
|
@@ -135,18 +151,10 @@ class DockerComposeParser:
|
|
|
135
151
|
svc.healthcheck = self._parse_healthcheck(d.get("healthcheck"))
|
|
136
152
|
|
|
137
153
|
# secrets referenced by service
|
|
138
|
-
|
|
139
|
-
secret_name = secret if isinstance(secret, str) else secret.get("source", "")
|
|
140
|
-
if secret_name and secret_name not in spec.secrets_referenced:
|
|
141
|
-
spec.secrets_referenced.append(secret_name)
|
|
154
|
+
self._collect_service_secrets(d, spec)
|
|
142
155
|
|
|
143
156
|
# env_files at service level
|
|
144
|
-
|
|
145
|
-
ef = ef if isinstance(ef, str) else ef.get("path", "")
|
|
146
|
-
if ef and ef not in spec.env_files:
|
|
147
|
-
spec.env_files.append(ef)
|
|
148
|
-
if ef and ef not in svc.env_files:
|
|
149
|
-
svc.env_files.append(ef)
|
|
157
|
+
self._merge_service_env_files(d, svc, spec)
|
|
150
158
|
|
|
151
159
|
return svc
|
|
152
160
|
|
|
@@ -60,11 +60,11 @@ def test_heal_runner_stops_on_repeating_hint_pattern(monkeypatch, tmp_path):
|
|
|
60
60
|
ask_calls.append(True)
|
|
61
61
|
return "```yaml\n- id: sync_env\n action: ssh_cmd\n description: \"manual\"\n```"
|
|
62
62
|
|
|
63
|
-
monkeypatch.setattr("redeploy.heal.parse_failed_step", lambda *_a, **_k: ("sync_env", "boom"))
|
|
64
|
-
monkeypatch.setattr("redeploy.heal.collect_diagnostics", lambda *_a, **_k: "ERROR: same")
|
|
65
|
-
monkeypatch.setattr("redeploy.heal.ask_llm", _fake_ask)
|
|
66
|
-
monkeypatch.setattr("redeploy.heal.apply_fix_to_spec", lambda *_a, **_k:
|
|
67
|
-
monkeypatch.setattr("redeploy.heal.write_repair_log", lambda *_a, **_k: None)
|
|
63
|
+
monkeypatch.setattr("redeploy.heal.runner.parse_failed_step", lambda *_a, **_k: ("sync_env", "boom"))
|
|
64
|
+
monkeypatch.setattr("redeploy.heal.runner.collect_diagnostics", lambda *_a, **_k: "ERROR: same")
|
|
65
|
+
monkeypatch.setattr("redeploy.heal.runner.ask_llm", _fake_ask)
|
|
66
|
+
monkeypatch.setattr("redeploy.heal.runner.apply_fix_to_spec", lambda *_a, **_k: True)
|
|
67
|
+
monkeypatch.setattr("redeploy.heal.runner.write_repair_log", lambda *_a, **_k: None)
|
|
68
68
|
monkeypatch.setattr(HealRunner, "_make_executor", _fake_make_executor)
|
|
69
69
|
monkeypatch.setattr(HealRunner, "_reload_migration", lambda *_a, **_k: None)
|
|
70
70
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from redeploy.heal.decider import Action, Decision, decide_after_failure, format_decision_message
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_decide_abort_on_loop_detected():
|
|
5
|
+
decision = decide_after_failure(
|
|
6
|
+
attempt=1,
|
|
7
|
+
max_retries=3,
|
|
8
|
+
failed_step="sync_env",
|
|
9
|
+
loop_detected=True,
|
|
10
|
+
llm_error=False,
|
|
11
|
+
spec_patched=False,
|
|
12
|
+
)
|
|
13
|
+
assert decision == Decision(Action.ABORT, "Loop detected: same hint for 'sync_env' ≥ 3 times in a row")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_decide_abort_when_failed_step_unknown():
|
|
17
|
+
decision = decide_after_failure(
|
|
18
|
+
attempt=1,
|
|
19
|
+
max_retries=3,
|
|
20
|
+
failed_step=None,
|
|
21
|
+
loop_detected=False,
|
|
22
|
+
llm_error=False,
|
|
23
|
+
spec_patched=False,
|
|
24
|
+
)
|
|
25
|
+
assert decision == Decision(Action.ABORT, "Cannot identify failed step — giving up")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_decide_retry_on_transient_llm_error_before_limit():
|
|
29
|
+
decision = decide_after_failure(
|
|
30
|
+
attempt=1,
|
|
31
|
+
max_retries=3,
|
|
32
|
+
failed_step="sync_env",
|
|
33
|
+
loop_detected=False,
|
|
34
|
+
llm_error=True,
|
|
35
|
+
spec_patched=False,
|
|
36
|
+
)
|
|
37
|
+
assert decision == Decision(Action.RETRY, "LLM error — retrying may help if transient")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_decide_abort_on_llm_error_at_retry_limit():
|
|
41
|
+
decision = decide_after_failure(
|
|
42
|
+
attempt=3,
|
|
43
|
+
max_retries=3,
|
|
44
|
+
failed_step="sync_env",
|
|
45
|
+
loop_detected=False,
|
|
46
|
+
llm_error=True,
|
|
47
|
+
spec_patched=False,
|
|
48
|
+
)
|
|
49
|
+
assert decision == Decision(Action.ABORT, "LLM error — retrying may help if transient")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_decide_abort_when_retry_budget_exhausted():
|
|
53
|
+
decision = decide_after_failure(
|
|
54
|
+
attempt=3,
|
|
55
|
+
max_retries=3,
|
|
56
|
+
failed_step="sync_env",
|
|
57
|
+
loop_detected=False,
|
|
58
|
+
llm_error=False,
|
|
59
|
+
spec_patched=False,
|
|
60
|
+
)
|
|
61
|
+
assert decision == Decision(Action.ABORT, "Max retries (3) exhausted for 'sync_env'")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_decide_retry_when_spec_was_patched():
|
|
65
|
+
decision = decide_after_failure(
|
|
66
|
+
attempt=1,
|
|
67
|
+
max_retries=3,
|
|
68
|
+
failed_step="sync_env",
|
|
69
|
+
loop_detected=False,
|
|
70
|
+
llm_error=False,
|
|
71
|
+
spec_patched=True,
|
|
72
|
+
)
|
|
73
|
+
assert decision == Decision(Action.RETRY, "Spec patched — re-running migration")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_decide_skip_when_patch_not_applicable():
|
|
77
|
+
decision = decide_after_failure(
|
|
78
|
+
attempt=1,
|
|
79
|
+
max_retries=3,
|
|
80
|
+
failed_step="sync_env",
|
|
81
|
+
loop_detected=False,
|
|
82
|
+
llm_error=False,
|
|
83
|
+
spec_patched=False,
|
|
84
|
+
)
|
|
85
|
+
assert decision == Decision(Action.SKIP, "LLM did not produce an applicable patch — skipping step")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_format_decision_message_uses_action_and_reason():
|
|
89
|
+
msg = format_decision_message(Decision(Action.RETRY, "Spec patched"), "sync_env")
|
|
90
|
+
assert "RETRY" in msg
|
|
91
|
+
assert "Spec patched" in msg
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import textwrap
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
|
|
6
|
+
from redeploy.iac.parsers.compose import DockerComposeParser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_compose_parse_contract_golden(tmp_path):
|
|
10
|
+
compose = tmp_path / "docker-compose.yml"
|
|
11
|
+
compose.write_text(
|
|
12
|
+
textwrap.dedent(
|
|
13
|
+
"""
|
|
14
|
+
version: "3.9"
|
|
15
|
+
env_file:
|
|
16
|
+
- .env.root
|
|
17
|
+
services:
|
|
18
|
+
web:
|
|
19
|
+
image: nginx:1.27
|
|
20
|
+
command: ["nginx", "-g", "daemon off;"]
|
|
21
|
+
restart: unless-stopped
|
|
22
|
+
deploy:
|
|
23
|
+
replicas: 2
|
|
24
|
+
ports:
|
|
25
|
+
- "127.0.0.1:8080:80/tcp"
|
|
26
|
+
- target: 443
|
|
27
|
+
published: 8443
|
|
28
|
+
protocol: tcp
|
|
29
|
+
volumes:
|
|
30
|
+
- ./html:/usr/share/nginx/html:ro
|
|
31
|
+
- type: volume
|
|
32
|
+
source: cache
|
|
33
|
+
target: /var/cache/nginx
|
|
34
|
+
networks: [frontend, backend]
|
|
35
|
+
environment:
|
|
36
|
+
APP_ENV: prod
|
|
37
|
+
FEATURE_FLAG: "1"
|
|
38
|
+
env_file:
|
|
39
|
+
- .env.web
|
|
40
|
+
depends_on:
|
|
41
|
+
db:
|
|
42
|
+
condition: service_healthy
|
|
43
|
+
labels:
|
|
44
|
+
- traefik.enable=true
|
|
45
|
+
healthcheck:
|
|
46
|
+
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
|
47
|
+
secrets:
|
|
48
|
+
- source: tls_cert
|
|
49
|
+
|
|
50
|
+
db:
|
|
51
|
+
image: postgres:16
|
|
52
|
+
build:
|
|
53
|
+
context: ./db
|
|
54
|
+
command: postgres -c max_connections=300
|
|
55
|
+
ports: ["5432"]
|
|
56
|
+
volumes:
|
|
57
|
+
- pgdata:/var/lib/postgresql/data
|
|
58
|
+
networks: [backend]
|
|
59
|
+
environment:
|
|
60
|
+
- POSTGRES_DB=app
|
|
61
|
+
- POSTGRES_PASSWORD=secret
|
|
62
|
+
env_file:
|
|
63
|
+
- .env.db
|
|
64
|
+
depends_on: [redis]
|
|
65
|
+
labels:
|
|
66
|
+
com.example.role: db
|
|
67
|
+
healthcheck: pg_isready -U postgres
|
|
68
|
+
secrets:
|
|
69
|
+
- db_password
|
|
70
|
+
|
|
71
|
+
networks:
|
|
72
|
+
frontend: {}
|
|
73
|
+
backend: {}
|
|
74
|
+
|
|
75
|
+
volumes:
|
|
76
|
+
pgdata: {}
|
|
77
|
+
cache: {}
|
|
78
|
+
|
|
79
|
+
secrets:
|
|
80
|
+
tls_cert:
|
|
81
|
+
file: ./secrets/tls.crt
|
|
82
|
+
db_password:
|
|
83
|
+
file: ./secrets/db_password.txt
|
|
84
|
+
"""
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
spec = DockerComposeParser().parse(compose)
|
|
89
|
+
|
|
90
|
+
actual = {
|
|
91
|
+
"services": [asdict(svc) for svc in spec.services],
|
|
92
|
+
"images": spec.images,
|
|
93
|
+
"networks": spec.networks,
|
|
94
|
+
"env_files": spec.env_files,
|
|
95
|
+
"secrets": spec.secrets_referenced,
|
|
96
|
+
"runtime_hints": spec.runtime_hints,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
expected = {
|
|
100
|
+
"services": [
|
|
101
|
+
{
|
|
102
|
+
"name": "web",
|
|
103
|
+
"image": "nginx:1.27",
|
|
104
|
+
"ports": [
|
|
105
|
+
{
|
|
106
|
+
"container": 80,
|
|
107
|
+
"host": 8080,
|
|
108
|
+
"protocol": "tcp",
|
|
109
|
+
"host_ip": "127.0.0.1",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"container": 443,
|
|
113
|
+
"host": 8443,
|
|
114
|
+
"protocol": "tcp",
|
|
115
|
+
"host_ip": "0.0.0.0",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
"volumes": [
|
|
119
|
+
{
|
|
120
|
+
"target": "/usr/share/nginx/html",
|
|
121
|
+
"source": "./html",
|
|
122
|
+
"source_type": "volume",
|
|
123
|
+
"read_only": True,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"target": "/var/cache/nginx",
|
|
127
|
+
"source": "cache",
|
|
128
|
+
"source_type": "volume",
|
|
129
|
+
"read_only": False,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
"env": {"APP_ENV": "prod", "FEATURE_FLAG": "1"},
|
|
133
|
+
"env_files": [".env.web"],
|
|
134
|
+
"networks": ["frontend", "backend"],
|
|
135
|
+
"depends_on": ["db"],
|
|
136
|
+
"healthcheck": "curl -f http://localhost/health",
|
|
137
|
+
"restart": "unless-stopped",
|
|
138
|
+
"command": "nginx -g daemon off;",
|
|
139
|
+
"build_context": None,
|
|
140
|
+
"replicas": 2,
|
|
141
|
+
"labels": {"traefik.enable": "true"},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "db",
|
|
145
|
+
"image": "postgres:16",
|
|
146
|
+
"ports": [
|
|
147
|
+
{
|
|
148
|
+
"container": 5432,
|
|
149
|
+
"host": None,
|
|
150
|
+
"protocol": "tcp",
|
|
151
|
+
"host_ip": "0.0.0.0",
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"volumes": [
|
|
155
|
+
{
|
|
156
|
+
"target": "/var/lib/postgresql/data",
|
|
157
|
+
"source": "pgdata",
|
|
158
|
+
"source_type": "volume",
|
|
159
|
+
"read_only": False,
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
"env": {"POSTGRES_DB": "app", "POSTGRES_PASSWORD": "secret"},
|
|
163
|
+
"env_files": [".env.db"],
|
|
164
|
+
"networks": ["backend"],
|
|
165
|
+
"depends_on": ["redis"],
|
|
166
|
+
"healthcheck": "pg_isready -U postgres",
|
|
167
|
+
"restart": None,
|
|
168
|
+
"command": "postgres -c max_connections=300",
|
|
169
|
+
"build_context": "./db",
|
|
170
|
+
"replicas": 1,
|
|
171
|
+
"labels": {"com.example.role": "db"},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
"images": ["nginx:1.27", "postgres:16"],
|
|
175
|
+
"networks": ["frontend", "backend"],
|
|
176
|
+
"env_files": [".env.web", ".env.db", ".env.root"],
|
|
177
|
+
"secrets": ["tls_cert", "db_password"],
|
|
178
|
+
"runtime_hints": ["compose-version: 3.9"],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
assert actual == expected
|