istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/ingress/conversion_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ingress 16 17 import ( 18 "fmt" 19 "os" 20 "sort" 21 "strings" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 corev1 "k8s.io/api/core/v1" 26 knetworking "k8s.io/api/networking/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/util/intstr" 30 "k8s.io/client-go/kubernetes/scheme" 31 "sigs.k8s.io/yaml" 32 33 meshconfig "istio.io/api/mesh/v1alpha1" 34 networking "istio.io/api/networking/v1alpha3" 35 "istio.io/istio/pilot/pkg/config/kube/crd" 36 "istio.io/istio/pilot/test/util" 37 "istio.io/istio/pkg/config" 38 "istio.io/istio/pkg/config/mesh" 39 "istio.io/istio/pkg/kube" 40 "istio.io/istio/pkg/kube/kclient" 41 "istio.io/istio/pkg/test" 42 ) 43 44 func TestGoldenConversion(t *testing.T) { 45 cases := []string{"simple", "tls", "overlay", "tls-no-secret"} 46 for _, tt := range cases { 47 t.Run(tt, func(t *testing.T) { 48 input, err := readConfig(t, fmt.Sprintf("testdata/%s.yaml", tt)) 49 if err != nil { 50 t.Fatal(err) 51 } 52 serviceLister := createFakeClient(t) 53 cfgs := map[string]*config.Config{} 54 for _, obj := range input { 55 ingress := obj.(*knetworking.Ingress) 56 ConvertIngressVirtualService(*ingress, "mydomain", cfgs, serviceLister) 57 } 58 ordered := []config.Config{} 59 for _, v := range cfgs { 60 ordered = append(ordered, *v) 61 } 62 for _, obj := range input { 63 ingress := obj.(*knetworking.Ingress) 64 m := mesh.DefaultMeshConfig() 65 gws := ConvertIngressV1alpha3(*ingress, m, "mydomain") 66 ordered = append(ordered, gws) 67 } 68 69 sort.Slice(ordered, func(i, j int) bool { 70 return ordered[i].Name < ordered[j].Name 71 }) 72 output := marshalYaml(t, ordered) 73 goldenFile := fmt.Sprintf("testdata/%s.yaml.golden", tt) 74 if util.Refresh() { 75 if err := os.WriteFile(goldenFile, output, 0o644); err != nil { 76 t.Fatal(err) 77 } 78 } 79 expected, err := os.ReadFile(goldenFile) 80 if err != nil { 81 t.Fatal(err) 82 } 83 if diff := cmp.Diff(expected, output); diff != "" { 84 t.Fatalf("Diff:\n%s", diff) 85 } 86 }) 87 } 88 } 89 90 // Print as YAML 91 func marshalYaml(t *testing.T, cl []config.Config) []byte { 92 t.Helper() 93 result := []byte{} 94 separator := []byte("---\n") 95 for _, config := range cl { 96 obj, err := crd.ConvertConfig(config) 97 if err != nil { 98 t.Fatalf("Could not decode %v: %v", config.Name, err) 99 } 100 bytes, err := yaml.Marshal(obj) 101 if err != nil { 102 t.Fatalf("Could not convert %v to YAML: %v", config, err) 103 } 104 result = append(result, bytes...) 105 result = append(result, separator...) 106 } 107 return result 108 } 109 110 func readConfig(t *testing.T, filename string) ([]runtime.Object, error) { 111 t.Helper() 112 113 data, err := os.ReadFile(filename) 114 if err != nil { 115 t.Fatalf("failed to read input yaml file: %v", err) 116 } 117 var varr []runtime.Object 118 for _, yml := range strings.Split(string(data), "\n---") { 119 obj, _, err := scheme.Codecs.UniversalDeserializer().Decode([]byte(yml), nil, nil) 120 if err != nil { 121 return nil, err 122 } 123 varr = append(varr, obj) 124 } 125 126 return varr, nil 127 } 128 129 func TestConversion(t *testing.T) { 130 prefix := knetworking.PathTypePrefix 131 exact := knetworking.PathTypeExact 132 133 ingress := knetworking.Ingress{ 134 ObjectMeta: metav1.ObjectMeta{ 135 Namespace: "mock", // goes into backend full name 136 }, 137 Spec: knetworking.IngressSpec{ 138 Rules: []knetworking.IngressRule{ 139 { 140 Host: "my.host.com", 141 IngressRuleValue: knetworking.IngressRuleValue{ 142 HTTP: &knetworking.HTTPIngressRuleValue{ 143 Paths: []knetworking.HTTPIngressPath{ 144 { 145 Path: "/test", 146 Backend: knetworking.IngressBackend{ 147 Service: &knetworking.IngressServiceBackend{ 148 Name: "foo", 149 Port: knetworking.ServiceBackendPort{Number: 8000}, 150 }, 151 }, 152 }, 153 { 154 Path: "/test/foo", 155 PathType: &prefix, 156 Backend: knetworking.IngressBackend{ 157 Service: &knetworking.IngressServiceBackend{ 158 Name: "foo", 159 Port: knetworking.ServiceBackendPort{Number: 8000}, 160 }, 161 }, 162 }, 163 }, 164 }, 165 }, 166 }, 167 { 168 Host: "my2.host.com", 169 IngressRuleValue: knetworking.IngressRuleValue{ 170 HTTP: &knetworking.HTTPIngressRuleValue{ 171 Paths: []knetworking.HTTPIngressPath{ 172 { 173 Path: "/test1.*", 174 Backend: knetworking.IngressBackend{ 175 Service: &knetworking.IngressServiceBackend{ 176 Name: "bar", 177 Port: knetworking.ServiceBackendPort{Number: 8000}, 178 }, 179 }, 180 }, 181 }, 182 }, 183 }, 184 }, 185 { 186 Host: "my3.host.com", 187 IngressRuleValue: knetworking.IngressRuleValue{ 188 HTTP: &knetworking.HTTPIngressRuleValue{ 189 Paths: []knetworking.HTTPIngressPath{ 190 { 191 Path: "/test/*", 192 Backend: knetworking.IngressBackend{ 193 Service: &knetworking.IngressServiceBackend{ 194 Name: "bar", 195 Port: knetworking.ServiceBackendPort{Number: 8000}, 196 }, 197 }, 198 }, 199 }, 200 }, 201 }, 202 }, 203 { 204 Host: "my4.host.com", 205 IngressRuleValue: knetworking.IngressRuleValue{ 206 HTTP: &knetworking.HTTPIngressRuleValue{ 207 Paths: []knetworking.HTTPIngressPath{ 208 { 209 Path: "/*", 210 Backend: knetworking.IngressBackend{ 211 Service: &knetworking.IngressServiceBackend{ 212 Name: "bar", 213 Port: knetworking.ServiceBackendPort{Number: 8000}, 214 }, 215 }, 216 }, 217 }, 218 }, 219 }, 220 }, 221 }, 222 }, 223 } 224 ingress2 := knetworking.Ingress{ 225 ObjectMeta: metav1.ObjectMeta{ 226 Namespace: "mock", 227 }, 228 Spec: knetworking.IngressSpec{ 229 Rules: []knetworking.IngressRule{ 230 { 231 Host: "my.host.com", 232 IngressRuleValue: knetworking.IngressRuleValue{ 233 HTTP: &knetworking.HTTPIngressRuleValue{ 234 Paths: []knetworking.HTTPIngressPath{ 235 { 236 Path: "/test2", 237 Backend: knetworking.IngressBackend{ 238 Service: &knetworking.IngressServiceBackend{ 239 Name: "foo", 240 Port: knetworking.ServiceBackendPort{Number: 8000}, 241 }, 242 }, 243 }, 244 { 245 Path: "/test/foo/bar", 246 PathType: &prefix, 247 Backend: knetworking.IngressBackend{ 248 Service: &knetworking.IngressServiceBackend{ 249 Name: "foo", 250 Port: knetworking.ServiceBackendPort{Number: 8000}, 251 }, 252 }, 253 }, 254 { 255 Path: "/test/foo/bar", 256 PathType: &exact, 257 Backend: knetworking.IngressBackend{ 258 Service: &knetworking.IngressServiceBackend{ 259 Name: "foo", 260 Port: knetworking.ServiceBackendPort{Number: 8000}, 261 }, 262 }, 263 }, 264 }, 265 }, 266 }, 267 }, 268 }, 269 }, 270 } 271 serviceLister := createFakeClient(t) 272 cfgs := map[string]*config.Config{} 273 ConvertIngressVirtualService(ingress, "mydomain", cfgs, serviceLister) 274 ConvertIngressVirtualService(ingress2, "mydomain", cfgs, serviceLister) 275 276 if len(cfgs) != 4 { 277 t.Error("VirtualServices, expected 4 got ", len(cfgs)) 278 } 279 280 expectedLength := [5]int{13, 13, 9, 6, 5} 281 expectedExact := [5]bool{true, false, false, true, true} 282 283 for n, cfg := range cfgs { 284 vs := cfg.Spec.(*networking.VirtualService) 285 286 if n == "my.host.com" { 287 if vs.Hosts[0] != "my.host.com" { 288 t.Error("Unexpected host", vs) 289 } 290 if len(vs.Http) != 5 { 291 t.Error("Unexpected rules", vs.Http) 292 } 293 for i, route := range vs.Http { 294 length, exact := getMatchURILength(route.Match[0]) 295 if length != expectedLength[i] || exact != expectedExact[i] { 296 t.Errorf("Unexpected rule at idx:%d, want {length:%d, exact:%v}, got {length:%d, exact: %v}", 297 i, expectedLength[i], expectedExact[i], length, exact) 298 } 299 } 300 } else if n == "my4.host.com" { 301 if vs.Hosts[0] != "my4.host.com" { 302 t.Error("Unexpected host", vs) 303 } 304 if len(vs.Http) != 1 { 305 t.Error("Unexpected rules", vs.Http) 306 } 307 if vs.Http[0].Match != nil { 308 t.Error("Expected HTTPMatchRequest to be nil, got {}") 309 } 310 } 311 } 312 } 313 314 func TestDecodeIngressRuleName(t *testing.T) { 315 cases := []struct { 316 ingressName string 317 ruleNum int 318 pathNum int 319 }{ 320 {"myingress", 0, 0}, 321 {"myingress", 1, 2}, 322 {"my-ingress", 1, 2}, 323 {"my-cool-ingress", 1, 2}, 324 } 325 326 for _, c := range cases { 327 encoded := EncodeIngressRuleName(c.ingressName, c.ruleNum, c.pathNum) 328 ingressName, ruleNum, pathNum, err := decodeIngressRuleName(encoded) 329 if err != nil { 330 t.Errorf("decodeIngressRuleName(%q) => error %v", encoded, err) 331 } 332 if ingressName != c.ingressName || ruleNum != c.ruleNum || pathNum != c.pathNum { 333 t.Errorf("decodeIngressRuleName(%q) => (%q, %d, %d), want (%q, %d, %d)", 334 encoded, 335 ingressName, ruleNum, pathNum, 336 c.ingressName, c.ruleNum, c.pathNum, 337 ) 338 } 339 } 340 } 341 342 func TestEncoding(t *testing.T) { 343 if got := EncodeIngressRuleName("name", 3, 5); got != "name-3-5" { 344 t.Errorf("unexpected ingress encoding %q", got) 345 } 346 347 cases := []string{ 348 "name", 349 "name-path-5", 350 "name-3-path", 351 } 352 for _, code := range cases { 353 if _, _, _, err := decodeIngressRuleName(code); err == nil { 354 t.Errorf("expected error on decoding %q", code) 355 } 356 } 357 } 358 359 func TestIngressClass(t *testing.T) { 360 istio := mesh.DefaultMeshConfig().IngressClass 361 ingressClassIstio := &knetworking.IngressClass{ 362 ObjectMeta: metav1.ObjectMeta{ 363 Name: "istio", 364 }, 365 Spec: knetworking.IngressClassSpec{ 366 Controller: IstioIngressController, 367 }, 368 } 369 ingressClassOther := &knetworking.IngressClass{ 370 ObjectMeta: metav1.ObjectMeta{ 371 Name: "foo", 372 }, 373 Spec: knetworking.IngressClassSpec{ 374 Controller: "foo", 375 }, 376 } 377 cases := []struct { 378 annotation string 379 ingressClass *knetworking.IngressClass 380 ingressMode meshconfig.MeshConfig_IngressControllerMode 381 shouldProcess bool 382 }{ 383 // Annotation 384 {ingressMode: meshconfig.MeshConfig_DEFAULT, annotation: "nginx", shouldProcess: false}, 385 {ingressMode: meshconfig.MeshConfig_STRICT, annotation: "nginx", shouldProcess: false}, 386 {ingressMode: meshconfig.MeshConfig_OFF, annotation: istio, shouldProcess: false}, 387 {ingressMode: meshconfig.MeshConfig_DEFAULT, annotation: istio, shouldProcess: true}, 388 {ingressMode: meshconfig.MeshConfig_STRICT, annotation: istio, shouldProcess: true}, 389 {ingressMode: meshconfig.MeshConfig_DEFAULT, annotation: "", shouldProcess: true}, 390 {ingressMode: meshconfig.MeshConfig_STRICT, annotation: "", shouldProcess: false}, 391 392 // IngressClass 393 {ingressMode: meshconfig.MeshConfig_DEFAULT, ingressClass: ingressClassOther, shouldProcess: false}, 394 {ingressMode: meshconfig.MeshConfig_STRICT, ingressClass: ingressClassOther, shouldProcess: false}, 395 {ingressMode: meshconfig.MeshConfig_DEFAULT, ingressClass: ingressClassIstio, shouldProcess: true}, 396 {ingressMode: meshconfig.MeshConfig_STRICT, ingressClass: ingressClassIstio, shouldProcess: true}, 397 {ingressMode: meshconfig.MeshConfig_DEFAULT, ingressClass: nil, shouldProcess: true}, 398 {ingressMode: meshconfig.MeshConfig_STRICT, ingressClass: nil, shouldProcess: false}, 399 400 // IngressClass and Annotation 401 // note: k8s rejects Ingress resources configured with kubernetes.io/ingress.class annotation *and* ingressClassName field so this shouldn't happen 402 // see https://github.com/kubernetes/kubernetes/blob/ededd08ba131b727e60f663bd7217fffaaccd448/pkg/apis/networking/validation/validation.go#L226 403 {ingressMode: meshconfig.MeshConfig_STRICT, ingressClass: ingressClassIstio, annotation: "nginx", shouldProcess: false}, 404 {ingressMode: meshconfig.MeshConfig_STRICT, ingressClass: ingressClassOther, annotation: istio, shouldProcess: true}, 405 {ingressMode: -1, shouldProcess: false}, 406 } 407 408 for i, c := range cases { 409 className := "" 410 if c.ingressClass != nil { 411 className = c.ingressClass.Name 412 } 413 t.Run(fmt.Sprintf("%d %s %s %s", i, c.ingressMode, c.annotation, className), func(t *testing.T) { 414 ing := knetworking.Ingress{ 415 ObjectMeta: metav1.ObjectMeta{ 416 Name: "test-ingress", 417 Namespace: "default", 418 Annotations: make(map[string]string), 419 }, 420 Spec: knetworking.IngressSpec{ 421 DefaultBackend: &knetworking.IngressBackend{ 422 Service: &knetworking.IngressServiceBackend{ 423 Name: "default-http-backend", 424 Port: knetworking.ServiceBackendPort{Number: 8000}, 425 }, 426 }, 427 }, 428 } 429 430 mesh := mesh.DefaultMeshConfig() 431 mesh.IngressControllerMode = c.ingressMode 432 433 if c.annotation != "" { 434 ing.Annotations["kubernetes.io/ingress.class"] = c.annotation 435 } 436 437 if c.shouldProcess != shouldProcessIngressWithClass(mesh, &ing, c.ingressClass) { 438 t.Errorf("got %v, want %v", 439 !c.shouldProcess, c.shouldProcess) 440 } 441 }) 442 } 443 } 444 445 func TestNamedPortIngressConversion(t *testing.T) { 446 ingress := knetworking.Ingress{ 447 ObjectMeta: metav1.ObjectMeta{ 448 Namespace: "mock", 449 }, 450 Spec: knetworking.IngressSpec{ 451 Rules: []knetworking.IngressRule{ 452 { 453 Host: "host.com", 454 IngressRuleValue: knetworking.IngressRuleValue{ 455 HTTP: &knetworking.HTTPIngressRuleValue{ 456 Paths: []knetworking.HTTPIngressPath{ 457 { 458 Path: "/test/*", 459 Backend: knetworking.IngressBackend{ 460 Service: &knetworking.IngressServiceBackend{ 461 Name: "foo", 462 Port: knetworking.ServiceBackendPort{Name: "test-svc-port"}, 463 }, 464 }, 465 }, 466 }, 467 }, 468 }, 469 }, 470 }, 471 }, 472 } 473 service := &corev1.Service{ 474 ObjectMeta: metav1.ObjectMeta{ 475 Name: "foo", 476 Namespace: "mock", 477 }, 478 Spec: corev1.ServiceSpec{ 479 Ports: []corev1.ServicePort{ 480 { 481 Name: "test-svc-port", 482 Protocol: "TCP", 483 Port: 8888, 484 TargetPort: intstr.IntOrString{ 485 Type: intstr.String, 486 StrVal: "test-port", 487 }, 488 }, 489 }, 490 Selector: map[string]string{ 491 "app": "test-app", 492 }, 493 }, 494 } 495 serviceLister := createFakeClient(t, service) 496 cfgs := map[string]*config.Config{} 497 ConvertIngressVirtualService(ingress, "mydomain", cfgs, serviceLister) 498 if len(cfgs) != 1 { 499 t.Error("VirtualServices, expected 1 got ", len(cfgs)) 500 } 501 if cfgs["host.com"] == nil { 502 t.Error("Host, found nil") 503 } 504 vs := cfgs["host.com"].Spec.(*networking.VirtualService) 505 if len(vs.Http) != 1 { 506 t.Error("HttpSpec, expected 1 got ", len(vs.Http)) 507 } 508 http := vs.Http[0] 509 if len(http.Route) != 1 { 510 t.Error("Route, expected 1 got ", len(http.Route)) 511 } 512 route := http.Route[0] 513 if route.Destination.Port.Number != 8888 { 514 t.Error("PortNumer, expected 8888 got ", route.Destination.Port.Number) 515 } 516 } 517 518 func createFakeClient(t test.Failer, objects ...runtime.Object) kclient.Client[*corev1.Service] { 519 kc := kube.NewFakeClient(objects...) 520 stop := test.NewStop(t) 521 services := kclient.New[*corev1.Service](kc) 522 kc.RunAndWait(stop) 523 kube.WaitForCacheSync("test", stop, services.HasSynced) 524 return services 525 }