diff --git a/test/extended/internalreleaseimage/helper.go b/test/extended/internalreleaseimage/helper.go index 4104868a8f4a..ad289bf022fa 100644 --- a/test/extended/internalreleaseimage/helper.go +++ b/test/extended/internalreleaseimage/helper.go @@ -2,6 +2,7 @@ package internalreleaseimage import ( "context" + "encoding/json" "fmt" "strings" "time" @@ -160,6 +161,111 @@ func (h *IRITestHelper) DeleteTestPod(namespace, name string) { } } +// dockerConfigJSON is a minimal representation of a .dockerconfigjson pull secret. +type dockerConfigJSON struct { + Auths map[string]dockerConfigEntry `json:"auths"` +} + +type dockerConfigEntry struct { + Auth string `json:"auth"` +} + +// registryHostFromImage extracts the registry host[:port] from an image reference. +// Example: "api-int.example.com:22625/openshift/release-images@sha256:abc" -> "api-int.example.com:22625" +func registryHostFromImage(image string) string { + return strings.SplitN(image, "/", 2)[0] +} + +// GetGlobalPullSecret returns the cluster-wide pull secret from openshift-config/pull-secret. +func (h *IRITestHelper) GetGlobalPullSecret() *corev1.Secret { + secret, err := h.oc.AdminKubeClient().CoreV1().Secrets("openshift-config").Get(context.Background(), "pull-secret", metav1.GetOptions{}) + o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get global pull secret openshift-config/pull-secret") + return secret +} + +// VerifyGlobalPullSecretHasRegistry asserts that the global pull secret contains credentials +// for the given registry host. This validates that the MCO template controller merged the +// InternalReleaseImage registry credentials into the global pull secret (OCPBUGS-85519), so +// that components beyond the kubelet can pull images from the internal registry. +func (h *IRITestHelper) VerifyGlobalPullSecretHasRegistry(registryHost string) { + secret := h.GetGlobalPullSecret() + o.Expect(secret.Type).To(o.Equal(corev1.SecretTypeDockerConfigJson), "global pull secret should be of type %s", corev1.SecretTypeDockerConfigJson) + + data, ok := secret.Data[corev1.DockerConfigJsonKey] + o.Expect(ok).To(o.BeTrue(), "global pull secret should contain a %s key", corev1.DockerConfigJsonKey) + + var dockerCfg dockerConfigJSON + err := json.Unmarshal(data, &dockerCfg) + o.Expect(err).NotTo(o.HaveOccurred(), "Failed to parse global pull secret dockerconfigjson") + + found := false + for registry, entry := range dockerCfg.Auths { + if (registry == registryHost || strings.HasPrefix(registry, registryHost+"/")) && entry.Auth != "" { + e2e.Logf("Found IRI registry credentials in global pull secret for %q", registry) + found = true + break + } + } + o.Expect(found).To(o.BeTrue(), "global pull secret must contain credentials for IRI registry host %q", registryHost) +} + +// CreateImagePullSecretFromGlobal copies the global pull secret's dockerconfigjson into a new +// pull secret in the given namespace and returns the new secret's name. This lets a workload +// pull images explicitly using the global pull secret credentials. +func (h *IRITestHelper) CreateImagePullSecretFromGlobal(namespace string) string { + global := h.GetGlobalPullSecret() + data := global.Data[corev1.DockerConfigJsonKey] + o.Expect(data).NotTo(o.BeEmpty(), "global pull secret dockerconfigjson must not be empty") + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "iri-global-pull-secret-" + string(uuid.NewUUID()), + Namespace: namespace, + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: data, + }, + } + + created, err := h.oc.AdminKubeClient().CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{}) + o.Expect(err).NotTo(o.HaveOccurred(), "Failed to create image pull secret from global pull secret") + e2e.Logf("Created image pull secret %s/%s from global pull secret", namespace, created.Name) + return created.Name +} + +// CreateTestPodWithPullSecret creates a test pod that pulls the specified image using the +// provided image pull secret. ImagePullPolicy is Always so the credentials are exercised on +// the manifest fetch rather than relying on an on-node image cache. +func (h *IRITestHelper) CreateTestPodWithPullSecret(namespace, image, pullSecretName string) *corev1.Pod { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "iri-pullsecret-test-" + string(uuid.NewUUID()), + Namespace: namespace, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + SecurityContext: e2epod.GetRestrictedPodSecurityContext(), + ImagePullSecrets: []corev1.LocalObjectReference{{Name: pullSecretName}}, + Containers: []corev1.Container{ + { + Name: "test", + Image: image, + ImagePullPolicy: corev1.PullAlways, + Command: []string{"echo", "success"}, + SecurityContext: e2epod.GetRestrictedContainerSecurityContext(), + }, + }, + }, + } + + createdPod, err := h.oc.AdminKubeClient().CoreV1().Pods(namespace).Create(context.Background(), pod, metav1.CreateOptions{}) + o.Expect(err).NotTo(o.HaveOccurred(), "Failed to create test pod with pull secret") + e2e.Logf("Created test pod with pull secret: %s/%s", createdPod.Namespace, createdPod.Name) + + return createdPod +} + // CreateSimpleNamespace creates a namespace with pod security labels and waits // for SCC annotations. It uses admin client only, avoiding the user/OAuth/project // request flow in SetupProject that breaks in proxied CI environments. diff --git a/test/extended/internalreleaseimage/internalreleaseimage.go b/test/extended/internalreleaseimage/internalreleaseimage.go index ff0feb6f7936..484ee86acb19 100644 --- a/test/extended/internalreleaseimage/internalreleaseimage.go +++ b/test/extended/internalreleaseimage/internalreleaseimage.go @@ -187,3 +187,51 @@ var _ = g.Describe("[sig-installer][Feature:NoRegistryClusterInstall] Cluster op }) }) }) + +var _ = g.Describe("[sig-installer][Feature:NoRegistryClusterInstall] InternalReleaseImage credentials are merged into the global pull secret", func() { + defer g.GinkgoRecover() + + var oc = exutil.NewCLIWithoutNamespace("no-registry") + var helper *IRITestHelper + + g.BeforeEach(func() { + skipIfNoRegistryFeatureUnsupported(oc) + helper = NewIRITestHelper(oc) + }) + + g.Context("when the NoRegistryClusterInstall feature is enabled", func() { + g.It("should allow a workload to pull the release image using the global pull secret [apigroup:machineconfiguration.openshift.io]", func() { + iri := helper.GetIRI() + releaseImage := iri.Status.Releases[0].Image + e2e.Logf("Using OCP release bundle image: %s", releaseImage) + + registryHost := registryHostFromImage(releaseImage) + e2e.Logf("IRI registry host: %s", registryHost) + + // The MCO template controller merges the IRI registry credentials into the global + // pull secret (OCPBUGS-85519). Previously these lived only in the kubelet's + // config.json, so only the kubelet could pull IRI images. + g.By("Verifying the IRI registry credentials were merged into the global pull secret") + helper.VerifyGlobalPullSecretHasRegistry(registryHost) + + g.By("Creating test namespace and an image pull secret from the global pull secret") + ns := helper.CreateSimpleNamespace() + defer helper.DeleteNamespace(ns) + + pullSecretName := helper.CreateImagePullSecretFromGlobal(ns) + + g.By("Creating a pod that pulls the IRI release image using the global pull secret") + pod := helper.CreateTestPodWithPullSecret(ns, releaseImage, pullSecretName) + defer helper.DeleteTestPod(ns, pod.Name) + + g.By("Waiting for the pod to complete successfully") + err := e2epod.WaitForPodSuccessInNamespace(context.Background(), oc.AdminKubeClient(), pod.Name, ns) + o.Expect(err).NotTo(o.HaveOccurred(), "Pod should pull the IRI image using the global pull secret and run successfully") + + completedPod, err := oc.AdminKubeClient().CoreV1().Pods(ns).Get(context.Background(), pod.Name, metav1.GetOptions{}) + o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get completed pod status") + o.Expect(completedPod.Status.ContainerStatuses).NotTo(o.BeEmpty(), "Pod should have at least one container status") + e2e.Logf("Workload successfully pulled IRI image using global pull secret (ImageID: %s)", completedPod.Status.ContainerStatuses[0].ImageID) + }) + }) +})