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

     1  /*
     2  Copyright 2020 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  	"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  	types "k8s.io/apimachinery/pkg/types"
    28  
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  	"k8s.io/apimachinery/pkg/watch"
    31  	clientset "k8s.io/client-go/kubernetes"
    32  	"k8s.io/kubernetes/test/e2e/feature"
    33  	"k8s.io/kubernetes/test/e2e/framework"
    34  	"k8s.io/kubernetes/test/e2e/network/common"
    35  	admissionapi "k8s.io/pod-security-admission/api"
    36  	utilpointer "k8s.io/utils/pointer"
    37  
    38  	"github.com/onsi/ginkgo/v2"
    39  	"github.com/onsi/gomega"
    40  )
    41  
    42  var _ = common.SIGDescribe("IngressClass", feature.Ingress, func() {
    43  	f := framework.NewDefaultFramework("ingressclass")
    44  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
    45  	var cs clientset.Interface
    46  	ginkgo.BeforeEach(func() {
    47  		cs = f.ClientSet
    48  	})
    49  
    50  	f.It("should set default value on new IngressClass", f.WithSerial(), func(ctx context.Context) {
    51  		ingressClass1, err := createIngressClass(ctx, cs, "ingressclass1", true, f.UniqueName)
    52  		framework.ExpectNoError(err)
    53  		ginkgo.DeferCleanup(deleteIngressClass, cs, ingressClass1.Name)
    54  
    55  		ctx, cancel := context.WithCancel(ctx)
    56  		defer cancel()
    57  		lastFailure := ""
    58  
    59  		// the admission controller may take a few seconds to observe the ingress classes
    60  		if err := wait.PollWithContext(ctx, time.Second, time.Minute, func(ctx context.Context) (bool, error) {
    61  			lastFailure = ""
    62  
    63  			ingress, err := createBasicIngress(ctx, cs, f.Namespace.Name)
    64  			if err != nil {
    65  				lastFailure = err.Error()
    66  				return false, err
    67  			}
    68  			defer func() {
    69  				err := cs.NetworkingV1().Ingresses(ingress.Namespace).Delete(ctx, ingress.Name, metav1.DeleteOptions{})
    70  				framework.Logf("%v", err)
    71  			}()
    72  
    73  			if ingress.Spec.IngressClassName == nil {
    74  				lastFailure = "Expected IngressClassName to be set by Admission Controller"
    75  				return false, nil
    76  			} else if *ingress.Spec.IngressClassName != ingressClass1.Name {
    77  				lastFailure = fmt.Sprintf("Expected IngressClassName to be %s, got %s", ingressClass1.Name, *ingress.Spec.IngressClassName)
    78  				return false, nil
    79  			}
    80  			return true, nil
    81  
    82  		}); err != nil {
    83  			framework.Failf("%v, final err= %v", lastFailure, err)
    84  		}
    85  	})
    86  
    87  	f.It("should not set default value if no default IngressClass", f.WithSerial(), func(ctx context.Context) {
    88  		ingressClass1, err := createIngressClass(ctx, cs, "ingressclass1", false, f.UniqueName)
    89  		framework.ExpectNoError(err)
    90  		ginkgo.DeferCleanup(deleteIngressClass, cs, ingressClass1.Name)
    91  
    92  		ctx, cancel := context.WithCancel(ctx)
    93  		defer cancel()
    94  		lastFailure := ""
    95  
    96  		// the admission controller may take a few seconds to observe the ingress classes
    97  		if err := wait.PollWithContext(ctx, time.Second, time.Minute, func(ctx context.Context) (bool, error) {
    98  			lastFailure = ""
    99  
   100  			ingress, err := createBasicIngress(ctx, cs, f.Namespace.Name)
   101  			if err != nil {
   102  				lastFailure = err.Error()
   103  				return false, err
   104  			}
   105  			defer func() {
   106  				err := cs.NetworkingV1().Ingresses(ingress.Namespace).Delete(ctx, ingress.Name, metav1.DeleteOptions{})
   107  				framework.Logf("%v", err)
   108  			}()
   109  
   110  			if ingress.Spec.IngressClassName != nil {
   111  				lastFailure = fmt.Sprintf("Expected IngressClassName to be nil, got %s", *ingress.Spec.IngressClassName)
   112  				return false, nil
   113  			}
   114  			return true, nil
   115  
   116  		}); err != nil {
   117  			framework.Failf("%v, final err= %v", lastFailure, err)
   118  		}
   119  	})
   120  
   121  	f.It("should choose the one with the later CreationTimestamp, if equal the one with the lower name when two ingressClasses are marked as default", f.WithSerial(), func(ctx context.Context) {
   122  		ingressClass1, err := createIngressClass(ctx, cs, "ingressclass1", true, f.UniqueName)
   123  		framework.ExpectNoError(err)
   124  		ginkgo.DeferCleanup(deleteIngressClass, cs, ingressClass1.Name)
   125  
   126  		ingressClass2, err := createIngressClass(ctx, cs, "ingressclass2", true, f.UniqueName)
   127  		framework.ExpectNoError(err)
   128  		ginkgo.DeferCleanup(deleteIngressClass, cs, ingressClass2.Name)
   129  
   130  		expectedName := ingressClass1.Name
   131  		if ingressClass2.CreationTimestamp.UnixNano() > ingressClass1.CreationTimestamp.UnixNano() {
   132  			expectedName = ingressClass2.Name
   133  		}
   134  
   135  		ctx, cancel := context.WithCancel(ctx)
   136  		defer cancel()
   137  
   138  		// the admission controller may take a few seconds to observe both ingress classes
   139  		if err := wait.Poll(time.Second, time.Minute, func() (bool, error) {
   140  			classes, err := cs.NetworkingV1().IngressClasses().List(ctx, metav1.ListOptions{})
   141  			if err != nil {
   142  				return false, nil
   143  			}
   144  			cntDefault := 0
   145  			for _, class := range classes.Items {
   146  				if class.Annotations[networkingv1.AnnotationIsDefaultIngressClass] == "true" {
   147  					cntDefault++
   148  				}
   149  			}
   150  			if cntDefault < 2 {
   151  				return false, nil
   152  			}
   153  			ingress, err := createBasicIngress(ctx, cs, f.Namespace.Name)
   154  			if err != nil {
   155  				return false, nil
   156  			}
   157  			if ingress.Spec.IngressClassName == nil {
   158  				return false, fmt.Errorf("expected IngressClassName to be set by Admission Controller")
   159  			}
   160  			if *ingress.Spec.IngressClassName != expectedName {
   161  				return false, fmt.Errorf("expected ingress class %s but created with %s", expectedName, *ingress.Spec.IngressClassName)
   162  			}
   163  			return true, nil
   164  		}); err != nil {
   165  			framework.Failf("Failed to create ingress when two ingressClasses are marked as default ,got error %v", err)
   166  		}
   167  	})
   168  
   169  	f.It("should allow IngressClass to have Namespace-scoped parameters", f.WithSerial(), func(ctx context.Context) {
   170  		ingressClass := &networkingv1.IngressClass{
   171  			ObjectMeta: metav1.ObjectMeta{
   172  				Name: "ingressclass1",
   173  				Labels: map[string]string{
   174  					"ingressclass":  f.UniqueName,
   175  					"special-label": "generic",
   176  				},
   177  			},
   178  			Spec: networkingv1.IngressClassSpec{
   179  				Controller: "example.com/controller",
   180  				Parameters: &networkingv1.IngressClassParametersReference{
   181  					Scope:     utilpointer.String("Namespace"),
   182  					Namespace: utilpointer.String("foo-ns"),
   183  					Kind:      "fookind",
   184  					Name:      "fooname",
   185  					APIGroup:  utilpointer.String("example.com"),
   186  				},
   187  			},
   188  		}
   189  		createdIngressClass, err := cs.NetworkingV1().IngressClasses().Create(ctx, ingressClass, metav1.CreateOptions{})
   190  		framework.ExpectNoError(err)
   191  		ginkgo.DeferCleanup(deleteIngressClass, cs, createdIngressClass.Name)
   192  
   193  		if createdIngressClass.Spec.Parameters == nil {
   194  			framework.Failf("Expected IngressClass.spec.parameters to be set")
   195  		}
   196  		scope := ""
   197  		if createdIngressClass.Spec.Parameters.Scope != nil {
   198  			scope = *createdIngressClass.Spec.Parameters.Scope
   199  		}
   200  
   201  		if scope != "Namespace" {
   202  			framework.Failf("Expected IngressClass.spec.parameters.scope to be set to 'Namespace', got %v", scope)
   203  		}
   204  	})
   205  
   206  })
   207  
   208  func createIngressClass(ctx context.Context, cs clientset.Interface, name string, isDefault bool, uniqueName string) (*networkingv1.IngressClass, error) {
   209  	ingressClass := &networkingv1.IngressClass{
   210  		ObjectMeta: metav1.ObjectMeta{
   211  			Name: name,
   212  			Labels: map[string]string{
   213  				"ingressclass":  uniqueName,
   214  				"special-label": "generic",
   215  			},
   216  		},
   217  		Spec: networkingv1.IngressClassSpec{
   218  			Controller: "example.com/controller",
   219  		},
   220  	}
   221  
   222  	if isDefault {
   223  		ingressClass.Annotations = map[string]string{networkingv1.AnnotationIsDefaultIngressClass: "true"}
   224  	}
   225  
   226  	return cs.NetworkingV1().IngressClasses().Create(ctx, ingressClass, metav1.CreateOptions{})
   227  }
   228  
   229  func createBasicIngress(ctx context.Context, cs clientset.Interface, namespace string) (*networkingv1.Ingress, error) {
   230  	return cs.NetworkingV1().Ingresses(namespace).Create(ctx, &networkingv1.Ingress{
   231  		ObjectMeta: metav1.ObjectMeta{
   232  			Name: "ingress1",
   233  		},
   234  		Spec: networkingv1.IngressSpec{
   235  			DefaultBackend: &networkingv1.IngressBackend{
   236  				Service: &networkingv1.IngressServiceBackend{
   237  					Name: "defaultbackend",
   238  					Port: networkingv1.ServiceBackendPort{
   239  						Number: 80,
   240  					},
   241  				},
   242  			},
   243  		},
   244  	}, metav1.CreateOptions{})
   245  }
   246  
   247  func deleteIngressClass(ctx context.Context, cs clientset.Interface, name string) {
   248  	err := cs.NetworkingV1().IngressClasses().Delete(ctx, name, metav1.DeleteOptions{})
   249  	framework.ExpectNoError(err)
   250  }
   251  
   252  var _ = common.SIGDescribe("IngressClass API", func() {
   253  	f := framework.NewDefaultFramework("ingressclass")
   254  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
   255  	var cs clientset.Interface
   256  	ginkgo.BeforeEach(func() {
   257  		cs = f.ClientSet
   258  	})
   259  	/*
   260  		Release: v1.19
   261  		Testname: IngressClass API
   262  		Description:
   263  		- The networking.k8s.io API group MUST exist in the /apis discovery document.
   264  		- The networking.k8s.io/v1 API group/version MUST exist in the /apis/networking.k8s.io discovery document.
   265  		- The ingressclasses resource MUST exist in the /apis/networking.k8s.io/v1 discovery document.
   266  		- The ingressclass resource must support create, get, list, watch, update, patch, delete, and deletecollection.
   267  	*/
   268  	framework.ConformanceIt("should support creating IngressClass API operations", func(ctx context.Context) {
   269  
   270  		// Setup
   271  		icClient := f.ClientSet.NetworkingV1().IngressClasses()
   272  		icVersion := "v1"
   273  
   274  		// Discovery
   275  		ginkgo.By("getting /apis")
   276  		{
   277  			discoveryGroups, err := f.ClientSet.Discovery().ServerGroups()
   278  			framework.ExpectNoError(err)
   279  			found := false
   280  			for _, group := range discoveryGroups.Groups {
   281  				if group.Name == networkingv1.GroupName {
   282  					for _, version := range group.Versions {
   283  						if version.Version == icVersion {
   284  							found = true
   285  							break
   286  						}
   287  					}
   288  				}
   289  			}
   290  			if !found {
   291  				framework.Failf("expected networking API group/version, got %#v", discoveryGroups.Groups)
   292  			}
   293  		}
   294  		ginkgo.By("getting /apis/networking.k8s.io")
   295  		{
   296  			group := &metav1.APIGroup{}
   297  			err := f.ClientSet.Discovery().RESTClient().Get().AbsPath("/apis/networking.k8s.io").Do(ctx).Into(group)
   298  			framework.ExpectNoError(err)
   299  			found := false
   300  			for _, version := range group.Versions {
   301  				if version.Version == icVersion {
   302  					found = true
   303  					break
   304  				}
   305  			}
   306  			if !found {
   307  				framework.Failf("expected networking API version, got %#v", group.Versions)
   308  			}
   309  		}
   310  
   311  		ginkgo.By("getting /apis/networking.k8s.io" + icVersion)
   312  		{
   313  			resources, err := f.ClientSet.Discovery().ServerResourcesForGroupVersion(networkingv1.SchemeGroupVersion.String())
   314  			framework.ExpectNoError(err)
   315  			foundIC := false
   316  			for _, resource := range resources.APIResources {
   317  				switch resource.Name {
   318  				case "ingressclasses":
   319  					foundIC = true
   320  				}
   321  			}
   322  			if !foundIC {
   323  				framework.Failf("expected ingressclasses, got %#v", resources.APIResources)
   324  			}
   325  		}
   326  
   327  		// IngressClass resource create/read/update/watch verbs
   328  		ginkgo.By("creating")
   329  		ingressClass1, err := createIngressClass(ctx, cs, "ingressclass1", false, f.UniqueName)
   330  		framework.ExpectNoError(err)
   331  		_, err = createIngressClass(ctx, cs, "ingressclass2", false, f.UniqueName)
   332  		framework.ExpectNoError(err)
   333  		_, err = createIngressClass(ctx, cs, "ingressclass3", false, f.UniqueName)
   334  		framework.ExpectNoError(err)
   335  
   336  		ginkgo.By("getting")
   337  		gottenIC, err := icClient.Get(ctx, ingressClass1.Name, metav1.GetOptions{})
   338  		framework.ExpectNoError(err)
   339  		gomega.Expect(gottenIC.UID).To(gomega.Equal(ingressClass1.UID))
   340  		gomega.Expect(gottenIC.UID).To(gomega.Equal(ingressClass1.UID))
   341  
   342  		ginkgo.By("listing")
   343  		ics, err := icClient.List(ctx, metav1.ListOptions{LabelSelector: "special-label=generic"})
   344  		framework.ExpectNoError(err)
   345  		gomega.Expect(ics.Items).To(gomega.HaveLen(3), "filtered list should have 3 items")
   346  
   347  		ginkgo.By("watching")
   348  		framework.Logf("starting watch")
   349  		icWatch, err := icClient.Watch(ctx, metav1.ListOptions{ResourceVersion: ics.ResourceVersion, LabelSelector: "ingressclass=" + f.UniqueName})
   350  		framework.ExpectNoError(err)
   351  
   352  		ginkgo.By("patching")
   353  		patchedIC, err := icClient.Patch(ctx, ingressClass1.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"patched":"true"}}}`), metav1.PatchOptions{})
   354  		framework.ExpectNoError(err)
   355  		gomega.Expect(patchedIC.Annotations).To(gomega.HaveKeyWithValue("patched", "true"), "patched object should have the applied annotation")
   356  
   357  		ginkgo.By("updating")
   358  		icToUpdate := patchedIC.DeepCopy()
   359  		icToUpdate.Annotations["updated"] = "true"
   360  		updatedIC, err := icClient.Update(ctx, icToUpdate, metav1.UpdateOptions{})
   361  		framework.ExpectNoError(err)
   362  		gomega.Expect(updatedIC.Annotations).To(gomega.HaveKeyWithValue("updated", "true"), "updated object should have the applied annotation")
   363  
   364  		framework.Logf("waiting for watch events with expected annotations")
   365  		for sawAnnotations := false; !sawAnnotations; {
   366  			select {
   367  			case evt, ok := <-icWatch.ResultChan():
   368  				if !ok {
   369  					framework.Fail("watch channel should not close")
   370  				}
   371  				gomega.Expect(evt.Type).To(gomega.Equal(watch.Modified))
   372  				watchedIngress, isIngress := evt.Object.(*networkingv1.IngressClass)
   373  				if !isIngress {
   374  					framework.Failf("expected Ingress, got %T", evt.Object)
   375  				}
   376  				if watchedIngress.Annotations["patched"] == "true" {
   377  					framework.Logf("saw patched and updated annotations")
   378  					sawAnnotations = true
   379  					icWatch.Stop()
   380  				} else {
   381  					framework.Logf("missing expected annotations, waiting: %#v", watchedIngress.Annotations)
   382  				}
   383  			case <-time.After(wait.ForeverTestTimeout):
   384  				framework.Fail("timed out waiting for watch event")
   385  			}
   386  		}
   387  
   388  		// IngressClass resource delete operations
   389  		ginkgo.By("deleting")
   390  		err = icClient.Delete(ctx, ingressClass1.Name, metav1.DeleteOptions{})
   391  		framework.ExpectNoError(err)
   392  		_, err = icClient.Get(ctx, ingressClass1.Name, metav1.GetOptions{})
   393  		if !apierrors.IsNotFound(err) {
   394  			framework.Failf("expected 404, got %#v", err)
   395  		}
   396  		ics, err = icClient.List(ctx, metav1.ListOptions{LabelSelector: "ingressclass=" + f.UniqueName})
   397  		framework.ExpectNoError(err)
   398  		gomega.Expect(ics.Items).To(gomega.HaveLen(2), "filtered list should have 2 items")
   399  
   400  		ginkgo.By("deleting a collection")
   401  		err = icClient.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: "ingressclass=" + f.UniqueName})
   402  		framework.ExpectNoError(err)
   403  		ics, err = icClient.List(ctx, metav1.ListOptions{LabelSelector: "ingressclass=" + f.UniqueName})
   404  		framework.ExpectNoError(err)
   405  		gomega.Expect(ics.Items).To(gomega.BeEmpty(), "filtered list should have 0 items")
   406  	})
   407  
   408  })