Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,10 @@ hbench datasets list --registry oci://ghcr.io/<namespace> # what it
```

- `oci://<registry>/<namespace>`: `-d <name>@<version>` is the manifest tagged `<version>` in
`<registry>/<namespace>/<name>`. Pulls are anonymous (with a registry token where the registry asks for one, as
GHCR does) and over HTTPS, except to `localhost`, `127.0.0.1` and `::1`, which use plain HTTP.
`<registry>/<namespace>/<name>`. Pulls use the docker config's credentials for the registry, as `push` does (see
[Publishing datasets](#publishing-datasets)), so a private dataset runs after `docker login`; without any they are
anonymous (with a registry token where the registry asks for one, as GHCR does). They are over HTTPS, except to
`localhost`, `127.0.0.1` and `::1`, which use plain HTTP.
- `oci-layout:<dir>`: `<dir>/<name>` is an OCI image layout, and the dataset is the manifest in its `index.json`
whose `org.opencontainers.image.ref.name` is `<version>`. `oras cp <registry>/<namespace>/<name>:<version>
--to-oci-layout <dir>/<name>:<version>` makes one, for offline use.
Expand Down Expand Up @@ -245,7 +247,7 @@ hbench datasets export -d hello@1.0 --registry oci://ghcr.io/<namespace> --oci-l
targets, `task_checksum` and package digest. Any difference fails the command.
- `export` copies `name@version` from `--registry` (as `run` takes it) into `<dir>/<name>`, tagged with the version and
checked blob by blob, so that `--registry oci-layout:<dir>` runs it offline. It reads with the docker config's
credentials; `run` and `datasets download` pull anonymously.
credentials, as `run` and `datasets download` do.

### Dataset format

Expand Down
21 changes: 17 additions & 4 deletions src/oci/store.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Where a dataset's manifests and blobs come from and go to: an OCI registry, pulled into the content-addressed
//! blob cache (anonymously, unless the store was opened with credentials), or OCI image layouts on disk,
//! blob cache with the docker config's credentials for it (anonymously without any), or OCI image layouts on disk,
//! `<root>/<repository>`. Every manifest and blob is checked against its digest before it is used.

use super::format::{ARTIFACT_TYPE, INDEX_MEDIA_TYPE, MANIFEST_MEDIA_TYPE, MAX_MANIFEST_BYTES, REF_NAME};
Expand Down Expand Up @@ -35,9 +35,20 @@ fn is_loopback(registry: &str) -> bool {

impl Store {
/// The store for a task's or dataset's `registry` (a registry host, or `oci-layout:<dir>`), caching
/// registry blobs under `cache_root/blobs/sha256`.
/// registry blobs under `cache_root/blobs/sha256` and reading with the credentials the docker config has for it.
pub fn open(registry: &str, cache_root: &Path) -> Result<Store> {
Store::open_with_auth(registry, cache_root, RegistryAuth::Anonymous)
Store::open_with_config(registry, cache_root, super::credentials::docker_config_path().as_deref())
}

/// [`Store::open`], with the docker config at `docker_config` (anonymous when there is none). An OCI layout
/// reads no credentials.
pub fn open_with_config(registry: &str, cache_root: &Path, docker_config: Option<&Path>) -> Result<Store> {
let auth = if registry.starts_with(LAYOUT_SCHEME) {
RegistryAuth::Anonymous
} else {
super::credentials::registry_auth(docker_config, registry)?
};
Store::open_with_auth(registry, cache_root, auth)
}

/// [`Store::open`], authenticating to a registry with `auth`.
Expand Down Expand Up @@ -314,7 +325,9 @@ impl Store {
fn fetch_error(reference: &str, auth: &RegistryAuth, e: OciDistributionError) -> anyhow::Error {
match (e, auth) {
(OciDistributionError::UnauthorizedError { .. }, RegistryAuth::Anonymous) => {
anyhow::anyhow!("{reference}: not found, or not public (hbench pulls anonymously)")
anyhow::anyhow!(
"{reference}: not found, or not public (the docker config has no credentials for this registry, so hbench pulled anonymously; log in with `docker login` or `oras login`)"
)
}
(OciDistributionError::UnauthorizedError { .. }, _) => anyhow::anyhow!("{reference}: not found, or the credentials can't read it"),
(e, _) => anyhow::Error::new(e).context(format!("fetching {reference}")),
Expand Down
20 changes: 20 additions & 0 deletions src/oci/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,26 @@ fn registry_specs() {
assert!(Location::parse("oci://ghcr.io/ns").unwrap().repository("Terminal-Bench").is_err());
}

#[test]
fn a_registry_store_reads_the_docker_config_and_a_layout_store_does_not() {
use oci_client::secrets::RegistryAuth;
let root = scratch("pull-auth");
let config = root.join("config.json");
std::fs::write(&config, r#"{"auths": {"registry.example": {"username": "reader", "password": "pw"}}}"#).unwrap();
let auth = |registry: &str| match super::store::Store::open_with_config(registry, &root, Some(&config)).unwrap() {
super::store::Store::Registry { auth, .. } => auth,
super::store::Store::Layout { .. } => panic!("{registry} is a registry"),
};
assert_eq!(auth("registry.example"), RegistryAuth::Basic("reader".into(), "pw".into()));
assert_eq!(auth("other.example"), RegistryAuth::Anonymous);

std::fs::write(&config, "not json").unwrap();
let err = super::store::Store::open_with_config("registry.example", &root, Some(&config)).err().unwrap();
assert!(format!("{err:#}").contains("parsing"), "{err:#}");
let layout = format!("oci-layout:{}", root.display());
assert!(matches!(super::store::Store::open_with_config(&layout, &root, Some(&config)), Ok(super::store::Store::Layout { .. })));
}

#[test]
fn lock_json_records_oci_tasks() {
let oci = OciTask {
Expand Down
9 changes: 8 additions & 1 deletion src/oci/tests/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,15 @@ async fn push_and_export_use_the_docker_config_credentials() {
let registry_spec = format!("oci://{registry}/{namespace}");
let offline = root.join("offline");
let err = format!("{:#}", publish::export("examples@1.0", &registry_spec, &offline, RegistryAuth::Anonymous).await.unwrap_err());
assert!(err.contains("not public (hbench pulls anonymously)"), "{err}");
assert!(err.contains("not public (the docker config has no credentials for this registry"), "{err}");
let exported = publish::export("examples@1.0", &registry_spec, &offline, auth).await.unwrap();
assert_eq!(exported.digest, packed.digest);
assert_reads_back_identical(format!("oci-layout:{}", offline.display()), &root.join("cache")).await;

let repository = format!("{namespace}/examples");
let pulled = Store::open_with_config(&registry, &root.join("pull"), Some(&docker_config)).unwrap();
assert_eq!(pulled.resolve_tag(&repository, "1.0").await.unwrap(), packed.digest, "a pull reads with the credentials");
let anonymous = Store::open_with_config(&registry, &root.join("pull"), None).unwrap();
let err = format!("{:#}", anonymous.resolve_tag(&repository, "1.0").await.unwrap_err());
assert!(err.contains("log in with `docker login`"), "{err}");
}
Loading