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
2 changes: 1 addition & 1 deletion .github/workflows/apps-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion apps/temps-e2e/docker-compose.e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/temps-e2e/src/commands/s3-restore-scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
2 changes: 1 addition & 1 deletion crates/temps-backup/src/engines/s3_mirror.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DatabaseConnection>,
Expand Down
45 changes: 24 additions & 21 deletions crates/temps-backup/src/services/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions crates/temps-backup/src/services/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}"),
Expand Down
27 changes: 14 additions & 13 deletions crates/temps-backup/src/services/s3_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions crates/temps-backup/tests/mariadb_pitr_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,18 @@ fn find_available_port(start: u16) -> Option<u16> {
/// 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;
}
let port = find_available_port(9100)?;
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}"),
Expand Down
2 changes: 1 addition & 1 deletion crates/temps-providers/src/externalsvc/rustfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
105 changes: 92 additions & 13 deletions crates/temps-providers/src/externalsvc/s3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -95,6 +95,30 @@ pub struct S3Config {
pub container_name: Option<String>,
}

/// 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<S3InputConfig> for S3Config {
fn from(input: S3InputConfig) -> Self {
Self {
Expand All @@ -107,7 +131,7 @@ impl From<S3InputConfig> 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,
}
}
Expand Down Expand Up @@ -185,11 +209,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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Persisted images remain broken

Existing S3/MinIO services store their docker_image in encrypted service configuration. Changing default_image() does not update that non-empty value, and initialization, updates, and redeployments continue to reuse it. Services created with the former minio/minio:RELEASE... default will therefore keep pulling from the denied Docker Hub repository after Temps is upgraded. Normalize the exact former built-in default to its quay.io equivalent while preserving user-supplied images.

Knowledge Base Used: Deployment orchestration

}

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};
Expand All @@ -208,7 +232,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`.
///
Expand Down Expand Up @@ -2646,7 +2670,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(),
)
}
Expand Down Expand Up @@ -2978,8 +3002,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."),
Expand All @@ -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());
Expand Down Expand Up @@ -3320,7 +3399,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 {
Expand Down
Loading
Loading