1#[cfg(test)]
8mod tests;
9
10use crate::{
11 durable_io::{
12 RegularFileReadError, create_new_bytes_with_parents, read_optional_regular_bytes,
13 },
14 release_build::{ReleaseBuildPlanError, ReleaseBuildPlanState, load_release_build_plan},
15 release_set::artifact::{
16 ReleaseArtifactMaterializationError, contains_release_build_identity,
17 materialize_qualified_release_artifact,
18 },
19};
20use std::{
21 io,
22 path::{Path, PathBuf},
23};
24
25use canic_core::ids::ReleaseBuildId;
26use sha2::{Digest, Sha256};
27use thiserror::Error as ThisError;
28
29use super::{
30 CanicInfrastructureArtifactInput, CanicInfrastructureArtifactManifest,
31 CanicInfrastructureArtifactManifestError, CanicInfrastructureRole,
32};
33
34const INFRASTRUCTURE_ARTIFACT_MANIFEST_FILE: &str = "infrastructure-artifact-manifest.json";
35
36#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct PersistedCanicInfrastructureArtifactManifest {
44 pub manifest: CanicInfrastructureArtifactManifest,
45 pub digest: [u8; 32],
46 pub path: PathBuf,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct CanicInfrastructureArtifactBuildOutput {
57 pub role: CanicInfrastructureRole,
58 pub package: String,
59 pub release_build_id: ReleaseBuildId,
60 pub wasm_path: PathBuf,
61 pub wasm_gz_path: PathBuf,
62}
63
64#[derive(Debug, ThisError)]
71pub enum CanicInfrastructureArtifactPersistenceError {
72 #[error("infrastructure artifact {role:?} {kind} path is outside the ICP root: {path}")]
73 ArtifactOutsideRoot {
74 role: CanicInfrastructureRole,
75 kind: &'static str,
76 path: PathBuf,
77 },
78
79 #[error("failed to read infrastructure artifact {role:?} {kind} at {path}: {source}")]
80 ArtifactRead {
81 role: CanicInfrastructureRole,
82 kind: &'static str,
83 path: PathBuf,
84 #[source]
85 source: io::Error,
86 },
87
88 #[error(
89 "infrastructure artifact manifest already exists with different canonical bytes: {path}"
90 )]
91 ConflictingManifest { path: PathBuf },
92
93 #[error(
94 "finalized release build {release_build_id} has no exact infrastructure artifact manifest"
95 )]
96 FinalizedWithoutExactManifest { release_build_id: ReleaseBuildId },
97
98 #[error("infrastructure artifact {role:?} has an invalid {kind} path: {path}")]
99 InvalidArtifactPath {
100 role: CanicInfrastructureRole,
101 kind: &'static str,
102 path: PathBuf,
103 },
104
105 #[error(
106 "infrastructure artifact {role:?} raw Wasm at {path} does not embed release build {release_build_id}"
107 )]
108 MissingReleaseBuildIdentity {
109 role: CanicInfrastructureRole,
110 release_build_id: ReleaseBuildId,
111 path: PathBuf,
112 },
113
114 #[error("invalid infrastructure artifact manifest {path}: {reason}")]
115 InvalidManifestDocument { path: PathBuf, reason: String },
116
117 #[error(transparent)]
118 Manifest(#[from] CanicInfrastructureArtifactManifestError),
119
120 #[error("failed to access infrastructure artifact manifest {path}: {source}")]
121 ManifestIo {
122 path: PathBuf,
123 #[source]
124 source: io::Error,
125 },
126
127 #[error("infrastructure artifact manifest is missing: {path}")]
128 MissingManifest { path: PathBuf },
129
130 #[error("infrastructure artifact {role:?} has a non-UTF-8 {kind} path: {path}")]
131 NonUtf8ArtifactPath {
132 role: CanicInfrastructureRole,
133 kind: &'static str,
134 path: PathBuf,
135 },
136
137 #[error(transparent)]
138 ReleaseBuild(#[from] ReleaseBuildPlanError),
139
140 #[error("infrastructure artifact {role:?} {kind} is not a regular no-follow file: {path}")]
141 UnsafeArtifact {
142 role: CanicInfrastructureRole,
143 kind: &'static str,
144 path: PathBuf,
145 },
146
147 #[error("infrastructure artifact manifest is not a regular no-follow file: {path}")]
148 UnsafeManifest { path: PathBuf },
149}
150
151struct MaterializedBuildOutput {
152 role: CanicInfrastructureRole,
153 package: String,
154 release_build_id: ReleaseBuildId,
155 wasm_relative_path: String,
156 wasm: Vec<u8>,
157 wasm_gz_relative_path: String,
158 wasm_gz: Vec<u8>,
159}
160
161impl MaterializedBuildOutput {
162 fn as_input(&self) -> CanicInfrastructureArtifactInput<'_> {
163 CanicInfrastructureArtifactInput {
164 role: self.role,
165 package: &self.package,
166 release_build_id: self.release_build_id,
167 wasm_relative_path: &self.wasm_relative_path,
168 wasm: &self.wasm,
169 wasm_gz_relative_path: &self.wasm_gz_relative_path,
170 wasm_gz: &self.wasm_gz,
171 }
172 }
173}
174
175pub fn compile_and_persist_canic_infrastructure_artifact_manifest(
177 root: &Path,
178 release_build_id: ReleaseBuildId,
179 outputs: &[CanicInfrastructureArtifactBuildOutput],
180) -> Result<PersistedCanicInfrastructureArtifactManifest, CanicInfrastructureArtifactPersistenceError>
181{
182 let release_build = load_release_build_plan(root, release_build_id)?;
183 let materialized = outputs
184 .iter()
185 .map(|output| materialize_build_output(root, release_build_id, output))
186 .collect::<Result<Vec<_>, _>>()?;
187 let inputs = materialized
188 .iter()
189 .map(MaterializedBuildOutput::as_input)
190 .collect::<Vec<_>>();
191 let manifest = CanicInfrastructureArtifactManifest::compile(release_build_id, &inputs)?;
192 let expected = persisted_manifest(root, manifest)?;
193 let existing = load_optional_persisted_manifest(&expected.path, release_build_id)?;
194
195 if matches!(release_build.state, ReleaseBuildPlanState::Finalized { .. }) {
196 return match existing {
197 Some(observed) if observed.manifest == expected.manifest => Ok(observed),
198 _ => Err(
199 CanicInfrastructureArtifactPersistenceError::FinalizedWithoutExactManifest {
200 release_build_id,
201 },
202 ),
203 };
204 }
205
206 if let Some(observed) = existing {
207 return if observed.manifest == expected.manifest {
208 Ok(observed)
209 } else {
210 Err(
211 CanicInfrastructureArtifactPersistenceError::ConflictingManifest {
212 path: expected.path,
213 },
214 )
215 };
216 }
217
218 let canonical_bytes = expected.manifest.canonical_bytes()?;
219 if let Err(source) = create_new_bytes_with_parents(&expected.path, &canonical_bytes) {
220 match load_optional_persisted_manifest(&expected.path, release_build_id) {
221 Ok(Some(observed)) if observed.manifest == expected.manifest => return Ok(observed),
222 Ok(Some(_)) if source.kind() == io::ErrorKind::AlreadyExists => {
223 return Err(
224 CanicInfrastructureArtifactPersistenceError::ConflictingManifest {
225 path: expected.path,
226 },
227 );
228 }
229 _ => {
230 return Err(CanicInfrastructureArtifactPersistenceError::ManifestIo {
231 path: expected.path,
232 source,
233 });
234 }
235 }
236 }
237
238 load_persisted_canic_infrastructure_artifact_manifest(root, release_build_id)
239}
240
241pub fn load_persisted_canic_infrastructure_artifact_manifest(
243 root: &Path,
244 release_build_id: ReleaseBuildId,
245) -> Result<PersistedCanicInfrastructureArtifactManifest, CanicInfrastructureArtifactPersistenceError>
246{
247 let path = infrastructure_artifact_manifest_path(root, release_build_id);
248 load_optional_persisted_manifest(&path, release_build_id)?
249 .ok_or(CanicInfrastructureArtifactPersistenceError::MissingManifest { path })
250}
251
252fn materialize_build_output(
253 root: &Path,
254 release_build_id: ReleaseBuildId,
255 output: &CanicInfrastructureArtifactBuildOutput,
256) -> Result<MaterializedBuildOutput, CanicInfrastructureArtifactPersistenceError> {
257 let role = output.role;
258 if output.release_build_id != release_build_id {
259 return Err(
260 CanicInfrastructureArtifactManifestError::ReleaseBuildMismatch {
261 role,
262 expected: release_build_id,
263 actual: output.release_build_id,
264 }
265 .into(),
266 );
267 }
268
269 let wasm = materialize_artifact(root, role, "raw Wasm", &output.wasm_path)?;
270 if !contains_release_build_identity(&wasm.bytes, release_build_id) {
271 return Err(
272 CanicInfrastructureArtifactPersistenceError::MissingReleaseBuildIdentity {
273 role,
274 release_build_id,
275 path: output.wasm_path.clone(),
276 },
277 );
278 }
279 let wasm_gz = materialize_artifact(root, role, "gzip Wasm", &output.wasm_gz_path)?;
280
281 Ok(MaterializedBuildOutput {
282 role,
283 package: output.package.clone(),
284 release_build_id: output.release_build_id,
285 wasm_relative_path: wasm.relative_path,
286 wasm: wasm.bytes,
287 wasm_gz_relative_path: wasm_gz.relative_path,
288 wasm_gz: wasm_gz.bytes,
289 })
290}
291
292fn materialize_artifact(
293 root: &Path,
294 role: CanicInfrastructureRole,
295 kind: &'static str,
296 path: &Path,
297) -> Result<
298 crate::release_set::artifact::MaterializedReleaseArtifact,
299 CanicInfrastructureArtifactPersistenceError,
300> {
301 materialize_qualified_release_artifact(root, path).map_err(|error| match error {
302 ReleaseArtifactMaterializationError::InvalidPath => {
303 CanicInfrastructureArtifactPersistenceError::InvalidArtifactPath {
304 role,
305 kind,
306 path: path.to_path_buf(),
307 }
308 }
309 ReleaseArtifactMaterializationError::NonUtf8Path => {
310 CanicInfrastructureArtifactPersistenceError::NonUtf8ArtifactPath {
311 role,
312 kind,
313 path: path.to_path_buf(),
314 }
315 }
316 ReleaseArtifactMaterializationError::OutsideRoot => {
317 CanicInfrastructureArtifactPersistenceError::ArtifactOutsideRoot {
318 role,
319 kind,
320 path: path.to_path_buf(),
321 }
322 }
323 ReleaseArtifactMaterializationError::Read(source) => {
324 CanicInfrastructureArtifactPersistenceError::ArtifactRead {
325 role,
326 kind,
327 path: path.to_path_buf(),
328 source,
329 }
330 }
331 ReleaseArtifactMaterializationError::UnsafeFile => {
332 CanicInfrastructureArtifactPersistenceError::UnsafeArtifact {
333 role,
334 kind,
335 path: path.to_path_buf(),
336 }
337 }
338 })
339}
340
341fn persisted_manifest(
342 root: &Path,
343 manifest: CanicInfrastructureArtifactManifest,
344) -> Result<PersistedCanicInfrastructureArtifactManifest, CanicInfrastructureArtifactPersistenceError>
345{
346 let digest = manifest.digest()?;
347 let path = infrastructure_artifact_manifest_path(root, manifest.release_build_id);
348 Ok(PersistedCanicInfrastructureArtifactManifest {
349 manifest,
350 digest,
351 path,
352 })
353}
354
355fn load_optional_persisted_manifest(
356 path: &Path,
357 release_build_id: ReleaseBuildId,
358) -> Result<
359 Option<PersistedCanicInfrastructureArtifactManifest>,
360 CanicInfrastructureArtifactPersistenceError,
361> {
362 let bytes = match read_optional_regular_bytes(path) {
363 Ok(bytes) => bytes,
364 Err(RegularFileReadError::NotRegular) => {
365 return Err(
366 CanicInfrastructureArtifactPersistenceError::UnsafeManifest {
367 path: path.to_path_buf(),
368 },
369 );
370 }
371 Err(RegularFileReadError::Io(source)) => {
372 return Err(CanicInfrastructureArtifactPersistenceError::ManifestIo {
373 path: path.to_path_buf(),
374 source,
375 });
376 }
377 #[cfg(not(unix))]
378 Err(RegularFileReadError::UnsupportedPlatform) => {
379 return Err(CanicInfrastructureArtifactPersistenceError::ManifestIo {
380 path: path.to_path_buf(),
381 source: io::Error::new(
382 io::ErrorKind::Unsupported,
383 "regular no-follow manifest reads are unsupported",
384 ),
385 });
386 }
387 };
388 let Some(bytes) = bytes else {
389 return Ok(None);
390 };
391
392 let manifest: CanicInfrastructureArtifactManifest =
393 serde_json::from_slice(&bytes).map_err(|error| {
394 CanicInfrastructureArtifactPersistenceError::InvalidManifestDocument {
395 path: path.to_path_buf(),
396 reason: error.to_string(),
397 }
398 })?;
399 if manifest.release_build_id != release_build_id {
400 return Err(
401 CanicInfrastructureArtifactPersistenceError::InvalidManifestDocument {
402 path: path.to_path_buf(),
403 reason: format!(
404 "manifest release build {} does not match path release build {release_build_id}",
405 manifest.release_build_id
406 ),
407 },
408 );
409 }
410 let canonical = manifest.canonical_bytes()?;
411 if canonical != bytes {
412 return Err(
413 CanicInfrastructureArtifactPersistenceError::InvalidManifestDocument {
414 path: path.to_path_buf(),
415 reason: "manifest bytes are not canonical".to_string(),
416 },
417 );
418 }
419
420 Ok(Some(PersistedCanicInfrastructureArtifactManifest {
421 digest: Sha256::digest(&bytes).into(),
422 manifest,
423 path: path.to_path_buf(),
424 }))
425}
426
427fn infrastructure_artifact_manifest_path(root: &Path, release_build_id: ReleaseBuildId) -> PathBuf {
428 root.join(".canic")
429 .join("release-builds")
430 .join(release_build_id.to_string())
431 .join(INFRASTRUCTURE_ARTIFACT_MANIFEST_FILE)
432}