Accepted
2025-10-15
The project uses multiple external CLI tools (SSH, Docker, Ansible, LXD, OpenTofu) through thin wrapper clients. These wrappers are currently scattered across two locations:
src/shared/: SSH, Docker, and the CommandExecutor base abstractionsrc/infrastructure/external_tools/: Ansible, LXD, OpenTofu (with their template renderers)
This split was based on an assumption about reusability:
- SSH and Docker were placed in
shared/because they were thought to be generic and reusable - Ansible, LXD, and OpenTofu were placed in
infrastructure/because they seemed application-specific
However, this organization creates several issues:
- Inconsistent Discoverability: External tool wrappers are split across modules, making them hard to find
- Semantic Confusion:
src/shared/mixes pure utilities (Clock, Username) with infrastructure adapters (SSH, Docker) - Artificial Split: The distinction is based on assumed reusability rather than nature of the code
- Pattern Inconsistency: All wrappers follow the same pattern (thin clients using CommandExecutor) but are organized differently
All external tool wrappers share these characteristics:
- Thin adapters: Minimal business logic, just command builders
- Common pattern: Use CommandExecutor for actual command execution
- Infrastructure concerns: All interact with external systems
- Consistent API: Client struct with domain-specific methods returning typed results
The key insight is that these are all infrastructure adapters, not pure utilities, regardless of whether they're currently used in production code or just tests.
Consolidate all external tool adapters into a new top-level src/adapters/ module.
This creates a three-tier semantic organization:
src/
├── adapters/ # External tool adapters (thin wrappers, reusable)
│ ├── ansible/ # Ansible CLI wrapper
│ ├── docker/ # Docker CLI wrapper
│ ├── lxd/ # LXD CLI wrapper
│ ├── ssh/ # SSH client wrapper
│ └── tofu/ # OpenTofu CLI wrapper
├── infrastructure/ # Application-specific infrastructure
│ └── external_tools/ # Application-specific logic for external tools
│ ├── ansible/
│ │ └── template/ # Ansible template rendering (app-specific)
│ ├── lxd/ # LXD-specific logic if any
│ └── tofu/
│ └── template/ # OpenTofu template rendering (app-specific)
│ ├── remote_actions/ # High-level remote operations
│ └── persistence/ # State management
├── shared/ # Pure utilities (Clock, Username, error types)
│ ├── command/ # CommandExecutor (used by all adapters)
│ └── ... # Port checkers, error types, etc.
└── ... # domain/, application/, testing/
src/adapters/ - Infrastructure adapters for external tools:
- Thin wrapper clients (AnsibleClient, DockerClient, etc.)
- Minimal business logic
- Generic, reusable across projects
- Examples: SSH authentication, Docker commands, LXD operations
src/infrastructure/external_tools/ - Application-specific logic for external tools:
- Ansible template renderers and inventory builders
- OpenTofu template renderers and cloud-init generation
- Application-specific orchestration and configuration
- Tightly coupled to this project's requirements
src/shared/ - Pure utilities with no external dependencies:
- CommandExecutor (used by all adapters)
- Clock, Username, error types
- Port checkers
- Generic abstractions
| From | To |
|---|---|
src/shared/ssh/ |
src/adapters/ssh/ |
src/shared/docker/ |
src/adapters/docker/ |
src/shared/command/ |
STAY (pure utility) |
src/infrastructure/external_tools/ansible/adapter/ |
src/adapters/ansible/ |
src/infrastructure/external_tools/lxd/adapter/ |
src/adapters/lxd/ |
src/infrastructure/external_tools/tofu/adapter/ |
src/adapters/tofu/ |
src/infrastructure/external_tools/ansible/template/ |
STAY (application-specific) |
src/infrastructure/external_tools/tofu/template/ |
STAY (application-specific) |
Considered alternatives:
src/clients/: Matches class names (*Client), but implies network/API clients specificallysrc/packages/: Suggests future extraction, but conflicts with workspace-levelpackages/directorysrc/infrastructure/adapters/: Keeps within DDD layers, but doesn't emphasize reusabilitysrc/wrappers/orsrc/tools/: Too informal or vague
Why "adapters" is best:
- ✅ Well-known design pattern (Adapter/Wrapper pattern)
- ✅ Common in port-adapter architecture (Hexagonal Architecture)
- ✅ Top-level placement signals reusability intent
- ✅ Clear semantic: adapts external tools to project's needs
- ✅ Future-proof: easy to extract to
packages/adapters/workspace package later
- Improved Discoverability: All external tool adapters in one predictable location
- Semantic Clarity: Clear distinction between pure utilities, adapters, and application infrastructure
- Consistent Conventions: Easier to apply uniform documentation, testing, and error handling patterns
- Code Sharing: Common adapter patterns can be extracted and shared more easily
- Better Mental Model: "Adapters wrap external tools" is clearer than current split
- Future Extraction Ready: Clean separation makes it easier to extract to workspace packages when needed
- Alignment with Architecture Patterns: Follows port-adapter (Hexagonal) architecture principles
- Migration Effort: Need to move files and update all imports across the codebase
- New Structure: Contributors need to learn the new organization
- Breaking Changes: External consumers (if any) would need to update imports
- Git History: File moves may complicate
git blame(mitigated bygit log --follow)
- Partial Module Reorganization: Only adapters move from
external_tools/*/adapter/to top-leveladapters/- Template logic stays in
infrastructure/external_tools/*/template/(application-specific) - Creates clear distinction between generic adapters and application-specific usage
external_tools/becomes purely about application-specific tool configuration
- Template logic stays in
Risk: Missing imports during migration
Mitigation: Use cargo check and cargo test to catch all broken imports
Risk: Test integration complexity
Mitigation: Integration tests (like tests/ssh_client_integration.rs) stay in tests/ directory
Risk: Confusion with workspace packages
Mitigation: Clear documentation distinguishing src/adapters/ (internal) from packages/ (workspace)
Rejected - Maintains semantic confusion between pure utilities and infrastructure adapters.
Rejected - Mixes generic reusable adapters with application-specific infrastructure, doesn't signal reusability.
Rejected - Premature abstraction. No other Torrust projects currently need these adapters. Would add complexity (versioning, publishing, dependency management) without proven need. Decision: wait for concrete reuse case before extracting to workspace package.
Considered viable - Would work equally well, matches class naming convention (*Client). Rejected in favor of "adapters" for clearer architectural semantics and broader applicability (adapters can include non-client patterns).
Rejected - Using src/infrastructure/adapters/ keeps them in DDD infrastructure layer but doesn't emphasize reusability as clearly as top-level placement.
- Repository Rename to Deployer - Project identity and scope
- LXD VM over Containers - LXD adapter usage
- Tera Minimal Templating Strategy - Template separation rationale
- Hexagonal Architecture: Ports and Adapters pattern by Alistair Cockburn
- DDD Infrastructure Layer: Domain-Driven Design by Eric Evans
- Adapter Pattern: Gang of Four Design Patterns
- Module Organization Guide - Project organization conventions
- Development Principles - Maintainability and clarity principles