use serde::{Deserialize, Serialize}; use anyhow::Result; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemplateConfig { pub id: String, pub name: String, pub description: String, pub version: String, pub min_hetzner_plan: String, #[serde(rename = "min_hetzner_plan_num")] pub min_hetzner_plan_num: u32, #[serde(rename = "estimated_monthly_cost")] pub estimated_monthly_cost: f64, pub services: Vec, pub requirements: TemplateRequirements, #[serde(rename = "estimated_time_minutes")] pub estimated_time_minutes: i32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServiceConfig { pub id: String, pub name: String, pub image: String, pub ports: Vec, #[serde(default)] pub environment: Vec, #[serde(default)] pub volumes: Vec, #[serde(rename = "resource_profile", default)] pub resource_profile: String, #[serde(rename = "has_persistent_data", default)] pub has_persistent_data: bool, #[serde(rename = "is_critical", default)] pub is_critical: bool, #[serde(default)] pub optional: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EnvVar { pub name: String, pub value: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemplateRequirements { #[serde(rename = "min_nodes")] pub min_nodes: i32, #[serde(rename = "max_nodes")] pub max_nodes: i32, #[serde(rename = "supports_ha", default)] pub supports_ha: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct TemplateValidation { pub valid: bool, pub warnings: Vec, } impl TemplateConfig { /// Load all available templates pub async fn all_templates() -> Vec { vec![ Self::db_node_template(), Self::worker_node_template(), Self::control_plane_node_template(), Self::monitoring_node_template(), Self::worker_db_combo_template(), Self::worker_monitor_combo_template(), Self::all_in_one_template(), ] } /// Load template by ID pub async fn from_template_id(id: &str) -> Result { let templates = Self::all_templates().await; templates.into_iter() .find(|t| t.id == id) .ok_or_else(|| anyhow::anyhow!("Template not found: {}", id)) } pub fn validate(&self) -> TemplateValidation { let mut warnings = Vec::new(); if self.min_hetzner_plan_num < 11 { warnings.push("Plan CX11 is minimum recommended".to_string()); } if self.services.is_empty() { warnings.push("Template has no services".to_string()); } if self.requirements.max_nodes > 1 && !self.requirements.supports_ha { warnings.push("Multiple nodes but HA not supported".to_string()); } TemplateValidation { valid: warnings.is_empty(), warnings, } } // Template definitions fn db_node_template() -> Self { Self { id: "db-node".to_string(), name: "Database Node".to_string(), description: "PostgreSQL with Patroni for HA clustering".to_string(), version: "1.0".to_string(), min_hetzner_plan: "CX21".to_string(), min_hetzner_plan_num: 21, estimated_monthly_cost: 6.94, estimated_time_minutes: 15, services: vec![ ServiceConfig { id: "postgresql".to_string(), name: "PostgreSQL".to_string(), image: "registry.gitlab.com/postgres-ai/postgresql-autobase/patroni:3.0.2".to_string(), ports: vec!["5432:5432".to_string(), "8008:8008".to_string()], environment: vec![], volumes: vec!["postgres_data:/var/lib/postgresql/data".to_string()], resource_profile: "balanced".to_string(), has_persistent_data: true, is_critical: true, optional: false, }, ServiceConfig { id: "etcd".to_string(), name: "etcd".to_string(), image: "quay.io/coreos/etcd:v3.5.9".to_string(), ports: vec!["2379:2379".to_string(), "2380:2380".to_string()], environment: vec![], volumes: vec!["etcd_data:/etcd-data".to_string()], resource_profile: "minimal".to_string(), has_persistent_data: true, is_critical: true, optional: false, }, ServiceConfig { id: "haproxy".to_string(), name: "HAProxy".to_string(), image: "haproxy:2.8-alpine".to_string(), ports: vec!["5433:5433".to_string()], environment: vec![], volumes: vec![], resource_profile: "minimal".to_string(), has_persistent_data: false, is_critical: false, optional: false, }, ], requirements: TemplateRequirements { min_nodes: 3, max_nodes: 7, supports_ha: true, }, } } fn worker_node_template() -> Self { Self { id: "worker-node".to_string(), name: "Worker Node".to_string(), description: "API worker nodes for horizontal scaling".to_string(), version: "1.0".to_string(), min_hetzner_plan: "CX11".to_string(), min_hetzner_plan_num: 11, estimated_monthly_cost: 3.69, estimated_time_minutes: 10, services: vec![ ServiceConfig { id: "worker".to_string(), name: "MadBase Worker".to_string(), image: "madbase/worker:latest".to_string(), ports: vec!["8002:8002".to_string()], environment: vec![], volumes: vec![], resource_profile: "cpu_intensive".to_string(), has_persistent_data: false, is_critical: true, optional: false, }, ServiceConfig { id: "vmagent".to_string(), name: "VictoriaMetrics Agent".to_string(), image: "victoriametrics/vmagent:latest".to_string(), ports: vec!["8429:8429".to_string()], environment: vec![], volumes: vec!["./config/vmagent.yml:/etc/vmagent/prometheus.yml:ro".to_string()], resource_profile: "minimal".to_string(), has_persistent_data: false, is_critical: false, optional: true, }, ], requirements: TemplateRequirements { min_nodes: 1, max_nodes: 20, supports_ha: true, }, } } fn control_plane_node_template() -> Self { Self { id: "control-plane-node".to_string(), name: "Control Plane Node".to_string(), description: "Management APIs and Studio UI".to_string(), version: "1.0".to_string(), min_hetzner_plan: "CX11".to_string(), min_hetzner_plan_num: 11, estimated_monthly_cost: 3.69, estimated_time_minutes: 12, services: vec![ ServiceConfig { id: "proxy".to_string(), name: "Gateway Proxy".to_string(), image: "madbase/proxy:latest".to_string(), ports: vec!["8080:8080".to_string()], environment: vec![], volumes: vec![], resource_profile: "balanced".to_string(), has_persistent_data: false, is_critical: true, optional: false, }, ServiceConfig { id: "control".to_string(), name: "Control Plane API".to_string(), image: "madbase/control:latest".to_string(), ports: vec!["8001:8001".to_string()], environment: vec![], volumes: vec![], resource_profile: "balanced".to_string(), has_persistent_data: false, is_critical: true, optional: false, }, ServiceConfig { id: "grafana".to_string(), name: "Grafana".to_string(), image: "grafana/grafana:latest".to_string(), ports: vec!["3030:3030".to_string()], environment: vec![], volumes: vec!["grafana_data:/var/lib/grafana".to_string()], resource_profile: "balanced".to_string(), has_persistent_data: true, is_critical: false, optional: true, }, ], requirements: TemplateRequirements { min_nodes: 1, max_nodes: 2, supports_ha: true, }, } } fn monitoring_node_template() -> Self { Self { id: "monitoring-node".to_string(), name: "Monitoring Node".to_string(), description: "Centralized metrics and logging".to_string(), version: "1.0".to_string(), min_hetzner_plan: "CX11".to_string(), min_hetzner_plan_num: 11, estimated_monthly_cost: 3.69, estimated_time_minutes: 10, services: vec![ ServiceConfig { id: "victoriametrics".to_string(), name: "VictoriaMetrics".to_string(), image: "victoriametrics/victoria-metrics:latest".to_string(), ports: vec!["8428:8428".to_string()], environment: vec![], volumes: vec!["vm_data:/victoria-metrics-data".to_string()], resource_profile: "balanced".to_string(), has_persistent_data: true, is_critical: false, optional: false, }, ServiceConfig { id: "loki".to_string(), name: "Loki".to_string(), image: "grafana/loki:latest".to_string(), ports: vec!["3100:3100".to_string()], environment: vec![], volumes: vec!["loki_data:/loki".to_string()], resource_profile: "balanced".to_string(), has_persistent_data: true, is_critical: false, optional: false, }, ], requirements: TemplateRequirements { min_nodes: 1, max_nodes: 2, supports_ha: true, }, } } fn worker_db_combo_template() -> Self { Self { id: "worker-db-combo".to_string(), name: "Worker + Database Combo".to_string(), description: "Combined worker and database node for smaller deployments".to_string(), version: "1.0".to_string(), min_hetzner_plan: "CX31".to_string(), min_hetzner_plan_num: 31, estimated_monthly_cost: 14.21, estimated_time_minutes: 20, services: vec![ ServiceConfig { id: "postgresql".to_string(), name: "PostgreSQL".to_string(), image: "registry.gitlab.com/postgres-ai/postgresql-autobase/patroni:3.0.2".to_string(), ports: vec!["5432:5432".to_string(), "8008:8008".to_string()], environment: vec![], volumes: vec!["postgres_data:/var/lib/postgresql/data".to_string()], resource_profile: "balanced".to_string(), has_persistent_data: true, is_critical: true, optional: false, }, ServiceConfig { id: "etcd".to_string(), name: "etcd".to_string(), image: "quay.io/coreos/etcd:v3.5.9".to_string(), ports: vec!["2379:2379".to_string(), "2380:2380".to_string()], environment: vec![], volumes: vec!["etcd_data:/etcd-data".to_string()], resource_profile: "minimal".to_string(), has_persistent_data: true, is_critical: true, optional: false, }, ServiceConfig { id: "haproxy".to_string(), name: "HAProxy".to_string(), image: "haproxy:2.8-alpine".to_string(), ports: vec!["5433:5433".to_string()], environment: vec![], volumes: vec![], resource_profile: "minimal".to_string(), has_persistent_data: false, is_critical: false, optional: false, }, ServiceConfig { id: "worker".to_string(), name: "MadBase Worker".to_string(), image: "madbase/worker:latest".to_string(), ports: vec!["8002:8002".to_string()], environment: vec![], volumes: vec![], resource_profile: "cpu_intensive".to_string(), has_persistent_data: false, is_critical: true, optional: false, }, ServiceConfig { id: "vmagent".to_string(), name: "VictoriaMetrics Agent".to_string(), image: "victoriametrics/vmagent:latest".to_string(), ports: vec!["8429:8429".to_string()], environment: vec![], volumes: vec!["./config/vmagent.yml:/etc/vmagent/prometheus.yml:ro".to_string()], resource_profile: "minimal".to_string(), has_persistent_data: false, is_critical: false, optional: false, }, ], requirements: TemplateRequirements { min_nodes: 1, max_nodes: 2, supports_ha: true, }, } } fn worker_monitor_combo_template() -> Self { Self { id: "worker-monitor-combo".to_string(), name: "Worker + Monitoring Combo".to_string(), description: "Worker node with local VictoriaMetrics and Loki".to_string(), version: "1.0".to_string(), min_hetzner_plan: "CX21".to_string(), min_hetzner_plan_num: 21, estimated_monthly_cost: 6.94, estimated_time_minutes: 15, services: vec![ ServiceConfig { id: "worker".to_string(), name: "MadBase Worker".to_string(), image: "madbase/worker:latest".to_string(), ports: vec!["8002:8002".to_string()], environment: vec![], volumes: vec![], resource_profile: "cpu_intensive".to_string(), has_persistent_data: false, is_critical: true, optional: false, }, ServiceConfig { id: "victoriametrics".to_string(), name: "VictoriaMetrics".to_string(), image: "victoriametrics/victoria-metrics:latest".to_string(), ports: vec!["8428:8428".to_string()], environment: vec![], volumes: vec!["vm_data:/victoria-metrics-data".to_string()], resource_profile: "balanced".to_string(), has_persistent_data: true, is_critical: false, optional: false, }, ServiceConfig { id: "loki".to_string(), name: "Loki".to_string(), image: "grafana/loki:latest".to_string(), ports: vec!["3100:3100".to_string()], environment: vec![], volumes: vec!["loki_data:/loki".to_string()], resource_profile: "balanced".to_string(), has_persistent_data: true, is_critical: false, optional: false, }, ], requirements: TemplateRequirements { min_nodes: 1, max_nodes: 3, supports_ha: true, }, } } fn all_in_one_template() -> Self { Self { id: "all-in-one".to_string(), name: "All-in-One Development Node".to_string(), description: "Complete MadBase stack on a single server".to_string(), version: "1.0".to_string(), min_hetzner_plan: "CX41".to_string(), min_hetzner_plan_num: 41, estimated_monthly_cost: 25.60, estimated_time_minutes: 25, services: vec![ ServiceConfig { id: "postgresql".to_string(), name: "PostgreSQL".to_string(), image: "registry.gitlab.com/postgres-ai/postgresql-autobase/patroni:3.0.2".to_string(), ports: vec!["5432:5432".to_string(), "8008:8008".to_string()], environment: vec![], volumes: vec!["postgres_data:/var/lib/postgresql/data".to_string()], resource_profile: "balanced".to_string(), has_persistent_data: true, is_critical: true, optional: false, }, ServiceConfig { id: "worker".to_string(), name: "MadBase Worker".to_string(), image: "madbase/worker:latest".to_string(), ports: vec!["8002:8002".to_string()], environment: vec![], volumes: vec![], resource_profile: "cpu_intensive".to_string(), has_persistent_data: false, is_critical: true, optional: false, }, ServiceConfig { id: "proxy".to_string(), name: "Gateway Proxy".to_string(), image: "madbase/proxy:latest".to_string(), ports: vec!["8080:8080".to_string()], environment: vec![], volumes: vec![], resource_profile: "balanced".to_string(), has_persistent_data: false, is_critical: true, optional: false, }, ServiceConfig { id: "control".to_string(), name: "Control Plane API".to_string(), image: "madbase/control:latest".to_string(), ports: vec!["8001:8001".to_string()], environment: vec![], volumes: vec![], resource_profile: "balanced".to_string(), has_persistent_data: false, is_critical: true, optional: false, }, ], requirements: TemplateRequirements { min_nodes: 1, max_nodes: 1, supports_ha: false, }, } } }