lmrc_config_validator/
lib.rs

1//! Configuration validation library for LMRC Stack projects
2//!
3//! This library provides types and validation for LMRC Stack project configurations.
4//! It defines the schema for project configuration files and validates them.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use thiserror::Error;
9use validator::Validate;
10
11pub mod providers;
12
13/// Main configuration structure for LMRC Stack projects
14#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
15pub struct LmrcConfig {
16    /// Project metadata
17    #[validate(nested)]
18    pub project: ProjectConfig,
19
20    /// Infrastructure provider selection
21    #[validate(nested)]
22    pub providers: ProviderConfig,
23
24    /// Application configuration
25    #[validate(nested)]
26    pub apps: AppsConfig,
27
28    /// Infrastructure-specific configurations
29    #[validate(nested)]
30    pub infrastructure: InfrastructureConfig,
31}
32
33/// Project metadata
34#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
35pub struct ProjectConfig {
36    /// Project name (alphanumeric with hyphens/underscores)
37    #[validate(length(min = 1, max = 100))]
38    pub name: String,
39
40    /// Project description
41    #[validate(length(min = 1, max = 500))]
42    pub description: String,
43}
44
45/// Provider selection configuration
46#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
47pub struct ProviderConfig {
48    /// Server provider (currently only "hetzner")
49    pub server: String,
50
51    /// Kubernetes provider (currently "k3s" or "kubernetes")
52    pub kubernetes: String,
53
54    /// Database provider (currently only "postgres")
55    pub database: String,
56
57    /// Queue/Message broker provider (currently only "rabbitmq")
58    #[serde(default = "default_queue_provider")]
59    pub queue: String,
60
61    /// DNS provider (currently only "cloudflare")
62    pub dns: String,
63
64    /// Git provider (currently only "gitlab")
65    pub git: String,
66}
67
68fn default_queue_provider() -> String {
69    "rabbitmq".to_string()
70}
71
72/// Applications configuration
73#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
74pub struct AppsConfig {
75    /// List of application names in the workspace
76    #[validate(length(min = 1))]
77    pub applications: Vec<ApplicationEntry>,
78}
79
80/// Application type - determines which template to use
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "kebab-case")]
83pub enum AppType {
84    /// Gateway with authentication (session-based)
85    Gateway,
86    /// API service without authentication
87    Api,
88    /// Database migrator
89    Migrator,
90    /// Basic application (simple Rust binary)
91    Basic,
92}
93
94impl AppType {
95    /// Get all available app types
96    pub fn all() -> Vec<Self> {
97        vec![
98            AppType::Gateway,
99            AppType::Api,
100            AppType::Migrator,
101            AppType::Basic,
102        ]
103    }
104
105    /// Get display name for the app type
106    pub fn display_name(&self) -> &'static str {
107        match self {
108            AppType::Gateway => "Gateway (with authentication)",
109            AppType::Api => "API Service (no auth)",
110            AppType::Migrator => "Database Migrator",
111            AppType::Basic => "Basic Application",
112        }
113    }
114
115    /// Get description for the app type
116    pub fn description(&self) -> &'static str {
117        match self {
118            AppType::Gateway => "HTTP API gateway with session-based authentication, uses SeaORM + PostgreSQL",
119            AppType::Api => "HTTP API service without authentication (for internal services behind gateway)",
120            AppType::Migrator => "SeaORM migration CLI for database schema management",
121            AppType::Basic => "Simple Rust binary application with no HTTP framework",
122        }
123    }
124}
125
126/// Single application entry
127#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
128pub struct ApplicationEntry {
129    /// Application name
130    #[validate(length(min = 1, max = 50))]
131    pub name: String,
132
133    /// Application type - determines which template to use
134    #[serde(default)]
135    pub app_type: Option<AppType>,
136
137    /// Docker configuration for this application
138    #[serde(default)]
139    #[validate(nested)]
140    pub docker: Option<DockerConfig>,
141
142    /// Kubernetes deployment configuration
143    #[serde(default)]
144    #[validate(nested)]
145    pub deployment: Option<DeploymentConfig>,
146}
147
148/// Docker build configuration for an application
149#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
150pub struct DockerConfig {
151    /// Dockerfile path relative to app directory
152    #[serde(default = "default_dockerfile")]
153    pub dockerfile: String,
154
155    /// Build context path relative to workspace root
156    #[serde(default = "default_context")]
157    pub context: String,
158
159    /// Image tags (e.g., ["latest", "v1.0.0"])
160    #[serde(default = "default_tags")]
161    pub tags: Vec<String>,
162}
163
164fn default_dockerfile() -> String {
165    "Dockerfile".to_string()
166}
167
168fn default_context() -> String {
169    ".".to_string()
170}
171
172fn default_tags() -> Vec<String> {
173    vec!["latest".to_string()]
174}
175
176impl Default for DockerConfig {
177    fn default() -> Self {
178        Self {
179            dockerfile: default_dockerfile(),
180            context: default_context(),
181            tags: default_tags(),
182        }
183    }
184}
185
186/// Kubernetes deployment configuration for an application
187#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
188pub struct DeploymentConfig {
189    /// Number of replicas
190    #[serde(default = "default_replicas")]
191    #[validate(range(min = 1, max = 100))]
192    pub replicas: u32,
193
194    /// Container port
195    #[serde(default = "default_port")]
196    #[validate(range(min = 1, max = 65535))]
197    pub port: u16,
198
199    /// CPU request (e.g., "100m", "1")
200    #[serde(default)]
201    pub cpu_request: Option<String>,
202
203    /// Memory request (e.g., "128Mi", "1Gi")
204    #[serde(default)]
205    pub memory_request: Option<String>,
206
207    /// CPU limit (e.g., "1", "2")
208    #[serde(default)]
209    pub cpu_limit: Option<String>,
210
211    /// Memory limit (e.g., "512Mi", "2Gi")
212    #[serde(default)]
213    pub memory_limit: Option<String>,
214
215    /// Environment variables
216    #[serde(default)]
217    pub env: Vec<EnvVar>,
218}
219
220fn default_replicas() -> u32 {
221    1
222}
223
224fn default_port() -> u16 {
225    8080
226}
227
228impl Default for DeploymentConfig {
229    fn default() -> Self {
230        Self {
231            replicas: default_replicas(),
232            port: default_port(),
233            cpu_request: None,
234            memory_request: None,
235            cpu_limit: None,
236            memory_limit: None,
237            env: Vec::new(),
238        }
239    }
240}
241
242/// Environment variable configuration
243#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
244pub struct EnvVar {
245    /// Variable name
246    #[validate(length(min = 1))]
247    pub name: String,
248
249    /// Variable value (for non-sensitive values)
250    #[serde(default)]
251    pub value: Option<String>,
252
253    /// Reference to a secret (alternative to value)
254    #[serde(default)]
255    pub secret_ref: Option<SecretRef>,
256}
257
258/// Reference to a Kubernetes secret
259#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
260pub struct SecretRef {
261    /// Secret name
262    #[validate(length(min = 1))]
263    pub name: String,
264
265    /// Key within the secret
266    #[validate(length(min = 1))]
267    pub key: String,
268}
269
270/// Infrastructure configuration for all providers
271#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
272pub struct InfrastructureConfig {
273    /// Infrastructure provider (e.g., "hetzner", "aws", "digitalocean")
274    #[serde(default = "default_provider")]
275    pub provider: String,
276
277    /// Network configuration
278    #[serde(default)]
279    #[validate(nested)]
280    pub network: Option<NetworkConfig>,
281
282    /// Server groups defining infrastructure topology
283    #[serde(default)]
284    pub servers: Vec<ServerGroup>,
285
286    /// K3s cluster configuration
287    #[validate(nested)]
288    pub k3s: Option<K3sConfig>,
289
290    /// PostgreSQL configuration
291    #[validate(nested)]
292    pub postgres: Option<PostgresConfig>,
293
294    /// RabbitMQ configuration
295    #[validate(nested)]
296    pub rabbitmq: Option<RabbitMqConfig>,
297
298    /// Vault configuration
299    #[validate(nested)]
300    pub vault: Option<VaultConfig>,
301
302    /// DNS configuration
303    #[validate(nested)]
304    pub dns: Option<DnsConfig>,
305
306    /// GitLab-specific configuration
307    #[validate(nested)]
308    pub gitlab: Option<GitLabConfig>,
309
310    /// Load balancer configuration
311    #[serde(default)]
312    #[validate(nested)]
313    pub load_balancer: Option<LoadBalancerConfig>,
314}
315
316fn default_provider() -> String {
317    "hetzner".to_string()
318}
319
320/// Network configuration
321#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
322pub struct NetworkConfig {
323    /// Enable private networking between servers
324    #[serde(default)]
325    pub enable_private_network: bool,
326
327    /// Private network CIDR (e.g., "10.0.0.0/16")
328    pub private_network: Option<String>,
329
330    /// Firewall rules
331    #[serde(default)]
332    pub firewall_rules: Vec<FirewallRule>,
333}
334
335/// Firewall rule configuration
336#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
337pub struct FirewallRule {
338    /// Rule name
339    #[validate(length(min = 1))]
340    pub name: String,
341
342    /// Direction: "inbound" or "outbound"
343    pub direction: String,
344
345    /// Protocol: "tcp", "udp", "icmp", or "any"
346    pub protocol: String,
347
348    /// Port or port range (e.g., "80", "8000-9000")
349    pub port: Option<String>,
350
351    /// Source (CIDR or server group name)
352    pub source: String,
353
354    /// Destination (server group name or "any")
355    pub destination: String,
356
357    /// Action: "allow" or "deny"
358    pub action: String,
359}
360
361/// Server group defining a set of similar servers
362#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
363pub struct ServerGroup {
364    /// Unique name for this server group
365    #[validate(length(min = 1, max = 50))]
366    pub name: String,
367
368    /// Role/purpose of this server group
369    pub role: ServerRole,
370
371    /// Server type (e.g., "cx11", "cpx21" for Hetzner)
372    pub server_type: String,
373
374    /// Location/region (e.g., "nbg1", "fsn1" for Hetzner)
375    pub location: String,
376
377    /// Number of servers in this group
378    #[validate(range(min = 1, max = 50))]
379    pub count: u32,
380
381    /// Labels for organization and selection
382    #[serde(default)]
383    pub labels: HashMap<String, String>,
384
385    /// SSH key names or IDs
386    #[serde(default)]
387    pub ssh_keys: Vec<String>,
388
389    /// Image/OS to use (optional, defaults to provider default)
390    pub image: Option<String>,
391}
392
393/// Server role/purpose
394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
395#[serde(rename_all = "kebab-case")]
396pub enum ServerRole {
397    /// K3s control plane node
398    K3sControl,
399    /// K3s worker node
400    K3sWorker,
401    /// PostgreSQL database server
402    Postgres,
403    /// MySQL/MariaDB database server
404    MySQL,
405    /// MongoDB database server
406    MongoDB,
407    /// Redis cache server
408    Redis,
409    /// Memcached cache server
410    Memcached,
411    /// RabbitMQ message broker
412    RabbitMQ,
413    /// Kafka message broker
414    Kafka,
415    /// Elasticsearch search engine
416    Elasticsearch,
417    /// Load balancer (HAProxy, Nginx, etc.)
418    LoadBalancer,
419    /// Monitoring server (Prometheus, Grafana, etc.)
420    Monitoring,
421    /// CI/CD runner (GitLab Runner, etc.)
422    CIRunner,
423    /// Bastion/Jump server for SSH access
424    Bastion,
425    /// Storage server (MinIO, NFS, etc.)
426    Storage,
427    /// Custom/generic server
428    Custom,
429}
430
431impl ServerRole {
432    /// Get all available server roles
433    pub fn all() -> Vec<Self> {
434        vec![
435            ServerRole::K3sControl,
436            ServerRole::K3sWorker,
437            ServerRole::Postgres,
438            ServerRole::MySQL,
439            ServerRole::MongoDB,
440            ServerRole::Redis,
441            ServerRole::Memcached,
442            ServerRole::RabbitMQ,
443            ServerRole::Kafka,
444            ServerRole::Elasticsearch,
445            ServerRole::LoadBalancer,
446            ServerRole::Monitoring,
447            ServerRole::CIRunner,
448            ServerRole::Bastion,
449            ServerRole::Storage,
450            ServerRole::Custom,
451        ]
452    }
453
454    /// Get display name for the role
455    pub fn display_name(&self) -> &'static str {
456        match self {
457            ServerRole::K3sControl => "K3s Control Plane",
458            ServerRole::K3sWorker => "K3s Worker Node",
459            ServerRole::Postgres => "PostgreSQL Database",
460            ServerRole::MySQL => "MySQL/MariaDB Database",
461            ServerRole::MongoDB => "MongoDB Database",
462            ServerRole::Redis => "Redis Cache",
463            ServerRole::Memcached => "Memcached Cache",
464            ServerRole::RabbitMQ => "RabbitMQ Message Broker",
465            ServerRole::Kafka => "Kafka Message Broker",
466            ServerRole::Elasticsearch => "Elasticsearch Search Engine",
467            ServerRole::LoadBalancer => "Load Balancer (HAProxy/Nginx)",
468            ServerRole::Monitoring => "Monitoring (Prometheus/Grafana)",
469            ServerRole::CIRunner => "CI/CD Runner (GitLab Runner)",
470            ServerRole::Bastion => "Bastion/Jump Server",
471            ServerRole::Storage => "Storage (MinIO/NFS)",
472            ServerRole::Custom => "Custom/Generic Server",
473        }
474    }
475
476    /// Get description for the role
477    pub fn description(&self) -> &'static str {
478        match self {
479            ServerRole::K3sControl => "Control plane node for K3s cluster management",
480            ServerRole::K3sWorker => "Worker node for running application workloads",
481            ServerRole::Postgres => "PostgreSQL relational database server",
482            ServerRole::MySQL => "MySQL or MariaDB relational database server",
483            ServerRole::MongoDB => "MongoDB NoSQL document database",
484            ServerRole::Redis => "In-memory data store and cache",
485            ServerRole::Memcached => "Distributed memory caching system",
486            ServerRole::RabbitMQ => "Message broker for async communication",
487            ServerRole::Kafka => "Distributed streaming platform and message broker",
488            ServerRole::Elasticsearch => "Distributed search and analytics engine",
489            ServerRole::LoadBalancer => "Traffic distribution and load balancing",
490            ServerRole::Monitoring => "Metrics collection and visualization",
491            ServerRole::CIRunner => "Continuous integration and deployment runner",
492            ServerRole::Bastion => "Secure SSH gateway for infrastructure access",
493            ServerRole::Storage => "Object storage or network file system",
494            ServerRole::Custom => "Custom server with user-defined purpose",
495        }
496    }
497}
498
499/// Setup mode for infrastructure configuration
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum SetupMode {
502    /// Quick setup - single K3s server (development/testing)
503    Quick,
504    /// Standard setup - HA production with control plane, workers, and database
505    Standard,
506    /// Advanced setup - full control over server topology
507    Advanced,
508}
509
510impl SetupMode {
511    /// Get all available setup modes
512    pub fn all() -> Vec<Self> {
513        vec![SetupMode::Quick, SetupMode::Standard, SetupMode::Advanced]
514    }
515
516    /// Get display name for the mode
517    pub fn display_name(&self) -> &'static str {
518        match self {
519            SetupMode::Quick => "Quick - Single Server (Development)",
520            SetupMode::Standard => "Standard - Multi-Server HA (Production)",
521            SetupMode::Advanced => "Advanced - Custom Topology",
522        }
523    }
524
525    /// Get description for the mode
526    pub fn description(&self) -> &'static str {
527        match self {
528            SetupMode::Quick => {
529                "Single K3s server for development and testing. Fastest to set up, lowest cost."
530            }
531            SetupMode::Standard => {
532                "Production-ready HA setup with control plane, workers, and dedicated database server."
533            }
534            SetupMode::Advanced => {
535                "Full control over infrastructure topology. Configure multiple server groups with custom roles."
536            }
537        }
538    }
539}
540
541/// K3s cluster configuration
542#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
543pub struct K3sConfig {
544    /// K3s version (e.g., "v1.28.5+k3s1")
545    pub version: String,
546
547    /// Server groups that are part of the cluster
548    #[validate(length(min = 1))]
549    pub deploy_on: Vec<String>,
550
551    /// Server groups that are control plane nodes
552    #[validate(length(min = 1))]
553    pub control_plane_servers: Vec<String>,
554
555    /// Server groups that are worker nodes (can be empty for single-node clusters)
556    #[serde(default)]
557    pub worker_servers: Vec<String>,
558
559    /// Enable Traefik ingress controller
560    #[serde(default = "default_true")]
561    pub enable_traefik: bool,
562
563    /// Enable metrics server
564    #[serde(default)]
565    pub enable_metrics_server: bool,
566
567    /// Additional flags for k3s server
568    #[serde(default)]
569    pub server_flags: Vec<String>,
570
571    /// Additional flags for k3s agent
572    #[serde(default)]
573    pub agent_flags: Vec<String>,
574}
575
576fn default_true() -> bool {
577    true
578}
579
580/// PostgreSQL configuration
581#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
582pub struct PostgresConfig {
583    /// PostgreSQL version
584    pub version: String,
585
586    /// Database name
587    #[validate(length(min = 1, max = 63))]
588    pub database_name: String,
589
590    /// Deployment mode
591    pub deployment_mode: PostgresDeploymentMode,
592
593    /// Configuration for standalone mode
594    #[serde(skip_serializing_if = "Option::is_none")]
595    #[validate(nested)]
596    pub standalone: Option<PostgresStandaloneConfig>,
597
598    /// Configuration for in-cluster mode
599    #[serde(skip_serializing_if = "Option::is_none")]
600    #[validate(nested)]
601    pub in_cluster: Option<PostgresInClusterConfig>,
602}
603
604/// PostgreSQL deployment mode
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
606#[serde(rename_all = "kebab-case")]
607pub enum PostgresDeploymentMode {
608    /// Standalone server (recommended for production)
609    Standalone,
610    /// Deployed in Kubernetes cluster
611    InCluster,
612}
613
614/// PostgreSQL standalone deployment configuration
615#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
616pub struct PostgresStandaloneConfig {
617    /// Server group to deploy on
618    #[validate(length(min = 1))]
619    pub deploy_on: String,
620
621    /// Data directory (optional, uses default if not specified)
622    pub data_dir: Option<String>,
623
624    /// Max connections (optional, uses default if not specified)
625    pub max_connections: Option<u32>,
626
627    /// Shared buffers setting (e.g., "256MB")
628    pub shared_buffers: Option<String>,
629}
630
631/// PostgreSQL in-cluster deployment configuration
632#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
633pub struct PostgresInClusterConfig {
634    /// Namespace to deploy in
635    #[validate(length(min = 1))]
636    pub namespace: String,
637
638    /// Storage class for persistent volume
639    pub storage_class: String,
640
641    /// Storage size (e.g., "20Gi")
642    pub storage_size: String,
643
644    /// Use operator (e.g., Zalando postgres-operator)
645    #[serde(default)]
646    pub use_operator: bool,
647}
648
649/// RabbitMQ configuration
650#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
651pub struct RabbitMqConfig {
652    /// RabbitMQ version
653    pub version: String,
654
655    /// Deployment mode
656    pub deployment_mode: RabbitMqDeploymentMode,
657
658    /// Configuration for standalone mode
659    #[serde(skip_serializing_if = "Option::is_none")]
660    #[validate(nested)]
661    pub standalone: Option<RabbitMqStandaloneConfig>,
662
663    /// Configuration for in-cluster mode
664    #[serde(skip_serializing_if = "Option::is_none")]
665    #[validate(nested)]
666    pub in_cluster: Option<RabbitMqInClusterConfig>,
667}
668
669/// RabbitMQ deployment mode
670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
671#[serde(rename_all = "kebab-case")]
672pub enum RabbitMqDeploymentMode {
673    /// Standalone server (recommended for production)
674    Standalone,
675    /// Deployed in Kubernetes cluster
676    InCluster,
677}
678
679/// RabbitMQ standalone deployment configuration
680#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
681pub struct RabbitMqStandaloneConfig {
682    /// Server group to deploy on
683    #[validate(length(min = 1))]
684    pub deploy_on: String,
685
686    /// Enable management plugin
687    #[serde(default = "default_true")]
688    pub enable_management: bool,
689
690    /// Management plugin port (default 15672)
691    #[serde(default = "default_management_port")]
692    pub management_port: u16,
693
694    /// AMQP port (default 5672)
695    #[serde(default = "default_amqp_port")]
696    pub amqp_port: u16,
697}
698
699fn default_management_port() -> u16 {
700    15672
701}
702
703fn default_amqp_port() -> u16 {
704    5672
705}
706
707/// RabbitMQ in-cluster deployment configuration
708#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
709pub struct RabbitMqInClusterConfig {
710    /// Namespace to deploy in
711    #[validate(length(min = 1))]
712    pub namespace: String,
713
714    /// Storage class for persistent volume
715    pub storage_class: String,
716
717    /// Storage size (e.g., "10Gi")
718    pub storage_size: String,
719
720    /// Number of replicas (for HA)
721    #[serde(default = "default_rabbitmq_replicas")]
722    #[validate(range(min = 1, max = 10))]
723    pub replicas: u32,
724
725    /// Use RabbitMQ cluster operator
726    #[serde(default)]
727    pub use_operator: bool,
728}
729
730fn default_rabbitmq_replicas() -> u32 {
731    1
732}
733
734/// Vault configuration
735#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
736pub struct VaultConfig {
737    /// Vault version
738    pub version: String,
739
740    /// Deployment mode
741    pub deployment_mode: VaultDeploymentMode,
742
743    /// Configuration for standalone mode
744    #[serde(skip_serializing_if = "Option::is_none")]
745    #[validate(nested)]
746    pub standalone: Option<VaultStandaloneConfig>,
747
748    /// Configuration for in-cluster mode
749    #[serde(skip_serializing_if = "Option::is_none")]
750    #[validate(nested)]
751    pub in_cluster: Option<VaultInClusterConfig>,
752}
753
754/// Vault deployment mode
755#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
756#[serde(rename_all = "kebab-case")]
757pub enum VaultDeploymentMode {
758    /// Standalone server (recommended for production)
759    Standalone,
760    /// Deployed in Kubernetes cluster
761    InCluster,
762}
763
764/// Vault standalone deployment configuration
765#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
766pub struct VaultStandaloneConfig {
767    /// Server group to deploy on
768    #[validate(length(min = 1))]
769    pub deploy_on: String,
770
771    /// Enable Vault UI
772    #[serde(default = "default_true")]
773    pub enable_ui: bool,
774
775    /// API port (default 8200)
776    #[serde(default = "default_vault_port")]
777    pub api_port: u16,
778}
779
780fn default_vault_port() -> u16 {
781    8200
782}
783
784/// Vault in-cluster deployment configuration
785#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
786pub struct VaultInClusterConfig {
787    /// Namespace to deploy in
788    #[validate(length(min = 1))]
789    pub namespace: String,
790
791    /// Number of replicas (for HA)
792    #[serde(default = "default_vault_replicas")]
793    #[validate(range(min = 1, max = 5))]
794    pub replicas: u32,
795
796    /// Storage size per replica (e.g., "10Gi")
797    #[serde(default = "default_vault_storage")]
798    pub storage_size: String,
799
800    /// Storage class for persistent volume
801    pub storage_class: Option<String>,
802
803    /// Enable Vault UI
804    #[serde(default = "default_true")]
805    pub enable_ui: bool,
806
807    /// Service type (ClusterIP, NodePort, LoadBalancer)
808    #[serde(default = "default_service_type")]
809    pub service_type: String,
810
811    /// NodePort for Vault API (if service_type is NodePort)
812    pub node_port: Option<u16>,
813
814    /// Ingress hostname for external access
815    pub ingress_host: Option<String>,
816
817    /// Enable TLS for ingress
818    #[serde(default = "default_true")]
819    pub enable_tls: bool,
820}
821
822fn default_vault_replicas() -> u32 {
823    1
824}
825
826fn default_vault_storage() -> String {
827    "10Gi".to_string()
828}
829
830fn default_service_type() -> String {
831    "ClusterIP".to_string()
832}
833
834/// DNS configuration
835#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
836pub struct DnsConfig {
837    /// DNS provider (e.g., "cloudflare", "route53")
838    pub provider: String,
839
840    /// Primary domain
841    #[validate(length(min = 1))]
842    pub domain: String,
843
844    /// DNS records
845    #[serde(default)]
846    pub records: Vec<DnsRecord>,
847}
848
849/// DNS record configuration
850#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
851pub struct DnsRecord {
852    /// Record type (A, AAAA, CNAME, MX, TXT, etc.)
853    #[serde(rename = "type")]
854    pub record_type: String,
855
856    /// Record name (e.g., "@", "www", "api")
857    #[validate(length(min = 1))]
858    pub name: String,
859
860    /// Target - can be:
861    /// - Server group name (will use public IP)
862    /// - IP address
863    /// - Domain name (for CNAME)
864    pub target: String,
865
866    /// TTL in seconds
867    #[serde(default = "default_ttl")]
868    pub ttl: u32,
869
870    /// Enable Cloudflare proxy (Cloudflare-specific)
871    #[serde(default)]
872    pub proxied: bool,
873}
874
875fn default_ttl() -> u32 {
876    300
877}
878
879/// GitLab configuration
880#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
881pub struct GitLabConfig {
882    /// GitLab instance URL
883    #[validate(url)]
884    pub url: String,
885
886    /// Project namespace/group
887    #[validate(length(min = 1))]
888    pub namespace: String,
889}
890
891/// Load balancer configuration
892#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
893pub struct LoadBalancerConfig {
894    /// Load balancer name
895    #[validate(length(min = 1, max = 50))]
896    pub name: String,
897
898    /// Load balancer type (e.g., "lb11", "lb21" for Hetzner)
899    #[serde(rename = "type")]
900    pub lb_type: String,
901
902    /// Location/region (e.g., "nbg1", "fsn1" for Hetzner)
903    pub location: String,
904
905    /// Load balancing algorithm (e.g., "round_robin", "least_connections")
906    pub algorithm: String,
907
908    /// Target servers for load balancer
909    #[validate(length(min = 1))]
910    pub targets: Vec<LoadBalancerTarget>,
911
912    /// Services/ports to expose
913    #[validate(length(min = 1))]
914    pub services: Vec<LoadBalancerService>,
915}
916
917/// Load balancer target configuration
918#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
919pub struct LoadBalancerTarget {
920    /// Target type (e.g., "server", "label_selector", "ip")
921    #[serde(rename = "type")]
922    pub target_type: String,
923
924    /// Use private IP for target
925    #[serde(default)]
926    pub use_private_ip: bool,
927
928    /// Server group names to use as targets
929    #[validate(length(min = 1))]
930    pub servers: Vec<String>,
931}
932
933/// Load balancer service/port configuration
934#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
935pub struct LoadBalancerService {
936    /// Protocol (http, https, tcp)
937    pub protocol: String,
938
939    /// Listen port (port on load balancer)
940    #[validate(range(min = 1, max = 65535))]
941    pub listen_port: u16,
942
943    /// Destination port (port on target servers)
944    #[validate(range(min = 1, max = 65535))]
945    pub destination_port: u16,
946
947    /// Health check protocol (http, https, tcp)
948    pub health_check_protocol: String,
949
950    /// Health check port
951    #[validate(range(min = 1, max = 65535))]
952    pub health_check_port: u16,
953
954    /// Health check path (for HTTP/HTTPS)
955    #[serde(default = "default_health_check_path")]
956    pub health_check_path: String,
957
958    /// Health check interval in seconds
959    #[serde(default = "default_health_check_interval")]
960    #[validate(range(min = 1, max = 3600))]
961    pub health_check_interval: u64,
962
963    /// Health check timeout in seconds
964    #[serde(default = "default_health_check_timeout")]
965    #[validate(range(min = 1, max = 300))]
966    pub health_check_timeout: u64,
967
968    /// Health check retries
969    #[serde(default = "default_health_check_retries")]
970    #[validate(range(min = 1, max = 10))]
971    pub health_check_retries: u64,
972
973    /// Enable HTTP to HTTPS redirect (for HTTPS services)
974    #[serde(default)]
975    pub http_redirect_http_to_https: bool,
976}
977
978fn default_health_check_path() -> String {
979    "/".to_string()
980}
981
982fn default_health_check_interval() -> u64 {
983    15
984}
985
986fn default_health_check_timeout() -> u64 {
987    10
988}
989
990fn default_health_check_retries() -> u64 {
991    3
992}
993
994/// Configuration validation errors
995#[derive(Debug, Error)]
996pub enum ConfigError {
997    /// Invalid TOML format
998    #[error("Invalid TOML format: {0}")]
999    InvalidToml(String),
1000
1001    /// TOML deserialization error
1002    #[error("TOML deserialization error: {0}")]
1003    TomlDe(#[from] toml::de::Error),
1004
1005    /// Validation failed
1006    #[error("Configuration validation failed: {0}")]
1007    ValidationFailed(#[from] validator::ValidationErrors),
1008
1009    /// IO error
1010    #[error("IO error: {0}")]
1011    Io(#[from] std::io::Error),
1012
1013    /// Missing required provider configuration
1014    #[error("Missing required configuration for provider: {0}")]
1015    MissingProviderConfig(String),
1016
1017    /// Architecture principle violation
1018    #[error("Architecture violation: {0}")]
1019    ArchitectureViolation(String),
1020}
1021
1022impl LmrcConfig {
1023    /// Load configuration from a TOML file
1024    pub fn from_file(path: &std::path::Path) -> Result<Self, ConfigError> {
1025        let content = std::fs::read_to_string(path)?;
1026        Self::from_toml_str(&content)
1027    }
1028
1029    /// Parse configuration from a TOML string
1030    pub fn from_toml_str(content: &str) -> Result<Self, ConfigError> {
1031        let config: Self = toml::from_str(content)?;
1032        config.validate()?;
1033        config.validate_provider_configs()?;
1034        Ok(config)
1035    }
1036
1037    /// Validate that provider-specific configs are present when providers are selected
1038    fn validate_provider_configs(&self) -> Result<(), ConfigError> {
1039        // Validate server provider config
1040        if self.infrastructure.servers.is_empty() {
1041            return Err(ConfigError::MissingProviderConfig(
1042                "servers (at least one server group required)".to_string(),
1043            ));
1044        }
1045
1046        // Validate kubernetes provider config
1047        if self.providers.kubernetes == "k3s" && self.infrastructure.k3s.is_none() {
1048            return Err(ConfigError::MissingProviderConfig("k3s".to_string()));
1049        }
1050
1051        // Validate database provider config
1052        if self.providers.database == "postgres" && self.infrastructure.postgres.is_none() {
1053            return Err(ConfigError::MissingProviderConfig("postgres".to_string()));
1054        }
1055
1056        // Validate DNS provider config
1057        if self.infrastructure.dns.is_none() {
1058            return Err(ConfigError::MissingProviderConfig("dns".to_string()));
1059        }
1060
1061        // Validate Git provider config
1062        if self.providers.git == "gitlab" && self.infrastructure.gitlab.is_none() {
1063            return Err(ConfigError::MissingProviderConfig("gitlab".to_string()));
1064        }
1065
1066        // Validate PostgreSQL deployment mode consistency
1067        if let Some(postgres) = &self.infrastructure.postgres {
1068            match postgres.deployment_mode {
1069                PostgresDeploymentMode::Standalone => {
1070                    if postgres.standalone.is_none() {
1071                        return Err(ConfigError::InvalidToml(
1072                            "PostgreSQL standalone mode requires 'standalone' configuration"
1073                                .to_string(),
1074                        ));
1075                    }
1076                    // Validate that deploy_on references a valid server group
1077                    if let Some(standalone) = &postgres.standalone
1078                        && !self
1079                            .infrastructure
1080                            .servers
1081                            .iter()
1082                            .any(|s| s.name == standalone.deploy_on)
1083                    {
1084                        return Err(ConfigError::InvalidToml(format!(
1085                            "PostgreSQL standalone deploy_on '{}' does not reference a valid server group",
1086                            standalone.deploy_on
1087                        )));
1088                    }
1089                }
1090                PostgresDeploymentMode::InCluster => {
1091                    // ARCHITECTURE PRINCIPLE: Stateless Kubernetes - No In-Cluster Services
1092                    return Err(ConfigError::ArchitectureViolation(
1093                        "PostgreSQL in-cluster deployment is not allowed. \
1094                        LMRC Stack architecture principle: 'Stateless Kubernetes - No In-Cluster Services'. \
1095                        Stateful services (databases, message queues) must run on dedicated servers, \
1096                        not in Kubernetes. Please use deployment_mode = 'standalone' instead. \
1097                        See docs/architecture/PRINCIPLES.md for details.".to_string()
1098                    ));
1099                }
1100            }
1101        }
1102
1103        // Validate RabbitMQ deployment mode consistency
1104        if let Some(rabbitmq) = &self.infrastructure.rabbitmq {
1105            match rabbitmq.deployment_mode {
1106                RabbitMqDeploymentMode::Standalone => {
1107                    if rabbitmq.standalone.is_none() {
1108                        return Err(ConfigError::InvalidToml(
1109                            "RabbitMQ standalone mode requires 'standalone' configuration"
1110                                .to_string(),
1111                        ));
1112                    }
1113                    // Validate that deploy_on references a valid server group
1114                    if let Some(standalone) = &rabbitmq.standalone
1115                        && !self
1116                            .infrastructure
1117                            .servers
1118                            .iter()
1119                            .any(|s| s.name == standalone.deploy_on)
1120                    {
1121                        return Err(ConfigError::InvalidToml(format!(
1122                            "RabbitMQ standalone deploy_on '{}' does not reference a valid server group",
1123                            standalone.deploy_on
1124                        )));
1125                    }
1126                }
1127                RabbitMqDeploymentMode::InCluster => {
1128                    // ARCHITECTURE PRINCIPLE: Stateless Kubernetes - No In-Cluster Services
1129                    return Err(ConfigError::ArchitectureViolation(
1130                        "RabbitMQ in-cluster deployment is not allowed. \
1131                        LMRC Stack architecture principle: 'Stateless Kubernetes - No In-Cluster Services'. \
1132                        Stateful services (databases, message queues) must run on dedicated servers, \
1133                        not in Kubernetes. Please use deployment_mode = 'standalone' instead. \
1134                        See docs/architecture/PRINCIPLES.md for details.".to_string()
1135                    ));
1136                }
1137            }
1138        }
1139
1140        // Validate Vault deployment mode consistency
1141        if let Some(vault) = &self.infrastructure.vault {
1142            match vault.deployment_mode {
1143                VaultDeploymentMode::Standalone => {
1144                    if vault.standalone.is_none() {
1145                        return Err(ConfigError::InvalidToml(
1146                            "Vault standalone mode requires 'standalone' configuration"
1147                                .to_string(),
1148                        ));
1149                    }
1150                    // Validate that deploy_on references a valid server group
1151                    if let Some(standalone) = &vault.standalone
1152                        && !self
1153                            .infrastructure
1154                            .servers
1155                            .iter()
1156                            .any(|s| s.name == standalone.deploy_on)
1157                    {
1158                        return Err(ConfigError::InvalidToml(format!(
1159                            "Vault standalone deploy_on '{}' does not reference a valid server group",
1160                            standalone.deploy_on
1161                        )));
1162                    }
1163                }
1164                VaultDeploymentMode::InCluster => {
1165                    if vault.in_cluster.is_none() {
1166                        return Err(ConfigError::InvalidToml(
1167                            "Vault in-cluster mode requires 'in_cluster' configuration"
1168                                .to_string(),
1169                        ));
1170                    }
1171                }
1172            }
1173        }
1174
1175        // Validate Load Balancer configuration
1176        if let Some(lb) = &self.infrastructure.load_balancer {
1177            // Validate that target servers reference valid server groups
1178            for target in &lb.targets {
1179                for server_name in &target.servers {
1180                    if !self
1181                        .infrastructure
1182                        .servers
1183                        .iter()
1184                        .any(|s| s.name == *server_name)
1185                    {
1186                        return Err(ConfigError::InvalidToml(format!(
1187                            "Load balancer target server '{}' does not reference a valid server group",
1188                            server_name
1189                        )));
1190                    }
1191                }
1192            }
1193        }
1194
1195        // Validate K3s server group references
1196        if let Some(k3s) = &self.infrastructure.k3s {
1197            for server_group in &k3s.deploy_on {
1198                if !self
1199                    .infrastructure
1200                    .servers
1201                    .iter()
1202                    .any(|s| s.name == *server_group)
1203                {
1204                    return Err(ConfigError::InvalidToml(format!(
1205                        "K3s deploy_on '{}' does not reference a valid server group",
1206                        server_group
1207                    )));
1208                }
1209            }
1210            for server_group in &k3s.control_plane_servers {
1211                if !self
1212                    .infrastructure
1213                    .servers
1214                    .iter()
1215                    .any(|s| s.name == *server_group)
1216                {
1217                    return Err(ConfigError::InvalidToml(format!(
1218                        "K3s control_plane_servers '{}' does not reference a valid server group",
1219                        server_group
1220                    )));
1221                }
1222            }
1223            for server_group in &k3s.worker_servers {
1224                if !self
1225                    .infrastructure
1226                    .servers
1227                    .iter()
1228                    .any(|s| s.name == *server_group)
1229                {
1230                    return Err(ConfigError::InvalidToml(format!(
1231                        "K3s worker_servers '{}' does not reference a valid server group",
1232                        server_group
1233                    )));
1234                }
1235            }
1236        }
1237
1238        Ok(())
1239    }
1240
1241    /// Generate a default/template configuration
1242    pub fn template() -> Self {
1243        let mut labels = HashMap::new();
1244        labels.insert("environment".to_string(), "production".to_string());
1245
1246        Self {
1247            project: ProjectConfig {
1248                name: "my-project".to_string(),
1249                description: "My LMRC Stack project".to_string(),
1250            },
1251            providers: ProviderConfig {
1252                server: "hetzner".to_string(),
1253                kubernetes: "k3s".to_string(),
1254                database: "postgres".to_string(),
1255                queue: "rabbitmq".to_string(),
1256                dns: "cloudflare".to_string(),
1257                git: "gitlab".to_string(),
1258            },
1259            apps: AppsConfig {
1260                applications: vec![ApplicationEntry {
1261                    name: "api".to_string(),
1262                    app_type: Some(AppType::Api),
1263                    docker: Some(DockerConfig {
1264                        dockerfile: "apps/api/Dockerfile".to_string(),
1265                        context: "apps/api".to_string(),
1266                        tags: vec!["latest".to_string()],
1267                    }),
1268                    deployment: Some(DeploymentConfig {
1269                        replicas: 2,
1270                        port: 8080,
1271                        cpu_request: Some("100m".to_string()),
1272                        memory_request: Some("128Mi".to_string()),
1273                        cpu_limit: Some("1".to_string()),
1274                        memory_limit: Some("512Mi".to_string()),
1275                        env: vec![],
1276                    }),
1277                }],
1278            },
1279            infrastructure: InfrastructureConfig {
1280                provider: "hetzner".to_string(),
1281                network: Some(NetworkConfig {
1282                    enable_private_network: true,
1283                    private_network: Some("10.0.0.0/16".to_string()),
1284                    firewall_rules: vec![],
1285                }),
1286                servers: vec![
1287                    ServerGroup {
1288                        name: "k3s-control".to_string(),
1289                        role: ServerRole::K3sControl,
1290                        server_type: "cpx11".to_string(),
1291                        location: "nbg1".to_string(),
1292                        count: 1,
1293                        labels: labels.clone(),
1294                        ssh_keys: vec![],
1295                        image: None,
1296                    },
1297                    ServerGroup {
1298                        name: "k3s-workers".to_string(),
1299                        role: ServerRole::K3sWorker,
1300                        server_type: "cx21".to_string(),
1301                        location: "nbg1".to_string(),
1302                        count: 2,
1303                        labels: labels.clone(),
1304                        ssh_keys: vec![],
1305                        image: None,
1306                    },
1307                    ServerGroup {
1308                        name: "postgres-server".to_string(),
1309                        role: ServerRole::Postgres,
1310                        server_type: "cx31".to_string(),
1311                        location: "nbg1".to_string(),
1312                        count: 1,
1313                        labels,
1314                        ssh_keys: vec![],
1315                        image: None,
1316                    },
1317                ],
1318                k3s: Some(K3sConfig {
1319                    version: "v1.28.5+k3s1".to_string(),
1320                    deploy_on: vec!["k3s-control".to_string(), "k3s-workers".to_string()],
1321                    control_plane_servers: vec!["k3s-control".to_string()],
1322                    worker_servers: vec!["k3s-workers".to_string()],
1323                    enable_traefik: true,
1324                    enable_metrics_server: false,
1325                    server_flags: vec![],
1326                    agent_flags: vec![],
1327                }),
1328                postgres: Some(PostgresConfig {
1329                    version: "16".to_string(),
1330                    database_name: "myapp".to_string(),
1331                    deployment_mode: PostgresDeploymentMode::Standalone,
1332                    standalone: Some(PostgresStandaloneConfig {
1333                        deploy_on: "postgres-server".to_string(),
1334                        data_dir: None,
1335                        max_connections: None,
1336                        shared_buffers: None,
1337                    }),
1338                    in_cluster: None,
1339                }),
1340                rabbitmq: None,
1341                vault: None,
1342                dns: Some(DnsConfig {
1343                    provider: "cloudflare".to_string(),
1344                    domain: "example.com".to_string(),
1345                    records: vec![
1346                        DnsRecord {
1347                            record_type: "A".to_string(),
1348                            name: "@".to_string(),
1349                            target: "k3s-control".to_string(),
1350                            ttl: 300,
1351                            proxied: true,
1352                        },
1353                        DnsRecord {
1354                            record_type: "A".to_string(),
1355                            name: "api".to_string(),
1356                            target: "k3s-control".to_string(),
1357                            ttl: 300,
1358                            proxied: true,
1359                        },
1360                    ],
1361                }),
1362                gitlab: Some(GitLabConfig {
1363                    url: "https://gitlab.com".to_string(),
1364                    namespace: "mygroup".to_string(),
1365                }),
1366                load_balancer: None,
1367            },
1368        }
1369    }
1370
1371    /// Convert config to TOML string
1372    pub fn to_toml_string(&self) -> Result<String, ConfigError> {
1373        toml::to_string_pretty(self).map_err(|e| ConfigError::InvalidToml(e.to_string()))
1374    }
1375}
1376
1377#[cfg(test)]
1378mod tests {
1379    use super::*;
1380
1381    #[test]
1382    fn test_template_config_is_valid() {
1383        let config = LmrcConfig::template();
1384        assert!(config.validate().is_ok());
1385        assert!(config.validate_provider_configs().is_ok());
1386    }
1387
1388    #[test]
1389    fn test_config_to_toml_and_back() {
1390        let config = LmrcConfig::template();
1391        let toml_str = config.to_toml_string().unwrap();
1392        let parsed = LmrcConfig::from_toml_str(&toml_str).unwrap();
1393        assert_eq!(config.project.name, parsed.project.name);
1394    }
1395
1396    #[test]
1397    fn test_missing_provider_config() {
1398        let mut config = LmrcConfig::template();
1399        config.infrastructure.servers = vec![];
1400        assert!(config.validate_provider_configs().is_err());
1401    }
1402
1403    #[test]
1404    fn test_invalid_server_group_reference() {
1405        let mut config = LmrcConfig::template();
1406        if let Some(ref mut postgres) = config.infrastructure.postgres {
1407            if let Some(ref mut standalone) = postgres.standalone {
1408                standalone.deploy_on = "nonexistent-server".to_string();
1409            }
1410        }
1411        assert!(config.validate_provider_configs().is_err());
1412    }
1413}