istio

Форк
0
/
conversion.go 
2626 строк · 82.6 Кб
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
	k8sv1 "sigs.k8s.io/gateway-api/apis/v1"
30
	k8s "sigs.k8s.io/gateway-api/apis/v1alpha2"
31

32
	istio "istio.io/api/networking/v1alpha3"
33
	"istio.io/istio/pilot/pkg/features"
34
	"istio.io/istio/pilot/pkg/model"
35
	creds "istio.io/istio/pilot/pkg/model/credentials"
36
	"istio.io/istio/pilot/pkg/model/kstatus"
37
	"istio.io/istio/pilot/pkg/serviceregistry/kube"
38
	"istio.io/istio/pkg/config"
39
	"istio.io/istio/pkg/config/constants"
40
	kubeconfig "istio.io/istio/pkg/config/gateway/kube"
41
	"istio.io/istio/pkg/config/host"
42
	"istio.io/istio/pkg/config/protocol"
43
	"istio.io/istio/pkg/config/schema/gvk"
44
	"istio.io/istio/pkg/config/schema/kind"
45
	"istio.io/istio/pkg/ptr"
46
	"istio.io/istio/pkg/slices"
47
	"istio.io/istio/pkg/util/sets"
48
)
49

50
func sortConfigByCreationTime(configs []config.Config) {
51
	sort.Slice(configs, func(i, j int) bool {
52
		if configs[i].CreationTimestamp.Equal(configs[j].CreationTimestamp) {
53
			in := configs[i].Namespace + "/" + configs[i].Name
54
			jn := configs[j].Namespace + "/" + configs[j].Name
55
			return in < jn
56
		}
57
		return configs[i].CreationTimestamp.Before(configs[j].CreationTimestamp)
58
	})
59
}
60

61
// convertResources is the top level entrypoint to our conversion logic, computing the full state based
62
// on KubernetesResources inputs.
63
func convertResources(r GatewayResources) IstioResources {
64
	// sort HTTPRoutes by creation timestamp and namespace/name
65
	sortConfigByCreationTime(r.HTTPRoute)
66
	sortConfigByCreationTime(r.GRPCRoute)
67

68
	result := IstioResources{}
69
	ctx := configContext{
70
		GatewayResources:   r,
71
		AllowedReferences:  convertReferencePolicies(r),
72
		resourceReferences: make(map[model.ConfigKey][]model.ConfigKey),
73
	}
74

75
	gw, gwMap, nsReferences := convertGateways(ctx)
76
	ctx.GatewayReferences = gwMap
77
	result.Gateway = gw
78

79
	result.VirtualService = convertVirtualService(ctx)
80

81
	// Once we have gone through all route computation, we will know how many routes bound to each gateway.
82
	// Report this in the status.
83
	for _, dm := range gwMap {
84
		for _, pri := range dm {
85
			if pri.ReportAttachedRoutes != nil {
86
				pri.ReportAttachedRoutes()
87
			}
88
		}
89
	}
90
	result.AllowedReferences = ctx.AllowedReferences
91
	result.ReferencedNamespaceKeys = nsReferences
92
	result.ResourceReferences = ctx.resourceReferences
93
	return result
94
}
95

96
// convertReferencePolicies extracts all ReferencePolicy into an easily accessibly index.
97
func convertReferencePolicies(r GatewayResources) AllowedReferences {
98
	res := map[Reference]map[Reference]*Grants{}
99
	type namespacedGrant struct {
100
		Namespace string
101
		Grant     *k8s.ReferenceGrantSpec
102
	}
103
	specs := make([]namespacedGrant, 0, len(r.ReferenceGrant))
104

105
	for _, obj := range r.ReferenceGrant {
106
		rp := obj.Spec.(*k8s.ReferenceGrantSpec)
107
		specs = append(specs, namespacedGrant{Namespace: obj.Namespace, Grant: rp})
108
	}
109
	for _, ng := range specs {
110
		rp := ng.Grant
111
		for _, from := range rp.From {
112
			fromKey := Reference{
113
				Namespace: from.Namespace,
114
			}
115
			if string(from.Group) == gvk.KubernetesGateway.Group && string(from.Kind) == gvk.KubernetesGateway.Kind {
116
				fromKey.Kind = gvk.KubernetesGateway
117
			} else if string(from.Group) == gvk.HTTPRoute.Group && string(from.Kind) == gvk.HTTPRoute.Kind {
118
				fromKey.Kind = gvk.HTTPRoute
119
			} else if string(from.Group) == gvk.TLSRoute.Group && string(from.Kind) == gvk.TLSRoute.Kind {
120
				fromKey.Kind = gvk.TLSRoute
121
			} else if string(from.Group) == gvk.TCPRoute.Group && string(from.Kind) == gvk.TCPRoute.Kind {
122
				fromKey.Kind = gvk.TCPRoute
123
			} else {
124
				// Not supported type. Not an error; may be for another controller
125
				continue
126
			}
127
			for _, to := range rp.To {
128
				toKey := Reference{
129
					Namespace: k8s.Namespace(ng.Namespace),
130
				}
131
				if to.Group == "" && string(to.Kind) == gvk.Secret.Kind {
132
					toKey.Kind = gvk.Secret
133
				} else if to.Group == "" && string(to.Kind) == gvk.Service.Kind {
134
					toKey.Kind = gvk.Service
135
				} else {
136
					// Not supported type. Not an error; may be for another controller
137
					continue
138
				}
139
				if _, f := res[fromKey]; !f {
140
					res[fromKey] = map[Reference]*Grants{}
141
				}
142
				if _, f := res[fromKey][toKey]; !f {
143
					res[fromKey][toKey] = &Grants{
144
						AllowedNames: sets.New[string](),
145
					}
146
				}
147
				if to.Name != nil {
148
					res[fromKey][toKey].AllowedNames.Insert(string(*to.Name))
149
				} else {
150
					res[fromKey][toKey].AllowAll = true
151
				}
152
			}
153
		}
154
	}
155
	return res
156
}
157

158
// convertVirtualService takes all xRoute types and generates corresponding VirtualServices.
159
func convertVirtualService(r configContext) []config.Config {
160
	result := []config.Config{}
161
	for _, obj := range r.TCPRoute {
162
		result = append(result, buildTCPVirtualService(r, obj)...)
163
	}
164

165
	for _, obj := range r.TLSRoute {
166
		result = append(result, buildTLSVirtualService(r, obj)...)
167
	}
168

169
	// for gateway routes, build one VS per gateway+host
170
	gatewayRoutes := make(map[string]map[string]*config.Config)
171
	// for mesh routes, build one VS per namespace+host
172
	meshRoutes := make(map[string]map[string]*config.Config)
173
	for _, obj := range r.HTTPRoute {
174
		buildHTTPVirtualServices(r, obj, gatewayRoutes, meshRoutes)
175
	}
176
	for _, obj := range r.GRPCRoute {
177
		buildGRPCVirtualServices(r, obj, gatewayRoutes, meshRoutes)
178
	}
179
	for _, vsByHost := range gatewayRoutes {
180
		for _, vsConfig := range vsByHost {
181
			result = append(result, *vsConfig)
182
		}
183
	}
184
	for _, vsByHost := range meshRoutes {
185
		for _, vsConfig := range vsByHost {
186
			result = append(result, *vsConfig)
187
		}
188
	}
189
	return result
190
}
191

192
func convertHTTPRoute(r k8s.HTTPRouteRule, ctx configContext,
193
	obj config.Config, pos int, enforceRefGrant bool,
194
) (*istio.HTTPRoute, *ConfigError) {
195
	// TODO: implement rewrite, timeout, corspolicy, retries
196
	vs := &istio.HTTPRoute{}
197
	// Auto-name the route. If upstream defines an explicit name, will use it instead
198
	// The position within the route is unique
199
	vs.Name = fmt.Sprintf("%s.%s.%d", obj.Namespace, obj.Name, pos)
200

201
	for _, match := range r.Matches {
202
		uri, err := createURIMatch(match)
203
		if err != nil {
204
			return nil, err
205
		}
206
		headers, err := createHeadersMatch(match)
207
		if err != nil {
208
			return nil, err
209
		}
210
		qp, err := createQueryParamsMatch(match)
211
		if err != nil {
212
			return nil, err
213
		}
214
		method, err := createMethodMatch(match)
215
		if err != nil {
216
			return nil, err
217
		}
218
		vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
219
			Uri:         uri,
220
			Headers:     headers,
221
			QueryParams: qp,
222
			Method:      method,
223
		})
224
	}
225
	for _, filter := range r.Filters {
226
		switch filter.Type {
227
		case k8sv1.HTTPRouteFilterRequestHeaderModifier:
228
			h := createHeadersFilter(filter.RequestHeaderModifier)
229
			if h == nil {
230
				continue
231
			}
232
			if vs.Headers == nil {
233
				vs.Headers = &istio.Headers{}
234
			}
235
			vs.Headers.Request = h
236
		case k8sv1.HTTPRouteFilterResponseHeaderModifier:
237
			h := createHeadersFilter(filter.ResponseHeaderModifier)
238
			if h == nil {
239
				continue
240
			}
241
			if vs.Headers == nil {
242
				vs.Headers = &istio.Headers{}
243
			}
244
			vs.Headers.Response = h
245
		case k8sv1.HTTPRouteFilterRequestRedirect:
246
			vs.Redirect = createRedirectFilter(filter.RequestRedirect)
247
		case k8sv1.HTTPRouteFilterRequestMirror:
248
			mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.HTTPRoute)
249
			if err != nil {
250
				return nil, err
251
			}
252
			vs.Mirrors = append(vs.Mirrors, mirror)
253
		case k8sv1.HTTPRouteFilterURLRewrite:
254
			vs.Rewrite = createRewriteFilter(filter.URLRewrite)
255
		default:
256
			return nil, &ConfigError{
257
				Reason:  InvalidFilter,
258
				Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
259
			}
260
		}
261
	}
262

263
	if r.Timeouts != nil {
264
		if r.Timeouts.Request != nil {
265
			request, _ := time.ParseDuration(string(*r.Timeouts.Request))
266
			if request != 0 {
267
				vs.Timeout = durationpb.New(request)
268
			}
269
		}
270
		if r.Timeouts.BackendRequest != nil {
271
			backendRequest, _ := time.ParseDuration(string(*r.Timeouts.BackendRequest))
272
			if backendRequest != 0 {
273
				timeout := durationpb.New(backendRequest)
274
				if vs.Retries != nil {
275
					vs.Retries.PerTryTimeout = timeout
276
				} else {
277
					vs.Timeout = timeout
278
				}
279
			}
280
		}
281
	}
282

283
	if weightSum(r.BackendRefs) == 0 && vs.Redirect == nil {
284
		// The spec requires us to return 500 when there are no >0 weight backends
285
		vs.DirectResponse = &istio.HTTPDirectResponse{
286
			Status: 500,
287
		}
288
	} else {
289
		route, backendErr, err := buildHTTPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant)
290
		if err != nil {
291
			return nil, err
292
		}
293
		vs.Route = route
294
		return vs, backendErr
295
	}
296

297
	return vs, nil
298
}
299

300
func convertGRPCRoute(r k8s.GRPCRouteRule, ctx configContext,
301
	obj config.Config, pos int, enforceRefGrant bool,
302
) (*istio.HTTPRoute, *ConfigError) {
303
	// TODO: implement rewrite, timeout, mirror, corspolicy, retries
304
	vs := &istio.HTTPRoute{}
305
	// Auto-name the route. If upstream defines an explicit name, will use it instead
306
	// The position within the route is unique
307
	vs.Name = fmt.Sprintf("%s.%s.%d", obj.Namespace, obj.Name, pos)
308

309
	for _, match := range r.Matches {
310
		uri, err := createGRPCURIMatch(match)
311
		if err != nil {
312
			return nil, err
313
		}
314
		headers, err := createGRPCHeadersMatch(match)
315
		if err != nil {
316
			return nil, err
317
		}
318
		vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
319
			Uri:     uri,
320
			Headers: headers,
321
		})
322
	}
323
	for _, filter := range r.Filters {
324
		switch filter.Type {
325
		case k8s.GRPCRouteFilterRequestHeaderModifier:
326
			h := createHeadersFilter(filter.RequestHeaderModifier)
327
			if h == nil {
328
				continue
329
			}
330
			if vs.Headers == nil {
331
				vs.Headers = &istio.Headers{}
332
			}
333
			vs.Headers.Request = h
334
		case k8s.GRPCRouteFilterResponseHeaderModifier:
335
			h := createHeadersFilter(filter.ResponseHeaderModifier)
336
			if h == nil {
337
				continue
338
			}
339
			if vs.Headers == nil {
340
				vs.Headers = &istio.Headers{}
341
			}
342
			vs.Headers.Response = h
343
		case k8s.GRPCRouteFilterRequestMirror:
344
			mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.GRPCRoute)
345
			if err != nil {
346
				return nil, err
347
			}
348
			vs.Mirrors = append(vs.Mirrors, mirror)
349
		default:
350
			return nil, &ConfigError{
351
				Reason:  InvalidFilter,
352
				Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
353
			}
354
		}
