Skip to content

Commit 30d1eda

Browse files
committed
Merge #98: Add automatic security updates configuration to ConfigureCommand
3d8b153 docs: [#17] add comprehensive guide for adding static Ansible playbooks (Jose Celano) b5d4e59 fix: [#17] include configure-security-updates.yml in static template copy list (Jose Celano) d2d25d9 fix: [#17] add Debian family conditionals to security updates playbook (copilot-swe-agent[bot]) 4d88cd5 feat: [#17] add automatic security updates configuration (copilot-swe-agent[bot]) db82319 Initial plan (copilot-swe-agent[bot]) Pull request description: Implements unattended-upgrades configuration as a new step in the ConfigureCommand workflow, enabling automatic security patching with scheduled 2:00 AM reboots. ## Changes **Domain Layer** - Added `ConfigureSecurityUpdates` variant to `ConfigureStep` enum for failure tracking **Application Layer** - Created `ConfigureSecurityUpdatesStep` that executes Ansible playbook via `AnsibleClient` - Integrated step into `ConfigureCommandHandler` workflow after Docker Compose installation - Follows existing step pattern with proper error mapping and tracing **Infrastructure Layer** - Created `templates/ansible/configure-security-updates.yml` playbook: - Installs and configures `unattended-upgrades` package - Enables automatic security updates via APT configuration - Sets automatic reboot schedule (02:00) - Includes Debian family conditionals for cross-platform safety - Verifies configuration with dry-run ## Usage The step runs automatically in the configure workflow: ```rust let current_step = ConfigureStep::ConfigureSecurityUpdates; ConfigureSecurityUpdatesStep::new(Arc::clone(&self.ansible_client)) .execute() .map_err(|e| (e.into(), current_step))?; ``` Configuration details in `/etc/apt/apt.conf.d/` files are backed up before modification. <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Configure Automatic Security Updates</issue_title> > <issue_description>Implement automatic security updates configuration in the `ConfigureCommand`. This task adds a new step that configures unattended-upgrades on provisioned instances to ensure they automatically receive and install security patches with scheduled reboots. > > This is the first phase of completing the system security configuration, chosen because it has lower implementation risk and provides immediate security value. > > ## Goals > > - [ ] **Automatic Security Updates**: Configure unattended-upgrades for automatic security patching > - [ ] **Scheduled Reboots**: Enable automatic reboots at 2:00 AM for security updates that require restart > - [ ] **New Domain Step**: Add `ConfigureSecurityUpdates` to the `ConfigureStep` enum > - [ ] **Ansible Integration**: Create new Ansible playbook for security updates configuration > - [ ] **Error Handling**: Implement proper error handling with actionable messages > - [ ] **Testing**: Ensure E2E tests validate the security updates configuration > > ## Specifications > > ### Domain Integration > > Update `ConfigureStep` enum in `src/domain/environment/state/configure_failed.rs`: > > ```rust > /// Steps in the configure workflow > #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] > pub enum ConfigureStep { > /// Installing Docker > InstallDocker, > /// Installing Docker Compose > InstallDockerCompose, > /// Configuring automatic security updates > ConfigureSecurityUpdates, // <- NEW > } > ``` > > ### New Application Step > > Create `src/application/steps/system/configure_security_updates.rs`: > - Implements the ConfigureSecurityUpdatesStep > - Uses AnsibleClient to execute security updates playbook > - Handles ansible errors and maps them to domain errors > - Follows the same pattern as existing Docker installation steps > > ### New Ansible Playbook > > Create `templates/ansible/configure-security-updates.yml` (static template): > - Installs unattended-upgrades package > - Configures automatic updates for security packages > - Sets up automatic reboot schedule (2:00 AM) > - Configures logging and notifications > > ### Integration > > Update `src/application/commands/configure.rs`: > - Add new ConfigureSecurityUpdates case to step matching > - Execute ConfigureSecurityUpdatesStep in the workflow > > ## Implementation Approach > > This is a **lower risk** implementation that: > - Uses a static Ansible playbook (no Tera template variables needed) > - Has no networking/firewall concerns > - Follows established patterns from Docker installation steps > - Can be implemented and tested independently > > ## Acceptance Criteria > > - [ ] **Security Updates Active**: Instances automatically check for and install security updates > - [ ] **Scheduled Reboots**: Automatic reboots occur at 2:00 AM when needed for security updates > - [ ] **Domain Integration**: ConfigureSecurityUpdates step properly integrated > - [ ] **Error Handling**: Clear, actionable error messages for configuration failures > - [ ] **Tests Pass**: All existing tests continue to pass > - [ ] **E2E Validation**: E2E tests confirm security updates are properly configured > - [ ] **Ansible Integration**: Security updates playbook executes successfully > > ## Related > > **Parent Epic**: #16 - Finish ConfigureCommand - System Security Configuration > **Estimated Effort**: 1-2 days > > Full specification: [Security Updates Documentation](https://github.com/torrust/torrust-tracker-deployer/blob/main/docs/issues/17-configure-automatic-security-updates.md)</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> - Fixes #17 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. ACKs for top commit: josecelano: ACK 3d8b153 Tree-SHA512: 6b94e8737e65a57c88e48d384577751d70219c47aff8b19b040fc532907dea80809f1ea2c210c1da53f7f314ba173b8b8dae8cfd3a7bbdbf15641f67c8260dfc
2 parents 9be37bc + 3d8b153 commit 30d1eda

File tree

10 files changed

+412
-11
lines changed

10 files changed

+412
-11
lines changed

.github/copilot-instructions.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,15 @@ These principles should guide all development decisions, code reviews, and featu
8787

8888
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.
8989

90-
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.
90+
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.
9191

92-
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.
92+
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.
9393

94-
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.
94+
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.
9595

96-
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.
96+
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.
97+
98+
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.
9799

98100
## 🧪 Build & Test
99101

docs/contributing/templates.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,190 @@ instance_name = "{{ instance_name }}"
9292
```
9393

9494
After applying the fix, manually correct any existing formatting issues in your `.tera` files by removing the spaces inside the curly braces.
95+
96+
## 📦 Adding New Ansible Playbooks
97+
98+
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.
99+
100+
### Static vs Dynamic Playbooks
101+
102+
#### Static Playbooks (No Tera Variables)
103+
104+
Static playbooks are standard Ansible YAML files that don't require variable substitution:
105+
106+
- **No `.tera` extension** - Just `.yml`
107+
- **No Tera variables** - No `{{ variable }}` syntax needed
108+
- **Direct copy** - Copied as-is from `templates/ansible/` to `build/` directory
109+
- **Examples**: `install-docker.yml`, `wait-cloud-init.yml`, `configure-security-updates.yml`
110+
111+
#### Dynamic Playbooks (With Tera Variables)
112+
113+
Dynamic playbooks need runtime variable substitution:
114+
115+
- **`.tera` extension** - Named like `inventory.ini.tera`
116+
- **Contains Tera variables** - Uses `{{ ansible_host }}`, `{{ username }}`, etc.
117+
- **Rendered during execution** - Variables replaced at runtime
118+
- **Examples**: Ansible inventory files with instance IPs
119+
120+
### Adding a Static Ansible Playbook
121+
122+
Follow these steps when adding a new static playbook:
123+
124+
#### Step 1: Create the Playbook File
125+
126+
Create your playbook in `templates/ansible/`:
127+
128+
```bash
129+
# Example: Adding a new security configuration playbook
130+
templates/ansible/configure-security-updates.yml
131+
```
132+
133+
Write standard Ansible YAML with no Tera variables:
134+
135+
```yaml
136+
---
137+
- name: Configure automatic security updates
138+
hosts: all
139+
become: true
140+
tasks:
141+
- name: Install unattended-upgrades package
142+
ansible.builtin.apt:
143+
name: unattended-upgrades
144+
state: present
145+
update_cache: true
146+
```
147+
148+
#### Step 2: Register in Template Copy List ⚠️ CRITICAL
149+
150+
**This is the step that's easy to miss!**
151+
152+
Add your playbook filename to the array in `src/infrastructure/external_tools/ansible/template/renderer/mod.rs`:
153+
154+
```rust
155+
// Find the copy_static_templates method
156+
async fn copy_static_templates(
157+
&self,
158+
template_manager: &TemplateManager,
159+
destination_dir: &Path,
160+
) -> Result<(), ConfigurationTemplateError> {
161+
// ... existing code ...
162+
163+
// Copy all playbook files
164+
for playbook in &[
165+
"update-apt-cache.yml",
166+
"install-docker.yml",
167+
"install-docker-compose.yml",
168+
"wait-cloud-init.yml",
169+
"configure-security-updates.yml", // 👈 ADD YOUR PLAYBOOK HERE
170+
] {
171+
self.copy_static_file(template_manager, playbook, destination_dir)
172+
.await?;
173+
}
174+
175+
tracing::debug!(
176+
"Successfully copied {} static template files",
177+
6 // 👈 UPDATE THE COUNT: ansible.cfg + N playbooks
178+
);
179+
180+
Ok(())
181+
}
182+
```
183+
184+
**Why This is Required:**
185+
186+
- The template system uses a **two-phase approach** (see `docs/technical/template-system-architecture.md`)
187+
- **Phase 1**: Static file copying - requires explicit registration
188+
- **Phase 2**: Dynamic rendering - automatic for `.tera` files
189+
- Without registration, your playbook **will not be copied** to the build directory
190+
- Ansible will fail with: `[ERROR]: the playbook: your-playbook.yml could not be found`
191+
192+
#### Step 3: Update the File Count
193+
194+
In the same method, update the debug log count:
195+
196+
```rust
197+
tracing::debug!(
198+
"Successfully copied {} static template files",
199+
6 // ansible.cfg + 5 playbooks 👈 Update this comment
200+
);
201+
```
202+
203+
#### Step 4: Test Your Changes
204+
205+
Run E2E tests to verify the playbook is copied correctly:
206+
207+
```bash
208+
# Run E2E config tests (faster, tests configuration only)
209+
cargo run --bin e2e-config-tests
210+
211+
# Or run full E2E tests
212+
cargo run --bin e2e-tests-full
213+
```
214+
215+
If you forgot Step 2, you'll see this error:
216+
217+
```text
218+
[ERROR]: the playbook: your-playbook.yml could not be found
219+
```
220+
221+
#### Step 5: Use the Playbook in Your Code
222+
223+
Create a step that executes your playbook:
224+
225+
```rust
226+
// In src/application/steps/system/your_step.rs
227+
pub struct YourStep {
228+
ansible_client: Arc<dyn AnsibleClient>,
229+
}
230+
231+
impl YourStep {
232+
pub async fn execute(&self) -> Result<(), YourStepError> {
233+
self.ansible_client
234+
.run_playbook("your-playbook.yml")
235+
.await
236+
.map_err(YourStepError::AnsibleExecution)?;
237+
238+
Ok(())
239+
}
240+
}
241+
```
242+
243+
### Common Mistakes
244+
245+
❌ **Forgetting to register the playbook** in `copy_static_templates`
246+
247+
- Error: Playbook not found during execution
248+
- Fix: Add playbook name to the array
249+
250+
❌ **Forgetting to update the file count** in debug log
251+
252+
- Error: Confusing logs during debugging
253+
- Fix: Update the count comment
254+
255+
❌ **Using `.tera` extension for static playbooks**
256+
257+
- Error: Unnecessary complexity
258+
- Fix: Only use `.tera` if you need variable substitution
259+
260+
❌ **Adding dynamic variables without `.tera` extension**
261+
262+
- Error: Variables not resolved, literal `{{ variable }}` in output
263+
- Fix: Rename to `.yml.tera` and handle in rendering phase
264+
265+
### Quick Checklist
266+
267+
When adding a static Ansible playbook:
268+
269+
- [ ] Create `.yml` file in `templates/ansible/`
270+
- [ ] Write standard Ansible YAML (no Tera variables)
271+
- [ ] Add filename to `copy_static_templates` array in `src/infrastructure/external_tools/ansible/template/renderer/mod.rs`
272+
- [ ] Update file count in debug log
273+
- [ ] Run E2E tests to verify
274+
- [ ] Create application step to execute the playbook
275+
- [ ] Verify playbook appears in `build/` directory during execution
276+
277+
### Related Documentation
278+
279+
- **Architecture**: [`docs/technical/template-system-architecture.md`](../technical/template-system-architecture.md) - Understanding the two-phase template system
280+
- **Tera Syntax**: This document (above) - When you DO need dynamic templates with variables
281+
- **Testing**: [`docs/e2e-testing.md`](../e2e-testing.md) - How to run E2E tests to validate your changes

docs/technical/template-system-architecture.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@ The system operates through two levels of indirection to balance portability wit
4040
### Static Templates
4141

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

4648
### Dynamic Templates (Tera)
4749

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

5255
## 🔧 Key Components
5356

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

69+
**Two-Phase Processing:**
70+
71+
1. **Phase 1 - Static File Copying**:
72+
73+
- Files without `.tera` extension are copied as-is
74+
- **Requires explicit registration** in the renderer's copy list
75+
- Example: `install-docker.yml` must be added to `copy_static_templates` array
76+
77+
2. **Phase 2 - Dynamic Template Rendering**:
78+
- Files with `.tera` extension are processed for variable substitution
79+
- Automatically discovered, no manual registration needed
80+
- Example: `inventory.ini.tera``inventory.ini` with resolved variables
81+
82+
⚠️ **Common Pitfall**: Forgetting to register static files in Phase 1 will cause "file not found" errors at runtime.
83+
6684
### Template Engine
6785

6886
- Tera-based templating for dynamic content

src/application/command_handlers/configure/handler.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use tracing::{info, instrument};
77
use super::errors::ConfigureCommandHandlerError;
88
use crate::adapters::ansible::AnsibleClient;
99
use crate::application::command_handlers::common::StepResult;
10-
use crate::application::steps::{InstallDockerComposeStep, InstallDockerStep};
10+
use crate::application::steps::{
11+
ConfigureSecurityUpdatesStep, InstallDockerComposeStep, InstallDockerStep,
12+
};
1113
use crate::domain::environment::repository::{EnvironmentRepository, TypedEnvironmentRepository};
1214
use crate::domain::environment::state::{ConfigureFailureContext, ConfigureStep};
1315
use crate::domain::environment::{Configured, Configuring, Environment, Provisioned};
@@ -21,6 +23,7 @@ use crate::shared::error::Traceable;
2123
/// This command handles all steps required to configure infrastructure:
2224
/// 1. Install Docker
2325
/// 2. Install Docker Compose
26+
/// 3. Configure automatic security updates
2427
///
2528
/// # State Management
2629
///
@@ -68,6 +71,7 @@ impl ConfigureCommandHandler {
6871
/// Returns an error if any step in the configuration workflow fails:
6972
/// * Docker installation fails
7073
/// * Docker Compose installation fails
74+
/// * Security updates configuration fails
7175
///
7276
/// On error, the environment transitions to `ConfigureFailed` state and is persisted.
7377
#[instrument(
@@ -152,6 +156,11 @@ impl ConfigureCommandHandler {
152156
.execute()
153157
.map_err(|e| (e.into(), current_step))?;
154158

159+
let current_step = ConfigureStep::ConfigureSecurityUpdates;
160+
ConfigureSecurityUpdatesStep::new(Arc::clone(&self.ansible_client))
161+
.execute()
162+
.map_err(|e| (e.into(), current_step))?;
163+
155164
// Transition to Configured state
156165
let configured = environment.clone().configured();
157166

src/application/steps/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub use rendering::{
3636
RenderAnsibleTemplatesError, RenderAnsibleTemplatesStep, RenderOpenTofuTemplatesStep,
3737
};
3838
pub use software::{InstallDockerComposeStep, InstallDockerStep};
39-
pub use system::WaitForCloudInitStep;
39+
pub use system::{ConfigureSecurityUpdatesStep, WaitForCloudInitStep};
4040
pub use validation::{
4141
ValidateCloudInitCompletionStep, ValidateDockerComposeInstallationStep,
4242
ValidateDockerInstallationStep,

0 commit comments

Comments
 (0)