blob: c352366087777fd7594cba8b5ebfbd2779fad46d [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"strings"
)
import (
"github.com/gogo/protobuf/jsonpb"
"google.golang.org/protobuf/proto"
networking "istio.io/api/networking/v1alpha3"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/constants"
"github.com/apache/dubbo-go-pixiu/pkg/config/host"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk"
"github.com/apache/dubbo-go-pixiu/pkg/config/visibility"
"github.com/apache/dubbo-go-pixiu/pkg/util/sets"
)
// SelectVirtualServices selects the virtual services by matching given services' host names.
// This is a common function used by both sidecar converter and http route.
func SelectVirtualServices(virtualServices []config.Config, hosts map[string][]host.Name) []config.Config {
importedVirtualServices := make([]config.Config, 0)
vsset := sets.New()
addVirtualService := func(vs config.Config, hosts host.Names) {
vsname := vs.Name + "/" + vs.Namespace
rule := vs.Spec.(*networking.VirtualService)
for _, ih := range hosts {
// Check if the virtual service is already processed.
if vsset.Contains(vsname) {
break
}
for _, h := range rule.Hosts {
// VirtualServices can have many hosts, so we need to avoid appending
// duplicated virtualservices to slice importedVirtualServices
if vsHostMatches(h, ih, vs) {
importedVirtualServices = append(importedVirtualServices, vs)
vsset.Insert(vsname)
break
}
}
}
}
for _, c := range virtualServices {
configNamespace := c.Namespace
// Selection algorithm:
// virtualservices have a list of hosts in the API spec
// if any host in the list matches one service hostname, select the virtual service
// and break out of the loop.
// Check if there is an explicit import of form ns/* or ns/host
if importedHosts, nsFound := hosts[configNamespace]; nsFound {
addVirtualService(c, importedHosts)
}
// Check if there is an import of form */host or */*
if importedHosts, wnsFound := hosts[wildcardNamespace]; wnsFound {
addVirtualService(c, importedHosts)
}
}
return importedVirtualServices
}
// vsHostMatches checks if the given VirtualService host matches the importedHost (from Sidecar)
func vsHostMatches(vsHost string, importedHost host.Name, vs config.Config) bool {
if UseGatewaySemantics(vs) {
// The new way. Matching logic exactly mirrors Service matching
// If a route defines `*.com` and we import `a.com`, it will not match
return host.Name(vsHost).SubsetOf(importedHost)
}
// The old way. We check Matches which is bi-directional. This is for backwards compatibility
return host.Name(vsHost).Matches(importedHost)
}
func resolveVirtualServiceShortnames(rule *networking.VirtualService, meta config.Meta) {
// resolve top level hosts
for i, h := range rule.Hosts {
rule.Hosts[i] = string(ResolveShortnameToFQDN(h, meta))
}
// resolve gateways to bind to
for i, g := range rule.Gateways {
if g != constants.IstioMeshGateway {
rule.Gateways[i] = resolveGatewayName(g, meta)
}
}
// resolve host in http route.destination, route.mirror
for _, d := range rule.Http {
for _, m := range d.Match {
for i, g := range m.Gateways {
if g != constants.IstioMeshGateway {
m.Gateways[i] = resolveGatewayName(g, meta)
}
}
}
for _, w := range d.Route {
if w.Destination != nil {
w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta))
}
}
if d.Mirror != nil {
d.Mirror.Host = string(ResolveShortnameToFQDN(d.Mirror.Host, meta))
}
}
// resolve host in tcp route.destination
for _, d := range rule.Tcp {
for _, m := range d.Match {
for i, g := range m.Gateways {
if g != constants.IstioMeshGateway {
m.Gateways[i] = resolveGatewayName(g, meta)
}
}
}
for _, w := range d.Route {
if w.Destination != nil {
w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta))
}
}
}
// resolve host in tls route.destination
for _, tls := range rule.Tls {
for _, m := range tls.Match {
for i, g := range m.Gateways {
if g != constants.IstioMeshGateway {
m.Gateways[i] = resolveGatewayName(g, meta)
}
}
}
for _, w := range tls.Route {
if w.Destination != nil {
w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta))
}
}
}
}
// Return merged virtual services and the root->delegate vs map
func mergeVirtualServicesIfNeeded(
vServices []config.Config,
defaultExportTo map[visibility.Instance]bool) ([]config.Config, map[ConfigKey][]ConfigKey) {
out := make([]config.Config, 0, len(vServices))
delegatesMap := map[string]config.Config{}
delegatesExportToMap := map[string]map[visibility.Instance]bool{}
// root virtualservices with delegate
var rootVses []config.Config
// 1. classify virtualservices
for _, vs := range vServices {
rule := vs.Spec.(*networking.VirtualService)
// it is delegate, add it to the indexer cache along with the exportTo for the delegate
if len(rule.Hosts) == 0 {
delegatesMap[key(vs.Name, vs.Namespace)] = vs
if len(rule.ExportTo) == 0 {
// No exportTo in virtualService. Use the global default
delegatesExportToMap[key(vs.Name, vs.Namespace)] = defaultExportTo
} else {
exportToMap := make(map[visibility.Instance]bool)
for _, e := range rule.ExportTo {
if e == string(visibility.Private) {
exportToMap[visibility.Instance(vs.Namespace)] = true
} else {
exportToMap[visibility.Instance(e)] = true
}
}
delegatesExportToMap[key(vs.Name, vs.Namespace)] = exportToMap
}
continue
}
// root vs
if isRootVs(rule) {
rootVses = append(rootVses, vs)
continue
}
// the others are normal vs without delegate
out = append(out, vs)
}
delegatesByRoot := make(map[ConfigKey][]ConfigKey, len(rootVses))
// 2. merge delegates and root
for _, root := range rootVses {
rootConfigKey := ConfigKey{Kind: gvk.VirtualService, Name: root.Name, Namespace: root.Namespace}
rootVs := root.Spec.(*networking.VirtualService)
mergedRoutes := []*networking.HTTPRoute{}
for _, route := range rootVs.Http {
// it is root vs with delegate
if delegate := route.Delegate; delegate != nil {
delegateNamespace := delegate.Namespace
if delegateNamespace == "" {
delegateNamespace = root.Namespace
}
delegateConfigKey := ConfigKey{Kind: gvk.VirtualService, Name: delegate.Name, Namespace: delegateNamespace}
delegatesByRoot[rootConfigKey] = append(delegatesByRoot[rootConfigKey], delegateConfigKey)
delegateVS, ok := delegatesMap[key(delegate.Name, delegateNamespace)]
if !ok {
log.Debugf("delegate virtual service %s/%s of %s/%s not found",
delegateNamespace, delegate.Name, root.Namespace, root.Name)
// delegate not found, ignore only the current HTTP route
continue
}
// make sure that the delegate is visible to root virtual service's namespace
exportTo := delegatesExportToMap[key(delegate.Name, delegateNamespace)]
if !exportTo[visibility.Public] && !exportTo[visibility.Instance(root.Namespace)] {
log.Debugf("delegate virtual service %s/%s of %s/%s is not exported to %s",
delegateNamespace, delegate.Name, root.Namespace, root.Name, root.Namespace)
continue
}
// DeepCopy to prevent mutate the original delegate, it can conflict
// when multiple routes delegate to one single VS.
copiedDelegate := delegateVS.DeepCopy()
vs := copiedDelegate.Spec.(*networking.VirtualService)
merged := mergeHTTPRoutes(route, vs.Http)
mergedRoutes = append(mergedRoutes, merged...)
} else {
mergedRoutes = append(mergedRoutes, route)
}
}
rootVs.Http = mergedRoutes
if log.DebugEnabled() {
jsonm := &jsonpb.Marshaler{Indent: " "}
vsString, _ := jsonm.MarshalToString(rootVs)
log.Debugf("merged virtualService: %s", vsString)
}
out = append(out, root)
}
return out, delegatesByRoot
}
// merge root's route with delegate's and the merged route number equals the delegate's.
// if there is a conflict with root, the route is ignored
func mergeHTTPRoutes(root *networking.HTTPRoute, delegate []*networking.HTTPRoute) []*networking.HTTPRoute {
root.Delegate = nil
out := make([]*networking.HTTPRoute, 0, len(delegate))
for _, subRoute := range delegate {
merged := mergeHTTPRoute(root, subRoute)
if merged != nil {
out = append(out, merged)
}
}
return out
}
// merge the two HTTPRoutes, if there is a conflict with root, the delegate route is ignored
func mergeHTTPRoute(root *networking.HTTPRoute, delegate *networking.HTTPRoute) *networking.HTTPRoute {
// suppose there are N1 match conditions in root, N2 match conditions in delegate
// if match condition of N2 is a subset of anyone in N1, this is a valid matching conditions
merged, conflict := mergeHTTPMatchRequests(root.Match, delegate.Match)
if conflict {
log.Debugf("HTTPMatchRequests conflict: root route %s, delegate route %s", root.Name, delegate.Name)
return nil
}
delegate.Match = merged
if delegate.Name == "" {
delegate.Name = root.Name
} else if root.Name != "" {
delegate.Name = root.Name + "-" + delegate.Name
}
if delegate.Rewrite == nil {
delegate.Rewrite = root.Rewrite
}
if delegate.Timeout == nil {
delegate.Timeout = root.Timeout
}
if delegate.Retries == nil {
delegate.Retries = root.Retries
}
if delegate.Fault == nil {
delegate.Fault = root.Fault
}
if delegate.Mirror == nil {
delegate.Mirror = root.Mirror
}
// nolint: staticcheck
if delegate.MirrorPercent == nil {
delegate.MirrorPercent = root.MirrorPercent
}
if delegate.MirrorPercentage == nil {
delegate.MirrorPercentage = root.MirrorPercentage
}
if delegate.CorsPolicy == nil {
delegate.CorsPolicy = root.CorsPolicy
}
if delegate.Headers == nil {
delegate.Headers = root.Headers
}
return delegate
}
// return merged match conditions if not conflicts
func mergeHTTPMatchRequests(root, delegate []*networking.HTTPMatchRequest) (out []*networking.HTTPMatchRequest, conflict bool) {
if len(root) == 0 {
return delegate, false
}
if len(delegate) == 0 {
return root, false
}
// each HTTPMatchRequest of delegate must find a superset in root.
// otherwise it conflicts
for _, subMatch := range delegate {
foundMatch := false
for _, rootMatch := range root {
if hasConflict(rootMatch, subMatch) {
log.Debugf("HTTPMatchRequests conflict: root %v, delegate %v", rootMatch, subMatch)
continue
}
// merge HTTPMatchRequest
out = append(out, mergeHTTPMatchRequest(rootMatch, subMatch))
foundMatch = true
}
if !foundMatch {
return nil, true
}
}
if len(out) == 0 {
conflict = true
}
return
}
func mergeHTTPMatchRequest(root, delegate *networking.HTTPMatchRequest) *networking.HTTPMatchRequest {
out := proto.Clone(delegate).(*networking.HTTPMatchRequest)
if out.Name == "" {
out.Name = root.Name
} else if root.Name != "" {
out.Name = root.Name + "-" + out.Name
}
if out.Uri == nil {
out.Uri = root.Uri
}
if out.Scheme == nil {
out.Scheme = root.Scheme
}
if out.Method == nil {
out.Method = root.Method
}
if out.Authority == nil {
out.Authority = root.Authority
}
// headers
if len(root.Headers) > 0 || len(delegate.Headers) > 0 {
out.Headers = make(map[string]*networking.StringMatch)
}
for k, v := range root.Headers {
out.Headers[k] = v
}
for k, v := range delegate.Headers {
out.Headers[k] = v
}
// withoutheaders
if len(root.WithoutHeaders) > 0 || len(delegate.WithoutHeaders) > 0 {
out.WithoutHeaders = make(map[string]*networking.StringMatch)
}
for k, v := range root.WithoutHeaders {
out.WithoutHeaders[k] = v
}
for k, v := range delegate.WithoutHeaders {
out.WithoutHeaders[k] = v
}
// queryparams
if len(root.QueryParams) > 0 || len(delegate.QueryParams) > 0 {
out.QueryParams = make(map[string]*networking.StringMatch)
}
for k, v := range root.QueryParams {
out.QueryParams[k] = v
}
for k, v := range delegate.QueryParams {
out.QueryParams[k] = v
}
if out.Port == 0 {
out.Port = root.Port
}
// SourceLabels
if len(root.SourceLabels) > 0 || len(delegate.SourceLabels) > 0 {
out.SourceLabels = make(map[string]string)
}
for k, v := range root.SourceLabels {
out.SourceLabels[k] = v
}
for k, v := range delegate.SourceLabels {
out.SourceLabels[k] = v
}
if out.SourceNamespace == "" {
out.SourceNamespace = root.SourceNamespace
}
if len(out.Gateways) == 0 {
out.Gateways = root.Gateways
}
return out
}
func hasConflict(root, leaf *networking.HTTPMatchRequest) bool {
roots := []*networking.StringMatch{root.Uri, root.Scheme, root.Method, root.Authority}
leaves := []*networking.StringMatch{leaf.Uri, leaf.Scheme, leaf.Method, leaf.Authority}
for i := range roots {
if stringMatchConflict(roots[i], leaves[i]) {
return true
}
}
// header conflicts
for key, leafHeader := range leaf.Headers {
if stringMatchConflict(root.Headers[key], leafHeader) {
return true
}
}
// without headers
for key, leafValue := range leaf.WithoutHeaders {
if stringMatchConflict(root.WithoutHeaders[key], leafValue) {
return true
}
}
// query params conflict
for key, value := range leaf.QueryParams {
if stringMatchConflict(root.QueryParams[key], value) {
return true
}
}
if root.IgnoreUriCase != leaf.IgnoreUriCase {
return true
}
if root.Port > 0 && leaf.Port > 0 && root.Port != leaf.Port {
return true
}
// sourceNamespace
if root.SourceNamespace != "" && leaf.SourceNamespace != root.SourceNamespace {
return true
}
// sourceLabels should not conflict, root should have superset of sourceLabels.
for key, leafValue := range leaf.SourceLabels {
if v, ok := root.SourceLabels[key]; ok && v != leafValue {
return true
}
}
// gateways should not conflict, root should have superset of gateways.
if len(root.Gateways) > 0 && len(leaf.Gateways) > 0 {
if len(root.Gateways) < len(leaf.Gateways) {
return true
}
rootGateway := sets.New(root.Gateways...)
for _, gw := range leaf.Gateways {
if !rootGateway.Contains(gw) {
return true
}
}
}
return false
}
func stringMatchConflict(root, leaf *networking.StringMatch) bool {
// no conflict when root or leaf is not specified
if root == nil || leaf == nil {
return false
}
// regex match is not allowed
if root.GetRegex() != "" || leaf.GetRegex() != "" {
return true
}
// root is exact match
if exact := root.GetExact(); exact != "" {
// leaf is prefix match, conflict
if leaf.GetPrefix() != "" {
return true
}
// both exact, but not equal
if leaf.GetExact() != exact {
return true
}
return false
}
// root is prefix match
if prefix := root.GetPrefix(); prefix != "" {
// leaf is prefix match
if leaf.GetPrefix() != "" {
// leaf(`/a`) is not subset of root(`/a/b`)
return !strings.HasPrefix(leaf.GetPrefix(), prefix)
}
// leaf is exact match
if leaf.GetExact() != "" {
// leaf(`/a`) is not subset of root(`/a/b`)
return !strings.HasPrefix(leaf.GetExact(), prefix)
}
}
return true
}
func isRootVs(vs *networking.VirtualService) bool {
for _, route := range vs.Http {
// it is root vs with delegate
if route.Delegate != nil {
return true
}
}
return false
}
// UseIngressSemantics determines which logic we should use for VirtualService
// This allows ingress and VS to both be represented by VirtualService, but have different
// semantics.
func UseIngressSemantics(cfg config.Config) bool {
return cfg.Annotations[constants.InternalRouteSemantics] == constants.RouteSemanticsIngress
}
// UseGatewaySemantics determines which logic we should use for VirtualService
// This allows gateway-api and VS to both be represented by VirtualService, but have different
// semantics.
func UseGatewaySemantics(cfg config.Config) bool {
return cfg.Annotations[constants.InternalRouteSemantics] == constants.RouteSemanticsGateway
}