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