Files
madbase/control_plane/src/templates.rs
Vlad Durnea a66d908eff
Some checks failed
CI / podman-build (push) Has been cancelled
CI / rust (push) Has been cancelled
chore: full stack stability and migration fixes, plus react UI progress
2026-03-18 09:01:38 +02:00

512 lines
20 KiB
Rust

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<ServiceConfig>,
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<String>,
#[serde(default)]
pub environment: Vec<EnvVar>,
#[serde(default)]
pub volumes: Vec<String>,
#[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<String>,
}
impl TemplateConfig {
/// Load all available templates
pub async fn all_templates() -> Vec<TemplateConfig> {
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<Self> {
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,
},
}
}
}