1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use thiserror::Error;
9use validator::Validate;
10
11pub mod providers;
12
13#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
15pub struct LmrcConfig {
16 #[validate(nested)]
18 pub project: ProjectConfig,
19
20 #[validate(nested)]
22 pub providers: ProviderConfig,
23
24 #[validate(nested)]
26 pub apps: AppsConfig,
27
28 #[validate(nested)]
30 pub infrastructure: InfrastructureConfig,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
35pub struct ProjectConfig {
36 #[validate(length(min = 1, max = 100))]
38 pub name: String,
39
40 #[validate(length(min = 1, max = 500))]
42 pub description: String,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
47pub struct ProviderConfig {
48 pub server: String,
50
51 pub kubernetes: String,
53
54 pub database: String,
56
57 pub dns: String,
59
60 pub git: String,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
66pub struct AppsConfig {
67 #[validate(length(min = 1))]
69 pub applications: Vec<ApplicationEntry>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "kebab-case")]
75pub enum AppType {
76 Gateway,
78 Api,
80 Migrator,
82 Basic,
84}
85
86impl AppType {
87 pub fn all() -> Vec<Self> {
89 vec![
90 AppType::Gateway,
91 AppType::Api,
92 AppType::Migrator,
93 AppType::Basic,
94 ]
95 }
96
97 pub fn display_name(&self) -> &'static str {
99 match self {
100 AppType::Gateway => "Gateway (with authentication)",
101 AppType::Api => "API Service (no auth)",
102 AppType::Migrator => "Database Migrator",
103 AppType::Basic => "Basic Application",
104 }
105 }
106
107 pub fn description(&self) -> &'static str {
109 match self {
110 AppType::Gateway => "HTTP API gateway with session-based authentication, uses SeaORM + PostgreSQL",
111 AppType::Api => "HTTP API service without authentication (for internal services behind gateway)",
112 AppType::Migrator => "SeaORM migration CLI for database schema management",
113 AppType::Basic => "Simple Rust binary application with no HTTP framework",
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
120pub struct ApplicationEntry {
121 #[validate(length(min = 1, max = 50))]
123 pub name: String,
124
125 #[serde(default)]
127 pub app_type: Option<AppType>,
128
129 #[serde(default)]
131 #[validate(nested)]
132 pub docker: Option<DockerConfig>,
133
134 #[serde(default)]
136 #[validate(nested)]
137 pub deployment: Option<DeploymentConfig>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
142pub struct DockerConfig {
143 #[serde(default = "default_dockerfile")]
145 pub dockerfile: String,
146
147 #[serde(default = "default_context")]
149 pub context: String,
150
151 #[serde(default = "default_tags")]
153 pub tags: Vec<String>,
154}
155
156fn default_dockerfile() -> String {
157 "Dockerfile".to_string()
158}
159
160fn default_context() -> String {
161 ".".to_string()
162}
163
164fn default_tags() -> Vec<String> {
165 vec!["latest".to_string()]
166}
167
168impl Default for DockerConfig {
169 fn default() -> Self {
170 Self {
171 dockerfile: default_dockerfile(),
172 context: default_context(),
173 tags: default_tags(),
174 }
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
180pub struct DeploymentConfig {
181 #[serde(default = "default_replicas")]
183 #[validate(range(min = 1, max = 100))]
184 pub replicas: u32,
185
186 #[serde(default = "default_port")]
188 #[validate(range(min = 1, max = 65535))]
189 pub port: u16,
190
191 #[serde(default)]
193 pub cpu_request: Option<String>,
194
195 #[serde(default)]
197 pub memory_request: Option<String>,
198
199 #[serde(default)]
201 pub cpu_limit: Option<String>,
202
203 #[serde(default)]
205 pub memory_limit: Option<String>,
206
207 #[serde(default)]
209 pub env: Vec<EnvVar>,
210}
211
212fn default_replicas() -> u32 {
213 1
214}
215
216fn default_port() -> u16 {
217 8080
218}
219
220impl Default for DeploymentConfig {
221 fn default() -> Self {
222 Self {
223 replicas: default_replicas(),
224 port: default_port(),
225 cpu_request: None,
226 memory_request: None,
227 cpu_limit: None,
228 memory_limit: None,
229 env: Vec::new(),
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
236pub struct EnvVar {
237 #[validate(length(min = 1))]
239 pub name: String,
240
241 #[serde(default)]
243 pub value: Option<String>,
244
245 #[serde(default)]
247 pub secret_ref: Option<SecretRef>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
252pub struct SecretRef {
253 #[validate(length(min = 1))]
255 pub name: String,
256
257 #[validate(length(min = 1))]
259 pub key: String,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
264pub struct InfrastructureConfig {
265 #[serde(default = "default_provider")]
267 pub provider: String,
268
269 #[serde(default)]
271 #[validate(nested)]
272 pub network: Option<NetworkConfig>,
273
274 #[serde(default)]
276 pub servers: Vec<ServerGroup>,
277
278 #[validate(nested)]
280 pub k3s: Option<K3sConfig>,
281
282 #[validate(nested)]
284 pub postgres: Option<PostgresConfig>,
285
286 #[validate(nested)]
288 pub dns: Option<DnsConfig>,
289
290 #[validate(nested)]
292 pub gitlab: Option<GitLabConfig>,
293}
294
295fn default_provider() -> String {
296 "hetzner".to_string()
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
301pub struct NetworkConfig {
302 #[serde(default)]
304 pub enable_private_network: bool,
305
306 pub private_network: Option<String>,
308
309 #[serde(default)]
311 pub firewall_rules: Vec<FirewallRule>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
316pub struct FirewallRule {
317 #[validate(length(min = 1))]
319 pub name: String,
320
321 pub direction: String,
323
324 pub protocol: String,
326
327 pub port: Option<String>,
329
330 pub source: String,
332
333 pub destination: String,
335
336 pub action: String,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
342pub struct ServerGroup {
343 #[validate(length(min = 1, max = 50))]
345 pub name: String,
346
347 pub role: ServerRole,
349
350 pub server_type: String,
352
353 pub location: String,
355
356 #[validate(range(min = 1, max = 50))]
358 pub count: u32,
359
360 #[serde(default)]
362 pub labels: HashMap<String, String>,
363
364 #[serde(default)]
366 pub ssh_keys: Vec<String>,
367
368 pub image: Option<String>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
374#[serde(rename_all = "kebab-case")]
375pub enum ServerRole {
376 K3sControl,
378 K3sWorker,
380 Postgres,
382 MySQL,
384 MongoDB,
386 Redis,
388 Memcached,
390 RabbitMQ,
392 Kafka,
394 Elasticsearch,
396 LoadBalancer,
398 Monitoring,
400 CIRunner,
402 Bastion,
404 Storage,
406 Custom,
408}
409
410impl ServerRole {
411 pub fn all() -> Vec<Self> {
413 vec![
414 ServerRole::K3sControl,
415 ServerRole::K3sWorker,
416 ServerRole::Postgres,
417 ServerRole::MySQL,
418 ServerRole::MongoDB,
419 ServerRole::Redis,
420 ServerRole::Memcached,
421 ServerRole::RabbitMQ,
422 ServerRole::Kafka,
423 ServerRole::Elasticsearch,
424 ServerRole::LoadBalancer,
425 ServerRole::Monitoring,
426 ServerRole::CIRunner,
427 ServerRole::Bastion,
428 ServerRole::Storage,
429 ServerRole::Custom,
430 ]
431 }
432
433 pub fn display_name(&self) -> &'static str {
435 match self {
436 ServerRole::K3sControl => "K3s Control Plane",
437 ServerRole::K3sWorker => "K3s Worker Node",
438 ServerRole::Postgres => "PostgreSQL Database",
439 ServerRole::MySQL => "MySQL/MariaDB Database",
440 ServerRole::MongoDB => "MongoDB Database",
441 ServerRole::Redis => "Redis Cache",
442 ServerRole::Memcached => "Memcached Cache",
443 ServerRole::RabbitMQ => "RabbitMQ Message Broker",
444 ServerRole::Kafka => "Kafka Message Broker",
445 ServerRole::Elasticsearch => "Elasticsearch Search Engine",
446 ServerRole::LoadBalancer => "Load Balancer (HAProxy/Nginx)",
447 ServerRole::Monitoring => "Monitoring (Prometheus/Grafana)",
448 ServerRole::CIRunner => "CI/CD Runner (GitLab Runner)",
449 ServerRole::Bastion => "Bastion/Jump Server",
450 ServerRole::Storage => "Storage (MinIO/NFS)",
451 ServerRole::Custom => "Custom/Generic Server",
452 }
453 }
454
455 pub fn description(&self) -> &'static str {
457 match self {
458 ServerRole::K3sControl => "Control plane node for K3s cluster management",
459 ServerRole::K3sWorker => "Worker node for running application workloads",
460 ServerRole::Postgres => "PostgreSQL relational database server",
461 ServerRole::MySQL => "MySQL or MariaDB relational database server",
462 ServerRole::MongoDB => "MongoDB NoSQL document database",
463 ServerRole::Redis => "In-memory data store and cache",
464 ServerRole::Memcached => "Distributed memory caching system",
465 ServerRole::RabbitMQ => "Message broker for async communication",
466 ServerRole::Kafka => "Distributed streaming platform and message broker",
467 ServerRole::Elasticsearch => "Distributed search and analytics engine",
468 ServerRole::LoadBalancer => "Traffic distribution and load balancing",
469 ServerRole::Monitoring => "Metrics collection and visualization",
470 ServerRole::CIRunner => "Continuous integration and deployment runner",
471 ServerRole::Bastion => "Secure SSH gateway for infrastructure access",
472 ServerRole::Storage => "Object storage or network file system",
473 ServerRole::Custom => "Custom server with user-defined purpose",
474 }
475 }
476}
477
478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub enum SetupMode {
481 Quick,
483 Standard,
485 Advanced,
487}
488
489impl SetupMode {
490 pub fn all() -> Vec<Self> {
492 vec![SetupMode::Quick, SetupMode::Standard, SetupMode::Advanced]
493 }
494
495 pub fn display_name(&self) -> &'static str {
497 match self {
498 SetupMode::Quick => "Quick - Single Server (Development)",
499 SetupMode::Standard => "Standard - Multi-Server HA (Production)",
500 SetupMode::Advanced => "Advanced - Custom Topology",
501 }
502 }
503
504 pub fn description(&self) -> &'static str {
506 match self {
507 SetupMode::Quick => {
508 "Single K3s server for development and testing. Fastest to set up, lowest cost."
509 }
510 SetupMode::Standard => {
511 "Production-ready HA setup with control plane, workers, and dedicated database server."
512 }
513 SetupMode::Advanced => {
514 "Full control over infrastructure topology. Configure multiple server groups with custom roles."
515 }
516 }
517 }
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
522pub struct K3sConfig {
523 pub version: String,
525
526 #[validate(length(min = 1))]
528 pub deploy_on: Vec<String>,
529
530 #[validate(length(min = 1))]
532 pub control_plane_servers: Vec<String>,
533
534 #[serde(default)]
536 pub worker_servers: Vec<String>,
537
538 #[serde(default = "default_true")]
540 pub enable_traefik: bool,
541
542 #[serde(default)]
544 pub enable_metrics_server: bool,
545
546 #[serde(default)]
548 pub server_flags: Vec<String>,
549
550 #[serde(default)]
552 pub agent_flags: Vec<String>,
553}
554
555fn default_true() -> bool {
556 true
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
561pub struct PostgresConfig {
562 pub version: String,
564
565 #[validate(length(min = 1, max = 63))]
567 pub database_name: String,
568
569 pub deployment_mode: PostgresDeploymentMode,
571
572 #[serde(skip_serializing_if = "Option::is_none")]
574 #[validate(nested)]
575 pub standalone: Option<PostgresStandaloneConfig>,
576
577 #[serde(skip_serializing_if = "Option::is_none")]
579 #[validate(nested)]
580 pub in_cluster: Option<PostgresInClusterConfig>,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
585#[serde(rename_all = "kebab-case")]
586pub enum PostgresDeploymentMode {
587 Standalone,
589 InCluster,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
595pub struct PostgresStandaloneConfig {
596 #[validate(length(min = 1))]
598 pub deploy_on: String,
599
600 pub data_dir: Option<String>,
602
603 pub max_connections: Option<u32>,
605
606 pub shared_buffers: Option<String>,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
612pub struct PostgresInClusterConfig {
613 #[validate(length(min = 1))]
615 pub namespace: String,
616
617 pub storage_class: String,
619
620 pub storage_size: String,
622
623 #[serde(default)]
625 pub use_operator: bool,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
630pub struct DnsConfig {
631 pub provider: String,
633
634 #[validate(length(min = 1))]
636 pub domain: String,
637
638 #[serde(default)]
640 pub records: Vec<DnsRecord>,
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
645pub struct DnsRecord {
646 #[serde(rename = "type")]
648 pub record_type: String,
649
650 #[validate(length(min = 1))]
652 pub name: String,
653
654 pub target: String,
659
660 #[serde(default = "default_ttl")]
662 pub ttl: u32,
663
664 #[serde(default)]
666 pub proxied: bool,
667}
668
669fn default_ttl() -> u32 {
670 300
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
675pub struct GitLabConfig {
676 #[validate(url)]
678 pub url: String,
679
680 #[validate(length(min = 1))]
682 pub namespace: String,
683}
684
685#[derive(Debug, Error)]
687pub enum ConfigError {
688 #[error("Invalid TOML format: {0}")]
690 InvalidToml(String),
691
692 #[error("TOML deserialization error: {0}")]
694 TomlDe(#[from] toml::de::Error),
695
696 #[error("Configuration validation failed: {0}")]
698 ValidationFailed(#[from] validator::ValidationErrors),
699
700 #[error("IO error: {0}")]
702 Io(#[from] std::io::Error),
703
704 #[error("Missing required configuration for provider: {0}")]
706 MissingProviderConfig(String),
707}
708
709impl LmrcConfig {
710 pub fn from_file(path: &std::path::Path) -> Result<Self, ConfigError> {
712 let content = std::fs::read_to_string(path)?;
713 Self::from_toml_str(&content)
714 }
715
716 pub fn from_toml_str(content: &str) -> Result<Self, ConfigError> {
718 let config: Self = toml::from_str(content)?;
719 config.validate()?;
720 config.validate_provider_configs()?;
721 Ok(config)
722 }
723
724 fn validate_provider_configs(&self) -> Result<(), ConfigError> {
726 if self.infrastructure.servers.is_empty() {
728 return Err(ConfigError::MissingProviderConfig(
729 "servers (at least one server group required)".to_string(),
730 ));
731 }
732
733 if self.providers.kubernetes == "k3s" && self.infrastructure.k3s.is_none() {
735 return Err(ConfigError::MissingProviderConfig("k3s".to_string()));
736 }
737
738 if self.providers.database == "postgres" && self.infrastructure.postgres.is_none() {
740 return Err(ConfigError::MissingProviderConfig("postgres".to_string()));
741 }
742
743 if self.infrastructure.dns.is_none() {
745 return Err(ConfigError::MissingProviderConfig("dns".to_string()));
746 }
747
748 if self.providers.git == "gitlab" && self.infrastructure.gitlab.is_none() {
750 return Err(ConfigError::MissingProviderConfig("gitlab".to_string()));
751 }
752
753 if let Some(postgres) = &self.infrastructure.postgres {
755 match postgres.deployment_mode {
756 PostgresDeploymentMode::Standalone => {
757 if postgres.standalone.is_none() {
758 return Err(ConfigError::InvalidToml(
759 "PostgreSQL standalone mode requires 'standalone' configuration"
760 .to_string(),
761 ));
762 }
763 if let Some(standalone) = &postgres.standalone
765 && !self
766 .infrastructure
767 .servers
768 .iter()
769 .any(|s| s.name == standalone.deploy_on)
770 {
771 return Err(ConfigError::InvalidToml(format!(
772 "PostgreSQL standalone deploy_on '{}' does not reference a valid server group",
773 standalone.deploy_on
774 )));
775 }
776 }
777 PostgresDeploymentMode::InCluster => {
778 if postgres.in_cluster.is_none() {
779 return Err(ConfigError::InvalidToml(
780 "PostgreSQL in-cluster mode requires 'in_cluster' configuration"
781 .to_string(),
782 ));
783 }
784 }
785 }
786 }
787
788 if let Some(k3s) = &self.infrastructure.k3s {
790 for server_group in &k3s.deploy_on {
791 if !self
792 .infrastructure
793 .servers
794 .iter()
795 .any(|s| s.name == *server_group)
796 {
797 return Err(ConfigError::InvalidToml(format!(
798 "K3s deploy_on '{}' does not reference a valid server group",
799 server_group
800 )));
801 }
802 }
803 for server_group in &k3s.control_plane_servers {
804 if !self
805 .infrastructure
806 .servers
807 .iter()
808 .any(|s| s.name == *server_group)
809 {
810 return Err(ConfigError::InvalidToml(format!(
811 "K3s control_plane_servers '{}' does not reference a valid server group",
812 server_group
813 )));
814 }
815 }
816 for server_group in &k3s.worker_servers {
817 if !self
818 .infrastructure
819 .servers
820 .iter()
821 .any(|s| s.name == *server_group)
822 {
823 return Err(ConfigError::InvalidToml(format!(
824 "K3s worker_servers '{}' does not reference a valid server group",
825 server_group
826 )));
827 }
828 }
829 }
830
831 Ok(())
832 }
833
834 pub fn template() -> Self {
836 let mut labels = HashMap::new();
837 labels.insert("environment".to_string(), "production".to_string());
838
839 Self {
840 project: ProjectConfig {
841 name: "my-project".to_string(),
842 description: "My LMRC Stack project".to_string(),
843 },
844 providers: ProviderConfig {
845 server: "hetzner".to_string(),
846 kubernetes: "k3s".to_string(),
847 database: "postgres".to_string(),
848 dns: "cloudflare".to_string(),
849 git: "gitlab".to_string(),
850 },
851 apps: AppsConfig {
852 applications: vec![ApplicationEntry {
853 name: "api".to_string(),
854 app_type: Some(AppType::Api),
855 docker: Some(DockerConfig {
856 dockerfile: "apps/api/Dockerfile".to_string(),
857 context: "apps/api".to_string(),
858 tags: vec!["latest".to_string()],
859 }),
860 deployment: Some(DeploymentConfig {
861 replicas: 2,
862 port: 8080,
863 cpu_request: Some("100m".to_string()),
864 memory_request: Some("128Mi".to_string()),
865 cpu_limit: Some("1".to_string()),
866 memory_limit: Some("512Mi".to_string()),
867 env: vec![],
868 }),
869 }],
870 },
871 infrastructure: InfrastructureConfig {
872 provider: "hetzner".to_string(),
873 network: Some(NetworkConfig {
874 enable_private_network: true,
875 private_network: Some("10.0.0.0/16".to_string()),
876 firewall_rules: vec![],
877 }),
878 servers: vec![
879 ServerGroup {
880 name: "k3s-control".to_string(),
881 role: ServerRole::K3sControl,
882 server_type: "cpx11".to_string(),
883 location: "nbg1".to_string(),
884 count: 1,
885 labels: labels.clone(),
886 ssh_keys: vec![],
887 image: None,
888 },
889 ServerGroup {
890 name: "k3s-workers".to_string(),
891 role: ServerRole::K3sWorker,
892 server_type: "cx21".to_string(),
893 location: "nbg1".to_string(),
894 count: 2,
895 labels: labels.clone(),
896 ssh_keys: vec![],
897 image: None,
898 },
899 ServerGroup {
900 name: "postgres-server".to_string(),
901 role: ServerRole::Postgres,
902 server_type: "cx31".to_string(),
903 location: "nbg1".to_string(),
904 count: 1,
905 labels,
906 ssh_keys: vec![],
907 image: None,
908 },
909 ],
910 k3s: Some(K3sConfig {
911 version: "v1.28.5+k3s1".to_string(),
912 deploy_on: vec!["k3s-control".to_string(), "k3s-workers".to_string()],
913 control_plane_servers: vec!["k3s-control".to_string()],
914 worker_servers: vec!["k3s-workers".to_string()],
915 enable_traefik: true,
916 enable_metrics_server: false,
917 server_flags: vec![],
918 agent_flags: vec![],
919 }),
920 postgres: Some(PostgresConfig {
921 version: "16".to_string(),
922 database_name: "myapp".to_string(),
923 deployment_mode: PostgresDeploymentMode::Standalone,
924 standalone: Some(PostgresStandaloneConfig {
925 deploy_on: "postgres-server".to_string(),
926 data_dir: None,
927 max_connections: None,
928 shared_buffers: None,
929 }),
930 in_cluster: None,
931 }),
932 dns: Some(DnsConfig {
933 provider: "cloudflare".to_string(),
934 domain: "example.com".to_string(),
935 records: vec![
936 DnsRecord {
937 record_type: "A".to_string(),
938 name: "@".to_string(),
939 target: "k3s-control".to_string(),
940 ttl: 300,
941 proxied: true,
942 },
943 DnsRecord {
944 record_type: "A".to_string(),
945 name: "api".to_string(),
946 target: "k3s-control".to_string(),
947 ttl: 300,
948 proxied: true,
949 },
950 ],
951 }),
952 gitlab: Some(GitLabConfig {
953 url: "https://gitlab.com".to_string(),
954 namespace: "mygroup".to_string(),
955 }),
956 },
957 }
958 }
959
960 pub fn to_toml_string(&self) -> Result<String, ConfigError> {
962 toml::to_string_pretty(self).map_err(|e| ConfigError::InvalidToml(e.to_string()))
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn test_template_config_is_valid() {
972 let config = LmrcConfig::template();
973 assert!(config.validate().is_ok());
974 assert!(config.validate_provider_configs().is_ok());
975 }
976
977 #[test]
978 fn test_config_to_toml_and_back() {
979 let config = LmrcConfig::template();
980 let toml_str = config.to_toml_string().unwrap();
981 let parsed = LmrcConfig::from_toml_str(&toml_str).unwrap();
982 assert_eq!(config.project.name, parsed.project.name);
983 }
984
985 #[test]
986 fn test_missing_provider_config() {
987 let mut config = LmrcConfig::template();
988 config.infrastructure.servers = vec![];
989 assert!(config.validate_provider_configs().is_err());
990 }
991
992 #[test]
993 fn test_invalid_server_group_reference() {
994 let mut config = LmrcConfig::template();
995 if let Some(ref mut postgres) = config.infrastructure.postgres {
996 if let Some(ref mut standalone) = postgres.standalone {
997 standalone.deploy_on = "nonexistent-server".to_string();
998 }
999 }
1000 assert!(config.validate_provider_configs().is_err());
1001 }
1002}