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