|  | 
|  | 1 | +/* | 
|  | 2 | +Copyright 2024 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 e2enode | 
|  | 18 | + | 
|  | 19 | +import ( | 
|  | 20 | +	"context" | 
|  | 21 | +	"fmt" | 
|  | 22 | +	"path/filepath" | 
|  | 23 | +	"strings" | 
|  | 24 | +	"time" | 
|  | 25 | + | 
|  | 26 | +	"github.com/onsi/ginkgo/v2" | 
|  | 27 | +	"github.com/onsi/gomega" | 
|  | 28 | +	v1 "k8s.io/api/core/v1" | 
|  | 29 | +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 
|  | 30 | +	"k8s.io/kubernetes/pkg/features" | 
|  | 31 | +	"k8s.io/kubernetes/pkg/kubelet/images" | 
|  | 32 | +	"k8s.io/kubernetes/test/e2e/framework" | 
|  | 33 | +	e2epod "k8s.io/kubernetes/test/e2e/framework/pod" | 
|  | 34 | +	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" | 
|  | 35 | +	"k8s.io/kubernetes/test/e2e/nodefeature" | 
|  | 36 | +	admissionapi "k8s.io/pod-security-admission/api" | 
|  | 37 | +	"k8s.io/utils/ptr" | 
|  | 38 | +) | 
|  | 39 | + | 
|  | 40 | +// Run this single test locally using a running CRI-O instance by: | 
|  | 41 | +// make test-e2e-node CONTAINER_RUNTIME_ENDPOINT="unix:///var/run/crio/crio.sock" TEST_ARGS='--ginkgo.focus="ImageVolume" --feature-gates=ImageVolume=true --service-feature-gates=ImageVolume=true --kubelet-flags="--cgroup-root=/ --runtime-cgroups=/system.slice/crio.service --kubelet-cgroups=/system.slice/kubelet.service --fail-swap-on=false"' | 
|  | 42 | +var _ = SIGDescribe("ImageVolume", nodefeature.ImageVolume, func() { | 
|  | 43 | +	f := framework.NewDefaultFramework("image-volume-test") | 
|  | 44 | +	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged | 
|  | 45 | + | 
|  | 46 | +	const ( | 
|  | 47 | +		podName          = "test-pod" | 
|  | 48 | +		containerName    = "test-container" | 
|  | 49 | +		validImageRef    = "quay.io/crio/artifact:v1" | 
|  | 50 | +		invalidImageRef  = "localhost/invalid" | 
|  | 51 | +		volumeName       = "volume" | 
|  | 52 | +		volumePathPrefix = "/volume" | 
|  | 53 | +	) | 
|  | 54 | + | 
|  | 55 | +	ginkgo.BeforeEach(func(ctx context.Context) { | 
|  | 56 | +		e2eskipper.SkipUnlessFeatureGateEnabled(features.ImageVolume) | 
|  | 57 | +	}) | 
|  | 58 | + | 
|  | 59 | +	createPod := func(ctx context.Context, volumes []v1.Volume, volumeMounts []v1.VolumeMount) { | 
|  | 60 | +		pod := &v1.Pod{ | 
|  | 61 | +			ObjectMeta: metav1.ObjectMeta{ | 
|  | 62 | +				Name:      podName, | 
|  | 63 | +				Namespace: f.Namespace.Name, | 
|  | 64 | +			}, | 
|  | 65 | +			Spec: v1.PodSpec{ | 
|  | 66 | +				RestartPolicy: v1.RestartPolicyAlways, | 
|  | 67 | +				Containers: []v1.Container{ | 
|  | 68 | +					{ | 
|  | 69 | +						Name:            containerName, | 
|  | 70 | +						Image:           busyboxImage, | 
|  | 71 | +						Command:         ExecCommand(podName, execCommand{LoopForever: true}), | 
|  | 72 | +						SecurityContext: &v1.SecurityContext{Privileged: ptr.To(true)}, | 
|  | 73 | +						VolumeMounts:    volumeMounts, | 
|  | 74 | +					}, | 
|  | 75 | +				}, | 
|  | 76 | +				Volumes: volumes, | 
|  | 77 | +			}, | 
|  | 78 | +		} | 
|  | 79 | + | 
|  | 80 | +		ginkgo.By(fmt.Sprintf("Creating a pod (%v/%v)", f.Namespace.Name, podName)) | 
|  | 81 | +		e2epod.NewPodClient(f).Create(ctx, pod) | 
|  | 82 | + | 
|  | 83 | +	} | 
|  | 84 | + | 
|  | 85 | +	f.It("should succeed with pod and pull policy of Always", func(ctx context.Context) { | 
|  | 86 | +		createPod(ctx, | 
|  | 87 | +			[]v1.Volume{{Name: volumeName, VolumeSource: v1.VolumeSource{Image: &v1.ImageVolumeSource{Reference: validImageRef, PullPolicy: v1.PullAlways}}}}, | 
|  | 88 | +			[]v1.VolumeMount{{Name: volumeName, MountPath: volumePathPrefix}}, | 
|  | 89 | +		) | 
|  | 90 | + | 
|  | 91 | +		ginkgo.By(fmt.Sprintf("Waiting for the pod (%v/%v) to be running", f.Namespace.Name, podName)) | 
|  | 92 | +		err := e2epod.WaitForPodNameRunningInNamespace(ctx, f.ClientSet, podName, f.Namespace.Name) | 
|  | 93 | +		framework.ExpectNoError(err, "Failed to await for the pod to be running: (%v/%v)", f.Namespace.Name, podName) | 
|  | 94 | + | 
|  | 95 | +		ginkgo.By(fmt.Sprintf("Verifying the volume mount contents for path: %s", volumePathPrefix)) | 
|  | 96 | + | 
|  | 97 | +		firstFileContents := e2epod.ExecCommandInContainer(f, podName, containerName, "/bin/cat", filepath.Join(volumePathPrefix, "dir", "file")) | 
|  | 98 | +		gomega.Expect(firstFileContents).To(gomega.Equal("1")) | 
|  | 99 | + | 
|  | 100 | +		secondFileContents := e2epod.ExecCommandInContainer(f, podName, containerName, "/bin/cat", filepath.Join(volumePathPrefix, "file")) | 
|  | 101 | +		gomega.Expect(secondFileContents).To(gomega.Equal("2")) | 
|  | 102 | +	}) | 
|  | 103 | + | 
|  | 104 | +	f.It("should succeed with pod and multiple volumes", func(ctx context.Context) { | 
|  | 105 | +		createPod(ctx, | 
|  | 106 | +			[]v1.Volume{ | 
|  | 107 | +				{Name: volumeName + "-0", VolumeSource: v1.VolumeSource{Image: &v1.ImageVolumeSource{Reference: validImageRef}}}, | 
|  | 108 | +				{Name: volumeName + "-1", VolumeSource: v1.VolumeSource{Image: &v1.ImageVolumeSource{Reference: validImageRef}}}, | 
|  | 109 | +			}, | 
|  | 110 | +			[]v1.VolumeMount{ | 
|  | 111 | +				{Name: volumeName + "-0", MountPath: volumePathPrefix + "-0"}, | 
|  | 112 | +				{Name: volumeName + "-1", MountPath: volumePathPrefix + "-1"}, | 
|  | 113 | +			}, | 
|  | 114 | +		) | 
|  | 115 | + | 
|  | 116 | +		ginkgo.By(fmt.Sprintf("Waiting for the pod (%v/%v) to be running", f.Namespace.Name, podName)) | 
|  | 117 | +		err := e2epod.WaitForPodNameRunningInNamespace(ctx, f.ClientSet, podName, f.Namespace.Name) | 
|  | 118 | +		framework.ExpectNoError(err, "Failed to await for the pod to be running: (%v/%v)", f.Namespace.Name, podName) | 
|  | 119 | + | 
|  | 120 | +		for i := range 2 { | 
|  | 121 | +			volumePath := fmt.Sprintf("%s-%d", volumePathPrefix, i) | 
|  | 122 | +			ginkgo.By(fmt.Sprintf("Verifying the volume mount contents for path: %s", volumePath)) | 
|  | 123 | + | 
|  | 124 | +			firstFileContents := e2epod.ExecCommandInContainer(f, podName, containerName, "/bin/cat", filepath.Join(volumePath, "dir", "file")) | 
|  | 125 | +			gomega.Expect(firstFileContents).To(gomega.Equal("1")) | 
|  | 126 | + | 
|  | 127 | +			secondFileContents := e2epod.ExecCommandInContainer(f, podName, containerName, "/bin/cat", filepath.Join(volumePath, "file")) | 
|  | 128 | +			gomega.Expect(secondFileContents).To(gomega.Equal("2")) | 
|  | 129 | +		} | 
|  | 130 | +	}) | 
|  | 131 | + | 
|  | 132 | +	f.It("should fail if image volume is not existing", func(ctx context.Context) { | 
|  | 133 | +		createPod(ctx, | 
|  | 134 | +			[]v1.Volume{{Name: volumeName, VolumeSource: v1.VolumeSource{Image: &v1.ImageVolumeSource{Reference: invalidImageRef}}}}, | 
|  | 135 | +			[]v1.VolumeMount{{Name: volumeName, MountPath: volumePathPrefix}}, | 
|  | 136 | +		) | 
|  | 137 | + | 
|  | 138 | +		ginkgo.By(fmt.Sprintf("Waiting for the pod (%v/%v) to fail", f.Namespace.Name, podName)) | 
|  | 139 | +		err := e2epod.WaitForPodContainerToFail(ctx, f.ClientSet, f.Namespace.Name, podName, 0, images.ErrImagePullBackOff.Error(), time.Minute) | 
|  | 140 | +		framework.ExpectNoError(err, "Failed to await for the pod to be running: (%v/%v)", f.Namespace.Name, podName) | 
|  | 141 | +	}) | 
|  | 142 | + | 
|  | 143 | +	f.It("should succeed if image volume is not existing but unused", func(ctx context.Context) { | 
|  | 144 | +		createPod(ctx, | 
|  | 145 | +			[]v1.Volume{{Name: volumeName, VolumeSource: v1.VolumeSource{Image: &v1.ImageVolumeSource{Reference: invalidImageRef}}}}, | 
|  | 146 | +			nil, | 
|  | 147 | +		) | 
|  | 148 | + | 
|  | 149 | +		ginkgo.By(fmt.Sprintf("Waiting for the pod (%v/%v) to be running", f.Namespace.Name, podName)) | 
|  | 150 | +		err := e2epod.WaitForPodNameRunningInNamespace(ctx, f.ClientSet, podName, f.Namespace.Name) | 
|  | 151 | +		framework.ExpectNoError(err, "Failed to await for the pod to be running: (%v/%v)", f.Namespace.Name, podName) | 
|  | 152 | + | 
|  | 153 | +		ginkgo.By(fmt.Sprintf("Verifying the volume mount is not used for path: %s", volumePathPrefix)) | 
|  | 154 | + | 
|  | 155 | +		output := e2epod.ExecCommandInContainer(f, podName, containerName, "/bin/ls", filepath.Dir(volumePathPrefix)) | 
|  | 156 | +		gomega.Expect(output).NotTo(gomega.ContainSubstring(strings.TrimPrefix(volumePathPrefix, "/"))) | 
|  | 157 | +	}) | 
|  | 158 | +}) | 
0 commit comments