Skip to content

feat: [#174] implement provision console command#175

Merged
josecelano merged 19 commits into
mainfrom
174-implement-provision-console-command
Nov 18, 2025
Merged

feat: [#174] implement provision console command#175
josecelano merged 19 commits into
mainfrom
174-implement-provision-console-command

Conversation

@josecelano

@josecelano josecelano commented Nov 17, 2025

Copy link
Copy Markdown
Member

Summary

Implements the provision console command for the Torrust Tracker Deployer, enabling infrastructure provisioning through the CLI.

Changes

New Command Implementation

  • Provision Command: Complete infrastructure provisioning workflow
    • OpenTofu template rendering
    • Infrastructure initialization, validation, planning, and application
    • Instance information retrieval
    • Ansible template rendering with runtime IP
    • SSH connectivity verification
    • Cloud-init completion waiting

Architecture Improvements

  • State Validation Layer: Moved from presentation to application layer
  • Dependency Injection: Injected TemplateManager as global dependency
  • Repository Pattern: Replaced direct Repository with RepositoryFactory
  • Dependency Building: Extracted methods for creating OpenTofu and Ansible clients

Code Quality

  • Refactored E2E Tests: Introduced PreflightCleanupContext for minimal context in cleanup operations
  • Import Improvements: Simplified imports using short type references
  • Variable Naming: Clear distinction between any_env (type-erased) and environment (type-safe)

Testing

  • ✅ All unit tests passing (1141 tests)
  • ✅ All integration tests passing
  • ✅ All E2E tests passing (provision/destroy + configuration)
  • ✅ Documentation builds successfully

Remaining Tasks

  • Manual CLI verification (to be done after PR review)

Related Issue

Closes #174

- 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
josecelano marked this pull request as ready for review November 18, 2025 07:43
@josecelano josecelano self-assigned this Nov 18, 2025
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/...)
@josecelano

Copy link
Copy Markdown
Member Author

ACK e2cf68f

@josecelano
josecelano merged commit f8d82b3 into main Nov 18, 2025
34 checks passed
@josecelano
josecelano deleted the 174-implement-provision-console-command branch April 15, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Provision Console Command

1 participant