Skip to main content

xbp_cli/commands/deploy_engine/
map_config.rs

1//! Map CLI `XbpConfig` → engine `ProjectConfig` (pure data view).
2
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7use crate::strategies::XbpConfig;
8use crate::utils::resolve_cargo_package_version;
9use xbp_deploy::{
10    DeployGroupView, KubernetesDefaults, ProjectConfig, ServiceConfigView, ServiceDeployEnvView,
11    ServiceDeployView, ServiceK8sView, ServiceOciView,
12};
13
14pub fn map_project_config(project_root: &Path, config: &XbpConfig) -> ProjectConfig {
15    let history_dir = config
16        .deploy
17        .as_ref()
18        .and_then(|d| d.history_dir.as_deref())
19        .filter(|s| !s.is_empty())
20        .unwrap_or(".xbp/deployments");
21    let lock_file = config
22        .deploy
23        .as_ref()
24        .and_then(|d| d.lock_file.as_deref())
25        .filter(|s| !s.is_empty())
26        .unwrap_or(".xbp/deploy-lock.json");
27
28    let mut groups = HashMap::new();
29    if let Some(deploy) = &config.deploy {
30        for (name, g) in &deploy.groups {
31            groups.insert(
32                name.clone(),
33                DeployGroupView {
34                    description: g.description.clone(),
35                    services: g.services.clone(),
36                    order: g.order.clone(),
37                },
38            );
39        }
40    }
41
42    let services = config
43        .services
44        .as_ref()
45        .map(|list| {
46            list.iter()
47                .map(|s| {
48                    let version = resolve_service_version(project_root, config, s);
49                    ServiceConfigView {
50                        name: s.name.clone(),
51                        root_directory: s.root_directory.clone(),
52                        version,
53                        depends_on: s.depends_on.clone().unwrap_or_default(),
54                        oci: s.oci.as_ref().and_then(|o| {
55                            let image = o.image.as_ref()?.trim();
56                            if image.is_empty() {
57                                return None;
58                            }
59                            Some(ServiceOciView {
60                                image: image.to_string(),
61                                tag: o.tag.clone(),
62                                tag_from: o.tag_from.clone(),
63                                dockerfile: o.dockerfile.clone(),
64                                platforms: o.platforms.clone(),
65                            })
66                        }),
67                        deploy: s.deploy.as_ref().map(|d| ServiceDeployView {
68                            provider: d
69                                .provider
70                                .clone()
71                                .unwrap_or_else(|| "kubernetes".into()),
72                            envs: d
73                                .envs
74                                .iter()
75                                .map(|(k, v)| {
76                                    (
77                                        k.clone(),
78                                        ServiceDeployEnvView {
79                                            namespace: v.namespace.clone(),
80                                            replicas: v.replicas,
81                                            health: v.health.clone(),
82                                            kubernetes: v.kubernetes.as_ref().map(|k8s| {
83                                                ServiceK8sView {
84                                                    manifests_base: k8s
85                                                        .manifests
86                                                        .as_ref()
87                                                        .and_then(|m| m.base.clone()),
88                                                    manifests_overlay: k8s
89                                                        .manifests
90                                                        .as_ref()
91                                                        .and_then(|m| m.overlay.clone()),
92                                                    workload: k8s.workload.clone(),
93                                                    service: k8s.service.clone(),
94                                                    crds_path: k8s.crds_path.clone(),
95                                                    install_path: k8s.install_path.clone(),
96                                                    selector: k8s.selector.clone(),
97                                                }
98                                            }),
99                                        },
100                                    )
101                                })
102                                .collect(),
103                        }),
104                    }
105                })
106                .collect()
107        })
108        .unwrap_or_default();
109
110    ProjectConfig {
111        project_name: config.project_name.clone(),
112        version: config.version.clone(),
113        project_root: project_root.to_path_buf(),
114        services,
115        groups,
116        default_env: config
117            .deploy
118            .as_ref()
119            .and_then(|d| d.default_env.clone()),
120        kubernetes: config.kubernetes.as_ref().map(|k| KubernetesDefaults {
121            default_context: k.default_context.clone(),
122            default_namespace: k.default_namespace.clone(),
123            require_context_confirmation_for_apply: k
124                .require_context_confirmation_for_apply
125                .unwrap_or(true),
126            kubeconfig: k.kubeconfig.clone(),
127        }),
128        history_dir: project_root.join(history_dir),
129        lock_file: project_root.join(lock_file),
130        git_sha: resolve_git_sha(project_root),
131    }
132}
133
134fn resolve_service_version(
135    project_root: &Path,
136    config: &XbpConfig,
137    service: &crate::strategies::ServiceConfig,
138) -> Option<String> {
139    if let Some(targets) = service.version_targets.as_ref() {
140        for target in targets {
141            let path = project_root.join(target);
142            if !path.exists() {
143                continue;
144            }
145            let path_str = path.to_string_lossy();
146            if path_str.ends_with("Cargo.toml") {
147                if let Ok(Some(v)) = resolve_cargo_package_version(&path) {
148                    return Some(v);
149                }
150            } else if path_str.ends_with("package.json") {
151                if let Ok(raw) = std::fs::read_to_string(&path) {
152                    if let Ok(val) = serde_json::from_str::<serde_json::Value>(&raw) {
153                        if let Some(v) = val.get("version").and_then(|v| v.as_str()) {
154                            return Some(v.to_string());
155                        }
156                    }
157                }
158            }
159        }
160    }
161    Some(config.version.clone())
162}
163
164fn resolve_git_sha(project_root: &Path) -> Option<String> {
165    let output = Command::new("git")
166        .args(["rev-parse", "--short", "HEAD"])
167        .current_dir(project_root)
168        .output()
169        .ok()?;
170    if !output.status.success() {
171        return None;
172    }
173    let sha = String::from_utf8_lossy(&output.stdout).trim().to_string();
174    if sha.is_empty() {
175        None
176    } else {
177        Some(sha)
178    }
179}
180
181#[allow(dead_code)]
182pub fn default_lock_path(project_root: &Path) -> PathBuf {
183    project_root.join(".xbp/deploy-lock.json")
184}