k8s.io/kubernetes@v1.29.3/test/e2e/network/hostport.go (about)

     1  /*
     2  Copyright 2021 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 network
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"math/rand"
    23  	"net"
    24  	"strconv"
    25  
    26  	"github.com/onsi/ginkgo/v2"
    27  	v1 "k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/util/intstr"
    30  	clientset "k8s.io/client-go/kubernetes"
    31  	"k8s.io/kubernetes/test/e2e/framework"
    32  	e2enode "k8s.io/kubernetes/test/e2e/framework/node"
    33  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    34  	"k8s.io/kubernetes/test/e2e/network/common"
    35  	imageutils "k8s.io/kubernetes/test/utils/image"
    36  	admissionapi "k8s.io/pod-security-admission/api"
    37  )
    38  
    39  var _ = common.SIGDescribe("HostPort", func() {
    40  
    41  	f := framework.NewDefaultFramework("hostport")
    42  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
    43  
    44  	var (
    45  		cs clientset.Interface
    46  		ns string
    47  	)
    48  
    49  	ginkgo.BeforeEach(func() {
    50  		cs = f.ClientSet
    51  		ns = f.Namespace.Name
    52  
    53  	})
    54  
    55  	/*
    56  		Release: v1.16, v1.21
    57  		Testname: Scheduling, HostPort matching and HostIP and Protocol not-matching
    58  		Description: Pods with the same HostPort value MUST be able to be scheduled to the same node
    59  		if the HostIP or Protocol is different. This test is marked LinuxOnly since hostNetwork is not supported on
    60  		Windows.
    61  	*/
    62  
    63  	framework.ConformanceIt("validates that there is no conflict between pods with same hostPort but different hostIP and protocol [LinuxOnly]", func(ctx context.Context) {
    64  
    65  		localhost := "127.0.0.1"
    66  		family := v1.IPv4Protocol
    67  		if framework.TestContext.ClusterIsIPv6() {
    68  			localhost = "::1"
    69  			family = v1.IPv6Protocol
    70  		}
    71  		// Get a node where to schedule the pods
    72  		nodes, err := e2enode.GetBoundedReadySchedulableNodes(ctx, cs, 1)
    73  		framework.ExpectNoError(err)
    74  		if len(nodes.Items) == 0 {
    75  			framework.Failf("No nodes available")
    76  
    77  		}
    78  		randomNode := &nodes.Items[rand.Intn(len(nodes.Items))]
    79  
    80  		ips := e2enode.GetAddressesByTypeAndFamily(randomNode, v1.NodeInternalIP, family)
    81  		if len(ips) == 0 {
    82  			framework.Failf("Failed to get NodeIP")
    83  		}
    84  		hostIP := ips[0]
    85  		port := int32(54323)
    86  
    87  		// Create pods with the same HostPort
    88  		ginkgo.By(fmt.Sprintf("Trying to create a pod(pod1) with hostport %v and hostIP %s and expect scheduled", port, localhost))
    89  		createHostPortPodOnNode(ctx, f, "pod1", ns, localhost, port, v1.ProtocolTCP, randomNode.Name)
    90  
    91  		ginkgo.By(fmt.Sprintf("Trying to create another pod(pod2) with hostport %v but hostIP %s on the node which pod1 resides and expect scheduled", port, hostIP))
    92  		createHostPortPodOnNode(ctx, f, "pod2", ns, hostIP, port, v1.ProtocolTCP, randomNode.Name)
    93  
    94  		ginkgo.By(fmt.Sprintf("Trying to create a third pod(pod3) with hostport %v, hostIP %s but use UDP protocol on the node which pod2 resides", port, hostIP))
    95  		createHostPortPodOnNode(ctx, f, "pod3", ns, hostIP, port, v1.ProtocolUDP, randomNode.Name)
    96  
    97  		// check that the port is being actually exposed to each container
    98  		// create a pod on the host network in the same node
    99  		hostExecPod := &v1.Pod{
   100  			ObjectMeta: metav1.ObjectMeta{
   101  				Name:      "e2e-host-exec",
   102  				Namespace: f.Namespace.Name,
   103  			},
   104  			Spec: v1.PodSpec{
   105  				HostNetwork: true,
   106  				NodeName:    randomNode.Name,
   107  				Containers: []v1.Container{
   108  					{
   109  						Name:  "e2e-host-exec",
   110  						Image: imageutils.GetE2EImage(imageutils.Agnhost),
   111  					},
   112  				},
   113  			},
   114  		}
   115  		e2epod.NewPodClient(f).CreateSync(ctx, hostExecPod)
   116  
   117  		// use a 5 seconds timeout per connection
   118  		timeout := 5
   119  		// IPv6 doesn't NAT from localhost -> localhost, it doesn't have the route_localnet kernel hack, so we need to specify the source IP
   120  		cmdPod1 := []string{"/bin/sh", "-c", fmt.Sprintf("curl -g --connect-timeout %v --interface %s http://%s/hostname", timeout, hostIP, net.JoinHostPort(localhost, strconv.Itoa(int(port))))}
   121  		cmdPod2 := []string{"/bin/sh", "-c", fmt.Sprintf("curl -g --connect-timeout %v http://%s/hostname", timeout, net.JoinHostPort(hostIP, strconv.Itoa(int(port))))}
   122  		cmdPod3 := []string{"/bin/sh", "-c", fmt.Sprintf("echo hostname | nc -u -w %v %s %d", timeout, hostIP, port)}
   123  		// try 5 times to connect to the exposed ports
   124  		for i := 0; i < 5; i++ {
   125  			// check pod1
   126  			ginkgo.By(fmt.Sprintf("checking connectivity from pod %s to serverIP: %s, port: %d", hostExecPod.Name, localhost, port))
   127  			hostname1, _, err := e2epod.ExecCommandInContainerWithFullOutput(f, hostExecPod.Name, "e2e-host-exec", cmdPod1...)
   128  			if err != nil {
   129  				framework.Logf("Can not connect from %s to pod(pod1) to serverIP: %s, port: %d", hostExecPod.Name, localhost, port)
   130  				continue
   131  			}
   132  			// check pod2
   133  			ginkgo.By(fmt.Sprintf("checking connectivity from pod %s to serverIP: %s, port: %d", hostExecPod.Name, hostIP, port))
   134  			hostname2, _, err := e2epod.ExecCommandInContainerWithFullOutput(f, hostExecPod.Name, "e2e-host-exec", cmdPod2...)
   135  			if err != nil {
   136  				framework.Logf("Can not connect from %s to pod(pod2) to serverIP: %s, port: %d", hostExecPod.Name, hostIP, port)
   137  				continue
   138  			}
   139  			// the hostname returned has to be different because we are exposing the same port to two different pods
   140  			if hostname1 == hostname2 {
   141  				framework.Logf("pods must have different hostname: pod1 has hostname %s, pod2 has hostname %s", hostname1, hostname2)
   142  				continue
   143  			}
   144  			// check pod3
   145  			ginkgo.By(fmt.Sprintf("checking connectivity from pod %s to serverIP: %s, port: %d UDP", hostExecPod.Name, hostIP, port))
   146  			hostname3, _, err := e2epod.ExecCommandInContainerWithFullOutput(f, hostExecPod.Name, "e2e-host-exec", cmdPod3...)
   147  			if err != nil {
   148  				framework.Logf("Can not connect from %s to pod(pod2) to serverIP: %s, port: %d", hostExecPod.Name, hostIP, port)
   149  				continue
   150  			}
   151  			if hostname1 == hostname3 {
   152  				framework.Logf("pods must have different hostname: pod1 has hostname %s, pod3 has hostname %s", hostname1, hostname3)
   153  				continue
   154  			}
   155  			if hostname2 == hostname3 {
   156  				framework.Logf("pods must have different hostname: pod2 has hostname %s, pod3 has hostname %s", hostname2, hostname3)
   157  				continue
   158  			}
   159  			return
   160  		}
   161  		framework.Failf("Failed to connect to exposed host ports")
   162  	})
   163  })
   164  
   165  // create pod which using hostport on the specified node according to the nodeSelector
   166  // it starts an http server on the exposed port
   167  func createHostPortPodOnNode(ctx context.Context, f *framework.Framework, podName, ns, hostIP string, port int32, protocol v1.Protocol, nodeName string) {
   168  
   169  	var netexecArgs []string
   170  	var readinessProbePort int32
   171  
   172  	if protocol == v1.ProtocolTCP {
   173  		readinessProbePort = 8080
   174  		netexecArgs = []string{"--http-port=8080", "--udp-port=-1"}
   175  	} else {
   176  		readinessProbePort = 8008
   177  		netexecArgs = []string{"--http-port=8008", "--udp-port=8080"}
   178  	}
   179  
   180  	hostPortPod := &v1.Pod{
   181  		ObjectMeta: metav1.ObjectMeta{
   182  			Name: podName,
   183  		},
   184  		Spec: v1.PodSpec{
   185  			Containers: []v1.Container{
   186  				{
   187  					Name:  "agnhost",
   188  					Image: imageutils.GetE2EImage(imageutils.Agnhost),
   189  					Args:  append([]string{"netexec"}, netexecArgs...),
   190  					Ports: []v1.ContainerPort{
   191  						{
   192  							HostPort:      port,
   193  							ContainerPort: 8080,
   194  							Protocol:      protocol,
   195  							HostIP:        hostIP,
   196  						},
   197  					},
   198  					ReadinessProbe: &v1.Probe{
   199  						ProbeHandler: v1.ProbeHandler{
   200  							HTTPGet: &v1.HTTPGetAction{
   201  								Path: "/hostname",
   202  								Port: intstr.IntOrString{
   203  									IntVal: readinessProbePort,
   204  								},
   205  								Scheme: v1.URISchemeHTTP,
   206  							},
   207  						},
   208  					},
   209  				},
   210  			},
   211  			NodeName: nodeName,
   212  		},
   213  	}
   214  	if _, err := f.ClientSet.CoreV1().Pods(ns).Create(ctx, hostPortPod, metav1.CreateOptions{}); err != nil {
   215  		framework.Failf("error creating pod %s, err:%v", podName, err)
   216  	}
   217  
   218  	if err := e2epod.WaitTimeoutForPodReadyInNamespace(ctx, f.ClientSet, podName, ns, framework.PodStartTimeout); err != nil {
   219  		framework.Failf("wait for pod %s timeout, err:%v", podName, err)
   220  	}
   221  }