nixfleet_reconciler/
manifest.rs1use anyhow::{Result, anyhow};
5use chrono::{DateTime, Utc};
6use nixfleet_proto::{FleetResolved, HostWave, Meta, RolloutBudget, RolloutId, RolloutManifest};
7
8pub fn current_rollout_ids(
15 fleet: &FleetResolved,
16 fleet_resolved_hash: &str,
17) -> std::collections::HashSet<String> {
18 fleet
19 .channels
20 .keys()
21 .filter_map(|ch| {
22 compute_rollout_id_for_channel(fleet, fleet_resolved_hash, ch)
23 .ok()
24 .flatten()
25 })
26 .collect()
27}
28
29pub fn compute_rollout_id_for_channel(
34 fleet: &FleetResolved,
35 fleet_resolved_hash: &str,
36 channel: &str,
37) -> Result<Option<String>> {
38 let signed_at = fleet
39 .meta
40 .signed_at
41 .ok_or_else(|| anyhow!("fleet.meta.signedAt is None - cannot project manifest"))?;
42 let ci_commit = fleet.meta.ci_commit.as_deref();
43 let manifest = match project_manifest(
44 fleet,
45 channel,
46 fleet_resolved_hash,
47 signed_at,
48 ci_commit,
49 fleet.meta.signature_algorithm_or_default(),
50 )? {
51 Some(m) => m,
52 None => return Ok(None),
53 };
54 Ok(Some(
55 RolloutId::new(&manifest.channel, &manifest.channel_ref)
56 .as_str()
57 .to_string(),
58 ))
59}
60
61pub fn project_manifest(
65 fleet: &FleetResolved,
66 channel: &str,
67 fleet_resolved_hash: &str,
68 signed_at: DateTime<Utc>,
69 ci_commit: Option<&str>,
70 signature_algorithm: &str,
71) -> Result<Option<RolloutManifest>> {
72 let channel_def = fleet
73 .channels
74 .get(channel)
75 .ok_or_else(|| anyhow!("channel {channel} missing from fleet.channels"))?;
76
77 let policy = fleet
78 .rollout_policies
79 .get(&channel_def.rollout_policy)
80 .ok_or_else(|| {
81 anyhow!(
82 "rollout policy {} for channel {channel} not found in fleet.rolloutPolicies",
83 channel_def.rollout_policy
84 )
85 })?;
86
87 let waves = fleet.waves.get(channel);
88
89 let mut host_set: Vec<HostWave> = Vec::new();
90 for (hostname, host) in fleet.hosts.iter() {
91 if host.channel != channel {
92 continue;
93 }
94 let target_closure = match host.closure_hash.as_ref() {
95 Some(c) => c.clone(),
96 None => continue,
97 };
98 let wave_index: u32 = match waves {
99 Some(ws) => ws
100 .iter()
101 .position(|w| w.hosts.iter().any(|h| h == hostname))
102 .map(|i| i as u32)
103 .unwrap_or(0),
104 None => 0,
105 };
106 host_set.push(HostWave {
107 hostname: hostname.clone(),
108 wave_index,
109 target_closure,
110 });
111 }
112
113 if host_set.is_empty() {
114 return Ok(None);
115 }
116 host_set.sort_by(|a, b| a.hostname.cmp(&b.hostname));
117
118 let display_name = format!(
119 "{}@{}",
120 channel,
121 ci_commit
122 .map(|c| c.chars().take(8).collect::<String>())
123 .unwrap_or_else(|| "unknown".to_string())
124 );
125
126 let channel_ref = ci_commit.unwrap_or_default().to_string();
127
128 let disruption_budgets: Vec<RolloutBudget> = fleet
132 .disruption_budgets
133 .iter()
134 .map(|b| {
135 let mut hosts = b.selector.resolve(fleet.hosts.iter());
136 hosts.sort();
137 RolloutBudget {
138 selector: b.selector.clone(),
139 hosts,
140 max_in_flight: b.max_in_flight,
141 max_in_flight_pct: b.max_in_flight_pct,
142 }
143 })
144 .collect();
145
146 Ok(Some(RolloutManifest {
147 schema_version: 1,
148 display_name,
149 channel: channel.to_string(),
150 channel_ref,
151 fleet_resolved_hash: fleet_resolved_hash.to_string(),
152 host_set,
153 health_gate: policy.health_gate.clone(),
154 disruption_budgets,
155 meta: Meta {
156 schema_version: 1,
157 signed_at: Some(signed_at),
158 ci_commit: ci_commit.map(|c| c.to_string()),
159 signature_algorithm: Some(signature_algorithm.to_string()),
160 },
161 }))
162}