feat: [#174] implement provision console command#175
Merged
Conversation
- Create detailed specification for provision console command - Document all 7 phases including manual E2E testing - Add architecture requirements and acceptance criteria - Update roadmap to reference issue #174
…nstructor Move AnsibleClient from constructor dependency to local creation within wait_for_readiness method. This is the first step in separating global dependencies (Clock, Repository) from runtime/environment-specific dependencies (AnsibleClient, TofuTemplateRenderer, etc.). Benefits: - Reduces presentation layer complexity by avoiding environment-specific dependency construction in controllers - Creates environment-specific services in application layer where environment context is available - Keeps environment path derivation logic centralized Trade-offs: - Harder to mock environment-specific dependencies in tests - Can be mitigated in future with factory pattern if needed Changes: - Remove ansible_client field from ProvisionCommandHandler - Create AnsibleClient locally in wait_for_readiness with environment's working directory - Update test builders and E2E tests to remove ansible_client injection
…n up tests Make all ProvisionCommandHandler struct fields private to enforce proper encapsulation. Remove test that relied on internal field access. Changes: - Change all pub(crate) fields to private in ProvisionCommandHandler - Remove it_should_create_provision_command_handler_with_all_dependencies test (relied on accessing private fields) - Add #[allow(dead_code)] to builder methods (will be used in future tests) - Add #[cfg(test)] to test modules for proper scoping
… wait_for_readiness Move AnsibleClient creation from wait_for_readiness method to the start of execute_provisioning_with_tracking. This improves the design by: - Creating environment-specific dependency at the workflow level where environment context is available - Reducing method parameters (ansible_working_dir -> ansible_client) - Making dependency flow more explicit and clearer - Keeping environment-specific service creation centralized The client is now created once with environment.ansible_build_dir() and passed to methods that need it, rather than passing paths and creating clients in multiple places.
…onstructor Move OpenTofuClient from constructor dependency to local creation within execute_provisioning_with_tracking method. This continues the pattern of separating global dependencies from runtime/environment-specific ones. Changes: - Remove opentofu_client field from ProvisionCommandHandler struct - Create OpenTofuClient locally with environment.tofu_build_dir() - Convert create_instance() and get_instance_info() to static methods that accept opentofu_client as parameter - Update test builders and E2E tests to remove opentofu_client injection - Add OpenTofuClient import to use type directly Benefits: - OpenTofuClient now created with environment-specific working directory - Clearer separation between global and runtime dependencies - Reduced constructor complexity (one less parameter) - Static helper methods are more testable independently
Add TemplateManager to ProvisionCommandHandler constructor as a global dependency. This is a preparatory step for removing TofuTemplateRenderer and AnsibleTemplateRenderer from the constructor. The TemplateManager is a truly global service that doesn't depend on environment context, making it suitable for constructor injection. It will be used to build template renderers locally within the command handler when environment context is available. Changes: - Add template_manager parameter to ProvisionCommandHandler::new() - Store as _template_manager field (prefixed with _ as currently unused) - Update test builders to pass TemplateManager - Update E2E tests to pass TemplateManager from test context Next step: Use TemplateManager to build TofuTemplateRenderer and AnsibleTemplateRenderer locally with environment-specific data.
…er constructor Remove TofuTemplateRenderer and AnsibleTemplateRenderer from constructor and create them locally within execute_provisioning_with_tracking using environment-specific context. This completes the refactoring to separate global dependencies from runtime/environment-specific dependencies. Changes: - Remove tofu_template_renderer and ansible_template_renderer fields - Remove underscore prefix from template_manager (now actively used) - Create TofuTemplateRenderer locally with environment data: - template_manager, build_dir, ssh_credentials, instance_name, profile_name - Create AnsibleTemplateRenderer locally with environment data: - build_dir, template_manager - Pass renderers as parameters to helper methods - Remove renderer construction from test builders and E2E tests - Simplify constructor to only accept truly global dependencies Constructor now only accepts global dependencies: - Clock (system time) - TemplateManager (global template service) - Repository (persistence) All environment-specific services are created within the workflow where environment context is available. This keeps the presentation layer thin and centralizes environment-specific logic in the application layer.
…ctor Replace environment-specific Repository with global RepositoryFactory in ProvisionCommandHandler constructor. Create repository locally within execute() method using environment's data directory. This completes the separation of global vs environment-specific dependencies. Changes: - Replace repository field with repository_factory in struct - Replace repository parameter with repository_factory in constructor - Create repository locally in execute() using: repository_factory.create(environment.data_dir()) - Pass repository to all persistence operations (save_provisioning, save_provisioned, save_provision_failed) - Update test builders to inject Arc<RepositoryFactory> - Update E2E container to wrap RepositoryFactory in Arc - Remove repository creation from E2E test helper Constructor now only has truly global dependencies: - Clock (system time service) - TemplateManager (global template service) - RepositoryFactory (creates environment-specific repositories) All environment-specific services are created at runtime when environment context (data_dir, build_dir, ssh_credentials, etc.) is available.
…andHandler Extract dependency creation logic into separate methods for better organization: - Extract build_infrastructure_dependencies() from provision_infrastructure() * Creates TofuTemplateRenderer and OpenTofuClient * Returns tuple of Arc-wrapped dependencies - Extract build_configuration_dependencies() from prepare_for_configuration() * Creates AnsibleClient and AnsibleTemplateRenderer * Returns tuple of Arc-wrapped dependencies Benefits: - Clear separation between dependency setup and workflow execution - Consistent pattern across both provisioning phases - Improved readability with focused, single-responsibility methods - Each method now has distinct purpose: build deps vs execute workflow This completes the ProvisionCommandHandler refactoring series: - Reduced constructor from 7 to 3 global dependencies - Extracted workflow orchestration methods - Separated dependency creation from execution - Improved code organization and maintainability
…mprovements - Add provision controller following destroy pattern - Implement ExecutionContext pattern for dependency injection - Add TemplateManager to Container and ExecutionContext - Use domain's try_into_created() for state validation - Simplify imports (remove unnecessary long paths) - Follow DDD layer separation (presentation/application/domain) Architecture improvements: - TemplateManager now managed by Container (proper DI) - Removed application layer validation from presentation layer - State validation delegated to domain layer via try_into_created() - Simplified import paths where disambiguation not needed All tests passing (1141/1141)
…on layer - Refactored ProvisionCommandHandler to accept EnvironmentName instead of Environment<Created>, matching DestroyCommandHandler pattern - Moved environment loading and state validation into application layer - Added StateTransition error variant with comprehensive help text - Simplified presentation layer to only handle user interaction - Fixed E2E test initialization order bug where preflight cleanup was deleting newly created environment files - Renamed temp_context to cleanup_context for clarity This change properly separates concerns following DDD principles: - Application layer now owns all business logic - Presentation layer delegates to application layer - State validation happens in application layer using try_into_created() - Type safety preserved through compile-time state transitions
…or E2E preflight cleanup - Create PreflightCleanupContext with only 5 essential fields needed for cleanup - Eliminates wasteful TestContext creation just to delete it immediately - Reduces coupling between preflight cleanup and test infrastructure - Use environment-specific paths (environment.build_dir(), environment.templates_dir()) - Apply pattern to both e2e_provision_and_destroy_tests and e2e_tests_full binaries - Remove redundant inline comments (struct fields already documented) This improves efficiency and follows the principle of creating focused types with minimal required data instead of reusing heavy objects.
- Add RepositoryError and EnvironmentName to import statements - Use short type names instead of fully qualified paths - Improves code readability and follows Rust conventions
Improve clarity in state handling by using distinct variable names: - any_env: Type-erased runtime state (AnyEnvironmentState) - environment: Type-safe compile-time state (Environment<S>) This makes the type restoration pattern more explicit and prevents confusion about state conversions. The try_into_created() method validates the state rather than forcing a conversion. Changes: - ProvisionCommandHandler: Rename environment -> any_env before try_into_created() - DestroyCommandHandler: Rename environment -> any_env before state matching - Update comments to clarify type erasure and restoration phases
…layers
Fixed four related path handling bugs that caused environment files
to be created at incorrect locations:
Production Code Bugs (Domain/Application/Presentation):
1. Domain Layer - Add with_working_dir() methods for absolute paths
- Added InternalConfig::with_working_dir() (lines 96-138)
- Added EnvironmentContext::with_working_dir() (lines 165-214)
- Added Environment::with_working_dir() (lines 262-325)
- Creates absolute paths: {working_dir}/data/{env}, {working_dir}/build/{env}
2. Application Layer - Update CreateCommandHandler signature
- Added working_dir parameter to execute() method
- Calls Environment::with_working_dir() instead of Environment::new()
- Updated handler.rs and mod.rs documentation
3. Presentation Layer - Fix repository factory in create controller
- Fixed src/presentation/controllers/create/subcommands/environment/handler.rs
- Changed: repository_factory.create(working_dir)
- To: repository_factory.create(working_dir.join("data"))
- Lines 346-348, 394
4. Presentation Layer - Fix repository factory in destroy controller
- Fixed src/presentation/controllers/destroy/handler.rs
- Applied identical fix as create controller
- Lines 225-226
Test Infrastructure Bug:
5. Testing - Fix TestContext::create_repository() absolute path
- Fixed src/testing/e2e/context.rs line 449
- Changed: PathBuf::from("data")
- To: self.config.project_root.join("data")
- Ensures E2E tests create files at correct absolute paths
Root Cause:
Repository factory expects absolute BASE data directory in all contexts.
Missing working directory context caused relative path generation.
Correct Path Structure:
- Production: working_dir.join("data") → {base}/data/{env_name}/environment.json
- Tests: project_root.join("data") → {base}/data/{env_name}/environment.json
Updated Files (14 total):
- Domain: internal_config.rs, context.rs, mod.rs
- Application: handler.rs, mod.rs, integration.rs
- Presentation: create/handler.rs, destroy/handler.rs
- Presentation Tests: create/tests.rs, environment.rs
- Testing: context.rs, run_create_command.rs, e2e_tests_full.rs
- Test Support: assertions.rs
Verification:
- All 1,468 tests passing
- Pre-commit checks passed (3m 39s)
- E2E provision and destroy tests passed
- E2E configuration tests passed
Fixed Bug #5: Provision controller repository path (same as bugs #2/#3) The provision controller was passing working_dir instead of working_dir.join("data") to repository factory. Changes: - Added: let data_dir = working_dir.join("data"); - Changed repository_factory.create() to use data_dir Verified with complete manual E2E test (Phase 7) and full E2E test suite
Fixed Bug #6: Templates created at wrong location Problem: - Container created global TemplateManager with hardcoded "templates" path - Templates written to ./templates/ instead of data/{env}/templates/ - Violated multi-environment isolation Solution: - Removed TemplateManager from Container (no longer global) - Removed from ExecutionContext and all handlers - Create per-environment in build_infrastructure_dependencies() - Create per-environment in build_configuration_dependencies() - Uses environment.templates_dir() → data/{env}/templates/ Additional improvements: - Converted build methods to static functions (clippy unused_self) - Boxed large error variant (clippy result_large_err) Files modified: - src/application/command_handlers/provision/handler.rs - src/application/command_handlers/provision/tests/builders.rs - src/bootstrap/container.rs - src/domain/template/embedded.rs - src/presentation/controllers/provision/errors.rs - src/presentation/controllers/provision/handler.rs - src/presentation/controllers/provision/mod.rs - src/presentation/dispatch/context.rs - src/testing/e2e/tasks/virtual_machine/run_provision_command.rs Benefits: - Proper multi-environment isolation - Templates cleaned up with environment - Aligns with design intent - Reduced error variant size Verified: - All 1,468 tests passing - Manual test: templates at data/{env}/templates/ - Pre-commit checks: 100% passed (3m 46s) - Full E2E test: passed (66s)
Bug #7: E2E test was creating environment.json at repository root instead of in data/ directory. Root cause: run_create_command was passing working_dir directly to RepositoryFactory.create() instead of working_dir.join("data"). Impact: - E2E tests created ./e2e-full/environment.json at repo root - This caused cspell errors (username in paths) - Files were tracked by git (not in .gitignore) Fix: Pass working_dir.join("data") to repository factory, matching the pattern used in provision controller (Bug #5 fix). This is the same category of bug as #2, #3, and #5 - passing working_dir instead of data_dir to RepositoryFactory.
josecelano
marked this pull request as ready for review
November 18, 2025 07:43
Ensure consistent path formatting across all code paths:
- InternalConfig::new() now uses PathBuf::from(".").join() pattern
- E2E tests use PathBuf::from(".") instead of current_dir()
- All environment states use ./data/... and ./build/... format
- Updated 9 doctest assertions to expect new format
- Added ADR documenting the relative paths decision
This eliminates the inconsistency where different constructors
produced different path formats (data/... vs ./data/...)
Member
Author
|
ACK e2cf68f |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the
provisionconsole command for the Torrust Tracker Deployer, enabling infrastructure provisioning through the CLI.Changes
New Command Implementation
Architecture Improvements
TemplateManageras global dependencyRepositorywithRepositoryFactoryCode Quality
PreflightCleanupContextfor minimal context in cleanup operationsany_env(type-erased) andenvironment(type-safe)Testing
Remaining Tasks
Related Issue
Closes #174