k8s.io/kubernetes@v1.29.3/test/e2e_node/pod_host_ips.go (about) 1 /* 2 Copyright 2023 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 23 "github.com/onsi/ginkgo/v2" 24 "github.com/onsi/gomega" 25 26 v1 "k8s.io/api/core/v1" 27 "k8s.io/apimachinery/pkg/api/resource" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/util/uuid" 30 netutils "k8s.io/utils/net" 31 32 utilfeature "k8s.io/apiserver/pkg/util/feature" 33 kubefeatures "k8s.io/kubernetes/pkg/features" 34 kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config" 35 "k8s.io/kubernetes/test/e2e/feature" 36 "k8s.io/kubernetes/test/e2e/framework" 37 e2enetwork "k8s.io/kubernetes/test/e2e/framework/network" 38 e2enode "k8s.io/kubernetes/test/e2e/framework/node" 39 e2epod "k8s.io/kubernetes/test/e2e/framework/pod" 40 e2epodoutput "k8s.io/kubernetes/test/e2e/framework/pod/output" 41 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" 42 "k8s.io/kubernetes/test/e2e/network/common" 43 "k8s.io/kubernetes/test/e2e/nodefeature" 44 imageutils "k8s.io/kubernetes/test/utils/image" 45 admissionapi "k8s.io/pod-security-admission/api" 46 ) 47 48 var _ = common.SIGDescribe("DualStack Host IP", framework.WithSerial(), nodefeature.PodHostIPs, feature.PodHostIPs, func() { 49 f := framework.NewDefaultFramework("dualstack") 50 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged 51 52 ginkgo.Context("when creating a Pod, it has no PodHostIPs feature", func() { 53 tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) { 54 initialConfig.FeatureGates = map[string]bool{ 55 string(kubefeatures.PodHostIPs): false, 56 } 57 }) 58 ginkgo.It("should create pod, add host ips is empty", func(ctx context.Context) { 59 60 podName := "pod-dualstack-host-ips" 61 62 pod := genPodHostIPs(podName+string(uuid.NewUUID()), false) 63 64 ginkgo.By("submitting the pod to kubernetes") 65 podClient := e2epod.NewPodClient(f) 66 p := podClient.CreateSync(ctx, pod) 67 68 gomega.Expect(p.Status.HostIP).ShouldNot(gomega.BeEquivalentTo("")) 69 gomega.Expect(p.Status.HostIPs).Should(gomega.BeNil()) 70 71 ginkgo.By("deleting the pod") 72 err := podClient.Delete(ctx, pod.Name, *metav1.NewDeleteOptions(1)) 73 framework.ExpectNoError(err, "failed to delete pod") 74 }) 75 76 ginkgo.It("should create pod with hostNetwork, add host ips is empty", func(ctx context.Context) { 77 78 podName := "pod-dualstack-host-ips" 79 80 pod := genPodHostIPs(podName+string(uuid.NewUUID()), true) 81 82 ginkgo.By("submitting the pod to kubernetes") 83 podClient := e2epod.NewPodClient(f) 84 p := podClient.CreateSync(ctx, pod) 85 86 gomega.Expect(p.Status.HostIP).ShouldNot(gomega.BeEquivalentTo("")) 87 gomega.Expect(p.Status.HostIPs).Should(gomega.BeNil()) 88 89 ginkgo.By("deleting the pod") 90 err := podClient.Delete(ctx, pod.Name, *metav1.NewDeleteOptions(1)) 91 framework.ExpectNoError(err, "failed to delete pod") 92 }) 93 }) 94 95 ginkgo.Context("when creating a Pod, it has PodHostIPs feature", func() { 96 tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) { 97 initialConfig.FeatureGates = map[string]bool{ 98 string(kubefeatures.PodHostIPs): true, 99 } 100 }) 101 ginkgo.It("should create pod, add ipv6 and ipv4 ip to host ips", func(ctx context.Context) { 102 103 podName := "pod-dualstack-host-ips" 104 105 pod := genPodHostIPs(podName+string(uuid.NewUUID()), false) 106 107 ginkgo.By("submitting the pod to kubernetes") 108 podClient := e2epod.NewPodClient(f) 109 p := podClient.CreateSync(ctx, pod) 110 111 gomega.Expect(p.Status.HostIP).ShouldNot(gomega.BeEquivalentTo("")) 112 gomega.Expect(p.Status.HostIPs).ShouldNot(gomega.BeNil()) 113 114 // validate first ip in HostIPs is same as HostIP 115 gomega.Expect(p.Status.HostIP).Should(gomega.Equal(p.Status.HostIPs[0].IP)) 116 if len(p.Status.HostIPs) > 1 { 117 // assert 2 host ips belong to different families 118 if netutils.IsIPv4String(p.Status.HostIPs[0].IP) == netutils.IsIPv4String(p.Status.HostIPs[1].IP) { 119 framework.Failf("both internalIPs %s and %s belong to the same families", p.Status.HostIPs[0].IP, p.Status.HostIPs[1].IP) 120 } 121 } 122 123 nodeList, err := e2enode.GetReadySchedulableNodes(ctx, f.ClientSet) 124 framework.ExpectNoError(err) 125 for _, node := range nodeList.Items { 126 if node.Name == p.Spec.NodeName { 127 nodeIPs := []v1.HostIP{} 128 for _, address := range node.Status.Addresses { 129 if address.Type == v1.NodeInternalIP { 130 nodeIPs = append(nodeIPs, v1.HostIP{IP: address.Address}) 131 } 132 } 133 gomega.Expect(p.Status.HostIPs).Should(gomega.Equal(nodeIPs)) 134 break 135 } 136 } 137 138 ginkgo.By("deleting the pod") 139 err = podClient.Delete(ctx, pod.Name, *metav1.NewDeleteOptions(1)) 140 framework.ExpectNoError(err, "failed to delete pod") 141 }) 142 143 ginkgo.It("should create pod with hostNetwork, add ipv6 and ipv4 ip to host ips", func(ctx context.Context) { 144 145 podName := "pod-dualstack-host-ips" 146 147 pod := genPodHostIPs(podName+string(uuid.NewUUID()), true) 148 149 ginkgo.By("submitting the pod to kubernetes") 150 podClient := e2epod.NewPodClient(f) 151 p := podClient.CreateSync(ctx, pod) 152 153 gomega.Expect(p.Status.HostIP).ShouldNot(gomega.BeEquivalentTo("")) 154 gomega.Expect(p.Status.HostIPs).ShouldNot(gomega.BeNil()) 155 156 // validate first ip in HostIPs is same as HostIP 157 gomega.Expect(p.Status.HostIP).Should(gomega.Equal(p.Status.HostIPs[0].IP)) 158 if len(p.Status.HostIPs) > 1 { 159 // assert 2 host ips belong to different families 160 if netutils.IsIPv4String(p.Status.HostIPs[0].IP) == netutils.IsIPv4String(p.Status.HostIPs[1].IP) { 161 framework.Failf("both internalIPs %s and %s belong to the same families", p.Status.HostIPs[0].IP, p.Status.HostIPs[1].IP) 162 } 163 } 164 165 nodeList, err := e2enode.GetReadySchedulableNodes(ctx, f.ClientSet) 166 framework.ExpectNoError(err) 167 for _, node := range nodeList.Items { 168 if node.Name == p.Spec.NodeName { 169 nodeIPs := []v1.HostIP{} 170 for _, address := range node.Status.Addresses { 171 if address.Type == v1.NodeInternalIP { 172 nodeIPs = append(nodeIPs, v1.HostIP{IP: address.Address}) 173 } 174 } 175 gomega.Expect(p.Status.HostIPs).Should(gomega.Equal(nodeIPs)) 176 break 177 } 178 } 179 180 ginkgo.By("deleting the pod") 181 err = podClient.Delete(ctx, pod.Name, *metav1.NewDeleteOptions(1)) 182 framework.ExpectNoError(err, "failed to delete pod") 183 }) 184 185 ginkgo.It("should provide hostIPs as an env var", func(ctx context.Context) { 186 if !utilfeature.DefaultFeatureGate.Enabled(kubefeatures.PodHostIPs) { 187 e2eskipper.Skipf("PodHostIPs feature is not enabled") 188 } 189 190 podName := "downward-api-" + string(uuid.NewUUID()) 191 env := []v1.EnvVar{ 192 { 193 Name: "HOST_IPS", 194 ValueFrom: &v1.EnvVarSource{ 195 FieldRef: &v1.ObjectFieldSelector{ 196 APIVersion: "v1", 197 FieldPath: "status.hostIPs", 198 }, 199 }, 200 }, 201 } 202 203 expectations := []string{ 204 fmt.Sprintf("HOST_IPS=%v|%v", e2enetwork.RegexIPv4, e2enetwork.RegexIPv6), 205 } 206 207 testDownwardAPI(ctx, f, podName, env, expectations) 208 }) 209 }) 210 211 ginkgo.Context("when feature rollback", func() { 212 tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) { 213 initialConfig.FeatureGates = map[string]bool{ 214 string(kubefeatures.PodHostIPs): true, 215 } 216 }) 217 ginkgo.It("should able upgrade and rollback", func(ctx context.Context) { 218 podName := "pod-dualstack-host-ips" 219 220 pod := genPodHostIPs(podName+string(uuid.NewUUID()), false) 221 222 ginkgo.By("submitting the pod to kubernetes") 223 podClient := e2epod.NewPodClient(f) 224 p := podClient.CreateSync(ctx, pod) 225 226 gomega.Expect(p.Status.HostIPs).ShouldNot(gomega.BeNil()) 227 228 ginkgo.By("Disable PodHostIPs feature") 229 cfg, err := getCurrentKubeletConfig(ctx) 230 framework.ExpectNoError(err) 231 232 newCfg := cfg.DeepCopy() 233 newCfg.FeatureGates = map[string]bool{ 234 string(kubefeatures.PodHostIPs): false, 235 } 236 237 updateKubeletConfig(ctx, f, newCfg, true) 238 239 gomega.Expect(p.Status.HostIPs).ShouldNot(gomega.BeNil()) 240 241 ginkgo.By("deleting the pod") 242 err = podClient.Delete(ctx, pod.Name, *metav1.NewDeleteOptions(1)) 243 framework.ExpectNoError(err, "failed to delete pod") 244 245 ginkgo.By("recreate pod") 246 pod = genPodHostIPs(podName+string(uuid.NewUUID()), false) 247 p = podClient.CreateSync(ctx, pod) 248 // Feature PodHostIPs is disabled, HostIPs should be nil 249 gomega.Expect(p.Status.HostIPs).Should(gomega.BeNil()) 250 251 newCfg.FeatureGates = map[string]bool{ 252 string(kubefeatures.PodHostIPs): true, 253 } 254 255 updateKubeletConfig(ctx, f, newCfg, true) 256 257 p, err = podClient.Get(ctx, pod.Name, metav1.GetOptions{}) 258 framework.ExpectNoError(err) 259 // Feature PodHostIPs is enabled, HostIPs should not be nil 260 gomega.Expect(p.Status.HostIPs).ShouldNot(gomega.BeNil()) 261 262 ginkgo.By("deleting the pod") 263 err = podClient.Delete(ctx, pod.Name, *metav1.NewDeleteOptions(1)) 264 framework.ExpectNoError(err, "failed to delete pod") 265 }) 266 }) 267 }) 268 269 func genPodHostIPs(podName string, hostNetwork bool) *v1.Pod { 270 return &v1.Pod{ 271 ObjectMeta: metav1.ObjectMeta{ 272 Name: podName, 273 Labels: map[string]string{"test": "dualstack-host-ips"}, 274 }, 275 Spec: v1.PodSpec{ 276 Containers: []v1.Container{ 277 { 278 Name: "test-container", 279 Image: imageutils.GetE2EImage(imageutils.Agnhost), 280 }, 281 }, 282 RestartPolicy: v1.RestartPolicyNever, 283 HostNetwork: hostNetwork, 284 }, 285 } 286 } 287 288 func testDownwardAPI(ctx context.Context, f *framework.Framework, podName string, env []v1.EnvVar, expectations []string) { 289 pod := &v1.Pod{ 290 ObjectMeta: metav1.ObjectMeta{ 291 Name: podName, 292 Labels: map[string]string{"name": podName}, 293 }, 294 Spec: v1.PodSpec{ 295 Containers: []v1.Container{ 296 { 297 Name: "dapi-container", 298 Image: imageutils.GetE2EImage(imageutils.BusyBox), 299 Command: []string{"sh", "-c", "env"}, 300 Resources: v1.ResourceRequirements{ 301 Requests: v1.ResourceList{ 302 v1.ResourceCPU: resource.MustParse("250m"), 303 v1.ResourceMemory: resource.MustParse("32Mi"), 304 }, 305 Limits: v1.ResourceList{ 306 v1.ResourceCPU: resource.MustParse("1250m"), 307 v1.ResourceMemory: resource.MustParse("64Mi"), 308 }, 309 }, 310 Env: env, 311 }, 312 }, 313 RestartPolicy: v1.RestartPolicyNever, 314 }, 315 } 316 317 e2epodoutput.TestContainerOutputRegexp(ctx, f, "downward api env vars", pod, 0, expectations) 318 }