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