#[cfg(test)]
mod tests;
use crate::{
MAX_DOCUMENT_READ_BYTES,
release_build::{ReleaseBuildPlanError, ReleaseBuildPlanState, load_release_build_plan},
release_set::{
PersistedApplicationArtifactUnion, PersistedCanicInfrastructureArtifactManifest,
fixture::{
FixtureArtifactError, PersistedFixtureArtifactManifest, load_fixture_artifact_manifest,
verify_fixture_artifacts,
},
},
};
use canic_contracts::ids::{BuildNetwork, ReleaseBuildId};
use canic_core::bootstrap::compiled::ComponentTopology;
use ic_host_artifacts::artifact::ArtifactError;
use ic_host_fs::{durable::create_new_bytes_with_parents, read::read_optional_file_no_follow};
use serde::{Deserialize, Serialize};
use sha2_host::{Digest, Sha256};
use std::{
io,
path::{Path, PathBuf},
};
use thiserror::Error as ThisError;
pub const CURRENT_RELEASE_SET_MANIFEST_FILE: &str = "current-release-set-manifest.json";
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReleaseTransitionMode {
ReinstallOnly,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CurrentReleaseSetManifest {
pub application_artifact_union_sha256: [u8; 32],
pub build_network: BuildNetwork,
pub fixture_artifact_manifest_sha256: [u8; 32],
pub infrastructure_artifact_manifest_sha256: [u8; 32],
pub release_build_id: ReleaseBuildId,
pub schema_version: u16,
pub transition_mode: ReleaseTransitionMode,
}
impl CurrentReleaseSetManifest {
pub const SCHEMA_VERSION: u16 = 1;
pub fn validate(
&self,
release_build_id: ReleaseBuildId,
) -> Result<(), CurrentReleaseSetManifestError> {
if self.schema_version != Self::SCHEMA_VERSION {
return Err(CurrentReleaseSetManifestError::Invalid(format!(
"unsupported schema version {}",
self.schema_version
)));
}
if self.release_build_id != release_build_id {
return Err(CurrentReleaseSetManifestError::Invalid(
"path and document release-build identities differ".to_string(),
));
}
Ok(())
}
pub fn verify_fixtures(
&self,
root: &Path,
topology: &ComponentTopology,
) -> Result<PersistedFixtureArtifactManifest, FixtureArtifactError> {
let fixtures = load_fixture_artifact_manifest(
root,
topology,
self.release_build_id,
self.fixture_artifact_manifest_sha256,
)?;
verify_fixture_artifacts(
root,
topology,
self.release_build_id,
self.fixture_artifact_manifest_sha256,
)?;
Ok(fixtures)
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, CurrentReleaseSetManifestError> {
self.validate(self.release_build_id)?;
serde_json::to_vec(self).map_err(CurrentReleaseSetManifestError::Serialize)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PersistedCurrentReleaseSetManifest {
pub digest: [u8; 32],
pub manifest: CurrentReleaseSetManifest,
pub path: PathBuf,
}
#[derive(Debug, ThisError)]
pub enum CurrentReleaseSetManifestError {
#[error("current release-set manifest already exists with different contents: {0}")]
Conflict(PathBuf),
#[error("invalid current release-set manifest: {0}")]
Invalid(String),
#[error("current release-set manifest is missing: {0}")]
Missing(PathBuf),
#[error("current release-set manifest is not a regular no-follow file: {0}")]
Unsafe(PathBuf),
#[error("failed to access current release-set manifest {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to encode current release-set manifest: {0}")]
Serialize(serde_json::Error),
#[error(transparent)]
Fixture(#[from] FixtureArtifactError),
#[error(transparent)]
ReleaseBuild(#[from] ReleaseBuildPlanError),
}
pub fn compile_and_persist_current_release_set_manifest(
root: &Path,
topology: &ComponentTopology,
release_build_id: ReleaseBuildId,
application: &PersistedApplicationArtifactUnion,
infrastructure: &PersistedCanicInfrastructureArtifactManifest,
fixtures: &PersistedFixtureArtifactManifest,
) -> Result<PersistedCurrentReleaseSetManifest, CurrentReleaseSetManifestError> {
let release_build = load_release_build_plan(root, release_build_id)?;
if application.union.release_build_id != release_build_id
|| infrastructure.manifest.release_build_id != release_build_id
{
return Err(CurrentReleaseSetManifestError::Invalid(
"child manifest release-build identities differ".to_string(),
));
}
let retained =
load_fixture_artifact_manifest(root, topology, release_build_id, fixtures.digest)?;
if retained != *fixtures
|| application.union.fleet_component_topology_digest
!= retained.manifest.component_topology_digest
{
return Err(FixtureArtifactError::Authority.into());
}
verify_fixture_artifacts(root, topology, release_build_id, fixtures.digest)?;
let expected = CurrentReleaseSetManifest {
application_artifact_union_sha256: application.digest,
build_network: release_build.build_network,
fixture_artifact_manifest_sha256: fixtures.digest,
infrastructure_artifact_manifest_sha256: infrastructure.digest,
release_build_id,
schema_version: CurrentReleaseSetManifest::SCHEMA_VERSION,
transition_mode: ReleaseTransitionMode::ReinstallOnly,
};
let path = current_release_set_manifest_path(root, release_build_id);
let existing = load_optional(&path, release_build_id)?;
if matches!(release_build.state, ReleaseBuildPlanState::Finalized { .. }) {
return match existing {
Some(existing) if existing.manifest == expected => Ok(existing),
_ => Err(CurrentReleaseSetManifestError::Conflict(path)),
};
}
if let Some(existing) = existing {
return if existing.manifest == expected {
Ok(existing)
} else {
Err(CurrentReleaseSetManifestError::Conflict(path))
};
}
let bytes = expected.canonical_bytes()?;
if let Err(source) =
create_new_bytes_with_parents(&path, &bytes).map_err(crate::publication::ops::io_error)
{
if let Ok(Some(existing)) = load_optional(&path, release_build_id)
&& existing.manifest == expected
{
return Ok(existing);
}
return Err(CurrentReleaseSetManifestError::Io { path, source });
}
load_persisted_current_release_set_manifest(root, release_build_id)
}
pub fn load_persisted_current_release_set_manifest(
root: &Path,
release_build_id: ReleaseBuildId,
) -> Result<PersistedCurrentReleaseSetManifest, CurrentReleaseSetManifestError> {
let path = current_release_set_manifest_path(root, release_build_id);
load_optional(&path, release_build_id)?.ok_or(CurrentReleaseSetManifestError::Missing(path))
}
fn load_optional(
path: &Path,
release_build_id: ReleaseBuildId,
) -> Result<Option<PersistedCurrentReleaseSetManifest>, CurrentReleaseSetManifestError> {
let bytes = match read_optional_file_no_follow(path, MAX_DOCUMENT_READ_BYTES) {
Ok(value) => value,
Err(ArtifactError::NotRegularFile) => {
return Err(CurrentReleaseSetManifestError::Unsafe(path.to_path_buf()));
}
Err(source) => {
return Err(CurrentReleaseSetManifestError::Io {
path: path.to_path_buf(),
source: source.into(),
});
}
};
let Some(bytes) = bytes else {
return Ok(None);
};
let manifest: CurrentReleaseSetManifest = serde_json::from_slice(&bytes)
.map_err(|error| CurrentReleaseSetManifestError::Invalid(error.to_string()))?;
manifest.validate(release_build_id)?;
if manifest.canonical_bytes()? != bytes {
return Err(CurrentReleaseSetManifestError::Invalid(
"document bytes are not canonical".to_string(),
));
}
Ok(Some(PersistedCurrentReleaseSetManifest {
digest: Sha256::digest(&bytes).into(),
manifest,
path: path.to_path_buf(),
}))
}
fn current_release_set_manifest_path(root: &Path, release_build_id: ReleaseBuildId) -> PathBuf {
root.join(".canic")
.join("release-builds")
.join(release_build_id.to_string())
.join(CURRENT_RELEASE_SET_MANIFEST_FILE)
}