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