diff --git a/go.mod b/go.mod index 6157f2469..728a58139 100644 --- a/go.mod +++ b/go.mod @@ -155,7 +155,6 @@ require ( github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.0 // indirect github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect github.com/yl2chen/cidranger v1.0.2 // indirect diff --git a/go.sum b/go.sum index a28755eb5..2586c4a21 100644 --- a/go.sum +++ b/go.sum @@ -253,7 +253,6 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220520190051-1e77728a1eaa h1:B/lvg4tQ5hfFZd4V2hcSfFVfUvAK6GSFKxIIzwnkv8g= @@ -1259,7 +1258,6 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -1271,7 +1269,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= diff --git a/pkg/ingress/kube/ingress/controller.go b/pkg/ingress/kube/ingress/controller.go index cfb9302e5..57a4d01a9 100644 --- a/pkg/ingress/kube/ingress/controller.go +++ b/pkg/ingress/kube/ingress/controller.go @@ -348,6 +348,13 @@ func extractTLSSecretName(host string, tls []ingress.IngressTLS) string { } func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error { + if convertOptions == nil { + return fmt.Errorf("convertOptions is nil") + } + if wrapper == nil { + return fmt.Errorf("wrapperConfig is nil") + } + // Ignore canary config. if wrapper.AnnotationsConfig.IsCanary() { return nil @@ -455,6 +462,13 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp } func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error { + if convertOptions == nil { + return fmt.Errorf("convertOptions is nil") + } + if wrapper == nil { + return fmt.Errorf("wrapperConfig is nil") + } + // Canary ingress will be processed in the end. if wrapper.AnnotationsConfig.IsCanary() { convertOptions.CanaryIngresses = append(convertOptions.CanaryIngresses, wrapper) @@ -618,6 +632,13 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra } func (c *controller) ApplyDefaultBackend(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error { + if convertOptions == nil { + return fmt.Errorf("convertOptions is nil") + } + if wrapper == nil { + return fmt.Errorf("wrapperConfig is nil") + } + if wrapper.AnnotationsConfig.IsCanary() { return nil } @@ -688,6 +709,13 @@ func (c *controller) ApplyDefaultBackend(convertOptions *common.ConvertOptions, } func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error { + if convertOptions == nil { + return fmt.Errorf("convertOptions is nil") + } + if wrapper == nil { + return fmt.Errorf("wrapperConfig is nil") + } + byHeader, byWeight := wrapper.AnnotationsConfig.CanaryKind() cfg := wrapper.Config @@ -820,6 +848,13 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w } func (c *controller) ConvertTrafficPolicy(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error { + if convertOptions == nil { + return fmt.Errorf("convertOptions is nil") + } + if wrapper == nil { + return fmt.Errorf("wrapperConfig is nil") + } + if !wrapper.AnnotationsConfig.NeedTrafficPolicy() { return nil } @@ -941,6 +976,10 @@ func (c *controller) createDefaultRoute(wrapper *common.WrapperConfig, backend * } func (c *controller) createServiceKey(service *ingress.IngressBackend, namespace string) (common.ServiceKey, error) { + if service == nil { + return common.ServiceKey{}, fmt.Errorf("ingressBackend is nil") + } + serviceKey := common.ServiceKey{} if service.ServiceName == "" { return serviceKey, errors.New("service name is empty") @@ -965,7 +1004,7 @@ func (c *controller) createServiceKey(service *ingress.IngressBackend, namespace } func isCanaryRoute(canary, route *common.WrapperHTTPRoute) bool { - return !route.WrapperConfig.AnnotationsConfig.IsCanary() && canary.OriginPath == route.OriginPath && + return route != nil && canary != nil && !route.WrapperConfig.AnnotationsConfig.IsCanary() && canary.OriginPath == route.OriginPath && canary.OriginPathType == route.OriginPathType } @@ -1016,6 +1055,10 @@ func (c *controller) backendToRouteDestination(backend *ingress.IngressBackend, } func resolveNamedPort(backend *ingress.IngressBackend, namespace string, serviceLister listerv1.ServiceLister) (int32, error) { + if backend == nil { + return 0, fmt.Errorf("ingressBackend is nil") + } + svc, err := serviceLister.Services(namespace).Get(backend.ServiceName) if err != nil { return 0, err @@ -1132,6 +1175,10 @@ func (c *controller) shouldProcessIngressUpdate(ing *ingress.Ingress) (bool, err // setDefaultMSEIngressOptionalField sets a default value for optional fields when is not defined. func setDefaultMSEIngressOptionalField(ing *ingress.Ingress) { + if ing == nil { + return + } + for idx, tls := range ing.Spec.TLS { if len(tls.Hosts) == 0 { ing.Spec.TLS[idx].Hosts = []string{common.DefaultHost} diff --git a/pkg/ingress/kube/ingress/controller_test.go b/pkg/ingress/kube/ingress/controller_test.go index 9fcc12b24..8a22bb0d3 100644 --- a/pkg/ingress/kube/ingress/controller_test.go +++ b/pkg/ingress/kube/ingress/controller_test.go @@ -15,14 +15,1229 @@ package ingress import ( + "context" "testing" + "time" + "istio.io/api/networking/v1alpha3" + "istio.io/istio/pilot/pkg/model" + "istio.io/istio/pkg/config" + "istio.io/istio/pkg/config/schema/gvk" + "istio.io/istio/pkg/kube/controllers" + v1 "k8s.io/api/core/v1" "k8s.io/api/networking/v1beta1" + ingress "k8s.io/api/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/util/workqueue" + "github.com/alibaba/higress/pkg/ingress/kube/annotations" "github.com/alibaba/higress/pkg/ingress/kube/common" + "github.com/alibaba/higress/pkg/ingress/kube/secret" + "github.com/alibaba/higress/pkg/kube" + "github.com/stretchr/testify/require" ) +func TestIngressControllerApplies(t *testing.T) { + fakeClient := kube.NewFakeClient() + localKubeClient, client := fakeClient, fakeClient + + options := common.Options{IngressClass: "mse", ClusterId: ""} + + secretController := secret.NewController(localKubeClient, options.ClusterId) + ingressController := NewController(localKubeClient, client, options, secretController) + + testcases := map[string]func(*testing.T, common.IngressController){ + "test apply canary ingress": testApplyCanaryIngress, + "test apply default backend": testApplyDefaultBackend, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + tc(t, ingressController) + }) + } +} + +func testApplyCanaryIngress(t *testing.T, c common.IngressController) { + testcases := []struct { + description string + input struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + } + expectNoError bool + }{ + { + description: "convertOptions is nil", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: nil, + wrapperConfig: nil, + }, + expectNoError: false, + }, { + description: "convertOptions is not nil but empty", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{}, + wrapperConfig: &common.WrapperConfig{ + Config: &config.Config{}, + AnnotationsConfig: &annotations.Ingress{}, + }, + }, + expectNoError: false, + }, + { + description: "valid canary ingress", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{ + IngressDomainCache: &common.IngressDomainCache{ + Valid: make(map[string]*common.IngressDomainBuilder), + Invalid: make([]model.IngressDomain, 0), + }, + HostAndPath2Ingress: make(map[string]*config.Config), + VirtualServices: make(map[string]*common.WrapperVirtualService), + Gateways: make(map[string]*common.WrapperGateway), + IngressRouteCache: &common.IngressRouteCache{}, + HTTPRoutes: map[string][]*common.WrapperHTTPRoute{ + "test1": make([]*common.WrapperHTTPRoute, 0), + }, + }, + wrapperConfig: &common.WrapperConfig{Config: &config.Config{ + Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{ + { + Host: "test1", + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + PathType: &defaultPathType, + }, + }, + }, + }, + }, + }, + Backend: &ingress.IngressBackend{}, + TLS: []ingress.IngressTLS{ + { + Hosts: []string{"test1", "test2"}, + SecretName: "test", + }, + }}, + }, AnnotationsConfig: &annotations.Ingress{}}, + }, + expectNoError: true, + }, + } + + for _, testcase := range testcases { + err := c.ApplyCanaryIngress(testcase.input.options, testcase.input.wrapperConfig) + if err != nil { + require.Equal(t, testcase.expectNoError, false) + } else { + require.Equal(t, testcase.expectNoError, true) + } + } +} + +func testApplyDefaultBackend(t *testing.T, c common.IngressController) { + testcases := []struct { + description string + input struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + } + expectNoError bool + }{ + { + description: "convertOptions is nil", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: nil, + wrapperConfig: nil, + }, + expectNoError: false, + }, { + description: "convertOptions is not nil but empty", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{}, + wrapperConfig: &common.WrapperConfig{ + Config: &config.Config{}, + AnnotationsConfig: &annotations.Ingress{}, + }, + }, + expectNoError: false, + }, { + description: "valid default backend", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{ + IngressDomainCache: &common.IngressDomainCache{ + Valid: make(map[string]*common.IngressDomainBuilder), + Invalid: make([]model.IngressDomain, 0), + }, + HostAndPath2Ingress: make(map[string]*config.Config), + VirtualServices: make(map[string]*common.WrapperVirtualService), + Gateways: make(map[string]*common.WrapperGateway), + IngressRouteCache: &common.IngressRouteCache{}, + HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute), + }, + wrapperConfig: &common.WrapperConfig{Config: &config.Config{ + Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{ + { + Host: "test1", + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + PathType: &defaultPathType, + }, + }, + }, + }, + }, + }, + Backend: &ingress.IngressBackend{}, + TLS: []ingress.IngressTLS{ + { + Hosts: []string{"test1", "test2"}, + SecretName: "test", + }, + }}, + }, AnnotationsConfig: &annotations.Ingress{}}, + }, + expectNoError: true, + }, + } + + for _, testcase := range testcases { + err := c.ApplyDefaultBackend(testcase.input.options, testcase.input.wrapperConfig) + if err != nil { + require.Equal(t, testcase.expectNoError, false) + } else { + require.Equal(t, testcase.expectNoError, true) + } + } +} + +func TestIngressControllerConventions(t *testing.T) { + fakeClient := kube.NewFakeClient() + localKubeClient, client := fakeClient, fakeClient + + options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true} + + secretController := secret.NewController(localKubeClient, options.ClusterId) + ingressController := NewController(localKubeClient, client, options, secretController) + + testcases := map[string]func(*testing.T, common.IngressController){ + "test convert Gateway": testConvertGateway, + "test convert HTTPRoute": testConvertHTTPRoute, + "test convert TrafficPolicy": testConvertTrafficPolicy, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + tc(t, ingressController) + }) + } +} + +func testConvertGateway(t *testing.T, c common.IngressController) { + testcases := []struct { + description string + input struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + } + expectNoError bool + }{ + { + description: "convertOptions is nil", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: nil, + wrapperConfig: nil, + }, + expectNoError: false, + }, { + description: "convertOptions is not nil but empty", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{}, + wrapperConfig: &common.WrapperConfig{ + Config: &config.Config{}, + AnnotationsConfig: &annotations.Ingress{}, + }, + }, + expectNoError: false, + }, { + description: "valid gateway convention", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{ + IngressDomainCache: &common.IngressDomainCache{ + Valid: make(map[string]*common.IngressDomainBuilder), + Invalid: make([]model.IngressDomain, 0), + }, + Gateways: make(map[string]*common.WrapperGateway), + }, + wrapperConfig: &common.WrapperConfig{Config: &config.Config{ + Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{ + { + Host: "test1", + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + }, + }, + }, + }, + }, + }, + Backend: &ingress.IngressBackend{}, + TLS: []ingress.IngressTLS{ + { + Hosts: []string{"test1", "test2"}, + SecretName: "test", + }, + }}, + }, AnnotationsConfig: &annotations.Ingress{}}, + }, + expectNoError: true, + }, + } + + for _, testcase := range testcases { + err := c.ConvertGateway(testcase.input.options, testcase.input.wrapperConfig) + if err != nil { + require.Equal(t, testcase.expectNoError, false) + } else { + require.Equal(t, testcase.expectNoError, true) + } + } +} + +func testConvertHTTPRoute(t *testing.T, c common.IngressController) { + testcases := []struct { + description string + input struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + } + expectNoError bool + }{ + { + description: "convertOptions is nil", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: nil, + wrapperConfig: nil, + }, + expectNoError: false, + }, { + description: "convertOptions is not nil but empty", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{}, + wrapperConfig: &common.WrapperConfig{ + Config: &config.Config{}, + AnnotationsConfig: &annotations.Ingress{}, + }, + }, + expectNoError: false, + }, { + description: "valid httpRoute convention", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{ + IngressDomainCache: &common.IngressDomainCache{ + Valid: make(map[string]*common.IngressDomainBuilder), + Invalid: make([]model.IngressDomain, 0), + }, + HostAndPath2Ingress: make(map[string]*config.Config), + VirtualServices: make(map[string]*common.WrapperVirtualService), + Gateways: make(map[string]*common.WrapperGateway), + IngressRouteCache: &common.IngressRouteCache{}, + HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute), + }, + wrapperConfig: &common.WrapperConfig{Config: &config.Config{ + Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{ + { + Host: "test1", + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + PathType: &defaultPathType, + }, + }, + }, + }, + }, + }, + Backend: &ingress.IngressBackend{}, + TLS: []ingress.IngressTLS{ + { + Hosts: []string{"test1", "test2"}, + SecretName: "test", + }, + }}, + }, AnnotationsConfig: &annotations.Ingress{}, + }, + }, + expectNoError: true, + }, + } + + for _, testcase := range testcases { + err := c.ConvertHTTPRoute(testcase.input.options, testcase.input.wrapperConfig) + if err != nil { + require.Equal(t, testcase.expectNoError, false) + } else { + require.Equal(t, testcase.expectNoError, true) + } + } +} + +func testConvertTrafficPolicy(t *testing.T, c common.IngressController) { + testcases := []struct { + description string + input struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + } + expectNoError bool + }{ + { + description: "convertOptions is nil", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: nil, + wrapperConfig: nil, + }, + expectNoError: false, + }, { + description: "convertOptions is not nil but empty", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{}, + wrapperConfig: &common.WrapperConfig{ + Config: &config.Config{}, + AnnotationsConfig: &annotations.Ingress{}, + }, + }, + expectNoError: true, + }, { + description: "valid trafficPolicy convention", + input: struct { + options *common.ConvertOptions + wrapperConfig *common.WrapperConfig + }{ + options: &common.ConvertOptions{ + IngressDomainCache: &common.IngressDomainCache{ + Valid: make(map[string]*common.IngressDomainBuilder), + Invalid: make([]model.IngressDomain, 0), + }, + HostAndPath2Ingress: make(map[string]*config.Config), + VirtualServices: make(map[string]*common.WrapperVirtualService), + Gateways: make(map[string]*common.WrapperGateway), + IngressRouteCache: &common.IngressRouteCache{}, + Service2TrafficPolicy: make(map[common.ServiceKey]*common.WrapperTrafficPolicy), + HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute), + }, + wrapperConfig: &common.WrapperConfig{Config: &config.Config{ + Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{ + { + Host: "test1", + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + PathType: &defaultPathType, + Backend: ingress.IngressBackend{ + ServiceName: "test", + ServicePort: intstr.FromInt(8080), + }, + }, + }, + }, + }, + }, + }, + Backend: &ingress.IngressBackend{ + ServiceName: "test", + }, + TLS: []ingress.IngressTLS{ + { + Hosts: []string{"test1", "test2"}, + SecretName: "test", + }, + }}, + }, AnnotationsConfig: &annotations.Ingress{ + LoadBalance: &annotations.LoadBalanceConfig{}, + }}, + }, + expectNoError: true, + }, + } + + for _, testcase := range testcases { + err := c.ConvertTrafficPolicy(testcase.input.options, testcase.input.wrapperConfig) + if err != nil { + require.Equal(t, testcase.expectNoError, false) + } else { + require.Equal(t, testcase.expectNoError, true) + } + } +} + +func TestIngressControllerGenerations(t *testing.T) { + c := &controller{ + options: common.Options{ + IngressClass: "mse", + SystemNamespace: "higress-system", + }, + ingresses: make(map[string]*v1beta1.Ingress), + } + + testcases := map[string]func(*testing.T, *controller){ + "test create DefaultRoute": testcreateDefaultRoute, + "test create ServiceKey": testcreateServiceKey, + "test backend to RouteDestination": testbackendToRouteDestination, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + tc(t, c) + }) + } +} + +func testcreateDefaultRoute(t *testing.T, c *controller) { + testcases := []struct { + input struct { + wrapper *common.WrapperConfig + backend *ingress.IngressBackend + host string + } + description string + expect *common.WrapperHTTPRoute + }{ + { + input: struct { + wrapper *common.WrapperConfig + backend *ingress.IngressBackend + host string + }{ + wrapper: nil, + backend: nil, + host: "", + }, + description: "wrapperConfig is nil", + expect: nil, + }, + { + input: struct { + wrapper *common.WrapperConfig + backend *ingress.IngressBackend + host string + }{ + wrapper: &common.WrapperConfig{}, + backend: &ingress.IngressBackend{}, + host: "test", + }, + description: "wrapperConfig is not nil but empty", + expect: nil, + }, + { + input: struct { + wrapper *common.WrapperConfig + backend *ingress.IngressBackend + host string + }{ + wrapper: &common.WrapperConfig{ + Config: &config.Config{ + Meta: config.Meta{ + Namespace: "higress-system", + Name: "test", + }, + }, + AnnotationsConfig: &annotations.Ingress{}}, + backend: &ingress.IngressBackend{ + ServiceName: "test", + ServicePort: intstr.FromInt(8088), + }, + host: "test", + }, + description: "create expected httpRoute", + expect: &common.WrapperHTTPRoute{ + WrapperConfig: &common.WrapperConfig{ + Config: &config.Config{ + Meta: config.Meta{ + Name: "test", + Namespace: "higress-system", + }, + }, + AnnotationsConfig: &annotations.Ingress{}, + }, + RawClusterId: "", + ClusterId: "", + ClusterName: "", + Host: "test", + OriginPath: "/", + OriginPathType: "prefix", + WeightTotal: 0, + IsDefaultBackend: true, + HTTPRoute: &v1alpha3.HTTPRoute{ + Name: "test-default", + Route: []*v1alpha3.HTTPRouteDestination{ + { + Weight: 100, + Destination: &v1alpha3.Destination{ + Port: &v1alpha3.PortSelector{ + Number: 8088, + }, + Host: "test.higress-system.svc.cluster.local", + }, + }, + }, + }, + }, + }, + } + + for _, testcase := range testcases { + httpRoute := c.createDefaultRoute(testcase.input.wrapper, testcase.input.backend, testcase.input.host) + require.Equal(t, testcase.expect, httpRoute) + } +} + +func testcreateServiceKey(t *testing.T, c *controller) { + testcases := []struct { + input struct { + backend *ingress.IngressBackend + namespace string + } + expectNoError bool + description string + }{ + { + description: "nil", + expectNoError: false, + input: struct { + backend *ingress.IngressBackend + namespace string + }{ + backend: nil, + namespace: "", + }, + }, + { + description: "nil", + expectNoError: false, + input: struct { + backend *ingress.IngressBackend + namespace string + }{ + backend: &ingress.IngressBackend{}, + namespace: "", + }, + }, + { + description: "create success", + expectNoError: true, + input: struct { + backend *ingress.IngressBackend + namespace string + }{ + backend: &ingress.IngressBackend{ + ServiceName: "test", + ServicePort: intstr.FromInt(8080), + }, + namespace: "default", + }, + }, + } + + for _, testcase := range testcases { + _, err := c.createServiceKey(testcase.input.backend, testcase.input.namespace) + if err != nil { + require.Equal(t, testcase.expectNoError, false) + } else { + require.Equal(t, testcase.expectNoError, true) + } + } +} + +func testbackendToRouteDestination(t *testing.T, c *controller) { + testcases := []struct { + input struct { + backend *ingress.IngressBackend + namespace string + builder *common.IngressRouteBuilder + config *annotations.DestinationConfig + } + expectNoError bool + description string + }{ + { + description: "nil", + expectNoError: false, + input: struct { + backend *ingress.IngressBackend + namespace string + builder *common.IngressRouteBuilder + config *annotations.DestinationConfig + }{ + backend: nil, + namespace: "", + builder: nil, + config: nil, + }, + }, + { + description: "nil", + expectNoError: false, + input: struct { + backend *ingress.IngressBackend + namespace string + builder *common.IngressRouteBuilder + config *annotations.DestinationConfig + }{ + backend: &ingress.IngressBackend{ServiceName: ""}, + namespace: "", + builder: nil, + config: nil, + }, + }, + { + description: "create success", + expectNoError: true, + input: struct { + backend *ingress.IngressBackend + namespace string + builder *common.IngressRouteBuilder + config *annotations.DestinationConfig + }{ + backend: &ingress.IngressBackend{ + ServiceName: "test", + ServicePort: intstr.FromInt(8080), + }, + namespace: "default", + builder: &common.IngressRouteBuilder{}, + config: nil, + }, + }, + } + + for _, testcase := range testcases { + _, err := c.backendToRouteDestination( + testcase.input.backend, + testcase.input.namespace, + testcase.input.builder, + testcase.input.config, + ) + + if err == common.InvalidBackendService { + require.Equal(t, testcase.expectNoError, false) + } else { + require.Equal(t, testcase.expectNoError, true) + } + } +} + +func TestIsCanaryRoute(t *testing.T) { + testcases := []struct { + input struct { + canary *common.WrapperHTTPRoute + route *common.WrapperHTTPRoute + } + expect bool + description string + }{ + { + input: struct { + canary *common.WrapperHTTPRoute + route *common.WrapperHTTPRoute + }{ + canary: nil, + route: nil, + }, + expect: false, + description: "both are nil", + }, { + input: struct { + canary *common.WrapperHTTPRoute + route *common.WrapperHTTPRoute + }{ + canary: &common.WrapperHTTPRoute{ + OriginPathType: common.Exact, + OriginPath: "/test", + }, + route: &common.WrapperHTTPRoute{ + WrapperConfig: &common.WrapperConfig{ + AnnotationsConfig: &annotations.Ingress{ + Canary: nil, + }, + }, + OriginPathType: common.Exact, + OriginPath: "/test", + }, + }, + expect: true, + description: "canary is nil", + }, { + input: struct { + canary *common.WrapperHTTPRoute + route *common.WrapperHTTPRoute + }{ + canary: &common.WrapperHTTPRoute{ + OriginPathType: common.Exact, + OriginPath: "/test", + }, + route: &common.WrapperHTTPRoute{ + WrapperConfig: &common.WrapperConfig{ + AnnotationsConfig: &annotations.Ingress{ + Canary: &annotations.CanaryConfig{ + Enabled: true, + }, + }, + }, + OriginPathType: common.Exact, + OriginPath: "/test", + }, + }, + expect: false, + description: "canary is not nil", + }, + } + for _, testcase := range testcases { + actual := isCanaryRoute(testcase.input.canary, testcase.input.route) + require.Equal(t, testcase.expect, actual) + } +} + +func TestExtractTLSSecretName(t *testing.T) { + testcases := []struct { + input struct { + host string + tls []ingress.IngressTLS + } + expect string + description string + }{ + { + input: struct { + host string + tls []ingress.IngressTLS + }{ + host: "", + tls: nil, + }, + expect: "", + description: "both are nil", + }, + { + input: struct { + host string + tls []ingress.IngressTLS + }{ + host: "test", + tls: []ingress.IngressTLS{ + { + Hosts: []string{"test"}, + SecretName: "test-secret", + }, + { + Hosts: []string{"test1"}, + SecretName: "test1-secret", + }, + }, + }, + expect: "test-secret", + description: "found secret name", + }, + } + + for _, testcase := range testcases { + actual := extractTLSSecretName(testcase.input.host, testcase.input.tls) + require.Equal(t, testcase.expect, actual) + } +} + +func TestSetDefaultMSEIngressOptionalField(t *testing.T) { + pathType := ingress.PathTypeImplementationSpecific + testcases := []struct { + input struct { + ing *ingress.Ingress + } + expect *ingress.Ingress + description string + }{ + { + input: struct{ ing *ingress.Ingress }{ + ing: nil, + }, + expect: nil, + description: "nil", + }, + { + input: struct{ ing *ingress.Ingress }{ + ing: &ingress.Ingress{}, + }, + expect: &ingress.Ingress{}, + description: "nil", + }, { + input: struct{ ing *ingress.Ingress }{ + ing: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + }, + }, + }, + }, + }, + expect: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"*"}, + }, + }, + }, + }, + description: "tls host is empty", + }, { + input: struct{ ing *ingress.Ingress }{ + ing: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"www.example.com"}, + }, + }, + }, + }, + }, + expect: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"www.example.com"}, + }, + }, + }, + }, + description: "tls host is not empty", + }, { + input: struct{ ing *ingress.Ingress }{ + ing: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + Rules: []ingress.IngressRule{ + { + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: nil, + }, + }, + }, + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"www.example.com"}, + }, + }, + }, + }, + }, + expect: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + Rules: []ingress.IngressRule{ + { + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: nil, + }, + }, + }, + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"www.example.com"}, + }, + }, + }, + }, + description: "http is nil", + }, { + input: struct{ ing *ingress.Ingress }{ + ing: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + Rules: []ingress.IngressRule{ + { + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + PathType: &defaultPathType, + Backend: ingress.IngressBackend{}, + }, + }, + }, + }, + }, + }, + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"www.example.com"}, + }, + }, + }, + }, + }, + expect: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + Rules: []ingress.IngressRule{ + { + Host: "*", + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + PathType: &defaultPathType, + Backend: ingress.IngressBackend{}, + }, + }, + }, + }, + }, + }, + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"www.example.com"}, + }, + }, + }, + }, + description: "http is not nil but host is empty", + }, { + input: struct{ ing *ingress.Ingress }{ + ing: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + Rules: []ingress.IngressRule{ + { + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + PathType: &pathType, + Backend: ingress.IngressBackend{}, + }, + }, + }, + }, + }, + }, + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"www.example.com"}, + }, + }, + }, + }, + }, + expect: &ingress.Ingress{ + Spec: ingress.IngressSpec{ + Rules: []ingress.IngressRule{ + { + Host: "*", + IngressRuleValue: ingress.IngressRuleValue{ + HTTP: &ingress.HTTPIngressRuleValue{ + Paths: []ingress.HTTPIngressPath{ + { + Path: "/test", + PathType: &defaultPathType, + Backend: ingress.IngressBackend{}, + }, + }, + }, + }, + }, + }, + TLS: []ingress.IngressTLS{ + { + SecretName: "test", + Hosts: []string{"www.example.com"}, + }, + }, + }, + }, + description: "http path type is ImplementationSpecific", + }, + } + + for _, testcase := range testcases { + setDefaultMSEIngressOptionalField(testcase.input.ing) + require.Equal(t, testcase.expect, testcase.input.ing) + } +} + +func TestIngressControllerProcessing(t *testing.T) { + fakeClient := kube.NewFakeClient() + localKubeClient, _ := fakeClient, fakeClient + + options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true} + + secretController := secret.NewController(localKubeClient, options.ClusterId) + q := workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter()) + + ingressInformer := fakeClient.KubeInformer().Networking().V1beta1().Ingresses() + serviceInformer := fakeClient.KubeInformer().Core().V1().Services() + + ingressController := &controller{ + options: options, + queue: q, + ingresses: make(map[string]*ingress.Ingress), + ingressInformer: ingressInformer.Informer(), + ingressLister: ingressInformer.Lister(), + serviceInformer: serviceInformer.Informer(), + serviceLister: serviceInformer.Lister(), + secretController: secretController, + } + + handler := controllers.LatestVersionHandlerFuncs(controllers.EnqueueForSelf(q)) + ingressController.ingressInformer.AddEventHandler(handler) + + stopChan := make(chan struct{}) + t.Cleanup(func() { + time.Sleep(3 * time.Second) + stopChan <- struct{}{} + }) + + go ingressController.ingressInformer.Run(stopChan) + go ingressController.serviceInformer.Run(stopChan) + go ingressController.secretController.Informer().Run(stopChan) + + go ingressController.Run(stopChan) + go secretController.Run(stopChan) + + ingressController.RegisterEventHandler(gvk.VirtualService, func(c1, c2 config.Config, e model.Event) {}) + ingressController.RegisterEventHandler(gvk.DestinationRule, func(c1, c2 config.Config, e model.Event) {}) + ingressController.RegisterEventHandler(gvk.EnvoyFilter, func(c1, c2 config.Config, e model.Event) {}) + ingressController.RegisterEventHandler(gvk.Gateway, func(c1, c2 config.Config, e model.Event) {}) + + serviceLister := ingressController.ServiceLister() + svcObj, err := fakeClient.CoreV1().Services("default").Create(context.Background(), &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test"}}, metav1.CreateOptions{}) + require.NoError(t, err) + err = serviceInformer.Informer().GetStore().Add(svcObj) + require.NoError(t, err) + services, err := serviceLister.List(labels.Everything()) + require.NoError(t, err) + require.Equal(t, 1, len(services)) + + ingress1 := &ingress.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-1", + }, + Spec: v1beta1.IngressSpec{ + IngressClassName: &options.IngressClass, + Rules: []v1beta1.IngressRule{ + { + Host: "test.com", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/test", + }, + }, + }, + }, + }, + }, + }, + } + ingressObj, err := fakeClient.NetworkingV1beta1().Ingresses("default").Create(context.Background(), ingress1, metav1.CreateOptions{}) + require.NoError(t, err) + err = ingressController.ingressInformer.GetStore().Add(ingressObj) + require.NoError(t, err) + ingresses := ingressController.List() + require.Equal(t, 1, len(ingresses)) + + ingress2 := &ingress.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-2", + Namespace: "test-2", + }, + Spec: v1beta1.IngressSpec{ + IngressClassName: &options.IngressClass, + Rules: []v1beta1.IngressRule{ + { + Host: "test.com", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/test", + }, + }, + }, + }, + }, + }, + }, + } + err = ingressController.ingressInformer.GetStore().Add(ingress2) + require.NoError(t, err) + ingresses = ingressController.List() + require.Equal(t, 2, len(ingresses)) +} + func TestShouldProcessIngressUpdate(t *testing.T) { c := controller{ options: common.Options{