This document describes the refactored code architecture for the Attack Range controller.
The Attack Range controller has been refactored from a single monolithic file (~2459 lines) into a modular, well-structured architecture following SOLID principles and Python best practices.
attack_range/
├── __init__.py
├── attack_range_controller.py # Main orchestrator (slim, ~260 lines)
├── ARCHITECTURE.md # This file
├── managers/ # Manager modules
│ ├── __init__.py
│ ├── config_manager.py # Configuration management
│ ├── terraform_manager.py # Terraform operations
│ ├── ansible_manager.py # Ansible operations
│ ├── ssh_manager.py # SSH key management
│ └── backend_manager.py # Remote backend management
└── cloud_providers/ # Cloud provider implementations
├── __init__.py
├── base_provider.py # Abstract base class
├── aws_provider.py # AWS-specific operations
├── azure_provider.py # Azure-specific operations
└── gcp_provider.py # GCP-specific operations
Responsibility: Orchestration and workflow coordination
The main controller is now a slim orchestrator that:
- Initializes all managers and cloud providers
- Coordinates the build and destroy workflows
- Delegates specific tasks to appropriate managers
- Maintains no business logic itself
Key Methods:
build(): Orchestrates the infrastructure build processdestroy(): Orchestrates the infrastructure teardown process
Managers handle specific aspects of the attack range infrastructure:
Responsibility: Configuration validation and management
- Validates zeek configuration for cloud providers
- Generates and manages attack range IDs
- Handles config file operations (save, load, remove)
- Manages config folder and file discovery
Responsibility: Terraform operations
- Initializes Terraform
- Applies/destroys Terraform configurations
- Retrieves Terraform outputs
- Manages Terraform variables
Responsibility: Ansible operations
- Updates inventory files
- Generates and updates playbooks
- Runs Ansible playbooks
- Installs Ansible Galaxy roles
- Waits for SSH availability
- Manages VPN connection prompts
Responsibility: SSH key management
- Generates SSH key pairs
- Uploads keys to cloud providers (when needed)
- Cleans up SSH keys
- Updates configuration with key paths
Responsibility: Terraform remote backend management
- Sets up remote state storage (S3/Azure Storage/GCS)
- Creates necessary resources (buckets, tables, containers)
- Cleans up remote backend resources
- Updates backend configuration files
Cloud provider classes implement cloud-specific operations:
Responsibility: Define cloud provider interface
Abstract base class defining the contract for all cloud providers:
get_region(): Get cloud provider region/locationsanitize_name(): Sanitize resource namescheck_backend_exists(): Check if backend storage existscreate_backend(): Create backend storagedelete_backend(): Delete backend storageimport_ssh_key(): Import SSH key (if applicable)delete_ssh_key(): Delete SSH key (if applicable)write_backend_config(): Create/update backend configuration fileget_backend_params(): Get backend parameters for the current provider
Responsibility: AWS-specific operations
- Manages S3 buckets for state storage (with S3 native locking via use_lockfile)
- Imports/deletes EC2 key pairs
- Sanitizes names for S3 requirements
Responsibility: Azure-specific operations
- Manages Storage Accounts and Containers
- Creates/deletes resource groups
- Handles Azure-specific naming constraints
- Note: SSH keys are used directly in VM config
Responsibility: GCP-specific operations
- Manages GCS buckets for state storage
- Handles GCP project and region configuration
- Sanitizes names for GCS requirements
- Note: SSH keys are used directly in VM config
Each component has a single, well-defined responsibility:
- Controllers orchestrate workflows
- Managers handle specific domains (config, terraform, ansible, etc.)
- Providers abstract cloud-specific operations
Each class has one reason to change:
ConfigManagerchanges only when config logic changesAWSProviderchanges only when AWS operations change- etc.
The system is open for extension but closed for modification:
- New cloud providers can be added by extending
BaseCloudProvider - No need to modify existing code
High-level modules depend on abstractions:
AttackRangeControllerdepends on manager interfaces- Managers depend on cloud provider abstractions
- Concrete implementations are injected
Interfaces are client-specific:
BaseCloudProviderdefines only methods all providers need- Providers can add cloud-specific methods as needed
- Smaller files: Each file is focused and easier to understand
- Clear boundaries: Changes are localized to specific modules
- Reduced coupling: Components interact through well-defined interfaces
- Unit testing: Each manager/provider can be tested independently
- Mocking: Interfaces make it easy to mock dependencies
- Isolation: Test failures are easier to diagnose
- New cloud providers: Add support by implementing
BaseCloudProvider - New features: Add managers without modifying existing code
- Custom workflows: Compose managers in different ways
- Clear naming: Class and method names describe their purpose
- Logical organization: Related functionality is grouped together
- Documentation: Each module has clear docstrings
- Composable: Managers can be reused in different contexts
- Modular: Import only what you need
- Flexible: Easy to create specialized workflows
The original attack_range_controller.py has been backed up to:
attack_range/attack_range_controller.py.bak
The public API of AttackRangeController remains unchanged:
__init__(config, config_path)build()destroy()
Existing code using the controller should work without modifications.
All internal methods (prefixed with _) have been moved to appropriate managers.
If you have code directly calling these methods, update it to use the managers.
from attack_range.attack_range_controller import AttackRangeController
# Initialize controller with config
config = {...} # Your configuration dictionary
controller = AttackRangeController(config, config_path="path/to/config.yml")
# Build infrastructure
controller.build()
# Destroy infrastructure
controller.destroy()Test each manager and provider independently:
# Example: Test ConfigManager
from attack_range.managers.config_manager import ConfigManager
def test_generate_attack_range_id():
config = {"general": {}}
manager = ConfigManager(config, None, "/tmp", mock_logger)
attack_range_id = manager.generate_and_set_attack_range_id()
assert config["general"]["attack_range_id"] == attack_range_idTest manager interactions:
# Example: Test backend setup workflow
def test_backend_setup():
# Mock cloud provider
mock_provider = Mock(spec=BaseCloudProvider)
backend_manager = BackendManager(terraform_dir, config, config_path, mock_provider, logger)
# Test backend setup
backend_manager.setup_remote_backend()
# Verify cloud provider methods were called
mock_provider.check_backend_exists.assert_called_once()Test complete workflows in test environments.
- Async Operations: Use async/await for parallel operations
- Event System: Add event hooks for custom actions
- Plugin System: Allow third-party extensions
- State Management: Add state tracking and rollback capabilities
- Monitoring: Add built-in metrics and monitoring
- Configuration Validation: JSON Schema validation for configs
- Create new provider class extending
BaseCloudProvider - Implement all abstract methods
- Add cloud-specific methods as needed
- Register in
_init_cloud_provider()method - Update documentation
If you see import errors, ensure:
- All
__init__.pyfiles are present - Python path includes the project root
- Dependencies are installed
If managers fail to initialize:
- Check config dictionary structure
- Verify directory paths exist
- Ensure cloud provider credentials are configured
If cloud operations fail:
- Verify credentials and permissions
- Check region/location configuration
- Review cloud provider error messages
When contributing to this codebase:
- Follow the existing architecture patterns
- Add new functionality to appropriate managers
- Create new managers for new domains
- Keep the controller thin and orchestrative
- Add tests for new components
- Update this documentation
For questions or issues with the architecture:
- Review this document
- Check inline code documentation
- Look at existing implementations as examples
- Consult the backup file for original implementation