From e498117f0374d4cf30b8e3ffe02e98c959e6458c Mon Sep 17 00:00:00 2001 From: Mulham Raee Date: Wed, 24 Jun 2026 17:21:29 +0200 Subject: [PATCH] fix(OCPBUGS-92034): registry override matching for digest and tag separators The registryoverride.Replace function only accepted "/" as a valid separator after the source prefix, causing repository-level --registry-overrides to fail for images with @sha256: digest references. This broke all component image rewrites in disconnected environments using overrides like: quay.io/openshift-release-dev/ocp-v4.0-art-dev=mirror/art-dev Extend matchesPrefix to also accept "@" (digest) and ":" (tag) as valid separators alongside "/". Co-Authored-By: Claude Opus 4.6 (1M context) --- .../util/registryoverride/registryoverride.go | 29 ++++++++++++-- .../registryoverride/registryoverride_test.go | 39 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/support/util/registryoverride/registryoverride.go b/support/util/registryoverride/registryoverride.go index 009c4d43da1a..e7eff74bad49 100644 --- a/support/util/registryoverride/registryoverride.go +++ b/support/util/registryoverride/registryoverride.go @@ -12,8 +12,11 @@ import "strings" // // Matching is strict: an override applies only when the image reference is // exactly equal to the source key or starts with the source key followed by a -// "/" separator. This prevents accidental substring matches (e.g. an override -// for "quay.io" must not match "quay.io.example.com/foo"). +// valid image-reference separator ("/" for path components, "@" for digests, +// or ":" for tags). This prevents accidental substring matches (e.g. an +// override for "quay.io" must not match "quay.io.example.com/foo") while +// correctly handling repository-level overrides against digest references +// (e.g. "quay.io/org/repo" must match "quay.io/org/repo@sha256:abc"). // // When several override keys match the same image, the longest key wins. This // makes the result deterministic regardless of map iteration order and lets @@ -31,7 +34,7 @@ func Replace(image string, overrides map[string]string) string { if source == "" { continue } - if image != source && !strings.HasPrefix(image, source+"/") { + if !matchesPrefix(image, source) { continue } if len(source) > len(bestSource) { @@ -43,3 +46,23 @@ func Replace(image string, overrides map[string]string) string { } return bestTarget + image[len(bestSource):] } + +// matchesPrefix reports whether image is exactly source, or source is a prefix +// of image followed by a valid separator. Valid separators are "/" (path) and +// "@" (digest), always. ":" is accepted only when source contains a "/" (i.e. +// includes a path component), where it denotes a tag boundary. Without a "/" +// the source is a bare hostname and ":" would be a port separator, which must +// not match (e.g. override "quay.io" must not match "quay.io:5000/foo"). +func matchesPrefix(image, source string) bool { + if image == source { + return true + } + if !strings.HasPrefix(image, source) { + return false + } + sep := image[len(source)] + if sep == '/' || sep == '@' { + return true + } + return sep == ':' && strings.ContainsRune(source, '/') +} diff --git a/support/util/registryoverride/registryoverride_test.go b/support/util/registryoverride/registryoverride_test.go index 487dbfd4c12f..1212c8ae9b74 100644 --- a/support/util/registryoverride/registryoverride_test.go +++ b/support/util/registryoverride/registryoverride_test.go @@ -89,6 +89,45 @@ func TestReplace(t *testing.T) { overrides: map[string]string{"quay.io": "mirror.example.com"}, want: "mirror.example.com/foo/bar:v1.2.3", }, + { + name: "When source matches full repository with digest separator it should replace prefix", + image: "quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:abc123", + overrides: map[string]string{"quay.io/openshift-release-dev/ocp-v4.0-art-dev": "mirror.example.com/art-dev"}, + want: "mirror.example.com/art-dev@sha256:abc123", + }, + { + name: "When source matches full repository with tag separator it should replace prefix", + image: "quay.io/openshift-release-dev/ocp-v4.0-art-dev:latest", + overrides: map[string]string{"quay.io/openshift-release-dev/ocp-v4.0-art-dev": "mirror.example.com/art-dev"}, + want: "mirror.example.com/art-dev:latest", + }, + { + name: "When multiple overrides match with digest it should pick longest prefix", + image: "quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:abc123", + overrides: map[string]string{ + "quay.io": "broad.example.com", + "quay.io/openshift-release-dev/ocp-v4.0-art-dev": "narrow.example.com/art-dev", + }, + want: "narrow.example.com/art-dev@sha256:abc123", + }, + { + name: "When source has trailing dash it should not match similar prefix (no false positive)", + image: "quay.io/openshift-release-dev/ocp-v4.0-art-dev-extra@sha256:abc", + overrides: map[string]string{"quay.io/openshift-release-dev/ocp-v4.0-art-dev": "mirror/art-dev"}, + want: "quay.io/openshift-release-dev/ocp-v4.0-art-dev-extra@sha256:abc", + }, + { + name: "When host-only source matches host:port image it should not match (port is not a tag)", + image: "quay.io:5000/org/repo@sha256:abc", + overrides: map[string]string{"quay.io": "mirror.example.com"}, + want: "quay.io:5000/org/repo@sha256:abc", + }, + { + name: "When host:port source matches host:port image it should match via slash", + image: "myregistry:5000/org/repo@sha256:abc", + overrides: map[string]string{"myregistry:5000": "mirror.example.com"}, + want: "mirror.example.com/org/repo@sha256:abc", + }, { name: "empty image returns empty", image: "",