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
42 changes: 40 additions & 2 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ use openshell_sandbox_backend::boundary_protocol::{
};
use opentelemetry::trace::TraceContextExt as _;
use sha2::{Digest as _, Sha256};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
Expand Down Expand Up @@ -2968,7 +2969,7 @@ impl DockerComputeDriver {
);
let mut stream = self.docker.create_image(
Some(CreateImageOptions {
from_image: Some(image.to_string()),
from_image: Some(normalize_pull_reference(image).into_owned()),
..Default::default()
}),
None,
Expand Down Expand Up @@ -6298,6 +6299,43 @@ fn container_name_for_sandbox(sandbox: &DriverSandbox) -> String {
format!("{CONTAINER_NAME_PREFIX}{workspace}--{truncated_name}-{id_suffix}")
}

/// Normalize an image reference for a pull request by appending `:latest`
/// when it carries neither an explicit tag nor a digest.
///
/// The Docker daemon's `create_image` endpoint interprets a `fromImage` with
/// no tag as "every tag in the repository" and pulls them all, so a bare
/// `--from` reference such as `nicolaka/netshoot` must be resolved to a single
/// tag the way `docker pull` (and the Podman driver) do.
///
/// Parsing mirrors [`openshell_core::driver_utils::supervisor_image_tag`]: only
/// the final path component may carry a tag, so a registry port such as the
/// `:5000` in `registry:5000/team/app` is not mistaken for one, and a
/// digest-pinned reference (`...@sha256:...`) already names an exact image and
/// is left untouched. A separate helper is needed because `supervisor_image_tag`
/// resolves a bare reference to an implied `latest` and so cannot distinguish a
/// reference that still needs a tag appended.
///
/// Examples:
/// - `"nicolaka/netshoot"` → `"nicolaka/netshoot:latest"`
/// - `"foo:1.2"` → `"foo:1.2"` (already tagged)
/// - `"foo@sha256:abc"` → `"foo@sha256:abc"` (digest-pinned)
/// - `"registry:5000/team/app"` → `"registry:5000/team/app:latest"`
/// - `"registry:5000/team/app:v1"` → `"registry:5000/team/app:v1"`
fn normalize_pull_reference(image: &str) -> Cow<'_, str> {
// A digest-pinned reference already identifies an exact image.
if image.contains('@') {
return Cow::Borrowed(image);
}
// A `:` only denotes a tag in the final path component; earlier ones are
// registry ports (e.g. `registry:5000/team/app`).
let last_component = image.rsplit('/').next().unwrap_or(image);
if last_component.contains(':') {
Cow::Borrowed(image)
} else {
Cow::Owned(format!("{image}:latest"))
}
}

/// Docker container names may not end with `-`, `.`, or `_`. Truncation can
/// leave one of those trailing, so strip them before returning.
fn trim_container_name_tail(mut value: String) -> String {
Expand Down Expand Up @@ -6329,7 +6367,7 @@ fn sanitize_docker_name(value: &str) -> String {
async fn pull_runtime_image(docker: &Docker, image: &str, role: &str) -> CoreResult<()> {
let mut stream = docker.create_image(
Some(CreateImageOptions {
from_image: Some(image.to_string()),
from_image: Some(normalize_pull_reference(image).into_owned()),
..Default::default()
}),
None,
Expand Down
29 changes: 29 additions & 0 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3688,3 +3688,32 @@ fn admission_provisioning_failure_distinguishes_denials_from_lookup_failures() {
assert_eq!(lookup.reason, "ResourceAdmissionLookupFailed");
assert_eq!(lookup.message, "inspect docker volume failed");
}

#[test]
fn normalize_pull_reference_appends_latest_only_when_untagged() {
// A bare repository reference must resolve to a single tag so the daemon
// does not pull every tag in the repository (issue #4029).
assert_eq!(
normalize_pull_reference("nicolaka/netshoot"),
"nicolaka/netshoot:latest"
);
// An explicit tag is preserved untouched.
assert_eq!(normalize_pull_reference("foo:1.2"), "foo:1.2");
// A digest-pinned reference already names an exact image.
assert_eq!(
normalize_pull_reference(
"foo@sha256:0000000000000000000000000000000000000000000000000000000000000000"
),
"foo@sha256:0000000000000000000000000000000000000000000000000000000000000000"
);
// A registry port is not a tag, so `:latest` is still appended.
assert_eq!(
normalize_pull_reference("registry:5000/team/app"),
"registry:5000/team/app:latest"
);
// A registry port combined with an explicit tag is left unchanged.
assert_eq!(
normalize_pull_reference("registry:5000/team/app:v1"),
"registry:5000/team/app:v1"
);
}
Loading