355
	}
356

357
	if grpcWeightSum(r.BackendRefs) == 0 && vs.Redirect == nil {
358
		// The spec requires us to return 500 when there are no >0 weight backends
359
		vs.DirectResponse = &istio.HTTPDirectResponse{
360
			Status: 500,
361
		}
362
	} else {
363
		route, backendErr, err := buildGRPCDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant)
364
		if err != nil {
365
			return nil, err
366
		}
367
		vs.Route = route
368
		return vs, backendErr
369
	}
370

371
	return vs, nil
372
}
373

374
func parentTypes(rpi []routeParentReference) (mesh, gateway bool) {
375
	for _, r := range rpi {
376
		if r.IsMesh() {
377
			mesh = true
378
		} else {
379
			gateway = true
380
		}
381
	}
382
	return
383
}
384

385
func buildHTTPVirtualServices(
386
	ctx configContext,
387
	obj config.Config,
388
	gatewayRoutes map[string]map[string]*config.Config,
389
	meshRoutes map[string]map[string]*config.Config,
390
) {
391
	route := obj.Spec.(*k8s.HTTPRouteSpec)
392
	parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, route.Hostnames, gvk.HTTPRoute, obj.Namespace)
393
	reportStatus := func(results []RouteParentResult) {
394
		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
395
			rs := s.(*k8s.HTTPRouteStatus)
396
			rs.Parents = createRouteStatus(results, obj, rs.Parents)
397
			return rs
398
		})
399
	}
400

401
	type conversionResult struct {
402
		error  *ConfigError
403
		routes []*istio.HTTPRoute
404
	}
405
	convertRules := func(mesh bool) conversionResult {
406
		res := conversionResult{}
407
		for n, r := range route.Rules {
408
			// split the rule to make sure each rule has up to one match
409
			matches := slices.Reference(r.Matches)
410
			if len(matches) == 0 {
411
				matches = append(matches, nil)
412
			}
413
			for _, m := range matches {
414
				if m != nil {
415
					r.Matches = []k8s.HTTPRouteMatch{*m}
416
				}
417
				vs, err := convertHTTPRoute(r, ctx, obj, n, !mesh)
418
				// This was a hard error
419
				if vs == nil {
420
					res.error = err
421
					return conversionResult{error: err}
422
				}
423
				// Got an error but also routes
424
				if err != nil {
425
					res.error = err
426
				}
427

428
				res.routes = append(res.routes, vs)
429
			}
430
		}
431
		return res
432
	}
433
	meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
434

435
	reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
436
		res := RouteParentResult{
437
			OriginalReference: r.OriginalReference,
438
			DeniedReason:      r.DeniedReason,
439
			RouteError:        gwResult.error,
440
		}
441
		if r.IsMesh() {
442
			res.RouteError = meshResult.error
443
		}
444
		return res
445
	}))
446
	count := 0
447
	for _, parent := range filteredReferences(parentRefs) {
448
		// for gateway routes, build one VS per gateway+host
449
		routeMap := gatewayRoutes
450
		routeKey := parent.InternalName
451
		vsHosts := hostnameToStringList(route.Hostnames)
452
		routes := gwResult.routes
453
		if parent.IsMesh() {
454
			routes = meshResult.routes
455
			// for mesh routes, build one VS per namespace/port->host
456
			routeMap = meshRoutes
457
			routeKey = obj.Namespace
458
			if parent.OriginalReference.Port != nil {
459
				routes = augmentPortMatch(routes, *parent.OriginalReference.Port)
460
				routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port)
461
			}
462
			if parent.InternalKind == gvk.ServiceEntry {
463
				vsHosts = serviceEntryHosts(ctx.ServiceEntry,
464
					string(parent.OriginalReference.Name),
465
					string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace))))
466
			} else {
467
				vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
468
					parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
469
			}
470
		}
471
		if len(routes) == 0 {
472
			continue
473
		}
474
		if _, f := routeMap[routeKey]; !f {
475
			routeMap[routeKey] = make(map[string]*config.Config)
476
		}
477

478
		// Create one VS per hostname with a single hostname.
479
		// This ensures we can treat each hostname independently, as the spec requires
480
		for _, h := range vsHosts {
481
			if cfg := routeMap[routeKey][h]; cfg != nil {
482
				// merge http routes
483
				vs := cfg.Spec.(*istio.VirtualService)
484
				vs.Http = append(vs.Http, routes...)
485
				// append parents
486
				cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s",
487
					cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
488
			} else {
489
				name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
490
				routeMap[routeKey][h] = &config.Config{
491
					Meta: config.Meta{
492
						CreationTimestamp: obj.CreationTimestamp,
493
						GroupVersionKind:  gvk.VirtualService,
494
						Name:              name,
495
						Annotations:       routeMeta(obj),
496
						Namespace:         obj.Namespace,
497
						Domain:            ctx.Domain,
498
					},
499
					Spec: &istio.VirtualService{
500
						Hosts:    []string{h},
501
						Gateways: []string{parent.InternalName},
502
						Http:     routes,
503
					},
504
				}
505
				count++
506
			}
507
		}
508
	}
509
	for _, vsByHost := range gatewayRoutes {
510
		for _, cfg := range vsByHost {
511
			vs := cfg.Spec.(*istio.VirtualService)
512
			sortHTTPRoutes(vs.Http)
513
		}
514
	}
515
	for _, vsByHost := range meshRoutes {
516
		for _, cfg := range vsByHost {
517
			vs := cfg.Spec.(*istio.VirtualService)
518
			sortHTTPRoutes(vs.Http)
519
		}
520
	}
521
}
522

523
func serviceEntryHosts(ses []config.Config, name, namespace string) []string {
524
	for _, obj := range ses {
525
		if obj.Meta.Name == name {
526
			ns := obj.Meta.Namespace
527
			if ns == "" {
528
				ns = metav1.NamespaceDefault
529
			}
530
			if ns == namespace {
531
				se := obj.Spec.(*istio.ServiceEntry)
532
				return se.Hosts
533
			}
534
		}
535
	}
536
	return []string{}
537
}
538

539
func buildMeshAndGatewayRoutes[T any](parentRefs []routeParentReference, convertRules func(mesh bool) T) (T, T) {
540
	var meshResult, gwResult T
541
	needMesh, needGw := parentTypes(parentRefs)
542
	if needMesh {
543
		meshResult = convertRules(true)
544
	}
545
	if needGw {
546
		gwResult = convertRules(false)
547
	}
548
	return meshResult, gwResult
549
}
550

551
func augmentPortMatch(routes []*istio.HTTPRoute, port k8sv1.PortNumber) []*istio.HTTPRoute {
552
	res := make([]*istio.HTTPRoute, 0, len(routes))
553
	for _, r := range routes {
554
		r = r.DeepCopy()
555
		for _, m := range r.Match {
556
			m.Port = uint32(port)
557
		}
558
		if len(r.Match) == 0 {
559
			r.Match = []*istio.HTTPMatchRequest{{
560
				Port: uint32(port),
561
			}}
562
		}
563
		res = append(res, r)
564
	}
565
	return res
566
}
567

568
func augmentTCPPortMatch(routes []*istio.TCPRoute, port k8sv1.PortNumber) []*istio.TCPRoute {
569
	res := make([]*istio.TCPRoute, 0, len(routes))
570
	for _, r := range routes {
571
		r = r.DeepCopy()
572
		for _, m := range r.Match {
573
			m.Port = uint32(port)
574
		}
575
		if len(r.Match) == 0 {
576
			r.Match = []*istio.L4MatchAttributes{{
577
				Port: uint32(port),
578
			}}
579
		}
580
		res = append(res, r)
581
	}
582
	return res
583
}
584

585
func augmentTLSPortMatch(routes []*istio.TLSRoute, port *k8sv1.PortNumber, parentHosts []string) []*istio.TLSRoute {
586
	res := make([]*istio.TLSRoute, 0, len(routes))
587
	for _, r := range routes {
588
		r = r.DeepCopy()
589
		if len(r.Match) == 1 && slices.Equal(r.Match[0].SniHosts, []string{"*"}) {
590
			// For mesh, we use parent hosts for SNI if TLSRroute.hostnames were not specified.
591
			r.Match[0].SniHosts = parentHosts
592
		}
593
		for _, m := range r.Match {
594
			if port != nil {
595
				m.Port = uint32(*port)
596
			}
597
		}
598
		res = append(res, r)
599
	}
600
	return res
601
}
602

603
func compatibleRoutesForHost(routes []*istio.TLSRoute, parentHost string) []*istio.TLSRoute {
604
	res := make([]*istio.TLSRoute, 0, len(routes))
605
	for _, r := range routes {
606
		if len(r.Match) == 1 && len(r.Match[0].SniHosts) > 1 {
607
			r = r.DeepCopy()
608
			sniHosts := []string{}
609
			for _, h := range r.Match[0].SniHosts {
610
				if host.Name(parentHost).Matches(host.Name(h)) {
611
					sniHosts = append(sniHosts, h)
612
				}
613
			}
614
			r.Match[0].SniHosts = sniHosts
615
		}
616
		res = append(res, r)
617
	}
618
	return res
619
}
620

621
func buildGRPCVirtualServices(
622
	ctx configContext,
623
	obj config.Config,
624
	gatewayRoutes map[string]map[string]*config.Config,
625
	meshRoutes map[string]map[string]*config.Config,
626
) {
627
	route := obj.Spec.(*k8s.GRPCRouteSpec)
628
	parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, route.Hostnames, gvk.GRPCRoute, obj.Namespace)
629
	reportStatus := func(results []RouteParentResult) {
630
		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
631
			rs := s.(*k8s.GRPCRouteStatus)
632
			rs.Parents = createRouteStatus(results, obj, rs.Parents)
633
			return rs
634
		})
635
	}
636

637
	type conversionResult struct {
638
		error  *ConfigError
639
		routes []*istio.HTTPRoute
640
	}
641
	convertRules := func(mesh bool) conversionResult {
642
		res := conversionResult{}
643
		for n, r := range route.Rules {
644
			// split the rule to make sure each rule has up to one match
645
			matches := slices.Reference(r.Matches)
646
			if len(matches) == 0 {
647
				matches = append(matches, nil)
648
			}
649
			for _, m := range matches {
650
				if m != nil {
651
					r.Matches = []k8s.GRPCRouteMatch{*m}
652
				}
653
				vs, err := convertGRPCRoute(r, ctx, obj, n, !mesh)
654
				// This was a hard error
655
				if vs == nil {
656
					res.error = err
657
					return conversionResult{error: err}
658
				}
659
				// Got an error but also routes
660
				if err != nil {
661
					res.error = err
662
				}
663

664
				res.routes = append(res.routes, vs)
665
			}
666
		}
667
		return res
668
	}
669
	meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
670

671
	reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
672
		res := RouteParentResult{
673
			OriginalReference: r.OriginalReference,
674
			DeniedReason:      r.DeniedReason,
675
			RouteError:        gwResult.error,
676
		}
677
		if r.IsMesh() {
678
			res.RouteError = meshResult.error
679
		}
680
		return res
681
	}))
682
	count := 0
683
	for _, parent := range filteredReferences(parentRefs) {
684
		// for gateway routes, build one VS per gateway+host
685
		routeMap := gatewayRoutes
686
		routeKey := parent.InternalName
687
		vsHosts := hostnameToStringList(route.Hostnames)
688
		routes := gwResult.routes
689
		if parent.IsMesh() {
690
			routes = meshResult.routes
691
			// for mesh routes, build one VS per namespace/port->host
692
			routeMap = meshRoutes
693
			routeKey = obj.Namespace
694
			if parent.OriginalReference.Port != nil {
695
				routes = augmentPortMatch(routes, *parent.OriginalReference.Port)
696
				routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port)
697
			}
698
			if parent.InternalKind == gvk.ServiceEntry {
699
				vsHosts = serviceEntryHosts(ctx.ServiceEntry,
700
					string(parent.OriginalReference.Name),
701
					string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace))))
702
			} else {
703
				vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
704
					parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
705
			}
706
		}
707
		if len(routes) == 0 {
708
			continue
709
		}
710
		if _, f := routeMap[routeKey]; !f {
711
			routeMap[routeKey] = make(map[string]*config.Config)
712
		}
713

714
		// Create one VS per hostname with a single hostname.
715
		// This ensures we can treat each hostname independently, as the spec requires
716
		for _, h := range vsHosts {
717
			if cfg := routeMap[routeKey][h]; cfg != nil {
718
				// merge http routes
719
				vs := cfg.Spec.(*istio.VirtualService)
720
				vs.Http = append(vs.Http, routes...)
721
				// append parents
722
				cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s",
723
					cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
724
			} else {
725
				name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
726
				routeMap[routeKey][h] = &config.Config{
727
					Meta: config.Meta{
728
						CreationTimestamp: obj.CreationTimestamp,
729
						GroupVersionKind:  gvk.VirtualService,
730
						Name:              name,
731
						Annotations:       routeMeta(obj),
732
						Namespace:         obj.Namespace,
733
						Domain:            ctx.Domain,
734
					},
735
					Spec: &istio.VirtualService{
736
						Hosts:    []string{h},
737
						Gateways: []string{parent.InternalName},
738
						Http:     routes,
739
					},
740
				}
741
				count++
742
			}
743
		}
