From 23a2ea4da3c65dc7636699c296dc00260eb615e2 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Fri, 11 Sep 2026 23:22:06 +0200 Subject: [PATCH 1/3] fix(providers): switch MinIO images from Docker Hub to quay.io Docker Hub now denies pulls of minio/minio and minio/mc entirely ("pull access denied ... repository does not exist or may require 'docker login'"), breaking every test and preset/default that pulled those images. quay.io/minio/minio and quay.io/minio/mc still serve the same content under existing tags (verified digest-for-digest for the mc image used by the backup/restore mirror engines), so this repoints test fixtures, CI workflows, and production defaults (S3/RustFS service presets, the mc utility image used by restore/mirror engines) to the quay.io registry while keeping the same pinned release tags. Signed-off-by: David Viejo --- .github/workflows/apps-e2e-tests.yml | 2 +- .github/workflows/rust-tests.yml | 4 +- apps/temps-e2e/docker-compose.e2e.yml | 2 +- .../src/commands/s3-restore-scenario.ts | 2 +- crates/temps-backup/src/engines/s3_mirror.rs | 2 +- crates/temps-backup/src/services/backup.rs | 45 ++++++++++--------- crates/temps-backup/src/services/restore.rs | 6 +-- .../temps-backup/src/services/s3_lifecycle.rs | 27 +++++------ crates/temps-backup/tests/mariadb_pitr_e2e.rs | 7 ++- .../temps-providers/src/externalsvc/rustfs.rs | 2 +- crates/temps-providers/src/externalsvc/s3.rs | 14 +++--- .../src/externalsvc/test_utils.rs | 6 ++- .../src/parameter_strategies.rs | 4 +- crates/temps-providers/src/services.rs | 2 +- tools/backup-stress/run.sh | 2 +- 15 files changed, 68 insertions(+), 59 deletions(-) diff --git a/.github/workflows/apps-e2e-tests.yml b/.github/workflows/apps-e2e-tests.yml index b9197d0ff4..c9e9a48418 100644 --- a/.github/workflows/apps-e2e-tests.yml +++ b/.github/workflows/apps-e2e-tests.yml @@ -110,7 +110,7 @@ jobs: docker run -d --name temps-e2e-registry -p 5111:5000 registry:2 docker compose -p temps-e2e -f apps/temps-e2e/docker-compose.e2e.yml up -d timeout 90 bash -c 'until nc -z localhost 14000 && nc -z localhost 8056 && nc -z localhost 1025 && nc -z localhost 8025 && nc -z localhost 9092; do sleep 2; done' - docker run --rm --network temps-e2e_default --entrypoint /bin/sh minio/mc \ + docker run --rm --network temps-e2e_default --entrypoint /bin/sh quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z \ -c 'until mc alias set backup http://minio:9000 minioadmin minioadmin; do sleep 2; done; mc mb --ignore-existing backup/temps-e2e-backups' - name: Build immutable MariaDB WAL-G recovery image diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index ae83e8e96a..97d5e40540 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -1022,12 +1022,12 @@ jobs: - name: Pull E2E images run: | - timeout 10m docker pull minio/minio:latest + timeout 10m docker pull quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z timeout 10m docker build --pull \ --build-arg MARIADB_VERSION=11.4 \ --tag ghcr.io/gotempsh/mariadb-walg:11.4 \ images/mariadb-walg - docker image ls | grep -E '^(minio/minio|ghcr.io/gotempsh/mariadb-walg)\s' || true + docker image ls | grep -E '^(quay.io/minio/minio|ghcr.io/gotempsh/mariadb-walg)\s' || true - name: Run MariaDB PITR E2E env: diff --git a/apps/temps-e2e/docker-compose.e2e.yml b/apps/temps-e2e/docker-compose.e2e.yml index a07c5eff9c..3c53373361 100644 --- a/apps/temps-e2e/docker-compose.e2e.yml +++ b/apps/temps-e2e/docker-compose.e2e.yml @@ -125,7 +125,7 @@ services: - "8025:8025" minio: - image: minio/minio:latest + image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z # `backup-restore-scenario` needs a real S3-compatible target so the # backup engine's actual upload (and the restore's actual download) get # exercised, not just the API's bookkeeping rows -- same reasoning as diff --git a/apps/temps-e2e/src/commands/s3-restore-scenario.ts b/apps/temps-e2e/src/commands/s3-restore-scenario.ts index ce485e62a0..68c579642f 100644 --- a/apps/temps-e2e/src/commands/s3-restore-scenario.ts +++ b/apps/temps-e2e/src/commands/s3-restore-scenario.ts @@ -92,7 +92,7 @@ async function runMc(args: string[], envVars: string[]): Promise<{ code: number; '--network', 'host', ...envVars.flatMap((e) => ['-e', e]), - 'minio/mc:latest', + 'quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z', ...args, ] const proc = Bun.spawn(['docker', ...dockerArgs], { stdout: 'pipe', stderr: 'pipe' }) diff --git a/crates/temps-backup/src/engines/s3_mirror.rs b/crates/temps-backup/src/engines/s3_mirror.rs index 400409c606..cfc03acaa9 100644 --- a/crates/temps-backup/src/engines/s3_mirror.rs +++ b/crates/temps-backup/src/engines/s3_mirror.rs @@ -52,7 +52,7 @@ use temps_backup_core::engine_v2::{BackupContext, BackupEngine, BackupError, Bac use temps_providers::externalsvc::SensitiveValues; const ENGINE_KEY: &str = "s3_mirror"; -const MC_IMAGE: &str = "minio/mc:RELEASE.2025-08-13T08-35-41Z@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727"; +const MC_IMAGE: &str = "quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727"; pub struct S3MirrorDeps { pub db: Arc, diff --git a/crates/temps-backup/src/services/backup.rs b/crates/temps-backup/src/services/backup.rs index 4ac1888b97..63d2b97774 100644 --- a/crates/temps-backup/src/services/backup.rs +++ b/crates/temps-backup/src/services/backup.rs @@ -11262,13 +11262,14 @@ mod tests { use testcontainers::{runners::AsyncRunner, GenericImage, ImageExt}; // Start MinIO container - let minio_container = GenericImage::new("minio/minio", "latest") - .with_env_var("MINIO_ROOT_USER", "minioadmin") - .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") - .with_cmd(vec!["server", "/data", "--console-address", ":9001"]) - .start() - .await - .expect("Failed to start MinIO container"); + let minio_container = + GenericImage::new("quay.io/minio/minio", "RELEASE.2025-09-07T16-13-09Z") + .with_env_var("MINIO_ROOT_USER", "minioadmin") + .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") + .with_cmd(vec!["server", "/data", "--console-address", ":9001"]) + .start() + .await + .expect("Failed to start MinIO container"); let minio_port = minio_container .get_host_port_ipv4(9000) @@ -11691,13 +11692,14 @@ mod tests { use temps_database::test_utils::TestDatabase; use testcontainers::{runners::AsyncRunner, GenericImage, ImageExt}; - let minio_container = GenericImage::new("minio/minio", "latest") - .with_env_var("MINIO_ROOT_USER", "minioadmin") - .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") - .with_cmd(vec!["server", "/data", "--console-address", ":9001"]) - .start() - .await - .expect("Failed to start MinIO container"); + let minio_container = + GenericImage::new("quay.io/minio/minio", "RELEASE.2025-09-07T16-13-09Z") + .with_env_var("MINIO_ROOT_USER", "minioadmin") + .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") + .with_cmd(vec!["server", "/data", "--console-address", ":9001"]) + .start() + .await + .expect("Failed to start MinIO container"); let minio_port = minio_container .get_host_port_ipv4(9000) .await @@ -11909,13 +11911,14 @@ mod tests { use testcontainers::{runners::AsyncRunner, GenericImage, ImageExt}; // Start MinIO container - let minio_container = GenericImage::new("minio/minio", "latest") - .with_env_var("MINIO_ROOT_USER", "minioadmin") - .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") - .with_cmd(vec!["server", "/data", "--console-address", ":9001"]) - .start() - .await - .expect("Failed to start MinIO container"); + let minio_container = + GenericImage::new("quay.io/minio/minio", "RELEASE.2025-09-07T16-13-09Z") + .with_env_var("MINIO_ROOT_USER", "minioadmin") + .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") + .with_cmd(vec!["server", "/data", "--console-address", ":9001"]) + .start() + .await + .expect("Failed to start MinIO container"); let minio_port = minio_container .get_host_port_ipv4(9000) diff --git a/crates/temps-backup/src/services/restore.rs b/crates/temps-backup/src/services/restore.rs index 547ecd5680..8a7a45e948 100644 --- a/crates/temps-backup/src/services/restore.rs +++ b/crates/temps-backup/src/services/restore.rs @@ -3578,8 +3578,8 @@ mod tests { let mut stream = docker.create_image( Some(bollard::query_parameters::CreateImageOptions { - from_image: Some("minio/minio".to_string()), - tag: Some("latest".to_string()), + from_image: Some("quay.io/minio/minio".to_string()), + tag: Some("RELEASE.2025-09-07T16-13-09Z".to_string()), ..Default::default() }), None, @@ -3599,7 +3599,7 @@ mod tests { let name = format!("temps-test-restore-loc-minio-{}", uuid::Uuid::new_v4()); let config = bollard::models::ContainerCreateBody { - image: Some("minio/minio:latest".to_string()), + image: Some("quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string()), cmd: Some(vec!["server".to_string(), "/data".to_string()]), env: Some(vec![ format!("MINIO_ROOT_USER={LOCATION_TEST_MINIO_ACCESS_KEY}"), diff --git a/crates/temps-backup/src/services/s3_lifecycle.rs b/crates/temps-backup/src/services/s3_lifecycle.rs index ac365ba65d..757eadf724 100644 --- a/crates/temps-backup/src/services/s3_lifecycle.rs +++ b/crates/temps-backup/src/services/s3_lifecycle.rs @@ -768,19 +768,20 @@ mod tests { } use testcontainers::{runners::AsyncRunner, GenericImage, ImageExt}; - let container = match GenericImage::new("minio/minio", "latest") - .with_env_var("MINIO_ROOT_USER", "minioadmin") - .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") - .with_cmd(vec!["server", "/data", "--console-address", ":9001"]) - .start() - .await - { - Ok(c) => c, - Err(e) => { - println!("Failed to start MinIO container ({}), skipping", e); - return; - } - }; + let container = + match GenericImage::new("quay.io/minio/minio", "RELEASE.2025-09-07T16-13-09Z") + .with_env_var("MINIO_ROOT_USER", "minioadmin") + .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin") + .with_cmd(vec!["server", "/data", "--console-address", ":9001"]) + .start() + .await + { + Ok(c) => c, + Err(e) => { + println!("Failed to start MinIO container ({}), skipping", e); + return; + } + }; let port = container .get_host_port_ipv4(9000) diff --git a/crates/temps-backup/tests/mariadb_pitr_e2e.rs b/crates/temps-backup/tests/mariadb_pitr_e2e.rs index b77eb100b6..3971e1a085 100644 --- a/crates/temps-backup/tests/mariadb_pitr_e2e.rs +++ b/crates/temps-backup/tests/mariadb_pitr_e2e.rs @@ -213,7 +213,10 @@ fn find_available_port(start: u16) -> Option { /// Boot a MinIO container, returning (host_port, container_name, guard). /// Skips (None) on failure so the test can bail gracefully. async fn boot_minio(docker: &Docker) -> Option<(u16, String, ContainerGuard)> { - if pull_image(docker, "minio/minio:latest").await.is_err() { + if pull_image(docker, "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z") + .await + .is_err() + { eprintln!("Could not pull MinIO image, skipping"); return None; } @@ -221,7 +224,7 @@ async fn boot_minio(docker: &Docker) -> Option<(u16, String, ContainerGuard)> { let name = format!("temps-test-pitr-minio-{}", uuid::Uuid::new_v4()); let config = bollard::models::ContainerCreateBody { - image: Some("minio/minio:latest".to_string()), + image: Some("quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string()), cmd: Some(vec!["server".to_string(), "/data".to_string()]), env: Some(vec![ format!("MINIO_ROOT_USER={MINIO_ACCESS_KEY}"), diff --git a/crates/temps-providers/src/externalsvc/rustfs.rs b/crates/temps-providers/src/externalsvc/rustfs.rs index a9128753f9..52e9d57bcd 100644 --- a/crates/temps-providers/src/externalsvc/rustfs.rs +++ b/crates/temps-providers/src/externalsvc/rustfs.rs @@ -457,7 +457,7 @@ fn adopted_ports_from_inspect( impl RustfsService { /// MinIO Client (mc) utility image - used for backup/restore operations via mc mirror - const MC_IMAGE: &'static str = "minio/mc:RELEASE.2025-08-13T08-35-41Z"; + const MC_IMAGE: &'static str = "quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z"; pub fn new( name: String, diff --git a/crates/temps-providers/src/externalsvc/s3.rs b/crates/temps-providers/src/externalsvc/s3.rs index cf5592f190..646b59ab77 100644 --- a/crates/temps-providers/src/externalsvc/s3.rs +++ b/crates/temps-providers/src/externalsvc/s3.rs @@ -62,7 +62,7 @@ pub struct S3InputConfig { #[schemars(example = example_region(), default = "default_region")] pub region: String, - /// Docker image to use for MinIO (e.g., minio/minio:RELEASE.2025-09-07T16-13-09Z) + /// Docker image to use for MinIO (e.g., quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z) #[serde(default = "default_image")] #[schemars(example = example_image(), default = "default_image")] pub docker_image: String, @@ -185,11 +185,11 @@ fn example_region() -> &'static str { } fn default_image() -> String { - "minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string() + "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string() } fn example_image() -> &'static str { - "minio/minio:RELEASE.2025-09-07T16-13-09Z" + "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" } use super::port_util::{find_available_port, find_available_port_async, is_port_conflict_error}; @@ -208,7 +208,7 @@ impl S3Service { /// MinIO Client (mc) utility image - used for temporary operations like migration and copy. /// Pinned to an immutable release tag — never use `:latest` here to prevent /// supply-chain / MITM attacks on floating tags. - const MC_IMAGE: &'static str = "minio/mc:RELEASE.2025-08-13T08-35-41Z"; + const MC_IMAGE: &'static str = "quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z"; /// Shell script executed inside a disposable mc container by `restore_in_place`. /// @@ -2646,7 +2646,7 @@ impl ExternalService for S3Service { // Return (image_name, version) // Default MinIO image and release version ( - "minio/minio".to_string(), + "quay.io/minio/minio".to_string(), "RELEASE.2025-09-07T16-13-09Z".to_string(), ) } @@ -2978,8 +2978,8 @@ mod tests { let service = S3Service::new("test-image".to_string(), docker, encryption_service); let (image_name, version) = service.get_default_docker_image(); assert_eq!( - image_name, "minio/minio", - "Default image should be minio/minio" + image_name, "quay.io/minio/minio", + "Default image should be quay.io/minio/minio" ); assert!( version.starts_with("RELEASE."), diff --git a/crates/temps-providers/src/externalsvc/test_utils.rs b/crates/temps-providers/src/externalsvc/test_utils.rs index 3a0ed746ae..3436068f9c 100644 --- a/crates/temps-providers/src/externalsvc/test_utils.rs +++ b/crates/temps-providers/src/externalsvc/test_utils.rs @@ -64,7 +64,9 @@ mod docker_utils { // Pull MinIO image let mut pull_stream = docker.create_image( Some(CreateImageOptions { - from_image: Some("minio/minio:latest".to_string()), + from_image: Some( + "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string(), + ), ..Default::default() }), None, @@ -81,7 +83,7 @@ mod docker_utils { rand::random::() ); let minio_config = bollard::models::ContainerCreateBody { - image: Some("minio/minio:latest".to_string()), + image: Some("quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string()), cmd: Some(vec!["server".to_string(), "/data".to_string()]), env: Some(vec![ format!("MINIO_ROOT_USER={}", access_key), diff --git a/crates/temps-providers/src/parameter_strategies.rs b/crates/temps-providers/src/parameter_strategies.rs index 2d44fac006..be67a412e2 100644 --- a/crates/temps-providers/src/parameter_strategies.rs +++ b/crates/temps-providers/src/parameter_strategies.rs @@ -937,7 +937,7 @@ impl ParameterStrategy for MinioParameterStrategy { if is_empty_value(params.get("docker_image")) { params.insert( "docker_image".to_string(), - JsonValue::String("minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string()), + JsonValue::String("quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string()), ); } @@ -1018,7 +1018,7 @@ impl ParameterStrategy for MinioParameterStrategy { "docker_image": { "type": "string", "description": "Docker image (updateable)", - "default": "minio/minio:RELEASE.2025-09-07T16-13-09Z" + "default": "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" } }, "readonly": ["access_key", "secret_key"] diff --git a/crates/temps-providers/src/services.rs b/crates/temps-providers/src/services.rs index a4dc5f8af6..10fb9b9afa 100644 --- a/crates/temps-providers/src/services.rs +++ b/crates/temps-providers/src/services.rs @@ -1946,7 +1946,7 @@ impl ExternalServiceManager { let image = parameters .get("docker_image") .cloned() - .unwrap_or_else(|| "minio/minio:latest".to_string()); + .unwrap_or_else(|| "quay.io/minio/minio:latest".to_string()); let access_key = parameters .get("access_key") .cloned() diff --git a/tools/backup-stress/run.sh b/tools/backup-stress/run.sh index a55c00bcda..8ef8fb8a40 100755 --- a/tools/backup-stress/run.sh +++ b/tools/backup-stress/run.sh @@ -19,7 +19,7 @@ REPORT_DIR=${TEMPS_BENCH_REPORT_DIR:-"/tmp/temps-backup-bench-$(date -u +%Y%m%dT NETWORK=temps-backup-bench_default PG_CONTAINER=temps-backup-bench-postgres-1 RUSTFS_CONTAINER=temps-backup-bench-rustfs-1 -MC_IMAGE=minio/mc:latest +MC_IMAGE=quay.io/minio/mc:latest PG_IMAGE=gotempsh/postgres-walg:18-bookworm S3_PREFIX=s3://backups/postgres/walg ACTIVE_BACKUP=0 From 8131a6a4f2046738fb18be07c020c1ba1bd488e5 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 12 Sep 2026 09:07:03 +0200 Subject: [PATCH 2/3] fix(providers): pin remaining test-fixture MinIO image to quay.io CI's docker-backups integration job caught one more spot: the restore-to-new-service test fixture in s3.rs still pulled minio/minio:latest from Docker Hub, which fails the same way as everything else fixed in this branch. Signed-off-by: David Viejo --- crates/temps-providers/src/externalsvc/s3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/temps-providers/src/externalsvc/s3.rs b/crates/temps-providers/src/externalsvc/s3.rs index 646b59ab77..401c7fe18e 100644 --- a/crates/temps-providers/src/externalsvc/s3.rs +++ b/crates/temps-providers/src/externalsvc/s3.rs @@ -3320,7 +3320,7 @@ mod tests { "access_key": source_minio.access_key.clone(), "secret_key": source_minio.secret_key.clone(), "region": "us-east-1", - "docker_image": "minio/minio:latest", + "docker_image": "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z", }); let s3_config = ServiceConfig { From 8a13bdb7d4f307280359fd993d2b84290ffffbbc Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sat, 12 Sep 2026 09:51:20 +0200 Subject: [PATCH 3/3] fix(providers): normalize persisted MinIO images to quay.io on load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile flagged that changing the built-in defaults didn't help services created before this fix: their docker_image is already persisted in the database as the old bare minio/minio or minio/mc reference, and create_container_once always re-pulls the image before even checking whether the container exists — so init()/restart on an existing S3 or RustFS-mc-backed service would keep retrying the now Docker-Hub-denied image forever. Normalize in From for S3Config, the single conversion point used both for new services and for reconstructing config from stored parameters on every init(): rewrite the exact former bare Docker Hub defaults (minio/minio:*, minio/mc:*) to their quay.io equivalents, and leave anything already qualified (quay.io/..., a private mirror, a fork) untouched since that reflects an explicit operator choice, not our old default. Signed-off-by: David Viejo --- crates/temps-providers/src/externalsvc/s3.rs | 89 ++++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/crates/temps-providers/src/externalsvc/s3.rs b/crates/temps-providers/src/externalsvc/s3.rs index 401c7fe18e..a37f176d5d 100644 --- a/crates/temps-providers/src/externalsvc/s3.rs +++ b/crates/temps-providers/src/externalsvc/s3.rs @@ -95,6 +95,30 @@ pub struct S3Config { pub container_name: Option, } +/// Rewrites a bare (unqualified, i.e. Docker-Hub-resolved) `minio/minio` or +/// `minio/mc` reference to its `quay.io` equivalent, preserving the tag. +/// +/// Docker Hub stopped serving these repositories entirely in 2026 ("pull +/// access denied ... repository does not exist"), so any service whose +/// `docker_image` was persisted before this fix — created back when +/// `default_image()`/`MC_IMAGE` still pointed at Docker Hub — would otherwise +/// keep retrying that dead reference on every `init()`/container recreation +/// forever, since `create_container_once` always re-pulls before checking +/// whether the container already exists. Only the exact former built-in +/// defaults are rewritten; an already-qualified reference (`quay.io/...`, +/// a private mirror, `ghcr.io/...`) is left untouched since it reflects an +/// explicit operator choice, not our old default. +fn normalize_minio_registry(image: String) -> String { + for repo in ["minio/minio", "minio/mc"] { + if let Some(rest) = image.strip_prefix(repo) { + if rest.is_empty() || rest.starts_with(':') || rest.starts_with('@') { + return format!("quay.io/{repo}{rest}"); + } + } + } + image +} + impl From for S3Config { fn from(input: S3InputConfig) -> Self { Self { @@ -107,7 +131,7 @@ impl From for S3Config { secret_key: input.secret_key.unwrap_or_else(default_secret_key), host: input.host, region: input.region, - docker_image: input.docker_image, + docker_image: normalize_minio_registry(input.docker_image), container_name: input.container_name, } } @@ -2989,27 +3013,82 @@ mod tests { #[test] fn test_image_field_in_configuration() { - // Test S3 configuration with docker_image field + // Test S3 configuration with an already-qualified docker_image field let input_config = S3InputConfig { port: Some("9000".to_string()), access_key: Some("minioadmin".to_string()), secret_key: Some("minioadmin".to_string()), host: "localhost".to_string(), region: "us-east-1".to_string(), - docker_image: "minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string(), + docker_image: "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z".to_string(), container_name: None, }; // Convert to runtime config let runtime_config: S3Config = input_config.into(); - // Verify docker_image is preserved + // Verify an already-qualified docker_image is preserved as-is assert_eq!( runtime_config.docker_image, - "minio/minio:RELEASE.2025-09-07T16-13-09Z" + "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z" ); } + #[test] + fn test_legacy_docker_hub_minio_image_is_normalized_to_quay_io() { + // Services created before the Docker Hub -> quay.io migration have + // this bare reference persisted in the database. It must be rewritten + // on load, since `create_container_once` always re-pulls the image + // (even when the container already exists) and Docker Hub now + // denies these repositories outright. + for (persisted, expected) in [ + ( + "minio/minio:RELEASE.2025-09-07T16-13-09Z", + "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z", + ), + ("minio/minio:latest", "quay.io/minio/minio:latest"), + ("minio/mc:latest", "quay.io/minio/mc:latest"), + ] { + let input_config = S3InputConfig { + port: Some("9000".to_string()), + access_key: Some("minioadmin".to_string()), + secret_key: Some("minioadmin".to_string()), + host: "localhost".to_string(), + region: "us-east-1".to_string(), + docker_image: persisted.to_string(), + container_name: None, + }; + + let runtime_config: S3Config = input_config.into(); + assert_eq!(runtime_config.docker_image, expected, "for {persisted}"); + } + } + + #[test] + fn test_custom_registry_minio_image_is_not_rewritten() { + // An operator-configured mirror or fork must never be silently + // redirected to quay.io -- only the exact former bare Docker Hub + // defaults are normalized. + for custom in [ + "registry.internal:5000/minio/minio:latest", + "ghcr.io/acme/minio:latest", + "minio-fork/minio:latest", + ] { + let input_config = S3InputConfig { + port: Some("9000".to_string()), + access_key: Some("minioadmin".to_string()), + secret_key: Some("minioadmin".to_string()), + host: "localhost".to_string(), + region: "us-east-1".to_string(), + docker_image: custom.to_string(), + container_name: None, + }; + + let runtime_config: S3Config = input_config.into(); + assert_eq!(runtime_config.docker_image, custom); + } + } + #[test] fn test_get_effective_address_docker_mode_uses_imported_container_name() { let docker = Arc::new(Docker::connect_with_local_defaults().unwrap());