alien_core/deployment/
state.rs1use crate::{ObservedInventoryBatch, Platform, ResourceHeartbeat, StackState};
4use alien_error::AlienError;
5use bon::Builder;
6use serde::{Deserialize, Serialize};
7
8use super::{DeploymentStatus, EnvironmentInfo, ReleaseInfo};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
14#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
15#[serde(rename_all = "camelCase")]
16pub struct RuntimeMetadata {
17 #[serde(skip_serializing_if = "Option::is_none")]
20 pub last_synced_env_vars_hash: Option<String>,
21
22 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 pub last_synced_secret_names: Vec<String>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
32 pub prepared_stack: Option<crate::Stack>,
33
34 #[serde(default, skip_serializing_if = "is_false")]
39 pub registry_access_granted: bool,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
50#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
51#[serde(rename_all = "camelCase")]
52pub struct DeploymentState {
53 pub status: DeploymentStatus,
55 pub platform: Platform,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub current_release: Option<ReleaseInfo>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub target_release: Option<ReleaseInfo>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub stack_state: Option<StackState>,
66 #[serde(skip_serializing_if = "Option::is_none")]
70 pub error: Option<AlienError>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub environment_info: Option<EnvironmentInfo>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub runtime_metadata: Option<RuntimeMetadata>,
77 #[serde(default, skip_serializing_if = "is_false")]
80 pub retry_requested: bool,
81 pub protocol_version: u32,
86}
87
88impl DeploymentState {
89 pub fn has_desired(&self) -> bool {
92 self.current_release.is_some()
93 || self.target_release.is_some()
94 || self.stack_state.is_some()
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
103#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
104#[serde(rename_all = "camelCase")]
105pub struct DeploymentStepResult {
106 pub state: DeploymentState,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
113 pub suggested_delay_ms: Option<u64>,
114
115 #[serde(default, skip_serializing_if = "is_false")]
119 pub update_heartbeat: bool,
120
121 #[serde(
123 default,
124 rename = "resourceHeartbeats",
125 skip_serializing_if = "Vec::is_empty"
126 )]
127 pub heartbeats: Vec<ResourceHeartbeat>,
128
129 #[serde(
131 default,
132 rename = "observedInventoryBatches",
133 skip_serializing_if = "Vec::is_empty"
134 )]
135 pub observed_inventory_batches: Vec<ObservedInventoryBatch>,
136}
137
138pub(crate) fn is_false(b: &bool) -> bool {
139 !*b
140}
141
142pub const MIN_SUPPORTED_DEPLOYMENT_PROTOCOL_VERSION: u32 = 1;
144
145pub const CURRENT_DEPLOYMENT_PROTOCOL_VERSION: u32 = 1;
148
149pub const DEPLOYMENT_PROTOCOL_VERSION: u32 = CURRENT_DEPLOYMENT_PROTOCOL_VERSION;
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::{Platform, ReleaseInfo, Stack, StackState};
156 use indexmap::IndexMap;
157
158 fn empty_stack() -> Stack {
159 Stack {
160 id: "stack_test".to_string(),
161 resources: IndexMap::new(),
162 inputs: vec![],
163 permissions: crate::PermissionsConfig::default(),
164 supported_platforms: None,
165 }
166 }
167
168 fn release_info(id: &str) -> ReleaseInfo {
169 ReleaseInfo {
170 release_id: Some(id.to_string()),
171 version: None,
172 description: None,
173 stack: empty_stack(),
174 }
175 }
176
177 fn state() -> DeploymentState {
178 DeploymentState {
179 status: DeploymentStatus::Pending,
180 platform: Platform::Kubernetes,
181 current_release: None,
182 target_release: None,
183 stack_state: None,
184 error: None,
185 environment_info: None,
186 runtime_metadata: None,
187 retry_requested: false,
188 protocol_version: DEPLOYMENT_PROTOCOL_VERSION,
189 }
190 }
191
192 #[test]
193 fn deployment_state_has_desired_when_release_or_stack_state_exists() {
194 let observe_only = state();
195 assert!(!observe_only.has_desired());
196
197 let mut current = state();
198 current.current_release = Some(release_info("rel_current"));
199 assert!(current.has_desired());
200
201 let mut target = state();
202 target.target_release = Some(release_info("rel_target"));
203 assert!(target.has_desired());
204
205 let mut imported = state();
206 imported.stack_state = Some(StackState::new(Platform::Kubernetes));
207 assert!(imported.has_desired());
208 }
209
210 #[test]
211 fn runtime_metadata_from_before_secret_inventory_defaults_to_empty() {
212 let metadata: RuntimeMetadata = serde_json::from_value(serde_json::json!({
213 "lastSyncedEnvVarsHash": "old-hash"
214 }))
215 .expect("old runtime metadata remains readable");
216
217 assert_eq!(
218 metadata.last_synced_env_vars_hash.as_deref(),
219 Some("old-hash")
220 );
221 assert!(metadata.last_synced_secret_names.is_empty());
222 }
223}