Skip to main content

xbp_deploy/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::{BTreeMap, HashMap};
3use std::path::PathBuf;
4use xbp_oci::OciRef;
5
6/// Resolved deploy target kind.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum DeployTarget {
10    Service(String),
11    Group(String),
12    All,
13}
14
15impl DeployTarget {
16    pub fn label(&self) -> String {
17        match self {
18            Self::Service(s) | Self::Group(s) => s.clone(),
19            Self::All => "all".into(),
20        }
21    }
22
23    pub fn kind_str(&self) -> &'static str {
24        match self {
25            Self::Service(_) => "service",
26            Self::Group(_) => "group",
27            Self::All => "all",
28        }
29    }
30}
31
32/// Project config view (CLI maps XbpConfig → this). Pure data, no clap.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ProjectConfig {
35    pub project_name: String,
36    pub version: String,
37    pub project_root: PathBuf,
38    pub services: Vec<ServiceConfigView>,
39    pub groups: HashMap<String, DeployGroupView>,
40    pub default_env: Option<String>,
41    pub kubernetes: Option<KubernetesDefaults>,
42    pub history_dir: PathBuf,
43    pub lock_file: PathBuf,
44    pub git_sha: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct DeployGroupView {
49    pub description: Option<String>,
50    pub services: Vec<String>,
51    /// Authoritative order when non-empty.
52    pub order: Vec<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct KubernetesDefaults {
57    pub default_context: Option<String>,
58    pub default_namespace: Option<String>,
59    pub require_context_confirmation_for_apply: bool,
60    pub kubeconfig: Option<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ServiceConfigView {
65    pub name: String,
66    pub root_directory: Option<String>,
67    pub version: Option<String>,
68    pub depends_on: Vec<String>,
69    pub oci: Option<ServiceOciView>,
70    pub deploy: Option<ServiceDeployView>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ServiceOciView {
75    pub image: String,
76    pub tag: Option<String>,
77    pub tag_from: Option<String>,
78    pub dockerfile: Option<String>,
79    pub platforms: Vec<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ServiceDeployView {
84    pub provider: String,
85    pub envs: HashMap<String, ServiceDeployEnvView>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ServiceDeployEnvView {
90    pub namespace: Option<String>,
91    pub replicas: Option<u32>,
92    pub health: Vec<String>,
93    pub kubernetes: Option<ServiceK8sView>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ServiceK8sView {
98    pub manifests_base: Option<String>,
99    pub manifests_overlay: Option<String>,
100    pub workload: Option<String>,
101    pub service: Option<String>,
102    pub crds_path: Option<String>,
103    pub install_path: Option<String>,
104    pub selector: Option<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct DeployPlan {
109    pub target: DeployTarget,
110    pub env: String,
111    pub project: String,
112    pub project_version: String,
113    pub git_sha: Option<String>,
114    pub services: Vec<ServicePlan>,
115    /// Final apply order (service names).
116    pub order: Vec<String>,
117    pub oci_plan: OciPlan,
118    pub k8s_plan: K8sPlanView,
119    /// Deterministic hash of the plan content (excluding generated_at).
120    pub hash: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ServicePlan {
125    pub name: String,
126    pub provider: String,
127    pub version: String,
128    pub image: Option<OciRef>,
129    pub image_ref: Option<String>,
130    pub digest: Option<String>,
131    pub deploy: ServiceDeployPlan,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ServiceDeployPlan {
136    pub namespace: Option<String>,
137    pub workload: Option<String>,
138    pub health: Vec<String>,
139    pub manifest_paths: Vec<PathBuf>,
140    pub crds_path: Option<PathBuf>,
141    pub install_path: Option<PathBuf>,
142    pub selector: Option<String>,
143    pub actions: Vec<String>,
144}
145
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
147pub struct OciPlan {
148    pub images: BTreeMap<String, OciImagePlan>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct OciImagePlan {
153    pub ref_str: String,
154    pub digest: Option<String>,
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
158pub struct K8sPlanView {
159    pub context: Option<String>,
160    pub default_namespace: Option<String>,
161    pub services: Vec<xbp_k8s::K8sServicePlan>,
162}
163
164impl DeployPlan {
165    pub fn render_human(&self) -> String {
166        let mut lines = vec![
167            format!(
168                "project={} version={} env={} target={} ({})",
169                self.project,
170                self.project_version,
171                self.env,
172                self.target.label(),
173                self.target.kind_str()
174            ),
175            format!("hash={}", self.hash),
176            format!("order: {}", self.order.join(" → ")),
177        ];
178        if let Some(sha) = &self.git_sha {
179            lines.push(format!("git_sha={sha}"));
180        }
181        for (idx, svc) in self.services.iter().enumerate() {
182            lines.push(format!(
183                "{}. {} [{}] v{}",
184                idx + 1,
185                svc.name,
186                svc.provider,
187                svc.version
188            ));
189            if let Some(ns) = &svc.deploy.namespace {
190                lines.push(format!("   namespace: {ns}"));
191            }
192            if let Some(image) = &svc.image_ref {
193                lines.push(format!("   image: {image}"));
194            }
195            if let Some(d) = &svc.digest {
196                lines.push(format!("   digest: {d}"));
197            }
198            for a in &svc.deploy.actions {
199                lines.push(format!("   • {a}"));
200            }
201        }
202        lines.join("\n")
203    }
204
205    pub fn to_k8s_plan(&self) -> xbp_k8s::K8sPlan {
206        xbp_k8s::K8sPlan {
207            services: self.k8s_plan.services.clone(),
208        }
209    }
210}