xbp_cli/commands/deploy_engine/
map_config.rs1use 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}