Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ These principles should guide all development decisions, code reviews, and featu

4. **Before working with Tera templates**: Read [`docs/contributing/templates.md`](../docs/contributing/templates.md) for correct variable syntax - use `{{ variable }}` not `{ { variable } }`. Tera template files have the `.tera` extension.

5. **When handling errors in code**: Read [`docs/contributing/error-handling.md`](../docs/contributing/error-handling.md) for error handling principles. Prefer explicit enum errors over anyhow for better pattern matching and user experience. Make errors clear, include sufficient context for traceability, and ensure they are actionable with specific fix instructions.
5. **When adding new Ansible playbooks**: Read [`docs/contributing/templates.md`](../docs/contributing/templates.md) for the complete guide. **CRITICAL**: Static playbooks (without `.tera` extension) must be registered in `src/infrastructure/external_tools/ansible/template/renderer/mod.rs` in the `copy_static_templates` method, otherwise they won't be copied to the build directory and Ansible will fail with "playbook not found" error.

6. **Understanding expected errors**: Read [`docs/contributing/known-issues.md`](../docs/contributing/known-issues.md) for known issues and expected behaviors. Some errors that appear red in E2E test output (like SSH host key warnings) are normal and expected - not actual failures.
6. **When handling errors in code**: Read [`docs/contributing/error-handling.md`](../docs/contributing/error-handling.md) for error handling principles. Prefer explicit enum errors over anyhow for better pattern matching and user experience. Make errors clear, include sufficient context for traceability, and ensure they are actionable with specific fix instructions.

7. **Before making engineering decisions**: Document significant architectural or design decisions as Architectural Decision Records (ADRs) in `docs/decisions/`. Read [`docs/decisions/README.md`](../docs/decisions/README.md) for the ADR template and guidelines. This ensures decisions are properly documented with context, rationale, and consequences for future reference.
7. **Understanding expected errors**: Read [`docs/contributing/known-issues.md`](../docs/contributing/known-issues.md) for known issues and expected behaviors. Some errors that appear red in E2E test output (like SSH host key warnings) are normal and expected - not actual failures.

8. **When organizing code within modules**: Follow the module organization conventions in [`docs/contributing/module-organization.md`](../docs/contributing/module-organization.md). Use top-down organization with public items first, high-level abstractions before low-level details, and important responsibilities before secondary concerns like error types.
8. **Before making engineering decisions**: Document significant architectural or design decisions as Architectural Decision Records (ADRs) in `docs/decisions/`. Read [`docs/decisions/README.md`](../docs/decisions/README.md) for the ADR template and guidelines. This ensures decisions are properly documented with context, rationale, and consequences for future reference.

9. **When organizing code within modules**: Follow the module organization conventions in [`docs/contributing/module-organization.md`](../docs/contributing/module-organization.md). Use top-down organization with public items first, high-level abstractions before low-level details, and important responsibilities before secondary concerns like error types.

## 🧪 Build & Test

