Skip to main content

canic_host/release_set/current/
mod.rs

1//! Module: release_set::current
2//!
3//! Responsibility: bind the application union and infrastructure manifest into one release authority.
4//! Does not own: artifact builds, topology, Fleet identities, or installation.
5//! Boundary: finalization hashes one canonical manifest whose children were independently validated.
6
7#[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/// Canonical complete current release authority consumed by Fleet generation.
31#[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/// One persisted complete current release authority.
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub struct PersistedCurrentReleaseSetManifest {
71    pub digest: [u8; 32],
72    pub manifest: CurrentReleaseSetManifest,
73    pub path: PathBuf,
74}
75
76/// Typed complete release-set persistence failure.
77#[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
105/// Bind two exact child manifests before the release build becomes immutable.
106pub 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
154/// Load one exact complete current release authority.
155pub 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}