k8s.io/kubernetes@v1.29.3/test/e2e/common/node/runtime.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package node
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"time"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/util/uuid"
    29  	"k8s.io/kubernetes/pkg/kubelet/images"
    30  	"k8s.io/kubernetes/test/e2e/framework"
    31  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    32  	imageutils "k8s.io/kubernetes/test/utils/image"
    33  	admissionapi "k8s.io/pod-security-admission/api"
    34  
    35  	"github.com/onsi/ginkgo/v2"
    36  	"github.com/onsi/gomega"
    37  	gomegatypes "github.com/onsi/gomega/types"
    38  )
    39  
    40  var _ = SIGDescribe("Container Runtime", func() {
    41  	f := framework.NewDefaultFramework("container-runtime")
    42  	f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
    43  
    44  	ginkgo.Describe("blackbox test", func() {
    45  		ginkgo.Context("when starting a container that exits", func() {
    46  
    47  			/*
    48  				Release: v1.13
    49  				Testname: Container Runtime, Restart Policy, Pod Phases
    50  				Description: If the restart policy is set to 'Always', Pod MUST be restarted when terminated, If restart policy is 'OnFailure', Pod MUST be started only if it is terminated with non-zero exit code. If the restart policy is 'Never', Pod MUST never be restarted. All these three test cases MUST verify the restart counts accordingly.
    51  			*/
    52  			framework.ConformanceIt("should run with the expected status", f.WithNodeConformance(), func(ctx context.Context) {
    53  				restartCountVolumeName := "restart-count"
    54  				restartCountVolumePath := "/restart-count"
    55  				testContainer := v1.Container{
    56  					Image: framework.BusyBoxImage,
    57  					VolumeMounts: []v1.VolumeMount{
    58  						{
    59  							MountPath: restartCountVolumePath,
    60  							Name:      restartCountVolumeName,
    61  						},
    62  					},
    63  				}
    64  				testVolumes := []v1.Volume{
    65  					{
    66  						Name: restartCountVolumeName,
    67  						VolumeSource: v1.VolumeSource{
    68  							EmptyDir: &v1.EmptyDirVolumeSource{Medium: v1.StorageMediumMemory},
    69  						},
    70  					},
    71  				}
    72  				testCases := []struct {
    73  					Name          string
    74  					RestartPolicy v1.RestartPolicy
    75  					Phase         v1.PodPhase
    76  					State         ContainerState
    77  					RestartCount  int32
    78  					Ready         bool
    79  				}{
    80  					{"terminate-cmd-rpa", v1.RestartPolicyAlways, v1.PodRunning, ContainerStateRunning, 2, true},
    81  					{"terminate-cmd-rpof", v1.RestartPolicyOnFailure, v1.PodSucceeded, ContainerStateTerminated, 1, false},
    82  					{"terminate-cmd-rpn", v1.RestartPolicyNever, v1.PodFailed, ContainerStateTerminated, 0, false},
    83  				}
    84  				for _, testCase := range testCases {
    85  
    86  					// It failed at the 1st run, then succeeded at 2nd run, then run forever
    87  					cmdScripts := `
    88  f=%s
    89  count=$(echo 'hello' >> $f ; wc -l $f | awk {'print $1'})
    90  if [ $count -eq 1 ]; then
    91  	exit 1
    92  fi
    93  if [ $count -eq 2 ]; then
    94  	exit 0
    95  fi
    96  while true; do sleep 1; done
    97  `
    98  					tmpCmd := fmt.Sprintf(cmdScripts, path.Join(restartCountVolumePath, "restartCount"))
    99  					testContainer.Name = testCase.Name
   100  					testContainer.Command = []string{"sh", "-c", tmpCmd}
   101  					terminateContainer := ConformanceContainer{
   102  						PodClient:     e2epod.NewPodClient(f),
   103  						Container:     testContainer,
   104  						RestartPolicy: testCase.RestartPolicy,
   105  						Volumes:       testVolumes,
   106  					}
   107  					terminateContainer.Create(ctx)
   108  					ginkgo.DeferCleanup(framework.IgnoreNotFound(terminateContainer.Delete))
   109  
   110  					ginkgo.By(fmt.Sprintf("Container '%s': should get the expected 'RestartCount'", testContainer.Name))
   111  					gomega.Eventually(ctx, func() (int32, error) {
   112  						status, err := terminateContainer.GetStatus(ctx)
   113  						return status.RestartCount, err
   114  					}, ContainerStatusRetryTimeout, ContainerStatusPollInterval).Should(gomega.Equal(testCase.RestartCount))
   115  
   116  					ginkgo.By(fmt.Sprintf("Container '%s': should get the expected 'Phase'", testContainer.Name))
   117  					gomega.Eventually(ctx, terminateContainer.GetPhase, ContainerStatusRetryTimeout, ContainerStatusPollInterval).Should(gomega.Equal(testCase.Phase))
   118  
   119  					ginkgo.By(fmt.Sprintf("Container '%s': should get the expected 'Ready' condition", testContainer.Name))
   120  					isReady, err := terminateContainer.IsReady(ctx)
   121  					gomega.Expect(isReady).To(gomega.Equal(testCase.Ready))
   122  					framework.ExpectNoError(err)
   123  
   124  					status, err := terminateContainer.GetStatus(ctx)
   125  					framework.ExpectNoError(err)
   126  
   127  					ginkgo.By(fmt.Sprintf("Container '%s': should get the expected 'State'", testContainer.Name))
   128  					gomega.Expect(GetContainerState(status.State)).To(gomega.Equal(testCase.State))
   129  
   130  					ginkgo.By(fmt.Sprintf("Container '%s': should be possible to delete", testContainer.Name))
   131  					gomega.Expect(terminateContainer.Delete(ctx)).To(gomega.Succeed())
   132  					gomega.Eventually(ctx, terminateContainer.Present, ContainerStatusRetryTimeout, ContainerStatusPollInterval).Should(gomega.BeFalse())
   133  				}
   134  			})
   135  		})
   136  
   137  		ginkgo.Context("on terminated container", func() {
   138  			rootUser := int64(0)
   139  			nonRootUser := int64(10000)
   140  			adminUserName := "ContainerAdministrator"
   141  			nonAdminUserName := "ContainerUser"
   142  
   143  			// Create and then terminate the container under defined PodPhase to verify if termination message matches the expected output. Lastly delete the created container.
   144  			matchTerminationMessage := func(ctx context.Context, container v1.Container, expectedPhase v1.PodPhase, expectedMsg gomegatypes.GomegaMatcher) {
   145  				container.Name = "termination-message-container"
   146  				c := ConformanceContainer{
   147  					PodClient:     e2epod.NewPodClient(f),
   148  					Container:     container,
   149  					RestartPolicy: v1.RestartPolicyNever,
   150  				}
   151  
   152  				ginkgo.By("create the container")
   153  				c.Create(ctx)
   154  				ginkgo.DeferCleanup(framework.IgnoreNotFound(c.Delete))
   155  
   156  				ginkgo.By(fmt.Sprintf("wait for the container to reach %s", expectedPhase))
   157  				gomega.Eventually(ctx, c.GetPhase, ContainerStatusRetryTimeout, ContainerStatusPollInterval).Should(gomega.Equal(expectedPhase))
   158  
   159  				ginkgo.By("get the container status")
   160  				status, err := c.GetStatus(ctx)
   161  				framework.ExpectNoError(err)
   162  
   163  				ginkgo.By("the container should be terminated")
   164  				gomega.Expect(GetContainerState(status.State)).To(gomega.Equal(ContainerStateTerminated))
   165  
   166  				ginkgo.By("the termination message should be set")
   167  				framework.Logf("Expected: %v to match Container's Termination Message: %v --", expectedMsg, status.State.Terminated.Message)
   168  				gomega.Expect(status.State.Terminated.Message).Should(expectedMsg)
   169  
   170  				ginkgo.By("delete the container")
   171  				gomega.Expect(c.Delete(ctx)).To(gomega.Succeed())
   172  			}
   173  
   174  			f.It("should report termination message if TerminationMessagePath is set", f.WithNodeConformance(), func(ctx context.Context) {
   175  				container := v1.Container{
   176  					Image:                  framework.BusyBoxImage,
   177  					Command:                []string{"/bin/sh", "-c"},
   178  					Args:                   []string{"/bin/echo -n DONE > /dev/termination-log"},
   179  					TerminationMessagePath: "/dev/termination-log",
   180  					SecurityContext:        &v1.SecurityContext{},
   181  				}
   182  				if framework.NodeOSDistroIs("windows") {
   183  					container.SecurityContext.WindowsOptions = &v1.WindowsSecurityContextOptions{RunAsUserName: &adminUserName}
   184  				} else {
   185  					container.SecurityContext.RunAsUser = &rootUser
   186  				}
   187  				matchTerminationMessage(ctx, container, v1.PodSucceeded, gomega.Equal("DONE"))
   188  			})
   189  
   190  			/*
   191  				Release: v1.15
   192  				Testname: Container Runtime, TerminationMessagePath, non-root user and non-default path
   193  				Description: Create a pod with a container to run it as a non-root user with a custom TerminationMessagePath set. Pod redirects the output to the provided path successfully. When the container is terminated, the termination message MUST match the expected output logged in the provided custom path.
   194  			*/
   195  			framework.ConformanceIt("should report termination message if TerminationMessagePath is set as non-root user and at a non-default path", f.WithNodeConformance(), func(ctx context.Context) {
   196  				container := v1.Container{
   197  					Image:                  framework.BusyBoxImage,
   198  					Command:                []string{"/bin/sh", "-c"},
   199  					Args:                   []string{"/bin/echo -n DONE > /dev/termination-custom-log"},
   200  					TerminationMessagePath: "/dev/termination-custom-log",
   201  					SecurityContext:        &v1.SecurityContext{},
   202  				}
   203  				if framework.NodeOSDistroIs("windows") {
   204  					container.SecurityContext.WindowsOptions = &v1.WindowsSecurityContextOptions{RunAsUserName: &nonAdminUserName}
   205  				} else {
   206  					container.SecurityContext.RunAsUser = &nonRootUser
   207  				}
   208  				matchTerminationMessage(ctx, container, v1.PodSucceeded, gomega.Equal("DONE"))
   209  			})
   210  
   211  			/*
   212  				Release: v1.15
   213  				Testname: Container Runtime, TerminationMessage, from container's log output of failing container
   214  				Description: Create a pod with an container. Container's output is recorded in log and container exits with an error. When container is terminated, termination message MUST match the expected output recorded from container's log.
   215  			*/
   216  			framework.ConformanceIt("should report termination message from log output if TerminationMessagePolicy FallbackToLogsOnError is set", f.WithNodeConformance(), func(ctx context.Context) {
   217  				container := v1.Container{
   218  					Image:                    framework.BusyBoxImage,
   219  					Command:                  []string{"/bin/sh", "-c"},
   220  					Args:                     []string{"/bin/echo -n DONE; /bin/false"},
   221  					TerminationMessagePath:   "/dev/termination-log",
   222  					TerminationMessagePolicy: v1.TerminationMessageFallbackToLogsOnError,
   223  				}
   224  				matchTerminationMessage(ctx, container, v1.PodFailed, gomega.Equal("DONE"))
   225  			})
   226  
   227  			/*
   228  				Release: v1.15
   229  				Testname: Container Runtime, TerminationMessage, from log output of succeeding container
   230  				Description: Create a pod with an container. Container's output is recorded in log and container exits successfully without an error. When container is terminated, terminationMessage MUST have no content as container succeed.
   231  			*/
   232  			framework.ConformanceIt("should report termination message as empty when pod succeeds and TerminationMessagePolicy FallbackToLogsOnError is set", f.WithNodeConformance(), func(ctx context.Context) {
   233  				container := v1.Container{
   234  					Image:                    framework.BusyBoxImage,
   235  					Command:                  []string{"/bin/sh", "-c"},
   236  					Args:                     []string{"/bin/echo -n DONE; /bin/true"},
   237  					TerminationMessagePath:   "/dev/termination-log",
   238  					TerminationMessagePolicy: v1.TerminationMessageFallbackToLogsOnError,
   239  				}
   240  				matchTerminationMessage(ctx, container, v1.PodSucceeded, gomega.Equal(""))
   241  			})
   242  
   243  			/*
   244  				Release: v1.15
   245  				Testname: Container Runtime, TerminationMessage, from file of succeeding container
   246  				Description: Create a pod with an container. Container's output is recorded in a file and the container exits successfully without an error. When container is terminated, terminationMessage MUST match with the content from file.
   247  			*/
   248  			framework.ConformanceIt("should report termination message from file when pod succeeds and TerminationMessagePolicy FallbackToLogsOnError is set", f.WithNodeConformance(), func(ctx context.Context) {
   249  				container := v1.Container{
   250  					Image:                    framework.BusyBoxImage,
   251  					Command:                  []string{"/bin/sh", "-c"},
   252  					Args:                     []string{"/bin/echo -n OK > /dev/termination-log; /bin/echo DONE; /bin/true"},
   253  					TerminationMessagePath:   "/dev/termination-log",
   254  					TerminationMessagePolicy: v1.TerminationMessageFallbackToLogsOnError,
   255  				}
   256  				matchTerminationMessage(ctx, container, v1.PodSucceeded, gomega.Equal("OK"))
   257  			})
   258  		})
   259  
   260  		ginkgo.Context("when running a container with a new image", func() {
   261  
   262  			// Images used for ConformanceContainer are not added into NodePrePullImageList, because this test is
   263  			// testing image pulling, these images don't need to be prepulled. The ImagePullPolicy
   264  			// is v1.PullAlways, so it won't be blocked by framework image pre-pull list check.
   265  			imagePullTest := func(ctx context.Context, image string, hasSecret bool, expectedPhase v1.PodPhase, expectedPullStatus bool, windowsImage bool) {
   266  				command := []string{"/bin/sh", "-c", "while true; do sleep 1; done"}
   267  				if windowsImage {
   268  					// -t: Ping the specified host until stopped.
   269  					command = []string{"ping", "-t", "localhost"}
   270  				}
   271  				container := ConformanceContainer{
   272  					PodClient: e2epod.NewPodClient(f),
   273  					Container: v1.Container{
   274  						Name:            "image-pull-test",
   275  						Image:           image,
   276  						Command:         command,
   277  						ImagePullPolicy: v1.PullAlways,
   278  					},
   279  					RestartPolicy: v1.RestartPolicyNever,
   280  				}
   281  				if hasSecret {
   282  					// The service account only has pull permission
   283  					auth := `
   284  {
   285  	"auths": {
   286  		"https://gcr.io": {
   287  			"auth": "X2pzb25fa2V5OnsKICAidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAogICJwcm9qZWN0X2lkIjogImF1dGhlbnRpY2F0ZWQtaW1hZ2UtcHVsbGluZyIsCiAgInByaXZhdGVfa2V5X2lkIjogImI5ZjJhNjY0YWE5YjIwNDg0Y2MxNTg2MDYzZmVmZGExOTIyNGFjM2IiLAogICJwcml2YXRlX2tleSI6ICItLS0tLUJFR0lOIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzdTSG5LVEVFaVlMamZcbkpmQVBHbUozd3JCY2VJNTBKS0xxS21GWE5RL3REWGJRK2g5YVl4aldJTDhEeDBKZTc0bVovS01uV2dYRjVLWlNcbm9BNktuSU85Yi9SY1NlV2VpSXRSekkzL1lYVitPNkNjcmpKSXl4anFWam5mVzJpM3NhMzd0OUE5VEZkbGZycm5cbjR6UkpiOWl4eU1YNGJMdHFGR3ZCMDNOSWl0QTNzVlo1ODhrb1FBZmgzSmhhQmVnTWorWjRSYko0aGVpQlFUMDNcbnZVbzViRWFQZVQ5RE16bHdzZWFQV2dydDZOME9VRGNBRTl4bGNJek11MjUzUG4vSzgySFpydEx4akd2UkhNVXhcbng0ZjhwSnhmQ3h4QlN3Z1NORit3OWpkbXR2b0wwRmE3ZGducFJlODZWRDY2ejNZenJqNHlLRXRqc2hLZHl5VWRcbkl5cVhoN1JSQWdNQkFBRUNnZ0VBT3pzZHdaeENVVlFUeEFka2wvSTVTRFVidi9NazRwaWZxYjJEa2FnbmhFcG9cbjFJajJsNGlWMTByOS9uenJnY2p5VlBBd3pZWk1JeDFBZVF0RDdoUzRHWmFweXZKWUc3NkZpWFpQUm9DVlB6b3VcbmZyOGRDaWFwbDV0enJDOWx2QXNHd29DTTdJWVRjZmNWdDdjRTEyRDNRS3NGNlo3QjJ6ZmdLS251WVBmK0NFNlRcbmNNMHkwaCtYRS9kMERvSERoVy96YU1yWEhqOFRvd2V1eXRrYmJzNGYvOUZqOVBuU2dET1lQd2xhbFZUcitGUWFcbkpSd1ZqVmxYcEZBUW14M0Jyd25rWnQzQ2lXV2lGM2QrSGk5RXRVYnRWclcxYjZnK1JRT0licWFtcis4YlJuZFhcbjZWZ3FCQWtKWjhSVnlkeFVQMGQxMUdqdU9QRHhCbkhCbmM0UW9rSXJFUUtCZ1FEMUNlaWN1ZGhXdGc0K2dTeGJcbnplanh0VjFONDFtZHVjQnpvMmp5b1dHbzNQVDh3ckJPL3lRRTM0cU9WSi9pZCs4SThoWjRvSWh1K0pBMDBzNmdcblRuSXErdi9kL1RFalk4MW5rWmlDa21SUFdiWHhhWXR4UjIxS1BYckxOTlFKS2ttOHRkeVh5UHFsOE1veUdmQ1dcbjJ2aVBKS05iNkhabnY5Q3lqZEo5ZzJMRG5RS0JnUUREcVN2eURtaGViOTIzSW96NGxlZ01SK205Z2xYVWdTS2dcbkVzZlllbVJmbU5XQitDN3ZhSXlVUm1ZNU55TXhmQlZXc3dXRldLYXhjK0krYnFzZmx6elZZdFpwMThNR2pzTURcbmZlZWZBWDZCWk1zVXQ3Qmw3WjlWSjg1bnRFZHFBQ0xwWitaLzN0SVJWdWdDV1pRMWhrbmxHa0dUMDI0SkVFKytcbk55SDFnM2QzUlFLQmdRQ1J2MXdKWkkwbVBsRklva0tGTkh1YTBUcDNLb1JTU1hzTURTVk9NK2xIckcxWHJtRjZcbkMwNGNTKzQ0N0dMUkxHOFVUaEpKbTRxckh0Ti9aK2dZOTYvMm1xYjRIakpORDM3TVhKQnZFYTN5ZUxTOHEvK1JcbjJGOU1LamRRaU5LWnhQcG84VzhOSlREWTVOa1BaZGh4a2pzSHdVNGRTNjZwMVRESUU0MGd0TFpaRFFLQmdGaldcbktyblFpTnEzOS9iNm5QOFJNVGJDUUFKbmR3anhTUU5kQTVmcW1rQTlhRk9HbCtqamsxQ1BWa0tNSWxLSmdEYkpcbk9heDl2OUc2Ui9NSTFIR1hmV3QxWU56VnRocjRIdHNyQTB0U3BsbWhwZ05XRTZWejZuQURqdGZQSnMyZUdqdlhcbmpQUnArdjhjY21MK3dTZzhQTGprM3ZsN2VlNXJsWWxNQndNdUdjUHhBb0dBZWRueGJXMVJMbVZubEFpSEx1L0xcbmxtZkF3RFdtRWlJMFVnK1BMbm9Pdk81dFE1ZDRXMS94RU44bFA0cWtzcGtmZk1Rbk5oNFNZR0VlQlQzMlpxQ1RcbkpSZ2YwWGpveXZ2dXA5eFhqTWtYcnBZL3ljMXpmcVRaQzBNTzkvMVVjMWJSR2RaMmR5M2xSNU5XYXA3T1h5Zk9cblBQcE5Gb1BUWGd2M3FDcW5sTEhyR3pNPVxuLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLVxuIiwKICAiY2xpZW50X2VtYWlsIjogImltYWdlLXB1bGxpbmdAYXV0aGVudGljYXRlZC1pbWFnZS1wdWxsaW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAiY2xpZW50X2lkIjogIjExMzc5NzkxNDUzMDA3MzI3ODcxMiIsCiAgImF1dGhfdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi9hdXRoIiwKICAidG9rZW5fdXJpIjogImh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsCiAgImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHMiLAogICJjbGllbnRfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9yb2JvdC92MS9tZXRhZGF0YS94NTA5L2ltYWdlLXB1bGxpbmclNDBhdXRoZW50aWNhdGVkLWltYWdlLXB1bGxpbmcuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iCn0=",
   288  			"email": "image-pulling@authenticated-image-pulling.iam.gserviceaccount.com"
   289  		}
   290  	}
   291  }`
   292  					// we might be told to use a different docker config JSON.
   293  					if framework.TestContext.DockerConfigFile != "" {
   294  						contents, err := os.ReadFile(framework.TestContext.DockerConfigFile)
   295  						framework.ExpectNoError(err)
   296  						auth = string(contents)
   297  					}
   298  					secret := &v1.Secret{
   299  						Data: map[string][]byte{v1.DockerConfigJsonKey: []byte(auth)},
   300  						Type: v1.SecretTypeDockerConfigJson,
   301  					}
   302  					secret.Name = "image-pull-secret-" + string(uuid.NewUUID())
   303  					ginkgo.By("create image pull secret")
   304  					_, err := f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Create(ctx, secret, metav1.CreateOptions{})
   305  					framework.ExpectNoError(err)
   306  					ginkgo.DeferCleanup(f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Delete, secret.Name, metav1.DeleteOptions{})
   307  					container.ImagePullSecrets = []string{secret.Name}
   308  				}
   309  				// checkContainerStatus checks whether the container status matches expectation.
   310  				checkContainerStatus := func(ctx context.Context) error {
   311  					status, err := container.GetStatus(ctx)
   312  					if err != nil {
   313  						return fmt.Errorf("failed to get container status: %w", err)
   314  					}
   315  					// We need to check container state first. The default pod status is pending, If we check pod phase first,
   316  					// and the expected pod phase is Pending, the container status may not even show up when we check it.
   317  					// Check container state
   318  					if !expectedPullStatus {
   319  						if status.State.Running == nil {
   320  							return fmt.Errorf("expected container state: Running, got: %q",
   321  								GetContainerState(status.State))
   322  						}
   323  					}
   324  					if expectedPullStatus {
   325  						if status.State.Waiting == nil {
   326  							return fmt.Errorf("expected container state: Waiting, got: %q",
   327  								GetContainerState(status.State))
   328  						}
   329  						reason := status.State.Waiting.Reason
   330  						if reason != images.ErrImagePull.Error() &&
   331  							reason != images.ErrImagePullBackOff.Error() {
   332  							return fmt.Errorf("unexpected waiting reason: %q", reason)
   333  						}
   334  					}
   335  					// Check pod phase
   336  					phase, err := container.GetPhase(ctx)
   337  					if err != nil {
   338  						return fmt.Errorf("failed to get pod phase: %w", err)
   339  					}
   340  					if phase != expectedPhase {
   341  						return fmt.Errorf("expected pod phase: %q, got: %q", expectedPhase, phase)
   342  					}
   343  					return nil
   344  				}
   345  
   346  				// The image registry is not stable, which sometimes causes the test to fail. Add retry mechanism to make this less flaky.
   347  				const flakeRetry = 3
   348  				for i := 1; i <= flakeRetry; i++ {
   349  					var err error
   350  					ginkgo.By("create the container")
   351  					container.Create(ctx)
   352  					ginkgo.By("check the container status")
   353  					for start := time.Now(); time.Since(start) < ContainerStatusRetryTimeout; time.Sleep(ContainerStatusPollInterval) {
   354  						if err = checkContainerStatus(ctx); err == nil {
   355  							break
   356  						}
   357  					}
   358  					ginkgo.By("delete the container")
   359  					_ = container.Delete(ctx)
   360  					if err == nil {
   361  						break
   362  					}
   363  					if i < flakeRetry {
   364  						framework.Logf("No.%d attempt failed: %v, retrying...", i, err)
   365  					} else {
   366  						framework.Failf("All %d attempts failed: %v", flakeRetry, err)
   367  					}
   368  				}
   369  			}
   370  
   371  			f.It("should not be able to pull image from invalid registry", f.WithNodeConformance(), func(ctx context.Context) {
   372  				image := imageutils.GetE2EImage(imageutils.InvalidRegistryImage)
   373  				imagePullTest(ctx, image, false, v1.PodPending, true, false)
   374  			})
   375  
   376  			f.It("should be able to pull image", f.WithNodeConformance(), func(ctx context.Context) {
   377  				// NOTE(claudiub): The agnhost image is supposed to work on both Linux and Windows.
   378  				image := imageutils.GetE2EImage(imageutils.Agnhost)
   379  				imagePullTest(ctx, image, false, v1.PodRunning, false, false)
   380  			})
   381  
   382  			f.It("should not be able to pull from private registry without secret", f.WithNodeConformance(), func(ctx context.Context) {
   383  				image := imageutils.GetE2EImage(imageutils.AuthenticatedAlpine)
   384  				imagePullTest(ctx, image, false, v1.PodPending, true, false)
   385  			})
   386  
   387  			f.It("should be able to pull from private registry with secret", f.WithNodeConformance(), func(ctx context.Context) {
   388  				image := imageutils.GetE2EImage(imageutils.AuthenticatedAlpine)
   389  				isWindows := false
   390  				if framework.NodeOSDistroIs("windows") {
   391  					image = imageutils.GetE2EImage(imageutils.AuthenticatedWindowsNanoServer)
   392  					isWindows = true
   393  				}
   394  				imagePullTest(ctx, image, true, v1.PodRunning, false, isWindows)
   395  			})
   396  		})
   397  	})
   398  })