canic_host/release_set/current/
mod.rs1#[cfg(test)]
8mod tests;
9
10use crate::{
11 MAX_DOCUMENT_READ_BYTES,
12 release_build::{ReleaseBuildPlanError, ReleaseBuildPlanState, load_release_build_plan},
13 release_set::{
14 PersistedApplicationArtifactUnion, PersistedCanicInfrastructureArtifactManifest,
15 fixture::{
16 FixtureArtifactError, PersistedFixtureArtifactManifest, load_fixture_artifact_manifest,
17 verify_fixture_artifacts,
18 },
19 },
20};
21use canic_contracts::ids::{BuildNetwork, ReleaseBuildId};
22use canic_core::bootstrap::compiled::ComponentTopology;
23use ic_host_artifacts::artifact::ArtifactError;
24use ic_host_fs::{durable::create_new_bytes_with_parents, read::read_optional_file_no_follow};
25use serde::{Deserialize, Serialize};
26use sha2_host::{Digest, Sha256};
27use std::{
28 io,
29 path::{Path, PathBuf},
30};
31use thiserror::Error as ThisError;
32
33pub const CURRENT_RELEASE_SET_MANIFEST_FILE: &str = "current-release-set-manifest.json";
34
35#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
38#[serde(rename_all = "snake_case")]
39pub enum ReleaseTransitionMode {
40 ReinstallOnly,
41}
42
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
45#[serde(deny_unknown_fields)]
46pub struct CurrentReleaseSetManifest {
47 pub application_artifact_union_sha256: [u8; 32],
48 pub build_network: BuildNetwork,
49 pub fixture_artifact_manifest_sha256: [u8; 32],
50 pub infrastructure_artifact_manifest_sha256: [u8; 32],
51 pub release_build_id: ReleaseBuildId,
52 pub schema_version: u16,
53 pub transition_mode: ReleaseTransitionMode,
54}
55
56impl CurrentReleaseSetManifest {
57 pub const SCHEMA_VERSION: u16 = 1;
58
59 pub fn validate(
60 &self,
61 release_build_id: ReleaseBuildId,
62 ) -> Result<(), CurrentReleaseSetManifestError> {
63 if self.schema_version != Self::SCHEMA_VERSION {
64 return Err(CurrentReleaseSetManifestError::Invalid(format!(
65 "unsupported schema version {}",
66 self.schema_version
67 )));
68 }
69 if self.release_build_id != release_build_id {
70 return Err(CurrentReleaseSetManifestError::Invalid(
71 "path and document release-build identities differ".to_string(),
72 ));
73 }
74 Ok(())
75 }
76
77 pub fn verify_fixtures(
79 &self,
80 root: &Path,
81 topology: &ComponentTopology,
82 ) -> Result<PersistedFixtureArtifactManifest, FixtureArtifactError> {
83 let fixtures = load_fixture_artifact_manifest(
84 root,
85 topology,
86 self.release_build_id,
87 self.fixture_artifact_manifest_sha256,
88 )?;
89 verify_fixture_artifacts(
90 root,
91 topology,
92 self.release_build_id,
93 self.fixture_artifact_manifest_sha256,
94 )?;
95 Ok(fixtures)
96 }
97
98 pub fn canonical_bytes(&self) -> Result<Vec<u8>, CurrentReleaseSetManifestError> {
99 self.validate(self.release_build_id)?;
100 serde_json::to_vec(self).map_err(CurrentReleaseSetManifestError::Serialize)
101 }
102}
103
104#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct PersistedCurrentReleaseSetManifest {
107 pub digest: [u8; 32],
108 pub manifest: CurrentReleaseSetManifest,
109 pub path: PathBuf,
110}
111
112#[derive(Debug, ThisError)]
114pub enum CurrentReleaseSetManifestError {
115 #[error("current release-set manifest already exists with different contents: {0}")]
116 Conflict(PathBuf),
117
118 #[error("invalid current release-set manifest: {0}")]
119 Invalid(String),
120
121 #[error("current release-set manifest is missing: {0}")]
122 Missing(PathBuf),
123
124 #[error("current release-set manifest is not a regular no-follow file: {0}")]
125 Unsafe(PathBuf),
126
127 #[error("failed to access current release-set manifest {path}: {source}")]
128 Io {
129 path: PathBuf,
130 #[source]
131 source: io::Error,
132 },
133
134 #[error("failed to encode current release-set manifest: {0}")]
135 Serialize(serde_json::Error),
136
137 #[error(transparent)]
138 Fixture(#[from] FixtureArtifactError),
139
140 #[error(transparent)]
141 ReleaseBuild(#[from] ReleaseBuildPlanError),
142}
143
144pub fn compile_and_persist_current_release_set_manifest(
146 root: &Path,
147 topology: &ComponentTopology,
148 release_build_id: ReleaseBuildId,
149 application: &PersistedApplicationArtifactUnion,
150 infrastructure: &PersistedCanicInfrastructureArtifactManifest,
151 fixtures: &PersistedFixtureArtifactManifest,
152) -> Result<PersistedCurrentReleaseSetManifest, CurrentReleaseSetManifestError> {
153 let release_build = load_release_build_plan(root, release_build_id)?;
154 if application.union.release_build_id != release_build_id
155 || infrastructure.manifest.release_build_id != release_build_id
156 {
157 return Err(CurrentReleaseSetManifestError::Invalid(
158 "child manifest release-build identities differ".to_string(),
159 ));
160 }
161 let retained =
162 load_fixture_artifact_manifest(root, topology, release_build_id, fixtures.digest)?;
163 if retained != *fixtures
164 || application.union.fleet_component_topology_digest
165 != retained.manifest.component_topology_digest
166 {
167 return Err(FixtureArtifactError::Authority.into());
168 }
169 verify_fixture_artifacts(root, topology, release_build_id, fixtures.digest)?;
170 let expected = CurrentReleaseSetManifest {
171 application_artifact_union_sha256: application.digest,
172 build_network: release_build.build_network,
173 fixture_artifact_manifest_sha256: fixtures.digest,
174 infrastructure_artifact_manifest_sha256: infrastructure.digest,
175 release_build_id,
176 schema_version: CurrentReleaseSetManifest::SCHEMA_VERSION,
177 transition_mode: ReleaseTransitionMode::ReinstallOnly,
178 };
179 let path = current_release_set_manifest_path(root, release_build_id);
180 let existing = load_optional(&path, release_build_id)?;
181 if matches!(release_build.state, ReleaseBuildPlanState::Finalized { .. }) {
182 return match existing {
183 Some(existing) if existing.manifest == expected => Ok(existing),
184 _ => Err(CurrentReleaseSetManifestError::Conflict(path)),
185 };
186 }
187 if let Some(existing) = existing {
188 return if existing.manifest == expected {
189 Ok(existing)
190 } else {
191 Err(CurrentReleaseSetManifestError::Conflict(path))
192 };
193 }
194 let bytes = expected.canonical_bytes()?;
195 if let Err(source) =
196 create_new_bytes_with_parents(&path, &bytes).map_err(crate::publication::ops::io_error)
197 {
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_file_no_follow(path, MAX_DOCUMENT_READ_BYTES) {
222 Ok(value) => value,
223 Err(ArtifactError::NotRegularFile) => {
224 return Err(CurrentReleaseSetManifestError::Unsafe(path.to_path_buf()));
225 }
226 Err(source) => {
227 return Err(CurrentReleaseSetManifestError::Io {
228 path: path.to_path_buf(),
229 source: source.into(),
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}