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