744
	}
745
	for _, vsByHost := range gatewayRoutes {
746
		for _, cfg := range vsByHost {
747
			vs := cfg.Spec.(*istio.VirtualService)
748
			sortHTTPRoutes(vs.Http)
749
		}
750
	}
751
	for _, vsByHost := range meshRoutes {
752
		for _, cfg := range vsByHost {
753
			vs := cfg.Spec.(*istio.VirtualService)
754
			sortHTTPRoutes(vs.Http)
755
		}
756
	}
757
}
758

759
func routeMeta(obj config.Config) map[string]string {
760
	m := parentMeta(obj, nil)
761
	m[constants.InternalRouteSemantics] = constants.RouteSemanticsGateway
762
	return m
763
}
764

765
// sortHTTPRoutes sorts generated vs routes to meet gateway-api requirements
766
// see https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRouteRule
767
func sortHTTPRoutes(routes []*istio.HTTPRoute) {
768
	sort.SliceStable(routes, func(i, j int) bool {
769
		if len(routes[i].Match) == 0 {
770
			return false
771
		} else if len(routes[j].Match) == 0 {
772
			return true
773
		}
774
		// Only look at match[0], we always generate only one match
775
		m1, m2 := routes[i].Match[0], routes[j].Match[0]
776
		r1, r2 := getURIRank(m1), getURIRank(m2)
777
		len1, len2 := getURILength(m1), getURILength(m2)
778
		switch {
779
		// 1: Exact/Prefix/Regex
780
		case r1 != r2:
781
			return r1 > r2
782
		case len1 != len2:
783
			return len1 > len2
784
			// 2: method math
785
		case (m1.Method == nil) != (m2.Method == nil):
786
			return m1.Method != nil
787
			// 3: number of header matches
788
		case len(m1.Headers) != len(m2.Headers):
789
			return len(m1.Headers) > len(m2.Headers)
790
			// 4: number of query matches
791
		default:
792
			return len(m1.QueryParams) > len(m2.QueryParams)
793
		}
794
	})
795
}
796

797
// getURIRank ranks a URI match type. Exact > Prefix > Regex
798
func getURIRank(match *istio.HTTPMatchRequest) int {
799
	if match.Uri == nil {
800
		return -1
801
	}
802
	switch match.Uri.MatchType.(type) {
803
	case *istio.StringMatch_Exact:
804
		return 3
805
	case *istio.StringMatch_Prefix:
806
		return 2
807
	case *istio.StringMatch_Regex:
808
		return 1
809
	}
810
	// should not happen
811
	return -1
812
}
813

814
func getURILength(match *istio.HTTPMatchRequest) int {
815
	if match.Uri == nil {
816
		return 0
817
	}
818
	switch match.Uri.MatchType.(type) {
819
	case *istio.StringMatch_Prefix:
820
		return len(match.Uri.GetPrefix())
821
	case *istio.StringMatch_Exact:
822
		return len(match.Uri.GetExact())
823
	case *istio.StringMatch_Regex:
824
		return len(match.Uri.GetRegex())
825
	}
826
	// should not happen
827
	return -1
828
}
829

830
func parentMeta(obj config.Config, sectionName *k8s.SectionName) map[string]string {
831
	name := fmt.Sprintf("%s/%s.%s", obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
832
	if sectionName != nil {
833
		name = fmt.Sprintf("%s/%s/%s.%s", obj.GroupVersionKind.Kind, obj.Name, *sectionName, obj.Namespace)
834
	}
835
	return map[string]string{
836
		constants.InternalParentNames: name,
837
	}
838
}
839

840
func hostnameToStringList(h []k8s.Hostname) []string {
841
	// In the Istio API, empty hostname is not allowed. In the Kubernetes API hosts means "any"
842
	if len(h) == 0 {
843
		return []string{"*"}
844
	}
845
	return slices.Map(h, func(e k8s.Hostname) string {
846
		return string(e)
847
	})
848
}
849

850
func toInternalParentReference(p k8s.ParentReference, localNamespace string) (parentKey, error) {
851
	empty := parentKey{}
852
	kind := ptr.OrDefault((*string)(p.Kind), gvk.KubernetesGateway.Kind)
853
	group := ptr.OrDefault((*string)(p.Group), gvk.KubernetesGateway.Group)
854
	var ik config.GroupVersionKind
855
	var ns string
856
	// Currently supported types are Gateway, Service, and ServiceEntry
857
	if kind == gvk.KubernetesGateway.Kind && group == gvk.KubernetesGateway.Group {
858
		ik = gvk.KubernetesGateway
859
	} else if kind == gvk.Service.Kind && group == gvk.Service.Group {
860
		ik = gvk.Service
861
	} else if kind == gvk.ServiceEntry.Kind && group == gvk.ServiceEntry.Group {
862
		ik = gvk.ServiceEntry
863
	} else {
864
		return empty, fmt.Errorf("unsupported parentKey: %v/%v", p.Group, kind)
865
	}
866
	// Unset namespace means "same namespace"
867
	ns = ptr.OrDefault((*string)(p.Namespace), localNamespace)
868
	return parentKey{
869
		Kind:      ik,
870
		Name:      string(p.Name),
871
		Namespace: ns,
872
	}, nil
873
}
874

875
func referenceAllowed(
876
	parent *parentInfo,
877
	routeKind config.GroupVersionKind,
878
	parentRef parentReference,
879
	hostnames []k8s.Hostname,
880
	namespace string,
881
) *ParentError {
882
	if parentRef.Kind == gvk.Service || parentRef.Kind == gvk.ServiceEntry {
883
		// TODO: check if the service reference is valid
884
		if false {
885
			return &ParentError{
886
				Reason:  ParentErrorParentRefConflict,
887
				Message: fmt.Sprintf("parent service: %q is invalid", parentRef.Name),
888
			}
889
		}
890
	} else {
891
		// First, check section and port apply. This must come first
892
		if parentRef.Port != 0 && parentRef.Port != parent.Port {
893
			return &ParentError{
894
				Reason:  ParentErrorNotAccepted,
895
				Message: fmt.Sprintf("port %v not found", parentRef.Port),
896
			}
897
		}
898
		if len(parentRef.SectionName) > 0 && parentRef.SectionName != parent.SectionName {
899
			return &ParentError{
900
				Reason:  ParentErrorNotAccepted,
901
				Message: fmt.Sprintf("sectionName %q not found", parentRef.SectionName),
902
			}
903
		}
904

905
		// Next check the hostnames are a match. This is a bi-directional wildcard match. Only one route
906
		// hostname must match for it to be allowed (but the others will be filtered at runtime)
907
		// If either is empty its treated as a wildcard which always matches
908

909
		if len(hostnames) == 0 {
910
			hostnames = []k8s.Hostname{"*"}
911
		}
912
		if len(parent.Hostnames) > 0 {
913
			// TODO: the spec actually has a label match, not a string match. That is, *.com does not match *.apple.com
914
			// We are doing a string match here
915
			matched := false
916
			hostMatched := false
917
		out:
918
			for _, routeHostname := range hostnames {
919
				for _, parentHostNamespace := range parent.Hostnames {
920
					spl := strings.Split(parentHostNamespace, "/")
921
					parentNamespace, parentHostname := spl[0], spl[1]
922
					hostnameMatch := host.Name(parentHostname).Matches(host.Name(routeHostname))
923
					namespaceMatch := parentNamespace == "*" || parentNamespace == namespace
924
					hostMatched = hostMatched || hostnameMatch
925
					if hostnameMatch && namespaceMatch {
926
						matched = true
927
						break out
928
					}
929
				}
930
			}
931
			if !matched {
932
				if hostMatched {
933
					return &ParentError{
934
						Reason: ParentErrorNotAllowed,
935
						Message: fmt.Sprintf(
936
							"hostnames matched parent hostname %q, but namespace %q is not allowed by the parent",
937
							parent.OriginalHostname, namespace,
938
						),
939
					}
940
				}
941
				return &ParentError{
942
					Reason: ParentErrorNoHostname,
943
					Message: fmt.Sprintf(
944
						"no hostnames matched parent hostname %q",
945
						parent.OriginalHostname,
946
					),
947
				}
948
			}
949
		}
950
	}
951
	// Also make sure this route kind is allowed
952
	matched := false
953
	for _, ak := range parent.AllowedKinds {
954
		if string(ak.Kind) == routeKind.Kind && ptr.OrDefault((*string)(ak.Group), gvk.GatewayClass.Group) == routeKind.Group {
955
			matched = true
956
			break
957
		}
958
	}
959
	if !matched {
960
		return &ParentError{
961
			Reason:  ParentErrorNotAllowed,
962
			Message: fmt.Sprintf("kind %v is not allowed", routeKind),
963
		}
964
	}
965
	return nil
966
}
967

968
func extractParentReferenceInfo(gateways map[parentKey][]*parentInfo, routeRefs []k8s.ParentReference,
969
	hostnames []k8s.Hostname, kind config.GroupVersionKind, localNamespace string,
970
) []routeParentReference {
971
	parentRefs := []routeParentReference{}
972
	for _, ref := range routeRefs {
973
		ir, err := toInternalParentReference(ref, localNamespace)
974
		if err != nil {
975
			// Cannot handle the reference. Maybe it is for another controller, so we just ignore it
976
			continue
977
		}
978
		pk := parentReference{
979
			parentKey:   ir,
980
			SectionName: ptr.OrEmpty(ref.SectionName),
981
			Port:        ptr.OrEmpty(ref.Port),
982
		}
983
		appendParent := func(pr *parentInfo, pk parentReference) {
984
			rpi := routeParentReference{
985
				InternalName:      pr.InternalName,
986
				InternalKind:      ir.Kind,
987
				Hostname:          pr.OriginalHostname,
988
				DeniedReason:      referenceAllowed(pr, kind, pk, hostnames, localNamespace),
989
				OriginalReference: ref,
990
			}
991
			if rpi.DeniedReason == nil {
992
				// Record that we were able to bind to the parent
993
				pr.AttachedRoutes++
994
			}
995
			parentRefs = append(parentRefs, rpi)
996
		}
997
		gk := ir
998
		if ir.Kind == gvk.Service || ir.Kind == gvk.ServiceEntry {
999
			gk = meshParentKey
1000
		}
1001
		for _, gw := range gateways[gk] {
1002
			// Append all matches. Note we may be adding mismatch section or ports; this is handled later
1003
			appendParent(gw, pk)
1004
		}
1005
	}
1006
	// Ensure stable order
1007
	slices.SortBy(parentRefs, func(a routeParentReference) string {
1008
		return parentRefString(a.OriginalReference)
1009
	})
1010
	return parentRefs
1011
}
1012

1013
func buildTCPVirtualService(ctx configContext, obj config.Config) []config.Config {
1014
	route := obj.Spec.(*k8s.TCPRouteSpec)
1015
	parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, nil, gvk.TCPRoute, obj.Namespace)
1016

1017
	reportStatus := func(results []RouteParentResult) {
1018
		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
1019
			rs := s.(*k8s.TCPRouteStatus)
1020
			rs.Parents = createRouteStatus(results, obj, rs.Parents)
1021
			return rs
1022
		})
1023
	}
1024
	type conversionResult struct {
1025
		error  *ConfigError
1026
		routes []*istio.TCPRoute
1027
	}
1028
	convertRules := func(mesh bool) conversionResult {
1029
		res := conversionResult{}
1030
		for _, r := range route.Rules {
1031
			vs, err := convertTCPRoute(ctx, r, obj, !mesh)
1032
			// This was a hard error
1033
			if vs == nil {
1034
				res.error = err
1035
				return conversionResult{error: err}
1036
			}
1037
			// Got an error but also routes
1038
			if err != nil {
1039
				res.error = err
1040
			}
1041
			res.routes = append(res.routes, vs)
1042
		}
1043
		return res
1044
	}
1045
	meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
1046
	reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
1047
		res := RouteParentResult{
1048
			OriginalReference: r.OriginalReference,
1049
			DeniedReason:      r.DeniedReason,
1050
			RouteError:        gwResult.error,
1051
		}
1052
		if r.IsMesh() {
1053
			res.RouteError = meshResult.error
1054
		}
1055
		return res
1056
	}))
1057

1058
	vs := []config.Config{}
1059
	for _, parent := range filteredReferences(parentRefs) {
1060
		routes := gwResult.routes
1061
		vsHosts := []string{"*"}
1062
		if parent.IsMesh() {
1063
			routes = meshResult.routes
1064
			if parent.OriginalReference.Port != nil {
1065
				routes = augmentTCPPortMatch(routes, *parent.OriginalReference.Port)
1066
			}
1067
			if parent.InternalKind == gvk.ServiceEntry {
1068
				vsHosts = serviceEntryHosts(ctx.ServiceEntry,
1069
					string(parent.OriginalReference.Name),
1070
					string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace))))
