1use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16use crate::chain::PcrsHex;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum StagedUpgradeStatus {
24 Building,
27 Staged,
31 Confirmed,
37 Promoted,
40 Revoked,
44 Failed,
46 Expired,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct StagedUpgradeJson {
68 pub id: Uuid,
70 pub enclave_id: Uuid,
72 pub status: StagedUpgradeStatus,
74 pub docker_image: String,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub image_digest: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub pcrs: Option<PcrsHex>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub valid_from: Option<DateTime<Utc>>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub upgrade_link_id: Option<Uuid>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub revocation_link_id: Option<Uuid>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub error_message: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub builder_rev: Option<String>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub crates_rev: Option<String>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub synchronizer_pcrs: Option<Vec<PcrsHex>>,
123 #[serde(default)]
128 pub synchronizer_enabled: bool,
129 pub created_at: DateTime<Utc>,
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use chrono::TimeZone;
137
138 fn sample() -> StagedUpgradeJson {
139 StagedUpgradeJson {
140 id: Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
141 enclave_id: Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(),
142 status: StagedUpgradeStatus::Confirmed,
143 docker_image: "registry.example.com/owner/app:v2".into(),
144 image_digest: Some("sha256:abcdef".into()),
145 pcrs: Some(PcrsHex {
146 pcr0: "aa".repeat(24),
147 pcr1: "bb".repeat(24),
148 pcr2: "cc".repeat(24),
149 }),
150 valid_from: Some(Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()),
151 upgrade_link_id: Some(Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap()),
152 revocation_link_id: None,
153 error_message: None,
154 builder_rev: Some("08cc66bf46b79981253011725b9e792d6353a586".into()),
155 crates_rev: Some("842b3394f1699b1fc7ae376ec7741fa9e4029621".into()),
156 synchronizer_pcrs: Some(vec![PcrsHex {
157 pcr0: "dd".repeat(48),
158 pcr1: "ee".repeat(48),
159 pcr2: "ff".repeat(48),
160 }]),
161 synchronizer_enabled: true,
162 created_at: Utc.with_ymd_and_hms(2024, 12, 31, 0, 0, 0).unwrap(),
163 }
164 }
165
166 #[test]
167 fn staged_upgrade_json_round_trip() {
168 let orig = sample();
169 let json = serde_json::to_string(&orig).unwrap();
170 let back: StagedUpgradeJson = serde_json::from_str(&json).unwrap();
171 assert_eq!(back.id, orig.id);
172 assert_eq!(back.enclave_id, orig.enclave_id);
173 assert_eq!(back.status, orig.status);
174 assert_eq!(back.docker_image, orig.docker_image);
175 assert_eq!(back.image_digest, orig.image_digest);
176 assert_eq!(back.upgrade_link_id, orig.upgrade_link_id);
177 assert!(back.revocation_link_id.is_none());
178 assert!(back.error_message.is_none());
179 }
180
181 #[test]
183 fn staged_upgrade_json_field_names() {
184 let v = serde_json::to_value(sample()).unwrap();
185 assert!(v.get("id").is_some(), "missing id");
187 assert!(v.get("enclave_id").is_some(), "missing enclave_id");
188 assert!(v.get("status").is_some(), "missing status");
189 assert!(v.get("docker_image").is_some(), "missing docker_image");
190 assert!(v.get("created_at").is_some(), "missing created_at");
191 assert!(v.get("image_digest").is_some(), "missing image_digest");
193 assert!(v.get("pcrs").is_some(), "missing pcrs");
194 assert!(v.get("valid_from").is_some(), "missing valid_from");
195 assert!(
196 v.get("upgrade_link_id").is_some(),
197 "missing upgrade_link_id"
198 );
199 assert!(
201 v.get("revocation_link_id").is_none(),
202 "revocation_link_id should be absent"
203 );
204 assert!(
205 v.get("error_message").is_none(),
206 "error_message should be absent"
207 );
208 }
209
210 #[test]
211 fn staged_upgrade_status_serde_lowercase() {
212 let cases = [
213 (StagedUpgradeStatus::Building, "building"),
214 (StagedUpgradeStatus::Staged, "staged"),
215 (StagedUpgradeStatus::Confirmed, "confirmed"),
216 (StagedUpgradeStatus::Promoted, "promoted"),
217 (StagedUpgradeStatus::Revoked, "revoked"),
218 (StagedUpgradeStatus::Failed, "failed"),
219 (StagedUpgradeStatus::Expired, "expired"),
220 ];
221 for (status, expected_json) in &cases {
222 let got = serde_json::to_string(status).unwrap();
223 assert_eq!(got, format!("\"{expected_json}\""), "status {status:?}");
224 let back: StagedUpgradeStatus = serde_json::from_str(&got).unwrap();
225 assert_eq!(back, *status);
226 }
227 }
228
229 #[test]
230 fn building_status_has_no_optional_fields() {
231 let building = StagedUpgradeJson {
232 id: Uuid::parse_str("00000000-0000-0000-0000-000000000010").unwrap(),
233 enclave_id: Uuid::parse_str("00000000-0000-0000-0000-000000000011").unwrap(),
234 status: StagedUpgradeStatus::Building,
235 docker_image: "registry.example.com/owner/app:v3".into(),
236 image_digest: None,
237 pcrs: None,
238 valid_from: None,
239 upgrade_link_id: None,
240 revocation_link_id: None,
241 error_message: None,
242 builder_rev: None,
243 crates_rev: None,
244 synchronizer_pcrs: None,
245 synchronizer_enabled: false,
246 created_at: Utc.with_ymd_and_hms(2024, 12, 31, 0, 0, 0).unwrap(),
247 };
248 let v = serde_json::to_value(&building).unwrap();
249 assert!(v.get("image_digest").is_none());
250 assert!(v.get("pcrs").is_none());
251 assert!(v.get("valid_from").is_none());
252 assert!(v.get("upgrade_link_id").is_none());
253 assert!(v.get("builder_rev").is_none());
255 assert!(v.get("crates_rev").is_none());
256 assert!(v.get("synchronizer_pcrs").is_none());
259 assert_eq!(
260 v.get("synchronizer_enabled"),
261 Some(&serde_json::json!(false))
262 );
263 }
264
265 #[test]
267 fn revs_round_trip_when_present() {
268 let v = serde_json::to_value(sample()).unwrap();
269 assert_eq!(
270 v.get("builder_rev").and_then(|x| x.as_str()),
271 Some("08cc66bf46b79981253011725b9e792d6353a586")
272 );
273 assert_eq!(
274 v.get("crates_rev").and_then(|x| x.as_str()),
275 Some("842b3394f1699b1fc7ae376ec7741fa9e4029621")
276 );
277 let back: StagedUpgradeJson = serde_json::from_value(v).unwrap();
278 assert_eq!(
279 back.builder_rev.as_deref(),
280 Some("08cc66bf46b79981253011725b9e792d6353a586")
281 );
282 assert_eq!(
283 back.crates_rev.as_deref(),
284 Some("842b3394f1699b1fc7ae376ec7741fa9e4029621")
285 );
286 }
287
288 #[test]
293 fn deserialises_payload_without_revs() {
294 let json = r#"{
295 "id": "00000000-0000-0000-0000-000000000001",
296 "enclave_id": "00000000-0000-0000-0000-000000000002",
297 "status": "staged",
298 "docker_image": "registry.example.com/owner/app:v2",
299 "image_digest": "sha256:abcdef",
300 "created_at": "2024-12-31T00:00:00Z"
301 }"#;
302 let back: StagedUpgradeJson = serde_json::from_str(json).unwrap();
303 assert!(back.builder_rev.is_none());
304 assert!(back.crates_rev.is_none());
305 assert_eq!(back.image_digest.as_deref(), Some("sha256:abcdef"));
306 assert!(back.synchronizer_pcrs.is_none());
309 assert!(!back.synchronizer_enabled);
310 }
311
312 #[test]
315 fn synchronizer_provenance_round_trips_when_present() {
316 let v = serde_json::to_value(sample()).unwrap();
317 let anchors = v
318 .get("synchronizer_pcrs")
319 .and_then(|x| x.as_array())
320 .expect("synchronizer_pcrs present as a list");
321 assert_eq!(anchors.len(), 1);
322 assert_eq!(
323 anchors[0].get("PCR0").and_then(|x| x.as_str()),
324 Some("dd".repeat(48).as_str())
325 );
326 assert_eq!(
327 v.get("synchronizer_enabled"),
328 Some(&serde_json::json!(true))
329 );
330
331 let back: StagedUpgradeJson = serde_json::from_value(v).unwrap();
332 let pcrs = back.synchronizer_pcrs.expect("anchors survive round-trip");
333 assert_eq!(pcrs[0].pcr1, "ee".repeat(48));
334 assert!(back.synchronizer_enabled);
335 }
336}