Expand Down
187 changes: 187 additions & 0 deletions docs/contributing/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,190 @@ instance_name = "{{ instance_name }}"
```

After applying the fix, manually correct any existing formatting issues in your `.tera` files by removing the spaces inside the curly braces.

## 📦 Adding New Ansible Playbooks

When adding new Ansible playbooks to the project, you need to understand the difference between **static playbooks** and **dynamic templates**, and follow the correct registration process.

### Static vs Dynamic Playbooks

#### Static Playbooks (No Tera Variables)

Static playbooks are standard Ansible YAML files that don't require variable substitution:

- **No `.tera` extension** - Just `.yml`
- **No Tera variables** - No `{{ variable }}` syntax needed
- **Direct copy** - Copied as-is from `templates/ansible/` to `build/` directory
- **Examples**: `install-docker.yml`, `wait-cloud-init.yml`, `configure-security-updates.yml`

#### Dynamic Playbooks (With Tera Variables)

Dynamic playbooks need runtime variable substitution:

- **`.tera` extension** - Named like `inventory.ini.tera`
- **Contains Tera variables** - Uses `{{ ansible_host }}`, `{{ username }}`, etc.
- **Rendered during execution** - Variables replaced at runtime
- **Examples**: Ansible inventory files with instance IPs

### Adding a Static Ansible Playbook

Follow these steps when adding a new static playbook:

#### Step 1: Create the Playbook File

Create your playbook in `templates/ansible/`:

```bash
# Example: Adding a new security configuration playbook
templates/ansible/configure-security-updates.yml
```

Write standard Ansible YAML with no Tera variables:

```yaml
---
- name: Configure automatic security updates
hosts: all
become: true
tasks:
- name: Install unattended-upgrades package
ansible.builtin.apt:
name: unattended-upgrades
state: present
update_cache: true
```

#### Step 2: Register in Template Copy List ⚠️ CRITICAL

**This is the step that's easy to miss!**

Add your playbook filename to the array in `src/infrastructure/external_tools/ansible/template/renderer/mod.rs`:

```rust
// Find the copy_static_templates method
async fn copy_static_templates(
&self,
template_manager: &TemplateManager,
destination_dir: &Path,
) -> Result<(), ConfigurationTemplateError> {
// ... existing code ...

// Copy all playbook files
for playbook in &[
"update-apt-cache.yml",
"install-docker.yml",
"install-docker-compose.yml",
"wait-cloud-init.yml",
"configure-security-updates.yml", // 👈 ADD YOUR PLAYBOOK HERE
] {
self.copy_static_file(template_manager, playbook, destination_dir)
.await?;
}

tracing::debug!(
"Successfully copied {} static template files",
6 // 👈 UPDATE THE COUNT: ansible.cfg + N playbooks
);

Ok(())
}
```

**Why This is Required:**

- The template system uses a **two-phase approach** (see `docs/technical/template-system-architecture.md`)
- **Phase 1**: Static file copying - requires explicit registration
- **Phase 2**: Dynamic rendering - automatic for `.tera` files
- Without registration, your playbook **will not be copied** to the build directory
- Ansible will fail with: `[ERROR]: the playbook: your-playbook.yml could not be found`

#### Step 3: Update the File Count

In the same method, update the debug log count:

```rust
tracing::debug!(
"Successfully copied {} static template files",
6 // ansible.cfg + 5 playbooks 👈 Update this comment
);
```

#### Step 4: Test Your Changes

Run E2E tests to verify the playbook is copied correctly:

```bash
# Run E2E config tests (faster, tests configuration only)
cargo run --bin e2e-config-tests

# Or run full E2E tests
cargo run --bin e2e-tests-full
```

If you forgot Step 2, you'll see this error:

```text
[ERROR]: the playbook: your-playbook.yml could not be found
```

#### Step 5: Use the Playbook in Your Code

Create a step that executes your playbook:

```rust
// In src/application/steps/system/your_step.rs
pub struct YourStep {
ansible_client: Arc<dyn AnsibleClient>,
}

