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 })