Use fake registry in Node's container runtime image pulling tests

Signed-off-by: Stanislav Láznička <slznika@microsoft.com>
This commit is contained in:
Stanislav Láznička
2025-08-15 15:02:42 +02:00
committed by Stanislav Láznička
parent a3f242676c
commit a0e64c21f2
6 changed files with 151 additions and 50 deletions

View File

@@ -43,6 +43,7 @@ type ConformanceContainer struct {
RestartPolicy v1.RestartPolicy
Volumes []v1.Volume
ImagePullSecrets []string
NodeName string
PodClient *e2epod.PodClient
podName string
@@ -65,6 +66,7 @@ func (cc *ConformanceContainer) Create(ctx context.Context) {
Containers: []v1.Container{
cc.Container,
},
NodeName: cc.NodeName,
SecurityContext: cc.PodSecurityContext,
Volumes: cc.Volumes,
ImagePullSecrets: imagePullSecrets,

View File

@@ -19,6 +19,7 @@ package node
import (
"context"
"fmt"
"net"
"os"
"path"
"time"
@@ -29,6 +30,7 @@ import (
"k8s.io/kubernetes/pkg/kubelet/images"
"k8s.io/kubernetes/test/e2e/framework"
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
e2eregistry "k8s.io/kubernetes/test/e2e/framework/registry"
imageutils "k8s.io/kubernetes/test/utils/image"
admissionapi "k8s.io/pod-security-admission/api"
@@ -39,7 +41,7 @@ import (
var _ = SIGDescribe("Container Runtime", func() {
f := framework.NewDefaultFramework("container-runtime")
f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged // custom registry pods need HostPorts
ginkgo.Describe("blackbox test", func() {
ginkgo.Context("when starting a container that exits", func() {
@@ -257,53 +259,47 @@ while true; do sleep 1; done
})
})
ginkgo.Context("when running a container with a new image", func() {
framework.Context("when running a container with a new image", framework.WithSerial(), func() {
var registrySetup bool
registryAddress := net.JoinHostPort("localhost", "5000")
ginkgo.BeforeEach(func(ctx context.Context) {
_, err := e2eregistry.SetupRegistry(ctx, f)
framework.ExpectNoError(err)
})
ginkgo.AfterEach(func(ctx context.Context) {
if !registrySetup {
return
}
f.DeleteNamespace(ctx, f.Namespace.Name) // we need to wait for the registry to be removed and so we need to delete the whole NS early (before the actual cleanup)
})
// Images used for ConformanceContainer are not added into NodePrePullImageList, because this test is
// testing image pulling, these images don't need to be prepulled. The ImagePullPolicy
// is v1.PullAlways, so it won't be blocked by framework image pre-pull list check.
imagePullTest := func(ctx context.Context, image string, hasSecret bool, expectedPhase v1.PodPhase, expectedPullStatus bool, windowsImage bool) {
command := []string{"/bin/sh", "-c", "while true; do sleep 1; done"}
if windowsImage {
// -t: Ping the specified host until stopped.
command = []string{"ping", "-t", "localhost"}
}
imagePullTest := func(ctx context.Context, image string, hasSecret bool, expectedPhase v1.PodPhase, expectedPullStatus bool) {
container := ConformanceContainer{
PodClient: e2epod.NewPodClient(f),
Container: v1.Container{
Name: "image-pull-test",
Image: image,
Command: command,
ImagePullPolicy: v1.PullAlways,
},
RestartPolicy: v1.RestartPolicyNever,
}
if hasSecret {
// The service account only has pull permission
auth := `
{
"auths": {
"https://gcr.io": {
"auth": "X2pzb25fa2V5OnsKICAidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAogICJwcm9qZWN0X2lkIjogImF1dGhlbnRpY2F0ZWQtaW1hZ2UtcHVsbGluZyIsCiAgInByaXZhdGVfa2V5X2lkIjogImI5ZjJhNjY0YWE5YjIwNDg0Y2MxNTg2MDYzZmVmZGExOTIyNGFjM2IiLAogICJwcml2YXRlX2tleSI6ICItLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzdTSG5LVEVFaVlMamZcbkpmQVBHbUozd3JCY2VJNTBKS0xxS21GWE5RL3REWGJRK2g5YVl4aldJTDhEeDBKZTc0bVovS01uV2dYRjVLWlNcbm9BNktuSU85Yi9SY1NlV2VpSXRSekkzL1lYVitPNkNjcmpKSXl4anFWam5mVzJpM3NhMzd0OUE5VEZkbGZycm5cbjR6UkpiOWl4eU1YNGJMdHFGR3ZCMDNOSWl0QTNzVlo1ODhrb1FBZmgzSmhhQmVnTWorWjRSYko0aGVpQlFUMDNcbnZVbzViRWFQZVQ5RE16bHdzZWFQV2dydDZOME9VRGNBRTl4bGNJek11MjUzUG4vSzgySFpydEx4akd2UkhNVXhcbng0ZjhwSnhmQ3h4QlN3Z1NORit3OWpkbXR2b0wwRmE3ZGducFJlODZWRDY2ejNZenJqNHlLRXRqc2hLZHl5VWRcbkl5cVhoN1JSQWdNQkFBRUNnZ0VBT3pzZHdaeENVVlFUeEFka2wvSTVTRFVidi9NazRwaWZxYjJEa2FnbmhFcG9cbjFJajJsNGlWMTByOS9uenJnY2p5VlBBd3pZWk1JeDFBZVF0RDdoUzRHWmFweXZKWUc3NkZpWFpQUm9DVlB6b3VcbmZyOGRDaWFwbDV0enJDOWx2QXNHd29DTTdJWVRjZmNWdDdjRTEyRDNRS3NGNlo3QjJ6ZmdLS251WVBmK0NFNlRcbmNNMHkwaCtYRS9kMERvSERoVy96YU1yWEhqOFRvd2V1eXRrYmJzNGYvOUZqOVBuU2dET1lQd2xhbFZUcitGUWFcbkpSd1ZqVmxYcEZBUW14M0Jyd25rWnQzQ2lXV2lGM2QrSGk5RXRVYnRWclcxYjZnK1JRT0licWFtcis4YlJuZFhcbjZWZ3FCQWtKWjhSVnlkeFVQMGQxMUdqdU9QRHhCbkhCbmM0UW9rSXJFUUtCZ1FEMUNlaWN1ZGhXdGc0K2dTeGJcbnplanh0VjFONDFtZHVjQnpvMmp5b1dHbzNQVDh3ckJPL3lRRTM0cU9WSi9pZCs4SThoWjRvSWh1K0pBMDBzNmdcblRuSXErdi9kL1RFalk4MW5rWmlDa21SUFdiWHhhWXR4UjIxS1BYckxOTlFKS2ttOHRkeVh5UHFsOE1veUdmQ1dcbjJ2aVBKS05iNkhabnY5Q3lqZEo5ZzJMRG5RS0JnUUREcVN2eURtaGViOTIzSW96NGxlZ01SK205Z2xYVWdTS2dcbkVzZlllbVJmbU5XQitDN3ZhSXlVUm1ZNU55TXhmQlZXc3dXRldLYXhjK0krYnFzZmx6elZZdFpwMThNR2pzTURcbmZlZWZBWDZCWk1zVXQ3Qmw3WjlWSjg1bnRFZHFBQ0xwWitaLzN0SVJWdWdDV1pRMWhrbmxHa0dUMDI0SkVFKytcbk55SDFnM2QzUlFLQmdRQ1J2MXdKWkkwbVBsRklva0tGTkh1YTBUcDNLb1JTU1hzTURTVk9NK2xIckcxWHJtRjZcbkMwNGNTKzQ0N0dMUkxHOFVUaEpKbTRxckh0Ti9aK2dZOTYvMm1xYjRIakpORDM3TVhKQnZFYTN5ZUxTOHEvK1JcbjJGOU1LamRRaU5LWnhQcG84VzhOSlREWTVOa1BaZGh4a2pzSHdVNGRTNjZwMVRESUU0MGd0TFpaRFFLQmdGaldcbktyblFpTnEzOS9iNm5QOFJNVGJDUUFKbmR3anhTUU5kQTVmcW1rQTlhRk9HbCtqamsxQ1BWa0tNSWxLSmdEYkpcbk9heDl2OUc2Ui9NSTFIR1hmV3QxWU56VnRocjRIdHNyQTB0U3BsbWhwZ05XRTZWejZuQURqdGZQSnMyZUdqdlhcbmpQUnArdjhjY21MK3dTZzhQTGprM3ZsN2VlNXJsWWxNQndNdUdjUHhBb0dBZWRueGJXMVJMbVZubEFpSEx1L0xcbmxtZkF3RFdtRWlJMFVnK1BMbm9Pdk81dFE1ZDRXMS94RU44bFA0cWtzcGtmZk1Rbk5oNFNZR0VlQlQzMlpxQ1RcbkpSZ2YwWGpveXZ2dXA5eFhqTWtYcnBZL3ljMXpmcVRaQzBNTzkvMVVjMWJSR2RaMmR5M2xSNU5XYXA3T1h5Zk9cblBQcE5Gb1BUWGd2M3FDcW5sTEhyR3pNPVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwKICAiY2xpZW50X2VtYWlsIjogImltYWdlLXB1bGxpbmdAYXV0aGVudGljYXRlZC1pbWFnZS1wdWxsaW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAiY2xpZW50X2lkIjogIjExMzc5NzkxNDUzMDA3MzI3ODcxMiIsCiAgImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKICAidG9rZW5fdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsCiAgImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAogICJjbGllbnRfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9yb2JvdC92MS9tZXRhZGF0YS94NTA5L2ltYWdlLXB1bGxpbmclNDBhdXRoZW50aWNhdGVkLWltYWdlLXB1bGxpbmcuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iCn0=",
"email": "image-pulling@authenticated-image-pulling.iam.gserviceaccount.com"
}
}
}`
secret := e2eregistry.User1DockerSecret(registryAddress)
secret.Name = "image-pull-secret-" + string(uuid.NewUUID())
// we might be told to use a different docker config JSON.
if framework.TestContext.DockerConfigFile != "" {
contents, err := os.ReadFile(framework.TestContext.DockerConfigFile)
framework.ExpectNoError(err)
auth = string(contents)
secret.Data[v1.DockerConfigJsonKey] = contents
}
secret := &v1.Secret{
Data: map[string][]byte{v1.DockerConfigJsonKey: []byte(auth)},
Type: v1.SecretTypeDockerConfigJson,
}
secret.Name = "image-pull-secret-" + string(uuid.NewUUID())
ginkgo.By("create image pull secret")
_, err := f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Create(ctx, secret, metav1.CreateOptions{})
framework.ExpectNoError(err)
ginkgo.DeferCleanup(f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Delete, secret.Name, metav1.DeleteOptions{})
ginkgo.DeferCleanup(framework.IgnoreNotFound(f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Delete), secret.Name, metav1.DeleteOptions{})
container.ImagePullSecrets = []string{secret.Name}
}
// checkContainerStatus checks whether the container status matches expectation.
@@ -323,6 +319,7 @@ while true; do sleep 1; done
}
if expectedPullStatus {
if status.State.Waiting == nil {
gomega.Expect(status.State.Running).To(gomega.BeNil())
return fmt.Errorf("expected container state: Waiting, got: %q",
GetContainerState(status.State))
}
@@ -370,28 +367,22 @@ while true; do sleep 1; done
f.It("should not be able to pull image from invalid registry", f.WithNodeConformance(), func(ctx context.Context) {
image := imageutils.GetE2EImage(imageutils.InvalidRegistryImage)
imagePullTest(ctx, image, false, v1.PodPending, true, false)
imagePullTest(ctx, image, false, v1.PodPending, true)
})
f.It("should be able to pull image", f.WithNodeConformance(), func(ctx context.Context) {
// NOTE(claudiub): The agnhost image is supposed to work on both Linux and Windows.
image := imageutils.GetE2EImage(imageutils.Agnhost)
imagePullTest(ctx, image, false, v1.PodRunning, false, false)
imagePullTest(ctx, image, false, v1.PodRunning, false)
})
f.It("should not be able to pull from private registry without secret", f.WithNodeConformance(), func(ctx context.Context) {
image := imageutils.GetE2EImage(imageutils.AuthenticatedAlpine)
imagePullTest(ctx, image, false, v1.PodPending, true, false)
image := registryAddress + "/pause:testing"
imagePullTest(ctx, image, false, v1.PodPending, true)
})
f.It("should be able to pull from private registry with secret", f.WithNodeConformance(), func(ctx context.Context) {
image := imageutils.GetE2EImage(imageutils.AuthenticatedAlpine)
isWindows := false
if framework.NodeOSDistroIs("windows") {
image = imageutils.GetE2EImage(imageutils.AuthenticatedWindowsNanoServer)
isWindows = true
}
imagePullTest(ctx, image, true, v1.PodRunning, false, isWindows)
image := registryAddress + "/pause:testing"
imagePullTest(ctx, image, true, v1.PodRunning, false)
})
})
})

View File

@@ -0,0 +1,14 @@
rules:
# Using k8s packages, other framework helpers and the public API should be good enough.
#
# public API
- selectorRegexp: ^k8s[.]io/(api|apimachinery|client-go|component-base|klog|pod-security-admission|utils)
allowedPrefixes: [ "" ]
# test helpers
- selectorRegexp: ^k8s[.]io/kubernetes/test
allowedPrefixes: [ "" ]
# stdlib
- selectorRegexp: ^[a-z]+(/|$)
allowedPrefixes: [ "" ]

View File

@@ -0,0 +1,104 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"context"
"fmt"
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/pod-security-admission/api"
"k8s.io/pod-security-admission/test"
"k8s.io/utils/ptr"
"k8s.io/kubernetes/test/e2e/framework"
e2edaemonset "k8s.io/kubernetes/test/e2e/framework/daemonset"
imageutils "k8s.io/kubernetes/test/utils/image"
)
const (
dockerCredsFmt = `{
"auths": {
"%s": {
"auth": "%s"
}
}
}`
user1creds = "dXNlcjpwYXNzd29yZA==" // user:password
)
func SetupRegistry(ctx context.Context, f *framework.Framework) ([]string, error) {
pod, err := test.GetMinimalValidLinuxPod(api.LevelRestricted, api.MajorMinorVersion(1, 22))
if err != nil {
return nil, err
}
pod.Spec.InitContainers = nil
pod.Spec.Containers[0].Name = "registry"
pod.Spec.Containers[0].Image = imageutils.GetE2EImage(imageutils.Agnhost)
pod.Spec.Containers[0].ImagePullPolicy = v1.PullIfNotPresent
pod.Spec.Containers[0].Args = []string{"fake-registry-server", "--private"}
pod.Spec.Containers[0].Ports = []v1.ContainerPort{
{
Name: "http",
ContainerPort: 5000,
HostPort: 5000,
},
}
pod.Spec.Containers[0].SecurityContext.RunAsUser = ptr.To[int64](5123)
podTestLabel := "test-registry-pod-" + f.UniqueName
labels := map[string]string{"kube-e2e": podTestLabel}
daemonset := e2edaemonset.NewDaemonSet("", "", labels, nil, nil, nil)
daemonset.GenerateName = "test-registry-"
daemonset.Spec.Template.Spec = pod.Spec
daemonset, err = f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Create(ctx, daemonset, metav1.CreateOptions{})
if err != nil {
return nil, err
}
err = wait.PollUntilContextTimeout(ctx, 1*time.Second, 120*time.Second, true, func(ctx context.Context) (done bool, err error) {
return e2edaemonset.CheckRunningOnAllNodes(ctx, f, daemonset)
})
if err != nil {
return nil, err
}
pods, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).List(ctx, metav1.ListOptions{LabelSelector: "kube-e2e=" + podTestLabel})
if err != nil {
return nil, err
}
podNodes := make([]string, 0, len(pods.Items))
for _, pod := range pods.Items {
podNodes = append(podNodes, pod.Spec.NodeName)
}
return podNodes, nil
}
func User1DockerSecret(registryAddress string) *v1.Secret {
return &v1.Secret{
Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
v1.DockerConfigJsonKey: fmt.Appendf(nil, dockerCredsFmt, "http://"+registryAddress, user1creds),
},
}
}

View File

@@ -3,8 +3,6 @@
# We are aiming to consolidate on: registry.k8s.io/e2e-test-images/agnhost
# The sources for which are in test/images/agnhost.
# If agnhost is missing functionality for your tests, please reach out to SIG Testing.
gcr.io/authenticated-image-pulling/alpine
gcr.io/authenticated-image-pulling/windows-nanoserver
gcr.io/k8s-authenticated-test/agnhost
invalid.registry.k8s.io/invalid/alpine
registry.k8s.io/build-image/distroless-iptables

View File

@@ -129,7 +129,6 @@ func readFromURL(url string, writer io.Writer) error {
var (
initRegistry = RegistryList{
GcAuthenticatedRegistry: "gcr.io/authenticated-image-pulling",
PromoterE2eRegistry: "registry.k8s.io/e2e-test-images",
BuildImageRegistry: "registry.k8s.io/build-image",
InvalidRegistry: "invalid.registry.k8s.io/invalid",
@@ -159,10 +158,6 @@ const (
APIServer
// AppArmorLoader image
AppArmorLoader
// AuthenticatedAlpine image
AuthenticatedAlpine
// AuthenticatedWindowsNanoServer image
AuthenticatedWindowsNanoServer
// BusyBox image
BusyBox
// DistrolessIptables Image
@@ -213,10 +208,8 @@ const (
func initImageConfigs(list RegistryList) (map[ImageID]Config, map[ImageID]Config) {
configs := map[ImageID]Config{}
configs[AgnhostPrev] = Config{list.PromoterE2eRegistry, "agnhost", "2.55"}
configs[Agnhost] = Config{list.PromoterE2eRegistry, "agnhost", "2.56"}
configs[Agnhost] = Config{list.PromoterE2eRegistry, "agnhost", "2.57"}
configs[AgnhostPrivate] = Config{list.PrivateRegistry, "agnhost", "2.6"}
configs[AuthenticatedAlpine] = Config{list.GcAuthenticatedRegistry, "alpine", "3.7"}
configs[AuthenticatedWindowsNanoServer] = Config{list.GcAuthenticatedRegistry, "windows-nanoserver", "v1"}
configs[APIServer] = Config{list.PromoterE2eRegistry, "sample-apiserver", "1.29.2"}
configs[AppArmorLoader] = Config{list.PromoterE2eRegistry, "apparmor-loader", "1.4"}
configs[BusyBox] = Config{list.PromoterE2eRegistry, "busybox", "1.37.0-1"}
@@ -263,8 +256,7 @@ func GetMappedImageConfigs(originalImageConfigs map[ImageID]Config, repo string)
configs := make(map[ImageID]Config)
for i, config := range originalImageConfigs {
switch i {
case InvalidRegistryImage, AuthenticatedAlpine,
AuthenticatedWindowsNanoServer, AgnhostPrivate:
case InvalidRegistryImage, AgnhostPrivate:
// These images are special and can't be run out of the cloud - some because they
// are authenticated, and others because they are not real images. Tests that depend
// on these images can't be run without access to the public internet.