1071
			} else {
1072
				vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
1073
					parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
1074
			}
1075
		}
1076
		for i, host := range vsHosts {
1077
			name := fmt.Sprintf("%s-tcp-%d-%s", obj.Name, i, constants.KubernetesGatewayName)
1078
			// Create one VS per hostname with a single hostname.
1079
			// This ensures we can treat each hostname independently, as the spec requires
1080
			vs = append(vs, config.Config{
1081
				Meta: config.Meta{
1082
					CreationTimestamp: obj.CreationTimestamp,
1083
					GroupVersionKind:  gvk.VirtualService,
1084
					Name:              name,
1085
					Annotations:       routeMeta(obj),
1086
					Namespace:         obj.Namespace,
1087
					Domain:            ctx.Domain,
1088
				},
1089
				Spec: &istio.VirtualService{
1090
					// We can use wildcard here since each listener can have at most one route bound to it, so we have
1091
					// a single VS per Gateway.
1092
					Hosts:    []string{host},
1093
					Gateways: []string{parent.InternalName},
1094
					Tcp:      routes,
1095
				},
1096
			})
1097
		}
1098
	}
1099
	return vs
1100
}
1101

1102
func buildTLSVirtualService(ctx configContext, obj config.Config) []config.Config {
1103
	route := obj.Spec.(*k8s.TLSRouteSpec)
1104
	parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, nil, gvk.TLSRoute, obj.Namespace)
1105

1106
	reportStatus := func(results []RouteParentResult) {
1107
		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
1108
			rs := s.(*k8s.TLSRouteStatus)
1109
			rs.Parents = createRouteStatus(results, obj, rs.Parents)
1110
			return rs
1111
		})
1112
	}
1113
	type conversionResult struct {
1114
		error  *ConfigError
1115
		routes []*istio.TLSRoute
1116
	}
1117
	convertRules := func(mesh bool) conversionResult {
1118
		res := conversionResult{}
1119
		for _, r := range route.Rules {
1120
			vs, err := convertTLSRoute(ctx, r, obj, !mesh)
1121
			// This was a hard error
1122
			if vs == nil {
1123
				res.error = err
1124
				return conversionResult{error: err}
1125
			}
1126
			// Got an error but also routes
1127
			if err != nil {
1128
				res.error = err
1129
			}
1130
			res.routes = append(res.routes, vs)
1131
		}
1132
		return res
1133
	}
1134
	meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
1135
	reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
1136
		res := RouteParentResult{
1137
			OriginalReference: r.OriginalReference,
1138
			DeniedReason:      r.DeniedReason,
1139
			RouteError:        gwResult.error,
1140
		}
1141
		if r.IsMesh() {
1142
			res.RouteError = meshResult.error
1143
		}
1144
		return res
1145
	}))
1146

1147
	vs := []config.Config{}
1148
	for _, parent := range filteredReferences(parentRefs) {
1149
		routes := gwResult.routes
1150
		vsHosts := hostnameToStringList(route.Hostnames)
1151
		if parent.IsMesh() {
1152
			routes = meshResult.routes
1153
			if parent.InternalKind == gvk.ServiceEntry {
1154
				vsHosts = serviceEntryHosts(ctx.ServiceEntry,
1155
					string(parent.OriginalReference.Name),
1156
					string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace))))
1157
			} else {
1158
				host := fmt.Sprintf("%s.%s.svc.%s",
1159
					parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)
1160
				vsHosts = []string{host}
1161
			}
1162
			routes = augmentTLSPortMatch(routes, parent.OriginalReference.Port, vsHosts)
1163
		}
1164

1165
		for i, host := range vsHosts {
1166
			name := fmt.Sprintf("%s-tls-%d-%s", obj.Name, i, constants.KubernetesGatewayName)
1167
			filteredRoutes := routes
1168
			if parent.IsMesh() {
1169
				filteredRoutes = compatibleRoutesForHost(routes, host)
1170
			}
1171
			// Create one VS per hostname with a single hostname.
1172
			// This ensures we can treat each hostname independently, as the spec requires
1173
			vs = append(vs, config.Config{
1174
				Meta: config.Meta{
1175
					CreationTimestamp: obj.CreationTimestamp,
1176
					GroupVersionKind:  gvk.VirtualService,
1177
					Name:              name,
1178
					Annotations:       routeMeta(obj),
1179
					Namespace:         obj.Namespace,
1180
					Domain:            ctx.Domain,
1181
				},
1182
				Spec: &istio.VirtualService{
1183
					Hosts:    []string{host},
1184
					Gateways: []string{parent.InternalName},
1185
					Tls:      filteredRoutes,
1186
				},
1187
			})
1188
		}
1189
	}
1190
	return vs
1191
}
1192

1193
func convertTCPRoute(ctx configContext, r k8s.TCPRouteRule, obj config.Config, enforceRefGrant bool) (*istio.TCPRoute, *ConfigError) {
1194
	if tcpWeightSum(r.BackendRefs) == 0 {
1195
		// The spec requires us to reject connections when there are no >0 weight backends
1196
		// We don't have a great way to do it. TODO: add a fault injection API for TCP?
1197
		return &istio.TCPRoute{
1198
			Route: []*istio.RouteDestination{{
1199
				Destination: &istio.Destination{
1200
					Host:   "internal.cluster.local",
1201
					Subset: "zero-weight",
1202
					Port:   &istio.PortSelector{Number: 65535},
1203
				},
1204
				Weight: 0,
1205
			}},
1206
		}, nil
1207
	}
1208
	dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TCPRoute)
1209
	if err != nil {
1210
		return nil, err
1211
	}
1212
	return &istio.TCPRoute{
1213
		Route: dest,
1214
	}, backendErr
1215
}
1216

1217
func convertTLSRoute(ctx configContext, r k8s.TLSRouteRule, obj config.Config, enforceRefGrant bool) (*istio.TLSRoute, *ConfigError) {
1218
	if tcpWeightSum(r.BackendRefs) == 0 {
1219
		// The spec requires us to reject connections when there are no >0 weight backends
1220
		// We don't have a great way to do it. TODO: add a fault injection API for TCP?
1221
		return &istio.TLSRoute{
1222
			Route: []*istio.RouteDestination{{
1223
				Destination: &istio.Destination{
1224
					Host:   "internal.cluster.local",
1225
					Subset: "zero-weight",
1226
					Port:   &istio.PortSelector{Number: 65535},
1227
				},
1228
				Weight: 0,
1229
			}},
1230
		}, nil
1231
	}
1232
	dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TLSRoute)
1233
	if err != nil {
1234
		return nil, err
1235
	}
1236
	return &istio.TLSRoute{
1237
		Match: buildTLSMatch(obj.Spec.(*k8s.TLSRouteSpec).Hostnames),
1238
		Route: dest,
1239
	}, backendErr
1240
}
1241

1242
func buildTCPDestination(
1243
	ctx configContext,
1244
	forwardTo []k8s.BackendRef,
1245
	ns string,
1246
	enforceRefGrant bool,
1247
	k config.GroupVersionKind,
1248
) ([]*istio.RouteDestination, *ConfigError, *ConfigError) {
1249
	if forwardTo == nil {
1250
		return nil, nil, nil
1251
	}
1252

1253
	weights := []int{}
1254
	action := []k8s.BackendRef{}
1255
	for _, w := range forwardTo {
1256
		wt := int(ptr.OrDefault(w.Weight, 1))
1257
		if wt == 0 {
1258
			continue
1259
		}
1260
		action = append(action, w)
1261
		weights = append(weights, wt)
1262
	}
1263
	if len(weights) == 1 {
1264
		weights = []int{0}
1265
	}
1266

1267
	var invalidBackendErr *ConfigError
1268
	res := []*istio.RouteDestination{}
1269
	for i, fwd := range action {
1270
		dst, err := buildDestination(ctx, fwd, ns, enforceRefGrant, k)
1271
		if err != nil {
1272
			if isInvalidBackend(err) {
1273
				invalidBackendErr = err
1274
				// keep going, we will gracefully drop invalid backends
1275
			} else {
1276
				return nil, nil, err
1277
			}
1278
		}
1279
		res = append(res, &istio.RouteDestination{
1280
			Destination: dst,
1281
			Weight:      int32(weights[i]),
1282
		})
1283
	}
1284
	return res, invalidBackendErr, nil
1285
}
1286

1287
func buildTLSMatch(hostnames []k8s.Hostname) []*istio.TLSMatchAttributes {
1288
	// Currently, the spec only supports extensions beyond hostname, which are not currently implemented by Istio.
1289
	return []*istio.TLSMatchAttributes{{
1290
		SniHosts: hostnamesToStringListWithWildcard(hostnames),
1291
	}}
1292
}
1293

1294
func hostnamesToStringListWithWildcard(h []k8s.Hostname) []string {
1295
	if len(h) == 0 {
1296
		return []string{"*"}
1297
	}
1298
	res := make([]string, 0, len(h))
1299
	for _, i := range h {
1300
		res = append(res, string(i))
1301
	}
1302
	return res
1303
}
1304

1305
func weightSum(forwardTo []k8s.HTTPBackendRef) int {
1306
	sum := int32(0)
1307
	for _, w := range forwardTo {
1308
		sum += ptr.OrDefault(w.Weight, 1)
1309
	}
1310
	return int(sum)
1311
}
1312

1313
func grpcWeightSum(forwardTo []k8s.GRPCBackendRef) int {
1314
	sum := int32(0)
1315
	for _, w := range forwardTo {
1316
		sum += ptr.OrDefault(w.Weight, 1)
1317
	}
1318
	return int(sum)
1319
}
1320

1321
func tcpWeightSum(forwardTo []k8s.BackendRef) int {
1322
	sum := int32(0)
1323
	for _, w := range forwardTo {
1324
		sum += ptr.OrDefault(w.Weight, 1)
1325
	}
1326
	return int(sum)
1327
}
1328

1329
func buildHTTPDestination(
1330
	ctx configContext,
1331
	forwardTo []k8s.HTTPBackendRef,
1332
	ns string,
1333
	enforceRefGrant bool,
1334
) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) {
1335
	if forwardTo == nil {
1336
		return nil, nil, nil
1337
	}
1338
	weights := []int{}
1339
	action := []k8s.HTTPBackendRef{}
1340
	for _, w := range forwardTo {
1341
		wt := int(ptr.OrDefault(w.Weight, 1))
1342
		if wt == 0 {
1343
			continue
1344
		}
1345
		action = append(action, w)
1346
		weights = append(weights, wt)
1347
	}
1348
	if len(weights) == 1 {
1349
		weights = []int{0}
1350
	}
1351

1352
	var invalidBackendErr *ConfigError
1353
	res := []*istio.HTTPRouteDestination{}
1354
	for i, fwd := range action {
1355
		dst, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.HTTPRoute)
1356
		if err != nil {
1357
			if isInvalidBackend(err) {
1358
				invalidBackendErr = err
1359
				// keep going, we will gracefully drop invalid backends
1360
			} else {
1361
				return nil, nil, err
1362
			}
1363
		}
1364
		rd := &istio.HTTPRouteDestination{
1365
			Destination: dst,
1366
			Weight:      int32(weights[i]),
1367
		}
1368
		for _, filter := range fwd.Filters {
1369
			switch filter.Type {
1370
			case k8sv1.HTTPRouteFilterRequestHeaderModifier:
1371
				h := createHeadersFilter(filter.RequestHeaderModifier)
1372
				if h == nil {
1373
					continue
1374
				}
1375
				if rd.Headers == nil {
1376
					rd.Headers = &istio.Headers{}
1377
				}
1378
				rd.Headers.Request = h
1379
			case k8sv1.HTTPRouteFilterResponseHeaderModifier:
1380
				h := createHeadersFilter(filter.ResponseHeaderModifier)
1381
				if h == nil {
1382
					continue
1383
				}
1384
				if rd.Headers == nil {
1385
					rd.Headers = &istio.Headers{}
1386
				}
1387
				rd.Headers.Response = h
1388
			default:
1389
				return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
1390
			}
1391
		}
1392
		res = append(res, rd)
1393
	}
1394
	return res, invalidBackendErr, nil
1395
}
1396

