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  }