diff --git a/hypershift-operator/controllers/nodepool/nodepool_controller.go b/hypershift-operator/controllers/nodepool/nodepool_controller.go index fbbd74145794..846d84f20f6a 100644 --- a/hypershift-operator/controllers/nodepool/nodepool_controller.go +++ b/hypershift-operator/controllers/nodepool/nodepool_controller.go @@ -74,8 +74,9 @@ const ( // possible values are 'required' (i.e. IMDSv2) or 'optional' which is the default. ec2InstanceMetadataHTTPTokensAnnotation = "hypershift.openshift.io/ec2-instance-metadata-http-tokens" - nodePoolAnnotationPlatformMachineTemplate = "hypershift.openshift.io/nodePoolPlatformMachineTemplate" - nodePoolAnnotationTaints = "hypershift.openshift.io/nodePoolTaints" + nodePoolAnnotationPlatformMachineTemplate = "hypershift.openshift.io/nodePoolPlatformMachineTemplate" + nodePoolAnnotationTaints = "hypershift.openshift.io/nodePoolTaints" + nodePoolAnnotationCanonicalDataPlaneImages = "hypershift.openshift.io/canonical-data-plane-images" nodePoolCoreIgnitionConfigLabel = "hypershift.openshift.io/core-ignition-config" tuningConfigKey = "tuning" @@ -1089,7 +1090,13 @@ func (r *NodePoolReconciler) getAdditionalTrustBundle(ctx context.Context, hoste // 1. NodePool annotation (highest priority) // 2. Shared ingress image (when cluster uses shared ingress for public endpoints) // 3. Release payload (default) -func resolveHAProxyImage(nodePool *hyperv1.NodePool, hcluster *hyperv1.HostedCluster, releaseImage *releaseinfo.ReleaseImage) (string, error) { +// +// When useCanonicalImages is true and the image comes from the release payload, +// any management-cluster registry overrides are reversed. The HAProxy image is +// embedded in a static pod manifest that runs on data plane nodes, where CRI-O +// handles mirroring natively via IDMS/ICSP — so the canonical (non-overridden) +// image reference must be used. +func resolveHAProxyImage(nodePool *hyperv1.NodePool, hcluster *hyperv1.HostedCluster, releaseImage *releaseinfo.ReleaseImage, releaseProvider releaseinfo.Provider, useCanonicalImages bool) (string, error) { if annotationImage := strings.TrimSpace(nodePool.Annotations[hyperv1.NodePoolHAProxyImageAnnotation]); annotationImage != "" { return annotationImage, nil } @@ -1102,11 +1109,32 @@ func resolveHAProxyImage(nodePool *hyperv1.NodePool, hcluster *hyperv1.HostedClu if !ok { return "", fmt.Errorf("release image doesn't have a %s image", haproxy.HAProxyRouterImageName) } + + if useCanonicalImages { + if p, ok := releaseProvider.(releaseinfo.ProviderWithRegistryOverrides); ok { + for registrySource, registryDest := range p.GetRegistryOverrides() { + haProxyImage = strings.Replace(haProxyImage, registryDest, registrySource, 1) + } + } + } + return haProxyImage, nil } func (r *NodePoolReconciler) generateHAProxyRawConfig(ctx context.Context, nodePool *hyperv1.NodePool, hcluster *hyperv1.HostedCluster, releaseImage *releaseinfo.ReleaseImage) (string, error) { - haProxyImage, err := resolveHAProxyImage(nodePool, hcluster, releaseImage) + useCanonicalImages := nodePool.Annotations[nodePoolAnnotationCanonicalDataPlaneImages] == "true" + if !useCanonicalImages { + isNewOrUpgrading := nodePool.Status.Version == "" || nodePool.Status.Version != releaseImage.Version() + if isNewOrUpgrading { + useCanonicalImages = true + if nodePool.Annotations == nil { + nodePool.Annotations = make(map[string]string) + } + nodePool.Annotations[nodePoolAnnotationCanonicalDataPlaneImages] = "true" + } + } + + haProxyImage, err := resolveHAProxyImage(nodePool, hcluster, releaseImage, r.ReleaseProvider, useCanonicalImages) if err != nil { return "", err } diff --git a/hypershift-operator/controllers/nodepool/nodepool_controller_test.go b/hypershift-operator/controllers/nodepool/nodepool_controller_test.go index f3ece180df0c..f466e47cb175 100644 --- a/hypershift-operator/controllers/nodepool/nodepool_controller_test.go +++ b/hypershift-operator/controllers/nodepool/nodepool_controller_test.go @@ -2507,11 +2507,14 @@ func TestResolveHAProxyImage(t *testing.T) { ) testCases := []struct { - name string - nodePoolAnnotations map[string]string - useSharedIngress bool - envVarImage string - expectedImage string + name string + nodePoolAnnotations map[string]string + nodePoolStatusVersion string + useSharedIngress bool + envVarImage string + registryOverrides map[string]string + componentImage string + expectedImage string }{ { name: "When NodePool annotation is set it should use annotation image", @@ -2566,6 +2569,49 @@ func TestResolveHAProxyImage(t *testing.T) { useSharedIngress: false, expectedImage: testReleaseImage, }, + { + name: "When registry overrides exist and NodePool is new it should reverse them for the release payload image", + useSharedIngress: false, + componentImage: "mirror.example.com/openshift/haproxy-router:v4.16", + registryOverrides: map[string]string{"registry.test.io": "mirror.example.com"}, + expectedImage: "registry.test.io/openshift/haproxy-router:v4.16", + }, + { + name: "When registry overrides exist and NodePool is upgrading it should reverse them", + nodePoolStatusVersion: "4.17.0", + useSharedIngress: false, + componentImage: "mirror.example.com/openshift/haproxy-router:v4.16", + registryOverrides: map[string]string{"registry.test.io": "mirror.example.com"}, + expectedImage: "registry.test.io/openshift/haproxy-router:v4.16", + }, + { + name: "When registry overrides exist and canonical-data-plane-images annotation is set it should reverse them", + nodePoolAnnotations: map[string]string{ + nodePoolAnnotationCanonicalDataPlaneImages: "true", + }, + nodePoolStatusVersion: "4.18.0", + useSharedIngress: false, + componentImage: "mirror.example.com/openshift/haproxy-router:v4.16", + registryOverrides: map[string]string{"registry.test.io": "mirror.example.com"}, + expectedImage: "registry.test.io/openshift/haproxy-router:v4.16", + }, + { + name: "When registry overrides exist and NodePool is stable without annotation it should preserve overridden image", + nodePoolStatusVersion: "4.18.0", + useSharedIngress: false, + componentImage: "mirror.example.com/openshift/haproxy-router:v4.16", + registryOverrides: map[string]string{"registry.test.io": "mirror.example.com"}, + expectedImage: "mirror.example.com/openshift/haproxy-router:v4.16", + }, + { + name: "When registry overrides exist it should not reverse them for an annotation image", + nodePoolAnnotations: map[string]string{ + hyperv1.NodePoolHAProxyImageAnnotation: "mirror.example.com/custom/haproxy:latest", + }, + componentImage: "mirror.example.com/openshift/haproxy-router:v4.16", + registryOverrides: map[string]string{"registry.test.io": "mirror.example.com"}, + expectedImage: "mirror.example.com/custom/haproxy:latest", + }, } for _, tc := range testCases { @@ -2587,6 +2633,9 @@ func TestResolveHAProxyImage(t *testing.T) { Namespace: "clusters", Annotations: tc.nodePoolAnnotations, }, + Status: hyperv1.NodePoolStatus{ + Version: tc.nodePoolStatusVersion, + }, } // Create kubeconfig secret @@ -2657,11 +2706,17 @@ kind: Config`), // Create fake client c := fake.NewClientBuilder().WithObjects(objects...).Build() + componentImage := testReleaseImage + if tc.componentImage != "" { + componentImage = tc.componentImage + } + // Create fake release provider with component images releaseProvider := &fakereleaseprovider.FakeReleaseProvider{ Components: map[string]string{ - haproxy.HAProxyRouterImageName: testReleaseImage, + haproxy.HAProxyRouterImageName: componentImage, }, + FakeRegistryOverrides: tc.registryOverrides, } // Create test HostedCluster diff --git a/support/releaseinfo/fake/fake.go b/support/releaseinfo/fake/fake.go index bd5866aa04a0..d5d730a816d2 100644 --- a/support/releaseinfo/fake/fake.go +++ b/support/releaseinfo/fake/fake.go @@ -26,8 +26,9 @@ type FakeReleaseProvider struct { // Version of the returned release image. Defaults to 4.14.0 if unset. Version string // Allows image-based versioning - ImageVersion map[string]string - Components map[string]string + ImageVersion map[string]string + Components map[string]string + FakeRegistryOverrides map[string]string } func (f *FakeReleaseProvider) Lookup(_ context.Context, image string, _ []byte) (*releaseinfo.ReleaseImage, error) { @@ -134,8 +135,8 @@ func (f *FakeReleaseProvider) Lookup(_ context.Context, image string, _ []byte) return releaseImage, nil } -func (*FakeReleaseProvider) GetRegistryOverrides() map[string]string { - return nil +func (f *FakeReleaseProvider) GetRegistryOverrides() map[string]string { + return f.FakeRegistryOverrides } func (*FakeReleaseProvider) GetOpenShiftImageRegistryOverrides() map[string][]string {