1397
func buildGRPCDestination(
1398
	ctx configContext,
1399
	forwardTo []k8s.GRPCBackendRef,
1400
	ns string,
1401
	enforceRefGrant bool,
1402
) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) {
1403
	if forwardTo == nil {
1404
		return nil, nil, nil
1405
	}
1406
	weights := []int{}
1407
	action := []k8s.GRPCBackendRef{}
1408
	for _, w := range forwardTo {
1409
		wt := int(ptr.OrDefault(w.Weight, 1))
1410
		if wt == 0 {
1411
			continue
1412
		}
1413
		action = append(action, w)
1414
		weights = append(weights, wt)
1415
	}
1416
	if len(weights) == 1 {
1417
		weights = []int{0}
1418
	}
1419

1420
	var invalidBackendErr *ConfigError
1421
	res := []*istio.HTTPRouteDestination{}
1422
	for i, fwd := range action {
1423
		dst, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.GRPCRoute)
1424
		if err != nil {
1425
			if isInvalidBackend(err) {
1426
				invalidBackendErr = err
1427
				// keep going, we will gracefully drop invalid backends
1428
			} else {
1429
				return nil, nil, err
1430
			}
1431
		}
1432
		rd := &istio.HTTPRouteDestination{
1433
			Destination: dst,
1434
			Weight:      int32(weights[i]),
1435
		}
1436
		for _, filter := range fwd.Filters {
1437
			switch filter.Type {
1438
			case k8s.GRPCRouteFilterRequestHeaderModifier:
1439
				h := createHeadersFilter(filter.RequestHeaderModifier)
1440
				if h == nil {
1441
					continue
1442
				}
1443
				if rd.Headers == nil {
1444
					rd.Headers = &istio.Headers{}
1445
				}
1446
				rd.Headers.Request = h
1447
			case k8s.GRPCRouteFilterResponseHeaderModifier:
1448
				h := createHeadersFilter(filter.ResponseHeaderModifier)
1449
				if h == nil {
1450
					continue
1451
				}
1452
				if rd.Headers == nil {
1453
					rd.Headers = &istio.Headers{}
1454
				}
1455
				rd.Headers.Response = h
1456
			default:
1457
				return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
1458
			}
1459
		}
1460
		res = append(res, rd)
1461
	}
1462
	return res, invalidBackendErr, nil
1463
}
1464

1465
func buildDestination(ctx configContext, to k8s.BackendRef, ns string, enforceRefGrant bool, k config.GroupVersionKind) (*istio.Destination, *ConfigError) {
1466
	// check if the reference is allowed
1467
	if enforceRefGrant {
1468
		refs := ctx.AllowedReferences
1469
		if toNs := to.Namespace; toNs != nil && string(*toNs) != ns {
1470
			if !refs.BackendAllowed(k, to.Name, *toNs, ns) {
1471
				return &istio.Destination{}, &ConfigError{
1472
					Reason:  InvalidDestinationPermit,
1473
					Message: fmt.Sprintf("backendRef %v/%v not accessible to a %s in namespace %q (missing a ReferenceGrant?)", to.Name, *toNs, k.Kind, ns),
1474
				}
1475
			}
1476
		}
1477
	}
1478

1479
	namespace := ptr.OrDefault((*string)(to.Namespace), ns)
1480
	var invalidBackendErr *ConfigError
1481
	if nilOrEqual((*string)(to.Group), "") && nilOrEqual((*string)(to.Kind), gvk.Service.Kind) {
1482
		// Service
1483
		if to.Port == nil {
1484
			// "Port is required when the referent is a Kubernetes Service."
1485
			return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
1486
		}
1487
		if strings.Contains(string(to.Name), ".") {
1488
			return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."}
1489
		}
1490
		hostname := fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.Domain)
1491
		if ctx.Context.GetService(hostname, namespace) == nil {
1492
			invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
1493
		}
1494
		return &istio.Destination{
1495
			// TODO: implement ReferencePolicy for cross namespace
1496
			Host: hostname,
1497
			Port: &istio.PortSelector{Number: uint32(*to.Port)},
1498
		}, invalidBackendErr
1499
	}
1500
	if nilOrEqual((*string)(to.Group), features.MCSAPIGroup) && nilOrEqual((*string)(to.Kind), "ServiceImport") {
1501
		// Service import
1502
		hostname := fmt.Sprintf("%s.%s.svc.clusterset.local", to.Name, namespace)
1503
		if !features.EnableMCSHost {
1504
			// They asked for ServiceImport, but actually don't have full support enabled...
1505
			// No problem, we can just treat it as Service, which is already cross-cluster in this mode anyways
1506
			hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.Domain)
1507
		}
1508
		if to.Port == nil {
1509
			// We don't know where to send without port
1510
			return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
1511
		}
1512
		if strings.Contains(string(to.Name), ".") {
1513
			return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."}
1514
		}
1515
		if ctx.Context.GetService(hostname, namespace) == nil {
1516
			invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
1517
		}
1518
		return &istio.Destination{
1519
			Host: hostname,
1520
			Port: &istio.PortSelector{Number: uint32(*to.Port)},
1521
		}, invalidBackendErr
1522
	}
1523
	if nilOrEqual((*string)(to.Group), gvk.ServiceEntry.Group) && nilOrEqual((*string)(to.Kind), "Hostname") {
1524
		// Hostname synthetic type
1525
		if to.Port == nil {
1526
			// We don't know where to send without port
1527
			return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
1528
		}
1529
		if to.Namespace != nil {
1530
			return nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"}
1531
		}
1532
		hostname := string(to.Name)
1533
		if ctx.Context.GetService(hostname, namespace) == nil {
1534
			invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
1535
		}
1536
		return &istio.Destination{
1537
			Host: string(to.Name),
1538
			Port: &istio.PortSelector{Number: uint32(*to.Port)},
1539
		}, invalidBackendErr
1540
	}
1541
	return &istio.Destination{}, &ConfigError{
1542
		Reason:  InvalidDestinationKind,
1543
		Message: fmt.Sprintf("referencing unsupported backendRef: group %q kind %q", ptr.OrEmpty(to.Group), ptr.OrEmpty(to.Kind)),
1544
	}
1545
}
1546

1547
// https://github.com/kubernetes-sigs/gateway-api/blob/cea484e38e078a2c1997d8c7a62f410a1540f519/apis/v1beta1/httproute_types.go#L207-L212
1548
func isInvalidBackend(err *ConfigError) bool {
1549
	return err.Reason == InvalidDestinationPermit ||
1550
		err.Reason == InvalidDestinationNotFound ||
1551
		err.Reason == InvalidDestinationKind
1552
}
1553

1554
func headerListToMap(hl []k8s.HTTPHeader) map[string]string {
1555
	if len(hl) == 0 {
1556
		return nil
1557
	}
1558
	res := map[string]string{}
1559
	for _, e := range hl {
1560
		k := strings.ToLower(string(e.Name))
1561
		if _, f := res[k]; f {
1562
			// "Subsequent entries with an equivalent header name MUST be ignored"
1563
			continue
1564
		}
1565
		res[k] = e.Value
1566
	}
1567
	return res
1568
}
1569

1570
func createMirrorFilter(ctx configContext, filter *k8s.HTTPRequestMirrorFilter, ns string,
1571
	enforceRefGrant bool, k config.GroupVersionKind,
1572
) (*istio.HTTPMirrorPolicy, *ConfigError) {
1573
	if filter == nil {
1574
		return nil, nil
1575
	}
1576
	var weightOne int32 = 1
1577
	dst, err := buildDestination(ctx, k8s.BackendRef{
1578
		BackendObjectReference: filter.BackendRef,
1579
		Weight:                 &weightOne,
1580
	}, ns, enforceRefGrant, k)
1581
	if err != nil {
1582
		return nil, err
1583
	}
1584
	return &istio.HTTPMirrorPolicy{Destination: dst}, nil
1585
}
1586

1587
func createRewriteFilter(filter *k8s.HTTPURLRewriteFilter) *istio.HTTPRewrite {
1588
	if filter == nil {
1589
		return nil
1590
	}
1591
	rewrite := &istio.HTTPRewrite{}
1592
	if filter.Path != nil {
1593
		switch filter.Path.Type {
1594
		case k8sv1.PrefixMatchHTTPPathModifier:
1595
			rewrite.Uri = strings.TrimSuffix(*filter.Path.ReplacePrefixMatch, "/")
1596
			if rewrite.Uri == "" {
1597
				// `/` means removing the prefix
1598
				rewrite.Uri = "/"
1599
			}
1600
		case k8sv1.FullPathHTTPPathModifier:
1601
			rewrite.UriRegexRewrite = &istio.RegexRewrite{
1602
				Match:   "/.*",
1603
				Rewrite: *filter.Path.ReplaceFullPath,
1604
			}
1605
		}
1606
	}
1607
	if filter.Hostname != nil {
1608
		rewrite.Authority = string(*filter.Hostname)
1609
	}
1610
	// Nothing done
1611
	if rewrite.Uri == "" && rewrite.UriRegexRewrite == nil && rewrite.Authority == "" {
1612
		return nil
1613
	}
1614
	return rewrite
1615
}
1616

1617
func createRedirectFilter(filter *k8s.HTTPRequestRedirectFilter) *istio.HTTPRedirect {
1618
	if filter == nil {
1619
		return nil
1620
	}
1621
	resp := &istio.HTTPRedirect{}
1622
	if filter.StatusCode != nil {
1623
		// Istio allows 301, 302, 303, 307, 308.
1624
		// Gateway allows only 301 and 302.
1625
		resp.RedirectCode = uint32(*filter.StatusCode)
1626
	}
1627
	if filter.Hostname != nil {
1628
		resp.Authority = string(*filter.Hostname)
1629
	}
1630
	if filter.Scheme != nil {
1631
		// Both allow http and https
1632
		resp.Scheme = *filter.Scheme
1633
	}
1634
	if filter.Port != nil {
1635
		resp.RedirectPort = &istio.HTTPRedirect_Port{Port: uint32(*filter.Port)}
1636
	} else {
1637
		// "When empty, port (if specified) of the request is used."
1638
		// this differs from Istio default
1639
		if filter.Scheme != nil {
1640
			resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_PROTOCOL_DEFAULT}
1641
		} else {
1642
			resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_REQUEST_PORT}
1643
		}
1644
	}
1645
	if filter.Path != nil {
1646
		switch filter.Path.Type {
1647
		case k8sv1.FullPathHTTPPathModifier:
1648
			resp.Uri = *filter.Path.ReplaceFullPath
1649
		case k8sv1.PrefixMatchHTTPPathModifier:
1650
			resp.Uri = fmt.Sprintf("%%PREFIX()%%%s", *filter.Path.ReplacePrefixMatch)
1651
		}
1652
	}
1653
	return resp
1654
}
1655

1656
func createHeadersFilter(filter *k8s.HTTPHeaderFilter) *istio.Headers_HeaderOperations {
1657
	if filter == nil {
1658
		return nil
1659
	}
1660
	return &istio.Headers_HeaderOperations{
1661
		Add:    headerListToMap(filter.Add),
1662
		Remove: filter.Remove,
1663
		Set:    headerListToMap(filter.Set),
1664
	}
1665
}
1666

1667
// nolint: unparam
1668
func createMethodMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
1669
	if match.Method == nil {
1670
		return nil, nil
1671
	}
1672
	return &istio.StringMatch{
1673
		MatchType: &istio.StringMatch_Exact{Exact: string(*match.Method)},
1674
	}, nil
1675
}
1676

1677
func createQueryParamsMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
1678
	res := map[string]*istio.StringMatch{}
1679
	for _, qp := range match.QueryParams {
1680
		tp := k8sv1.QueryParamMatchExact
1681
		if qp.Type != nil {
1682
			tp = *qp.Type
1683
		}
1684
		switch tp {
1685
		case k8sv1.QueryParamMatchExact:
1686
			res[string(qp.Name)] = &istio.StringMatch{
1687
				MatchType: &istio.StringMatch_Exact{Exact: qp.Value},
1688
			}
1689
		case k8sv1.QueryParamMatchRegularExpression:
1690
			res[string(qp.Name)] = &istio.StringMatch{
1691
				MatchType: &istio.StringMatch_Regex{Regex: qp.Value},
1692
			}
1693
		default:
1694
			// Should never happen, unless a new field is added
1695
			return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported QueryParams type", tp)}
1696
		}
1697
	}
1698

1699
	if len(res) == 0 {
1700
		return nil, nil
1701
	}
1702
	return res, nil
1703
}
1704

1705
func createHeadersMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
1706
	res := map[string]*istio.StringMatch{}
1707
	for _, header := range match.Headers {
1708
		tp := k8sv1.HeaderMatchExact
1709
		if header.Type != nil {
1710
			tp = *header.Type
1711
		}
1712
		switch tp {
1713
		case k8sv1.HeaderMatchExact:
1714
			res[string(header.Name)] = &istio.StringMatch{
1715
				MatchType: &istio.StringMatch_Exact{Exact: header.Value},
1716
			}
1717
		case k8sv1.HeaderMatchRegularExpression:
1718
			res[string(header.Name)] = &istio.StringMatch{
1719
				MatchType: &istio.StringMatch_Regex{Regex: header.Value},
1720
			}
1721
		default:
1722
			// Should never happen, unless a new field is added
1723
			return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)}
1724
		}
1725
	}
1726

1727
	if len(res) == 0 {
1728
		return nil, nil
1729
	}
1730
	return res, nil
1731
}
1732

1733
func createGRPCHeadersMatch(match k8s.GRPCRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
1734
	res := map[string]*istio.StringMatch{}
1735
	for _, header := range match.Headers {
1736
		tp := k8sv1.HeaderMatchExact
1737
		if header.Type != nil {
1738
			tp = *header.Type
1739
		}
1740
		switch tp {
1741
		case k8sv1.HeaderMatchExact:
1742
			res[string(header.Name)] = &istio.StringMatch{
1743
				MatchType: &istio.StringMatch_Exact{Exact: header.Value},
1744
			}
1745
		case k8sv1.HeaderMatchRegularExpression:
1746
			res[string(header.Name)] = &istio.StringMatch{
1747
				MatchType: &istio.StringMatch_Regex{Regex: header.Value},
1748
			}
1749
		default:
1750
			// Should never happen, unless a new field is added
1751
			return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)}
1752
		}
