1use 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)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "camelCase")]
14pub struct SetupUpdateAuthorization {
15 pub nonce: String,
17 pub baseline_frozen_digest: String,
19 pub target_frozen_digest: String,
21 pub release_id: String,
23 pub setup_target: String,
25 pub setup_fingerprint: String,
27 pub setup_fingerprint_version: u32,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
35#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
36#[serde(rename_all = "camelCase")]
37pub struct RuntimeMetadata {
38 #[serde(skip_serializing_if = "Option::is_none")]
41 pub last_synced_env_vars_hash: Option<String>,
42
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub last_synced_secret_names: Vec<String>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
53 pub prepared_stack: Option<crate::Stack>,
54
55 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub pending_prepared_stack: Option<crate::Stack>,
59
60 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub setup_update_authorization: Option<SetupUpdateAuthorization>,
64
65 #[serde(default, skip_serializing_if = "is_false")]
70 pub registry_access_granted: bool,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
81#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
82#[serde(rename_all = "camelCase")]
83pub struct DeploymentState {
84 pub status: DeploymentStatus,
86 pub platform: Platform,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub current_release: Option<ReleaseInfo>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub target_release: Option<ReleaseInfo>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub stack_state: Option<StackState>,
97 #[serde(skip_serializing_if = "Option::is_none")]
101 pub error: Option<AlienError>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub environment_info: Option<EnvironmentInfo>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub runtime_metadata: Option<RuntimeMetadata>,
108 #[serde(default, skip_serializing_if = "is_false")]
111 pub retry_requested: bool,
112 pub protocol_version: u32,
117}
118
119impl DeploymentState {
120 pub fn has_desired(&self) -> bool {
123 self.current_release.is_some()
124 || self.target_release.is_some()
125 || self.stack_state.is_some()
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
134#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
135#[serde(rename_all = "camelCase")]
136pub struct DeploymentStepResult {
137 pub state: DeploymentState,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
144 pub suggested_delay_ms: Option<u64>,
145
146 #[serde(default, skip_serializing_if = "is_false")]
150 pub update_heartbeat: bool,
151
152 #[serde(
154 default,
155 rename = "resourceHeartbeats",
156 skip_serializing_if = "Vec::is_empty"
157 )]
158 pub heartbeats: Vec<ResourceHeartbeat>,
159
160 #[serde(
162 default,
163 rename = "observedInventoryBatches",
164 skip_serializing_if = "Vec::is_empty"
165 )]
166 pub observed_inventory_batches: Vec<ObservedInventoryBatch>,
167}
168
169pub(crate) fn is_false(b: &bool) -> bool {
170 !*b
171}
172
173pub const MIN_SUPPORTED_DEPLOYMENT_PROTOCOL_VERSION: u32 = 1;
175
176pub const CURRENT_DEPLOYMENT_PROTOCOL_VERSION: u32 = 1;
179
180pub const DEPLOYMENT_PROTOCOL_VERSION: u32 = CURRENT_DEPLOYMENT_PROTOCOL_VERSION;
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::{Platform, ReleaseInfo, Stack, StackState};
187 use indexmap::IndexMap;
188
189 fn empty_stack() -> Stack {
190 Stack {
191 id: "stack_test".to_string(),
192 resources: IndexMap::new(),
193 inputs: vec![],
194 permissions: crate::PermissionsConfig::default(),
195 supported_platforms: None,
196 }
197 }
198
199 fn release_info(id: &str) -> ReleaseInfo {
200 ReleaseInfo {
201 release_id: Some(id.to_string()),
202 version: None,
203 description: None,
204 stack: empty_stack(),
205 }
206 }
207
208 fn state() -> DeploymentState {
209 DeploymentState {
210 status: DeploymentStatus::Pending,
211 platform: Platform::Kubernetes,
212 current_release: None,
213 target_release: None,
214 stack_state: None,
215 error: None,
216 environment_info: None,
217 runtime_metadata: None,
218 retry_requested: false,
219 protocol_version: DEPLOYMENT_PROTOCOL_VERSION,
220 }
221 }
222
223 #[test]
224 fn deployment_state_has_desired_when_release_or_stack_state_exists() {
225 let observe_only = state();
226 assert!(!observe_only.has_desired());
227
228 let mut current = state();
229 current.current_release = Some(release_info("rel_current"));
230 assert!(current.has_desired());
231
232 let mut target = state();
233 target.target_release = Some(release_info("rel_target"));
234 assert!(target.has_desired());
235
236 let mut imported = state();
237 imported.stack_state = Some(StackState::new(Platform::Kubernetes));
238 assert!(imported.has_desired());
239 }
240
241 #[test]
242 fn runtime_metadata_from_before_secret_inventory_defaults_to_empty() {
243 let metadata: RuntimeMetadata = serde_json::from_value(serde_json::json!({
244 "lastSyncedEnvVarsHash": "old-hash"
245 }))
246 .expect("old runtime metadata remains readable");
247
248 assert_eq!(
249 metadata.last_synced_env_vars_hash.as_deref(),
250 Some("old-hash")
251 );
252 assert!(metadata.last_synced_secret_names.is_empty());
253 assert!(metadata.pending_prepared_stack.is_none());
254 assert!(metadata.setup_update_authorization.is_none());
255 }
256}