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  }