istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/virtualservice.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 model
    16  
    17  import (
    18  	"strings"
    19  
    20  	"k8s.io/apimachinery/pkg/types"
    21  
    22  	networking "istio.io/api/networking/v1alpha3"
    23  	"istio.io/istio/pkg/config"
    24  	"istio.io/istio/pkg/config/constants"
    25  	"istio.io/istio/pkg/config/host"
    26  	"istio.io/istio/pkg/config/schema/kind"
    27  	"istio.io/istio/pkg/config/visibility"
    28  	"istio.io/istio/pkg/maps"
    29  	"istio.io/istio/pkg/util/protomarshal"
    30  	"istio.io/istio/pkg/util/sets"
    31  )
    32  
    33  // SelectVirtualServices selects the virtual services by matching given services' host names.
    34  // This function is used by sidecar converter.
    35  func SelectVirtualServices(vsidx virtualServiceIndex, configNamespace string, hostsByNamespace map[string]hostClassification) []config.Config {
    36  	importedVirtualServices := make([]config.Config, 0)
    37  	vsset := sets.New[types.NamespacedName]()
    38  
    39  	addVirtualService := func(vs config.Config, hc hostClassification) {
    40  		key := vs.NamespacedName()
    41  		if vsset.Contains(key) {
    42  			return
    43  		}
    44  
    45  		rule := vs.Spec.(*networking.VirtualService)
    46  		useGatewaySemantics := UseGatewaySemantics(vs)
    47  		for _, vh := range rule.Hosts {
    48  			if hc.VSMatches(host.Name(vh), useGatewaySemantics) {
    49  				importedVirtualServices = append(importedVirtualServices, vs)
    50  				vsset.Insert(key)
    51  				return
    52  			}
    53  		}
    54  	}
    55  
    56  	wnsImportedHosts, wnsFound := hostsByNamespace[wildcardNamespace]
    57  	loopAndAdd := func(vses []config.Config) {
    58  		for _, c := range vses {
    59  			configNamespace := c.Namespace
    60  			// Selection algorithm:
    61  			// virtualservices have a list of hosts in the API spec
    62  			// if any host in the list matches one service hostname, select the virtual service
    63  			// and break out of the loop.
    64  
    65  			// Check if there is an explicit import of form ns/* or ns/host
    66  			if importedHosts, nsFound := hostsByNamespace[configNamespace]; nsFound {
    67  				addVirtualService(c, importedHosts)
    68  			}
    69  
    70  			// Check if there is an import of form */host or */*
    71  			if wnsFound {
    72  				addVirtualService(c, wnsImportedHosts)
    73  			}
    74  		}
    75  	}
    76  
    77  	n := types.NamespacedName{Namespace: configNamespace, Name: constants.IstioMeshGateway}
    78  	loopAndAdd(vsidx.privateByNamespaceAndGateway[n])
    79  	loopAndAdd(vsidx.exportedToNamespaceByGateway[n])
    80  	loopAndAdd(vsidx.publicByGateway[constants.IstioMeshGateway])
    81  
    82  	return importedVirtualServices
    83  }
    84  
    85  func resolveVirtualServiceShortnames(rule *networking.VirtualService, meta config.Meta) {
    86  	// Kubernetes Gateway API semantics support shortnames
    87  	if UseGatewaySemantics(config.Config{Meta: meta}) {
    88  		return
    89  	}
    90  
    91  	// resolve top level hosts
    92  	for i, h := range rule.Hosts {
    93  		rule.Hosts[i] = string(ResolveShortnameToFQDN(h, meta))
    94  	}
    95  	// resolve gateways to bind to
    96  	for i, g := range rule.Gateways {
    97  		if g != constants.IstioMeshGateway {
    98  			rule.Gateways[i] = resolveGatewayName(g, meta)
    99  		}
   100  	}
   101  	// resolve host in http route.destination, route.mirror
   102  	for _, d := range rule.Http {
   103  		for _, m := range d.Match {
   104  			for i, g := range m.Gateways {
   105  				if g != constants.IstioMeshGateway {
   106  					m.Gateways[i] = resolveGatewayName(g, meta)
   107  				}
   108  			}
   109  		}
   110  		for _, w := range d.Route {
   111  			if w.Destination != nil {
   112  				w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta))
   113  			}
   114  		}
   115  		if d.Mirror != nil {
   116  			d.Mirror.Host = string(ResolveShortnameToFQDN(d.Mirror.Host, meta))
   117  		}
   118  		for _, m := range d.Mirrors {
   119  			if m.Destination != nil {
   120  				m.Destination.Host = string(ResolveShortnameToFQDN(m.Destination.Host, meta))
   121  			}
   122  		}
   123  	}
   124  	// resolve host in tcp route.destination
   125  	for _, d := range rule.Tcp {
   126  		for _, m := range d.Match {
   127  			for i, g := range m.Gateways {
   128  				if g != constants.IstioMeshGateway {
   129  					m.Gateways[i] = resolveGatewayName(g, meta)
   130  				}
   131  			}
   132  		}
   133  		for _, w := range d.Route {
   134  			if w.Destination != nil {
   135  				w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta))
   136  			}
   137  		}
   138  	}
   139  	// resolve host in tls route.destination
   140  	for _, tls := range rule.Tls {
   141  		for _, m := range tls.Match {
   142  			for i, g := range m.Gateways {
   143  				if g != constants.IstioMeshGateway {
   144  					m.Gateways[i] = resolveGatewayName(g, meta)
   145  				}
   146  			}
   147  		}
   148  		for _, w := range tls.Route {
   149  			if w.Destination != nil {
   150  				w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta))
   151  			}
   152  		}
   153  	}
   154  }
   155  
   156  // Return merged virtual services and the root->delegate vs map
   157  func mergeVirtualServicesIfNeeded(
   158  	vServices []config.Config,
   159  	defaultExportTo sets.Set[visibility.Instance],
   160  ) ([]config.Config, map[ConfigKey][]ConfigKey) {
   161  	out := make([]config.Config, 0, len(vServices))
   162  	delegatesMap := map[types.NamespacedName]config.Config{}
   163  	delegatesExportToMap := make(map[types.NamespacedName]sets.Set[visibility.Instance])
   164  	// root virtualservices with delegate
   165  	var rootVses []config.Config
   166  
   167  	// 1. classify virtualservices
   168  	for _, vs := range vServices {
   169  		rule := vs.Spec.(*networking.VirtualService)
   170  		// it is delegate, add it to the indexer cache along with the exportTo for the delegate
   171  		if len(rule.Hosts) == 0 {
   172  			delegatesMap[vs.NamespacedName()] = vs
   173  			var exportToSet sets.Set[visibility.Instance]
   174  			if len(rule.ExportTo) == 0 {
   175  				// No exportTo in virtualService. Use the global default
   176  				exportToSet = sets.NewWithLength[visibility.Instance](defaultExportTo.Len())
   177  				for v := range defaultExportTo {
   178  					if v == visibility.Private {
   179  						exportToSet.Insert(visibility.Instance(vs.Namespace))
   180  					} else {
   181  						exportToSet.Insert(v)
   182  					}
   183  				}
   184  			} else {
   185  				exportToSet = sets.NewWithLength[visibility.Instance](len(rule.ExportTo))
   186  				for _, e := range rule.ExportTo {
   187  					if e == string(visibility.Private) {
   188  						exportToSet.Insert(visibility.Instance(vs.Namespace))
   189  					} else {
   190  						exportToSet.Insert(visibility.Instance(e))
   191  					}
   192  				}
   193  			}
   194  			delegatesExportToMap[vs.NamespacedName()] = exportToSet
   195  
   196  			continue
   197  		}
   198  
   199  		// root vs
   200  		if isRootVs(rule) {
   201  			rootVses = append(rootVses, vs)
   202  			continue
   203  		}
   204  
   205  		// the others are normal vs without delegate
   206  		out = append(out, vs)
   207  	}
   208  
   209  	delegatesByRoot := make(map[ConfigKey][]ConfigKey, len(rootVses))
   210  
   211  	// 2. merge delegates and root
   212  	for _, root := range rootVses {
   213  		rootConfigKey := ConfigKey{Kind: kind.VirtualService, Name: root.Name, Namespace: root.Namespace}
   214  		rootVs := root.Spec.(*networking.VirtualService)
   215  		mergedRoutes := []*networking.HTTPRoute{}
   216  		for _, route := range rootVs.Http {
   217  			// it is root vs with delegate
   218  			if delegate := route.Delegate; delegate != nil {
   219  				delegateNamespace := delegate.Namespace
   220  				if delegateNamespace == "" {
   221  					delegateNamespace = root.Namespace
   222  				}
   223  				delegateConfigKey := ConfigKey{Kind: kind.VirtualService, Name: delegate.Name, Namespace: delegateNamespace}
   224  				delegatesByRoot[rootConfigKey] = append(delegatesByRoot[rootConfigKey], delegateConfigKey)
   225  				delegateVS, ok := delegatesMap[types.NamespacedName{Namespace: delegateNamespace, Name: delegate.Name}]
   226  				if !ok {
   227  					log.Warnf("delegate virtual service %s/%s of %s/%s not found",
   228  						delegateNamespace, delegate.Name, root.Namespace, root.Name)
   229  					// delegate not found, ignore only the current HTTP route
   230  					continue
   231  				}
   232  				// make sure that the delegate is visible to root virtual service's namespace
   233  				exportTo := delegatesExportToMap[types.NamespacedName{Namespace: delegateNamespace, Name: delegate.Name}]
   234  				if !exportTo.Contains(visibility.Public) && !exportTo.Contains(visibility.Instance(root.Namespace)) {
   235  					log.Warnf("delegate virtual service %s/%s of %s/%s is not exported to %s",
   236  						delegateNamespace, delegate.Name, root.Namespace, root.Name, root.Namespace)
   237  					continue
   238  				}
   239  				// DeepCopy to prevent mutate the original delegate, it can conflict
   240  				// when multiple routes delegate to one single VS.
   241  				copiedDelegate := config.DeepCopy(delegateVS.Spec)
   242  				vs := copiedDelegate.(*networking.VirtualService)
   243  				merged := mergeHTTPRoutes(route, vs.Http)
   244  				mergedRoutes = append(mergedRoutes, merged...)
   245  			} else {
   246  				mergedRoutes = append(mergedRoutes, route)
   247  			}
   248  		}
   249  		rootVs.Http = mergedRoutes
   250  		if log.DebugEnabled() {
   251  			vsString, _ := protomarshal.ToJSONWithIndent(rootVs, "   ")
   252  			log.Debugf("merged virtualService: %s", vsString)
   253  		}
   254  		out = append(out, root)
   255  	}
   256  
   257  	sortConfigByCreationTime(out)
   258  
   259  	return out, delegatesByRoot
   260  }
   261  
   262  // merge root's route with delegate's and the merged route number equals the delegate's.
   263  // if there is a conflict with root, the route is ignored
   264  func mergeHTTPRoutes(root *networking.HTTPRoute, delegate []*networking.HTTPRoute) []*networking.HTTPRoute {
   265  	root.Delegate = nil
   266  
   267  	out := make([]*networking.HTTPRoute, 0, len(delegate))
   268  	for _, subRoute := range delegate {
   269  		merged := mergeHTTPRoute(root, subRoute)
   270  		if merged != nil {
   271  			out = append(out, merged)
   272  		}
   273  	}
   274  	return out
   275  }
   276  
   277  // merge the two HTTPRoutes, if there is a conflict with root, the delegate route is ignored
   278  func mergeHTTPRoute(root *networking.HTTPRoute, delegate *networking.HTTPRoute) *networking.HTTPRoute {
   279  	// suppose there are N1 match conditions in root, N2 match conditions in delegate
   280  	// if match condition of N2 is a subset of anyone in N1, this is a valid matching conditions
   281  	merged, conflict := mergeHTTPMatchRequests(root.Match, delegate.Match)
   282  	if conflict {
   283  		log.Warnf("HTTPMatchRequests conflict: root route %s, delegate route %s", root.Name, delegate.Name)
   284  		return nil
   285  	}
   286  	delegate.Match = merged
   287  
   288  	if delegate.Name == "" {
   289  		delegate.Name = root.Name
   290  	} else if root.Name != "" {
   291  		delegate.Name = root.Name + "-" + delegate.Name
   292  	}
   293  	if delegate.Rewrite == nil {
   294  		delegate.Rewrite = root.Rewrite
   295  	}
   296  	if delegate.DirectResponse == nil {
   297  		delegate.DirectResponse = root.DirectResponse
   298  	}
   299  	if delegate.Timeout == nil {
   300  		delegate.Timeout = root.Timeout
   301  	}
   302  	if delegate.Retries == nil {
   303  		delegate.Retries = root.Retries
   304  	}
   305  	if delegate.Fault == nil {
   306  		delegate.Fault = root.Fault
   307  	}
   308  	if delegate.Mirror == nil {
   309  		delegate.Mirror = root.Mirror
   310  	}
   311  	// nolint: staticcheck
   312  	if delegate.MirrorPercent == nil {
   313  		delegate.MirrorPercent = root.MirrorPercent
   314  	}
   315  	if delegate.MirrorPercentage == nil {
   316  		delegate.MirrorPercentage = root.MirrorPercentage
   317  	}
   318  	if delegate.CorsPolicy == nil {
   319  		delegate.CorsPolicy = root.CorsPolicy
   320  	}
   321  	if delegate.Mirrors == nil {
   322  		delegate.Mirrors = root.Mirrors
   323  	}
   324  	if delegate.Headers == nil {
   325  		delegate.Headers = root.Headers
   326  	}
   327  	return delegate
   328  }
   329  
   330  // return merged match conditions if not conflicts
   331  func mergeHTTPMatchRequests(root, delegate []*networking.HTTPMatchRequest) (out []*networking.HTTPMatchRequest, conflict bool) {
   332  	if len(root) == 0 {
   333  		return delegate, false
   334  	}
   335  
   336  	if len(delegate) == 0 {
   337  		return root, false
   338  	}
   339  
   340  	// each HTTPMatchRequest of delegate must find a superset in root.
   341  	// otherwise it conflicts
   342  	for _, subMatch := range delegate {
   343  		foundMatch := false
   344  		for _, rootMatch := range root {
   345  			if hasConflict(rootMatch, subMatch) {
   346  				log.Warnf("HTTPMatchRequests conflict: root %v, delegate %v", rootMatch, subMatch)
   347  				continue
   348  			}
   349  			// merge HTTPMatchRequest
   350  			out = append(out, mergeHTTPMatchRequest(rootMatch, subMatch))
   351  			foundMatch = true
   352  		}
   353  		if !foundMatch {
   354  			return nil, true
   355  		}
   356  	}
   357  	if len(out) == 0 {
   358  		conflict = true
   359  	}
   360  	return
   361  }
   362  
   363  func mergeHTTPMatchRequest(root, delegate *networking.HTTPMatchRequest) *networking.HTTPMatchRequest {
   364  	// nolint: govet
   365  	out := *delegate
   366  	if out.Name == "" {
   367  		out.Name = root.Name
   368  	} else if root.Name != "" {
   369  		out.Name = root.Name + "-" + out.Name
   370  	}
   371  	if out.Uri == nil {
   372  		out.Uri = root.Uri
   373  	}
   374  	if out.Scheme == nil {
   375  		out.Scheme = root.Scheme
   376  	}
   377  	if out.Method == nil {
   378  		out.Method = root.Method
   379  	}
   380  	if out.Authority == nil {
   381  		out.Authority = root.Authority
   382  	}
   383  	// headers
   384  	out.Headers = maps.MergeCopy(root.Headers, delegate.Headers)
   385  
   386  	// withoutheaders
   387  	out.WithoutHeaders = maps.MergeCopy(root.WithoutHeaders, delegate.WithoutHeaders)
   388  
   389  	// queryparams
   390  	out.QueryParams = maps.MergeCopy(root.QueryParams, delegate.QueryParams)
   391  
   392  	if out.Port == 0 {
   393  		out.Port = root.Port
   394  	}
   395  
   396  	// SourceLabels
   397  	out.SourceLabels = maps.MergeCopy(root.SourceLabels, delegate.SourceLabels)
   398  
   399  	if out.SourceNamespace == "" {
   400  		out.SourceNamespace = root.SourceNamespace
   401  	}
   402  
   403  	if len(out.Gateways) == 0 {
   404  		out.Gateways = root.Gateways
   405  	}
   406  
   407  	if len(out.StatPrefix) == 0 {
   408  		out.StatPrefix = root.StatPrefix
   409  	}
   410  	return &out
   411  }
   412  
   413  func hasConflict(root, leaf *networking.HTTPMatchRequest) bool {
   414  	roots := []*networking.StringMatch{root.Uri, root.Scheme, root.Method, root.Authority}
   415  	leaves := []*networking.StringMatch{leaf.Uri, leaf.Scheme, leaf.Method, leaf.Authority}
   416  	for i := range roots {
   417  		if stringMatchConflict(roots[i], leaves[i]) {
   418  			return true
   419  		}
   420  	}
   421  	// header conflicts
   422  	for key, leafHeader := range leaf.Headers {
   423  		if stringMatchConflict(root.Headers[key], leafHeader) {
   424  			return true
   425  		}
   426  	}
   427  
   428  	// without headers
   429  	for key, leafValue := range leaf.WithoutHeaders {
   430  		if stringMatchConflict(root.WithoutHeaders[key], leafValue) {
   431  			return true
   432  		}
   433  	}
   434  
   435  	// query params conflict
   436  	for key, value := range leaf.QueryParams {
   437  		if stringMatchConflict(root.QueryParams[key], value) {
   438  			return true
   439  		}
   440  	}
   441  
   442  	if root.IgnoreUriCase != leaf.IgnoreUriCase {
   443  		return true
   444  	}
   445  	if root.Port > 0 && leaf.Port > 0 && root.Port != leaf.Port {
   446  		return true
   447  	}
   448  
   449  	// sourceNamespace
   450  	if root.SourceNamespace != "" && leaf.SourceNamespace != root.SourceNamespace {
   451  		return true
   452  	}
   453  
   454  	// sourceLabels should not conflict, root should have superset of sourceLabels.
   455  	for key, leafValue := range leaf.SourceLabels {
   456  		if v, ok := root.SourceLabels[key]; ok && v != leafValue {
   457  			return true
   458  		}
   459  	}
   460  
   461  	// gateways should not conflict, root should have superset of gateways.
   462  	if len(root.Gateways) > 0 && len(leaf.Gateways) > 0 {
   463  		if len(root.Gateways) < len(leaf.Gateways) {
   464  			return true
   465  		}
   466  		rootGateway := sets.New(root.Gateways...)
   467  		for _, gw := range leaf.Gateways {
   468  			if !rootGateway.Contains(gw) {
   469  				return true
   470  			}
   471  		}
   472  	}
   473  
   474  	return false
   475  }
   476  
   477  func stringMatchConflict(root, leaf *networking.StringMatch) bool {
   478  	// no conflict when root or leaf is not specified
   479  	if root == nil || leaf == nil {
   480  		return false
   481  	}
   482  	// If root regex match is specified, delegate should not have other matches.
   483  	if root.GetRegex() != "" {
   484  		if leaf.GetRegex() != "" || leaf.GetPrefix() != "" || leaf.GetExact() != "" {
   485  			return true
   486  		}
   487  	}
   488  	// If delegate regex match is specified, root should not have other matches.
   489  	if leaf.GetRegex() != "" {
   490  		if root.GetRegex() != "" || root.GetPrefix() != "" || root.GetExact() != "" {
   491  			return true
   492  		}
   493  	}
   494  	// root is exact match
   495  	if exact := root.GetExact(); exact != "" {
   496  		// leaf is prefix match, conflict
   497  		if leaf.GetPrefix() != "" {
   498  			return true
   499  		}
   500  		// both exact, but not equal
   501  		if leaf.GetExact() != exact {
   502  			return true
   503  		}
   504  		return false
   505  	}
   506  	// root is prefix match
   507  	if prefix := root.GetPrefix(); prefix != "" {
   508  		// leaf is prefix match
   509  		if leaf.GetPrefix() != "" {
   510  			// leaf(`/a`) is not subset of root(`/a/b`)
   511  			return !strings.HasPrefix(leaf.GetPrefix(), prefix)
   512  		}
   513  		// leaf is exact match
   514  		if leaf.GetExact() != "" {
   515  			// leaf(`/a`) is not subset of root(`/a/b`)
   516  			return !strings.HasPrefix(leaf.GetExact(), prefix)
   517  		}
   518  	}
   519  
   520  	return true
   521  }
   522  
   523  func isRootVs(vs *networking.VirtualService) bool {
   524  	for _, route := range vs.Http {
   525  		// it is root vs with delegate
   526  		if route.Delegate != nil {
   527  			return true
   528  		}
   529  	}
   530  	return false
   531  }
   532  
   533  // UseIngressSemantics determines which logic we should use for VirtualService
   534  // This allows ingress and VS to both be represented by VirtualService, but have different
   535  // semantics.
   536  func UseIngressSemantics(cfg config.Config) bool {
   537  	return cfg.Annotations[constants.InternalRouteSemantics] == constants.RouteSemanticsIngress
   538  }
   539  
   540  // UseGatewaySemantics determines which logic we should use for VirtualService
   541  // This allows gateway-api and VS to both be represented by VirtualService, but have different
   542  // semantics.
   543  func UseGatewaySemantics(cfg config.Config) bool {
   544  	return cfg.Annotations[constants.InternalRouteSemantics] == constants.RouteSemanticsGateway
   545  }
   546  
   547  // VirtualServiceDependencies returns dependent configs of the vs,
   548  // for internal vs generated from gateway-api routes, it returns the parent routes,
   549  // otherwise it just returns the vs as is.
   550  func VirtualServiceDependencies(vs config.Config) []ConfigKey {
   551  	if !UseGatewaySemantics(vs) {
   552  		return []ConfigKey{
   553  			{
   554  				Kind:      kind.VirtualService,
   555  				Namespace: vs.Namespace,
   556  				Name:      vs.Name,
   557  			},
   558  		}
   559  	}
   560  
   561  	// synthetic vs, get internal parents
   562  	internalParents := strings.Split(vs.Annotations[constants.InternalParentNames], ",")
   563  	out := make([]ConfigKey, 0, len(internalParents))
   564  	for _, p := range internalParents {
   565  		// kind/name.namespace
   566  		ks, nsname, ok := strings.Cut(p, "/")
   567  		if !ok {
   568  			log.Errorf("invalid InternalParentName parts: %s", p)
   569  			continue
   570  		}
   571  		var k kind.Kind
   572  		switch ks {
   573  		case kind.HTTPRoute.String():
   574  			k = kind.HTTPRoute
   575  		case kind.TCPRoute.String():
   576  			k = kind.TCPRoute
   577  		case kind.TLSRoute.String():
   578  			k = kind.TLSRoute
   579  		case kind.GRPCRoute.String():
   580  			k = kind.GRPCRoute
   581  		case kind.UDPRoute.String():
   582  			k = kind.UDPRoute
   583  		default:
   584  			// shouldn't happen
   585  			continue
   586  		}
   587  		name, ns, ok := strings.Cut(nsname, ".")
   588  		if !ok {
   589  			log.Errorf("invalid InternalParentName name: %s", nsname)
   590  			continue
   591  		}
   592  		out = append(out, ConfigKey{
   593  			Kind:      k,
   594  			Name:      name,
   595  			Namespace: ns,
   596  		})
   597  	}
   598  	return out
   599  }