torrust-tracker-deployer 0.1.0

Torrust Tracker Deployer - Deployment Infrastructure with Ansible and OpenTofu
Documentation
# Tera Template Variable Syntax

This document explains the correct syntax for defining variables in Tera templates used in the Torrust Tracker Deployer project.

> **See Also**: For architectural details about how the template system works, see [Template System Architecture]template-system-architecture.md.

## 📝 Correct Variable Syntax

All Tera template variables must use **double curly braces** with **no spaces** inside the braces:

```yaml
# ✅ CORRECT
{{ variable_name }}
{{ username }}
{{ ssh_public_key }}
{{ instance_name }}
```

## ❌ Incorrect Syntax to Avoid

```yaml
# ❌ WRONG - Spaces inside braces
{ { variable_name } }
{ { username } }

# ❌ WRONG - Single braces
{ variable_name }

# ❌ WRONG - Mixed spacing
{{ variable_name}}
{{variable_name }}
```

## 📖 Examples in Practice

### Cloud-init Template

```yaml
users:
  - name: { { username } }
    ssh_authorized_keys:
      - { { ssh_public_key } }
```

### Ansible Inventory Template

```yaml
torrust_servers:
  hosts:
    torrust_vm:
      ansible_host: { { ansible_host } }
```

### OpenTofu Variables Template

```hcl
instance_name = "{{ instance_name }}"
```

## 🎯 Key Rules

1. Always use double curly braces: `{{` and `}}`
2. No spaces between braces and variable name: `{{variable}}` not `{ { variable } }`
3. Variable names are case-sensitive
4. Works in any file format (YAML, HCL, etc.)

## 🔧 Troubleshooting

### VS Code Prettier Extension Adding Spaces in Variables

**Problem**: When using VS Code with the Prettier extension, saving `.tera` files automatically adds unwanted spaces inside Tera variables:

- **Before saving**: `{{ username }}` ✅
- **After saving**: `{ { username } }` ❌

**Cause**: Prettier doesn't understand Tera template syntax and tries to format `.tera` files incorrectly.

**Solution**: Create a `.prettierignore` file in your project root to exclude Tera template files:

```gitignore
# Ignore Tera template files - they have specific syntax that Prettier doesn't understand
*.tera
```

**Alternative Solution**: Disable formatting for `.tera` files in your VS Code settings:

```json
{
  "[tera]": {
    "editor.formatOnSave": false,
    "editor.defaultFormatter": null
  }
}
```

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/project_generator.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 [Template System Architecture]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 and release tests (faster, tests configuration only)
cargo run --bin e2e-deployment-workflow-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/project_generator.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

## 🎯 Using Centralized Variables in Ansible Playbooks

When creating new Ansible playbooks that need dynamic variables (ports, paths, etc.), use the **centralized variables pattern** instead of creating new Tera templates.

### DO ✅

**Add variables to `templates/ansible/variables.yml.tera`:**

```yaml
# System Configuration
ssh_port: { { ssh_port } }
my_service_port: { { my_service_port } } # ← Add your new variable
```

**Reference variables in static playbook using `vars_files`:**

```yaml
# templates/ansible/my-new-service.yml (static playbook, no .tera extension)
---
- name: Configure My Service
  hosts: all
  vars_files:
    - variables.yml # Load centralized variables

  tasks:
    - name: Configure service port
      ansible.builtin.lineinfile:
        path: /etc/myservice/config
        line: "port={{ my_service_port }}"
```

**Register playbook in `copy_static_templates()` method:**

```rust
for playbook in &[
    "update-apt-cache.yml",
    "install-docker.yml",
    "my-new-service.yml",  // ← Add here
] {
    // ...
}
```

### DON'T ❌

- ❌ Create a new `.tera` template for the playbook
- ❌ Create a new renderer/wrapper/context for each playbook
- ❌ Add variables directly in `inventory.yml.tera` (unless inventory-specific)

### Benefits

1. **Minimal Code**: No Rust boilerplate (renderer, wrapper, context) needed
2. **Centralized Management**: All variables in one place
3. **Runtime Resolution**: Variables resolved by Ansible, not at template rendering
4. **Easy Maintenance**: Adding new variables requires minimal changes

### When to Create a New Tera Template

Only create a new `.tera` template if:

1. The file **cannot** use Ansible's `vars_files` directive (e.g., inventory files)
2. The file requires **complex logic** that Tera provides but Ansible doesn't
3. The file needs **different variable scopes** than what centralized variables provide

Otherwise, use the centralized variables pattern for simplicity.

### Related Documentation

- **Architecture**: [Template System Architecture]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/`]../e2e-testing/ - How to run E2E tests to validate your changes