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
388 changes: 307 additions & 81 deletions src/application/command_handlers/release/errors.rs

Large diffs are not rendered by default.

768 changes: 6 additions & 762 deletions src/application/command_handlers/release/handler.rs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/application/command_handlers/release/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
//! - **Explicit State Transitions**: Type-safe state machine for environment lifecycle
//! - **Explicit Errors**: All errors implement `.help()` with actionable guidance
//!
//! ## Module Organization
//!
//! - `handler.rs` - Core handler with `execute()`, state transitions, workflow orchestration
//! - `workflow.rs` - Release workflow orchestration (step coordination)
//! - `errors.rs` - Error types for release operations
//! - `steps/` - Service-specific step implementations (tracker, prometheus, etc.)
//!
//! ## Release Workflow
//!
//! The command handler orchestrates a multi-step workflow:
Expand All @@ -40,6 +47,8 @@

pub mod errors;
pub mod handler;
mod steps;
mod workflow;

#[cfg(test)]
mod tests;
Expand Down
120 changes: 120 additions & 0 deletions src/application/command_handlers/release/steps/caddy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! Caddy service release steps
//!
//! This module contains all steps required to release the Caddy service:
//! - Configuration template rendering
//! - Configuration deployment to remote
//!
//! All steps are optional and only execute if HTTPS is configured.

use std::sync::Arc;

use tracing::info;

use super::common::ansible_client;
use crate::application::command_handlers::common::StepResult;
use crate::application::command_handlers::release::errors::ReleaseCommandHandlerError;
use crate::application::steps::application::DeployCaddyConfigStep;
use crate::application::steps::rendering::RenderCaddyTemplatesStep;
use crate::domain::environment::state::ReleaseStep;
use crate::domain::environment::{Environment, Releasing};
use crate::domain::template::TemplateManager;

/// Release the Caddy service (if HTTPS enabled)
///
/// Executes all steps required to release Caddy:
/// 1. Render configuration templates
/// 2. Deploy configuration to remote
///
/// If HTTPS is not configured, all steps are skipped.
///
/// # Errors
///
/// Returns a tuple of (error, step) if any Caddy step fails
#[allow(clippy::result_large_err)]
pub fn release(
environment: &Environment<Releasing>,
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
// Check if HTTPS is configured
if environment.context().user_inputs.https().is_none() {
info!(
command = "release",
service = "caddy",
status = "skipped",
"HTTPS not configured - skipping all Caddy steps"
);
return Ok(());
}

render_templates(environment)?;
deploy_config_to_remote(environment)?;
Ok(())
}

/// Render Caddy configuration templates
///
/// # Errors
///
/// Returns a tuple of (error, `ReleaseStep::RenderCaddyTemplates`) if rendering fails
#[allow(clippy::result_large_err)]
fn render_templates(
environment: &Environment<Releasing>,
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
let current_step = ReleaseStep::RenderCaddyTemplates;

let template_manager = Arc::new(TemplateManager::new(environment.templates_dir()));
let step = RenderCaddyTemplatesStep::new(
Arc::new(environment.clone()),
template_manager,
environment.build_dir().clone(),
);

step.execute().map_err(|e| {
(
ReleaseCommandHandlerError::TemplateRendering {
message: e.to_string(),
source: Box::new(e),
},
current_step,
)
})?;

info!(
command = "release",
step = %current_step,
"Caddy configuration templates rendered successfully"
);

Ok(())
}

/// Deploy Caddy configuration to the remote host
///
/// # Errors
///
/// Returns a tuple of (error, `ReleaseStep::DeployCaddyConfigToRemote`) if deployment fails
#[allow(clippy::result_large_err)]
fn deploy_config_to_remote(
environment: &Environment<Releasing>,
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
let current_step = ReleaseStep::DeployCaddyConfigToRemote;

DeployCaddyConfigStep::new(ansible_client(environment))
.execute()
.map_err(|e| {
(
ReleaseCommandHandlerError::CaddyConfigDeployment {
message: e.to_string(),
source: Box::new(e),
},
current_step,
)
})?;

info!(
command = "release",
step = %current_step,
"Caddy configuration deployed to remote successfully"
);

Ok(())
}
14 changes: 14 additions & 0 deletions src/application/command_handlers/release/steps/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! Common utilities shared across release steps

