use super::{
digest::sha256_digest,
ghcr,
local_registry::{
LocalRegistry, RefUpdate, StoredDescriptor, TempLocalRegistry, UnsealedArtifact,
},
media_types::{self, OCI_EMPTY_CONFIG_BYTES},
ImageRef,
};
use anyhow::{bail, ensure, Context, Result};
use oci_spec::image::{Descriptor, DescriptorBuilder, Digest, ImageManifest, MediaType};
use serde::Serialize;
use std::{
collections::HashMap,
path::Path,
str::FromStr,
sync::{Arc, OnceLock},
};
use url::Url;
#[derive(Debug, Clone)]
pub struct LocalArtifact<'reg> {
registry: &'reg LocalRegistry,
image_name: ImageRef,
manifest_digest: Digest,
manifest_cache: Arc<OnceLock<LocalManifest>>,
}
#[derive(Debug, Clone)]
pub struct LocalRegistryHandle {
inner: Arc<LocalRegistryHandleInner>,
}
#[derive(Debug)]
enum LocalRegistryHandleInner {
Default(&'static LocalRegistry),
Temp(TempLocalRegistry),
}
impl LocalRegistryHandle {
pub fn shared_default() -> Result<Self> {
Ok(Self {
inner: Arc::new(LocalRegistryHandleInner::Default(
LocalRegistry::shared_default()?,
)),
})
}
pub fn temp() -> Result<Self> {
Ok(Self {
inner: Arc::new(LocalRegistryHandleInner::Temp(TempLocalRegistry::new()?)),
})
}
pub fn registry(&self) -> &LocalRegistry {
match self.inner.as_ref() {
LocalRegistryHandleInner::Default(registry) => registry,
LocalRegistryHandleInner::Temp(temp) => temp.registry(),
}
}
pub fn get_blob_dyn(&self, descriptor: &Descriptor) -> Result<Vec<u8>> {
let descriptor = self.registry().stored_descriptor(descriptor.clone())?;
self.registry().get_blob(&descriptor)
}
}
#[derive(Debug, Clone)]
pub struct LocalArtifactDyn {
registry_handle: LocalRegistryHandle,
image_name: ImageRef,
manifest_digest: Digest,
manifest_cache: Arc<OnceLock<LocalManifest>>,
}
impl LocalArtifactDyn {
pub fn open(image_name: ImageRef) -> Result<Self> {
let registry_handle = LocalRegistryHandle::shared_default()?;
Self::open_in_registry_handle(registry_handle, image_name)
}
pub fn import_archive(path: &Path) -> Result<Self> {
let registry_handle = LocalRegistryHandle::shared_default()?;
let registry = registry_handle.registry();
let image_name = if path.is_file() {
registry.import_oci_archive(path)?.image_name
} else if path.is_dir() {
registry.import_oci_dir(path)?.image_name
} else {
bail!("Path must be a file or a directory")
};
Self::open_in_registry_handle(registry_handle, image_name)
}
#[cfg(feature = "remote-artifact")]
pub fn load(image_name: ImageRef) -> Result<Self> {
let registry_handle = LocalRegistryHandle::shared_default()?;
registry_handle.registry().pull_image(&image_name)?;
Self::open_in_registry_handle(registry_handle, image_name)
}
#[cfg(not(feature = "remote-artifact"))]
pub fn load(image_name: ImageRef) -> Result<Self> {
Self::open(image_name)
}
pub fn open_in_registry_handle(
registry_handle: LocalRegistryHandle,
image_name: ImageRef,
) -> Result<Self> {
let manifest_digest = registry_handle
.registry()
.resolve_image_name(&image_name)?
.with_context(|| {
format!(
"Artifact not found in the SQLite-backed local registry: {image_name}. \
If this artifact exists in the legacy OCI directory local registry, \
run `ommx import-legacy` once, then retry."
)
})?;
Ok(Self {
registry_handle,
image_name,
manifest_digest,
manifest_cache: Arc::new(OnceLock::new()),
})
}
pub(crate) fn as_local_artifact(&self) -> LocalArtifact<'_> {
LocalArtifact {
registry: self.registry_handle.registry(),
image_name: self.image_name.clone(),
manifest_digest: self.manifest_digest.clone(),
manifest_cache: Arc::clone(&self.manifest_cache),
}
}
pub fn registry_handle(&self) -> LocalRegistryHandle {
self.registry_handle.clone()
}
pub fn image_name(&self) -> &ImageRef {
&self.image_name
}
pub fn manifest_digest(&self) -> &Digest {
&self.manifest_digest
}
pub fn annotations(&self) -> Result<HashMap<String, String>> {
self.as_local_artifact().annotations()
}
pub fn layers(&self) -> Result<Vec<StoredDescriptor<'_>>> {
self.as_local_artifact().layers()
}
pub fn get_blob(&self, descriptor: &Descriptor) -> Result<Vec<u8>> {
self.registry_handle.get_blob_dyn(descriptor)
}
pub fn get_instance_layer(&self, descriptor: &Descriptor) -> Result<crate::Instance> {
let descriptor = self
.registry_handle
.registry()
.stored_descriptor(descriptor.clone())?;
self.registry_handle
.registry()
.get_instance_layer(&descriptor)
}
pub fn get_parametric_instance_layer(
&self,
descriptor: &Descriptor,
) -> Result<crate::ParametricInstance> {
let descriptor = self
.registry_handle
.registry()
.stored_descriptor(descriptor.clone())?;
self.registry_handle
.registry()
.get_parametric_instance_layer(&descriptor)
}
pub fn get_solution_layer(&self, descriptor: &Descriptor) -> Result<crate::Solution> {
let descriptor = self
.registry_handle
.registry()
.stored_descriptor(descriptor.clone())?;
self.registry_handle
.registry()
.get_solution_layer(&descriptor)
}
pub fn get_sample_set_layer(&self, descriptor: &Descriptor) -> Result<crate::SampleSet> {
let descriptor = self
.registry_handle
.registry()
.stored_descriptor(descriptor.clone())?;
self.registry_handle
.registry()
.get_sample_set_layer(&descriptor)
}
pub fn save(&self, output: &Path) -> crate::Result<()> {
AsArtifact::save(self, output)
}
#[cfg(feature = "remote-artifact")]
pub fn push(&self) -> crate::Result<()> {
AsArtifact::push(self)
}
pub fn tag_as(&self, image_name: ImageRef) -> Result<Self> {
let artifact = self.as_local_artifact().tag_as(image_name)?;
Ok(Self {
registry_handle: self.registry_handle.clone(),
image_name: artifact.image_name().clone(),
manifest_digest: artifact.manifest_digest().clone(),
manifest_cache: Arc::new(OnceLock::new()),
})
}
}
pub trait AsArtifact<'reg> {
fn as_artifact(&'reg self) -> crate::Result<LocalArtifact<'reg>>;
fn save(&'reg self, output: &Path) -> crate::Result<()> {
self.as_artifact()?.save(output)
}
#[cfg(feature = "remote-artifact")]
fn push(&'reg self) -> crate::Result<()> {
self.as_artifact()?.push()
}
}
impl<'reg> AsArtifact<'reg> for LocalArtifactDyn {
fn as_artifact(&'reg self) -> crate::Result<LocalArtifact<'reg>> {
Ok(self.as_local_artifact())
}
}
impl LocalArtifact<'static> {
pub fn open(image_name: ImageRef) -> Result<Self> {
let registry = LocalRegistry::shared_default()?;
Self::open_in_registry(registry, image_name)
}
pub fn try_open(image_name: ImageRef) -> Result<Option<Self>> {
let registry = LocalRegistry::shared_default()?;
Self::try_open_in_registry(registry, image_name)
}
}
impl<'reg> LocalArtifact<'reg> {
pub(crate) fn from_parts(
registry: &'reg LocalRegistry,
image_name: ImageRef,
manifest_digest: Digest,
) -> Self {
Self {
registry,
image_name,
manifest_digest,
manifest_cache: Arc::new(OnceLock::new()),
}
}
pub fn open_in_registry(registry: &'reg LocalRegistry, image_name: ImageRef) -> Result<Self> {
Self::try_open_in_registry(registry, image_name.clone())?.with_context(|| {
format!(
"Artifact not found in the SQLite-backed local registry: {image_name}. \
If this artifact exists in the legacy OCI directory local registry, \
run `ommx import-legacy` once, then retry."
)
})
}
pub fn try_open_in_registry(
registry: &'reg LocalRegistry,
image_name: ImageRef,
) -> Result<Option<Self>> {
let Some(manifest_digest) = registry.resolve_image_name(&image_name)? else {
return Ok(None);
};
Ok(Some(Self::from_parts(
registry,
image_name,
manifest_digest,
)))
}
pub fn image_name(&self) -> &ImageRef {
&self.image_name
}
pub fn manifest_digest(&self) -> &Digest {
&self.manifest_digest
}
pub(crate) fn registry(&self) -> &'reg LocalRegistry {
self.registry
}
pub fn get_manifest(&self) -> Result<&LocalManifest> {
if let Some(cached) = self.manifest_cache.get() {
return Ok(cached);
}
let manifest = self.read_manifest_uncached()?;
Ok(self.manifest_cache.get_or_init(|| manifest))
}
fn read_manifest_uncached(&self) -> Result<LocalManifest> {
let bytes = self.registry.read_blob(&self.manifest_digest)?;
LocalManifest::parse(&bytes)
}
pub fn annotations(&self) -> Result<HashMap<String, String>> {
Ok(self.get_manifest()?.annotations())
}
pub fn stored_config(&self) -> Result<StoredDescriptor<'reg>> {
self.registry
.stored_descriptor(self.get_manifest()?.config())
}
pub fn layers(&self) -> Result<Vec<StoredDescriptor<'reg>>> {
self.get_manifest()?
.layers()
.into_iter()
.map(|descriptor| self.registry.stored_descriptor(descriptor))
.collect()
}
pub fn subject(&self) -> Result<Option<Descriptor>> {
Ok(self.get_manifest()?.subject())
}
pub fn get_blob(&self, descriptor: &StoredDescriptor<'_>) -> Result<Vec<u8>> {
ensure!(
descriptor.is_stored_in(self.registry),
"Descriptor {} is not stored in this LocalArtifact's Local Registry",
descriptor.digest()
);
self.registry.get_blob(descriptor)
}
pub(crate) fn get_blob_by_descriptor(&self, descriptor: &Descriptor) -> Result<Vec<u8>> {
let descriptor = self.registry.stored_descriptor(descriptor.clone())?;
self.get_blob(&descriptor)
}
pub(crate) fn read_blob_by_digest(&self, digest: &Digest) -> Result<Vec<u8>> {
self.registry.read_blob(digest)
}
pub(crate) fn stored_manifest_descriptor(&self) -> Result<StoredDescriptor<'reg>> {
self.registry
.stored_manifest_descriptor(&self.manifest_digest)
}
pub fn tag_as(&self, image_name: ImageRef) -> Result<Self> {
let ref_update = self
.registry
.publish_existing_manifest_ref(&image_name, &self.manifest_digest)?;
reject_conflicting_ref(&image_name, ref_update)?;
Ok(Self::from_parts(
self.registry,
image_name,
self.manifest_digest.clone(),
))
}
}
#[derive(Debug, Clone)]
pub struct LocalManifest(Box<ImageManifest>);
impl LocalManifest {
fn parse(bytes: &[u8]) -> Result<Self> {
let manifest: ImageManifest =
serde_json::from_slice(bytes).context("Failed to parse OCI image manifest")?;
ensure_ommx_image_manifest(&manifest)?;
Ok(Self(Box::new(manifest)))
}
pub fn media_type(&self) -> &'static str {
media_types::OCI_IMAGE_MANIFEST_MEDIA_TYPE
}
pub fn artifact_type(&self) -> &MediaType {
self.0
.artifact_type()
.as_ref()
.expect("ensure_ommx_image_manifest guarantees Image Manifest carries an artifactType")
}
pub fn config(&self) -> Descriptor {
self.0.config().clone()
}
pub fn layers(&self) -> Vec<Descriptor> {
self.0.layers().to_vec()
}
pub fn into_inner(self) -> ImageManifest {
*self.0
}
pub(crate) fn as_image_manifest(&self) -> &ImageManifest {
&self.0
}
pub fn annotations(&self) -> HashMap<String, String> {
self.0.annotations().clone().unwrap_or_default()
}
pub fn subject(&self) -> Option<Descriptor> {
self.0.subject().clone()
}
}
#[derive(Debug, Clone)]
pub struct ArtifactDraft<'reg> {
registry: &'reg LocalRegistry,
image_name: ImageRef,
artifact_type: MediaType,
layers: Vec<StoredDescriptor<'reg>>,
subject: Option<Descriptor>,
annotations: HashMap<String, String>,
}
impl ArtifactDraft<'static> {
pub fn new(image_name: ImageRef) -> Result<Self> {
let registry = LocalRegistry::shared_default()?;
Ok(Self::with_registry(registry, image_name))
}
pub fn new_anonymous() -> Result<Self> {
let registry = LocalRegistry::shared_default()?;
Self::new_anonymous_in_registry(registry)
}
pub fn temp() -> Result<Self> {
let id = uuid::Uuid::new_v4();
let image_name = ImageRef::parse(&format!("ttl.sh/{id}:1h"))?;
Self::new(image_name)
}
pub fn for_github(org: &str, repo: &str, name: &str, tag: &str) -> Result<Self> {
let image_name = ghcr(org, repo, name, tag)?;
let source = Url::parse(&format!("https://github.com/{org}/{repo}"))?;
let mut draft = Self::new(image_name)?;
draft.add_source(&source);
Ok(draft)
}
}
impl<'reg> ArtifactDraft<'reg> {
pub fn with_temp_local_registry<T>(
image_name: ImageRef,
f: impl FnOnce(ArtifactDraft<'_>) -> Result<T>,
) -> Result<T> {
let temp = TempLocalRegistry::new()?;
let draft = ArtifactDraft::with_registry(temp.registry(), image_name);
f(draft)
}
pub fn with_registry(registry: &'reg LocalRegistry, image_name: ImageRef) -> Self {
Self {
registry,
image_name,
artifact_type: MediaType::Other(media_types::V1_ARTIFACT_MEDIA_TYPE.to_string()),
layers: Vec::new(),
subject: None,
annotations: HashMap::new(),
}
}
pub fn new_anonymous_in_registry(registry: &'reg LocalRegistry) -> Result<Self> {
let registry_id = registry.registry_id()?;
let image_name = anonymous_artifact_image_name(®istry_id)?;
Ok(Self::with_registry(registry, image_name))
}
pub fn add_layer_bytes(
&mut self,
media_type: MediaType,
bytes: Vec<u8>,
annotations: HashMap<String, String>,
) -> Result<StoredDescriptor<'reg>> {
let descriptor = descriptor_from_bytes(media_type, &bytes, annotations)?;
let stored_descriptor = self.registry.store_blob(descriptor, &bytes)?;
self.layers.push(stored_descriptor.clone());
Ok(stored_descriptor)
}
pub fn add_instance(&mut self, instance: crate::Instance) -> Result<StoredDescriptor<'reg>> {
let stored_descriptor = self.registry.store_instance_layer(&instance)?;
self.layers.push(stored_descriptor.clone());
Ok(stored_descriptor)
}
pub fn add_solution(&mut self, solution: crate::Solution) -> Result<StoredDescriptor<'reg>> {
let stored_descriptor = self.registry.store_solution_layer(&solution)?;
self.layers.push(stored_descriptor.clone());
Ok(stored_descriptor)
}
pub fn add_parametric_instance(
&mut self,
instance: crate::ParametricInstance,
) -> Result<StoredDescriptor<'reg>> {
let stored_descriptor = self.registry.store_parametric_instance_layer(&instance)?;
self.layers.push(stored_descriptor.clone());
Ok(stored_descriptor)
}
pub fn add_sample_set(
&mut self,
sample_set: crate::SampleSet,
) -> Result<StoredDescriptor<'reg>> {
let stored_descriptor = self.registry.store_sample_set_layer(&sample_set)?;
self.layers.push(stored_descriptor.clone());
Ok(stored_descriptor)
}
pub fn set_subject(&mut self, subject: Descriptor) -> &mut Self {
self.subject = Some(subject);
self
}
pub fn add_annotation(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.annotations.insert(key.into(), value.into());
}
pub fn add_source(&mut self, url: &Url) {
self.add_annotation("org.opencontainers.image.source", url.to_string());
}
pub fn commit(self) -> Result<LocalArtifact<'reg>> {
self.commit_inner(false)
}
pub fn commit_replace(self) -> Result<LocalArtifact<'reg>> {
self.commit_inner(true)
}
fn commit_inner(self, replace_ref: bool) -> Result<LocalArtifact<'reg>> {
let registry = self.registry;
let image_name = self.image_name.clone();
let artifact = self.into_unsealed_artifact()?;
let sealed_artifact = registry.seal_artifact(artifact)?;
let ref_update = if replace_ref {
registry.replace_manifest_ref(&image_name, &sealed_artifact)?
} else {
registry.publish_manifest_ref(&image_name, &sealed_artifact)?
};
reject_conflicting_ref(&image_name, ref_update)?;
Ok(LocalArtifact::from_parts(
registry,
image_name,
sealed_artifact.digest().clone(),
))
}
fn into_unsealed_artifact(self) -> Result<UnsealedArtifact<'reg>> {
let empty_config_bytes = OCI_EMPTY_CONFIG_BYTES.to_vec();
let config_descriptor = DescriptorBuilder::default()
.media_type(MediaType::EmptyJSON)
.digest(
Digest::from_str(&sha256_digest(&empty_config_bytes))
.context("Failed to parse empty config digest")?,
)
.size(empty_config_bytes.len() as u64)
.build()
.context("Failed to build empty config descriptor")?;
let config_descriptor = self
.registry
.store_blob(config_descriptor, &empty_config_bytes)?;
Ok(UnsealedArtifact::new(
self.artifact_type,
config_descriptor,
self.layers,
self.subject,
self.annotations,
))
}
}
pub(crate) fn descriptor_from_bytes(
media_type: MediaType,
bytes: &[u8],
annotations: HashMap<String, String>,
) -> Result<Descriptor> {
let digest = Digest::from_str(&sha256_digest(bytes)).context("Failed to parse blob digest")?;
let descriptor = DescriptorBuilder::default()
.media_type(media_type)
.digest(digest)
.size(bytes.len() as u64)
.annotations(annotations)
.build()
.context("Failed to build OCI descriptor")?;
Ok(descriptor)
}
pub(crate) fn stable_json_bytes(value: &impl Serialize) -> Result<Vec<u8>> {
let mut value = serde_json::to_value(value).context("Failed to encode JSON value")?;
value.sort_all_objects();
serde_json::to_vec(&value).context("Failed to encode stable JSON bytes")
}
fn reject_conflicting_ref(image_name: &ImageRef, ref_update: RefUpdate) -> Result<()> {
if let RefUpdate::Conflicted {
existing_manifest_digest,
incoming_manifest_digest,
} = ref_update
{
bail!(
"Local registry ref {image_name} already points to {existing_manifest_digest}; \
incoming manifest {incoming_manifest_digest} was not published"
);
}
Ok(())
}
fn ensure_ommx_image_manifest(manifest: &ImageManifest) -> Result<()> {
let artifact_type = manifest
.artifact_type()
.as_ref()
.context("OCI image manifest is not an OMMX artifact: artifactType is missing")?;
anyhow::ensure!(
media_types::is_ommx_artifact_type(artifact_type),
"OCI image manifest is not an OMMX artifact: {artifact_type}",
);
Ok(())
}
pub(crate) const ANONYMOUS_ARTIFACT_REF_NAME_SUFFIX: &str = ".ommx.local/anonymous";
pub(crate) const ANONYMOUS_EXPERIMENT_REF_NAME_SUFFIX: &str = ".ommx.local/experiment";
const ANONYMOUS_REGISTRY_ID_HOST_LEN: usize = 8;
const ANONYMOUS_TAG_NONCE_HEX_LEN: usize = 12;
pub(crate) fn anonymous_artifact_image_name(registry_id: &str) -> Result<ImageRef> {
anonymous_local_image_name(registry_id, "anonymous")
.with_context(|| "Failed to synthesise anonymous artifact image name")
}
pub(crate) fn anonymous_local_repository_key(
registry_id: &str,
repository: &str,
) -> Result<String> {
anyhow::ensure!(
!repository.is_empty()
&& repository
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_'),
"Anonymous local repository name must be lowercase OCI-safe text; got {repository:?}",
);
let prefix: String = registry_id
.chars()
.take(ANONYMOUS_REGISTRY_ID_HOST_LEN)
.collect();
anyhow::ensure!(
prefix.len() == ANONYMOUS_REGISTRY_ID_HOST_LEN
&& prefix
.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)),
"Anonymous artifact registry id must be at least {} lowercase hex chars; got {prefix:?}",
ANONYMOUS_REGISTRY_ID_HOST_LEN,
);
Ok(format!("{prefix}.ommx.local/{repository}"))
}
pub(crate) fn anonymous_local_image_name(registry_id: &str, repository: &str) -> Result<ImageRef> {
let repository_key = anonymous_local_repository_key(registry_id, repository)?;
let stamp = chrono::Local::now().format("%Y%m%dT%H%M%S");
let nonce: String = uuid::Uuid::new_v4()
.simple()
.to_string()
.chars()
.take(ANONYMOUS_TAG_NONCE_HEX_LEN)
.collect();
ImageRef::parse(&format!("{repository_key}:{stamp}-{nonce}")).with_context(|| {
format!("Invalid anonymous local image name for registry {repository_key}")
})
}
pub fn is_anonymous_artifact_ref_name(name: &str) -> bool {
is_anonymous_local_ref_name(name, ANONYMOUS_ARTIFACT_REF_NAME_SUFFIX)
}
pub(crate) fn is_anonymous_experiment_ref_name(name: &str) -> bool {
is_anonymous_local_ref_name(name, ANONYMOUS_EXPERIMENT_REF_NAME_SUFFIX)
}
fn is_anonymous_local_ref_name(name: &str, suffix: &str) -> bool {
let Some(host_segment) = name.strip_suffix(suffix) else {
return false;
};
host_segment.len() == ANONYMOUS_REGISTRY_ID_HOST_LEN
&& host_segment
.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
}
pub fn is_anonymous_artifact_tag(tag: &str) -> bool {
let expected_len = 15 + 1 + ANONYMOUS_TAG_NONCE_HEX_LEN;
if tag.len() != expected_len {
return false;
}
let bytes = tag.as_bytes();
for (i, b) in bytes[..15].iter().enumerate() {
let ok = if i == 8 {
*b == b'T'
} else {
b.is_ascii_digit()
};
if !ok {
return false;
}
}
if bytes[15] != b'-' {
return false;
}
bytes[16..]
.iter()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(b))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{v1, Parse};
fn test_image_name(tag: &str) -> Result<ImageRef> {
ImageRef::parse(&format!("ghcr.io/jij-inc/ommx/demo:{tag}"))
}
fn instance_with_one_hot_constraint() -> crate::Instance {
let variable_1 = crate::VariableID::from(1);
let variable_2 = crate::VariableID::from(2);
let one_hot_id = crate::OneHotConstraintID::from(10);
crate::Instance::builder()
.sense(crate::Sense::Minimize)
.objective(crate::Function::Zero)
.decision_variables(std::collections::BTreeMap::from([
(variable_1, crate::DecisionVariable::binary()),
(variable_2, crate::DecisionVariable::binary()),
]))
.constraints(std::collections::BTreeMap::new())
.one_hot_constraints(std::collections::BTreeMap::from([(
one_hot_id,
crate::OneHotConstraint::new(std::collections::BTreeSet::from([
variable_1, variable_2,
]))
.unwrap(),
)]))
.build()
.unwrap()
}
#[test]
fn anonymous_image_name_parses() {
let fake_registry_id = "deadbeef0123456789abcdef01234567";
let name = anonymous_artifact_image_name(fake_registry_id).expect("synthetic ref parses");
let s = name.to_string();
let (before_colon, tag) = s.rsplit_once(':').expect("ref must include `:tag`");
assert!(
before_colon.ends_with(ANONYMOUS_ARTIFACT_REF_NAME_SUFFIX),
"ref `{before_colon}` must end with `{ANONYMOUS_ARTIFACT_REF_NAME_SUFFIX}`",
);
assert!(
before_colon.starts_with("deadbeef."),
"ref `{before_colon}` must start with the truncated registry-id prefix",
);
let expected_tag_len = 15 + 1 + ANONYMOUS_TAG_NONCE_HEX_LEN;
assert_eq!(
tag.len(),
expected_tag_len,
"tag `{tag}` must be {expected_tag_len} chars",
);
assert!(
tag.chars().nth(8) == Some('T'),
"tag `{tag}` must have `T` at position 8",
);
assert!(
tag.chars().nth(15) == Some('-'),
"tag `{tag}` must have `-` separator before the nonce",
);
}
#[test]
fn anonymous_image_name_nonce_differentiates_same_second_builds() {
let id = "0123456789abcdef0123456789abcdef";
let n1 = anonymous_artifact_image_name(id).unwrap().to_string();
let n2 = anonymous_artifact_image_name(id).unwrap().to_string();
assert_ne!(n1, n2, "back-to-back anonymous names must differ: {n1}");
}
#[test]
fn anonymous_ref_filter_rejects_false_positives() {
let synth = anonymous_artifact_image_name("0123456789abcdef0123456789abcdef")
.unwrap()
.to_string();
let (name, tag) = synth.rsplit_once(':').unwrap();
assert!(is_anonymous_artifact_ref_name(name));
assert!(is_anonymous_artifact_tag(tag));
assert!(!is_anonymous_artifact_ref_name(
"myhost.ommx.local/anonymous"
));
assert!(!is_anonymous_artifact_ref_name(
"ABCDEFGH.ommx.local/anonymous"
));
assert!(!is_anonymous_artifact_tag("v1"));
assert!(!is_anonymous_artifact_tag("20260512-15304500a3f17b9c"));
assert!(!is_anonymous_artifact_tag("20260512T153045"));
assert!(!is_anonymous_artifact_tag("20260512T153045-XXXXXXXX"));
assert!(!is_anonymous_artifact_tag("20260512T153045_a3f17b9c"));
}
#[test]
fn anonymous_image_name_truncates_registry_id() {
let full = "0123456789abcdef0123456789abcdef";
let name = anonymous_artifact_image_name(full).unwrap().to_string();
let host = name
.split('/')
.next()
.expect("synthetic ref has a host segment");
assert_eq!(host, format!("{}.ommx.local", &full[..8]));
}
#[test]
fn anonymous_build_in_same_second_does_not_fail() -> Result<()> {
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
for tag in ["a", "b"] {
let mut builder = ArtifactDraft::new_anonymous_in_registry(®istry)?;
builder.add_layer_bytes(
MediaType::Other(media_types::V1_INSTANCE_MEDIA_TYPE.to_string()),
format!("anon-{tag}").into_bytes(),
HashMap::new(),
)?;
builder.commit()?;
}
Ok(())
}
#[test]
fn add_solution_stores_solution_blob() -> Result<()> {
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
let mut builder = ArtifactDraft::with_registry(®istry, test_image_name("solution")?);
#[allow(deprecated)]
let solution_proto = v1::Solution {
state: Some(v1::State {
entries: HashMap::from([(1, 1.0)]),
}),
objective: 1.0,
decision_variables: Vec::new(),
evaluated_constraints: Vec::new(),
evaluated_named_functions: Vec::new(),
feasible: true,
feasible_relaxed: Some(true),
feasible_unrelaxed: true,
optimality: v1::Optimality::Optimal as i32,
relaxation: v1::Relaxation::Unspecified as i32,
sense: v1::instance::Sense::Minimize as i32,
format_version: crate::CURRENT_FORMAT_VERSION,
metadata: None,
annotations: HashMap::new(),
};
let solution = solution_proto.parse(&())?;
let descriptor = builder.add_solution(solution.clone())?;
assert_eq!(descriptor.media_type(), &media_types::v2_solution());
let blob = registry.get_blob(&descriptor)?;
let decoded = crate::Solution::from_v2_bytes(&blob)?;
assert_eq!(decoded, solution);
Ok(())
}
#[test]
fn add_instance_projects_annotations_to_descriptor() -> Result<()> {
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
let mut builder = ArtifactDraft::with_registry(®istry, test_image_name("instance")?);
let mut instance = crate::Instance::default();
instance.description = Some(crate::v1::instance::Description {
name: Some("domain title".to_string()),
..Default::default()
});
instance.annotations.insert(
"org.example.annotation".to_string(),
"domain value".to_string(),
);
let descriptor = builder.add_instance(instance)?;
let annotations = descriptor
.annotations()
.as_ref()
.context("descriptor annotations should be present")?;
assert_eq!(
annotations.get(crate::annotation_keys::INSTANCE_TITLE),
Some(&"domain title".to_string())
);
assert_eq!(
annotations.get("org.example.annotation"),
Some(&"domain value".to_string())
);
let bytes = registry.get_blob(&descriptor)?;
let decoded = crate::Instance::from_v2_bytes(&bytes)?;
assert_eq!(
decoded.description.and_then(|description| description.name),
Some("domain title".to_string())
);
assert_eq!(
decoded.annotations.get("org.example.annotation"),
Some(&"domain value".to_string())
);
Ok(())
}
#[test]
fn add_instance_stores_v2_special_constraint_blob() -> Result<()> {
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
let mut builder =
ArtifactDraft::with_registry(®istry, test_image_name("special-instance")?);
let instance = instance_with_one_hot_constraint();
let descriptor = builder.add_instance(instance.clone())?;
assert_eq!(descriptor.media_type(), &media_types::v2_instance());
let restored = registry.get_instance_layer(&descriptor)?;
assert_eq!(restored, instance);
Ok(())
}
#[test]
fn get_instance_layer_merges_descriptor_annotations_without_overwriting_proto() -> Result<()> {
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
let mut instance = crate::Instance::default();
instance.description = Some(crate::v1::instance::Description {
name: Some("proto title".to_string()),
..Default::default()
});
instance
.annotations
.insert("org.example.owner".to_string(), "proto".to_string());
let descriptor = registry.store_layer_blob(
media_types::v1_instance(),
&instance.to_v1_bytes()?,
HashMap::from([
(
crate::annotation_keys::INSTANCE_TITLE.to_string(),
"descriptor title".to_string(),
),
(
crate::annotation_keys::INSTANCE_LICENSE.to_string(),
"MIT".to_string(),
),
("org.example.owner".to_string(), "descriptor".to_string()),
("org.example.source".to_string(), "descriptor".to_string()),
]),
)?;
let restored = registry.get_instance_layer(&descriptor)?;
let description = restored
.description
.as_ref()
.context("descriptor fallback should create description")?;
assert_eq!(description.name.as_deref(), Some("proto title"));
assert_eq!(description.license.as_deref(), Some("MIT"));
assert_eq!(
restored
.annotations
.get("org.example.owner")
.map(String::as_str),
Some("proto")
);
assert_eq!(
restored
.annotations
.get("org.example.source")
.map(String::as_str),
Some("descriptor")
);
Ok(())
}
#[test]
#[allow(deprecated)]
fn get_solution_layer_merges_descriptor_annotations_without_overwriting_proto() -> Result<()> {
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
let solution_proto = v1::Solution {
state: Some(v1::State::default()),
objective: 1.0,
decision_variables: Vec::new(),
evaluated_constraints: Vec::new(),
evaluated_named_functions: Vec::new(),
feasible: true,
feasible_relaxed: Some(true),
feasible_unrelaxed: true,
optimality: v1::Optimality::Optimal as i32,
relaxation: v1::Relaxation::Unspecified as i32,
sense: v1::instance::Sense::Minimize as i32,
format_version: crate::CURRENT_FORMAT_VERSION,
metadata: Some(v1::ProcessMetadata {
solver: Some(r#"{"name":"proto"}"#.to_string()),
..Default::default()
}),
annotations: HashMap::from([("org.example.owner".to_string(), "proto".to_string())]),
};
let solution = solution_proto.parse(&())?;
let descriptor = registry.store_layer_blob(
media_types::v1_solution(),
&solution.to_v1_bytes(),
HashMap::from([
(
format!("{}.solver", crate::annotation_keys::SOLUTION_NAMESPACE),
r#"{"name":"descriptor"}"#.to_string(),
),
(
format!("{}.instance", crate::annotation_keys::SOLUTION_NAMESPACE),
"sha256:descriptor".to_string(),
),
("org.example.owner".to_string(), "descriptor".to_string()),
("org.example.source".to_string(), "descriptor".to_string()),
]),
)?;
let restored = registry.get_solution_layer(&descriptor)?;
let metadata = restored
.metadata
.as_ref()
.context("descriptor fallback should create process metadata")?;
assert_eq!(metadata.solver.as_deref(), Some(r#"{"name":"proto"}"#));
assert_eq!(metadata.instance.as_deref(), Some("sha256:descriptor"));
assert_eq!(
restored
.annotations
.get("org.example.owner")
.map(String::as_str),
Some("proto")
);
assert_eq!(
restored
.annotations
.get("org.example.source")
.map(String::as_str),
Some("descriptor")
);
Ok(())
}
#[test]
fn builds_native_oci_image_manifest_with_artifact_type() -> Result<()> {
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
let mut builder = ArtifactDraft::with_registry(®istry, test_image_name("v1")?);
let layer = builder.add_layer_bytes(
MediaType::Other(media_types::V1_INSTANCE_MEDIA_TYPE.to_string()),
b"instance".to_vec(),
HashMap::from([(
crate::annotation_keys::INSTANCE_TITLE.to_string(),
"demo".to_string(),
)]),
)?;
builder.add_annotation("org.opencontainers.image.ref.name", "example.com/demo:v1");
let manifest = builder
.into_unsealed_artifact()?
.into_oci_image_manifest()?;
assert_eq!(manifest.media_type().as_ref(), None);
assert_eq!(
manifest.artifact_type().as_ref(),
Some(&MediaType::Other(
media_types::V1_ARTIFACT_MEDIA_TYPE.to_string()
))
);
assert_eq!(manifest.layers(), &[Descriptor::from(layer)]);
let config = manifest.config();
assert_eq!(config.media_type(), &MediaType::EmptyJSON);
assert_eq!(
config.size(),
media_types::OCI_EMPTY_CONFIG_BYTES.len() as u64
);
assert_eq!(
config.digest().to_string(),
media_types::OCI_EMPTY_CONFIG_DIGEST
);
let manifest_bytes = stable_json_bytes(&manifest)?;
let manifest_descriptor =
descriptor_from_bytes(MediaType::ImageManifest, &manifest_bytes, HashMap::new())?;
assert_eq!(manifest_descriptor.media_type(), &MediaType::ImageManifest);
assert_eq!(
manifest_descriptor.digest().to_string(),
sha256_digest(&manifest_bytes)
);
let parsed: ImageManifest = serde_json::from_slice(&manifest_bytes)?;
assert_eq!(parsed, manifest);
Ok(())
}
#[test]
fn stable_manifest_json_is_independent_of_annotation_insertion_order() -> Result<()> {
let first = unsealed_artifact_with_annotations("order-a", [("b", "2"), ("a", "1")])?;
let second = unsealed_artifact_with_annotations("order-b", [("a", "1"), ("b", "2")])?;
let first_bytes = stable_json_bytes(&first)?;
let second_bytes = stable_json_bytes(&second)?;
let first_descriptor =
descriptor_from_bytes(MediaType::ImageManifest, &first_bytes, HashMap::new())?;
let second_descriptor =
descriptor_from_bytes(MediaType::ImageManifest, &second_bytes, HashMap::new())?;
assert_eq!(first_bytes, second_bytes);
assert_eq!(first_descriptor.digest(), second_descriptor.digest());
Ok(())
}
#[test]
fn builds_manifest_with_subject() -> Result<()> {
let subject =
descriptor_from_bytes(MediaType::ImageManifest, b"parent manifest", HashMap::new())?;
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
let mut builder = ArtifactDraft::with_registry(®istry, test_image_name("subject")?);
builder.add_layer_bytes(
MediaType::Other(media_types::V1_INSTANCE_MEDIA_TYPE.to_string()),
b"instance".to_vec(),
HashMap::new(),
)?;
builder.set_subject(subject.clone());
let manifest = builder
.into_unsealed_artifact()?
.into_oci_image_manifest()?;
assert_eq!(manifest.subject(), &Some(subject));
Ok(())
}
#[test]
fn rejects_invalid_descriptor_digest_through_oci_spec() {
assert!(Digest::from_str("sha256:../bad").is_err());
}
fn unsealed_artifact_with_annotations(
tag: &str,
annotations: impl IntoIterator<Item = (&'static str, &'static str)>,
) -> Result<ImageManifest> {
let dir = tempfile::tempdir()?;
let registry = LocalRegistry::open(dir.path())?;
let mut builder = ArtifactDraft::with_registry(®istry, test_image_name(tag)?);
builder.add_layer_bytes(
MediaType::Other(media_types::V1_INSTANCE_MEDIA_TYPE.to_string()),
b"instance".to_vec(),
HashMap::new(),
)?;
for (key, value) in annotations {
builder.add_annotation(key, value);
}
builder.into_unsealed_artifact()?.into_oci_image_manifest()
}
}