lmrc_cli/generator/
workspace.rs

1use lmrc_config_validator::LmrcConfig;
2use lmrc_toml_writer::WorkspaceToml;
3use std::fs;
4use std::path::Path;
5
6use crate::error::Result;
7
8// Auto-generated version constants from workspace Cargo.toml at compile time
9include!(concat!(env!("OUT_DIR"), "/versions.rs"));
10
11pub fn generate_workspace_toml(project_path: &Path, config: &LmrcConfig) -> Result<()> {
12    let mut builder = WorkspaceToml::new().resolver("2");
13
14    // Add pipeline if it exists on disk
15    if project_path.join("infra/pipeline/Cargo.toml").exists() {
16        builder = builder.member("infra/pipeline");
17    }
18
19    // Add bundled infrastructure apps only if they exist on disk
20    for app in ["gateway", "infra-api", "infra-migrator", "app-migrator"] {
21        if project_path.join(format!("apps/{}/Cargo.toml", app)).exists() {
22            builder = builder.member(format!("apps/{}", app));
23        }
24    }
25
26    // Add all user-defined applications as members
27    for app in &config.apps.applications {
28        // Skip Gateway and Migrator types as they're now bundled
29        if matches!(
30            app.app_type,
31            Some(lmrc_config_validator::AppType::Gateway)
32                | Some(lmrc_config_validator::AppType::Migrator)
33        ) {
34            continue;
35        }
36        builder = builder.member(format!("apps/{}", app.name));
37    }
38
39    // Set workspace package metadata
40    let authors = config
41        .infrastructure
42        .gitlab
43        .as_ref()
44        .map(|g| format!("{} Team", g.namespace))
45        .unwrap_or_else(|| "LMRC Team".to_string());
46
47    // Build repository URL from GitLab config if available
48    let repository = config
49        .infrastructure
50        .gitlab
51        .as_ref()
52        .map(|g| format!("{}/{}/{}", g.url, g.namespace, config.project.name))
53        .unwrap_or_else(|| "https://example.com/repo".to_string());
54
55    builder = builder
56        .workspace_version("0.1.0")
57        .workspace_edition("2021")
58        .workspace_author(authors)
59        .workspace_license("MIT OR Apache-2.0")
60        .workspace_repository(repository);
61
62    // Add infrastructure library dependencies (versions auto-generated at compile time)
63    builder = builder
64        .dependency("lmrc-gitlab", LMRC_GITLAB_VERSION)
65        .dependency("lmrc-hetzner", LMRC_HETZNER_VERSION)
66        .dependency("lmrc-k3s", LMRC_K3S_VERSION)
67        .dependency("lmrc-cloudflare", LMRC_CLOUDFLARE_VERSION)
68        .dependency("lmrc-postgres", LMRC_POSTGRES_VERSION)
69        .dependency("lmrc-ssh", LMRC_SSH_VERSION)
70        .dependency("lmrc-docker", LMRC_DOCKER_VERSION)
71        .dependency("lmrc-kubernetes", LMRC_KUBERNETES_VERSION)
72        .dependency("lmrc-pipeline", LMRC_PIPELINE_VERSION)
73        .dependency("lmrc-http-common", LMRC_HTTP_COMMON_VERSION)
74        .dependency("lmrc-config-validator", LMRC_CONFIG_VALIDATOR_VERSION);
75
76    // Add common dependencies
77    builder = builder
78        .dependency("tokio", r#"{ version = "1.0", features = ["full"] }"#)
79        .dependency("serde", r#"{ version = "1.0", features = ["derive"] }"#)
80        .dependency("serde_json", "1.0")
81        .dependency("thiserror", "2.0")
82        .dependency("tracing", "0.1")
83        .dependency("tracing-subscriber", "0.3")
84        .dependency("clap", r#"{ version = "4.5", features = ["derive"] }"#)
85        .dependency("toml", "0.8")
86        // HTTP service dependencies
87        .dependency("axum", "0.7")
88        .dependency("tower", "0.5")
89        .dependency("tower-http", r#"{ version = "0.6", features = ["trace", "cors", "request-id", "util"] }"#)
90        .dependency("hyper", r#"{ version = "1.0", features = ["full"] }"#)
91        .dependency("hyper-util", r#"{ version = "0.1", features = ["tokio"] }"#)
92        .dependency("sea-orm", r#"{ version = "1.1", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }"#)
93        .dependency("sea-orm-migration", "1.1")
94        .dependency("jsonwebtoken", "9.3")
95        .dependency("bcrypt", "0.16")
96        .dependency("cookie", "0.18")
97        .dependency("chrono", r#"{ version = "0.4", features = ["serde"] }"#)
98        .dependency("time", "0.3")
99        .dependency("dotenvy", "0.15")
100        .dependency("validator", r#"{ version = "0.18", features = ["derive"] }"#)
101        .dependency("uuid", r#"{ version = "1.0", features = ["v4"] }"#)
102        .dependency("anyhow", "1.0")
103        .dependency("async-trait", "0.1")
104        .dependency("reqwest", r#"{ version = "0.12", default-features = false, features = ["rustls-tls", "json"] }"#)
105   ;
106
107    let workspace_toml = builder.build();
108
109    let cargo_path = project_path.join("Cargo.toml");
110    fs::write(&cargo_path, workspace_toml)?;
111
112    Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use lmrc_config_validator::*;
119    use std::collections::HashMap;
120    use tempfile::TempDir;
121
122    fn create_test_config() -> LmrcConfig {
123        LmrcConfig {
124            project: ProjectConfig {
125                name: "test-project".to_string(),
126                description: "Test project".to_string(),
127            },
128            providers: ProviderConfig {
129                server: "hetzner".to_string(),
130                kubernetes: "k3s".to_string(),
131                database: "postgres".to_string(),
132            queue: "rabbitmq".to_string(),
133                dns: "cloudflare".to_string(),
134                git: "gitlab".to_string(),
135            },
136            apps: AppsConfig {
137                applications: vec![
138                    ApplicationEntry {
139                        name: "app1".to_string(),
140                        app_type: Some(lmrc_config_validator::AppType::Api),
141                        docker: None,
142                        deployment: None,
143                    },
144                    ApplicationEntry {
145                        name: "app2".to_string(),
146                        app_type: Some(lmrc_config_validator::AppType::Basic),
147                        docker: None,
148                        deployment: None,
149                    },
150                ],
151            },
152            infrastructure: InfrastructureConfig {
153                provider: "hetzner".to_string(),
154                network: None,
155                servers: vec![ServerGroup {
156                    name: "k3s-server".to_string(),
157                    role: ServerRole::K3sControl,
158                    server_type: "cx11".to_string(),
159                    location: "nbg1".to_string(),
160                    count: 1,
161                    labels: HashMap::new(),
162                    ssh_keys: vec![],
163                    image: None,
164                }],
165                k3s: Some(K3sConfig {
166                    version: "v1.28.5+k3s1".to_string(),
167                    deploy_on: vec!["k3s-server".to_string()],
168                    control_plane_servers: vec!["k3s-server".to_string()],
169                    worker_servers: vec![],
170                    enable_traefik: true,
171                    enable_metrics_server: false,
172                    server_flags: vec![],
173                    agent_flags: vec![],
174                }),
175                postgres: Some(PostgresConfig {
176                    version: "16".to_string(),
177                    database_name: "testdb".to_string(),
178                    deployment_mode: PostgresDeploymentMode::InCluster,
179                    standalone: None,
180                    in_cluster: Some(PostgresInClusterConfig {
181                        namespace: "default".to_string(),
182                        storage_class: "local-path".to_string(),
183                        storage_size: "10Gi".to_string(),
184                        use_operator: false,
185                    }),
186                }),
187                rabbitmq: None,
188                vault: None,
189                dns: Some(DnsConfig {
190                    provider: "cloudflare".to_string(),
191                    domain: "test.example.com".to_string(),
192                    records: vec![],
193                }),
194                gitlab: Some(GitLabConfig {
195                    url: "https://gitlab.com".to_string(),
196                    namespace: "testuser".to_string(),
197                }),
198            },
199        }
200    }
201
202    #[test]
203    fn test_generate_workspace_toml_creates_file() {
204        let temp_dir = TempDir::new().unwrap();
205        let config = create_test_config();
206
207        let result = generate_workspace_toml(temp_dir.path(), &config);
208        assert!(result.is_ok());
209
210        let cargo_toml = temp_dir.path().join("Cargo.toml");
211        assert!(cargo_toml.exists());
212    }
213
214    #[test]
215    fn test_workspace_toml_has_workspace_section() {
216        let temp_dir = TempDir::new().unwrap();
217        let config = create_test_config();
218
219        generate_workspace_toml(temp_dir.path(), &config).unwrap();
220
221        let cargo_toml = temp_dir.path().join("Cargo.toml");
222        let content = fs::read_to_string(cargo_toml).unwrap();
223
224        assert!(content.contains("[workspace]"));
225    }
226
227    #[test]
228    fn test_workspace_toml_has_correct_resolver() {
229        let temp_dir = TempDir::new().unwrap();
230        let config = create_test_config();
231
232        generate_workspace_toml(temp_dir.path(), &config).unwrap();
233
234        let cargo_toml = temp_dir.path().join("Cargo.toml");
235        let content = fs::read_to_string(cargo_toml).unwrap();
236
237        assert!(content.contains("resolver = \"2\""));
238    }
239
240    #[test]
241    fn test_workspace_toml_includes_all_app_members() {
242        let temp_dir = TempDir::new().unwrap();
243        let config = create_test_config();
244
245        generate_workspace_toml(temp_dir.path(), &config).unwrap();
246
247        let cargo_toml = temp_dir.path().join("Cargo.toml");
248        let content = fs::read_to_string(cargo_toml).unwrap();
249
250        assert!(content.contains("\"apps/app1\""));
251        assert!(content.contains("\"apps/app2\""));
252    }
253
254    #[test]
255    fn test_workspace_toml_includes_pipeline_member() {
256        let temp_dir = TempDir::new().unwrap();
257        let config = create_test_config();
258
259        // Create pipeline directory with Cargo.toml so it gets included
260        let pipeline_dir = temp_dir.path().join("infra/pipeline");
261        fs::create_dir_all(&pipeline_dir).unwrap();
262        fs::write(pipeline_dir.join("Cargo.toml"), "[package]\nname = \"pipeline\"\n").unwrap();
263
264        generate_workspace_toml(temp_dir.path(), &config).unwrap();
265
266        let cargo_toml = temp_dir.path().join("Cargo.toml");
267        let content = fs::read_to_string(cargo_toml).unwrap();
268
269        assert!(content.contains("\"infra/pipeline\""));
270    }
271
272    #[test]
273    fn test_workspace_toml_includes_lmrc_dependencies() {
274        let temp_dir = TempDir::new().unwrap();
275        let config = create_test_config();
276
277        generate_workspace_toml(temp_dir.path(), &config).unwrap();
278
279        let cargo_toml = temp_dir.path().join("Cargo.toml");
280        let content = fs::read_to_string(cargo_toml).unwrap();
281
282        assert!(content.contains("[workspace.dependencies]"));
283
284        // Check for all lmrc library dependencies
285        let lmrc_deps = [
286            "lmrc-gitlab",
287            "lmrc-hetzner",
288            "lmrc-k3s",
289            "lmrc-cloudflare",
290            "lmrc-postgres",
291            "lmrc-ssh",
292            "lmrc-docker",
293            "lmrc-kubernetes",
294            "lmrc-pipeline",
295        ];
296
297        for dep in &lmrc_deps {
298            assert!(
299                content.contains(dep),
300                "Missing workspace dependency: {}",
301                dep
302            );
303        }
304    }
305
306    #[test]
307    fn test_workspace_toml_includes_common_dependencies() {
308        let temp_dir = TempDir::new().unwrap();
309        let config = create_test_config();
310
311        generate_workspace_toml(temp_dir.path(), &config).unwrap();
312
313        let cargo_toml = temp_dir.path().join("Cargo.toml");
314        let content = fs::read_to_string(cargo_toml).unwrap();
315
316        // Check for common dependencies
317        let common_deps = [
318            "tokio",
319            "serde",
320            "serde_json",
321            "thiserror",
322            "tracing",
323            "clap",
324            "toml",
325        ];
326
327        for dep in &common_deps {
328            assert!(content.contains(dep), "Missing common dependency: {}", dep);
329        }
330    }
331
332    #[test]
333    fn test_workspace_toml_with_single_app() {
334        let temp_dir = TempDir::new().unwrap();
335        let mut config = create_test_config();
336        config.apps.applications = vec![ApplicationEntry {
337            name: "single-app".to_string(),
338            app_type: Some(lmrc_config_validator::AppType::Api),
339            docker: None,
340            deployment: None,
341        }];
342
343        generate_workspace_toml(temp_dir.path(), &config).unwrap();
344
345        let cargo_toml = temp_dir.path().join("Cargo.toml");
346        let content = fs::read_to_string(cargo_toml).unwrap();
347
348        assert!(content.contains("\"apps/single-app\""));
349        assert!(!content.contains("\"apps/app1\""));
350    }
351
352    #[test]
353    fn test_workspace_toml_sets_workspace_metadata() {
354        let temp_dir = TempDir::new().unwrap();
355        let config = create_test_config();
356
357        generate_workspace_toml(temp_dir.path(), &config).unwrap();
358
359        let cargo_toml = temp_dir.path().join("Cargo.toml");
360        let content = fs::read_to_string(cargo_toml).unwrap();
361
362        assert!(content.contains("[workspace.package]"));
363        assert!(content.contains("version = \"0.1.0\""));
364        assert!(content.contains("edition = \"2021\""));
365    }
366
367    #[test]
368    fn test_workspace_toml_author_from_gitlab_namespace() {
369        let temp_dir = TempDir::new().unwrap();
370        let config = create_test_config();
371
372        generate_workspace_toml(temp_dir.path(), &config).unwrap();
373
374        let cargo_toml = temp_dir.path().join("Cargo.toml");
375        let content = fs::read_to_string(cargo_toml).unwrap();
376
377        // Should include author based on GitLab namespace
378        assert!(content.contains("testuser Team"));
379    }
380}