istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/gateway/conversion.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 gateway
    16  
    17  import (
    18  	"crypto/tls"
    19  	"fmt"
    20  	"net"
    21  	"net/netip"
    22  	"sort"
    23  	"strings"
    24  	"time"
    25  
    26  	"google.golang.org/protobuf/types/known/durationpb"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	klabels "k8s.io/apimachinery/pkg/labels"
    29  	k8s "sigs.k8s.io/gateway-api/apis/v1"
    30  	k8salpha "sigs.k8s.io/gateway-api/apis/v1alpha2"
    31  	k8sbeta "sigs.k8s.io/gateway-api/apis/v1beta1"
    32  
    33  	istio "istio.io/api/networking/v1alpha3"
    34  	"istio.io/istio/pilot/pkg/features"
    35  	"istio.io/istio/pilot/pkg/model"
    36  	creds "istio.io/istio/pilot/pkg/model/credentials"
    37  	"istio.io/istio/pilot/pkg/model/kstatus"
    38  	"istio.io/istio/pilot/pkg/serviceregistry/kube"
    39  	"istio.io/istio/pkg/config"
    40  	"istio.io/istio/pkg/config/constants"
    41  	kubeconfig "istio.io/istio/pkg/config/gateway/kube"
    42  	"istio.io/istio/pkg/config/host"
    43  	"istio.io/istio/pkg/config/protocol"
    44  	"istio.io/istio/pkg/config/schema/gvk"
    45  	"istio.io/istio/pkg/config/schema/kind"
    46  	"istio.io/istio/pkg/ptr"
    47  	"istio.io/istio/pkg/slices"
    48  	"istio.io/istio/pkg/util/sets"
    49  )
    50  
    51  func sortConfigByCreationTime(configs []config.Config) {
    52  	sort.Slice(configs, func(i, j int) bool {
    53  		if configs[i].CreationTimestamp.Equal(configs[j].CreationTimestamp) {
    54  			in := configs[i].Namespace + "/" + configs[i].Name
    55  			jn := configs[j].Namespace + "/" + configs[j].Name
    56  			return in < jn
    57  		}
    58  		return configs[i].CreationTimestamp.Before(configs[j].CreationTimestamp)
    59  	})
    60  }
    61  
    62  // convertResources is the top level entrypoint to our conversion logic, computing the full state based
    63  // on KubernetesResources inputs.
    64  func convertResources(r GatewayResources) IstioResources {
    65  	// sort HTTPRoutes by creation timestamp and namespace/name
    66  	sortConfigByCreationTime(r.HTTPRoute)
    67  	sortConfigByCreationTime(r.GRPCRoute)
    68  
    69  	result := IstioResources{}
    70  	ctx := configContext{
    71  		GatewayResources:   r,
    72  		AllowedReferences:  convertReferencePolicies(r),
    73  		resourceReferences: make(map[model.ConfigKey][]model.ConfigKey),
    74  	}
    75  
    76  	gw, gwMap, nsReferences := convertGateways(ctx)
    77  	ctx.GatewayReferences = gwMap
    78  	result.Gateway = gw
    79  
    80  	result.VirtualService = convertVirtualService(ctx)
    81  
    82  	// Once we have gone through all route computation, we will know how many routes bound to each gateway.
    83  	// Report this in the status.
    84  	for _, dm := range gwMap {
    85  		for _, pri := range dm {
    86  			if pri.ReportAttachedRoutes != nil {
    87  				pri.ReportAttachedRoutes()
    88  			}
    89  		}
    90  	}
    91  	result.AllowedReferences = ctx.AllowedReferences
    92  	result.ReferencedNamespaceKeys = nsReferences
    93  	result.ResourceReferences = ctx.resourceReferences
    94  	return result
    95  }
    96  
    97  // convertReferencePolicies extracts all ReferencePolicy into an easily accessibly index.
    98  func convertReferencePolicies(r GatewayResources) AllowedReferences {
    99  	res := map[Reference]map[Reference]*Grants{}
   100  	type namespacedGrant struct {
   101  		Namespace string
   102  		Grant     *k8sbeta.ReferenceGrantSpec
   103  	}
   104  	specs := make([]namespacedGrant, 0, len(r.ReferenceGrant))
   105  
   106  	for _, obj := range r.ReferenceGrant {
   107  		rp := obj.Spec.(*k8sbeta.ReferenceGrantSpec)
   108  		specs = append(specs, namespacedGrant{Namespace: obj.Namespace, Grant: rp})
   109  	}
   110  	for _, ng := range specs {
   111  		rp := ng.Grant
   112  		for _, from := range rp.From {
   113  			fromKey := Reference{
   114  				Namespace: from.Namespace,
   115  			}
   116  			if string(from.Group) == gvk.KubernetesGateway.Group && string(from.Kind) == gvk.KubernetesGateway.Kind {
   117  				fromKey.Kind = gvk.KubernetesGateway
   118  			} else if string(from.Group) == gvk.HTTPRoute.Group && string(from.Kind) == gvk.HTTPRoute.Kind {
   119  				fromKey.Kind = gvk.HTTPRoute
   120  			} else if string(from.Group) == gvk.TLSRoute.Group && string(from.Kind) == gvk.TLSRoute.Kind {
   121  				fromKey.Kind = gvk.TLSRoute
   122  			} else if string(from.Group) == gvk.TCPRoute.Group && string(from.Kind) == gvk.TCPRoute.Kind {
   123  				fromKey.Kind = gvk.TCPRoute
   124  			} else {
   125  				// Not supported type. Not an error; may be for another controller
   126  				continue
   127  			}
   128  			for _, to := range rp.To {
   129  				toKey := Reference{
   130  					Namespace: k8s.Namespace(ng.Namespace),
   131  				}
   132  				if to.Group == "" && string(to.Kind) == gvk.Secret.Kind {
   133  					toKey.Kind = gvk.Secret
   134  				} else if to.Group == "" && string(to.Kind) == gvk.Service.Kind {
   135  					toKey.Kind = gvk.Service
   136  				} else {
   137  					// Not supported type. Not an error; may be for another controller
   138  					continue
   139  				}
   140  				if _, f := res[fromKey]; !f {
   141  					res[fromKey] = map[Reference]*Grants{}
   142  				}
   143  				if _, f := res[fromKey][toKey]; !f {
   144  					res[fromKey][toKey] = &Grants{
   145  						AllowedNames: sets.New[string](),
   146  					}
   147  				}
   148  				if to.Name != nil {
   149  					res[fromKey][toKey].AllowedNames.Insert(string(*to.Name))
   150  				} else {
   151  					res[fromKey][toKey].AllowAll = true
   152  				}
   153  			}
   154  		}
   155  	}
   156  	return res
   157  }
   158  
   159  // convertVirtualService takes all xRoute types and generates corresponding VirtualServices.
   160  func convertVirtualService(r configContext) []config.Config {
   161  	result := []config.Config{}
   162  	for _, obj := range r.TCPRoute {
   163  		result = append(result, buildTCPVirtualService(r, obj)...)
   164  	}
   165  
   166  	for _, obj := range r.TLSRoute {
   167  		result = append(result, buildTLSVirtualService(r, obj)...)
   168  	}
   169  
   170  	// for gateway routes, build one VS per gateway+host
   171  	gatewayRoutes := make(map[string]map[string]*config.Config)
   172  	// for mesh routes, build one VS per namespace+host
   173  	meshRoutes := make(map[string]map[string]*config.Config)
   174  	for _, obj := range r.HTTPRoute {
   175  		buildHTTPVirtualServices(r, obj, gatewayRoutes, meshRoutes)
   176  	}
   177  	for _, obj := range r.GRPCRoute {
   178  		buildGRPCVirtualServices(r, obj, gatewayRoutes, meshRoutes)
   179  	}
   180  	for _, vsByHost := range gatewayRoutes {
   181  		for _, vsConfig := range vsByHost {
   182  			result = append(result, *vsConfig)
   183  		}
   184  	}
   185  	for _, vsByHost := range meshRoutes {
   186  		for _, vsConfig := range vsByHost {
   187  			result = append(result, *vsConfig)
   188  		}
   189  	}
   190  	return result
   191  }
   192  
   193  func convertHTTPRoute(r k8s.HTTPRouteRule, ctx configContext,
   194  	obj config.Config, pos int, enforceRefGrant bool,
   195  ) (*istio.HTTPRoute, *ConfigError) {
   196  	// TODO: implement rewrite, timeout, corspolicy, retries
   197  	vs := &istio.HTTPRoute{}
   198  	// Auto-name the route. If upstream defines an explicit name, will use it instead
   199  	// The position within the route is unique
   200  	vs.Name = fmt.Sprintf("%s.%s.%d", obj.Namespace, obj.Name, pos)
   201  
   202  	for _, match := range r.Matches {
   203  		uri, err := createURIMatch(match)
   204  		if err != nil {
   205  			return nil, err
   206  		}
   207  		headers, err := createHeadersMatch(match)
   208  		if err != nil {
   209  			return nil, err
   210  		}
   211  		qp, err := createQueryParamsMatch(match)
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  		method, err := createMethodMatch(match)
   216  		if err != nil {
   217  			return nil, err
   218  		}
   219  		vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
   220  			Uri:         uri,
   221  			Headers:     headers,
   222  			QueryParams: qp,
   223  			Method:      method,
   224  		})
   225  	}
   226  	for _, filter := range r.Filters {
   227  		switch filter.Type {
   228  		case k8s.HTTPRouteFilterRequestHeaderModifier:
   229  			h := createHeadersFilter(filter.RequestHeaderModifier)
   230  			if h == nil {
   231  				continue
   232  			}
   233  			if vs.Headers == nil {
   234  				vs.Headers = &istio.Headers{}
   235  			}
   236  			vs.Headers.Request = h
   237  		case k8s.HTTPRouteFilterResponseHeaderModifier:
   238  			h := createHeadersFilter(filter.ResponseHeaderModifier)
   239  			if h == nil {
   240  				continue
   241  			}
   242  			if vs.Headers == nil {
   243  				vs.Headers = &istio.Headers{}
   244  			}
   245  			vs.Headers.Response = h
   246  		case k8s.HTTPRouteFilterRequestRedirect:
   247  			vs.Redirect = createRedirectFilter(filter.RequestRedirect)
   248  		case k8s.HTTPRouteFilterRequestMirror:
   249  			mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.HTTPRoute)
   250  			if err != nil {
   251  				return nil, err
   252  			}
   253  			vs.Mirrors = append(vs.Mirrors, mirror)
   254  		case k8s.HTTPRouteFilterURLRewrite:
   255  			vs.Rewrite = createRewriteFilter(filter.URLRewrite)
   256  		default:
   257  			return nil, &ConfigError{
   258  				Reason:  InvalidFilter,
   259  				Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
   260  			}
   261  		}
   262  	}
   263  
   264  	if r.Timeouts != nil {
   265  		if r.Timeouts.Request != nil {
   266  			request, _ := time.ParseDuration(string(*r.Timeouts.Request))
   267  			if request != 0 {
   268  				vs.Timeout = durationpb.New(request)
   269  			}
   270  		}
   271  		if r.Timeouts.BackendRequest != nil {
   272  			backendRequest, _ := time.ParseDuration(string(*r.Timeouts.BackendRequest))
   273  			if backendRequest != 0 {
   274  				timeout := durationpb.New(backendRequest)
   275  				if vs.Retries != nil {
   276  					vs.Retries.PerTryTimeout = timeout
   277  				} else {
   278  					vs.Timeout = timeout
   279  				}
   280  			}
   281  		}
   282  	}
   283  
   284  	if weightSum(r.BackendRefs) == 0 && vs.Redirect == nil {
   285  		// The spec requires us to return 500 when there are no >0 weight backends
   286  		vs.DirectResponse = &istio.HTTPDirectResponse{
   287  			Status: 500,
   288  		}
   289  	} else {
   290  		route, backendErr, err := buildHTTPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant)
   291  		if err != nil {
   292  			return nil, err
   293  		}
   294  		vs.Route = route
   295  		return vs, backendErr
   296  	}
   297  
   298  	return vs, nil
   299  }
   300  
   301  func convertGRPCRoute(r k8s.GRPCRouteRule, ctx configContext,
   302  	obj config.Config, pos int, enforceRefGrant bool,
   303  ) (*istio.HTTPRoute, *ConfigError) {
   304  	// TODO: implement rewrite, timeout, mirror, corspolicy, retries
   305  	vs := &istio.HTTPRoute{}
   306  	// Auto-name the route. If upstream defines an explicit name, will use it instead
   307  	// The position within the route is unique
   308  	vs.Name = fmt.Sprintf("%s.%s.%d", obj.Namespace, obj.Name, pos)
   309  
   310  	for _, match := range r.Matches {
   311  		uri, err := createGRPCURIMatch(match)
   312  		if err != nil {
   313  			return nil, err
   314  		}
   315  		headers, err := createGRPCHeadersMatch(match)
   316  		if err != nil {
   317  			return nil, err
   318  		}
   319  		vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
   320  			Uri:     uri,
   321  			Headers: headers,
   322  		})
   323  	}
   324  	for _, filter := range r.Filters {
   325  		switch filter.Type {
   326  		case k8s.GRPCRouteFilterRequestHeaderModifier:
   327  			h := createHeadersFilter(filter.RequestHeaderModifier)
   328  			if h == nil {
   329  				continue
   330  			}
   331  			if vs.Headers == nil {
   332  				vs.Headers = &istio.Headers{}
   333  			}
   334  			vs.Headers.Request = h
   335  		case k8s.GRPCRouteFilterResponseHeaderModifier:
   336  			h := createHeadersFilter(filter.ResponseHeaderModifier)
   337  			if h == nil {
   338  				continue
   339  			}
   340  			if vs.Headers == nil {
   341  				vs.Headers = &istio.Headers{}
   342  			}
   343  			vs.Headers.Response = h
   344  		case k8s.GRPCRouteFilterRequestMirror:
   345  			mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.GRPCRoute)
   346  			if err != nil {
   347  				return nil, err
   348  			}
   349  			vs.Mirrors = append(vs.Mirrors, mirror)
   350  		default:
   351  			return nil, &ConfigError{
   352  				Reason:  InvalidFilter,
   353  				Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
   354  			}
   355  		}
   356  	}
   357  
   358  	if grpcWeightSum(r.BackendRefs) == 0 && vs.Redirect == nil {
   359  		// The spec requires us to return 500 when there are no >0 weight backends
   360  		vs.DirectResponse = &istio.HTTPDirectResponse{
   361  			Status: 500,
   362  		}
   363  	} else {
   364  		route, backendErr, err := buildGRPCDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant)
   365  		if err != nil {
   366  			return nil, err
   367  		}
   368  		vs.Route = route
   369  		return vs, backendErr
   370  	}
   371  
   372  	return vs, nil
   373  }
   374  
   375  func parentTypes(rpi []routeParentReference) (mesh, gateway bool) {
   376  	for _, r := range rpi {
   377  		if r.IsMesh() {
   378  			mesh = true
   379  		} else {
   380  			gateway = true
   381  		}
   382  	}
   383  	return
   384  }
   385  
   386  func buildHTTPVirtualServices(
   387  	ctx configContext,
   388  	obj config.Config,
   389  	gatewayRoutes map[string]map[string]*config.Config,
   390  	meshRoutes map[string]map[string]*config.Config,
   391  ) {
   392  	route := obj.Spec.(*k8s.HTTPRouteSpec)
   393  	parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, route.Hostnames, gvk.HTTPRoute, obj.Namespace)
   394  	reportStatus := func(results []RouteParentResult) {
   395  		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
   396  			rs := s.(*k8s.HTTPRouteStatus)
   397  			rs.Parents = createRouteStatus(results, obj, rs.Parents)
   398  			return rs
   399  		})
   400  	}
   401  
   402  	type conversionResult struct {
   403  		error  *ConfigError
   404  		routes []*istio.HTTPRoute
   405  	}
   406  	convertRules := func(mesh bool) conversionResult {
   407  		res := conversionResult{}
   408  		for n, r := range route.Rules {
   409  			// split the rule to make sure each rule has up to one match
   410  			matches := slices.Reference(r.Matches)
   411  			if len(matches) == 0 {
   412  				matches = append(matches, nil)
   413  			}
   414  			for _, m := range matches {
   415  				if m != nil {
   416  					r.Matches = []k8s.HTTPRouteMatch{*m}
   417  				}
   418  				vs, err := convertHTTPRoute(r, ctx, obj, n, !mesh)
   419  				// This was a hard error
   420  				if vs == nil {
   421  					res.error = err
   422  					return conversionResult{error: err}
   423  				}
   424  				// Got an error but also routes
   425  				if err != nil {
   426  					res.error = err
   427  				}
   428  
   429  				res.routes = append(res.routes, vs)
   430  			}
   431  		}
   432  		return res
   433  	}
   434  	meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
   435  
   436  	reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
   437  		res := RouteParentResult{
   438  			OriginalReference: r.OriginalReference,
   439  			DeniedReason:      r.DeniedReason,
   440  			RouteError:        gwResult.error,
   441  		}
   442  		if r.IsMesh() {
   443  			res.RouteError = meshResult.error
   444  		}
   445  		return res
   446  	}))
   447  	count := 0
   448  	for _, parent := range filteredReferences(parentRefs) {
   449  		// for gateway routes, build one VS per gateway+host
   450  		routeMap := gatewayRoutes
   451  		routeKey := parent.InternalName
   452  		vsHosts := hostnameToStringList(route.Hostnames)
   453  		routes := gwResult.routes
   454  		if parent.IsMesh() {
   455  			routes = meshResult.routes
   456  			// for mesh routes, build one VS per namespace/port->host
   457  			routeMap = meshRoutes
   458  			routeKey = obj.Namespace
   459  			if parent.OriginalReference.Port != nil {
   460  				routes = augmentPortMatch(routes, *parent.OriginalReference.Port)
   461  				routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port)
   462  			}
   463  			if parent.InternalKind == gvk.ServiceEntry {
   464  				vsHosts = serviceEntryHosts(ctx.ServiceEntry,
   465  					string(parent.OriginalReference.Name),
   466  					string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace))))
   467  			} else {
   468  				vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
   469  					parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
   470  			}
   471  		}
   472  		if len(routes) == 0 {
   473  			continue
   474  		}
   475  		if _, f := routeMap[routeKey]; !f {
   476  			routeMap[routeKey] = make(map[string]*config.Config)
   477  		}
   478  
   479  		// Create one VS per hostname with a single hostname.
   480  		// This ensures we can treat each hostname independently, as the spec requires
   481  		for _, h := range vsHosts {
   482  			if !parent.hostnameAllowedByIsolation(h) {
   483  				// TODO: standardize a status message for this upstream and report
   484  				continue
   485  			}
   486  			if cfg := routeMap[routeKey][h]; cfg != nil {
   487  				// merge http routes
   488  				vs := cfg.Spec.(*istio.VirtualService)
   489  				vs.Http = append(vs.Http, routes...)
   490  				// append parents
   491  				cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s",
   492  					cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
   493  			} else {
   494  				name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
   495  				routeMap[routeKey][h] = &config.Config{
   496  					Meta: config.Meta{
   497  						CreationTimestamp: obj.CreationTimestamp,
   498  						GroupVersionKind:  gvk.VirtualService,
   499  						Name:              name,
   500  						Annotations:       routeMeta(obj),
   501  						Namespace:         obj.Namespace,
   502  						Domain:            ctx.Domain,
   503  					},
   504  					Spec: &istio.VirtualService{
   505  						Hosts:    []string{h},
   506  						Gateways: []string{parent.InternalName},
   507  						Http:     routes,
   508  					},
   509  				}
   510  				count++
   511  			}
   512  		}
   513  	}
   514  	for _, vsByHost := range gatewayRoutes {
   515  		for _, cfg := range vsByHost {
   516  			vs := cfg.Spec.(*istio.VirtualService)
   517  			sortHTTPRoutes(vs.Http)
   518  		}
   519  	}
   520  	for _, vsByHost := range meshRoutes {
   521  		for _, cfg := range vsByHost {
   522  			vs := cfg.Spec.(*istio.VirtualService)
   523  			sortHTTPRoutes(vs.Http)
   524  		}
   525  	}
   526  }
   527  
   528  func serviceEntryHosts(ses []config.Config, name, namespace string) []string {
   529  	for _, obj := range ses {
   530  		if obj.Meta.Name == name {
   531  			ns := obj.Meta.Namespace
   532  			if ns == "" {
   533  				ns = metav1.NamespaceDefault
   534  			}
   535  			if ns == namespace {
   536  				se := obj.Spec.(*istio.ServiceEntry)
   537  				return se.Hosts
   538  			}
   539  		}
   540  	}
   541  	return []string{}
   542  }
   543  
   544  func buildMeshAndGatewayRoutes[T any](parentRefs []routeParentReference, convertRules func(mesh bool) T) (T, T) {
   545  	var meshResult, gwResult T
   546  	needMesh, needGw := parentTypes(parentRefs)
   547  	if needMesh {
   548  		meshResult = convertRules(true)
   549  	}
   550  	if needGw {
   551  		gwResult = convertRules(false)
   552  	}
   553  	return meshResult, gwResult
   554  }
   555  
   556  func augmentPortMatch(routes []*istio.HTTPRoute, port k8s.PortNumber) []*istio.HTTPRoute {
   557  	res := make([]*istio.HTTPRoute, 0, len(routes))
   558  	for _, r := range routes {
   559  		r = r.DeepCopy()
   560  		for _, m := range r.Match {
   561  			m.Port = uint32(port)
   562  		}
   563  		if len(r.Match) == 0 {
   564  			r.Match = []*istio.HTTPMatchRequest{{
   565  				Port: uint32(port),
   566  			}}
   567  		}
   568  		res = append(res, r)
   569  	}
   570  	return res
   571  }
   572  
   573  func augmentTCPPortMatch(routes []*istio.TCPRoute, port k8s.PortNumber) []*istio.TCPRoute {
   574  	res := make([]*istio.TCPRoute, 0, len(routes))
   575  	for _, r := range routes {
   576  		r = r.DeepCopy()
   577  		for _, m := range r.Match {
   578  			m.Port = uint32(port)
   579  		}
   580  		if len(r.Match) == 0 {
   581  			r.Match = []*istio.L4MatchAttributes{{
   582  				Port: uint32(port),
   583  			}}
   584  		}
   585  		res = append(res, r)
   586  	}
   587  	return res
   588  }
   589  
   590  func augmentTLSPortMatch(routes []*istio.TLSRoute, port *k8s.PortNumber, parentHosts []string) []*istio.TLSRoute {
   591  	res := make([]*istio.TLSRoute, 0, len(routes))
   592  	for _, r := range routes {
   593  		r = r.DeepCopy()
   594  		if len(r.Match) == 1 && slices.Equal(r.Match[0].SniHosts, []string{"*"}) {
   595  			// For mesh, we use parent hosts for SNI if TLSRroute.hostnames were not specified.
   596  			r.Match[0].SniHosts = parentHosts
   597  		}
   598  		for _, m := range r.Match {
   599  			if port != nil {
   600  				m.Port = uint32(*port)
   601  			}
   602  		}
   603  		res = append(res, r)
   604  	}
   605  	return res
   606  }
   607  
   608  func compatibleRoutesForHost(routes []*istio.TLSRoute, parentHost string) []*istio.TLSRoute {
   609  	res := make([]*istio.TLSRoute, 0, len(routes))
   610  	for _, r := range routes {
   611  		if len(r.Match) == 1 && len(r.Match[0].SniHosts) > 1 {
   612  			r = r.DeepCopy()
   613  			sniHosts := []string{}
   614  			for _, h := range r.Match[0].SniHosts {
   615  				if host.Name(parentHost).Matches(host.Name(h)) {
   616  					sniHosts = append(sniHosts, h)
   617  				}
   618  			}
   619  			r.Match[0].SniHosts = sniHosts
   620  		}
   621  		res = append(res, r)
   622  	}
   623  	return res
   624  }
   625  
   626  func buildGRPCVirtualServices(
   627  	ctx configContext,
   628  	obj config.Config,
   629  	gatewayRoutes map[string]map[string]*config.Config,
   630  	meshRoutes map[string]map[string]*config.Config,
   631  ) {
   632  	route := obj.Spec.(*k8s.GRPCRouteSpec)
   633  	parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, route.Hostnames, gvk.GRPCRoute, obj.Namespace)
   634  	reportStatus := func(results []RouteParentResult) {
   635  		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
   636  			rs := s.(*k8s.GRPCRouteStatus)
   637  			rs.Parents = createRouteStatus(results, obj, rs.Parents)
   638  			return rs
   639  		})
   640  	}
   641  
   642  	type conversionResult struct {
   643  		error  *ConfigError
   644  		routes []*istio.HTTPRoute
   645  	}
   646  	convertRules := func(mesh bool) conversionResult {
   647  		res := conversionResult{}
   648  		for n, r := range route.Rules {
   649  			// split the rule to make sure each rule has up to one match
   650  			matches := slices.Reference(r.Matches)
   651  			if len(matches) == 0 {
   652  				matches = append(matches, nil)
   653  			}
   654  			for _, m := range matches {
   655  				if m != nil {
   656  					r.Matches = []k8s.GRPCRouteMatch{*m}
   657  				}
   658  				vs, err := convertGRPCRoute(r, ctx, obj, n, !mesh)
   659  				// This was a hard error
   660  				if vs == nil {
   661  					res.error = err
   662  					return conversionResult{error: err}
   663  				}
   664  				// Got an error but also routes
   665  				if err != nil {
   666  					res.error = err
   667  				}
   668  
   669  				res.routes = append(res.routes, vs)
   670  			}
   671  		}
   672  		return res
   673  	}
   674  	meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
   675  
   676  	reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
   677  		res := RouteParentResult{
   678  			OriginalReference: r.OriginalReference,
   679  			DeniedReason:      r.DeniedReason,
   680  			RouteError:        gwResult.error,
   681  		}
   682  		if r.IsMesh() {
   683  			res.RouteError = meshResult.error
   684  		}
   685  		return res
   686  	}))
   687  	count := 0
   688  	for _, parent := range filteredReferences(parentRefs) {
   689  		// for gateway routes, build one VS per gateway+host
   690  		routeMap := gatewayRoutes
   691  		routeKey := parent.InternalName
   692  		vsHosts := hostnameToStringList(route.Hostnames)
   693  		routes := gwResult.routes
   694  		if parent.IsMesh() {
   695  			routes = meshResult.routes
   696  			// for mesh routes, build one VS per namespace/port->host
   697  			routeMap = meshRoutes
   698  			routeKey = obj.Namespace
   699  			if parent.OriginalReference.Port != nil {
   700  				routes = augmentPortMatch(routes, *parent.OriginalReference.Port)
   701  				routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port)
   702  			}
   703  			if parent.InternalKind == gvk.ServiceEntry {
   704  				vsHosts = serviceEntryHosts(ctx.ServiceEntry,
   705  					string(parent.OriginalReference.Name),
   706  					string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace))))
   707  			} else {
   708  				vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
   709  					parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
   710  			}
   711  		}
   712  		if len(routes) == 0 {
   713  			continue
   714  		}
   715  		if _, f := routeMap[routeKey]; !f {
   716  			routeMap[routeKey] = make(map[string]*config.Config)
   717  		}
   718  
   719  		// Create one VS per hostname with a single hostname.
   720  		// This ensures we can treat each hostname independently, as the spec requires
   721  		for _, h := range vsHosts {
   722  			if cfg := routeMap[routeKey][h]; cfg != nil {
   723  				// merge http routes
   724  				vs := cfg.Spec.(*istio.VirtualService)
   725  				vs.Http = append(vs.Http, routes...)
   726  				// append parents
   727  				cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s",
   728  					cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
   729  			} else {
   730  				name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
   731  				routeMap[routeKey][h] = &config.Config{
   732  					Meta: config.Meta{
   733  						CreationTimestamp: obj.CreationTimestamp,
   734  						GroupVersionKind:  gvk.VirtualService,
   735  						Name:              name,
   736  						Annotations:       routeMeta(obj),
   737  						Namespace:         obj.Namespace,
   738  						Domain:            ctx.Domain,
   739  					},
   740  					Spec: &istio.VirtualService{
   741  						Hosts:    []string{h},
   742  						Gateways: []string{parent.InternalName},
   743  						Http:     routes,
   744  					},
   745  				}
   746  				count++
   747  			}
   748  		}
   749  	}
   750  	for _, vsByHost := range gatewayRoutes {
   751  		for _, cfg := range vsByHost {
   752  			vs := cfg.Spec.(*istio.VirtualService)
   753  			sortHTTPRoutes(vs.Http)
   754  		}
   755  	}
   756  	for _, vsByHost := range meshRoutes {
   757  		for _, cfg := range vsByHost {
   758  			vs := cfg.Spec.(*istio.VirtualService)
   759  			sortHTTPRoutes(vs.Http)
   760  		}
   761  	}
   762  }
   763  
   764  func routeMeta(obj config.Config) map[string]string {
   765  	m := parentMeta(obj, nil)
   766  	m[constants.InternalRouteSemantics] = constants.RouteSemanticsGateway
   767  	return m
   768  }
   769  
   770  // sortHTTPRoutes sorts generated vs routes to meet gateway-api requirements
   771  // see https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRouteRule
   772  func sortHTTPRoutes(routes []*istio.HTTPRoute) {
   773  	sort.SliceStable(routes, func(i, j int) bool {
   774  		if len(routes[i].Match) == 0 {
   775  			return false
   776  		} else if len(routes[j].Match) == 0 {
   777  			return true
   778  		}
   779  		// Only look at match[0], we always generate only one match
   780  		m1, m2 := routes[i].Match[0], routes[j].Match[0]
   781  		r1, r2 := getURIRank(m1), getURIRank(m2)
   782  		len1, len2 := getURILength(m1), getURILength(m2)
   783  		switch {
   784  		// 1: Exact/Prefix/Regex
   785  		case r1 != r2:
   786  			return r1 > r2
   787  		case len1 != len2:
   788  			return len1 > len2
   789  			// 2: method math
   790  		case (m1.Method == nil) != (m2.Method == nil):
   791  			return m1.Method != nil
   792  			// 3: number of header matches
   793  		case len(m1.Headers) != len(m2.Headers):
   794  			return len(m1.Headers) > len(m2.Headers)
   795  			// 4: number of query matches
   796  		default:
   797  			return len(m1.QueryParams) > len(m2.QueryParams)
   798  		}
   799  	})
   800  }
   801  
   802  // getURIRank ranks a URI match type. Exact > Prefix > Regex
   803  func getURIRank(match *istio.HTTPMatchRequest) int {
   804  	if match.Uri == nil {
   805  		return -1
   806  	}
   807  	switch match.Uri.MatchType.(type) {
   808  	case *istio.StringMatch_Exact:
   809  		return 3
   810  	case *istio.StringMatch_Prefix:
   811  		return 2
   812  	case *istio.StringMatch_Regex:
   813  		return 1
   814  	}
   815  	// should not happen
   816  	return -1
   817  }
   818  
   819  func getURILength(match *istio.HTTPMatchRequest) int {
   820  	if match.Uri == nil {
   821  		return 0
   822  	}
   823  	switch match.Uri.MatchType.(type) {
   824  	case *istio.StringMatch_Prefix:
   825  		return len(match.Uri.GetPrefix())
   826  	case *istio.StringMatch_Exact:
   827  		return len(match.Uri.GetExact())
   828  	case *istio.StringMatch_Regex:
   829  		return len(match.Uri.GetRegex())
   830  	}
   831  	// should not happen
   832  	return -1
   833  }
   834  
   835  func parentMeta(obj config.Config, sectionName *k8s.SectionName) map[string]string {
   836  	name := fmt.Sprintf("%s/%s.%s", obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
   837  	if sectionName != nil {
   838  		name = fmt.Sprintf("%s/%s/%s.%s", obj.GroupVersionKind.Kind, obj.Name, *sectionName, obj.Namespace)
   839  	}
   840  	return map[string]string{
   841  		constants.InternalParentNames: name,
   842  	}
   843  }
   844  
   845  func hostnameToStringList(h []k8s.Hostname) []string {
   846  	// In the Istio API, empty hostname is not allowed. In the Kubernetes API hosts means "any"
   847  	if len(h) == 0 {
   848  		return []string{"*"}
   849  	}
   850  	return slices.Map(h, func(e k8s.Hostname) string {
   851  		return string(e)
   852  	})
   853  }
   854  
   855  func toInternalParentReference(p k8s.ParentReference, localNamespace string) (parentKey, error) {
   856  	empty := parentKey{}
   857  	kind := ptr.OrDefault((*string)(p.Kind), gvk.KubernetesGateway.Kind)
   858  	group := ptr.OrDefault((*string)(p.Group), gvk.KubernetesGateway.Group)
   859  	var ik config.GroupVersionKind
   860  	var ns string
   861  	// Currently supported types are Gateway, Service, and ServiceEntry
   862  	if kind == gvk.KubernetesGateway.Kind && group == gvk.KubernetesGateway.Group {
   863  		ik = gvk.KubernetesGateway
   864  	} else if kind == gvk.Service.Kind && group == gvk.Service.Group {
   865  		ik = gvk.Service
   866  	} else if kind == gvk.ServiceEntry.Kind && group == gvk.ServiceEntry.Group {
   867  		ik = gvk.ServiceEntry
   868  	} else {
   869  		return empty, fmt.Errorf("unsupported parentKey: %v/%v", p.Group, kind)
   870  	}
   871  	// Unset namespace means "same namespace"
   872  	ns = ptr.OrDefault((*string)(p.Namespace), localNamespace)
   873  	return parentKey{
   874  		Kind:      ik,
   875  		Name:      string(p.Name),
   876  		Namespace: ns,
   877  	}, nil
   878  }
   879  
   880  func referenceAllowed(
   881  	parent *parentInfo,
   882  	routeKind config.GroupVersionKind,
   883  	parentRef parentReference,
   884  	hostnames []k8s.Hostname,
   885  	namespace string,
   886  ) *ParentError {
   887  	if parentRef.Kind == gvk.Service || parentRef.Kind == gvk.ServiceEntry {
   888  		// TODO: check if the service reference is valid
   889  		if false {
   890  			return &ParentError{
   891  				Reason:  ParentErrorParentRefConflict,
   892  				Message: fmt.Sprintf("parent service: %q is invalid", parentRef.Name),
   893  			}
   894  		}
   895  	} else {
   896  		// First, check section and port apply. This must come first
   897  		if parentRef.Port != 0 && parentRef.Port != parent.Port {
   898  			return &ParentError{
   899  				Reason:  ParentErrorNotAccepted,
   900  				Message: fmt.Sprintf("port %v not found", parentRef.Port),
   901  			}
   902  		}
   903  		if len(parentRef.SectionName) > 0 && parentRef.SectionName != parent.SectionName {
   904  			return &ParentError{
   905  				Reason:  ParentErrorNotAccepted,
   906  				Message: fmt.Sprintf("sectionName %q not found", parentRef.SectionName),
   907  			}
   908  		}
   909  
   910  		// Next check the hostnames are a match. This is a bi-directional wildcard match. Only one route
   911  		// hostname must match for it to be allowed (but the others will be filtered at runtime)
   912  		// If either is empty its treated as a wildcard which always matches
   913  
   914  		if len(hostnames) == 0 {
   915  			hostnames = []k8s.Hostname{"*"}
   916  		}
   917  		if len(parent.Hostnames) > 0 {
   918  			// TODO: the spec actually has a label match, not a string match. That is, *.com does not match *.apple.com
   919  			// We are doing a string match here
   920  			matched := false
   921  			hostMatched := false
   922  		out:
   923  			for _, routeHostname := range hostnames {
   924  				for _, parentHostNamespace := range parent.Hostnames {
   925  					spl := strings.Split(parentHostNamespace, "/")
   926  					parentNamespace, parentHostname := spl[0], spl[1]
   927  					hostnameMatch := host.Name(parentHostname).Matches(host.Name(routeHostname))
   928  					namespaceMatch := parentNamespace == "*" || parentNamespace == namespace
   929  					hostMatched = hostMatched || hostnameMatch
   930  					if hostnameMatch && namespaceMatch {
   931  						matched = true
   932  						break out
   933  					}
   934  				}
   935  			}
   936  			if !matched {
   937  				if hostMatched {
   938  					return &ParentError{
   939  						Reason: ParentErrorNotAllowed,
   940  						Message: fmt.Sprintf(
   941  							"hostnames matched parent hostname %q, but namespace %q is not allowed by the parent",
   942  							parent.OriginalHostname, namespace,
   943  						),
   944  					}
   945  				}
   946  				return &ParentError{
   947  					Reason: ParentErrorNoHostname,
   948  					Message: fmt.Sprintf(
   949  						"no hostnames matched parent hostname %q",
   950  						parent.OriginalHostname,
   951  					),
   952  				}
   953  			}
   954  		}
   955  	}
   956  	// Also make sure this route kind is allowed
   957  	matched := false
   958  	for _, ak := range parent.AllowedKinds {
   959  		if string(ak.Kind) == routeKind.Kind && ptr.OrDefault((*string)(ak.Group), gvk.GatewayClass.Group) == routeKind.Group {
   960  			matched = true
   961  			break
   962  		}
   963  	}
   964  	if !matched {
   965  		return &ParentError{
   966  			Reason:  ParentErrorNotAllowed,
   967  			Message: fmt.Sprintf("kind %v is not allowed", routeKind),
   968  		}
   969  	}
   970  	return nil
   971  }
   972  
   973  func extractParentReferenceInfo(gateways map[parentKey][]*parentInfo, routeRefs []k8s.ParentReference,
   974  	hostnames []k8s.Hostname, kind config.GroupVersionKind, localNamespace string,
   975  ) []routeParentReference {
   976  	parentRefs := []routeParentReference{}
   977  	for _, ref := range routeRefs {
   978  		ir, err := toInternalParentReference(ref, localNamespace)
   979  		if err != nil {
   980  			// Cannot handle the reference. Maybe it is for another controller, so we just ignore it
   981  			continue
   982  		}
   983  		pk := parentReference{
   984  			parentKey:   ir,
   985  			SectionName: ptr.OrEmpty(ref.SectionName),
   986  			Port:        ptr.OrEmpty(ref.Port),
   987  		}
   988  		gk := ir
   989  		if ir.Kind == gvk.Service || ir.Kind == gvk.ServiceEntry {
   990  			gk = meshParentKey
   991  		}
   992  		appendParent := func(pr *parentInfo, pk parentReference) {
   993  			bannedHostnames := sets.New[string]()
   994  			for _, gw := range gateways[gk] {
   995  				if gw == pr {
   996  					continue // do not ban ourself
   997  				}
   998  				if gw.Port != pr.Port {
   999  					// We only care about listeners on the same port
  1000  					continue
  1001  				}
  1002  				if gw.Protocol != pr.Protocol {
  1003  					// We only care about listeners on the same protocol
  1004  					continue
  1005  				}
  1006  				bannedHostnames.Insert(gw.OriginalHostname)
  1007  			}
  1008  			rpi := routeParentReference{
  1009  				InternalName:      pr.InternalName,
  1010  				InternalKind:      ir.Kind,
  1011  				Hostname:          pr.OriginalHostname,
  1012  				DeniedReason:      referenceAllowed(pr, kind, pk, hostnames, localNamespace),
  1013  				OriginalReference: ref,
  1014  				BannedHostnames:   bannedHostnames.Copy().Delete(pr.OriginalHostname),
  1015  			}
  1016  			if rpi.DeniedReason == nil {
  1017  				// Record that we were able to bind to the parent
  1018  				pr.AttachedRoutes++
  1019  			}
  1020  			parentRefs = append(parentRefs, rpi)
  1021  		}
  1022  		for _, gw := range gateways[gk] {
  1023  			// Append all matches. Note we may be adding mismatch section or ports; this is handled later
  1024  			appendParent(gw, pk)
  1025  		}
  1026  	}
  1027  	// Ensure stable order
  1028  	slices.SortBy(parentRefs, func(a routeParentReference) string {
  1029  		return parentRefString(a.OriginalReference)
  1030  	})
  1031  	return parentRefs
  1032  }
  1033  
  1034  func buildTCPVirtualService(ctx configContext, obj config.Config) []config.Config {
  1035  	route := obj.Spec.(*k8salpha.TCPRouteSpec)
  1036  	parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, nil, gvk.TCPRoute, obj.Namespace)
  1037  
  1038  	reportStatus := func(results []RouteParentResult) {
  1039  		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
  1040  			rs := s.(*k8salpha.TCPRouteStatus)
  1041  			rs.Parents = createRouteStatus(results, obj, rs.Parents)
  1042  			return rs
  1043  		})
  1044  	}
  1045  	type conversionResult struct {
  1046  		error  *ConfigError
  1047  		routes []*istio.TCPRoute
  1048  	}
  1049  	convertRules := func(mesh bool) conversionResult {
  1050  		res := conversionResult{}
  1051  		for _, r := range route.Rules {
  1052  			vs, err := convertTCPRoute(ctx, r, obj, !mesh)
  1053  			// This was a hard error
  1054  			if vs == nil {
  1055  				res.error = err
  1056  				return conversionResult{error: err}
  1057  			}
  1058  			// Got an error but also routes
  1059  			if err != nil {
  1060  				res.error = err
  1061  			}
  1062  			res.routes = append(res.routes, vs)
  1063  		}
  1064  		return res
  1065  	}
  1066  	meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
  1067  	reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
  1068  		res := RouteParentResult{
  1069  			OriginalReference: r.OriginalReference,
  1070  			DeniedReason:      r.DeniedReason,
  1071  			RouteError:        gwResult.error,
  1072  		}
  1073  		if r.IsMesh() {
  1074  			res.RouteError = meshResult.error
  1075  		}
  1076  		return res
  1077  	}))
  1078  
  1079  	vs := []config.Config{}
  1080  	for _, parent := range filteredReferences(parentRefs) {
  1081  		routes := gwResult.routes
  1082  		vsHosts := []string{"*"}
  1083  		if parent.IsMesh() {
  1084  			routes = meshResult.routes
  1085  			if parent.OriginalReference.Port != nil {
  1086  				routes = augmentTCPPortMatch(routes, *parent.OriginalReference.Port)
  1087  			}
  1088  			if parent.InternalKind == gvk.ServiceEntry {
  1089  				vsHosts = serviceEntryHosts(ctx.ServiceEntry,
  1090  					string(parent.OriginalReference.Name),
  1091  					string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace))))
  1092  			} else {
  1093  				vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
  1094  					parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
  1095  			}
  1096  		}
  1097  		for i, host := range vsHosts {
  1098  			name := fmt.Sprintf("%s-tcp-%d-%s", obj.Name, i, constants.KubernetesGatewayName)
  1099  			// Create one VS per hostname with a single hostname.
  1100  			// This ensures we can treat each hostname independently, as the spec requires
  1101  			vs = append(vs, config.Config{
  1102  				Meta: config.Meta{
  1103  					CreationTimestamp: obj.CreationTimestamp,
  1104  					GroupVersionKind:  gvk.VirtualService,
  1105  					Name:              name,
  1106  					Annotations:       routeMeta(obj),
  1107  					Namespace:         obj.Namespace,
  1108  					Domain:            ctx.Domain,
  1109  				},
  1110  				Spec: &istio.VirtualService{
  1111  					// We can use wildcard here since each listener can have at most one route bound to it, so we have
  1112  					// a single VS per Gateway.
  1113  					Hosts:    []string{host},
  1114  					Gateways: []string{parent.InternalName},
  1115  					Tcp:      routes,
  1116  				},
  1117  			})
  1118  		}
  1119  	}
  1120  	return vs
  1121  }
  1122  
  1123  func buildTLSVirtualService(ctx configContext, obj config.Config) []config.Config {
  1124  	route := obj.Spec.(*k8salpha.TLSRouteSpec)
  1125  	parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, nil, gvk.TLSRoute, obj.Namespace)
  1126  
  1127  	reportStatus := func(results []RouteParentResult) {
  1128  		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
  1129  			rs := s.(*k8salpha.TLSRouteStatus)
  1130  			rs.Parents = createRouteStatus(results, obj, rs.Parents)
  1131  			return rs
  1132  		})
  1133  	}
  1134  	type conversionResult struct {
  1135  		error  *ConfigError
  1136  		routes []*istio.TLSRoute
  1137  	}
  1138  	convertRules := func(mesh bool) conversionResult {
  1139  		res := conversionResult{}
  1140  		for _, r := range route.Rules {
  1141  			vs, err := convertTLSRoute(ctx, r, obj, !mesh)
  1142  			// This was a hard error
  1143  			if vs == nil {
  1144  				res.error = err
  1145  				return conversionResult{error: err}
  1146  			}
  1147  			// Got an error but also routes
  1148  			if err != nil {
  1149  				res.error = err
  1150  			}
  1151  			res.routes = append(res.routes, vs)
  1152  		}
  1153  		return res
  1154  	}
  1155  	meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
  1156  	reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
  1157  		res := RouteParentResult{
  1158  			OriginalReference: r.OriginalReference,
  1159  			DeniedReason:      r.DeniedReason,
  1160  			RouteError:        gwResult.error,
  1161  		}
  1162  		if r.IsMesh() {
  1163  			res.RouteError = meshResult.error
  1164  		}
  1165  		return res
  1166  	}))
  1167  
  1168  	vs := []config.Config{}
  1169  	for _, parent := range filteredReferences(parentRefs) {
  1170  		routes := gwResult.routes
  1171  		vsHosts := hostnameToStringList(route.Hostnames)
  1172  		if parent.IsMesh() {
  1173  			routes = meshResult.routes
  1174  			if parent.InternalKind == gvk.ServiceEntry {
  1175  				vsHosts = serviceEntryHosts(ctx.ServiceEntry,
  1176  					string(parent.OriginalReference.Name),
  1177  					string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace))))
  1178  			} else {
  1179  				host := fmt.Sprintf("%s.%s.svc.%s",
  1180  					parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)
  1181  				vsHosts = []string{host}
  1182  			}
  1183  			routes = augmentTLSPortMatch(routes, parent.OriginalReference.Port, vsHosts)
  1184  		}
  1185  
  1186  		for i, host := range vsHosts {
  1187  			name := fmt.Sprintf("%s-tls-%d-%s", obj.Name, i, constants.KubernetesGatewayName)
  1188  			filteredRoutes := routes
  1189  			if parent.IsMesh() {
  1190  				filteredRoutes = compatibleRoutesForHost(routes, host)
  1191  			}
  1192  			// Create one VS per hostname with a single hostname.
  1193  			// This ensures we can treat each hostname independently, as the spec requires
  1194  			vs = append(vs, config.Config{
  1195  				Meta: config.Meta{
  1196  					CreationTimestamp: obj.CreationTimestamp,
  1197  					GroupVersionKind:  gvk.VirtualService,
  1198  					Name:              name,
  1199  					Annotations:       routeMeta(obj),
  1200  					Namespace:         obj.Namespace,
  1201  					Domain:            ctx.Domain,
  1202  				},
  1203  				Spec: &istio.VirtualService{
  1204  					Hosts:    []string{host},
  1205  					Gateways: []string{parent.InternalName},
  1206  					Tls:      filteredRoutes,
  1207  				},
  1208  			})
  1209  		}
  1210  	}
  1211  	return vs
  1212  }
  1213  
  1214  func convertTCPRoute(ctx configContext, r k8salpha.TCPRouteRule, obj config.Config, enforceRefGrant bool) (*istio.TCPRoute, *ConfigError) {
  1215  	if tcpWeightSum(r.BackendRefs) == 0 {
  1216  		// The spec requires us to reject connections when there are no >0 weight backends
  1217  		// We don't have a great way to do it. TODO: add a fault injection API for TCP?
  1218  		return &istio.TCPRoute{
  1219  			Route: []*istio.RouteDestination{{
  1220  				Destination: &istio.Destination{
  1221  					Host:   "internal.cluster.local",
  1222  					Subset: "zero-weight",
  1223  					Port:   &istio.PortSelector{Number: 65535},
  1224  				},
  1225  				Weight: 0,
  1226  			}},
  1227  		}, nil
  1228  	}
  1229  	dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TCPRoute)
  1230  	if err != nil {
  1231  		return nil, err
  1232  	}
  1233  	return &istio.TCPRoute{
  1234  		Route: dest,
  1235  	}, backendErr
  1236  }
  1237  
  1238  func convertTLSRoute(ctx configContext, r k8salpha.TLSRouteRule, obj config.Config, enforceRefGrant bool) (*istio.TLSRoute, *ConfigError) {
  1239  	if tcpWeightSum(r.BackendRefs) == 0 {
  1240  		// The spec requires us to reject connections when there are no >0 weight backends
  1241  		// We don't have a great way to do it. TODO: add a fault injection API for TCP?
  1242  		return &istio.TLSRoute{
  1243  			Route: []*istio.RouteDestination{{
  1244  				Destination: &istio.Destination{
  1245  					Host:   "internal.cluster.local",
  1246  					Subset: "zero-weight",
  1247  					Port:   &istio.PortSelector{Number: 65535},
  1248  				},
  1249  				Weight: 0,
  1250  			}},
  1251  		}, nil
  1252  	}
  1253  	dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TLSRoute)
  1254  	if err != nil {
  1255  		return nil, err
  1256  	}
  1257  	return &istio.TLSRoute{
  1258  		Match: buildTLSMatch(obj.Spec.(*k8salpha.TLSRouteSpec).Hostnames),
  1259  		Route: dest,
  1260  	}, backendErr
  1261  }
  1262  
  1263  func buildTCPDestination(
  1264  	ctx configContext,
  1265  	forwardTo []k8s.BackendRef,
  1266  	ns string,
  1267  	enforceRefGrant bool,
  1268  	k config.GroupVersionKind,
  1269  ) ([]*istio.RouteDestination, *ConfigError, *ConfigError) {
  1270  	if forwardTo == nil {
  1271  		return nil, nil, nil
  1272  	}
  1273  
  1274  	weights := []int{}
  1275  	action := []k8s.BackendRef{}
  1276  	for _, w := range forwardTo {
  1277  		wt := int(ptr.OrDefault(w.Weight, 1))
  1278  		if wt == 0 {
  1279  			continue
  1280  		}
  1281  		action = append(action, w)
  1282  		weights = append(weights, wt)
  1283  	}
  1284  	if len(weights) == 1 {
  1285  		weights = []int{0}
  1286  	}
  1287  
  1288  	var invalidBackendErr *ConfigError
  1289  	res := []*istio.RouteDestination{}
  1290  	for i, fwd := range action {
  1291  		dst, err := buildDestination(ctx, fwd, ns, enforceRefGrant, k)
  1292  		if err != nil {
  1293  			if isInvalidBackend(err) {
  1294  				invalidBackendErr = err
  1295  				// keep going, we will gracefully drop invalid backends
  1296  			} else {
  1297  				return nil, nil, err
  1298  			}
  1299  		}
  1300  		res = append(res, &istio.RouteDestination{
  1301  			Destination: dst,
  1302  			Weight:      int32(weights[i]),
  1303  		})
  1304  	}
  1305  	return res, invalidBackendErr, nil
  1306  }
  1307  
  1308  func buildTLSMatch(hostnames []k8s.Hostname) []*istio.TLSMatchAttributes {
  1309  	// Currently, the spec only supports extensions beyond hostname, which are not currently implemented by Istio.
  1310  	return []*istio.TLSMatchAttributes{{
  1311  		SniHosts: hostnamesToStringListWithWildcard(hostnames),
  1312  	}}
  1313  }
  1314  
  1315  func hostnamesToStringListWithWildcard(h []k8s.Hostname) []string {
  1316  	if len(h) == 0 {
  1317  		return []string{"*"}
  1318  	}
  1319  	res := make([]string, 0, len(h))
  1320  	for _, i := range h {
  1321  		res = append(res, string(i))
  1322  	}
  1323  	return res
  1324  }
  1325  
  1326  func weightSum(forwardTo []k8s.HTTPBackendRef) int {
  1327  	sum := int32(0)
  1328  	for _, w := range forwardTo {
  1329  		sum += ptr.OrDefault(w.Weight, 1)
  1330  	}
  1331  	return int(sum)
  1332  }
  1333  
  1334  func grpcWeightSum(forwardTo []k8s.GRPCBackendRef) int {
  1335  	sum := int32(0)
  1336  	for _, w := range forwardTo {
  1337  		sum += ptr.OrDefault(w.Weight, 1)
  1338  	}
  1339  	return int(sum)
  1340  }
  1341  
  1342  func tcpWeightSum(forwardTo []k8s.BackendRef) int {
  1343  	sum := int32(0)
  1344  	for _, w := range forwardTo {
  1345  		sum += ptr.OrDefault(w.Weight, 1)
  1346  	}
  1347  	return int(sum)
  1348  }
  1349  
  1350  func buildHTTPDestination(
  1351  	ctx configContext,
  1352  	forwardTo []k8s.HTTPBackendRef,
  1353  	ns string,
  1354  	enforceRefGrant bool,
  1355  ) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) {
  1356  	if forwardTo == nil {
  1357  		return nil, nil, nil
  1358  	}
  1359  	weights := []int{}
  1360  	action := []k8s.HTTPBackendRef{}
  1361  	for _, w := range forwardTo {
  1362  		wt := int(ptr.OrDefault(w.Weight, 1))
  1363  		if wt == 0 {
  1364  			continue
  1365  		}
  1366  		action = append(action, w)
  1367  		weights = append(weights, wt)
  1368  	}
  1369  	if len(weights) == 1 {
  1370  		weights = []int{0}
  1371  	}
  1372  
  1373  	var invalidBackendErr *ConfigError
  1374  	res := []*istio.HTTPRouteDestination{}
  1375  	for i, fwd := range action {
  1376  		dst, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.HTTPRoute)
  1377  		if err != nil {
  1378  			if isInvalidBackend(err) {
  1379  				invalidBackendErr = err
  1380  				// keep going, we will gracefully drop invalid backends
  1381  			} else {
  1382  				return nil, nil, err
  1383  			}
  1384  		}
  1385  		rd := &istio.HTTPRouteDestination{
  1386  			Destination: dst,
  1387  			Weight:      int32(weights[i]),
  1388  		}
  1389  		for _, filter := range fwd.Filters {
  1390  			switch filter.Type {
  1391  			case k8s.HTTPRouteFilterRequestHeaderModifier:
  1392  				h := createHeadersFilter(filter.RequestHeaderModifier)
  1393  				if h == nil {
  1394  					continue
  1395  				}
  1396  				if rd.Headers == nil {
  1397  					rd.Headers = &istio.Headers{}
  1398  				}
  1399  				rd.Headers.Request = h
  1400  			case k8s.HTTPRouteFilterResponseHeaderModifier:
  1401  				h := createHeadersFilter(filter.ResponseHeaderModifier)
  1402  				if h == nil {
  1403  					continue
  1404  				}
  1405  				if rd.Headers == nil {
  1406  					rd.Headers = &istio.Headers{}
  1407  				}
  1408  				rd.Headers.Response = h
  1409  			default:
  1410  				return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
  1411  			}
  1412  		}
  1413  		res = append(res, rd)
  1414  	}
  1415  	return res, invalidBackendErr, nil
  1416  }
  1417  
  1418  func buildGRPCDestination(
  1419  	ctx configContext,
  1420  	forwardTo []k8s.GRPCBackendRef,
  1421  	ns string,
  1422  	enforceRefGrant bool,
  1423  ) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) {
  1424  	if forwardTo == nil {
  1425  		return nil, nil, nil
  1426  	}
  1427  	weights := []int{}
  1428  	action := []k8s.GRPCBackendRef{}
  1429  	for _, w := range forwardTo {
  1430  		wt := int(ptr.OrDefault(w.Weight, 1))
  1431  		if wt == 0 {
  1432  			continue
  1433  		}
  1434  		action = append(action, w)
  1435  		weights = append(weights, wt)
  1436  	}
  1437  	if len(weights) == 1 {
  1438  		weights = []int{0}
  1439  	}
  1440  
  1441  	var invalidBackendErr *ConfigError
  1442  	res := []*istio.HTTPRouteDestination{}
  1443  	for i, fwd := range action {
  1444  		dst, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.GRPCRoute)
  1445  		if err != nil {
  1446  			if isInvalidBackend(err) {
  1447  				invalidBackendErr = err
  1448  				// keep going, we will gracefully drop invalid backends
  1449  			} else {
  1450  				return nil, nil, err
  1451  			}
  1452  		}
  1453  		rd := &istio.HTTPRouteDestination{
  1454  			Destination: dst,
  1455  			Weight:      int32(weights[i]),
  1456  		}
  1457  		for _, filter := range fwd.Filters {
  1458  			switch filter.Type {
  1459  			case k8s.GRPCRouteFilterRequestHeaderModifier:
  1460  				h := createHeadersFilter(filter.RequestHeaderModifier)
  1461  				if h == nil {
  1462  					continue
  1463  				}
  1464  				if rd.Headers == nil {
  1465  					rd.Headers = &istio.Headers{}
  1466  				}
  1467  				rd.Headers.Request = h
  1468  			case k8s.GRPCRouteFilterResponseHeaderModifier:
  1469  				h := createHeadersFilter(filter.ResponseHeaderModifier)
  1470  				if h == nil {
  1471  					continue
  1472  				}
  1473  				if rd.Headers == nil {
  1474  					rd.Headers = &istio.Headers{}
  1475  				}
  1476  				rd.Headers.Response = h
  1477  			default:
  1478  				return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
  1479  			}
  1480  		}
  1481  		res = append(res, rd)
  1482  	}
  1483  	return res, invalidBackendErr, nil
  1484  }
  1485  
  1486  func buildDestination(ctx configContext, to k8s.BackendRef, ns string, enforceRefGrant bool, k config.GroupVersionKind) (*istio.Destination, *ConfigError) {
  1487  	// check if the reference is allowed
  1488  	if enforceRefGrant {
  1489  		refs := ctx.AllowedReferences
  1490  		if toNs := to.Namespace; toNs != nil && string(*toNs) != ns {
  1491  			if !refs.BackendAllowed(k, to.Name, *toNs, ns) {
  1492  				return &istio.Destination{}, &ConfigError{
  1493  					Reason:  InvalidDestinationPermit,
  1494  					Message: fmt.Sprintf("backendRef %v/%v not accessible to a %s in namespace %q (missing a ReferenceGrant?)", to.Name, *toNs, k.Kind, ns),
  1495  				}
  1496  			}
  1497  		}
  1498  	}
  1499  
  1500  	namespace := ptr.OrDefault((*string)(to.Namespace), ns)
  1501  	var invalidBackendErr *ConfigError
  1502  	if nilOrEqual((*string)(to.Group), "") && nilOrEqual((*string)(to.Kind), gvk.Service.Kind) {
  1503  		// Service
  1504  		if to.Port == nil {
  1505  			// "Port is required when the referent is a Kubernetes Service."
  1506  			return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
  1507  		}
  1508  		if strings.Contains(string(to.Name), ".") {
  1509  			return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."}
  1510  		}
  1511  		hostname := fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.Domain)
  1512  		if ctx.Context.GetService(hostname, namespace) == nil {
  1513  			invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
  1514  		}
  1515  		return &istio.Destination{
  1516  			// TODO: implement ReferencePolicy for cross namespace
  1517  			Host: hostname,
  1518  			Port: &istio.PortSelector{Number: uint32(*to.Port)},
  1519  		}, invalidBackendErr
  1520  	}
  1521  	if nilOrEqual((*string)(to.Group), features.MCSAPIGroup) && nilOrEqual((*string)(to.Kind), "ServiceImport") {
  1522  		// Service import
  1523  		hostname := fmt.Sprintf("%s.%s.svc.clusterset.local", to.Name, namespace)
  1524  		if !features.EnableMCSHost {
  1525  			// They asked for ServiceImport, but actually don't have full support enabled...
  1526  			// No problem, we can just treat it as Service, which is already cross-cluster in this mode anyways
  1527  			hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.Domain)
  1528  		}
  1529  		if to.Port == nil {
  1530  			// We don't know where to send without port
  1531  			return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
  1532  		}
  1533  		if strings.Contains(string(to.Name), ".") {
  1534  			return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."}
  1535  		}
  1536  		if ctx.Context.GetService(hostname, namespace) == nil {
  1537  			invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
  1538  		}
  1539  		return &istio.Destination{
  1540  			Host: hostname,
  1541  			Port: &istio.PortSelector{Number: uint32(*to.Port)},
  1542  		}, invalidBackendErr
  1543  	}
  1544  	if nilOrEqual((*string)(to.Group), gvk.ServiceEntry.Group) && nilOrEqual((*string)(to.Kind), "Hostname") {
  1545  		// Hostname synthetic type
  1546  		if to.Port == nil {
  1547  			// We don't know where to send without port
  1548  			return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
  1549  		}
  1550  		if to.Namespace != nil {
  1551  			return nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"}
  1552  		}
  1553  		hostname := string(to.Name)
  1554  		if ctx.Context.GetService(hostname, namespace) == nil {
  1555  			invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
  1556  		}
  1557  		return &istio.Destination{
  1558  			Host: string(to.Name),
  1559  			Port: &istio.PortSelector{Number: uint32(*to.Port)},
  1560  		}, invalidBackendErr
  1561  	}
  1562  	return &istio.Destination{}, &ConfigError{
  1563  		Reason:  InvalidDestinationKind,
  1564  		Message: fmt.Sprintf("referencing unsupported backendRef: group %q kind %q", ptr.OrEmpty(to.Group), ptr.OrEmpty(to.Kind)),
  1565  	}
  1566  }
  1567  
  1568  // https://github.com/kubernetes-sigs/gateway-api/blob/cea484e38e078a2c1997d8c7a62f410a1540f519/apis/v1beta1/httproute_types.go#L207-L212
  1569  func isInvalidBackend(err *ConfigError) bool {
  1570  	return err.Reason == InvalidDestinationPermit ||
  1571  		err.Reason == InvalidDestinationNotFound ||
  1572  		err.Reason == InvalidDestinationKind
  1573  }
  1574  
  1575  func headerListToMap(hl []k8s.HTTPHeader) map[string]string {
  1576  	if len(hl) == 0 {
  1577  		return nil
  1578  	}
  1579  	res := map[string]string{}
  1580  	for _, e := range hl {
  1581  		k := strings.ToLower(string(e.Name))
  1582  		if _, f := res[k]; f {
  1583  			// "Subsequent entries with an equivalent header name MUST be ignored"
  1584  			continue
  1585  		}
  1586  		res[k] = e.Value
  1587  	}
  1588  	return res
  1589  }
  1590  
  1591  func createMirrorFilter(ctx configContext, filter *k8s.HTTPRequestMirrorFilter, ns string,
  1592  	enforceRefGrant bool, k config.GroupVersionKind,
  1593  ) (*istio.HTTPMirrorPolicy, *ConfigError) {
  1594  	if filter == nil {
  1595  		return nil, nil
  1596  	}
  1597  	var weightOne int32 = 1
  1598  	dst, err := buildDestination(ctx, k8s.BackendRef{
  1599  		BackendObjectReference: filter.BackendRef,
  1600  		Weight:                 &weightOne,
  1601  	}, ns, enforceRefGrant, k)
  1602  	if err != nil {
  1603  		return nil, err
  1604  	}
  1605  	return &istio.HTTPMirrorPolicy{Destination: dst}, nil
  1606  }
  1607  
  1608  func createRewriteFilter(filter *k8s.HTTPURLRewriteFilter) *istio.HTTPRewrite {
  1609  	if filter == nil {
  1610  		return nil
  1611  	}
  1612  	rewrite := &istio.HTTPRewrite{}
  1613  	if filter.Path != nil {
  1614  		switch filter.Path.Type {
  1615  		case k8s.PrefixMatchHTTPPathModifier:
  1616  			rewrite.Uri = strings.TrimSuffix(*filter.Path.ReplacePrefixMatch, "/")
  1617  			if rewrite.Uri == "" {
  1618  				// `/` means removing the prefix
  1619  				rewrite.Uri = "/"
  1620  			}
  1621  		case k8s.FullPathHTTPPathModifier:
  1622  			rewrite.UriRegexRewrite = &istio.RegexRewrite{
  1623  				Match:   "/.*",
  1624  				Rewrite: *filter.Path.ReplaceFullPath,
  1625  			}
  1626  		}
  1627  	}
  1628  	if filter.Hostname != nil {
  1629  		rewrite.Authority = string(*filter.Hostname)
  1630  	}
  1631  	// Nothing done
  1632  	if rewrite.Uri == "" && rewrite.UriRegexRewrite == nil && rewrite.Authority == "" {
  1633  		return nil
  1634  	}
  1635  	return rewrite
  1636  }
  1637  
  1638  func createRedirectFilter(filter *k8s.HTTPRequestRedirectFilter) *istio.HTTPRedirect {
  1639  	if filter == nil {
  1640  		return nil
  1641  	}
  1642  	resp := &istio.HTTPRedirect{}
  1643  	if filter.StatusCode != nil {
  1644  		// Istio allows 301, 302, 303, 307, 308.
  1645  		// Gateway allows only 301 and 302.
  1646  		resp.RedirectCode = uint32(*filter.StatusCode)
  1647  	}
  1648  	if filter.Hostname != nil {
  1649  		resp.Authority = string(*filter.Hostname)
  1650  	}
  1651  	if filter.Scheme != nil {
  1652  		// Both allow http and https
  1653  		resp.Scheme = *filter.Scheme
  1654  	}
  1655  	if filter.Port != nil {
  1656  		resp.RedirectPort = &istio.HTTPRedirect_Port{Port: uint32(*filter.Port)}
  1657  	} else {
  1658  		// "When empty, port (if specified) of the request is used."
  1659  		// this differs from Istio default
  1660  		if filter.Scheme != nil {
  1661  			resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_PROTOCOL_DEFAULT}
  1662  		} else {
  1663  			resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_REQUEST_PORT}
  1664  		}
  1665  	}
  1666  	if filter.Path != nil {
  1667  		switch filter.Path.Type {
  1668  		case k8s.FullPathHTTPPathModifier:
  1669  			resp.Uri = *filter.Path.ReplaceFullPath
  1670  		case k8s.PrefixMatchHTTPPathModifier:
  1671  			resp.Uri = fmt.Sprintf("%%PREFIX()%%%s", *filter.Path.ReplacePrefixMatch)
  1672  		}
  1673  	}
  1674  	return resp
  1675  }
  1676  
  1677  func createHeadersFilter(filter *k8s.HTTPHeaderFilter) *istio.Headers_HeaderOperations {
  1678  	if filter == nil {
  1679  		return nil
  1680  	}
  1681  	return &istio.Headers_HeaderOperations{
  1682  		Add:    headerListToMap(filter.Add),
  1683  		Remove: filter.Remove,
  1684  		Set:    headerListToMap(filter.Set),
  1685  	}
  1686  }
  1687  
  1688  // nolint: unparam
  1689  func createMethodMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
  1690  	if match.Method == nil {
  1691  		return nil, nil
  1692  	}
  1693  	return &istio.StringMatch{
  1694  		MatchType: &istio.StringMatch_Exact{Exact: string(*match.Method)},
  1695  	}, nil
  1696  }
  1697  
  1698  func createQueryParamsMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
  1699  	res := map[string]*istio.StringMatch{}
  1700  	for _, qp := range match.QueryParams {
  1701  		tp := k8s.QueryParamMatchExact
  1702  		if qp.Type != nil {
  1703  			tp = *qp.Type
  1704  		}
  1705  		switch tp {
  1706  		case k8s.QueryParamMatchExact:
  1707  			res[string(qp.Name)] = &istio.StringMatch{
  1708  				MatchType: &istio.StringMatch_Exact{Exact: qp.Value},
  1709  			}
  1710  		case k8s.QueryParamMatchRegularExpression:
  1711  			res[string(qp.Name)] = &istio.StringMatch{
  1712  				MatchType: &istio.StringMatch_Regex{Regex: qp.Value},
  1713  			}
  1714  		default:
  1715  			// Should never happen, unless a new field is added
  1716  			return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported QueryParams type", tp)}
  1717  		}
  1718  	}
  1719  
  1720  	if len(res) == 0 {
  1721  		return nil, nil
  1722  	}
  1723  	return res, nil
  1724  }
  1725  
  1726  func createHeadersMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
  1727  	res := map[string]*istio.StringMatch{}
  1728  	for _, header := range match.Headers {
  1729  		tp := k8s.HeaderMatchExact
  1730  		if header.Type != nil {
  1731  			tp = *header.Type
  1732  		}
  1733  		switch tp {
  1734  		case k8s.HeaderMatchExact:
  1735  			res[string(header.Name)] = &istio.StringMatch{
  1736  				MatchType: &istio.StringMatch_Exact{Exact: header.Value},
  1737  			}
  1738  		case k8s.HeaderMatchRegularExpression:
  1739  			res[string(header.Name)] = &istio.StringMatch{
  1740  				MatchType: &istio.StringMatch_Regex{Regex: header.Value},
  1741  			}
  1742  		default:
  1743  			// Should never happen, unless a new field is added
  1744  			return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)}
  1745  		}
  1746  	}
  1747  
  1748  	if len(res) == 0 {
  1749  		return nil, nil
  1750  	}
  1751  	return res, nil
  1752  }
  1753  
  1754  func createGRPCHeadersMatch(match k8s.GRPCRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
  1755  	res := map[string]*istio.StringMatch{}
  1756  	for _, header := range match.Headers {
  1757  		tp := k8s.HeaderMatchExact
  1758  		if header.Type != nil {
  1759  			tp = *header.Type
  1760  		}
  1761  		switch tp {
  1762  		case k8s.HeaderMatchExact:
  1763  			res[string(header.Name)] = &istio.StringMatch{
  1764  				MatchType: &istio.StringMatch_Exact{Exact: header.Value},
  1765  			}
  1766  		case k8s.HeaderMatchRegularExpression:
  1767  			res[string(header.Name)] = &istio.StringMatch{
  1768  				MatchType: &istio.StringMatch_Regex{Regex: header.Value},
  1769  			}
  1770  		default:
  1771  			// Should never happen, unless a new field is added
  1772  			return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)}
  1773  		}
  1774  	}
  1775  
  1776  	if len(res) == 0 {
  1777  		return nil, nil
  1778  	}
  1779  	return res, nil
  1780  }
  1781  
  1782  func createURIMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
  1783  	tp := k8s.PathMatchPathPrefix
  1784  	if match.Path.Type != nil {
  1785  		tp = *match.Path.Type
  1786  	}
  1787  	dest := "/"
  1788  	if match.Path.Value != nil {
  1789  		dest = *match.Path.Value
  1790  	}
  1791  	switch tp {
  1792  	case k8s.PathMatchPathPrefix:
  1793  		// "When specified, a trailing `/` is ignored."
  1794  		if dest != "/" {
  1795  			dest = strings.TrimSuffix(dest, "/")
  1796  		}
  1797  		return &istio.StringMatch{
  1798  			MatchType: &istio.StringMatch_Prefix{Prefix: dest},
  1799  		}, nil
  1800  	case k8s.PathMatchExact:
  1801  		return &istio.StringMatch{
  1802  			MatchType: &istio.StringMatch_Exact{Exact: dest},
  1803  		}, nil
  1804  	case k8s.PathMatchRegularExpression:
  1805  		return &istio.StringMatch{
  1806  			MatchType: &istio.StringMatch_Regex{Regex: dest},
  1807  		}, nil
  1808  	default:
  1809  		// Should never happen, unless a new field is added
  1810  		return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)}
  1811  	}
  1812  }
  1813  
  1814  func createGRPCURIMatch(match k8s.GRPCRouteMatch) (*istio.StringMatch, *ConfigError) {
  1815  	m := match.Method
  1816  	if m == nil {
  1817  		return nil, nil
  1818  	}
  1819  	tp := k8s.GRPCMethodMatchExact
  1820  	if m.Type != nil {
  1821  		tp = *m.Type
  1822  	}
  1823  	if m.Method == nil && m.Service == nil {
  1824  		// Should never happen, invalid per spec
  1825  		return nil, &ConfigError{Reason: InvalidConfiguration, Message: "gRPC match must have method or service defined"}
  1826  	}
  1827  	// gRPC format is /<Service>/<Method>. Since we don't natively understand this, convert to various string matches
  1828  	switch tp {
  1829  	case k8s.GRPCMethodMatchExact:
  1830  		if m.Method == nil {
  1831  			return &istio.StringMatch{
  1832  				MatchType: &istio.StringMatch_Prefix{Prefix: fmt.Sprintf("/%s/", *m.Service)},
  1833  			}, nil
  1834  		}
  1835  		if m.Service == nil {
  1836  			return &istio.StringMatch{
  1837  				MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)},
  1838  			}, nil
  1839  		}
  1840  		return &istio.StringMatch{
  1841  			MatchType: &istio.StringMatch_Exact{Exact: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)},
  1842  		}, nil
  1843  	case k8s.GRPCMethodMatchRegularExpression:
  1844  		if m.Method == nil {
  1845  			return &istio.StringMatch{
  1846  				MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/.+", *m.Service)},
  1847  			}, nil
  1848  		}
  1849  		if m.Service == nil {
  1850  			return &istio.StringMatch{
  1851  				MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)},
  1852  			}, nil
  1853  		}
  1854  		return &istio.StringMatch{
  1855  			MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)},
  1856  		}, nil
  1857  	default:
  1858  		// Should never happen, unless a new field is added
  1859  		return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)}
  1860  	}
  1861  }
  1862  
  1863  // getGatewayClass finds all gateway class that are owned by Istio
  1864  // Response is ClassName -> Controller type
  1865  func getGatewayClasses(r GatewayResources, supportedFeatures []k8s.SupportedFeature) map[string]k8s.GatewayController {
  1866  	res := map[string]k8s.GatewayController{}
  1867  	// Setup builtin ones - these can be overridden possibly
  1868  	for name, controller := range builtinClasses {
  1869  		res[string(name)] = controller
  1870  	}
  1871  	for _, obj := range r.GatewayClass {
  1872  		gwc := obj.Spec.(*k8s.GatewayClassSpec)
  1873  		_, known := classInfos[gwc.ControllerName]
  1874  		if !known {
  1875  			continue
  1876  		}
  1877  		res[obj.Name] = gwc.ControllerName
  1878  
  1879  		// Set status. If we created it, it may already be there. If not, set it again
  1880  		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
  1881  			gcs := s.(*k8s.GatewayClassStatus)
  1882  			*gcs = GetClassStatus(gcs, obj.Generation)
  1883  			gcs.SupportedFeatures = supportedFeatures
  1884  			return gcs
  1885  		})
  1886  	}
  1887  
  1888  	return res
  1889  }
  1890  
  1891  // parentKey holds info about a parentRef (eg route binding to a Gateway). This is a mirror of
  1892  // k8s.ParentReference in a form that can be stored in a map
  1893  type parentKey struct {
  1894  	Kind config.GroupVersionKind
  1895  	// Name is the original name of the resource (eg Kubernetes Gateway name)
  1896  	Name string
  1897  	// Namespace is the namespace of the resource
  1898  	Namespace string
  1899  }
  1900  
  1901  type parentReference struct {
  1902  	parentKey
  1903  
  1904  	SectionName k8s.SectionName
  1905  	Port        k8s.PortNumber
  1906  }
  1907  
  1908  var meshGVK = config.GroupVersionKind{
  1909  	Group:   gvk.KubernetesGateway.Group,
  1910  	Version: gvk.KubernetesGateway.Version,
  1911  	Kind:    "Mesh",
  1912  }
  1913  
  1914  var meshParentKey = parentKey{
  1915  	Kind: meshGVK,
  1916  	Name: "istio",
  1917  }
  1918  
  1919  type configContext struct {
  1920  	GatewayResources
  1921  	AllowedReferences AllowedReferences
  1922  	GatewayReferences map[parentKey][]*parentInfo
  1923  
  1924  	// key: referenced resources(e.g. secrets), value: gateway-api resources(e.g. gateways)
  1925  	resourceReferences map[model.ConfigKey][]model.ConfigKey
  1926  }
  1927  
  1928  // parentInfo holds info about a "parent" - something that can be referenced as a ParentRef in the API.
  1929  // Today, this is just Gateway and Mesh.
  1930  type parentInfo struct {
  1931  	// InternalName refers to the internal name we can reference it by. For example, "mesh" or "my-ns/my-gateway"
  1932  	InternalName string
  1933  	// AllowedKinds indicates which kinds can be admitted by this parent
  1934  	AllowedKinds []k8s.RouteGroupKind
  1935  	// Hostnames is the hostnames that must be match to reference to the parent. For gateway this is listener hostname
  1936  	// Format is ns/hostname
  1937  	Hostnames []string
  1938  	// OriginalHostname is the unprocessed form of Hostnames; how it appeared in users' config
  1939  	OriginalHostname string
  1940  
  1941  	// AttachedRoutes keeps track of how many routes are attached to this parent. This is tracked for status.
  1942  	// Because this is mutate in the route generation, parentInfo must be passed as a pointer
  1943  	AttachedRoutes int32
  1944  	// ReportAttachedRoutes is a callback that should be triggered once all AttachedRoutes are computed, to
  1945  	// actually store the attached route count in the status
  1946  	ReportAttachedRoutes func()
  1947  	SectionName          k8s.SectionName
  1948  	Port                 k8s.PortNumber
  1949  	Protocol             k8s.ProtocolType
  1950  }
  1951  
  1952  // routeParentReference holds information about a route's parent reference
  1953  type routeParentReference struct {
  1954  	// InternalName refers to the internal name of the parent we can reference it by. For example, "mesh" or "my-ns/my-gateway"
  1955  	InternalName string
  1956  	// InternalKind is the Group/Kind of the parent
  1957  	InternalKind config.GroupVersionKind
  1958  	// DeniedReason, if present, indicates why the reference was not valid
  1959  	DeniedReason *ParentError
  1960  	// OriginalReference contains the original reference
  1961  	OriginalReference k8s.ParentReference
  1962  	// Hostname is the hostname match of the parent, if any
  1963  	Hostname        string
  1964  	BannedHostnames sets.Set[string]
  1965  }
  1966  
  1967  func (r routeParentReference) IsMesh() bool {
  1968  	return r.InternalName == "mesh"
  1969  }
  1970  
  1971  func (r routeParentReference) hostnameAllowedByIsolation(rawRouteHost string) bool {
  1972  	routeHost := host.Name(rawRouteHost)
  1973  	ourListener := host.Name(r.Hostname)
  1974  	if len(ourListener) > 0 && !ourListener.IsWildCarded() {
  1975  		// Short circuit: this logic only applies to wildcards
  1976  		// Not required for correctness, just an optimization
  1977  		return true
  1978  	}
  1979  	if len(ourListener) > 0 && !routeHost.Matches(ourListener) {
  1980  		return false
  1981  	}
  1982  	for checkListener := range r.BannedHostnames {
  1983  		// We have 3 hostnames here:
  1984  		// * routeHost, the hostname in the route entry
  1985  		// * ourListener, the hostname of the listener the route is bound to
  1986  		// * checkListener, the hostname of the other listener we are comparing to
  1987  		// We want to return false if checkListener would match the routeHost and it would be a more exact match
  1988  		if len(ourListener) > len(checkListener) {
  1989  			// If our hostname is longer, it must be more exact than the check
  1990  			continue
  1991  		}
  1992  		// Ours is shorter. If it matches the checkListener, then it should ONLY match that one
  1993  		// Note protocol, port, etc are already considered when we construct bannedHostnames
  1994  		if routeHost.SubsetOf(host.Name(checkListener)) {
  1995  			return false
  1996  		}
  1997  	}
  1998  	return true
  1999  }
  2000  
  2001  func filteredReferences(parents []routeParentReference) []routeParentReference {
  2002  	ret := make([]routeParentReference, 0, len(parents))
  2003  	for _, p := range parents {
  2004  		if p.DeniedReason != nil {
  2005  			// We should filter this out
  2006  			continue
  2007  		}
  2008  		ret = append(ret, p)
  2009  	}
  2010  	// To ensure deterministic order, sort them
  2011  	sort.Slice(ret, func(i, j int) bool {
  2012  		return ret[i].InternalName < ret[j].InternalName
  2013  	})
  2014  	return ret
  2015  }
  2016  
  2017  func getDefaultName(name string, kgw *k8s.GatewaySpec, disableNameSuffix bool) string {
  2018  	if disableNameSuffix {
  2019  		return name
  2020  	}
  2021  	return fmt.Sprintf("%v-%v", name, kgw.GatewayClassName)
  2022  }
  2023  
  2024  func convertGateways(r configContext) ([]config.Config, map[parentKey][]*parentInfo, sets.String) {
  2025  	// result stores our generated Istio Gateways
  2026  	result := []config.Config{}
  2027  	// gwMap stores an index to access parentInfo (which corresponds to a Kubernetes Gateway)
  2028  	gwMap := map[parentKey][]*parentInfo{}
  2029  	// namespaceLabelReferences keeps track of all namespace label keys referenced by Gateways. This is
  2030  	// used to ensure we handle namespace updates for those keys.
  2031  	namespaceLabelReferences := sets.New[string]()
  2032  	classes := getGatewayClasses(r.GatewayResources, gatewaySupportedFeatures)
  2033  	for _, obj := range r.Gateway {
  2034  		obj := obj
  2035  		kgw := obj.Spec.(*k8s.GatewaySpec)
  2036  		controllerName, f := classes[string(kgw.GatewayClassName)]
  2037  		if !f {
  2038  			// No gateway class found, this may be meant for another controller; should be skipped.
  2039  			continue
  2040  		}
  2041  		classInfo, f := classInfos[controllerName]
  2042  		if !f {
  2043  			continue
  2044  		}
  2045  		if classInfo.disableRouteGeneration {
  2046  			// We found it, but don't want to handle this class
  2047  			continue
  2048  		}
  2049  
  2050  		servers := []*istio.Server{}
  2051  
  2052  		// Extract the addresses. A gateway will bind to a specific Service
  2053  		gatewayServices, err := extractGatewayServices(r.GatewayResources, kgw, obj, classInfo)
  2054  		if len(gatewayServices) == 0 && err != nil {
  2055  			// Short circuit if its a hard failure
  2056  			reportGatewayStatus(r, obj, classInfo, gatewayServices, servers, err)
  2057  			continue
  2058  		}
  2059  		for i, l := range kgw.Listeners {
  2060  			i := i
  2061  			namespaceLabelReferences.InsertAll(getNamespaceLabelReferences(l.AllowedRoutes)...)
  2062  			server, programmed := buildListener(r, obj, l, i, controllerName)
  2063  
  2064  			servers = append(servers, server)
  2065  			if controllerName == constants.ManagedGatewayMeshController {
  2066  				// Waypoint doesn't actually convert the routes to VirtualServices
  2067  				continue
  2068  			}
  2069  			meta := parentMeta(obj, &l.Name)
  2070  			meta[constants.InternalGatewaySemantics] = constants.GatewaySemanticsGateway
  2071  			meta[model.InternalGatewayServiceAnnotation] = strings.Join(gatewayServices, ",")
  2072  
  2073  			// Each listener generates an Istio Gateway with a single Server. This allows binding to a specific listener.
  2074  			gatewayConfig := config.Config{
  2075  				Meta: config.Meta{
  2076  					CreationTimestamp: obj.CreationTimestamp,
  2077  					GroupVersionKind:  gvk.Gateway,
  2078  					Name:              kubeconfig.InternalGatewayName(obj.Name, string(l.Name)),
  2079  					Annotations:       meta,
  2080  					Namespace:         obj.Namespace,
  2081  					Domain:            r.Domain,
  2082  				},
  2083  				Spec: &istio.Gateway{
  2084  					Servers: []*istio.Server{server},
  2085  				},
  2086  			}
  2087  			ref := parentKey{
  2088  				Kind:      gvk.KubernetesGateway,
  2089  				Name:      obj.Name,
  2090  				Namespace: obj.Namespace,
  2091  			}
  2092  			if _, f := gwMap[ref]; !f {
  2093  				gwMap[ref] = []*parentInfo{}
  2094  			}
  2095  
  2096  			allowed, _ := generateSupportedKinds(l)
  2097  			pri := &parentInfo{
  2098  				InternalName:     obj.Namespace + "/" + gatewayConfig.Name,
  2099  				AllowedKinds:     allowed,
  2100  				Hostnames:        server.Hosts,
  2101  				OriginalHostname: string(ptr.OrEmpty(l.Hostname)),
  2102  				SectionName:      l.Name,
  2103  				Port:             l.Port,
  2104  				Protocol:         l.Protocol,
  2105  			}
  2106  			pri.ReportAttachedRoutes = func() {
  2107  				reportListenerAttachedRoutes(i, obj, pri.AttachedRoutes)
  2108  			}
  2109  			gwMap[ref] = append(gwMap[ref], pri)
  2110  
  2111  			if programmed {
  2112  				result = append(result, gatewayConfig)
  2113  			}
  2114  		}
  2115  
  2116  		// If "gateway.istio.io/alias-for" annotation is present, any Route
  2117  		// that binds to the gateway will bind to its alias instead.
  2118  		// The typical usage is when the original gateway is not managed by the gateway controller
  2119  		// but the ( generated ) alias is. This allows people to build their own
  2120  		// gateway controllers on top of Istio Gateway Controller.
  2121  		if obj.Annotations != nil && obj.Annotations[gatewayAliasForAnnotationKey] != "" {
  2122  			ref := parentKey{
  2123  				Kind:      gvk.KubernetesGateway,
  2124  				Name:      obj.Annotations[gatewayAliasForAnnotationKey],
  2125  				Namespace: obj.Namespace,
  2126  			}
  2127  			alias := parentKey{
  2128  				Kind:      gvk.KubernetesGateway,
  2129  				Name:      obj.Name,
  2130  				Namespace: obj.Namespace,
  2131  			}
  2132  			gwMap[ref] = gwMap[alias]
  2133  		}
  2134  
  2135  		reportGatewayStatus(r, obj, classInfo, gatewayServices, servers, err)
  2136  	}
  2137  	// Insert a parent for Mesh references.
  2138  	gwMap[meshParentKey] = []*parentInfo{
  2139  		{
  2140  			InternalName: "mesh",
  2141  			// Mesh has no configurable AllowedKinds, so allow all supported
  2142  			AllowedKinds: []k8s.RouteGroupKind{
  2143  				{Group: (*k8s.Group)(ptr.Of(gvk.HTTPRoute.Group)), Kind: k8s.Kind(gvk.HTTPRoute.Kind)},
  2144  				{Group: (*k8s.Group)(ptr.Of(gvk.GRPCRoute.Group)), Kind: k8s.Kind(gvk.GRPCRoute.Kind)},
  2145  				{Group: (*k8s.Group)(ptr.Of(gvk.TCPRoute.Group)), Kind: k8s.Kind(gvk.TCPRoute.Kind)},
  2146  				{Group: (*k8s.Group)(ptr.Of(gvk.TLSRoute.Group)), Kind: k8s.Kind(gvk.TLSRoute.Kind)},
  2147  			},
  2148  		},
  2149  	}
  2150  	return result, gwMap, namespaceLabelReferences
  2151  }
  2152  
  2153  // Gateway currently requires a listener (https://github.com/kubernetes-sigs/gateway-api/pull/1596).
  2154  // We don't *really* care about the listener, but it may make sense to add a warning if users do not
  2155  // configure it in an expected way so that we have consistency and can make changes in the future as needed.
  2156  // We could completely reject but that seems more likely to cause pain.
  2157  func unexpectedWaypointListener(l k8s.Listener) bool {
  2158  	if l.Port != 15008 {
  2159  		return true
  2160  	}
  2161  	if l.Protocol != k8s.ProtocolType(protocol.HBONE) {
  2162  		return true
  2163  	}
  2164  	return false
  2165  }
  2166  
  2167  func getListenerNames(obj config.Config) sets.Set[k8s.SectionName] {
  2168  	res := sets.New[k8s.SectionName]()
  2169  	for _, l := range obj.Spec.(*k8s.GatewaySpec).Listeners {
  2170  		res.Insert(l.Name)
  2171  	}
  2172  	return res
  2173  }
  2174  
  2175  func reportGatewayStatus(
  2176  	r configContext,
  2177  	obj config.Config,
  2178  	classInfo classInfo,
  2179  	gatewayServices []string,
  2180  	servers []*istio.Server,
  2181  	gatewayErr *ConfigError,
  2182  ) {
  2183  	// TODO: we lose address if servers is empty due to an error
  2184  	internal, internalIP, external, pending, warnings, allUsable := r.Context.ResolveGatewayInstances(obj.Namespace, gatewayServices, servers)
  2185  
  2186  	// Setup initial conditions to the success state. If we encounter errors, we will update this.
  2187  	// We have two status
  2188  	// Accepted: is the configuration valid. We only have errors in listeners, and the status is not supposed to
  2189  	// be tied to listeners, so this is always accepted
  2190  	// Programmed: is the data plane "ready" (note: eventually consistent)
  2191  	gatewayConditions := map[string]*condition{
  2192  		string(k8s.GatewayConditionAccepted): {
  2193  			reason:  string(k8s.GatewayReasonAccepted),
  2194  			message: "Resource accepted",
  2195  		},
  2196  		string(k8s.GatewayConditionProgrammed): {
  2197  			reason:  string(k8s.GatewayReasonProgrammed),
  2198  			message: "Resource programmed",
  2199  		},
  2200  	}
  2201  
  2202  	if gatewayErr != nil {
  2203  		gatewayConditions[string(k8s.GatewayConditionAccepted)].error = gatewayErr
  2204  	}
  2205  
  2206  	if len(internal) > 0 {
  2207  		msg := fmt.Sprintf("Resource programmed, assigned to service(s) %s", humanReadableJoin(internal))
  2208  		gatewayConditions[string(k8s.GatewayReasonProgrammed)].message = msg
  2209  	}
  2210  
  2211  	if len(gatewayServices) == 0 {
  2212  		gatewayConditions[string(k8s.GatewayReasonProgrammed)].error = &ConfigError{
  2213  			Reason:  InvalidAddress,
  2214  			Message: "Failed to assign to any requested addresses",
  2215  		}
  2216  	} else if len(warnings) > 0 {
  2217  		var msg string
  2218  		var reason string
  2219  		if len(internal) != 0 {
  2220  			msg = fmt.Sprintf("Assigned to service(s) %s, but failed to assign to all requested addresses: %s",
  2221  				humanReadableJoin(internal), strings.Join(warnings, "; "))
  2222  		} else {
  2223  			msg = fmt.Sprintf("Failed to assign to any requested addresses: %s", strings.Join(warnings, "; "))
  2224  		}
  2225  		if allUsable {
  2226  			reason = string(k8s.GatewayReasonAddressNotAssigned)
  2227  		} else {
  2228  			reason = string(k8s.GatewayReasonAddressNotUsable)
  2229  		}
  2230  		gatewayConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{
  2231  			// TODO: this only checks Service ready, we should also check Deployment ready?
  2232  			Reason:  reason,
  2233  			Message: msg,
  2234  		}
  2235  	}
  2236  	obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
  2237  		gs := s.(*k8s.GatewayStatus)
  2238  		addressesToReport := external
  2239  		if len(addressesToReport) == 0 {
  2240  			wantAddressType := classInfo.addressType
  2241  			if override, ok := obj.Annotations[addressTypeOverride]; ok {
  2242  				wantAddressType = k8s.AddressType(override)
  2243  			}
  2244  			// There are no external addresses, so report the internal ones
  2245  			// TODO: should we always report both?
  2246  			if wantAddressType == k8s.IPAddressType {
  2247  				addressesToReport = internalIP
  2248  			} else {
  2249  				for _, hostport := range internal {
  2250  					svchost, _, _ := net.SplitHostPort(hostport)
  2251  					if !slices.Contains(pending, svchost) && !slices.Contains(addressesToReport, svchost) {
  2252  						addressesToReport = append(addressesToReport, svchost)
  2253  					}
  2254  				}
  2255  			}
  2256  		}
  2257  		// Do not report an address until we are ready. But once we are ready, never remove the address.
  2258  		if len(addressesToReport) > 0 {
  2259  			gs.Addresses = make([]k8s.GatewayStatusAddress, 0, len(addressesToReport))
  2260  			for _, addr := range addressesToReport {
  2261  				var addrType k8s.AddressType
  2262  				if _, err := netip.ParseAddr(addr); err == nil {
  2263  					addrType = k8s.IPAddressType
  2264  				} else {
  2265  					addrType = k8s.HostnameAddressType
  2266  				}
  2267  				gs.Addresses = append(gs.Addresses, k8s.GatewayStatusAddress{
  2268  					Value: addr,
  2269  					Type:  &addrType,
  2270  				})
  2271  			}
  2272  		}
  2273  		// Prune listeners that have been removed
  2274  		haveListeners := getListenerNames(obj)
  2275  		listeners := make([]k8s.ListenerStatus, 0, len(gs.Listeners))
  2276  		for _, l := range gs.Listeners {
  2277  			if haveListeners.Contains(l.Name) {
  2278  				haveListeners.Delete(l.Name)
  2279  				listeners = append(listeners, l)
  2280  			}
  2281  		}
  2282  		gs.Listeners = listeners
  2283  		gs.Conditions = setConditions(obj.Generation, gs.Conditions, gatewayConditions)
  2284  		return gs
  2285  	})
  2286  }
  2287  
  2288  // IsManaged checks if a Gateway is managed (ie we create the Deployment and Service) or unmanaged.
  2289  // This is based on the address field of the spec. If address is set with a Hostname type, it should point to an existing
  2290  // Service that handles the gateway traffic. If it is not set, or refers to only a single IP, we will consider it managed and provision the Service.
  2291  // If there is an IP, we will set the `loadBalancerIP` type.
  2292  // While there is no defined standard for this in the API yet, it is tracked in https://github.com/kubernetes-sigs/gateway-api/issues/892.
  2293  // So far, this mirrors how out of clusters work (address set means to use existing IP, unset means to provision one),
  2294  // and there has been growing consensus on this model for in cluster deployments.
  2295  //
  2296  // Currently, the supported options are:
  2297  // * 1 Hostname value. This can be short Service name ingress, or FQDN ingress.ns.svc.cluster.local, example.com. If its a non-k8s FQDN it is a ServiceEntry.
  2298  // * 1 IP address. This is managed, with IP explicit
  2299  // * Nothing. This is managed, with IP auto assigned
  2300  //
  2301  // Not supported:
  2302  // Multiple hostname/IP - It is feasible but preference is to create multiple Gateways. This would also break the 1:1 mapping of GW:Service
  2303  // Mixed hostname and IP - doesn't make sense; user should define the IP in service
  2304  // NamedAddress - Service has no concept of named address. For cloud's that have named addresses they can be configured by annotations,
  2305  //
  2306  //	which users can add to the Gateway.
  2307  func IsManaged(gw *k8s.GatewaySpec) bool {
  2308  	if len(gw.Addresses) == 0 {
  2309  		return true
  2310  	}
  2311  	if len(gw.Addresses) > 1 {
  2312  		return false
  2313  	}
  2314  	if t := gw.Addresses[0].Type; t == nil || *t == k8s.IPAddressType {
  2315  		return true
  2316  	}
  2317  	return false
  2318  }
  2319  
  2320  func extractGatewayServices(r GatewayResources, kgw *k8s.GatewaySpec, obj config.Config, info classInfo) ([]string, *ConfigError) {
  2321  	if IsManaged(kgw) {
  2322  		name := model.GetOrDefault(obj.Annotations[gatewayNameOverride], getDefaultName(obj.Name, kgw, info.disableNameSuffix))
  2323  		return []string{fmt.Sprintf("%s.%s.svc.%v", name, obj.Namespace, r.Domain)}, nil
  2324  	}
  2325  	gatewayServices := []string{}
  2326  	skippedAddresses := []string{}
  2327  	for _, addr := range kgw.Addresses {
  2328  		if addr.Type != nil && *addr.Type != k8s.HostnameAddressType {
  2329  			// We only support HostnameAddressType. Keep track of invalid ones so we can report in status.
  2330  			skippedAddresses = append(skippedAddresses, addr.Value)
  2331  			continue
  2332  		}
  2333  		// TODO: For now we are using Addresses. There has been some discussion of allowing inline
  2334  		// parameters on the class field like a URL, in which case we will probably just use that. See
  2335  		// https://github.com/kubernetes-sigs/gateway-api/pull/614
  2336  		fqdn := addr.Value
  2337  		if !strings.Contains(fqdn, ".") {
  2338  			// Short name, expand it
  2339  			fqdn = fmt.Sprintf("%s.%s.svc.%s", fqdn, obj.Namespace, r.Domain)
  2340  		}
  2341  		gatewayServices = append(gatewayServices, fqdn)
  2342  	}
  2343  	if len(skippedAddresses) > 0 {
  2344  		// Give error but return services, this is a soft failure
  2345  		return gatewayServices, &ConfigError{
  2346  			Reason:  InvalidAddress,
  2347  			Message: fmt.Sprintf("only Hostname is supported, ignoring %v", skippedAddresses),
  2348  		}
  2349  	}
  2350  	if _, f := obj.Annotations[serviceTypeOverride]; f {
  2351  		// Give error but return services, this is a soft failure
  2352  		// Remove entirely in 1.20
  2353  		return gatewayServices, &ConfigError{
  2354  			Reason:  DeprecateFieldUsage,
  2355  			Message: fmt.Sprintf("annotation %v is deprecated, use Spec.Infrastructure.Routeability", serviceTypeOverride),
  2356  		}
  2357  	}
  2358  	return gatewayServices, nil
  2359  }
  2360  
  2361  // getNamespaceLabelReferences fetches all label keys used in namespace selectors. Return order may not be stable.
  2362  func getNamespaceLabelReferences(routes *k8s.AllowedRoutes) []string {
  2363  	if routes == nil || routes.Namespaces == nil || routes.Namespaces.Selector == nil {
  2364  		return nil
  2365  	}
  2366  	res := []string{}
  2367  	for k := range routes.Namespaces.Selector.MatchLabels {
  2368  		res = append(res, k)
  2369  	}
  2370  	for _, me := range routes.Namespaces.Selector.MatchExpressions {
  2371  		if me.Operator == metav1.LabelSelectorOpNotIn || me.Operator == metav1.LabelSelectorOpDoesNotExist {
  2372  			// Over-matching is fine because this only controls the set of namespace
  2373  			// label change events to watch and the actual binding enforcement happens
  2374  			// by checking the intersection of the generated VirtualService.spec.hosts
  2375  			// and Istio Gateway.spec.servers.hosts arrays - we just can't miss
  2376  			// potentially relevant namespace label events here.
  2377  			res = append(res, "*")
  2378  		}
  2379  
  2380  		res = append(res, me.Key)
  2381  	}
  2382  	return res
  2383  }
  2384  
  2385  func buildListener(r configContext, obj config.Config, l k8s.Listener, listenerIndex int, controllerName k8s.GatewayController) (*istio.Server, bool) {
  2386  	listenerConditions := map[string]*condition{
  2387  		string(k8s.ListenerConditionAccepted): {
  2388  			reason:  string(k8s.ListenerReasonAccepted),
  2389  			message: "No errors found",
  2390  		},
  2391  		string(k8s.ListenerConditionProgrammed): {
  2392  			reason:  string(k8s.ListenerReasonProgrammed),
  2393  			message: "No errors found",
  2394  		},
  2395  		string(k8s.ListenerConditionConflicted): {
  2396  			reason:  string(k8s.ListenerReasonNoConflicts),
  2397  			message: "No errors found",
  2398  			status:  kstatus.StatusFalse,
  2399  		},
  2400  		string(k8s.ListenerConditionResolvedRefs): {
  2401  			reason:  string(k8s.ListenerReasonResolvedRefs),
  2402  			message: "No errors found",
  2403  		},
  2404  	}
  2405  
  2406  	ok := true
  2407  	tls, err := buildTLS(r, l.TLS, obj, kube.IsAutoPassthrough(obj.Labels, l))
  2408  	if err != nil {
  2409  		listenerConditions[string(k8s.ListenerConditionResolvedRefs)].error = err
  2410  		listenerConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{
  2411  			Reason:  string(k8s.GatewayReasonInvalid),
  2412  			Message: "Bad TLS configuration",
  2413  		}
  2414  		ok = false
  2415  	}
  2416  	hostnames := buildHostnameMatch(obj.Namespace, r.GatewayResources, l)
  2417  	server := &istio.Server{
  2418  		Port: &istio.Port{
  2419  			// Name is required. We only have one server per Gateway, so we can just name them all the same
  2420  			Name:     "default",
  2421  			Number:   uint32(l.Port),
  2422  			Protocol: listenerProtocolToIstio(l.Protocol),
  2423  		},
  2424  		Hosts: hostnames,
  2425  		Tls:   tls,
  2426  	}
  2427  	if controllerName == constants.ManagedGatewayMeshController {
  2428  		if unexpectedWaypointListener(l) {
  2429  			listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{
  2430  				Reason:  string(k8s.ListenerReasonUnsupportedProtocol),
  2431  				Message: `Expected a single listener on port 15008 with protocol "HBONE"`,
  2432  			}
  2433  		}
  2434  	}
  2435  
  2436  	reportListenerCondition(listenerIndex, l, obj, listenerConditions)
  2437  	return server, ok
  2438  }
  2439  
  2440  func listenerProtocolToIstio(protocol k8s.ProtocolType) string {
  2441  	// Currently, all gateway-api protocols are valid Istio protocols.
  2442  	return string(protocol)
  2443  }
  2444  
  2445  func buildTLS(ctx configContext, tls *k8s.GatewayTLSConfig, gw config.Config, isAutoPassthrough bool) (*istio.ServerTLSSettings, *ConfigError) {
  2446  	if tls == nil {
  2447  		return nil, nil
  2448  	}
  2449  	// Explicitly not supported: file mounted
  2450  	// Not yet implemented: TLS mode, https redirect, max protocol version, SANs, CipherSuites, VerifyCertificate
  2451  	out := &istio.ServerTLSSettings{
  2452  		HttpsRedirect: false,
  2453  	}
  2454  	mode := k8s.TLSModeTerminate
  2455  	if tls.Mode != nil {
  2456  		mode = *tls.Mode
  2457  	}
  2458  	namespace := gw.Namespace
  2459  	switch mode {
  2460  	case k8s.TLSModeTerminate:
  2461  		out.Mode = istio.ServerTLSSettings_SIMPLE
  2462  		if tls.Options != nil {
  2463  			switch tls.Options[gatewayTLSTerminateModeKey] {
  2464  			case "MUTUAL":
  2465  				out.Mode = istio.ServerTLSSettings_MUTUAL
  2466  			case "ISTIO_MUTUAL":
  2467  				out.Mode = istio.ServerTLSSettings_ISTIO_MUTUAL
  2468  				return out, nil
  2469  			}
  2470  		}
  2471  		if len(tls.CertificateRefs) != 1 {
  2472  			// This is required in the API, should be rejected in validation
  2473  			return out, &ConfigError{Reason: InvalidTLS, Message: "exactly 1 certificateRefs should be present for TLS termination"}
  2474  		}
  2475  		cred, err := buildSecretReference(ctx, tls.CertificateRefs[0], gw)
  2476  		if err != nil {
  2477  			return out, err
  2478  		}
  2479  		credNs := ptr.OrDefault((*string)(tls.CertificateRefs[0].Namespace), namespace)
  2480  		sameNamespace := credNs == namespace
  2481  		if !sameNamespace && !ctx.AllowedReferences.SecretAllowed(creds.ToResourceName(cred), namespace) {
  2482  			return out, &ConfigError{
  2483  				Reason: InvalidListenerRefNotPermitted,
  2484  				Message: fmt.Sprintf(
  2485  					"certificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)",
  2486  					tls.CertificateRefs[0].Name, credNs, namespace,
  2487  				),
  2488  			}
  2489  		}
  2490  		out.CredentialName = cred
  2491  	case k8s.TLSModePassthrough:
  2492  		out.Mode = istio.ServerTLSSettings_PASSTHROUGH
  2493  		if isAutoPassthrough {
  2494  			out.Mode = istio.ServerTLSSettings_AUTO_PASSTHROUGH
  2495  		}
  2496  	}
  2497  	return out, nil
  2498  }
  2499  
  2500  func buildSecretReference(ctx configContext, ref k8s.SecretObjectReference, gw config.Config) (string, *ConfigError) {
  2501  	if !nilOrEqual((*string)(ref.Group), gvk.Secret.Group) || !nilOrEqual((*string)(ref.Kind), gvk.Secret.Kind) {
  2502  		return "", &ConfigError{Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, only secret is allowed", objectReferenceString(ref))}
  2503  	}
  2504  
  2505  	secret := model.ConfigKey{
  2506  		Kind:      kind.Secret,
  2507  		Name:      string(ref.Name),
  2508  		Namespace: ptr.OrDefault((*string)(ref.Namespace), gw.Namespace),
  2509  	}
  2510  
  2511  	ctx.resourceReferences[secret] = append(ctx.resourceReferences[secret], model.ConfigKey{
  2512  		Kind:      kind.KubernetesGateway,
  2513  		Namespace: gw.Namespace,
  2514  		Name:      gw.Name,
  2515  	})
  2516  
  2517  	if ctx.Credentials != nil {
  2518  		if certInfo, err := ctx.Credentials.GetCertInfo(secret.Name, secret.Namespace); err != nil {
  2519  			return "", &ConfigError{
  2520  				Reason:  InvalidTLS,
  2521  				Message: fmt.Sprintf("invalid certificate reference %v, %v", objectReferenceString(ref), err),
  2522  			}
  2523  		} else if _, err = tls.X509KeyPair(certInfo.Cert, certInfo.Key); err != nil {
  2524  			return "", &ConfigError{
  2525  				Reason:  InvalidTLS,
  2526  				Message: fmt.Sprintf("invalid certificate reference %v, the certificate is malformed: %v", objectReferenceString(ref), err),
  2527  			}
  2528  		}
  2529  	}
  2530  
  2531  	return creds.ToKubernetesGatewayResource(secret.Namespace, secret.Name), nil
  2532  }
  2533  
  2534  func objectReferenceString(ref k8s.SecretObjectReference) string {
  2535  	return fmt.Sprintf("%s/%s/%s.%s",
  2536  		ptr.OrEmpty(ref.Group),
  2537  		ptr.OrEmpty(ref.Kind),
  2538  		ref.Name,
  2539  		ptr.OrEmpty(ref.Namespace))
  2540  }
  2541  
  2542  func parentRefString(ref k8s.ParentReference) string {
  2543  	return fmt.Sprintf("%s/%s/%s/%s/%d.%s",
  2544  		ptr.OrEmpty(ref.Group),
  2545  		ptr.OrEmpty(ref.Kind),
  2546  		ref.Name,
  2547  		ptr.OrEmpty(ref.SectionName),
  2548  		ptr.OrEmpty(ref.Port),
  2549  		ptr.OrEmpty(ref.Namespace))
  2550  }
  2551  
  2552  // buildHostnameMatch generates a Gateway.spec.servers.hosts section from a listener
  2553  func buildHostnameMatch(localNamespace string, r GatewayResources, l k8s.Listener) []string {
  2554  	// We may allow all hostnames or a specific one
  2555  	hostname := "*"
  2556  	if l.Hostname != nil {
  2557  		hostname = string(*l.Hostname)
  2558  	}
  2559  
  2560  	resp := []string{}
  2561  	for _, ns := range namespacesFromSelector(localNamespace, r, l.AllowedRoutes) {
  2562  		// This check is necessary to prevent adding a hostname with an invalid empty namespace
  2563  		if len(ns) > 0 {
  2564  			resp = append(resp, fmt.Sprintf("%s/%s", ns, hostname))
  2565  		}
  2566  	}
  2567  
  2568  	// If nothing matched use ~ namespace (match nothing). We need this since its illegal to have an
  2569  	// empty hostname list, but we still need the Gateway provisioned to ensure status is properly set and
  2570  	// SNI matches are established; we just don't want to actually match any routing rules (yet).
  2571  	if len(resp) == 0 {
  2572  		return []string{"~/" + hostname}
  2573  	}
  2574  	return resp
  2575  }
  2576  
  2577  // namespacesFromSelector determines a list of allowed namespaces for a given AllowedRoutes
  2578  func namespacesFromSelector(localNamespace string, r GatewayResources, lr *k8s.AllowedRoutes) []string {
  2579  	// Default is to allow only the same namespace
  2580  	if lr == nil || lr.Namespaces == nil || lr.Namespaces.From == nil || *lr.Namespaces.From == k8s.NamespacesFromSame {
  2581  		return []string{localNamespace}
  2582  	}
  2583  	if *lr.Namespaces.From == k8s.NamespacesFromAll {
  2584  		return []string{"*"}
  2585  	}
  2586  
  2587  	if lr.Namespaces.Selector == nil {
  2588  		// Should never happen, invalid config
  2589  		return []string{"*"}
  2590  	}
  2591  
  2592  	// gateway-api has selectors, but Istio Gateway just has a list of names. We will run the selector
  2593  	// against all namespaces and get a list of matching namespaces that can be converted into a list
  2594  	// Istio can handle.
  2595  	ls, err := metav1.LabelSelectorAsSelector(lr.Namespaces.Selector)
  2596  	if err != nil {
  2597  		return nil
  2598  	}
  2599  	namespaces := []string{}
  2600  	for _, ns := range r.Namespaces {
  2601  		if ls.Matches(toNamespaceSet(ns.Name, ns.Labels)) {
  2602  			namespaces = append(namespaces, ns.Name)
  2603  		}
  2604  	}
  2605  	// Ensure stable order
  2606  	sort.Strings(namespaces)
  2607  	return namespaces
  2608  }
  2609  
  2610  func nilOrEqual(have *string, expected string) bool {
  2611  	return have == nil || *have == expected
  2612  }
  2613  
  2614  func humanReadableJoin(ss []string) string {
  2615  	switch len(ss) {
  2616  	case 0:
  2617  		return ""
  2618  	case 1:
  2619  		return ss[0]
  2620  	case 2:
  2621  		return ss[0] + " and " + ss[1]
  2622  	default:
  2623  		return strings.Join(ss[:len(ss)-1], ", ") + ", and " + ss[len(ss)-1]
  2624  	}
  2625  }
  2626  
  2627  // NamespaceNameLabel represents that label added automatically to namespaces is newer Kubernetes clusters
  2628  const NamespaceNameLabel = "kubernetes.io/metadata.name"
  2629  
  2630  // toNamespaceSet converts a set of namespace labels to a Set that can be used to select against.
  2631  func toNamespaceSet(name string, labels map[string]string) klabels.Set {
  2632  	// If namespace label is not set, implicitly insert it to support older Kubernetes versions
  2633  	if labels[NamespaceNameLabel] == name {
  2634  		// Already set, avoid copies
  2635  		return labels
  2636  	}
  2637  	// First we need a copy to not modify the underlying object
  2638  	ret := make(map[string]string, len(labels)+1)
  2639  	for k, v := range labels {
  2640  		ret[k] = v
  2641  	}
  2642  	ret[NamespaceNameLabel] = name
  2643  	return ret
  2644  }
  2645  
  2646  func (kr GatewayResources) FuzzValidate() bool {
  2647  	for _, gwc := range kr.GatewayClass {
  2648  		if gwc.Spec == nil {
  2649  			return false
  2650  		}
  2651  	}
  2652  	for _, rp := range kr.ReferenceGrant {
  2653  		if rp.Spec == nil {
  2654  			return false
  2655  		}
  2656  	}
  2657  	for _, hr := range kr.HTTPRoute {
  2658  		if hr.Spec == nil {
  2659  			return false
  2660  		}
  2661  	}
  2662  	for _, hr := range kr.GRPCRoute {
  2663  		if hr.Spec == nil {
  2664  			return false
  2665  		}
  2666  	}
  2667  	for _, tr := range kr.TLSRoute {
  2668  		if tr.Spec == nil {
  2669  			return false
  2670  		}
  2671  	}
  2672  	for _, g := range kr.Gateway {
  2673  		if g.Spec == nil {
  2674  			return false
  2675  		}
  2676  	}
  2677  	for _, tr := range kr.TCPRoute {
  2678  		if tr.Spec == nil {
  2679  			return false
  2680  		}
  2681  	}
  2682  	return true
  2683  }