use std::sync::Arc;

use crate::adapters::ansible::AnsibleClient;
use crate::domain::environment::{Environment, Releasing};

/// Create an Ansible client configured for the environment's build directory
///
/// This is a helper function to reduce duplication across step implementations.
#[must_use]
pub fn ansible_client(environment: &Environment<Releasing>) -> Arc<AnsibleClient> {
Arc::new(AnsibleClient::new(environment.build_dir().join("ansible")))
}
111 changes: 111 additions & 0 deletions src/application/command_handlers/release/steps/compose.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! Docker Compose release steps
//!
//! This module contains all steps required to deploy Docker Compose:
//! - Template rendering
//! - Compose files deployment to remote

use std::path::{Path, PathBuf};
use std::sync::Arc;

use tracing::info;

use crate::adapters::ansible::AnsibleClient;
use crate::application::command_handlers::common::StepResult;
use crate::application::command_handlers::release::errors::ReleaseCommandHandlerError;
use crate::application::steps::{DeployComposeFilesStep, RenderDockerComposeTemplatesStep};
use crate::domain::environment::state::ReleaseStep;
use crate::domain::environment::{Environment, Releasing};
use crate::domain::template::TemplateManager;

/// Release Docker Compose configuration
///
/// Executes all steps required to deploy Docker Compose:
/// 1. Render Docker Compose templates
/// 2. Deploy compose files to remote
///
/// # Errors
///
/// Returns a tuple of (error, step) if any Docker Compose step fails
pub async fn release(
environment: &Environment<Releasing>,
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
let compose_build_dir = render_templates(environment).await?;
deploy_files_to_remote(environment, &compose_build_dir)?;
Ok(())
}

/// Render Docker Compose templates to the build directory
///
/// # Errors
///
/// Returns a tuple of (error, `ReleaseStep::RenderDockerComposeTemplates`) if rendering fails
async fn render_templates(
environment: &Environment<Releasing>,
) -> StepResult<PathBuf, ReleaseCommandHandlerError, ReleaseStep> {
let current_step = ReleaseStep::RenderDockerComposeTemplates;

let template_manager = Arc::new(TemplateManager::new(environment.templates_dir()));
let step = RenderDockerComposeTemplatesStep::new(
Arc::new(environment.clone()),
template_manager,
environment.build_dir().clone(),
);

let compose_build_dir = step.execute().await.map_err(|e| {
(
ReleaseCommandHandlerError::TemplateRendering {
message: e.to_string(),
source: Box::new(e),
},
current_step,
)
})?;

info!(
command = "release",
compose_build_dir = %compose_build_dir.display(),
"Docker Compose templates rendered successfully"
);

Ok(compose_build_dir)
}

/// Deploy compose files to the remote host via Ansible
///
/// # Arguments
///
/// * `environment` - The environment in Releasing state
/// * `compose_build_dir` - Path to the rendered compose files
///
/// # Errors
///
/// Returns a tuple of (error, `ReleaseStep::DeployComposeFilesToRemote`) if deployment fails
#[allow(clippy::result_large_err)]
fn deploy_files_to_remote(
environment: &Environment<Releasing>,
compose_build_dir: &Path,
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
let current_step = ReleaseStep::DeployComposeFilesToRemote;

let ansible_client = Arc::new(AnsibleClient::new(environment.ansible_build_dir()));
let step = DeployComposeFilesStep::new(ansible_client, compose_build_dir.to_path_buf());

step.execute().map_err(|e| {
(
ReleaseCommandHandlerError::ComposeFilesDeployment {
message: e.to_string(),
source: Box::new(e),
},
current_step,
)
})?;

info!(
command = "release",
compose_build_dir = %compose_build_dir.display(),
instance_ip = ?environment.instance_ip(),
"Compose files deployed to remote host successfully"
);

Ok(())
}
Loading
Loading