Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/os/thar-be-registries-toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ std = { version = "v1", helpers = ["join_array"]}
[[mirrors]]
registry = "{{registry}}"
endpoint = [{{{join_array ", " endpoint}}}]
{{#if capabilities}}
capabilities = [{{{join_array ", " capabilities}}}]
{{/if}}
{{/each}}
{{/if}}
{{#if settings.container-registry.credentials}}
Expand Down
106 changes: 105 additions & 1 deletion sources/api/thar-be-registries/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ fn write_hosts_toml(base_dir: &Path, mirror: &Mirror) -> Result<()> {
..Default::default()
};

let caps = resolve_capabilities(mirror.capabilities.as_deref());
for endpoint in &mirror.endpoint {
let ep = Endpoint::new(endpoint);
let mut cfg = HostConfig::new([Capability::Pull, Capability::Resolve]);
let mut cfg = HostConfig::new(caps.iter().copied());
if ep.has_path_component() {
cfg = cfg.with_override_path(true);
}
Expand All @@ -163,6 +164,27 @@ fn write_hosts_toml(base_dir: &Path, mirror: &Mirror) -> Result<()> {
Ok(())
}

/// Map an optional list of capability strings to Capability values.
/// Returns the default ["pull", "resolve"] when capabilities is None.
/// Unknown strings are skipped with a warning.
fn resolve_capabilities(caps: Option<&[String]>) -> Vec<Capability> {
match caps {
None => vec![Capability::Pull, Capability::Resolve],
Some(strings) => strings
.iter()
.filter_map(|s| match s.as_str() {
"pull" => Some(Capability::Pull),
"resolve" => Some(Capability::Resolve),
"push" => Some(Capability::Push),
other => {
warn!("Unknown capability '{}', skipping", other);
None
}
})
.collect(),
}
}

/// Write credentials.toml for a registry.
/// For docker.io, writes to registry-1.docker.io since containerd's credential
/// callback receives the actual registry host, not the image reference host.
Expand Down Expand Up @@ -227,62 +249,71 @@ mod tests {
#[test_case(
"docker.io",
&["https://mirror.example.com"],
None,
"docker.io/hosts.toml",
&[r#"server = "https://registry-1.docker.io""#, r#"[host."https://mirror.example.com"]"#]
; "docker.io mirror"
)]
#[test_case(
"registry.example.com:5000",
&["https://mirror.local"],
None,
"registry.example.com_5000_/hosts.toml",
&[r#"server = "https://registry.example.com:5000""#]
; "registry with port"
)]
#[test_case(
"docker.io",
&["https://ecr-cache.example.com/v2/docker-hub"],
None,
"docker.io/hosts.toml",
&["override_path = true"]
; "endpoint with path sets override_path"
)]
#[test_case(
"*",
&["https://mirror.global"],
None,
"_default/hosts.toml",
&[r#"[host."https://mirror.global"]"#]
; "global mirror"
)]
#[test_case(
"registry.example.com:443",
&["https://mirror.local"],
None,
"registry.example.com_443_/hosts.toml",
&[r#"server = "https://registry.example.com:443""#]
; "port 443 preserves port in directory and server"
)]
#[test_case(
"registry.example.com:80",
&["http://mirror.local"],
None,
"registry.example.com_80_/hosts.toml",
&[r#"server = "http://registry.example.com:80""#]
; "port 80 infers http scheme"
)]
#[test_case(
"http://registry.local:5000",
&["http://mirror.local:5000"],
None,
"registry.local_5000_/hosts.toml",
&[r#"server = "http://registry.local:5000""#]
; "explicit http scheme preserved"
)]
#[test_case(
"docker.io",
&["https://10.0.0.1:443/path/to/resource"],
None,
"docker.io/hosts.toml",
&[r#"[host."https://10.0.0.1:443/path/to/resource"]"#, "override_path = true"]
; "https ip port 443 with path sets override_path"
)]
#[test_case(
"registry.example.com",
&["https://172.16.0.5:8443/path/to/resource"],
None,
"registry.example.com/hosts.toml",
&[r#"[host."https://172.16.0.5:8443/path/to/resource"]"#, "override_path = true"]
; "https ip custom port with path sets override_path"
Expand All @@ -291,20 +322,23 @@ mod tests {
#[test_case(
"public.ecr.aws",
&["196.18.8.18:443/v2/eks-a-test"],
None,
"public.ecr.aws/hosts.toml",
&[r#"[host."196.18.8.18:443/v2/eks-a-test"]"#, "override_path = true"]
; "schemeless ip port 443 with path against public.ecr.aws sets override_path"
)]
#[test_case(
"registry.example.com",
&["192.168.1.1:5000/path/to/resource"],
None,
"registry.example.com/hosts.toml",
&[r#"[host."192.168.1.1:5000/path/to/resource"]"#, "override_path = true"]
; "schemeless ip custom port with path sets override_path"
)]
#[test_case(
"registry.example.com",
&["mirror.local:5000/path/to/resource"],
None,
"registry.example.com/hosts.toml",
&[r#"[host."mirror.local:5000/path/to/resource"]"#, "override_path = true"]
; "schemeless hostname custom port with path sets override_path"
Expand All @@ -313,6 +347,7 @@ mod tests {
#[test_case(
"registry.example.com",
&["http://mirror.local:5000/path/to/resource"],
None,
"registry.example.com/hosts.toml",
&[r#"[host."http://mirror.local:5000/path/to/resource"]"#, "override_path = true"]
; "http hostname custom port with path sets override_path"
Expand All @@ -321,20 +356,47 @@ mod tests {
#[test_case(
"docker.io",
&["https://mirror.example.com/path/to/resource"],
None,
"docker.io/hosts.toml",
&[r#"[host."https://mirror.example.com/path/to/resource"]"#, "override_path = true"]
; "https hostname no port with path sets override_path"
)]
#[test_case(
"docker.io",
&["https://mirror.example.com"],
Some(&["pull"]),
"docker.io/hosts.toml",
&[r#"capabilities = ["pull"]"#]
; "explicit pull-only capability"
)]
#[test_case(
"docker.io",
&["https://mirror.example.com"],
None,
"docker.io/hosts.toml",
&["pull", "resolve"]
; "default capabilities when none specified"
)]
#[test_case(
"docker.io",
&["https://mirror.example.com"],
Some(&["pull", "resolve", "push"]),
"docker.io/hosts.toml",
&["pull", "resolve", "push"]
; "all three capabilities"
)]
fn test_write_hosts_toml(
registry: &str,
endpoints: &[&str],
capabilities: Option<&[&str]>,
expected_path: &str,
expected_contents: &[&str],
) {
let dir = tempfile::tempdir().unwrap();
let mirror = Mirror {
registry: registry.to_string(),
endpoint: endpoints.iter().map(|s| s.to_string()).collect(),
capabilities: capabilities.map(|cs| cs.iter().map(|s| s.to_string()).collect()),
};
write_hosts_toml(dir.path(), &mirror).unwrap();
verify_file(dir.path(), expected_path, expected_contents);
Expand Down Expand Up @@ -409,6 +471,7 @@ mod tests {
let mirror = Mirror {
registry: "registry.example.com".to_string(),
endpoint: vec!["https://mirror.example.com".to_string()],
capabilities: None,
};
let cred = Credential {
registry: "registry.example.com".to_string(),
Expand Down Expand Up @@ -453,6 +516,7 @@ mod tests {
let mirror = Mirror {
registry: "*".to_string(),
endpoint: vec!["https://mirror.example.com".to_string()],
capabilities: None,
};
write_hosts_toml(dir.path(), &mirror).unwrap();
let content = fs::read_to_string(dir.path().join("_default/hosts.toml")).unwrap();
Expand All @@ -465,6 +529,7 @@ mod tests {
let mirror = Mirror {
registry: "docker.io".to_string(),
endpoint: vec!["https://mirror.example.com".to_string()],
capabilities: None,
};
write_hosts_toml(dir.path(), &mirror).unwrap();
let content = fs::read_to_string(dir.path().join("docker.io/hosts.toml")).unwrap();
Expand All @@ -481,6 +546,7 @@ mod tests {
"https://mirror2.example.com".to_string(),
"https://mirror3.example.com".to_string(),
],
capabilities: None,
};
write_hosts_toml(dir.path(), &mirror).unwrap();
let content = fs::read_to_string(dir.path().join("test.io/hosts.toml")).unwrap();
Expand All @@ -496,13 +562,51 @@ mod tests {
let mirror = Mirror {
registry: "test.io".to_string(),
endpoint: vec![],
capabilities: None,
};
write_hosts_toml(dir.path(), &mirror).unwrap();
let content = fs::read_to_string(dir.path().join("test.io/hosts.toml")).unwrap();
assert!(content.contains(r#"server = "https://test.io""#));
assert!(!content.contains("[host."));
}

#[test]
fn test_resolve_capabilities_none_returns_default() {
let caps = resolve_capabilities(None);
assert_eq!(caps, vec![Capability::Pull, Capability::Resolve]);
}

#[test]
fn test_resolve_capabilities_pull_only() {
let caps = resolve_capabilities(Some(&["pull".to_string()]));
assert_eq!(caps, vec![Capability::Pull]);
}

#[test]
fn test_resolve_capabilities_all_three() {
let caps = resolve_capabilities(Some(&[
"pull".to_string(),
"resolve".to_string(),
"push".to_string(),
]));
assert_eq!(
caps,
vec![Capability::Pull, Capability::Resolve, Capability::Push]
);
}

#[test]
fn test_resolve_capabilities_unknown_skipped() {
let caps = resolve_capabilities(Some(&["pull".to_string(), "fly".to_string()]));
assert_eq!(caps, vec![Capability::Pull]);
}

#[test]
fn test_resolve_capabilities_empty_returns_empty() {
let caps = resolve_capabilities(Some(&[]));
assert!(caps.is_empty());
}

#[test]
fn test_rename_exchange_dir() {
let dir = tempfile::tempdir().unwrap();
Expand Down
30 changes: 30 additions & 0 deletions sources/api/thar-be-registries/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(crate) struct Settings {
pub(crate) struct Mirror {
pub(crate) registry: String,
pub(crate) endpoint: Vec<String>,
pub(crate) capabilities: Option<Vec<String>>,
}

/// Registry authentication credentials
Expand Down Expand Up @@ -50,6 +51,10 @@ mod tests {
registry = "r.io"
username = "u"
password = "p""#, false, true; "credentials only")]
#[test_case(r#"[[mirrors]]
registry = "docker.io"
endpoint = ["https://mirror.example.com"]
capabilities = ["pull"]"#, true, false; "mirrors with capabilities")]
fn test_settings_schema(toml_str: &str, has_mirrors: bool, has_creds: bool) {
let settings: Settings = toml::from_str(toml_str).unwrap();
assert_eq!(settings.mirrors.is_some(), has_mirrors);
Expand All @@ -70,4 +75,29 @@ mod tests {
let result = read_settings(path.to_str().unwrap());
assert!(matches!(result, Err(Error::ParseSettings { .. })));
}

#[test]
fn test_mirror_capabilities_deserialized() {
let toml_str = r#"
[[mirrors]]
registry = "docker.io"
endpoint = ["https://mirror.example.com"]
capabilities = ["pull"]
"#;
let settings: Settings = toml::from_str(toml_str).unwrap();
let mirror = settings.mirrors.unwrap().into_iter().next().unwrap();
assert_eq!(mirror.capabilities, Some(vec!["pull".to_string()]));
}

#[test]
fn test_mirror_without_capabilities_is_none() {
let toml_str = r#"
[[mirrors]]
registry = "docker.io"
endpoint = ["https://mirror.example.com"]
"#;
let settings: Settings = toml::from_str(toml_str).unwrap();
let mirror = settings.mirrors.unwrap().into_iter().next().unwrap();
assert!(mirror.capabilities.is_none());
}
}