1753
	}
1754

1755
	if len(res) == 0 {
1756
		return nil, nil
1757
	}
1758
	return res, nil
1759
}
1760

1761
func createURIMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
1762
	tp := k8sv1.PathMatchPathPrefix
1763
	if match.Path.Type != nil {
1764
		tp = *match.Path.Type
1765
	}
1766
	dest := "/"
1767
	if match.Path.Value != nil {
1768
		dest = *match.Path.Value
1769
	}
1770
	switch tp {
1771
	case k8sv1.PathMatchPathPrefix:
1772
		// "When specified, a trailing `/` is ignored."
1773
		if dest != "/" {
1774
			dest = strings.TrimSuffix(dest, "/")
1775
		}
1776
		return &istio.StringMatch{
1777
			MatchType: &istio.StringMatch_Prefix{Prefix: dest},
1778
		}, nil
1779
	case k8sv1.PathMatchExact:
1780
		return &istio.StringMatch{
1781
			MatchType: &istio.StringMatch_Exact{Exact: dest},
1782
		}, nil
1783
	case k8sv1.PathMatchRegularExpression:
1784
		return &istio.StringMatch{
1785
			MatchType: &istio.StringMatch_Regex{Regex: dest},
1786
		}, nil
1787
	default:
1788
		// Should never happen, unless a new field is added
1789
		return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)}
1790
	}
1791
}
1792

1793
func createGRPCURIMatch(match k8s.GRPCRouteMatch) (*istio.StringMatch, *ConfigError) {
1794
	m := match.Method
1795
	if m == nil {
1796
		return nil, nil
1797
	}
1798
	tp := k8s.GRPCMethodMatchExact
1799
	if m.Type != nil {
1800
		tp = *m.Type
1801
	}
1802
	if m.Method == nil && m.Service == nil {
1803
		// Should never happen, invalid per spec
1804
		return nil, &ConfigError{Reason: InvalidConfiguration, Message: "gRPC match must have method or service defined"}
1805
	}
1806
	// gRPC format is /<Service>/<Method>. Since we don't natively understand this, convert to various string matches
1807
	switch tp {
1808
	case k8s.GRPCMethodMatchExact:
1809
		if m.Method == nil {
1810
			return &istio.StringMatch{
1811
				MatchType: &istio.StringMatch_Prefix{Prefix: fmt.Sprintf("/%s/", *m.Service)},
1812
			}, nil
1813
		}
1814
		if m.Service == nil {
1815
			return &istio.StringMatch{
1816
				MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)},
1817
			}, nil
1818
		}
1819
		return &istio.StringMatch{
1820
			MatchType: &istio.StringMatch_Exact{Exact: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)},
1821
		}, nil
1822
	case k8s.GRPCMethodMatchRegularExpression:
1823
		if m.Method == nil {
1824
			return &istio.StringMatch{
1825
				MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/.+", *m.Service)},
1826
			}, nil
1827
		}
1828
		if m.Service == nil {
1829
			return &istio.StringMatch{
1830
				MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)},
1831
			}, nil
1832
		}
1833
		return &istio.StringMatch{
1834
			MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)},
1835
		}, nil
1836
	default:
1837
		// Should never happen, unless a new field is added
1838
		return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)}
1839
	}
1840
}
1841

1842
// getGatewayClass finds all gateway class that are owned by Istio
1843
// Response is ClassName -> Controller type
1844
func getGatewayClasses(r GatewayResources, supportedFeatures []k8sv1.SupportedFeature) map[string]k8s.GatewayController {
1845
	res := map[string]k8s.GatewayController{}
1846
	// Setup builtin ones - these can be overridden possibly
1847
	for name, controller := range builtinClasses {
1848
		res[string(name)] = controller
1849
	}
1850
	for _, obj := range r.GatewayClass {
1851
		gwc := obj.Spec.(*k8s.GatewayClassSpec)
1852
		_, known := classInfos[gwc.ControllerName]
1853
		if !known {
1854
			continue
1855
		}
1856
		res[obj.Name] = gwc.ControllerName
1857

1858
		// Set status. If we created it, it may already be there. If not, set it again
1859
		obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
1860
			gcs := s.(*k8sv1.GatewayClassStatus)
1861
			*gcs = GetClassStatus(gcs, obj.Generation)
1862
			gcs.SupportedFeatures = supportedFeatures
1863
			return gcs
1864
		})
1865
	}
1866

1867
	return res
1868
}
1869

1870
// parentKey holds info about a parentRef (eg route binding to a Gateway). This is a mirror of
1871
// k8s.ParentReference in a form that can be stored in a map
1872
type parentKey struct {
1873
	Kind config.GroupVersionKind
1874
	// Name is the original name of the resource (eg Kubernetes Gateway name)
1875
	Name string
1876
	// Namespace is the namespace of the resource
1877
	Namespace string
1878
}
1879

1880
type parentReference struct {
1881
	parentKey
1882

1883
	SectionName k8s.SectionName
1884
	Port        k8sv1.PortNumber
1885
}
1886

1887
var meshGVK = config.GroupVersionKind{
1888
	Group:   gvk.KubernetesGateway.Group,
1889
	Version: gvk.KubernetesGateway.Version,
1890
	Kind:    "Mesh",
1891
}
1892

1893
var meshParentKey = parentKey{
1894
	Kind: meshGVK,
1895
	Name: "istio",
1896
}
1897

1898
type configContext struct {
1899
	GatewayResources
1900
	AllowedReferences AllowedReferences
1901
	GatewayReferences map[parentKey][]*parentInfo
1902

1903
	// key: referenced resources(e.g. secrets), value: gateway-api resources(e.g. gateways)
1904
	resourceReferences map[model.ConfigKey][]model.ConfigKey
1905
}
1906

1907
// parentInfo holds info about a "parent" - something that can be referenced as a ParentRef in the API.
1908
// Today, this is just Gateway and Mesh.
1909
type parentInfo struct {
1910
	// InternalName refers to the internal name we can reference it by. For example, "mesh" or "my-ns/my-gateway"
1911
	InternalName string
1912
	// AllowedKinds indicates which kinds can be admitted by this parent
1913
	AllowedKinds []k8s.RouteGroupKind
1914
	// Hostnames is the hostnames that must be match to reference to the parent. For gateway this is listener hostname
1915
	// Format is ns/hostname
1916
	Hostnames []string
1917
	// OriginalHostname is the unprocessed form of Hostnames; how it appeared in users' config
1918
	OriginalHostname string
1919

1920
	// AttachedRoutes keeps track of how many routes are attached to this parent. This is tracked for status.
1921
	// Because this is mutate in the route generation, parentInfo must be passed as a pointer
1922
	AttachedRoutes int32
1923
	// ReportAttachedRoutes is a callback that should be triggered once all AttachedRoutes are computed, to
1924
	// actually store the attached route count in the status
1925
	ReportAttachedRoutes func()
1926
	SectionName          k8s.SectionName
1927
	Port                 k8sv1.PortNumber
1928
}
1929

1930
// routeParentReference holds information about a route's parent reference
1931
type routeParentReference struct {
1932
	// InternalName refers to the internal name of the parent we can reference it by. For example, "mesh" or "my-ns/my-gateway"
1933
	InternalName string
1934
	// InternalKind is the Group/Kind of the parent
1935
	InternalKind config.GroupVersionKind
1936
	// DeniedReason, if present, indicates why the reference was not valid
1937
	DeniedReason *ParentError
1938
	// OriginalReference contains the original reference
1939
	OriginalReference k8s.ParentReference
1940
	// Hostname is the hostname match of the parent, if any
1941
	Hostname string
1942
}
1943

1944
func (r routeParentReference) IsMesh() bool {
1945
	return r.InternalName == "mesh"
1946
}
1947

1948
func filteredReferences(parents []routeParentReference) []routeParentReference {
1949
	ret := make([]routeParentReference, 0, len(parents))
1950
	for _, p := range parents {
1951
		if p.DeniedReason != nil {
1952
			// We should filter this out
1953
			continue
1954
		}
1955
		ret = append(ret, p)
1956
	}
1957
	// To ensure deterministic order, sort them
1958
	sort.Slice(ret, func(i, j int) bool {
1959
		return ret[i].InternalName < ret[j].InternalName
1960
	})
1961
	return ret
1962
}
1963

1964
func getDefaultName(name string, kgw *k8s.GatewaySpec) string {
1965
	return fmt.Sprintf("%v-%v", name, kgw.GatewayClassName)
1966
}
1967

1968
func convertGateways(r configContext) ([]config.Config, map[parentKey][]*parentInfo, sets.String) {
1969
	// result stores our generated Istio Gateways
1970
	result := []config.Config{}
1971
	// gwMap stores an index to access parentInfo (which corresponds to a Kubernetes Gateway)
1972
	gwMap := map[parentKey][]*parentInfo{}
1973
	// namespaceLabelReferences keeps track of all namespace label keys referenced by Gateways. This is
1974
	// used to ensure we handle namespace updates for those keys.
1975
	namespaceLabelReferences := sets.New[string]()
1976
	classes := getGatewayClasses(r.GatewayResources, gatewaySupportedFeatures)
1977
	for _, obj := range r.Gateway {
1978
		obj := obj
1979
		kgw := obj.Spec.(*k8s.GatewaySpec)
1980
		controllerName, f := classes[string(kgw.GatewayClassName)]
1981
		if !f {
1982
			// No gateway class found, this may be meant for another controller; should be skipped.
1983
			continue
1984
		}
1985
		classInfo, f := classInfos[controllerName]
1986
		if !f {
1987
			continue
1988
		}
1989
		if classInfo.disableRouteGeneration {
1990
			// We found it, but don't want to handle this class
1991
			continue
1992
		}
1993

1994
		servers := []*istio.Server{}
1995

1996
		// Extract the addresses. A gateway will bind to a specific Service
1997
		gatewayServices, err := extractGatewayServices(r.GatewayResources, kgw, obj)
1998
		if len(gatewayServices) == 0 && err != nil {
1999
			// Short circuit if its a hard failure
2000
			reportGatewayStatus(r, obj, classInfo, gatewayServices, servers, err)
2001
			continue
2002
		}
2003
		for i, l := range kgw.Listeners {
2004
			i := i
2005
			namespaceLabelReferences.InsertAll(getNamespaceLabelReferences(l.AllowedRoutes)...)
2006
			server, programmed := buildListener(r, obj, l, i, controllerName)
2007

2008
			servers = append(servers, server)
2009
			if controllerName == constants.ManagedGatewayMeshController {
2010
				// Waypoint doesn't actually convert the routes to VirtualServices
2011
				continue
2012
			}
2013
			meta := parentMeta(obj, &l.Name)
2014
			meta[constants.InternalGatewaySemantics] = constants.GatewaySemanticsGateway
2015
			meta[model.InternalGatewayServiceAnnotation] = strings.Join(gatewayServices, ",")
2016

2017
			// Each listener generates an Istio Gateway with a single Server. This allows binding to a specific listener.
2018
			gatewayConfig := config.Config{
2019
				Meta: config.Meta{
2020
					CreationTimestamp: obj.CreationTimestamp,
2021
					GroupVersionKind:  gvk.Gateway,
2022
					Name:              kubeconfig.InternalGatewayName(obj.Name, string(l.Name)),
2023
					Annotations:       meta,
2024
					Namespace:         obj.Namespace,
2025
					Domain:            r.Domain,
2026
				},
2027
				Spec: &istio.Gateway{
2028
					Servers: []*istio.Server{server},
2029
				},
2030
			}
2031
			ref := parentKey{
2032
				Kind:      gvk.KubernetesGateway,
2033
				Name:      obj.Name,
2034
				Namespace: obj.Namespace,
2035
			}
2036
			if _, f := gwMap[ref]; !f {
2037
				gwMap[ref] = []*parentInfo{}
2038
			}
2039

2040
			allowed, _ := generateSupportedKinds(l)
2041
			pri := &parentInfo{
2042
				InternalName:     obj.Namespace + "/" + gatewayConfig.Name,
2043
				AllowedKinds:     allowed,
2044
				Hostnames:        server.Hosts,
2045
				OriginalHostname: string(ptr.OrEmpty(l.Hostname)),
2046
				SectionName:      l.Name,
2047
				Port:             l.Port,
2048
			}
2049
			pri.ReportAttachedRoutes = func() {
2050
				reportListenerAttachedRoutes(i, obj, pri.AttachedRoutes)
2051
			}
2052
			gwMap[ref] = append(gwMap[ref], pri)
2053

2054
			if programmed {
2055
				result = append(result, gatewayConfig)
2056
			}
2057
		}
2058

2059
		// If "gateway.istio.io/alias-for" annotation is present, any Route
2060
		// that binds to the gateway will bind to its alias instead.
2061
		// The typical usage is when the original gateway is not managed by the gateway controller
2062
		// but the ( generated ) alias is. This allows people to build their own
2063
		// gateway controllers on top of Istio Gateway Controller.
2064
		if obj.Annotations != nil && obj.Annotations[gatewayAliasForAnnotationKey] != "" {
2065
			ref := parentKey{
2066
				Kind:      gvk.KubernetesGateway,
2067
				Name:      obj.Annotations[gatewayAliasForAnnotationKey],
2068
				Namespace: obj.Namespace,
2069
			}
2070
			alias := parentKey{
2071
				Kind:      gvk.KubernetesGateway,
2072
				Name:      obj.Name,
2073
				Namespace: obj.Namespace,
2074
			}
2075
			gwMap[ref] = gwMap[alias]
2076
		}
2077

2078
		reportGatewayStatus(r, obj, classInfo, gatewayServices, servers, err)
2079
	}
2080
	// Insert a parent for Mesh references.
2081
	gwMap[meshParentKey] = []*parentInfo{
2082
		{
2083
			InternalName: "mesh",
2084
			// Mesh has no configurable AllowedKinds, so allow all supported
2085
			AllowedKinds: []k8s.RouteGroupKind{
2086
				{Group: (*k8s.Group)(ptr.Of(gvk.HTTPRoute.Group)), Kind: k8s.Kind(gvk.HTTPRoute.Kind)},
2087
				{Group: (*k8s.Group)(ptr.Of(gvk.GRPCRoute.Group)), Kind: k8s.Kind(gvk.GRPCRoute.Kind)},
2088
				{Group: (*k8s.Group)(ptr.Of(gvk.TCPRoute.Group)), Kind: k8s.Kind(gvk.TCPRoute.Kind)},
2089
				{Group: (*k8s.Group)(ptr.Of(gvk.TLSRoute.Group)), Kind: k8s.Kind(gvk.TLSRoute.Kind)},
2090
			},
2091
		},
2092
	}
2093
	return result, gwMap, namespaceLabelReferences
2094
}
2095

