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