use super::{PublicationReceipt, PublicationState, Publisher, VerificationReport, receipt};
use crate::command::{CommandRequest, CommandRunner};
use crate::config::TargetConfig;
use crate::domain::{ArtifactManifest, TargetPlan};
use crate::oci::{
OCI_REGISTRY_PUBLISHER, OciClient, PreparedImageAction, PreparedOciImage, canonical_reference,
command_error, image_identity, image_reference, verify_local, verify_remote,
};
use anyhow::{Result, bail};
use std::path::PathBuf;
use std::sync::Arc;
pub struct OciRegistryPublisher {
root: PathBuf,
target: String,
image: String,
platform: String,
client: OciClient,
runner: Arc<dyn CommandRunner>,
}
impl OciRegistryPublisher {
pub fn new(
root: impl Into<PathBuf>,
target_config: &TargetConfig,
runner: Arc<dyn CommandRunner>,
) -> Result<Self> {
let TargetConfig::OciImage {
name,
image,
platform,
..
} = target_config
else {
bail!("OciRegistryPublisher requires an oci_image target config");
};
let root = root.into();
Ok(Self {
client: OciClient::new(&root, Arc::clone(&runner)),
root,
target: name.clone(),
image: image.clone(),
platform: platform.clone(),
runner,
})
}
fn validate_plan(&self, plan: &TargetPlan) -> Result<()> {
if plan.name != self.target || plan.publisher != OCI_REGISTRY_PUBLISHER {
bail!("target plan does not belong to the OCI registry publisher");
}
let expected = vec![image_identity(&self.image, &self.platform, &plan.release)?];
if plan.artifacts != expected {
bail!("OCI target plan inventory differs from its configuration");
}
Ok(())
}
fn validate_manifest(&self, manifest: &ArtifactManifest) -> Result<PreparedOciImage> {
let plan = TargetPlan {
name: manifest.target.clone(),
publisher: manifest.publisher.clone(),
release: manifest.release.clone(),
artifacts: manifest
.artifacts
.iter()
.map(|artifact| artifact.identity.clone())
.collect(),
};
self.validate_plan(&plan)?;
PreparedOciImage::read(manifest)
}
fn verify_prepared_remote(
&self,
prepared: &PreparedOciImage,
remote: &crate::oci::RemoteImageMetadata,
) -> Result<()> {
verify_remote(prepared, remote)
}
fn publish_prepared(&self, prepared: &PreparedOciImage) -> Result<bool> {
let current = prepared.current_reference();
if let Some(remote) = self.client.remote_image(¤t)? {
self.verify_prepared_remote(prepared, &remote)?;
return Ok(false);
}
let request = match &prepared.action {
PreparedImageAction::Existing { .. } => {
bail!(
"OCI image `{current}` disappeared after Prepare; refusing to recreate unexpected remote state"
)
}
PreparedImageAction::Reuse {
source_reference, ..
} => CommandRequest::new(
"docker",
[
"buildx",
"imagetools",
"create",
"--prefer-index=false",
"--tag",
current.as_str(),
source_reference.as_str(),
],
&self.root,
),
PreparedImageAction::Build { local_reference } => {
let local = self.client.local_image(local_reference)?;
verify_local(prepared, &local)?;
CommandRequest::new(
"docker",
["image", "push", local_reference.as_str()],
&self.root,
)
}
};
let write = self.runner.execute(&request)?;
let write_succeeded = write.status == 0;
match self.client.remote_image(¤t) {
Ok(Some(remote)) => match self.verify_prepared_remote(prepared, &remote) {
Ok(()) => Ok(true),
Err(verification_error) if !write_succeeded => Err(command_error(
write,
"publish OCI image",
)
.context(format!(
"OCI write failed and remote reconciliation found invalid state: {verification_error:#}"
))),
Err(verification_error) => Err(verification_error),
},
Ok(None) if !write_succeeded => Err(command_error(write, "publish OCI image")
.context(format!("OCI write failed and `{current}` remains absent"))),
Err(inspect_error) if !write_succeeded => Err(command_error(
write,
"publish OCI image",
)
.context(format!(
"OCI write failed; remote reconciliation also failed: {inspect_error:#}"
))),
Ok(None) => bail!("OCI image `{current}` is absent after a successful write"),
Err(inspect_error) => Err(inspect_error),
}
}
}
impl Publisher for OciRegistryPublisher {
fn inspect(&self, plan: &TargetPlan) -> Result<PublicationState> {
self.validate_plan(plan)?;
let (repository, _, current) = image_reference(&self.image, &plan.release.tag.to_string())?;
let Some(remote) = self.client.remote_image(¤t)? else {
return Ok(PublicationState::Absent);
};
if remote.config.platform != self.platform {
return Ok(PublicationState::Invalid {
reason: format!(
"OCI image `{current}` platform is {}, expected {}",
remote.config.platform, self.platform
),
});
}
if !plan.release.tag_already_sealed {
return Ok(PublicationState::Invalid {
reason: "OCI artifacts exist for an unsealed release candidate; refusing to adopt orphan publication state"
.to_owned(),
});
}
Ok(PublicationState::Partial {
present: vec![canonical_reference(&repository, &remote.manifest_digest)],
missing: vec![format!("reuse_check:{}", self.target)],
})
}
fn publish(&self, manifest: &ArtifactManifest) -> Result<PublicationReceipt> {
if !manifest.release.tag_already_sealed {
bail!(
"release {} is not sealed on the remote",
manifest.release.tag
);
}
let prepared = self.validate_manifest(manifest)?;
let wrote = self.publish_prepared(&prepared)?;
self.verify(manifest)?;
Ok(receipt(manifest, OCI_REGISTRY_PUBLISHER, !wrote))
}
fn verify(&self, manifest: &ArtifactManifest) -> Result<VerificationReport> {
let prepared = self.validate_manifest(manifest)?;
let current = prepared.current_reference();
let remote = self
.client
.remote_image(¤t)?
.ok_or_else(|| anyhow::anyhow!("published OCI image is missing: {current}"))?;
self.verify_prepared_remote(&prepared, &remote)?;
Ok(VerificationReport {
target: manifest.target.clone(),
verified: true,
artifacts: vec![canonical_reference(
&prepared.repository,
&remote.manifest_digest,
)],
})
}
fn verify_existing(&self, plan: &TargetPlan) -> Result<VerificationReport> {
self.validate_plan(plan)?;
bail!(
"OCI image targets require Prepare/reuse_check before existing artifacts are verified"
)
}
}