1use crate::contract::{API_VERSION, canonical_json};
2use crate::host_lifecycle::{
3 ApprovalRequirement, EnrollmentStrategy, InstallMode, LifecycleIntent,
4};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::{BTreeMap, BTreeSet};
8
9#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
10#[serde(rename_all = "camelCase", deny_unknown_fields)]
11pub struct PlanEnvelope {
12 pub api_version: String,
13 pub kind: PlanKind,
14 pub metadata: PlanMetadata,
15 pub spec: PlanSpec,
16}
17
18#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
19#[serde(rename_all = "camelCase", deny_unknown_fields)]
20pub struct EventEnvelope {
21 pub api_version: String,
22 pub kind: EventKind,
23 pub metadata: EventMetadata,
24 pub spec: EventSpec,
25}
26
27#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
28pub enum EventKind {
29 Event,
30}
31
32#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34pub struct EventMetadata {
35 pub run_id: String,
36 pub sequence: u64,
37}
38
39#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
40#[serde(rename_all = "camelCase", deny_unknown_fields)]
41pub struct EventSpec {
42 pub phase: EventPhase,
43 pub action: Option<String>,
44 pub message: String,
45}
46
47#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
48#[serde(rename_all = "kebab-case")]
49pub enum EventPhase {
50 RunStarted,
51 ActionStarted,
52 ActionCompleted,
53 ActionFailed,
54 RunCompleted,
55 RunFailed,
56 RunCancelled,
57}
58
59impl EventEnvelope {
60 pub fn new(run_id: &str, sequence: u64, phase: EventPhase, action: Option<String>) -> Self {
61 let message = match phase {
62 EventPhase::RunStarted => "run started",
63 EventPhase::ActionStarted => "action started",
64 EventPhase::ActionCompleted => "action completed",
65 EventPhase::ActionFailed => "action failed",
66 EventPhase::RunCompleted => "run completed",
67 EventPhase::RunFailed => "run failed",
68 EventPhase::RunCancelled => "run cancelled",
69 };
70 Self {
71 api_version: API_VERSION.to_string(),
72 kind: EventKind::Event,
73 metadata: EventMetadata { run_id: run_id.to_string(), sequence },
74 spec: EventSpec { phase, action, message: message.to_string() },
75 }
76 }
77}
78
79#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
80pub enum PlanKind {
81 Plan,
82}
83
84#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
85#[serde(rename_all = "camelCase", deny_unknown_fields)]
86pub struct PlanMetadata {
87 pub name: String,
88}
89
90#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
91#[serde(rename_all = "camelCase", deny_unknown_fields)]
92pub struct PlanSpec {
93 pub source_digest: String,
94 pub config_digest: String,
95 pub provider_digest: String,
96 pub selectors: Vec<String>,
97 pub expires_at_unix: u64,
98 pub preconditions: Vec<PlanPrecondition>,
99 pub actions: Vec<PlanAction>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub lifecycle_intent: Option<LifecycleIntent>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub install_mode: Option<InstallMode>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub enrollment_strategy: Option<EnrollmentStrategy>,
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
111 pub approval_requirements: Vec<ApprovalRequirement>,
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
114 pub provider_runtimes: Vec<ProviderRuntimeRecord>,
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub secret_runtimes: Vec<SecretRuntimeRecord>,
118}
119
120#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
122#[serde(rename_all = "camelCase", deny_unknown_fields)]
123pub struct ProviderRuntimeRecord {
124 pub provider_instance: String,
125 pub provider_kind: String,
126 pub provider_version: String,
127 pub runtime_digest: String,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub nxd_revision: Option<String>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub protocol: Option<String>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub executable_path: Option<String>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub executable_digest: Option<String>,
136 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 pub capabilities: Vec<String>,
138 #[serde(default, skip_serializing_if = "Vec::is_empty")]
139 pub resource_schema_digests: Vec<String>,
140 #[serde(default, skip_serializing_if = "Vec::is_empty")]
141 pub arguments: Vec<String>,
142}
143
144#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
145#[serde(rename_all = "camelCase", deny_unknown_fields)]
146pub struct SecretRuntimeRecord {
147 pub resolver_id: String,
148 pub protocol: String,
149 pub executable_path: String,
150 pub executable_digest: String,
151 #[serde(default, skip_serializing_if = "Vec::is_empty")]
152 pub arguments: Vec<String>,
153}
154
155#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct PlanInputDigests {
157 pub source: String,
158 pub config: String,
159 pub provider: String,
160}
161
162#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
163#[serde(rename_all = "camelCase", deny_unknown_fields)]
164pub struct PlanAction {
165 pub id: String,
166 pub provider_instance: String,
167 pub resource: String,
168 pub operation: PlanActionOperation,
169 pub risk: PlanActionRisk,
170 pub depends_on: Vec<String>,
171 pub lock_keys: Vec<String>,
172 pub timeout_seconds: u64,
173 pub secret_references: Vec<String>,
174 pub details: Value,
175}
176
177#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
178#[serde(rename_all = "camelCase", deny_unknown_fields)]
179pub struct PlanPrecondition {
180 pub resource: String,
181 pub observed_digest: String,
182}
183
184#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
185#[serde(rename_all = "lowercase")]
186pub enum PlanActionOperation {
187 Create,
188 Update,
189 Delete,
190 Noop,
191}
192
193#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
194#[serde(rename_all = "kebab-case")]
195pub enum PlanActionRisk {
196 ReadOnly,
197 Reversible,
198 ServiceImpacting,
199 Destructive,
200 IdentityCritical,
201}
202
203#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
204#[serde(rename_all = "camelCase", deny_unknown_fields)]
205pub struct RunEnvelope {
206 pub api_version: String,
207 pub kind: RunKind,
208 pub metadata: RunMetadata,
209 pub spec: RunSpec,
210 pub status: RunStatus,
211}
212
213#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
214pub enum RunKind {
215 Run,
216}
217
218#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
219#[serde(rename_all = "camelCase", deny_unknown_fields)]
220pub struct RunMetadata {
221 pub id: String,
222}
223
224#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
225#[serde(rename_all = "camelCase", deny_unknown_fields)]
226pub struct RunSpec {
227 pub plan_digest: String,
228}
229
230#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
231#[serde(rename_all = "camelCase", deny_unknown_fields)]
232pub struct RunStatus {
233 pub phase: RunPhase,
234 pub completed_actions: Vec<String>,
235}
236
237#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
238#[serde(rename_all = "kebab-case")]
239pub enum RunPhase {
240 Pending,
241 Running,
242 Succeeded,
243 Failed,
244 Cancelled,
245}
246
247impl PlanEnvelope {
248 pub fn new(
253 name: impl Into<String>,
254 input_digests: PlanInputDigests,
255 selectors: Vec<String>,
256 expires_at_unix: u64,
257 preconditions: Vec<PlanPrecondition>,
258 actions: Vec<PlanAction>,
259 ) -> Self {
260 let approval_requirements = crate::host_lifecycle::approval_requirements_from_actions(&actions);
261 Self::new_with_lifecycle(
262 name,
263 input_digests,
264 selectors,
265 expires_at_unix,
266 preconditions,
267 actions,
268 None,
269 approval_requirements,
270 )
271 }
272
273 #[allow(clippy::too_many_arguments)]
274 pub fn new_with_lifecycle(
275 name: impl Into<String>,
276 input_digests: PlanInputDigests,
277 selectors: Vec<String>,
278 expires_at_unix: u64,
279 preconditions: Vec<PlanPrecondition>,
280 actions: Vec<PlanAction>,
281 lifecycle_intent: Option<LifecycleIntent>,
282 approval_requirements: Vec<ApprovalRequirement>,
283 ) -> Self {
284 let approval_requirements = if approval_requirements.is_empty() && lifecycle_intent.is_none() {
288 crate::host_lifecycle::approval_requirements_from_actions(&actions)
289 } else {
290 approval_requirements
291 };
292 Self {
293 api_version: API_VERSION.to_string(),
294 kind: PlanKind::Plan,
295 metadata: PlanMetadata { name: name.into() },
296 spec: PlanSpec {
297 source_digest: input_digests.source,
298 config_digest: input_digests.config,
299 provider_digest: input_digests.provider,
300 selectors,
301 expires_at_unix,
302 preconditions,
303 actions,
304 lifecycle_intent,
305 install_mode: None,
306 enrollment_strategy: None,
307 approval_requirements,
308 provider_runtimes: Vec::new(),
309 secret_runtimes: Vec::new(),
310 },
311 }
312 }
313
314 pub fn with_provider_runtimes(mut self, runtimes: Vec<ProviderRuntimeRecord>) -> Self {
316 self.spec.provider_runtimes = runtimes;
317 self
318 }
319
320 pub fn with_secret_runtimes(mut self, runtimes: Vec<SecretRuntimeRecord>) -> Self {
321 self.spec.secret_runtimes = runtimes;
322 self
323 }
324
325 pub fn digest(&self) -> Result<String, serde_json::Error> {
326 sha256_hex(&canonical_json(self)?)
327 }
328}
329
330pub fn sha256_hex(input: &str) -> Result<String, serde_json::Error> {
331 Ok(format!("sha256:{}", sha256_hex_bytes(input.as_bytes())))
332}
333
334pub fn parse_plan_envelope(input: &str) -> Result<PlanEnvelope, serde_json::Error> {
335 serde_json::from_str(input)
336}
337
338#[derive(Clone, Debug, PartialEq, Eq)]
339pub enum PlanApplyError {
340 DuplicateActionId(String),
341 MissingActionDependency { action: String, dependency: String },
342 ActionDependencyCycle(Vec<String>),
343}
344
345pub trait CancellationToken: Send + Sync {
346 fn is_cancelled(&self) -> bool;
347}
348
349pub trait EventSink {
350 fn emit(&mut self, event: EventEnvelope);
351}
352
353#[derive(Clone, Copy, Debug, Default)]
354pub struct NeverCancelled;
355
356impl CancellationToken for NeverCancelled {
357 fn is_cancelled(&self) -> bool {
358 false
359 }
360}
361
362impl EventSink for Vec<EventEnvelope> {
363 fn emit(&mut self, event: EventEnvelope) {
364 self.push(event);
365 }
366}
367
368pub fn ordered_plan_actions(actions: &[PlanAction]) -> Result<Vec<PlanAction>, PlanApplyError> {
369 let mut by_id = BTreeMap::new();
370 for action in actions {
371 if by_id.insert(action.id.clone(), action).is_some() {
372 return Err(PlanApplyError::DuplicateActionId(action.id.clone()));
373 }
374 }
375
376 for action in actions {
377 for dependency in &action.depends_on {
378 if !by_id.contains_key(dependency) {
379 return Err(PlanApplyError::MissingActionDependency {
380 action: action.id.clone(),
381 dependency: dependency.clone(),
382 });
383 }
384 }
385 }
386
387 let mut ordered = Vec::with_capacity(actions.len());
388 let mut emitted = BTreeSet::new();
389
390 while ordered.len() < actions.len() {
391 let mut progressed = false;
392 for (id, action) in &by_id {
393 if emitted.contains(id) {
394 continue;
395 }
396 if action.depends_on.iter().all(|dependency| emitted.contains(dependency)) {
397 ordered.push((*action).clone());
398 emitted.insert(id.clone());
399 progressed = true;
400 }
401 }
402 if !progressed {
403 let cycle = by_id.keys().filter(|id| !emitted.contains(*id)).cloned().collect::<Vec<_>>();
404 return Err(PlanApplyError::ActionDependencyCycle(cycle));
405 }
406 }
407
408 Ok(ordered)
409}
410
411pub fn sha256_hex_bytes(input: &[u8]) -> String {
412 const INITIAL: [u32; 8] = [
413 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
414 ];
415 const K: [u32; 64] = [
416 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
417 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
418 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
419 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
420 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
421 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
422 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
423 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
424 ];
425
426 let mut message = input.to_vec();
427 let bit_len = (message.len() as u64) * 8;
428 message.push(0x80);
429 while (message.len() % 64) != 56 {
430 message.push(0);
431 }
432 message.extend_from_slice(&bit_len.to_be_bytes());
433
434 let mut state = INITIAL;
435 for chunk in message.chunks_exact(64) {
436 let mut words = [0u32; 64];
437 for (index, word) in words.iter_mut().take(16).enumerate() {
438 let offset = index * 4;
439 *word = u32::from_be_bytes([
440 chunk[offset],
441 chunk[offset + 1],
442 chunk[offset + 2],
443 chunk[offset + 3],
444 ]);
445 }
446 for index in 16..64 {
447 let s0 = words[index - 15].rotate_right(7)
448 ^ words[index - 15].rotate_right(18)
449 ^ (words[index - 15] >> 3);
450 let s1 = words[index - 2].rotate_right(17)
451 ^ words[index - 2].rotate_right(19)
452 ^ (words[index - 2] >> 10);
453 words[index] =
454 words[index - 16].wrapping_add(s0).wrapping_add(words[index - 7]).wrapping_add(s1);
455 }
456
457 let mut a = state[0];
458 let mut b = state[1];
459 let mut c = state[2];
460 let mut d = state[3];
461 let mut e = state[4];
462 let mut f = state[5];
463 let mut g = state[6];
464 let mut h = state[7];
465
466 for index in 0..64 {
467 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
468 let ch = (e & f) ^ ((!e) & g);
469 let temp1 =
470 h.wrapping_add(s1).wrapping_add(ch).wrapping_add(K[index]).wrapping_add(words[index]);
471 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
472 let maj = (a & b) ^ (a & c) ^ (b & c);
473 let temp2 = s0.wrapping_add(maj);
474
475 h = g;
476 g = f;
477 f = e;
478 e = d.wrapping_add(temp1);
479 d = c;
480 c = b;
481 b = a;
482 a = temp1.wrapping_add(temp2);
483 }
484
485 state[0] = state[0].wrapping_add(a);
486 state[1] = state[1].wrapping_add(b);
487 state[2] = state[2].wrapping_add(c);
488 state[3] = state[3].wrapping_add(d);
489 state[4] = state[4].wrapping_add(e);
490 state[5] = state[5].wrapping_add(f);
491 state[6] = state[6].wrapping_add(g);
492 state[7] = state[7].wrapping_add(h);
493 }
494
495 let mut output = String::with_capacity(64);
496 for word in state {
497 output.push_str(&format!("{word:08x}"));
498 }
499 output
500}
501
502#[cfg(test)]
503mod tests;