#![cfg(feature = "remote-artifact")]
use anyhow::Result;
use ommx::artifact::{
local_registry::LocalRegistry, media_types, ArtifactDraft, ImageRef, LocalArtifact,
};
use serial_test::serial;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use testcontainers::{
core::{ContainerPort, Mount, WaitFor},
runners::SyncRunner,
Container, GenericImage, ImageExt,
};
use testcontainers_modules::cncf_distribution::CncfDistribution;
const ALICE_PASSWORD: &str = "secret";
const ALICE_USER: &str = "alice";
fn htpasswd_path() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("htpasswd")
}
fn start_anonymous_registry() -> Container<CncfDistribution> {
CncfDistribution::default()
.start()
.expect("failed to start anonymous test registry")
}
fn start_htpasswd_registry() -> Container<GenericImage> {
let mount = Mount::bind_mount(htpasswd_path().to_string_lossy(), "/auth/htpasswd");
GenericImage::new("registry", "2")
.with_wait_for(WaitFor::message_on_stderr("listening on [::]:5000"))
.with_exposed_port(ContainerPort::Tcp(5000))
.with_env_var("REGISTRY_AUTH", "htpasswd")
.with_env_var("REGISTRY_AUTH_HTPASSWD_REALM", "Registry Realm")
.with_env_var("REGISTRY_AUTH_HTPASSWD_PATH", "/auth/htpasswd")
.with_mount(mount)
.start()
.expect("failed to start htpasswd test registry")
}
fn with_test_artifact<T>(
image_name: ImageRef,
f: impl FnOnce(LocalArtifact<'_>) -> Result<T>,
) -> Result<T> {
ArtifactDraft::with_temp_local_registry(image_name, |mut draft| {
draft.add_layer_bytes(
oci_spec::image::MediaType::Other(media_types::V1_INSTANCE_MEDIA_TYPE.to_string()),
b"auth-e2e-test".to_vec(),
HashMap::from([(
"org.ommx.v1.instance.title".to_string(),
"auth-e2e".to_string(),
)]),
)?;
let artifact = draft.commit()?;
f(artifact)
})
}
fn write_docker_config(host: &str, user: &str, password: &str) -> Result<tempfile::TempDir> {
use base64::{engine::general_purpose, Engine as _};
let auth = general_purpose::STANDARD.encode(format!("{user}:{password}"));
let body = format!(r#"{{ "auths": {{ "{host}": {{ "auth": "{auth}" }} }} }}"#,);
let dir = tempfile::tempdir()?;
std::fs::write(dir.path().join("config.json"), body)?;
unsafe {
std::env::set_var("DOCKER_CONFIG", dir.path());
}
Ok(dir)
}
struct EnvGuard;
impl EnvGuard {
fn new() -> Self {
Self::clear();
Self
}
fn clear() {
for k in [
"OMMX_BASIC_AUTH_DOMAIN",
"OMMX_BASIC_AUTH_USERNAME",
"OMMX_BASIC_AUTH_PASSWORD",
"DOCKER_CONFIG",
] {
unsafe {
std::env::remove_var(k);
}
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
Self::clear();
}
}
fn set_env(key: &str, value: &str) {
unsafe {
std::env::set_var(key, value);
}
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn push_anonymous_against_open_registry() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_anonymous_registry();
let port = registry.get_host_port_ipv4(5000)?;
let image_name = ImageRef::parse(&format!("localhost:{port}/ommx-test/anon:tag1"))?;
with_test_artifact(image_name, |artifact| artifact.push())
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn push_with_docker_config_only() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_htpasswd_registry();
let port = registry.get_host_port_ipv4(5000)?;
let host = format!("localhost:{port}");
let _docker_dir = write_docker_config(&host, ALICE_USER, ALICE_PASSWORD)?;
let image_name = ImageRef::parse(&format!("{host}/ommx-test/docker-only:tag1"))?;
with_test_artifact(image_name, |artifact| artifact.push())
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn push_anonymous_against_htpasswd_registry_fails() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_htpasswd_registry();
let port = registry.get_host_port_ipv4(5000)?;
let image_name = ImageRef::parse(&format!("localhost:{port}/ommx-test/anon-fail:tag1"))?;
with_test_artifact(image_name, |artifact| {
let err = artifact
.push()
.expect_err("anonymous push must be rejected");
let msg = format!("{err:#}");
assert!(
msg.contains("auth") || msg.contains("401") || msg.contains("unauthorized"),
"expected an auth-related error, got: {msg}"
);
Ok(())
})
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn push_with_env_basic_auth() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_htpasswd_registry();
let port = registry.get_host_port_ipv4(5000)?;
let host = format!("localhost:{port}");
set_env("OMMX_BASIC_AUTH_DOMAIN", &host);
set_env("OMMX_BASIC_AUTH_USERNAME", ALICE_USER);
set_env("OMMX_BASIC_AUTH_PASSWORD", ALICE_PASSWORD);
let image_name = ImageRef::parse(&format!("{host}/ommx-test/env-auth:tag1"))?;
with_test_artifact(image_name, |artifact| artifact.push())
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn push_with_wrong_env_password_fails() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_htpasswd_registry();
let port = registry.get_host_port_ipv4(5000)?;
let host = format!("localhost:{port}");
set_env("OMMX_BASIC_AUTH_DOMAIN", &host);
set_env("OMMX_BASIC_AUTH_USERNAME", ALICE_USER);
set_env("OMMX_BASIC_AUTH_PASSWORD", "wrong-password");
let image_name = ImageRef::parse(&format!("{host}/ommx-test/wrong-pw:tag1"))?;
with_test_artifact(image_name, |artifact| {
assert!(artifact.push().is_err());
Ok(())
})
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn env_override_beats_docker_config() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_htpasswd_registry();
let port = registry.get_host_port_ipv4(5000)?;
let host = format!("localhost:{port}");
let _docker_dir = write_docker_config(&host, ALICE_USER, "wrong-password")?;
set_env("OMMX_BASIC_AUTH_DOMAIN", &host);
set_env("OMMX_BASIC_AUTH_USERNAME", ALICE_USER);
set_env("OMMX_BASIC_AUTH_PASSWORD", ALICE_PASSWORD);
let image_name = ImageRef::parse(&format!("{host}/ommx-test/env-wins:tag1"))?;
with_test_artifact(image_name, |artifact| artifact.push())
}
#[cfg(feature = "cli")]
#[test]
#[ignore = "requires docker and the ommx binary"]
#[serial]
fn cli_push_routes_through_native_path() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_anonymous_registry();
let port = registry.get_host_port_ipv4(5000)?;
let image_name = format!("localhost:{port}/ommx-test/cli-dispatch:tag1");
let dir = tempfile::tempdir()?;
{
let local = Arc::new(LocalRegistry::open(dir.path())?);
let mut builder =
ArtifactDraft::with_registry(local.as_ref(), ImageRef::parse(&image_name)?);
builder.add_layer_bytes(
oci_spec::image::MediaType::Other(media_types::V1_INSTANCE_MEDIA_TYPE.to_string()),
b"cli-dispatch".to_vec(),
HashMap::new(),
)?;
builder.commit()?;
}
let bin = env!("CARGO_BIN_EXE_ommx");
let output = std::process::Command::new(bin)
.env("OMMX_LOCAL_REGISTRY_ROOT", dir.path())
.args(["push", &image_name])
.output()?;
assert!(
output.status.success(),
"ommx push failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
Ok(())
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn push_oci_archive_via_load_then_push() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_anonymous_registry();
let port = registry.get_host_port_ipv4(5000)?;
let image_name = ImageRef::parse(&format!("localhost:{port}/ommx-test/archive-push:tag1"))?;
let archive_dir = tempfile::tempdir()?;
let archive_path = archive_dir.path().join("artifact.ommx");
with_test_artifact(image_name.clone(), |sender_local| {
sender_local.save(&archive_path)
})?;
let receiver_dir = tempfile::tempdir()?;
let receiver = Arc::new(LocalRegistry::open(receiver_dir.path())?);
receiver.import_oci_archive(&archive_path)?;
let receiver_local = LocalArtifact::open_in_registry(receiver.as_ref(), image_name)?;
receiver_local.push()
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn pull_image_round_trips_through_anonymous_registry() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_anonymous_registry();
let port = registry.get_host_port_ipv4(5000)?;
let image_name = ImageRef::parse(&format!("localhost:{port}/ommx-test/pull-rt:tag1"))?;
let expected_layer_bytes = with_test_artifact(image_name.clone(), |sender_local| {
let layers = sender_local.layers()?;
assert_eq!(layers.len(), 1);
let expected_layer_bytes = sender_local.get_blob(&layers[0])?;
sender_local.push()?;
Ok(expected_layer_bytes)
})?;
let receiver_dir = tempfile::tempdir()?;
let receiver = Arc::new(LocalRegistry::open(receiver_dir.path())?);
let outcome = receiver.pull_image(&image_name)?;
assert_eq!(&outcome.image_name, &image_name);
let pulled = LocalArtifact::open_in_registry(receiver.as_ref(), image_name)?;
assert_eq!(pulled.manifest_digest(), &outcome.manifest_digest);
let layers = pulled.layers()?;
assert_eq!(layers.len(), 1);
let pulled_bytes = pulled.get_blob(&layers[0])?;
assert_eq!(pulled_bytes, expected_layer_bytes);
Ok(())
}
#[test]
#[ignore = "requires docker"]
#[serial]
fn partial_env_override_bails_before_registry_call() -> Result<()> {
let _env = EnvGuard::new();
let registry = start_htpasswd_registry();
let port = registry.get_host_port_ipv4(5000)?;
let host = format!("localhost:{port}");
set_env("OMMX_BASIC_AUTH_DOMAIN", &host);
set_env("OMMX_BASIC_AUTH_USERNAME", ALICE_USER);
let image_name = ImageRef::parse(&format!("{host}/ommx-test/partial:tag1"))?;
with_test_artifact(image_name, |artifact| {
let err = artifact
.push()
.expect_err("partial OMMX_BASIC_AUTH_* must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("OMMX_BASIC_AUTH_PASSWORD") && msg.contains("unset"),
"error should name the missing var: {msg}"
);
Ok(())
})
}