2096
// Gateway currently requires a listener (https://github.com/kubernetes-sigs/gateway-api/pull/1596).
2097
// We don't *really* care about the listener, but it may make sense to add a warning if users do not
2098
// configure it in an expected way so that we have consistency and can make changes in the future as needed.
2099
// We could completely reject but that seems more likely to cause pain.
2100
func unexpectedWaypointListener(l k8s.Listener) bool {
2101
	if l.Port != 15008 {
2102
		return true
2103
	}
2104
	if l.Protocol != k8s.ProtocolType(protocol.HBONE) {
2105
		return true
2106
	}
2107
	return false
2108
}
2109

2110
func getListenerNames(obj config.Config) sets.Set[k8s.SectionName] {
2111
	res := sets.New[k8s.SectionName]()
2112
	for _, l := range obj.Spec.(*k8s.GatewaySpec).Listeners {
2113
		res.Insert(l.Name)
2114
	}
2115
	return res
2116
}
2117

2118
func reportGatewayStatus(
2119
	r configContext,
2120
	obj config.Config,
2121
	classInfo classInfo,
2122
	gatewayServices []string,
2123
	servers []*istio.Server,
2124
	gatewayErr *ConfigError,
2125
) {
2126
	// TODO: we lose address if servers is empty due to an error
2127
	internal, internalIP, external, pending, warnings, allUsable := r.Context.ResolveGatewayInstances(obj.Namespace, gatewayServices, servers)
2128

2129
	// Setup initial conditions to the success state. If we encounter errors, we will update this.
2130
	// We have two status
2131
	// Accepted: is the configuration valid. We only have errors in listeners, and the status is not supposed to
2132
	// be tied to listeners, so this is always accepted
2133
	// Programmed: is the data plane "ready" (note: eventually consistent)
2134
	gatewayConditions := map[string]*condition{
2135
		string(k8sv1.GatewayConditionAccepted): {
2136
			reason:  string(k8sv1.GatewayReasonAccepted),
2137
			message: "Resource accepted",
2138
		},
2139
		string(k8sv1.GatewayConditionProgrammed): {
2140
			reason:  string(k8sv1.GatewayReasonProgrammed),
2141
			message: "Resource programmed",
2142
		},
2143
	}
2144

2145
	if gatewayErr != nil {
2146
		gatewayConditions[string(k8sv1.GatewayConditionAccepted)].error = gatewayErr
2147
	}
2148

2149
	if len(internal) > 0 {
2150
		msg := fmt.Sprintf("Resource programmed, assigned to service(s) %s", humanReadableJoin(internal))
2151
		gatewayConditions[string(k8sv1.GatewayReasonProgrammed)].message = msg
2152
	}
2153

2154
	if len(gatewayServices) == 0 {
2155
		gatewayConditions[string(k8sv1.GatewayReasonProgrammed)].error = &ConfigError{
2156
			Reason:  InvalidAddress,
2157
			Message: "Failed to assign to any requested addresses",
2158
		}
2159
	} else if len(warnings) > 0 {
2160
		var msg string
2161
		var reason string
2162
		if len(internal) != 0 {
2163
			msg = fmt.Sprintf("Assigned to service(s) %s, but failed to assign to all requested addresses: %s",
2164
				humanReadableJoin(internal), strings.Join(warnings, "; "))
2165
		} else {
2166
			msg = fmt.Sprintf("Failed to assign to any requested addresses: %s", strings.Join(warnings, "; "))
2167
		}
2168
		if allUsable {
2169
			reason = string(k8sv1.GatewayReasonAddressNotAssigned)
2170
		} else {
2171
			reason = string(k8sv1.GatewayReasonAddressNotUsable)
2172
		}
2173
		gatewayConditions[string(k8sv1.GatewayConditionProgrammed)].error = &ConfigError{
2174
			// TODO: this only checks Service ready, we should also check Deployment ready?
2175
			Reason:  reason,
2176
			Message: msg,
2177
		}
2178
	}
2179
	obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
2180
		gs := s.(*k8s.GatewayStatus)
2181
		addressesToReport := external
2182
		if len(addressesToReport) == 0 {
2183
			wantAddressType := classInfo.addressType
2184
			if override, ok := obj.Annotations[addressTypeOverride]; ok {
2185
				wantAddressType = k8s.AddressType(override)
2186
			}
2187
			// There are no external addresses, so report the internal ones
2188
			// TODO: should we always report both?
2189
			if wantAddressType == k8s.IPAddressType {
2190
				addressesToReport = internalIP
2191
			} else {
2192
				for _, hostport := range internal {
2193
					svchost, _, _ := net.SplitHostPort(hostport)
2194
					if !slices.Contains(pending, svchost) && !slices.Contains(addressesToReport, svchost) {
2195
						addressesToReport = append(addressesToReport, svchost)
2196
					}
2197
				}
2198
			}
2199
		}
2200
		// Do not report an address until we are ready. But once we are ready, never remove the address.
2201
		if len(addressesToReport) > 0 {
2202
			gs.Addresses = make([]k8sv1.GatewayStatusAddress, 0, len(addressesToReport))
2203
			for _, addr := range addressesToReport {
2204
				var addrType k8s.AddressType
2205
				if _, err := netip.ParseAddr(addr); err == nil {
2206
					addrType = k8s.IPAddressType
2207
				} else {
2208
					addrType = k8s.HostnameAddressType
2209
				}
2210
				gs.Addresses = append(gs.Addresses, k8sv1.GatewayStatusAddress{
2211
					Value: addr,
2212
					Type:  &addrType,
2213
				})
2214
			}
2215
		}
2216
		// Prune listeners that have been removed
2217
		haveListeners := getListenerNames(obj)
2218
		listeners := make([]k8s.ListenerStatus, 0, len(gs.Listeners))
2219
		for _, l := range gs.Listeners {
2220
			if haveListeners.Contains(l.Name) {
2221
				haveListeners.Delete(l.Name)
2222
				listeners = append(listeners, l)
2223
			}
2224
		}
2225
		gs.Listeners = listeners
2226
		gs.Conditions = setConditions(obj.Generation, gs.Conditions, gatewayConditions)
2227
		return gs
2228
	})
2229
}
2230

2231
// IsManaged checks if a Gateway is managed (ie we create the Deployment and Service) or unmanaged.
2232
// This is based on the address field of the spec. If address is set with a Hostname type, it should point to an existing
2233
// 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.
2234
// If there is an IP, we will set the `loadBalancerIP` type.
2235
// 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.
2236
// So far, this mirrors how out of clusters work (address set means to use existing IP, unset means to provision one),
2237
// and there has been growing consensus on this model for in cluster deployments.
2238
//
2239
// Currently, the supported options are:
2240
// * 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.
2241
// * 1 IP address. This is managed, with IP explicit
2242
// * Nothing. This is managed, with IP auto assigned
2243
//
2244
// Not supported:
2245
// Multiple hostname/IP - It is feasible but preference is to create multiple Gateways. This would also break the 1:1 mapping of GW:Service
2246
// Mixed hostname and IP - doesn't make sense; user should define the IP in service
2247
// NamedAddress - Service has no concept of named address. For cloud's that have named addresses they can be configured by annotations,
2248
//
2249
//	which users can add to the Gateway.
2250
func IsManaged(gw *k8s.GatewaySpec) bool {
2251
	if len(gw.Addresses) == 0 {
2252
		return true
2253
	}
2254
	if len(gw.Addresses) > 1 {
2255
		return false
2256
	}
2257
	if t := gw.Addresses[0].Type; t == nil || *t == k8s.IPAddressType {
2258
		return true
2259
	}
2260
	return false
2261
}
2262

2263
func extractGatewayServices(r GatewayResources, kgw *k8s.GatewaySpec, obj config.Config) ([]string, *ConfigError) {
2264
	if IsManaged(kgw) {
2265
		name := model.GetOrDefault(obj.Annotations[gatewayNameOverride], getDefaultName(obj.Name, kgw))
2266
		return []string{fmt.Sprintf("%s.%s.svc.%v", name, obj.Namespace, r.Domain)}, nil
2267
	}
2268
	gatewayServices := []string{}
2269
	skippedAddresses := []string{}
2270
	for _, addr := range kgw.Addresses {
2271
		if addr.Type != nil && *addr.Type != k8s.HostnameAddressType {
2272
			// We only support HostnameAddressType. Keep track of invalid ones so we can report in status.
2273
			skippedAddresses = append(skippedAddresses, addr.Value)
2274
			continue
2275
		}
2276
		// TODO: For now we are using Addresses. There has been some discussion of allowing inline
2277
		// parameters on the class field like a URL, in which case we will probably just use that. See
2278
		// https://github.com/kubernetes-sigs/gateway-api/pull/614
2279
		fqdn := addr.Value
2280
		if !strings.Contains(fqdn, ".") {
2281
			// Short name, expand it
2282
			fqdn = fmt.Sprintf("%s.%s.svc.%s", fqdn, obj.Namespace, r.Domain)
2283
		}
2284
		gatewayServices = append(gatewayServices, fqdn)
2285
	}
2286
	if len(skippedAddresses) > 0 {
2287
		// Give error but return services, this is a soft failure
2288
		return gatewayServices, &ConfigError{
2289
			Reason:  InvalidAddress,
2290
			Message: fmt.Sprintf("only Hostname is supported, ignoring %v", skippedAddresses),
2291
		}
2292
	}
2293
	if _, f := obj.Annotations[serviceTypeOverride]; f {
2294
		// Give error but return services, this is a soft failure
2295
		// Remove entirely in 1.20
2296
		return gatewayServices, &ConfigError{
2297
			Reason:  DeprecateFieldUsage,
2298
			Message: fmt.Sprintf("annotation %v is deprecated, use Spec.Infrastructure.Routeability", serviceTypeOverride),
2299
		}
2300
	}
2301
	return gatewayServices, nil
2302
}
2303

2304
// getNamespaceLabelReferences fetches all label keys used in namespace selectors. Return order may not be stable.
2305
func getNamespaceLabelReferences(routes *k8s.AllowedRoutes) []string {
2306
	if routes == nil || routes.Namespaces == nil || routes.Namespaces.Selector == nil {
2307
		return nil
2308
	}
2309
	res := []string{}
2310
	for k := range routes.Namespaces.Selector.MatchLabels {
2311
		res = append(res, k)
2312
	}
2313
	for _, me := range routes.Namespaces.Selector.MatchExpressions {
2314
		if me.Operator == metav1.LabelSelectorOpNotIn || me.Operator == metav1.LabelSelectorOpDoesNotExist {
2315
			// Over-matching is fine because this only controls the set of namespace
2316
			// label change events to watch and the actual binding enforcement happens
2317
			// by checking the intersection of the generated VirtualService.spec.hosts
2318
			// and Istio Gateway.spec.servers.hosts arrays - we just can't miss
2319
			// potentially relevant namespace label events here.
2320
			res = append(res, "*")
2321
		}
2322

2323
		res = append(res, me.Key)
2324
	}
2325
	return res
2326
}
2327

