k8s.io/kubernetes@v1.29.3/pkg/kubelet/images/image_manager_test.go (about)

     1  /*
     2  Copyright 2015 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 images
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"sync"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  	v1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    32  	"k8s.io/client-go/tools/record"
    33  	"k8s.io/client-go/util/flowcontrol"
    34  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    35  	crierrors "k8s.io/cri-api/pkg/errors"
    36  	"k8s.io/kubernetes/pkg/features"
    37  	. "k8s.io/kubernetes/pkg/kubelet/container"
    38  	ctest "k8s.io/kubernetes/pkg/kubelet/container/testing"
    39  	testingclock "k8s.io/utils/clock/testing"
    40  	utilpointer "k8s.io/utils/pointer"
    41  )
    42  
    43  type pullerExpects struct {
    44  	calls                           []string
    45  	err                             error
    46  	shouldRecordStartedPullingTime  bool
    47  	shouldRecordFinishedPullingTime bool
    48  }
    49  
    50  type pullerTestCase struct {
    51  	testName       string
    52  	containerImage string
    53  	policy         v1.PullPolicy
    54  	inspectErr     error
    55  	pullerErr      error
    56  	qps            float32
    57  	burst          int
    58  	expected       []pullerExpects
    59  }
    60  
    61  func pullerTestCases() []pullerTestCase {
    62  	return []pullerTestCase{
    63  		{ // pull missing image
    64  			testName:       "image missing, pull",
    65  			containerImage: "missing_image",
    66  			policy:         v1.PullIfNotPresent,
    67  			inspectErr:     nil,
    68  			pullerErr:      nil,
    69  			qps:            0.0,
    70  			burst:          0,
    71  			expected: []pullerExpects{
    72  				{[]string{"GetImageRef", "PullImage"}, nil, true, true},
    73  			}},
    74  
    75  		{ // image present, don't pull
    76  			testName:       "image present, don't pull ",
    77  			containerImage: "present_image",
    78  			policy:         v1.PullIfNotPresent,
    79  			inspectErr:     nil,
    80  			pullerErr:      nil,
    81  			qps:            0.0,
    82  			burst:          0,
    83  			expected: []pullerExpects{
    84  				{[]string{"GetImageRef"}, nil, false, false},
    85  				{[]string{"GetImageRef"}, nil, false, false},
    86  				{[]string{"GetImageRef"}, nil, false, false},
    87  			}},
    88  		// image present, pull it
    89  		{containerImage: "present_image",
    90  			testName:   "image present, pull ",
    91  			policy:     v1.PullAlways,
    92  			inspectErr: nil,
    93  			pullerErr:  nil,
    94  			qps:        0.0,
    95  			burst:      0,
    96  			expected: []pullerExpects{
    97  				{[]string{"GetImageRef", "PullImage"}, nil, true, true},
    98  				{[]string{"GetImageRef", "PullImage"}, nil, true, true},
    99  				{[]string{"GetImageRef", "PullImage"}, nil, true, true},
   100  			}},
   101  		// missing image, error PullNever
   102  		{containerImage: "missing_image",
   103  			testName:   "image missing, never pull",
   104  			policy:     v1.PullNever,
   105  			inspectErr: nil,
   106  			pullerErr:  nil,
   107  			qps:        0.0,
   108  			burst:      0,
   109  			expected: []pullerExpects{
   110  				{[]string{"GetImageRef"}, ErrImageNeverPull, false, false},
   111  				{[]string{"GetImageRef"}, ErrImageNeverPull, false, false},
   112  				{[]string{"GetImageRef"}, ErrImageNeverPull, false, false},
   113  			}},
   114  		// missing image, unable to inspect
   115  		{containerImage: "missing_image",
   116  			testName:   "image missing, pull if not present",
   117  			policy:     v1.PullIfNotPresent,
   118  			inspectErr: errors.New("unknown inspectError"),
   119  			pullerErr:  nil,
   120  			qps:        0.0,
   121  			burst:      0,
   122  			expected: []pullerExpects{
   123  				{[]string{"GetImageRef"}, ErrImageInspect, false, false},
   124  				{[]string{"GetImageRef"}, ErrImageInspect, false, false},
   125  				{[]string{"GetImageRef"}, ErrImageInspect, false, false},
   126  			}},
   127  		// missing image, unable to fetch
   128  		{containerImage: "typo_image",
   129  			testName:   "image missing, unable to fetch",
   130  			policy:     v1.PullIfNotPresent,
   131  			inspectErr: nil,
   132  			pullerErr:  errors.New("404"),
   133  			qps:        0.0,
   134  			burst:      0,
   135  			expected: []pullerExpects{
   136  				{[]string{"GetImageRef", "PullImage"}, ErrImagePull, true, false},
   137  				{[]string{"GetImageRef", "PullImage"}, ErrImagePull, true, false},
   138  				{[]string{"GetImageRef"}, ErrImagePullBackOff, false, false},
   139  				{[]string{"GetImageRef", "PullImage"}, ErrImagePull, true, false},
   140  				{[]string{"GetImageRef"}, ErrImagePullBackOff, false, false},
   141  				{[]string{"GetImageRef"}, ErrImagePullBackOff, false, false},
   142  			}},
   143  		// image present, non-zero qps, try to pull
   144  		{containerImage: "present_image",
   145  			testName:   "image present and qps>0, pull",
   146  			policy:     v1.PullAlways,
   147  			inspectErr: nil,
   148  			pullerErr:  nil,
   149  			qps:        400.0,
   150  			burst:      600,
   151  			expected: []pullerExpects{
   152  				{[]string{"GetImageRef", "PullImage"}, nil, true, true},
   153  				{[]string{"GetImageRef", "PullImage"}, nil, true, true},
   154  				{[]string{"GetImageRef", "PullImage"}, nil, true, true},
   155  			}},
   156  		// image present, non-zero qps, try to pull when qps exceeded
   157  		{containerImage: "present_image",
   158  			testName:   "image present and excessive qps rate, pull",
   159  			policy:     v1.PullAlways,
   160  			inspectErr: nil,
   161  			pullerErr:  nil,
   162  			qps:        2000.0,
   163  			burst:      0,
   164  			expected: []pullerExpects{
   165  				{[]string{"GetImageRef"}, ErrImagePull, true, false},
   166  				{[]string{"GetImageRef"}, ErrImagePull, true, false},
   167  				{[]string{"GetImageRef"}, ErrImagePullBackOff, false, false},
   168  			}},
   169  		// error case if image name fails validation due to invalid reference format
   170  		{containerImage: "FAILED_IMAGE",
   171  			testName:   "invalid image name, no pull",
   172  			policy:     v1.PullAlways,
   173  			inspectErr: nil,
   174  			pullerErr:  nil,
   175  			qps:        0.0,
   176  			burst:      0,
   177  			expected: []pullerExpects{
   178  				{[]string(nil), ErrInvalidImageName, false, false},
   179  			}},
   180  		// error case if image name contains http
   181  		{containerImage: "http://url",
   182  			testName:   "invalid image name with http, no pull",
   183  			policy:     v1.PullAlways,
   184  			inspectErr: nil,
   185  			pullerErr:  nil,
   186  			qps:        0.0,
   187  			burst:      0,
   188  			expected: []pullerExpects{
   189  				{[]string(nil), ErrInvalidImageName, false, false},
   190  			}},
   191  		// error case if image name contains sha256
   192  		{containerImage: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
   193  			testName:   "invalid image name with sha256, no pull",
   194  			policy:     v1.PullAlways,
   195  			inspectErr: nil,
   196  			pullerErr:  nil,
   197  			qps:        0.0,
   198  			burst:      0,
   199  			expected: []pullerExpects{
   200  				{[]string(nil), ErrInvalidImageName, false, false},
   201  			}},
   202  	}
   203  }
   204  
   205  type mockPodPullingTimeRecorder struct {
   206  	sync.Mutex
   207  	startedPullingRecorded  bool
   208  	finishedPullingRecorded bool
   209  }
   210  
   211  func (m *mockPodPullingTimeRecorder) RecordImageStartedPulling(podUID types.UID) {
   212  	m.Lock()
   213  	defer m.Unlock()
   214  	m.startedPullingRecorded = true
   215  }
   216  
   217  func (m *mockPodPullingTimeRecorder) RecordImageFinishedPulling(podUID types.UID) {
   218  	m.Lock()
   219  	defer m.Unlock()
   220  	m.finishedPullingRecorded = true
   221  }
   222  
   223  func (m *mockPodPullingTimeRecorder) reset() {
   224  	m.Lock()
   225  	defer m.Unlock()
   226  	m.startedPullingRecorded = false
   227  	m.finishedPullingRecorded = false
   228  }
   229  
   230  func pullerTestEnv(t *testing.T, c pullerTestCase, serialized bool, maxParallelImagePulls *int32) (puller ImageManager, fakeClock *testingclock.FakeClock, fakeRuntime *ctest.FakeRuntime, container *v1.Container, fakePodPullingTimeRecorder *mockPodPullingTimeRecorder) {
   231  	container = &v1.Container{
   232  		Name:            "container_name",
   233  		Image:           c.containerImage,
   234  		ImagePullPolicy: c.policy,
   235  	}
   236  
   237  	backOff := flowcontrol.NewBackOff(time.Second, time.Minute)
   238  	fakeClock = testingclock.NewFakeClock(time.Now())
   239  	backOff.Clock = fakeClock
   240  
   241  	fakeRuntime = &ctest.FakeRuntime{T: t}
   242  	fakeRecorder := &record.FakeRecorder{}
   243  
   244  	fakeRuntime.ImageList = []Image{{ID: "present_image:latest"}}
   245  	fakeRuntime.Err = c.pullerErr
   246  	fakeRuntime.InspectErr = c.inspectErr
   247  
   248  	fakePodPullingTimeRecorder = &mockPodPullingTimeRecorder{}
   249  
   250  	puller = NewImageManager(fakeRecorder, fakeRuntime, backOff, serialized, maxParallelImagePulls, c.qps, c.burst, fakePodPullingTimeRecorder)
   251  	return
   252  }
   253  
   254  func TestParallelPuller(t *testing.T) {
   255  	pod := &v1.Pod{
   256  		ObjectMeta: metav1.ObjectMeta{
   257  			Name:            "test_pod",
   258  			Namespace:       "test-ns",
   259  			UID:             "bar",
   260  			ResourceVersion: "42",
   261  		}}
   262  
   263  	cases := pullerTestCases()
   264  
   265  	useSerializedEnv := false
   266  	for _, c := range cases {
   267  		t.Run(c.testName, func(t *testing.T) {
   268  			ctx := context.Background()
   269  			puller, fakeClock, fakeRuntime, container, fakePodPullingTimeRecorder := pullerTestEnv(t, c, useSerializedEnv, nil)
   270  
   271  			for _, expected := range c.expected {
   272  				fakeRuntime.CalledFunctions = nil
   273  				fakeClock.Step(time.Second)
   274  
   275  				_, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "")
   276  				fakeRuntime.AssertCalls(expected.calls)
   277  				assert.Equal(t, expected.err, err)
   278  				assert.Equal(t, expected.shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded)
   279  				assert.Equal(t, expected.shouldRecordFinishedPullingTime, fakePodPullingTimeRecorder.finishedPullingRecorded)
   280  				fakePodPullingTimeRecorder.reset()
   281  			}
   282  		})
   283  	}
   284  }
   285  
   286  func TestSerializedPuller(t *testing.T) {
   287  	pod := &v1.Pod{
   288  		ObjectMeta: metav1.ObjectMeta{
   289  			Name:            "test_pod",
   290  			Namespace:       "test-ns",
   291  			UID:             "bar",
   292  			ResourceVersion: "42",
   293  		}}
   294  
   295  	cases := pullerTestCases()
   296  
   297  	useSerializedEnv := true
   298  	for _, c := range cases {
   299  		t.Run(c.testName, func(t *testing.T) {
   300  			ctx := context.Background()
   301  			puller, fakeClock, fakeRuntime, container, fakePodPullingTimeRecorder := pullerTestEnv(t, c, useSerializedEnv, nil)
   302  
   303  			for _, expected := range c.expected {
   304  				fakeRuntime.CalledFunctions = nil
   305  				fakeClock.Step(time.Second)
   306  
   307  				_, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "")
   308  				fakeRuntime.AssertCalls(expected.calls)
   309  				assert.Equal(t, expected.err, err)
   310  				assert.Equal(t, expected.shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded)
   311  				assert.Equal(t, expected.shouldRecordFinishedPullingTime, fakePodPullingTimeRecorder.finishedPullingRecorded)
   312  				fakePodPullingTimeRecorder.reset()
   313  			}
   314  		})
   315  	}
   316  }
   317  
   318  func TestApplyDefaultImageTag(t *testing.T) {
   319  	for _, testCase := range []struct {
   320  		testName string
   321  		Input    string
   322  		Output   string
   323  	}{
   324  		{testName: "root", Input: "root", Output: "root:latest"},
   325  		{testName: "root:tag", Input: "root:tag", Output: "root:tag"},
   326  		{testName: "root@sha", Input: "root@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Output: "root@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
   327  		{testName: "root:latest@sha", Input: "root:latest@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Output: "root:latest@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
   328  		{testName: "root:latest", Input: "root:latest", Output: "root:latest"},
   329  	} {
   330  		t.Run(testCase.testName, func(t *testing.T) {
   331  			image, err := applyDefaultImageTag(testCase.Input)
   332  			if err != nil {
   333  				t.Errorf("applyDefaultImageTag(%s) failed: %v", testCase.Input, err)
   334  			} else if image != testCase.Output {
   335  				t.Errorf("Expected image reference: %q, got %q", testCase.Output, image)
   336  			}
   337  		})
   338  	}
   339  }
   340  
   341  func TestPullAndListImageWithPodAnnotations(t *testing.T) {
   342  	pod := &v1.Pod{
   343  		ObjectMeta: metav1.ObjectMeta{
   344  			Name:            "test_pod",
   345  			Namespace:       "test-ns",
   346  			UID:             "bar",
   347  			ResourceVersion: "42",
   348  			Annotations: map[string]string{
   349  				"kubernetes.io/runtimehandler": "handler_name",
   350  			},
   351  		}}
   352  	c := pullerTestCase{ // pull missing image
   353  		testName:       "test pull and list image with pod annotations",
   354  		containerImage: "missing_image",
   355  		policy:         v1.PullIfNotPresent,
   356  		inspectErr:     nil,
   357  		pullerErr:      nil,
   358  		expected: []pullerExpects{
   359  			{[]string{"GetImageRef", "PullImage"}, nil, true, true},
   360  		}}
   361  
   362  	useSerializedEnv := true
   363  	t.Run(c.testName, func(t *testing.T) {
   364  		ctx := context.Background()
   365  		puller, fakeClock, fakeRuntime, container, fakePodPullingTimeRecorder := pullerTestEnv(t, c, useSerializedEnv, nil)
   366  		fakeRuntime.CalledFunctions = nil
   367  		fakeRuntime.ImageList = []Image{}
   368  		fakeClock.Step(time.Second)
   369  
   370  		_, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "")
   371  		fakeRuntime.AssertCalls(c.expected[0].calls)
   372  		assert.Equal(t, c.expected[0].err, err, "tick=%d", 0)
   373  		assert.Equal(t, c.expected[0].shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded)
   374  		assert.Equal(t, c.expected[0].shouldRecordFinishedPullingTime, fakePodPullingTimeRecorder.finishedPullingRecorded)
   375  
   376  		images, _ := fakeRuntime.ListImages(ctx)
   377  		assert.Equal(t, 1, len(images), "ListImages() count")
   378  
   379  		image := images[0]
   380  		assert.Equal(t, "missing_image:latest", image.ID, "Image ID")
   381  		assert.Equal(t, "", image.Spec.RuntimeHandler, "image.Spec.RuntimeHandler not empty", "ImageID", image.ID)
   382  
   383  		expectedAnnotations := []Annotation{
   384  			{
   385  				Name:  "kubernetes.io/runtimehandler",
   386  				Value: "handler_name",
   387  			}}
   388  		assert.Equal(t, expectedAnnotations, image.Spec.Annotations, "image spec annotations")
   389  	})
   390  }
   391  
   392  func TestPullAndListImageWithRuntimeHandlerInImageCriAPIFeatureGate(t *testing.T) {
   393  	runtimeHandler := "handler_name"
   394  	pod := &v1.Pod{
   395  		ObjectMeta: metav1.ObjectMeta{
   396  			Name:            "test_pod",
   397  			Namespace:       "test-ns",
   398  			UID:             "bar",
   399  			ResourceVersion: "42",
   400  			Annotations: map[string]string{
   401  				"kubernetes.io/runtimehandler": runtimeHandler,
   402  			},
   403  		},
   404  		Spec: v1.PodSpec{
   405  			RuntimeClassName: &runtimeHandler,
   406  		},
   407  	}
   408  	c := pullerTestCase{ // pull missing image
   409  		testName:       "test pull and list image with pod annotations",
   410  		containerImage: "missing_image",
   411  		policy:         v1.PullIfNotPresent,
   412  		inspectErr:     nil,
   413  		pullerErr:      nil,
   414  		expected: []pullerExpects{
   415  			{[]string{"GetImageRef", "PullImage"}, nil, true, true},
   416  		}}
   417  
   418  	useSerializedEnv := true
   419  	t.Run(c.testName, func(t *testing.T) {
   420  		defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RuntimeClassInImageCriAPI, true)()
   421  		ctx := context.Background()
   422  		puller, fakeClock, fakeRuntime, container, fakePodPullingTimeRecorder := pullerTestEnv(t, c, useSerializedEnv, nil)
   423  		fakeRuntime.CalledFunctions = nil
   424  		fakeRuntime.ImageList = []Image{}
   425  		fakeClock.Step(time.Second)
   426  
   427  		_, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, runtimeHandler)
   428  		fakeRuntime.AssertCalls(c.expected[0].calls)
   429  		assert.Equal(t, c.expected[0].err, err, "tick=%d", 0)
   430  		assert.Equal(t, c.expected[0].shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded)
   431  		assert.Equal(t, c.expected[0].shouldRecordFinishedPullingTime, fakePodPullingTimeRecorder.finishedPullingRecorded)
   432  
   433  		images, _ := fakeRuntime.ListImages(ctx)
   434  		assert.Equal(t, 1, len(images), "ListImages() count")
   435  
   436  		image := images[0]
   437  		assert.Equal(t, "missing_image:latest", image.ID, "Image ID")
   438  
   439  		// when RuntimeClassInImageCriAPI feature gate is enabled, check runtime
   440  		// handler information for every image in the ListImages() response
   441  		assert.Equal(t, runtimeHandler, image.Spec.RuntimeHandler, "runtime handler returned not as expected", "Image ID", image)
   442  
   443  		expectedAnnotations := []Annotation{
   444  			{
   445  				Name:  "kubernetes.io/runtimehandler",
   446  				Value: "handler_name",
   447  			}}
   448  		assert.Equal(t, expectedAnnotations, image.Spec.Annotations, "image spec annotations")
   449  	})
   450  }
   451  
   452  func TestMaxParallelImagePullsLimit(t *testing.T) {
   453  	ctx := context.Background()
   454  	pod := &v1.Pod{
   455  		ObjectMeta: metav1.ObjectMeta{
   456  			Name:            "test_pod",
   457  			Namespace:       "test-ns",
   458  			UID:             "bar",
   459  			ResourceVersion: "42",
   460  		}}
   461  
   462  	testCase := &pullerTestCase{
   463  		containerImage: "present_image",
   464  		testName:       "image present, pull ",
   465  		policy:         v1.PullAlways,
   466  		inspectErr:     nil,
   467  		pullerErr:      nil,
   468  		qps:            0.0,
   469  		burst:          0,
   470  	}
   471  
   472  	useSerializedEnv := false
   473  	maxParallelImagePulls := 5
   474  	var wg sync.WaitGroup
   475  
   476  	puller, fakeClock, fakeRuntime, container, _ := pullerTestEnv(t, *testCase, useSerializedEnv, utilpointer.Int32Ptr(int32(maxParallelImagePulls)))
   477  	fakeRuntime.BlockImagePulls = true
   478  	fakeRuntime.CalledFunctions = nil
   479  	fakeRuntime.T = t
   480  	fakeClock.Step(time.Second)
   481  
   482  	// First 5 EnsureImageExists should result in runtime calls
   483  	for i := 0; i < maxParallelImagePulls; i++ {
   484  		wg.Add(1)
   485  		go func() {
   486  			_, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "")
   487  			assert.Nil(t, err)
   488  			wg.Done()
   489  		}()
   490  	}
   491  	time.Sleep(1 * time.Second)
   492  	fakeRuntime.AssertCallCounts("PullImage", 5)
   493  
   494  	// Next two EnsureImageExists should be blocked because maxParallelImagePulls is hit
   495  	for i := 0; i < 2; i++ {
   496  		wg.Add(1)
   497  		go func() {
   498  			_, _, err := puller.EnsureImageExists(ctx, pod, container, nil, nil, "")
   499  			assert.Nil(t, err)
   500  			wg.Done()
   501  		}()
   502  	}
   503  	time.Sleep(1 * time.Second)
   504  	fakeRuntime.AssertCallCounts("PullImage", 5)
   505  
   506  	// Unblock two image pulls from runtime, and two EnsureImageExists can go through
   507  	fakeRuntime.UnblockImagePulls(2)
   508  	time.Sleep(1 * time.Second)
   509  	fakeRuntime.AssertCallCounts("PullImage", 7)
   510  
   511  	// Unblock the remaining 5 image pulls from runtime, and all EnsureImageExists can go through
   512  	fakeRuntime.UnblockImagePulls(5)
   513  
   514  	wg.Wait()
   515  	fakeRuntime.AssertCallCounts("PullImage", 7)
   516  }
   517  
   518  func TestEvalCRIPullErr(t *testing.T) {
   519  	t.Parallel()
   520  	for _, tc := range []struct {
   521  		name   string
   522  		input  error
   523  		assert func(string, error)
   524  	}{
   525  		{
   526  			name:  "fallback error",
   527  			input: errors.New("test"),
   528  			assert: func(msg string, err error) {
   529  				assert.ErrorIs(t, err, ErrImagePull)
   530  				assert.Contains(t, msg, "test")
   531  			},
   532  		},
   533  		{
   534  			name:  "registry is unavailable",
   535  			input: crierrors.ErrRegistryUnavailable,
   536  			assert: func(msg string, err error) {
   537  				assert.ErrorIs(t, err, crierrors.ErrRegistryUnavailable)
   538  				assert.Equal(t, msg, "image pull failed for test because the registry is unavailable")
   539  			},
   540  		},
   541  		{
   542  			name:  "registry is unavailable with additional error message",
   543  			input: fmt.Errorf("%v: foo", crierrors.ErrRegistryUnavailable),
   544  			assert: func(msg string, err error) {
   545  				assert.ErrorIs(t, err, crierrors.ErrRegistryUnavailable)
   546  				assert.Equal(t, msg, "image pull failed for test because the registry is unavailable: foo")
   547  			},
   548  		},
   549  		{
   550  			name:  "signature is invalid",
   551  			input: crierrors.ErrSignatureValidationFailed,
   552  			assert: func(msg string, err error) {
   553  				assert.ErrorIs(t, err, crierrors.ErrSignatureValidationFailed)
   554  				assert.Equal(t, msg, "image pull failed for test because the signature validation failed")
   555  			},
   556  		},
   557  		{
   558  			name:  "signature is invalid with additional error message (wrapped)",
   559  			input: fmt.Errorf("%w: bar", crierrors.ErrSignatureValidationFailed),
   560  			assert: func(msg string, err error) {
   561  				assert.ErrorIs(t, err, crierrors.ErrSignatureValidationFailed)
   562  				assert.Equal(t, msg, "image pull failed for test because the signature validation failed: bar")
   563  			},
   564  		},
   565  	} {
   566  		testInput := tc.input
   567  		testAssert := tc.assert
   568  
   569  		t.Run(tc.name, func(t *testing.T) {
   570  			t.Parallel()
   571  			msg, err := evalCRIPullErr(&v1.Container{Image: "test"}, testInput)
   572  			testAssert(msg, err)
   573  		})
   574  	}
   575  }