canic_host/release_set/current/
mod.rs1#[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::{
16 PersistedApplicationArtifactUnion, PersistedCanicInfrastructureArtifactManifest,
17 },
18};
19use canic_core::ids::ReleaseBuildId;
20use serde::{Deserialize, Serialize};
21use sha2::{Digest, Sha256};
22use std::{
23 io,
24 path::{Path, PathBuf},
25};
26use thiserror::Error as ThisError;
27
28pub const CURRENT_RELEASE_SET_MANIFEST_FILE: &str = "current-release-set-manifest.json";
29
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(deny_unknown_fields)]
33pub struct CurrentReleaseSetManifest {
34 pub application_artifact_union_sha256: [u8; 32],
35 pub infrastructure_artifact_manifest_sha256: [u8; 32],
36 pub release_build_id: ReleaseBuildId,
37 pub schema_version: u16,
38}
39
40impl CurrentReleaseSetManifest {
41 pub const SCHEMA_VERSION: u16 = 1;
42
43 pub fn validate(
44 &self,
45 release_build_id: ReleaseBuildId,
46 ) -> Result<(), CurrentReleaseSetManifestError> {
47 if self.schema_version != Self::SCHEMA_VERSION {
48 return Err(CurrentReleaseSetManifestError::Invalid(format!(
49 "unsupported schema version {}",
50 self.schema_version
51 )));
52 }
53 if self.release_build_id != release_build_id {
54 return Err(CurrentReleaseSetManifestError::Invalid(
55 "path and document release-build identities differ".to_string(),
56 ));
57 }
58 Ok(())
59 }
60
61 pub fn canonical_bytes(&self) -> Result<Vec<u8>, CurrentReleaseSetManifestError> {
62 self.validate(self.release_build_id)?;
63 serde_json::to_vec(self).map_err(CurrentReleaseSetManifestError::Serialize)
64 }
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct PersistedCurrentReleaseSetManifest {
70 pub digest: [u8; 32],
71 pub manifest: CurrentReleaseSetManifest,
72 pub path: PathBuf,
73}
74
75#[derive(Debug, ThisError)]
77pub enum CurrentReleaseSetManifestError {
78 #[error("current release-set manifest already exists with different contents: {0}")]
79 Conflict(PathBuf),
80
81 #[error("invalid current release-set manifest: {0}")]
82 Invalid(String),
83
84 #[error("current release-set manifest is missing: {0}")]
85 Missing(PathBuf),
86
87 #[error("current release-set manifest is not a regular no-follow file: {0}")]
88 Unsafe(PathBuf),
89
90 #[error("failed to access current release-set manifest {path}: {source}")]
91 Io {
92 path: PathBuf,
93 #[source]
94 source: io::Error,
95 },
96
97 #[error("failed to encode current release-set manifest: {0}")]
98 Serialize(serde_json::Error),
99
100 #[error(transparent)]
101 ReleaseBuild(#[from] ReleaseBuildPlanError),
102}
103
104pub fn compile_and_persist_current_release_set_manifest(
106 root: &Path,
107 release_build_id: ReleaseBuildId,
108 application: &PersistedApplicationArtifactUnion,
109 infrastructure: &PersistedCanicInfrastructureArtifactManifest,
110) -> Result<PersistedCurrentReleaseSetManifest, CurrentReleaseSetManifestError> {
111 let release_build = load_release_build_plan(root, release_build_id)?;
112 if application.union.release_build_id != release_build_id
113 || infrastructure.manifest.release_build_id != release_build_id
114 {
115 return Err(CurrentReleaseSetManifestError::Invalid(
116 "child manifest release-build identities differ".to_string(),
117 ));
118 }
119 let expected = CurrentReleaseSetManifest {
120 application_artifact_union_sha256: application.digest,
121 infrastructure_artifact_manifest_sha256: infrastructure.digest,
122 release_build_id,
123 schema_version: CurrentReleaseSetManifest::SCHEMA_VERSION,
124 };
125 let path = current_release_set_manifest_path(root, release_build_id);
126 let existing = load_optional(&path, release_build_id)?;
127 if matches!(release_build.state, ReleaseBuildPlanState::Finalized { .. }) {
128 return match existing {
129 Some(existing) if existing.manifest == expected => Ok(existing),
130 _ => Err(CurrentReleaseSetManifestError::Conflict(path)),
131 };
132 }
133 if let Some(existing) = existing {
134 return if existing.manifest == expected {
135 Ok(existing)
136 } else {
137 Err(CurrentReleaseSetManifestError::Conflict(path))
138 };
139 }
140 let bytes = expected.canonical_bytes()?;
141 if let Err(source) = create_new_bytes_with_parents(&path, &bytes) {
142 if let Ok(Some(existing)) = load_optional(&path, release_build_id)
143 && existing.manifest == expected
144 {
145 return Ok(existing);
146 }
147 return Err(CurrentReleaseSetManifestError::Io { path, source });
148 }
149 load_persisted_current_release_set_manifest(root, release_build_id)
150}
151
152pub fn load_persisted_current_release_set_manifest(
154 root: &Path,
155 release_build_id: ReleaseBuildId,
156) -> Result<PersistedCurrentReleaseSetManifest, CurrentReleaseSetManifestError> {
157 let path = current_release_set_manifest_path(root, release_build_id);
158 load_optional(&path, release_build_id)?.ok_or(CurrentReleaseSetManifestError::Missing(path))
159}
160
161fn load_optional(
162 path: &Path,
163 release_build_id: ReleaseBuildId,
164) -> Result<Option<PersistedCurrentReleaseSetManifest>, CurrentReleaseSetManifestError> {
165 let bytes = match read_optional_regular_bytes(path) {
166 Ok(value) => value,
167 Err(RegularFileReadError::NotRegular) => {
168 return Err(CurrentReleaseSetManifestError::Unsafe(path.to_path_buf()));
169 }
170 Err(RegularFileReadError::Io(source)) => {
171 return Err(CurrentReleaseSetManifestError::Io {
172 path: path.to_path_buf(),
173 source,
174 });
175 }
176 #[cfg(not(unix))]
177 Err(RegularFileReadError::UnsupportedPlatform) => {
178 return Err(CurrentReleaseSetManifestError::Io {
179 path: path.to_path_buf(),
180 source: io::Error::new(
181 io::ErrorKind::Unsupported,
182 "safe file reads are unavailable",
183 ),
184 });
185 }
186 };
187 let Some(bytes) = bytes else {
188 return Ok(None);
189 };
190 let manifest: CurrentReleaseSetManifest = serde_json::from_slice(&bytes)
191 .map_err(|error| CurrentReleaseSetManifestError::Invalid(error.to_string()))?;
192 manifest.validate(release_build_id)?;
193 if manifest.canonical_bytes()? != bytes {
194 return Err(CurrentReleaseSetManifestError::Invalid(
195 "document bytes are not canonical".to_string(),
196 ));
197 }
198 Ok(Some(PersistedCurrentReleaseSetManifest {
199 digest: Sha256::digest(&bytes).into(),
200 manifest,
201 path: path.to_path_buf(),
202 }))
203}
204
205fn current_release_set_manifest_path(root: &Path, release_build_id: ReleaseBuildId) -> PathBuf {
206 root.join(".canic")
207 .join("release-builds")
208 .join(release_build_id.to_string())
209 .join(CURRENT_RELEASE_SET_MANIFEST_FILE)
210}