use colored::Colorize;
use include_dir::{Dir, include_dir};
use lmrc_config_validator::LmrcConfig;
use std::fs;
use std::path::Path;
use crate::error::Result;
static PIPELINE_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/embedded-apps/pipeline-template");
pub fn generate_pipeline_app(project_path: &Path, _config: &LmrcConfig) -> Result<()> {
let pipeline_path = project_path.join("infra").join("pipeline");
fs::create_dir_all(&pipeline_path)?;
extract_template(&pipeline_path)?;
println!(" {} infra/pipeline", "Created:".green());
println!(
" {} Pipeline uses lmrc-pipeline library",
"Note:".bright_blue()
);
println!(
" {} All files statically validated at compile time",
"Note:".bright_blue()
);
Ok(())
}
fn extract_template(target_path: &Path) -> Result<()> {
for entry in PIPELINE_TEMPLATE.entries() {
extract_entry(entry, target_path)?;
}
Ok(())
}
fn extract_entry(entry: &include_dir::DirEntry, base_path: &Path) -> Result<()> {
match entry {
include_dir::DirEntry::Dir(dir) => {
let dir_path = base_path.join(dir.path());
fs::create_dir_all(&dir_path)?;
for child in dir.entries() {
extract_entry(child, base_path)?;
}
}
include_dir::DirEntry::File(file) => {
let mut file_path = base_path.join(file.path());
if let Some(path_str) = file_path.to_str() {
if path_str.ends_with(".template") {
file_path = Path::new(&path_str.trim_end_matches(".template")).to_path_buf();
}
}
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&file_path, file.contents())?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use lmrc_config_validator::*;
use std::collections::HashMap;
use tempfile::TempDir;
#[test]
fn test_pipeline_template_is_embedded() {
assert!(PIPELINE_TEMPLATE.get_file("Cargo.toml.template").is_some());
assert!(PIPELINE_TEMPLATE.get_file("src/main.rs").is_some());
}
#[test]
fn test_embedded_cargo_toml_is_valid() {
let cargo_toml = PIPELINE_TEMPLATE
.get_file("Cargo.toml.template")
.expect("Cargo.toml.template should be embedded");
let content = cargo_toml
.contents_utf8()
.expect("Cargo.toml should be UTF-8");
let parsed: toml::Value = toml::from_str(content).expect("Cargo.toml should be valid TOML");
assert_eq!(
parsed["package"]["name"].as_str(),
Some("pipeline")
);
assert!(parsed["dependencies"]["lmrc-pipeline"].is_table());
assert!(parsed["dependencies"]["lmrc-config-validator"].is_table());
}
#[test]
fn test_embedded_main_rs_contains_required_code() {
let main_rs = PIPELINE_TEMPLATE
.get_file("src/main.rs")
.expect("main.rs should be embedded");
let content = main_rs.contents_utf8().expect("main.rs should be UTF-8");
assert!(content.contains("use lmrc_pipeline"));
assert!(content.contains("use lmrc_config_validator::LmrcConfig"));
assert!(content.contains("#[tokio::main]"));
assert!(content.contains("async fn main()"));
assert!(content.contains("Commands::Provision"));
assert!(content.contains("Commands::Deploy"));
assert!(content.contains("Commands::Full"));
}
fn create_test_config() -> LmrcConfig {
LmrcConfig {
project: ProjectConfig {
name: "test-project".to_string(),
description: "Test project".to_string(),
},
providers: ProviderConfig {
server: "hetzner".to_string(),
kubernetes: "k3s".to_string(),
database: "postgres".to_string(),
queue: "rabbitmq".to_string(),
dns: "cloudflare".to_string(),
git: "gitlab".to_string(),
},
apps: AppsConfig {
applications: vec![ApplicationEntry {
name: "test-app".to_string(),
app_type: Some(lmrc_config_validator::AppType::Api),
docker: None,
deployment: None,
}],
},
infrastructure: InfrastructureConfig {
provider: "hetzner".to_string(),
network: None,
servers: vec![ServerGroup {
name: "k3s-server".to_string(),
role: ServerRole::K3sControl,
server_type: "cx11".to_string(),
location: "nbg1".to_string(),
count: 1,
labels: HashMap::new(),
ssh_keys: vec![],
image: None,
}],
k3s: Some(K3sConfig {
version: "v1.28.5+k3s1".to_string(),
deploy_on: vec!["k3s-server".to_string()],
control_plane_servers: vec!["k3s-server".to_string()],
worker_servers: vec![],
enable_traefik: true,
enable_metrics_server: false,
server_flags: vec![],
agent_flags: vec![],
}),
postgres: Some(PostgresConfig {
version: "16".to_string(),
database_name: "testdb".to_string(),
deployment_mode: PostgresDeploymentMode::InCluster,
standalone: None,
in_cluster: Some(PostgresInClusterConfig {
namespace: "default".to_string(),
storage_class: "local-path".to_string(),
storage_size: "10Gi".to_string(),
use_operator: false,
}),
}),
rabbitmq: None,
vault: None,
dns: Some(DnsConfig {
provider: "cloudflare".to_string(),
domain: "test.example.com".to_string(),
records: vec![],
}),
gitlab: Some(GitLabConfig {
url: "https://gitlab.com".to_string(),
namespace: "testuser".to_string(),
}),
load_balancer: None,
},
}
}
#[test]
fn test_generate_pipeline_app_creates_directory() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
let result = generate_pipeline_app(temp_dir.path(), &config);
assert!(result.is_ok());
let pipeline_path = temp_dir.path().join("infra").join("pipeline");
assert!(pipeline_path.exists());
assert!(pipeline_path.is_dir());
}
#[test]
fn test_generate_pipeline_app_creates_cargo_toml() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let cargo_toml = temp_dir.path().join("infra/pipeline/Cargo.toml");
assert!(cargo_toml.exists());
}
#[test]
fn test_generate_pipeline_app_creates_main_rs() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let main_rs = temp_dir.path().join("infra/pipeline/src/main.rs");
assert!(main_rs.exists());
}
#[test]
fn test_pipeline_cargo_toml_has_correct_name() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let cargo_toml = temp_dir.path().join("infra/pipeline/Cargo.toml");
let content = fs::read_to_string(cargo_toml).unwrap();
assert!(content.contains("name = \"pipeline\""));
}
#[test]
fn test_pipeline_cargo_toml_has_bin_section() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let cargo_toml = temp_dir.path().join("infra/pipeline/Cargo.toml");
let content = fs::read_to_string(cargo_toml).unwrap();
assert!(content.contains("[[bin]]"));
assert!(content.contains("name = \"pipeline\""));
assert!(content.contains("path = \"src/main.rs\""));
}
#[test]
fn test_pipeline_cargo_toml_has_required_dependencies() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let cargo_toml = temp_dir.path().join("infra/pipeline/Cargo.toml");
let content = fs::read_to_string(cargo_toml).unwrap();
let deps = [
"tokio",
"clap",
"anyhow",
"lmrc-pipeline",
"lmrc-config-validator",
];
for dep in &deps {
assert!(
content.contains(dep),
"Pipeline Cargo.toml missing dependency: {}",
dep
);
}
}
#[test]
fn test_pipeline_main_has_tokio_main() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let main_rs = temp_dir.path().join("infra/pipeline/src/main.rs");
let content = fs::read_to_string(main_rs).unwrap();
assert!(content.contains("#[tokio::main]"));
assert!(content.contains("async fn main()"));
}
#[test]
fn test_pipeline_main_has_all_commands() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let main_rs = temp_dir.path().join("infra/pipeline/src/main.rs");
let content = fs::read_to_string(main_rs).unwrap();
let commands = [
"Check",
"Test",
"Build",
"DockerBuild",
"Provision",
"Setup",
"Deploy",
"Full",
];
for cmd in &commands {
assert!(
content.contains(cmd),
"Pipeline main.rs missing command: {}",
cmd
);
}
}
#[test]
fn test_pipeline_main_uses_pipeline_library() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let main_rs = temp_dir.path().join("infra/pipeline/src/main.rs");
let content = fs::read_to_string(main_rs).unwrap();
assert!(content.contains("use lmrc_pipeline::{"));
assert!(content.contains("Pipeline"));
assert!(content.contains("PipelineContext"));
assert!(content.contains("use lmrc_pipeline::steps::*"));
assert!(content.contains("Pipeline::new(ctx)"));
assert!(content.contains("PipelineContext::new(config)?") || content.contains("PipelineContext::new(config.clone())?"));
}
#[test]
fn test_pipeline_main_has_clap_parser() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let main_rs = temp_dir.path().join("infra/pipeline/src/main.rs");
let content = fs::read_to_string(main_rs).unwrap();
assert!(content.contains("use clap::{Parser, Subcommand}"));
assert!(content.contains("#[derive(Parser)]"));
assert!(content.contains("#[derive(Subcommand)]"));
assert!(content.contains("Cli::parse()"));
}
#[test]
fn test_pipeline_main_full_command_includes_all_steps() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let main_rs = temp_dir.path().join("infra/pipeline/src/main.rs");
let content = fs::read_to_string(main_rs).unwrap();
assert!(content.contains("StepRegistry::default()") || content.contains("StepRegistry::new()"));
assert!(content.contains("build_build_steps"));
assert!(content.contains("build_provision_steps"));
assert!(content.contains("build_setup_steps"));
assert!(content.contains("build_deploy_steps"));
assert!(content.contains("DockerBuildStep::new()"));
assert!(content.contains("Commands::Full"));
}
#[test]
fn test_pipeline_main_loads_config_from_file() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let main_rs = temp_dir.path().join("infra/pipeline/src/main.rs");
let content = fs::read_to_string(main_rs).unwrap();
assert!(content.contains("LmrcConfig::from_file"));
assert!(content.contains("lmrc.toml"));
}
#[test]
fn test_pipeline_main_has_valid_rust_syntax() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let main_rs = temp_dir.path().join("infra/pipeline/src/main.rs");
let content = fs::read_to_string(main_rs).unwrap();
assert!(content.contains("fn main()"));
assert!(content.contains("Ok(())"));
let open_braces = content.chars().filter(|&c| c == '{').count();
let close_braces = content.chars().filter(|&c| c == '}').count();
assert_eq!(
open_braces, close_braces,
"Unbalanced braces in generated main.rs"
);
}
#[test]
fn test_pipeline_workspace_dependencies() {
let temp_dir = TempDir::new().unwrap();
let config = create_test_config();
generate_pipeline_app(temp_dir.path(), &config).unwrap();
let cargo_toml = temp_dir.path().join("infra/pipeline/Cargo.toml");
let content = fs::read_to_string(cargo_toml).unwrap();
assert!(content.contains("workspace = true"));
}
}