canic_host/deployment_truth/observe/artifacts/
mod.rs1use super::super::*;
2use super::shared::observation_gap;
3use crate::release_set::{
4 ROOT_RELEASE_SET_MANIFEST_FILE, configured_deployable_roles, configured_fleet_name,
5 load_root_release_set_manifest,
6};
7use std::{
8 collections::BTreeMap,
9 fs,
10 path::{Path, PathBuf},
11};
12
13#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct LocalArtifactManifestRequest {
18 pub network: String,
19 pub workspace_root: PathBuf,
20 pub icp_root: PathBuf,
21 pub config_path: Option<PathBuf>,
22}
23
24pub fn collect_local_role_artifact_manifest(
26 request: &LocalArtifactManifestRequest,
27) -> RoleArtifactManifestV1 {
28 let config = deployment_config_path(&request.workspace_root, request.config_path.as_deref());
29 let mut unresolved_artifacts = Vec::new();
30 let fleet_name = configured_fleet_name(&config).unwrap_or_else(|err| {
31 unresolved_artifacts.push(observation_gap(
32 "local_config.fleet_name",
33 format!(
34 "could not resolve fleet name from {}: {err}",
35 config.display()
36 ),
37 ));
38 "unknown".to_string()
39 });
40 let roles = configured_deployable_roles(&config).map_or_else(
41 |err| {
42 unresolved_artifacts.push(observation_gap(
43 "local_config.roles",
44 format!(
45 "could not resolve configured roles from {}: {err}",
46 config.display()
47 ),
48 ));
49 Vec::new()
50 },
51 deployment_truth_roles_with_implicit_wasm_store,
52 );
53 let artifact_root_result =
54 resolve_artifact_root_for_observation(&request.icp_root, &request.network);
55 let artifact_root = match artifact_root_result {
56 Ok(root) => Some(root),
57 Err(err) => {
58 unresolved_artifacts.push(observation_gap(
59 "local_artifacts.root",
60 format!(
61 "could not resolve artifact root for network {}: {err}",
62 request.network
63 ),
64 ));
65 None
66 }
67 };
68 let release_entries = artifact_root
69 .as_ref()
70 .and_then(|root| load_release_entries(root, &mut unresolved_artifacts));
71 let role_artifacts = artifact_root.as_ref().map_or_else(Vec::new, |root| {
72 roles
73 .iter()
74 .map(|role| {
75 role_artifact_from_local_files(
76 root,
77 role,
78 release_entries.as_ref(),
79 &mut unresolved_artifacts,
80 )
81 })
82 .collect()
83 });
84
85 RoleArtifactManifestV1 {
86 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
87 manifest_id: format!("local:{}:{fleet_name}:artifacts", request.network),
88 network: request.network.clone(),
89 artifact_root: artifact_root.map(|root| root.display().to_string()),
90 role_artifacts,
91 unresolved_artifacts,
92 }
93}
94
95pub(super) fn collect_observed_artifacts(
96 icp_root: &Path,
97 network: &str,
98 roles: &[String],
99 unresolved_observations: &mut Vec<DeploymentObservationGapV1>,
100) -> Vec<ObservedArtifactV1> {
101 let artifact_root = match resolve_artifact_root_for_observation(icp_root, network) {
102 Ok(root) => root,
103 Err(err) => {
104 unresolved_observations.push(observation_gap(
105 "local_artifacts.root",
106 format!("could not resolve artifact root for network {network}: {err}"),
107 ));
108 return Vec::new();
109 }
110 };
111
112 roles
113 .iter()
114 .filter_map(|role| {
115 let path = artifact_root.join(role).join(format!("{role}.wasm.gz"));
116 if !path.is_file() {
117 unresolved_observations.push(observation_gap(
118 format!("local_artifacts.{role}"),
119 format!("missing built artifact {}", path.display()),
120 ));
121 return None;
122 }
123 let size = fs::metadata(&path).ok().map(|metadata| metadata.len());
124 let file_sha256 = observe_file_sha256(&path, role, unresolved_observations);
125 let file_sha256_source = file_sha256
126 .as_ref()
127 .map(|_| ArtifactDigestSourceV1::ObservedFileDigest);
128 Some(ObservedArtifactV1 {
129 role: role.clone(),
130 artifact_path: path.display().to_string(),
131 file_sha256,
132 file_sha256_source,
133 payload_sha256: None,
134 payload_size_bytes: size,
135 source: deployment_truth_artifact_source(role),
136 })
137 })
138 .collect()
139}
140
141fn load_release_entries(
142 artifact_root: &Path,
143 unresolved_artifacts: &mut Vec<DeploymentObservationGapV1>,
144) -> Option<BTreeMap<String, crate::release_set::ReleaseSetEntry>> {
145 let manifest_path = artifact_root
146 .join("root")
147 .join(ROOT_RELEASE_SET_MANIFEST_FILE);
148 if !manifest_path.is_file() {
149 unresolved_artifacts.push(observation_gap(
150 "local_artifacts.release_set_manifest",
151 format!("missing release-set manifest {}", manifest_path.display()),
152 ));
153 return None;
154 }
155 match load_root_release_set_manifest(&manifest_path) {
156 Ok(manifest) => Some(
157 manifest
158 .entries
159 .into_iter()
160 .map(|entry| (entry.role.clone(), entry))
161 .collect(),
162 ),
163 Err(err) => {
164 unresolved_artifacts.push(observation_gap(
165 "local_artifacts.release_set_manifest",
166 format!(
167 "could not read release-set manifest {}: {err}",
168 manifest_path.display()
169 ),
170 ));
171 None
172 }
173 }
174}
175
176fn resolve_artifact_root_for_observation(
177 icp_root: &Path,
178 network: &str,
179) -> Result<PathBuf, Box<dyn std::error::Error>> {
180 let preferred = icp_root.join(".icp").join(network).join("canisters");
181 if preferred.is_dir() {
182 return Ok(preferred);
183 }
184
185 Err(format!("missing built ICP artifacts under {}", preferred.display()).into())
186}
187
188pub(in crate::deployment_truth) fn release_set_manifest_digest(
189 icp_root: &Path,
190 network: &str,
191 gaps: &mut Vec<DeploymentObservationGapV1>,
192) -> Option<String> {
193 let artifact_root = match resolve_artifact_root_for_observation(icp_root, network) {
194 Ok(root) => root,
195 Err(err) => {
196 gaps.push(observation_gap(
197 "deployment_manifest.path",
198 format!("could not resolve release-set manifest root: {err}"),
199 ));
200 return None;
201 }
202 };
203 let manifest_path = artifact_root
204 .join("root")
205 .join(ROOT_RELEASE_SET_MANIFEST_FILE);
206 if !manifest_path.is_file() {
207 gaps.push(observation_gap(
208 "deployment_manifest.path",
209 format!("missing release-set manifest {}", manifest_path.display()),
210 ));
211 return None;
212 }
213
214 match file_sha256_hex(&manifest_path) {
215 Ok(hash) => Some(hash),
216 Err(err) => {
217 gaps.push(observation_gap(
218 "deployment_manifest.sha256",
219 format!(
220 "could not hash release-set manifest {}: {err}",
221 manifest_path.display()
222 ),
223 ));
224 None
225 }
226 }
227}
228
229fn role_artifact_from_local_files(
230 artifact_root: &Path,
231 role: &str,
232 release_entries: Option<&BTreeMap<String, crate::release_set::ReleaseSetEntry>>,
233 unresolved_artifacts: &mut Vec<DeploymentObservationGapV1>,
234) -> RoleArtifactV1 {
235 let wasm_gz_path = artifact_root.join(role).join(format!("{role}.wasm.gz"));
236 let (wasm_gz_size_bytes, observed_wasm_gz_file_sha256) = if wasm_gz_path.is_file() {
237 (
238 fs::metadata(&wasm_gz_path)
239 .ok()
240 .map(|metadata| metadata.len()),
241 observe_file_sha256(&wasm_gz_path, role, unresolved_artifacts),
242 )
243 } else {
244 unresolved_artifacts.push(observation_gap(
245 format!("local_artifacts.{role}"),
246 format!("missing built artifact {}", wasm_gz_path.display()),
247 ));
248 (None, None)
249 };
250 let observed_wasm_gz_file_sha256_source = observed_wasm_gz_file_sha256
251 .as_ref()
252 .map(|_| ArtifactDigestSourceV1::ObservedFileDigest);
253 let release_entry = release_entries.and_then(|entries| entries.get(role));
254 RoleArtifactV1 {
255 role: role.to_string(),
256 source: deployment_truth_artifact_source(role),
257 build_profile: "unknown".to_string(),
258 wasm_path: None,
259 wasm_gz_path: Some(wasm_gz_path.display().to_string()),
260 wasm_gz_size_bytes,
261 wasm_sha256: None,
262 wasm_gz_sha256: release_entry.map(|entry| entry.payload_sha256_hex.clone()),
263 wasm_gz_sha256_source: release_entry.map(|_| ArtifactDigestSourceV1::ReleaseSetManifest),
264 observed_wasm_gz_file_sha256,
265 observed_wasm_gz_file_sha256_source,
266 installed_module_hash: None,
267 candid_path: None,
268 candid_sha256: None,
269 raw_config_sha256: None,
270 canonical_embedded_config_sha256: None,
271 embedded_topology_sha256: None,
272 builder_version: Some(env!("CARGO_PKG_VERSION").to_string()),
273 rust_toolchain: None,
274 package_version: None,
275 }
276}
277
278fn observe_file_sha256(
279 path: &Path,
280 role: &str,
281 gaps: &mut Vec<DeploymentObservationGapV1>,
282) -> Option<String> {
283 match file_sha256_hex(path) {
284 Ok(hash) => Some(hash),
285 Err(err) => {
286 gaps.push(observation_gap(
287 format!("local_artifacts.{role}.file_sha256"),
288 format!("could not hash artifact {}: {err}", path.display()),
289 ));
290 None
291 }
292 }
293}
294
295pub(super) fn observe_config_sha256(
296 path: &Path,
297 gaps: &mut Vec<DeploymentObservationGapV1>,
298) -> Option<String> {
299 match file_sha256_hex(path) {
300 Ok(hash) => Some(hash),
301 Err(err) => {
302 gaps.push(observation_gap(
303 "local_config.raw_sha256",
304 format!("could not hash config {}: {err}", path.display()),
305 ));
306 None
307 }
308 }
309}
310
311pub(super) fn observe_deployment_manifest_digest(
312 icp_root: &Path,
313 network: &str,
314 gaps: &mut Vec<DeploymentObservationGapV1>,
315) -> Option<String> {
316 release_set_manifest_digest(icp_root, network, gaps)
317}
318
319pub(super) fn observe_canonical_runtime_config_digest(
320 path: &Path,
321 gaps: &mut Vec<DeploymentObservationGapV1>,
322) -> Option<String> {
323 match canonical_runtime_config_sha256_hex(path) {
324 Ok(hash) => Some(hash),
325 Err(err) => {
326 gaps.push(observation_gap(
327 "local_config.canonical_runtime_config_sha256",
328 format!(
329 "could not hash canonical runtime config {}: {err}",
330 path.display()
331 ),
332 ));
333 None
334 }
335 }
336}