impl YourStep {
pub async fn execute(&self) -> Result<(), YourStepError> {
self.ansible_client
.run_playbook("your-playbook.yml")
.await
.map_err(YourStepError::AnsibleExecution)?;

Ok(())
}
}
```

### Common Mistakes

❌ **Forgetting to register the playbook** in `copy_static_templates`

- Error: Playbook not found during execution
- Fix: Add playbook name to the array

❌ **Forgetting to update the file count** in debug log

- Error: Confusing logs during debugging
- Fix: Update the count comment

❌ **Using `.tera` extension for static playbooks**

- Error: Unnecessary complexity
- Fix: Only use `.tera` if you need variable substitution

❌ **Adding dynamic variables without `.tera` extension**

- Error: Variables not resolved, literal `{{ variable }}` in output
- Fix: Rename to `.yml.tera` and handle in rendering phase

### Quick Checklist

When adding a static Ansible playbook:

- [ ] Create `.yml` file in `templates/ansible/`
- [ ] Write standard Ansible YAML (no Tera variables)
- [ ] Add filename to `copy_static_templates` array in `src/infrastructure/external_tools/ansible/template/renderer/mod.rs`
- [ ] Update file count in debug log
- [ ] Run E2E tests to verify
- [ ] Create application step to execute the playbook
- [ ] Verify playbook appears in `build/` directory during execution

### Related Documentation

- **Architecture**: [`docs/technical/template-system-architecture.md`](../technical/template-system-architecture.md) - Understanding the two-phase template system
- **Tera Syntax**: This document (above) - When you DO need dynamic templates with variables
- **Testing**: [`docs/e2e-testing.md`](../e2e-testing.md) - How to run E2E tests to validate your changes
24 changes: 21 additions & 3 deletions docs/technical/template-system-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,17 @@ The system operates through two levels of indirection to balance portability wit
### Static Templates

- **Processing**: Direct file copy from templates to build directory
- **Examples**: Infrastructure definitions, playbooks
- **Examples**: Infrastructure definitions, Ansible playbooks (`install-docker.yml`, `configure-security-updates.yml`)
- **Use Case**: Configuration files that don't need variable substitution
- **Registration**: **Must be explicitly registered** in the template renderer's copy list
- **Guide**: See [`docs/contributing/templates.md`](../contributing/templates.md#-adding-new-ansible-playbooks) for adding new static Ansible playbooks

### Dynamic Templates (Tera)

- **Processing**: Variable substitution using Tera templating engine
- **File Suffix**: `.tera` extension (e.g., `variables.tfvars.tera`)
- **Use Case**: Configuration files requiring runtime parameters
- **File Suffix**: `.tera` extension (e.g., `variables.tfvars.tera`, `inventory.ini.tera`)
- **Use Case**: Configuration files requiring runtime parameters (IPs, usernames, paths)
- **Registration**: Automatically discovered by `.tera` extension

## 🔧 Key Components

Expand All @@ -63,6 +66,21 @@ The system operates through two levels of indirection to balance portability wit
- **Ansible Renderer**: Processes configuration management templates
- Handle the template → build directory rendering process

**Two-Phase Processing:**

1. **Phase 1 - Static File Copying**:

- Files without `.tera` extension are copied as-is
- **Requires explicit registration** in the renderer's copy list
- Example: `install-docker.yml` must be added to `copy_static_templates` array

2. **Phase 2 - Dynamic Template Rendering**:
- Files with `.tera` extension are processed for variable substitution
- Automatically discovered, no manual registration needed
- Example: `inventory.ini.tera` → `inventory.ini` with resolved variables

⚠️ **Common Pitfall**: Forgetting to register static files in Phase 1 will cause "file not found" errors at runtime.

### Template Engine

- Tera-based templating for dynamic content
Expand Down
11 changes: 10 additions & 1 deletion src/application/command_handlers/configure/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use tracing::{info, instrument};
use super::errors::ConfigureCommandHandlerError;
use crate::adapters::ansible::AnsibleClient;
use crate::application::command_handlers::common::StepResult;
use crate::application::steps::{InstallDockerComposeStep, InstallDockerStep};
use crate::application::steps::{
ConfigureSecurityUpdatesStep, InstallDockerComposeStep, InstallDockerStep,
};
use crate::domain::environment::repository::{EnvironmentRepository, TypedEnvironmentRepository};
use crate::domain::environment::state::{ConfigureFailureContext, ConfigureStep};
use crate::domain::environment::{Configured, Configuring, Environment, Provisioned};
Expand All @@ -21,6 +23,7 @@ use crate::shared::error::Traceable;
/// This command handles all steps required to configure infrastructure:
/// 1. Install Docker
/// 2. Install Docker Compose
/// 3. Configure automatic security updates
///
/// # State Management
///
Expand Down Expand Up @@ -68,6 +71,7 @@ impl ConfigureCommandHandler {
/// Returns an error if any step in the configuration workflow fails:
/// * Docker installation fails
/// * Docker Compose installation fails
/// * Security updates configuration fails
///
/// On error, the environment transitions to `ConfigureFailed` state and is persisted.
#[instrument(
Expand Down Expand Up @@ -152,6 +156,11 @@ impl ConfigureCommandHandler {
.execute()
.map_err(|e| (e.into(), current_step))?;

let current_step = ConfigureStep::ConfigureSecurityUpdates;
ConfigureSecurityUpdatesStep::new(Arc::clone(&self.ansible_client))
.execute()
.map_err(|e| (e.into(), current_step))?;

// Transition to Configured state
let configured = environment.clone().configured();

Expand Down
2 changes: 1 addition & 1 deletion src/application/steps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub use rendering::{
RenderAnsibleTemplatesError, RenderAnsibleTemplatesStep, RenderOpenTofuTemplatesStep,
};
pub use software::{InstallDockerComposeStep, InstallDockerStep};
pub use system::WaitForCloudInitStep;
pub use system::{ConfigureSecurityUpdatesStep, WaitForCloudInitStep};
pub use validation::{
ValidateCloudInitCompletionStep, ValidateDockerComposeInstallationStep,
ValidateDockerInstallationStep,
Expand Down
Loading