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