k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/network/ingress.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 network
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"time"
    23  
    24  	networkingv1 "k8s.io/api/networking/v1"
    25  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	types "k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/apimachinery/pkg/util/wait"
    31  	"k8s.io/apimachinery/pkg/watch"
    32  	"k8s.io/client-go/util/retry"
    33  	"k8s.io/kubernetes/test/e2e/framework"
    34  	"k8s.io/kubernetes/test/e2e/network/common"
    35  	admissionapi "k8s.io/pod-security-admission/api"
    36  
    37  	"github.com/onsi/ginkgo/v2"
    38  	"github.com/onsi/gomega"
    39  )
    40  
    41  var _ = common.SIGDescribe("Ingress API", func() {
    42  	f := framework.NewDefaultFramework("ingress")
    43  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
    44  	/*
    45  		Release: v1.19
    46  		Testname: Ingress API
    47  		Description:
    48  		The networking.k8s.io API group MUST exist in the /apis discovery document.
    49  		The networking.k8s.io/v1 API group/version MUST exist in the /apis/networking.k8s.io discovery document.
    50  		The ingresses resources MUST exist in the /apis/networking.k8s.io/v1 discovery document.
    51  		The ingresses resource must support create, get, list, watch, update, patch, delete, and deletecollection.
    52  		The ingresses/status resource must support update and patch
    53  	*/
    54  
    55  	framework.ConformanceIt("should support creating Ingress API operations", func(ctx context.Context) {
    56  		// Setup
    57  		ns := f.Namespace.Name
    58  		ingVersion := "v1"
    59  		ingClient := f.ClientSet.NetworkingV1().Ingresses(ns)
    60  
    61  		prefixPathType := networkingv1.PathTypeImplementationSpecific
    62  		serviceBackend := &networkingv1.IngressServiceBackend{
    63  			Name: "default-backend",
    64  			Port: networkingv1.ServiceBackendPort{
    65  				Name:   "",
    66  				Number: 8080,
    67  			},
    68  		}
    69  		defaultBackend := networkingv1.IngressBackend{
    70  			Service: serviceBackend,
    71  		}
    72  
    73  		ingTemplate := &networkingv1.Ingress{
    74  			ObjectMeta: metav1.ObjectMeta{GenerateName: "e2e-example-ing",
    75  				Labels: map[string]string{
    76  					"special-label": f.UniqueName,
    77  				}},
    78  			Spec: networkingv1.IngressSpec{
    79  				DefaultBackend: &defaultBackend,
    80  				Rules: []networkingv1.IngressRule{
    81  					{
    82  						Host: "foo.bar.com",
    83  						IngressRuleValue: networkingv1.IngressRuleValue{
    84  							HTTP: &networkingv1.HTTPIngressRuleValue{
    85  								Paths: []networkingv1.HTTPIngressPath{{
    86  									Path:     "/",
    87  									PathType: &prefixPathType,
    88  									Backend: networkingv1.IngressBackend{
    89  										Service: &networkingv1.IngressServiceBackend{
    90  											Name: "test-backend",
    91  											Port: networkingv1.ServiceBackendPort{
    92  												Number: 8080,
    93  											},
    94  										},
    95  									},
    96  								}},
    97  							},
    98  						},
    99  					},
   100  				},
   101  			},
   102  			Status: networkingv1.IngressStatus{LoadBalancer: networkingv1.IngressLoadBalancerStatus{}},
   103  		}
   104  
   105  		ingress1 := ingTemplate.DeepCopy()
   106  		ingress1.Spec.Rules[0].Host = "host1.bar.com"
   107  		ingress2 := ingTemplate.DeepCopy()
   108  		ingress2.Spec.Rules[0].Host = "host2.bar.com"
   109  		ingress3 := ingTemplate.DeepCopy()
   110  		ingress3.Spec.Rules[0].Host = "host3.bar.com"
   111  
   112  		// Discovery
   113  		ginkgo.By("getting /apis")
   114  		{
   115  			discoveryGroups, err := f.ClientSet.Discovery().ServerGroups()
   116  			framework.ExpectNoError(err)
   117  			found := false
   118  			for _, group := range discoveryGroups.Groups {
   119  				if group.Name == networkingv1.GroupName {
   120  					for _, version := range group.Versions {
   121  						if version.Version == ingVersion {
   122  							found = true
   123  							break
   124  						}
   125  					}
   126  				}
   127  			}
   128  			if !found {
   129  				framework.Failf("expected networking API group/version, got %#v", discoveryGroups.Groups)
   130  			}
   131  		}
   132  
   133  		ginkgo.By("getting /apis/networking.k8s.io")
   134  		{
   135  			group := &metav1.APIGroup{}
   136  			err := f.ClientSet.Discovery().RESTClient().Get().AbsPath("/apis/networking.k8s.io").Do(ctx).Into(group)
   137  			framework.ExpectNoError(err)
   138  			found := false
   139  			for _, version := range group.Versions {
   140  				if version.Version == ingVersion {
   141  					found = true
   142  					break
   143  				}
   144  			}
   145  			if !found {
   146  				framework.Failf("expected networking API version, got %#v", group.Versions)
   147  			}
   148  		}
   149  
   150  		ginkgo.By("getting /apis/networking.k8s.io" + ingVersion)
   151  		{
   152  			resources, err := f.ClientSet.Discovery().ServerResourcesForGroupVersion(networkingv1.SchemeGroupVersion.String())
   153  			framework.ExpectNoError(err)
   154  			foundIngress := false
   155  			for _, resource := range resources.APIResources {
   156  				switch resource.Name {
   157  				case "ingresses":
   158  					foundIngress = true
   159  				}
   160  			}
   161  			if !foundIngress {
   162  				framework.Failf("expected ingresses, got %#v", resources.APIResources)
   163  			}
   164  		}
   165  
   166  		// Ingress resource create/read/update/watch verbs
   167  		ginkgo.By("creating")
   168  		_, err := ingClient.Create(ctx, ingress1, metav1.CreateOptions{})
   169  		framework.ExpectNoError(err)
   170  		_, err = ingClient.Create(ctx, ingress2, metav1.CreateOptions{})
   171  		framework.ExpectNoError(err)
   172  		createdIngress, err := ingClient.Create(ctx, ingress3, metav1.CreateOptions{})
   173  		framework.ExpectNoError(err)
   174  
   175  		ginkgo.By("getting")
   176  		gottenIngress, err := ingClient.Get(ctx, createdIngress.Name, metav1.GetOptions{})
   177  		framework.ExpectNoError(err)
   178  		gomega.Expect(gottenIngress.UID).To(gomega.Equal(createdIngress.UID))
   179  
   180  		ginkgo.By("listing")
   181  		ings, err := ingClient.List(ctx, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName})
   182  		framework.ExpectNoError(err)
   183  		gomega.Expect(ings.Items).To(gomega.HaveLen(3), "filtered list should have 3 items")
   184  
   185  		ginkgo.By("watching")
   186  		framework.Logf("starting watch")
   187  		ingWatch, err := ingClient.Watch(ctx, metav1.ListOptions{ResourceVersion: ings.ResourceVersion, LabelSelector: "special-label=" + f.UniqueName})
   188  		framework.ExpectNoError(err)
   189  
   190  		// Test cluster-wide list and watch
   191  		clusterIngClient := f.ClientSet.NetworkingV1().Ingresses("")
   192  		ginkgo.By("cluster-wide listing")
   193  		clusterIngs, err := clusterIngClient.List(ctx, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName})
   194  		framework.ExpectNoError(err)
   195  		gomega.Expect(clusterIngs.Items).To(gomega.HaveLen(3), "filtered list should have 3 items")
   196  
   197  		ginkgo.By("cluster-wide watching")
   198  		framework.Logf("starting watch")
   199  		_, err = clusterIngClient.Watch(ctx, metav1.ListOptions{ResourceVersion: ings.ResourceVersion, LabelSelector: "special-label=" + f.UniqueName})
   200  		framework.ExpectNoError(err)
   201  
   202  		ginkgo.By("patching")
   203  		patchedIngress, err := ingClient.Patch(ctx, createdIngress.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"patched":"true"}}}`), metav1.PatchOptions{})
   204  		framework.ExpectNoError(err)
   205  		gomega.Expect(patchedIngress.Annotations).To(gomega.HaveKeyWithValue("patched", "true"), "patched object should have the applied annotation")
   206  
   207  		ginkgo.By("updating")
   208  		var ingToUpdate, updatedIngress *networkingv1.Ingress
   209  		err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
   210  			ingToUpdate, err = ingClient.Get(ctx, createdIngress.Name, metav1.GetOptions{})
   211  			if err != nil {
   212  				return err
   213  			}
   214  			ingToUpdate.Annotations["updated"] = "true"
   215  			updatedIngress, err = ingClient.Update(ctx, ingToUpdate, metav1.UpdateOptions{})
   216  			return err
   217  		})
   218  		framework.ExpectNoError(err)
   219  		gomega.Expect(updatedIngress.Annotations).To(gomega.HaveKeyWithValue("updated", "true"), "updated object should have the applied annotation")
   220  
   221  		framework.Logf("waiting for watch events with expected annotations")
   222  		for sawAnnotations := false; !sawAnnotations; {
   223  			select {
   224  			case evt, ok := <-ingWatch.ResultChan():
   225  				if !ok {
   226  					framework.Fail("watch channel should not close")
   227  				}
   228  				gomega.Expect(evt.Type).To(gomega.Equal(watch.Modified))
   229  				watchedIngress, isIngress := evt.Object.(*networkingv1.Ingress)
   230  				if !isIngress {
   231  					framework.Failf("expected Ingress, got %T", evt.Object)
   232  				}
   233  				if watchedIngress.Annotations["patched"] == "true" {
   234  					framework.Logf("saw patched and updated annotations")
   235  					sawAnnotations = true
   236  					ingWatch.Stop()
   237  				} else {
   238  					framework.Logf("missing expected annotations, waiting: %#v", watchedIngress.Annotations)
   239  				}
   240  			case <-time.After(wait.ForeverTestTimeout):
   241  				framework.Fail("timed out waiting for watch event")
   242  			}
   243  		}
   244  
   245  		// /status subresource operations
   246  		ginkgo.By("patching /status")
   247  		lbStatus := networkingv1.IngressLoadBalancerStatus{
   248  			Ingress: []networkingv1.IngressLoadBalancerIngress{{IP: "169.1.1.1"}},
   249  		}
   250  		lbStatusJSON, err := json.Marshal(lbStatus)
   251  		framework.ExpectNoError(err)
   252  		patchedStatus, err := ingClient.Patch(ctx, createdIngress.Name, types.MergePatchType,
   253  			[]byte(`{"metadata":{"annotations":{"patchedstatus":"true"}},"status":{"loadBalancer":`+string(lbStatusJSON)+`}}`),
   254  			metav1.PatchOptions{}, "status")
   255  		framework.ExpectNoError(err)
   256  		gomega.Expect(patchedStatus.Status.LoadBalancer).To(gomega.Equal(lbStatus), "patched object should have the applied loadBalancer status")
   257  		gomega.Expect(patchedStatus.Annotations).To(gomega.HaveKeyWithValue("patchedstatus", "true"), "patched object should have the applied annotation")
   258  
   259  		ginkgo.By("updating /status")
   260  		var statusToUpdate, updatedStatus *networkingv1.Ingress
   261  		err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
   262  			statusToUpdate, err = ingClient.Get(ctx, createdIngress.Name, metav1.GetOptions{})
   263  			if err != nil {
   264  				return err
   265  			}
   266  			statusToUpdate.Status.LoadBalancer = networkingv1.IngressLoadBalancerStatus{
   267  				Ingress: []networkingv1.IngressLoadBalancerIngress{{IP: "169.1.1.2"}},
   268  			}
   269  			updatedStatus, err = ingClient.UpdateStatus(ctx, statusToUpdate, metav1.UpdateOptions{})
   270  			return err
   271  		})
   272  		framework.ExpectNoError(err)
   273  		gomega.Expect(updatedStatus.Status.LoadBalancer).To(gomega.Equal(statusToUpdate.Status.LoadBalancer), "updated object expected to have updated loadbalancer status %#v, got %#v", statusToUpdate.Status.LoadBalancer, updatedStatus.Status.LoadBalancer)
   274  
   275  		ginkgo.By("get /status")
   276  		ingResource := schema.GroupVersionResource{Group: "networking.k8s.io", Version: ingVersion, Resource: "ingresses"}
   277  		gottenStatus, err := f.DynamicClient.Resource(ingResource).Namespace(ns).Get(ctx, createdIngress.Name, metav1.GetOptions{}, "status")
   278  		framework.ExpectNoError(err)
   279  		statusUID, _, err := unstructured.NestedFieldCopy(gottenStatus.Object, "metadata", "uid")
   280  		framework.ExpectNoError(err)
   281  		gomega.Expect(string(createdIngress.UID)).To(gomega.Equal(statusUID), "createdIngress.UID: %v expected to match statusUID: %v ", createdIngress.UID, statusUID)
   282  
   283  		// Ingress resource delete operations
   284  		ginkgo.By("deleting")
   285  
   286  		expectFinalizer := func(ing *networkingv1.Ingress, msg string) {
   287  			gomega.Expect(ing.DeletionTimestamp).ToNot(gomega.BeNil(), "expected deletionTimestamp, got nil on step: %q, ingress: %+v", msg, ing)
   288  			if len(ing.Finalizers) == 0 {
   289  				framework.Failf("expected finalizers on ingress, got none on step: %q, ingress: %+v", msg, ing)
   290  			}
   291  		}
   292  
   293  		err = ingClient.Delete(ctx, createdIngress.Name, metav1.DeleteOptions{})
   294  		framework.ExpectNoError(err)
   295  		ing, err := ingClient.Get(ctx, createdIngress.Name, metav1.GetOptions{})
   296  		// If ingress controller does not support finalizers, we expect a 404.  Otherwise we validate finalizer behavior.
   297  		if err == nil {
   298  			expectFinalizer(ing, "deleting createdIngress")
   299  		} else {
   300  			if !apierrors.IsNotFound(err) {
   301  				framework.Failf("expected 404, got %v", err)
   302  			}
   303  		}
   304  		ings, err = ingClient.List(ctx, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName})
   305  		framework.ExpectNoError(err)
   306  		// Should have <= 3 items since some ingresses might not have been deleted yet due to finalizers
   307  		if len(ings.Items) > 3 {
   308  			framework.Fail("filtered list should have <= 3 items")
   309  		}
   310  		// Validate finalizer on the deleted ingress
   311  		for _, ing := range ings.Items {
   312  			if ing.Namespace == createdIngress.Namespace && ing.Name == createdIngress.Name {
   313  				expectFinalizer(&ing, "listing after deleting createdIngress")
   314  			}
   315  		}
   316  
   317  		ginkgo.By("deleting a collection")
   318  		err = ingClient.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName})
   319  		framework.ExpectNoError(err)
   320  		ings, err = ingClient.List(ctx, metav1.ListOptions{LabelSelector: "special-label=" + f.UniqueName})
   321  		framework.ExpectNoError(err)
   322  		// Should have <= 3 items since some ingresses might not have been deleted yet due to finalizers
   323  		if len(ings.Items) > 3 {
   324  			framework.Fail("filtered list should have <= 3 items")
   325  		}
   326  		// Validate finalizers
   327  		for _, ing := range ings.Items {
   328  			expectFinalizer(&ing, "deleting ingress collection")
   329  		}
   330  	})
   331  })