diff --git a/pkg/controller/utils/servicemesh/application.go b/pkg/controller/utils/servicemesh/application.go index abfbc8ab8ce278fdeed655428f6f4e7c1ffa9c02..2e3a016d153e08859785a4b016cf1fa1b5474bef 100644 --- a/pkg/controller/utils/servicemesh/application.go +++ b/pkg/controller/utils/servicemesh/application.go @@ -118,6 +118,7 @@ func FillDestinationPort(vs *v1alpha3.VirtualService, service *corev1.Service) { vs.Spec.Http[i].Route[j].Destination.Port = &apiv1alpha3.PortSelector{ Number: uint32(service.Spec.Ports[0].Port), } + vs.Spec.Http[i].Match[j].Port = uint32(service.Spec.Ports[0].Port) } } @@ -135,7 +136,18 @@ func FillDestinationPort(vs *v1alpha3.VirtualService, service *corev1.Service) { vs.Spec.Tcp[i].Route[j].Destination.Port = &apiv1alpha3.PortSelector{ Number: uint32(service.Spec.Ports[0].Port), } + vs.Spec.Tcp[i].Match[j].Port = uint32(service.Spec.Ports[0].Port) } } } } + +func SupportHttpProtocol(protocol string) bool { + httpPro := []string{"http", "http2", "grpc"} + for _, i := range httpPro { + if strings.ToLower(protocol) == i || strings.HasPrefix(strings.ToLower(protocol), i+"-") { + return true + } + } + return false +} diff --git a/pkg/controller/utils/servicemesh/application_test.go b/pkg/controller/utils/servicemesh/application_test.go new file mode 100644 index 0000000000000000000000000000000000000000..20b044cd372c5df78e2bc80a6d5e73bf45bc63c2 --- /dev/null +++ b/pkg/controller/utils/servicemesh/application_test.go @@ -0,0 +1,21 @@ +package servicemesh + +import "testing" + +func TestIsHTTP(t *testing.T) { + if !SupportHttpProtocol("gRPC") { + t.Errorf("gRPC is HTTP protocol") + } + if !SupportHttpProtocol("HTTP") { + t.Errorf("HTTP is HTTP protocol") + } + if !SupportHttpProtocol("HTTP2") { + t.Errorf("HTTP2 is HTTP protocol") + } + if SupportHttpProtocol("Mysql") { + t.Errorf("mysql is not HTTP protocol") + } + if SupportHttpProtocol("udp") { + t.Errorf("UDP is not HTTP protocol") + } +} diff --git a/pkg/controller/virtualservice/virtualservice_controller.go b/pkg/controller/virtualservice/virtualservice_controller.go index e5f6c491a0047cdb386140b6a00cc0a941e7f481..ca0fde49ac1db8fce4456c4143dd1492ead952f3 100644 --- a/pkg/controller/virtualservice/virtualservice_controller.go +++ b/pkg/controller/virtualservice/virtualservice_controller.go @@ -19,10 +19,6 @@ package virtualservice import ( "context" "fmt" - "kubesphere.io/kubesphere/pkg/controller/utils/servicemesh" - "reflect" - "strings" - apinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" clientgonetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istioclient "istio.io/client-go/pkg/clientset/versioned" @@ -49,6 +45,8 @@ import ( servicemeshclient "kubesphere.io/kubesphere/pkg/client/clientset/versioned" servicemeshinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/servicemesh/v1alpha2" servicemeshlisters "kubesphere.io/kubesphere/pkg/client/listers/servicemesh/v1alpha2" + "kubesphere.io/kubesphere/pkg/controller/utils/servicemesh" + "reflect" "time" ) @@ -320,9 +318,13 @@ func (v *VirtualServiceController) syncService(key string) error { // TODO(jeff): use FQDN to replace service name vs.Spec.Hosts = []string{name} + vs.Spec.Http = []*apinetworkingv1alpha3.HTTPRoute{} + vs.Spec.Tcp = []*apinetworkingv1alpha3.TCPRoute{} + // check if service has TCP protocol ports for _, port := range service.Spec.Ports { var route apinetworkingv1alpha3.HTTPRouteDestination + var match apinetworkingv1alpha3.HTTPMatchRequest if port.Protocol == v1.ProtocolTCP { route = apinetworkingv1alpha3.HTTPRouteDestination{ Destination: &apinetworkingv1alpha3.Destination{ @@ -335,22 +337,29 @@ func (v *VirtualServiceController) syncService(key string) error { Weight: 100, } + match = apinetworkingv1alpha3.HTTPMatchRequest{Port: uint32(port.Port)} + // a http port, add to HTTPRoute - if len(port.Name) > 0 && (port.Name == "http" || strings.HasPrefix(port.Name, "http-")) { - vs.Spec.Http = []*apinetworkingv1alpha3.HTTPRoute{{Route: []*apinetworkingv1alpha3.HTTPRouteDestination{&route}}} - break - } - // everything else treated as TCPRoute - tcpRoute := apinetworkingv1alpha3.TCPRoute{ - Route: []*apinetworkingv1alpha3.RouteDestination{ - { - Destination: route.Destination, - Weight: route.Weight, + if servicemesh.SupportHttpProtocol(port.Name) { + httpRoute := apinetworkingv1alpha3.HTTPRoute{ + Route: []*apinetworkingv1alpha3.HTTPRouteDestination{&route}, + Match: []*apinetworkingv1alpha3.HTTPMatchRequest{&match}, + } + vs.Spec.Http = append(vs.Spec.Http, &httpRoute) + } else { + // everything else treated as TCPRoute + tcpRoute := apinetworkingv1alpha3.TCPRoute{ + Route: []*apinetworkingv1alpha3.RouteDestination{ + { + Destination: route.Destination, + Weight: route.Weight, + }, }, - }, + Match: []*apinetworkingv1alpha3.L4MatchAttributes{{Port: match.Port}}, + } + vs.Spec.Tcp = append(vs.Spec.Tcp, &tcpRoute) } - vs.Spec.Tcp = []*apinetworkingv1alpha3.TCPRoute{&tcpRoute} } } @@ -385,7 +394,6 @@ func (v *VirtualServiceController) syncService(key string) error { default: vs.Spec = v.generateVirtualServiceSpec(strategies[0], service).Spec } - } createVirtualService := len(currentVirtualService.ResourceVersion) == 0 @@ -542,8 +550,40 @@ func (v *VirtualServiceController) getSubsets(strategy *servicemeshv1alpha2.Stra func (v *VirtualServiceController) generateVirtualServiceSpec(strategy *servicemeshv1alpha2.Strategy, service *v1.Service) *clientgonetworkingv1alpha3.VirtualService { // Define VirtualService to be created - vs := &clientgonetworkingv1alpha3.VirtualService{ - Spec: strategy.Spec.Template.Spec, + vs := &clientgonetworkingv1alpha3.VirtualService{} + vs.Spec.Hosts = strategy.Spec.Template.Spec.Hosts + + // For multi-ports, apply the rules to each port matched http/tcp protocol + for _, port := range service.Spec.Ports { + s := strategy.DeepCopy() + strategyTempSpec := s.Spec.Template.Spec + // fill route.destination.port and match.port filed + if len(strategyTempSpec.Http) > 0 && servicemesh.SupportHttpProtocol(port.Name) { + for _, http := range strategyTempSpec.Http { + if len(http.Match) == 0 { + http.Match = []*apinetworkingv1alpha3.HTTPMatchRequest{{Port: uint32(port.Port)}} + } else { + for _, match := range http.Match { + match.Port = uint32(port.Port) + } + } + for _, route := range http.Route { + route.Destination.Port = &apinetworkingv1alpha3.PortSelector{ + Number: uint32(port.Port), + } + } + } + vs.Spec.Http = append(vs.Spec.Http, strategyTempSpec.Http...) + } + if len(strategyTempSpec.Tcp) > 0 && !servicemesh.SupportHttpProtocol(port.Name) { + for _, tcp := range strategyTempSpec.Tcp { + tcp.Match = []*apinetworkingv1alpha3.L4MatchAttributes{{Port: uint32(port.Port)}} + for _, r := range tcp.Route { + r.Destination.Port = &apinetworkingv1alpha3.PortSelector{Number: uint32(port.Port)} + } + } + vs.Spec.Tcp = append(vs.Spec.Tcp, strategyTempSpec.Tcp...) + } } // one version rules them all @@ -556,29 +596,34 @@ func (v *VirtualServiceController) generateVirtualServiceSpec(strategy *servicem Weight: 100, } - if len(strategy.Spec.Template.Spec.Http) > 0 { - governorRoute := apinetworkingv1alpha3.HTTPRoute{ - Route: []*apinetworkingv1alpha3.HTTPRouteDestination{&governorDestinationWeight}, - } + for _, port := range service.Spec.Ports { + match := apinetworkingv1alpha3.HTTPMatchRequest{Port: uint32(port.Port)} + if len(strategy.Spec.Template.Spec.Http) > 0 { + governorRoute := apinetworkingv1alpha3.HTTPRoute{ + Route: []*apinetworkingv1alpha3.HTTPRouteDestination{&governorDestinationWeight}, + Match: []*apinetworkingv1alpha3.HTTPMatchRequest{&match}, + } + vs.Spec.Http = []*apinetworkingv1alpha3.HTTPRoute{&governorRoute} - vs.Spec.Http = []*apinetworkingv1alpha3.HTTPRoute{&governorRoute} - } else if len(strategy.Spec.Template.Spec.Tcp) > 0 { - tcpRoute := apinetworkingv1alpha3.TCPRoute{ - Route: []*apinetworkingv1alpha3.RouteDestination{ - { - Destination: &apinetworkingv1alpha3.Destination{ - Host: governorDestinationWeight.Destination.Host, - Subset: governorDestinationWeight.Destination.Subset, + } + if len(strategy.Spec.Template.Spec.Tcp) > 0 { + tcpRoute := apinetworkingv1alpha3.TCPRoute{ + Route: []*apinetworkingv1alpha3.RouteDestination{ + { + Destination: &apinetworkingv1alpha3.Destination{ + Host: governorDestinationWeight.Destination.Host, + Subset: governorDestinationWeight.Destination.Subset, + }, + Weight: governorDestinationWeight.Weight, }, - Weight: governorDestinationWeight.Weight, }, - }, - } + Match: []*apinetworkingv1alpha3.L4MatchAttributes{{Port: match.Port}}, + } - //governorRoute := v1alpha3.TCPRoute{tcpRoute} - vs.Spec.Tcp = []*apinetworkingv1alpha3.TCPRoute{&tcpRoute} + //governorRoute := v1alpha3.TCPRoute{tcpRoute} + vs.Spec.Tcp = []*apinetworkingv1alpha3.TCPRoute{&tcpRoute} + } } - } servicemesh.FillDestinationPort(vs, service) diff --git a/pkg/controller/virtualservice/virtualservice_controller_test.go b/pkg/controller/virtualservice/virtualservice_controller_test.go index d8929e8f7342777c8dc7f4c73fd4b83bc2a0345b..8616a892cdfa8cdc116f388e8bfe169048375ad2 100644 --- a/pkg/controller/virtualservice/virtualservice_controller_test.go +++ b/pkg/controller/virtualservice/virtualservice_controller_test.go @@ -45,6 +45,9 @@ var ( applicationName = "bookinfo" namespace = metav1.NamespaceDefault subsets = []string{"v1", "v2"} + httpPort = 80 + grpcPort = 81 + mysqlPort = 82 ) type fixture struct { @@ -123,7 +126,7 @@ func newVirtualService(name string, host string, labels map[string]string) *v1al return &vr } -func newService(name string, labels map[string]string, selector map[string]string, protocol string, port int) *v1.Service { +func newService(name string, labels map[string]string, selector map[string]string) *v1.Service { svc := v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -134,9 +137,21 @@ func newService(name string, labels map[string]string, selector map[string]strin Ports: []v1.ServicePort{ { Protocol: v1.ProtocolTCP, - Port: int32(port), - Name: fmt.Sprintf("%s-aaa", protocol), - TargetPort: intstr.FromInt(port), + Port: int32(httpPort), + Name: "HTTP-1", + TargetPort: intstr.FromInt(httpPort), + }, + { + Protocol: v1.ProtocolTCP, + Port: int32(grpcPort), + Name: "grpc-1", + TargetPort: intstr.FromInt(grpcPort), + }, + { + Protocol: v1.ProtocolTCP, + Port: int32(mysqlPort), + Name: "mysql-1", + TargetPort: intstr.FromInt(mysqlPort), }, }, Selector: selector, @@ -299,7 +314,7 @@ func (f *fixture) run_(serviceKey string, expectedVS *v1alpha3.VirtualService, s func TestInitialStrategyCreate(t *testing.T) { f := newFixture(t) - svc := newService("foo", NewLabels().WithApplication(applicationName).WithApp(serviceName), NewLabels().WithApplication(serviceName).WithApp(applicationName), "http", 80) + svc := newService("foo", NewLabels().WithApplication(applicationName).WithApp(serviceName), NewLabels().WithApplication(serviceName).WithApp(applicationName)) dr := newDestinationRule(svc.Name, toHost(svc), NewLabels().WithApp("foo").WithApplication(applicationName), subsets[0]) svc.Annotations = NewLabels().WithServiceMeshEnabled(true) @@ -310,21 +325,46 @@ func TestInitialStrategyCreate(t *testing.T) { vs := newVirtualService(svc.Name, "foo", NewLabels().WithApplication("bookinfo").WithApp(svc.Name)) vs.Annotations = make(map[string]string) - vs.Spec.Http = []*apiv1alpha3.HTTPRoute{ - { - Route: []*apiv1alpha3.HTTPRouteDestination{ - { - Destination: &apiv1alpha3.Destination{ - Host: svc.Name, - Subset: "v1", - Port: &apiv1alpha3.PortSelector{ - Number: uint32(svc.Spec.Ports[0].Port), + for _, port := range svc.Spec.Ports { + if servicemesh.SupportHttpProtocol(port.Name) { + httpRoute := apiv1alpha3.HTTPRoute{ + Route: []*apiv1alpha3.HTTPRouteDestination{ + { + Destination: &apiv1alpha3.Destination{ + Host: svc.Name, + Subset: "v1", + Port: &apiv1alpha3.PortSelector{ + Number: uint32(port.Port), + }, }, + Weight: 100, }, - Weight: 100, }, - }, - }, + Match: []*apiv1alpha3.HTTPMatchRequest{ + {Port: uint32(port.Port)}, + }, + } + vs.Spec.Http = append(vs.Spec.Http, &httpRoute) + } else { + tcpRoute := apiv1alpha3.TCPRoute{ + Route: []*apiv1alpha3.RouteDestination{ + { + Destination: &apiv1alpha3.Destination{ + Host: svc.Name, + Subset: "v1", + Port: &apiv1alpha3.PortSelector{ + Number: uint32(port.Port), + }, + }, + Weight: 100, + }, + }, + Match: []*apiv1alpha3.L4MatchAttributes{ + {Port: uint32(port.Port)}, + }, + } + vs.Spec.Tcp = append(vs.Spec.Tcp, &tcpRoute) + } } key, err := cache.MetaNamespaceKeyFunc(svc) @@ -354,7 +394,7 @@ func runStrategy(t *testing.T, svc *v1.Service, dr *v1alpha3.DestinationRule, st func TestStrategies(t *testing.T) { - svc := newService(serviceName, NewLabels().WithApplication(applicationName).WithApp(serviceName), NewLabels().WithApplication(applicationName).WithApp(serviceName), "http", 80) + svc := newService(serviceName, NewLabels().WithApplication(applicationName).WithApp(serviceName), NewLabels().WithApplication(applicationName).WithApp(serviceName)) defaultDr := newDestinationRule(svc.Name, toHost(svc), NewLabels().WithApp(serviceName).WithApplication(applicationName), subsets...) svc.Annotations = NewLabels().WithServiceMeshEnabled(true) defaultStrategy := &v1alpha2.Strategy{ @@ -395,6 +435,9 @@ func TestStrategies(t *testing.T) { Weight: 20, }, }, + Match: []*apiv1alpha3.HTTPMatchRequest{ + {Port: 0}, + }, }, }, }, @@ -412,7 +455,7 @@ func TestStrategies(t *testing.T) { Host: svc.Name, Subset: "v1", Port: &apiv1alpha3.PortSelector{ - Number: uint32(svc.Spec.Ports[0].Port), + Number: uint32(httpPort), }, }, Weight: 80, @@ -422,12 +465,38 @@ func TestStrategies(t *testing.T) { Host: svc.Name, Subset: "v2", Port: &apiv1alpha3.PortSelector{ - Number: uint32(svc.Spec.Ports[0].Port), + Number: uint32(httpPort), }, }, Weight: 20, }, }, + Match: []*apiv1alpha3.HTTPMatchRequest{{Port: uint32(httpPort)}}, + }, + { + Route: []*apiv1alpha3.HTTPRouteDestination{ + { + Destination: &apiv1alpha3.Destination{ + Host: svc.Name, + Subset: "v1", + Port: &apiv1alpha3.PortSelector{ + Number: uint32(grpcPort), + }, + }, + Weight: 80, + }, + { + Destination: &apiv1alpha3.Destination{ + Host: svc.Name, + Subset: "v2", + Port: &apiv1alpha3.PortSelector{ + Number: uint32(grpcPort), + }, + }, + Weight: 20, + }, + }, + Match: []*apiv1alpha3.HTTPMatchRequest{{Port: uint32(grpcPort)}}, }, } @@ -443,6 +512,8 @@ func TestStrategies(t *testing.T) { expected := defaultExpected.DeepCopy() expected.Spec.Http[0].Route[0].Weight = 0 expected.Spec.Http[0].Route[1].Weight = 100 + expected.Spec.Http[1].Route[0].Weight = 0 + expected.Spec.Http[1].Route[1].Weight = 100 runStrategy(t, svc, defaultDr, strategy, expected) }) @@ -454,6 +525,7 @@ func TestStrategies(t *testing.T) { expected.Spec.Http[0].Route[0].Weight = 100 expected.Spec.Http[0].Route[0].Destination.Subset = "v2" expected.Spec.Http[0].Route = expected.Spec.Http[0].Route[:1] + expected.Spec.Http = expected.Spec.Http[:1] runStrategy(t, svc, defaultDr, strategy, expected) }) @@ -483,6 +555,20 @@ func TestStrategies(t *testing.T) { Uri: &apiv1alpha3.StringMatch{ MatchType: &apiv1alpha3.StringMatch_Prefix{Prefix: "/apis"}, }, + Port: expected.Spec.Http[0].Route[0].Destination.Port.Number, + }, + } + expected.Spec.Http[1].Match = []*apiv1alpha3.HTTPMatchRequest{ + { + Headers: map[string]*apiv1alpha3.StringMatch{ + "X-USER": { + MatchType: &apiv1alpha3.StringMatch_Regex{Regex: "users"}, + }, + }, + Uri: &apiv1alpha3.StringMatch{ + MatchType: &apiv1alpha3.StringMatch_Prefix{Prefix: "/apis"}, + }, + Port: expected.Spec.Http[1].Route[0].Destination.Port.Number, }, } runStrategy(t, svc, defaultDr, strategy, expected)