2328
func buildListener(r configContext, obj config.Config, l k8s.Listener, listenerIndex int, controllerName k8s.GatewayController) (*istio.Server, bool) {
2329
	listenerConditions := map[string]*condition{
2330
		string(k8sv1.ListenerConditionAccepted): {
2331
			reason:  string(k8sv1.ListenerReasonAccepted),
2332
			message: "No errors found",
2333
		},
2334
		string(k8sv1.ListenerConditionProgrammed): {
2335
			reason:  string(k8sv1.ListenerReasonProgrammed),
2336
			message: "No errors found",
2337
		},
2338
		string(k8sv1.ListenerConditionConflicted): {
2339
			reason:  string(k8sv1.ListenerReasonNoConflicts),
2340
			message: "No errors found",
2341
			status:  kstatus.StatusFalse,
2342
		},
2343
		string(k8sv1.ListenerConditionResolvedRefs): {
2344
			reason:  string(k8sv1.ListenerReasonResolvedRefs),
2345
			message: "No errors found",
2346
		},
2347
	}
2348

2349
	ok := true
2350
	tls, err := buildTLS(r, l.TLS, obj, kube.IsAutoPassthrough(obj.Labels, l))
2351
	if err != nil {
2352
		listenerConditions[string(k8sv1.ListenerConditionResolvedRefs)].error = err
2353
		listenerConditions[string(k8sv1.GatewayConditionProgrammed)].error = &ConfigError{
2354
			Reason:  string(k8sv1.GatewayReasonInvalid),
2355
			Message: "Bad TLS configuration",
2356
		}
2357
		ok = false
2358
	}
2359
	hostnames := buildHostnameMatch(obj.Namespace, r.GatewayResources, l)
2360
	server := &istio.Server{
2361
		Port: &istio.Port{
2362
			// Name is required. We only have one server per Gateway, so we can just name them all the same
2363
			Name:     "default",
2364
			Number:   uint32(l.Port),
2365
			Protocol: listenerProtocolToIstio(l.Protocol),
2366
		},
2367
		Hosts: hostnames,
2368
		Tls:   tls,
2369
	}
2370
	if controllerName == constants.ManagedGatewayMeshController {
2371
		if unexpectedWaypointListener(l) {
2372
			listenerConditions[string(k8sv1.ListenerConditionAccepted)].error = &ConfigError{
2373
				Reason:  string(k8sv1.ListenerReasonUnsupportedProtocol),
2374
				Message: `Expected a single listener on port 15008 with protocol "HBONE"`,
2375
			}
2376
		}
2377
	}
2378

2379
	reportListenerCondition(listenerIndex, l, obj, listenerConditions)
2380
	return server, ok
2381
}
2382

2383
func listenerProtocolToIstio(protocol k8s.ProtocolType) string {
2384
	// Currently, all gateway-api protocols are valid Istio protocols.
2385
	return string(protocol)
2386
}
2387

2388
func buildTLS(ctx configContext, tls *k8s.GatewayTLSConfig, gw config.Config, isAutoPassthrough bool) (*istio.ServerTLSSettings, *ConfigError) {
2389
	if tls == nil {
2390
		return nil, nil
2391
	}
2392
	// Explicitly not supported: file mounted
2393
	// Not yet implemented: TLS mode, https redirect, max protocol version, SANs, CipherSuites, VerifyCertificate
2394
	out := &istio.ServerTLSSettings{
2395
		HttpsRedirect: false,
2396
	}
2397
	mode := k8sv1.TLSModeTerminate
2398
	if tls.Mode != nil {
2399
		mode = *tls.Mode
2400
	}
2401
	namespace := gw.Namespace
2402
	switch mode {
2403
	case k8sv1.TLSModeTerminate:
2404
		out.Mode = istio.ServerTLSSettings_SIMPLE
2405
		if tls.Options != nil {
2406
			switch tls.Options[gatewayTLSTerminateModeKey] {
2407
			case "MUTUAL":
2408
				out.Mode = istio.ServerTLSSettings_MUTUAL
2409
			case "ISTIO_MUTUAL":
2410
				out.Mode = istio.ServerTLSSettings_ISTIO_MUTUAL
2411
				return out, nil
2412
			}
2413
		}
2414
		if len(tls.CertificateRefs) != 1 {
2415
			// This is required in the API, should be rejected in validation
2416
			return out, &ConfigError{Reason: InvalidTLS, Message: "exactly 1 certificateRefs should be present for TLS termination"}
2417
		}
2418
		cred, err := buildSecretReference(ctx, tls.CertificateRefs[0], gw)
2419
		if err != nil {
2420
			return out, err
2421
		}
2422
		credNs := ptr.OrDefault((*string)(tls.CertificateRefs[0].Namespace), namespace)
2423
		sameNamespace := credNs == namespace
2424
		if !sameNamespace && !ctx.AllowedReferences.SecretAllowed(creds.ToResourceName(cred), namespace) {
2425
			return out, &ConfigError{
2426
				Reason: InvalidListenerRefNotPermitted,
2427
				Message: fmt.Sprintf(
2428
					"certificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)",
2429
					tls.CertificateRefs[0].Name, credNs, namespace,
2430
				),
2431
			}
2432
		}
2433
		out.CredentialName = cred
2434
	case k8sv1.TLSModePassthrough:
2435
		out.Mode = istio.ServerTLSSettings_PASSTHROUGH
2436
		if isAutoPassthrough {
2437
			out.Mode = istio.ServerTLSSettings_AUTO_PASSTHROUGH
2438
		}
2439
	}
2440
	return out, nil
2441
}
2442

2443
func buildSecretReference(ctx configContext, ref k8s.SecretObjectReference, gw config.Config) (string, *ConfigError) {
2444
	if !nilOrEqual((*string)(ref.Group), gvk.Secret.Group) || !nilOrEqual((*string)(ref.Kind), gvk.Secret.Kind) {
2445
		return "", &ConfigError{Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, only secret is allowed", objectReferenceString(ref))}
2446
	}
2447

2448
	secret := model.ConfigKey{
2449
		Kind:      kind.Secret,
2450
		Name:      string(ref.Name),
2451
		Namespace: ptr.OrDefault((*string)(ref.Namespace), gw.Namespace),
2452
	}
2453

2454
	ctx.resourceReferences[secret] = append(ctx.resourceReferences[secret], model.ConfigKey{
2455
		Kind:      kind.KubernetesGateway,
2456
		Namespace: gw.Namespace,
2457
		Name:      gw.Name,
2458
	})
2459

2460
	if ctx.Credentials != nil {
2461
		if certInfo, err := ctx.Credentials.GetCertInfo(secret.Name, secret.Namespace); err != nil {
2462
			return "", &ConfigError{
2463
				Reason:  InvalidTLS,
2464
				Message: fmt.Sprintf("invalid certificate reference %v, %v", objectReferenceString(ref), err),
2465
			}
2466
		} else if _, err = tls.X509KeyPair(certInfo.Cert, certInfo.Key); err != nil {
2467
			return "", &ConfigError{
2468
				Reason:  InvalidTLS,
2469
				Message: fmt.Sprintf("invalid certificate reference %v, the certificate is malformed: %v", objectReferenceString(ref), err),
2470
			}
2471
		}
2472
	}
2473

2474
	return creds.ToKubernetesGatewayResource(secret.Namespace, secret.Name), nil
2475
}
2476

2477
func objectReferenceString(ref k8s.SecretObjectReference) string {
2478
	return fmt.Sprintf("%s/%s/%s.%s",
2479
		ptr.OrEmpty(ref.Group),
2480
		ptr.OrEmpty(ref.Kind),
2481
		ref.Name,
2482
		ptr.OrEmpty(ref.Namespace))
2483
}
2484

2485
func parentRefString(ref k8s.ParentReference) string {
2486
	return fmt.Sprintf("%s/%s/%s/%s/%d.%s",
2487
		ptr.OrEmpty(ref.Group),
2488
		ptr.OrEmpty(ref.Kind),
2489
		ref.Name,
2490
		ptr.OrEmpty(ref.SectionName),
2491
		ptr.OrEmpty(ref.Port),
2492
		ptr.OrEmpty(ref.Namespace))
2493
}
2494

2495
// buildHostnameMatch generates a Gateway.spec.servers.hosts section from a listener
2496
func buildHostnameMatch(localNamespace string, r GatewayResources, l k8s.Listener) []string {
2497
	// We may allow all hostnames or a specific one
2498
	hostname := "*"
2499
	if l.Hostname != nil {
2500
		hostname = string(*l.Hostname)
2501
	}
2502

2503
	resp := []string{}
2504
	for _, ns := range namespacesFromSelector(localNamespace, r, l.AllowedRoutes) {
2505
		// This check is necessary to prevent adding a hostname with an invalid empty namespace
2506
		if len(ns) > 0 {
2507
			resp = append(resp, fmt.Sprintf("%s/%s", ns, hostname))
2508
		}
2509
	}
2510

2511
	// If nothing matched use ~ namespace (match nothing). We need this since its illegal to have an
2512
	// empty hostname list, but we still need the Gateway provisioned to ensure status is properly set and
2513
	// SNI matches are established; we just don't want to actually match any routing rules (yet).
2514
	if len(resp) == 0 {
2515
		return []string{"~/" + hostname}
2516
	}
2517
	return resp
2518
}
2519

2520
// namespacesFromSelector determines a list of allowed namespaces for a given AllowedRoutes
2521
func namespacesFromSelector(localNamespace string, r GatewayResources, lr *k8s.AllowedRoutes) []string {
2522
	// Default is to allow only the same namespace
2523
	if lr == nil || lr.Namespaces == nil || lr.Namespaces.From == nil || *lr.Namespaces.From == k8sv1.NamespacesFromSame {
2524
		return []string{localNamespace}
2525
	}
2526
	if *lr.Namespaces.From == k8sv1.NamespacesFromAll {
2527
		return []string{"*"}
2528
	}
2529

2530
	if lr.Namespaces.Selector == nil {
2531
		// Should never happen, invalid config
2532
		return []string{"*"}
2533
	}
2534

2535
	// gateway-api has selectors, but Istio Gateway just has a list of names. We will run the selector
2536
	// against all namespaces and get a list of matching namespaces that can be converted into a list
2537
	// Istio can handle.
2538
	ls, err := metav1.LabelSelectorAsSelector(lr.Namespaces.Selector)
2539
	if err != nil {
2540
		return nil
2541
	}
2542
	namespaces := []string{}
2543
	for _, ns := range r.Namespaces {
2544
		if ls.Matches(toNamespaceSet(ns.Name, ns.Labels)) {
2545
			namespaces = append(namespaces, ns.Name)
2546
		}
2547
	}
2548
	// Ensure stable order
2549
	sort.Strings(namespaces)
2550
	return namespaces
2551
}
2552

2553
func nilOrEqual(have *string, expected string) bool {
2554
	return have == nil || *have == expected
2555
}
2556

2557
func humanReadableJoin(ss []string) string {
2558
	switch len(ss) {
2559
	case 0:
2560
		return ""
2561
	case 1:
2562
		return ss[0]
2563
	case 2:
2564
		return ss[0] + " and " + ss[1]
2565
	default:
2566
		return strings.Join(ss[:len(ss)-1], ", ") + ", and " + ss[len(ss)-1]
2567
	}
2568
}
2569

2570
// NamespaceNameLabel represents that label added automatically to namespaces is newer Kubernetes clusters
2571
const NamespaceNameLabel = "kubernetes.io/metadata.name"
2572

2573
// toNamespaceSet converts a set of namespace labels to a Set that can be used to select against.
2574
func toNamespaceSet(name string, labels map[string]string) klabels.Set {
2575
	// If namespace label is not set, implicitly insert it to support older Kubernetes versions
2576
	if labels[NamespaceNameLabel] == name {
2577
		// Already set, avoid copies
2578
		return labels
2579
	}
2580
	// First we need a copy to not modify the underlying object
2581
	ret := make(map[string]string, len(labels)+1)
2582
	for k, v := range labels {
2583
		ret[k] = v
2584
	}
2585
	ret[NamespaceNameLabel] = name
2586
	return ret
2587
}
2588

2589
func (kr GatewayResources) FuzzValidate() bool {
2590
	for _, gwc := range kr.GatewayClass {
2591
		if gwc.Spec == nil {
2592
			return false
2593
		}
2594
	}
2595
	for _, rp := range kr.ReferenceGrant {
2596
		if rp.Spec == nil {
2597
			return false
2598
		}
2599
	}
2600
	for _, hr := range kr.HTTPRoute {
2601
		if hr.Spec == nil {
2602
			return false
2603
		}
2604
	}
2605
	for _, hr := range kr.GRPCRoute {
2606
		if hr.Spec == nil {
2607
			return false
2608
		}
2609
	}
2610
	for _, tr := range kr.TLSRoute {
2611
		if tr.Spec == nil {
2612
			return false
2613
		}
2614
	}
2615
	for _, g := range kr.Gateway {
2616
		if g.Spec == nil {
2617
			return false
2618
		}
2619
	}
2620
	for _, tr := range kr.TCPRoute {
2621
		if tr.Spec == nil {
2622
			return false
2623
		}
2624
	}
2625
	return true
2626
}
2627

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.