1
// Copyright Istio Authors
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
7
// http://www.apache.org/licenses/LICENSE-2.0
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.
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"
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"
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
57
return configs[i].CreationTimestamp.Before(configs[j].CreationTimestamp)
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)
68
result := IstioResources{}
71
AllowedReferences: convertReferencePolicies(r),
72
resourceReferences: make(map[model.ConfigKey][]model.ConfigKey),
75
gw, gwMap, nsReferences := convertGateways(ctx)
76
ctx.GatewayReferences = gwMap
79
result.VirtualService = convertVirtualService(ctx)
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()
90
result.AllowedReferences = ctx.AllowedReferences
91
result.ReferencedNamespaceKeys = nsReferences
92
result.ResourceReferences = ctx.resourceReferences
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 {
101
Grant *k8s.ReferenceGrantSpec
103
specs := make([]namespacedGrant, 0, len(r.ReferenceGrant))
105
for _, obj := range r.ReferenceGrant {
106
rp := obj.Spec.(*k8s.ReferenceGrantSpec)
107
specs = append(specs, namespacedGrant{Namespace: obj.Namespace, Grant: rp})
109
for _, ng := range specs {
111
for _, from := range rp.From {
112
fromKey := Reference{
113
Namespace: from.Namespace,
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
124
// Not supported type. Not an error; may be for another controller
127
for _, to := range rp.To {
129
Namespace: k8s.Namespace(ng.Namespace),
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
136
// Not supported type. Not an error; may be for another controller
139
if _, f := res[fromKey]; !f {
140
res[fromKey] = map[Reference]*Grants{}
142
if _, f := res[fromKey][toKey]; !f {
143
res[fromKey][toKey] = &Grants{
144
AllowedNames: sets.New[string](),
148
res[fromKey][toKey].AllowedNames.Insert(string(*to.Name))
150
res[fromKey][toKey].AllowAll = true
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)...)
165
for _, obj := range r.TLSRoute {
166
result = append(result, buildTLSVirtualService(r, obj)...)
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)
176
for _, obj := range r.GRPCRoute {
177
buildGRPCVirtualServices(r, obj, gatewayRoutes, meshRoutes)
179
for _, vsByHost := range gatewayRoutes {
180
for _, vsConfig := range vsByHost {
181
result = append(result, *vsConfig)
184
for _, vsByHost := range meshRoutes {
185
for _, vsConfig := range vsByHost {
186
result = append(result, *vsConfig)
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)
201
for _, match := range r.Matches {
202
uri, err := createURIMatch(match)
206
headers, err := createHeadersMatch(match)
210
qp, err := createQueryParamsMatch(match)
214
method, err := createMethodMatch(match)
218
vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
225
for _, filter := range r.Filters {
227
case k8sv1.HTTPRouteFilterRequestHeaderModifier:
228
h := createHeadersFilter(filter.RequestHeaderModifier)
232
if vs.Headers == nil {
233
vs.Headers = &istio.Headers{}
235
vs.Headers.Request = h
236
case k8sv1.HTTPRouteFilterResponseHeaderModifier:
237
h := createHeadersFilter(filter.ResponseHeaderModifier)
241
if vs.Headers == nil {
242
vs.Headers = &istio.Headers{}
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)
252
vs.Mirrors = append(vs.Mirrors, mirror)
253
case k8sv1.HTTPRouteFilterURLRewrite:
254
vs.Rewrite = createRewriteFilter(filter.URLRewrite)
256
return nil, &ConfigError{
257
Reason: InvalidFilter,
258
Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
263
if r.Timeouts != nil {
264
if r.Timeouts.Request != nil {
265
request, _ := time.ParseDuration(string(*r.Timeouts.Request))
267
vs.Timeout = durationpb.New(request)
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
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{
289
route, backendErr, err := buildHTTPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant)
294
return vs, backendErr
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)
309
for _, match := range r.Matches {
310
uri, err := createGRPCURIMatch(match)
314
headers, err := createGRPCHeadersMatch(match)
318
vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
323
for _, filter := range r.Filters {
325
case k8s.GRPCRouteFilterRequestHeaderModifier:
326
h := createHeadersFilter(filter.RequestHeaderModifier)
330
if vs.Headers == nil {
331
vs.Headers = &istio.Headers{}
333
vs.Headers.Request = h
334
case k8s.GRPCRouteFilterResponseHeaderModifier:
335
h := createHeadersFilter(filter.ResponseHeaderModifier)
339
if vs.Headers == nil {
340
vs.Headers = &istio.Headers{}
342
vs.Headers.Response = h
343
case k8s.GRPCRouteFilterRequestMirror:
344
mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.GRPCRoute)
348
vs.Mirrors = append(vs.Mirrors, mirror)
350
return nil, &ConfigError{
351
Reason: InvalidFilter,
352
Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
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{
363
route, backendErr, err := buildGRPCDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant)
368
return vs, backendErr
374
func parentTypes(rpi []routeParentReference) (mesh, gateway bool) {
375
for _, r := range rpi {
385
func buildHTTPVirtualServices(
388
gatewayRoutes map[string]map[string]*config.Config,
389
meshRoutes map[string]map[string]*config.Config,
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)
401
type conversionResult struct {
403
routes []*istio.HTTPRoute
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)
413
for _, m := range matches {
415
r.Matches = []k8s.HTTPRouteMatch{*m}
417
vs, err := convertHTTPRoute(r, ctx, obj, n, !mesh)
418
// This was a hard error
421
return conversionResult{error: err}
423
// Got an error but also routes
428
res.routes = append(res.routes, vs)
433
meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
435
reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
436
res := RouteParentResult{
437
OriginalReference: r.OriginalReference,
438
DeniedReason: r.DeniedReason,
439
RouteError: gwResult.error,
442
res.RouteError = meshResult.error
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
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)
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))))
467
vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
468
parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
471
if len(routes) == 0 {
474
if _, f := routeMap[routeKey]; !f {
475
routeMap[routeKey] = make(map[string]*config.Config)
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 {
483
vs := cfg.Spec.(*istio.VirtualService)
484
vs.Http = append(vs.Http, routes...)
486
cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s",
487
cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
489
name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
490
routeMap[routeKey][h] = &config.Config{
492
CreationTimestamp: obj.CreationTimestamp,
493
GroupVersionKind: gvk.VirtualService,
495
Annotations: routeMeta(obj),
496
Namespace: obj.Namespace,
499
Spec: &istio.VirtualService{
501
Gateways: []string{parent.InternalName},
509
for _, vsByHost := range gatewayRoutes {
510
for _, cfg := range vsByHost {
511
vs := cfg.Spec.(*istio.VirtualService)
512
sortHTTPRoutes(vs.Http)
515
for _, vsByHost := range meshRoutes {
516
for _, cfg := range vsByHost {
517
vs := cfg.Spec.(*istio.VirtualService)
518
sortHTTPRoutes(vs.Http)
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
528
ns = metav1.NamespaceDefault
531
se := obj.Spec.(*istio.ServiceEntry)
539
func buildMeshAndGatewayRoutes[T any](parentRefs []routeParentReference, convertRules func(mesh bool) T) (T, T) {
540
var meshResult, gwResult T
541
needMesh, needGw := parentTypes(parentRefs)
543
meshResult = convertRules(true)
546
gwResult = convertRules(false)
548
return meshResult, gwResult
551
func augmentPortMatch(routes []*istio.HTTPRoute, port k8sv1.PortNumber) []*istio.HTTPRoute {
552
res := make([]*istio.HTTPRoute, 0, len(routes))
553
for _, r := range routes {
555
for _, m := range r.Match {
556
m.Port = uint32(port)
558
if len(r.Match) == 0 {
559
r.Match = []*istio.HTTPMatchRequest{{
568
func augmentTCPPortMatch(routes []*istio.TCPRoute, port k8sv1.PortNumber) []*istio.TCPRoute {
569
res := make([]*istio.TCPRoute, 0, len(routes))
570
for _, r := range routes {
572
for _, m := range r.Match {
573
m.Port = uint32(port)
575
if len(r.Match) == 0 {
576
r.Match = []*istio.L4MatchAttributes{{
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 {
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
593
for _, m := range r.Match {
595
m.Port = uint32(*port)
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 {
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)
614
r.Match[0].SniHosts = sniHosts
621
func buildGRPCVirtualServices(
624
gatewayRoutes map[string]map[string]*config.Config,
625
meshRoutes map[string]map[string]*config.Config,
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)
637
type conversionResult struct {
639
routes []*istio.HTTPRoute
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)
649
for _, m := range matches {
651
r.Matches = []k8s.GRPCRouteMatch{*m}
653
vs, err := convertGRPCRoute(r, ctx, obj, n, !mesh)
654
// This was a hard error
657
return conversionResult{error: err}
659
// Got an error but also routes
664
res.routes = append(res.routes, vs)
669
meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules)
671
reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult {
672
res := RouteParentResult{
673
OriginalReference: r.OriginalReference,
674
DeniedReason: r.DeniedReason,
675
RouteError: gwResult.error,
678
res.RouteError = meshResult.error
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
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)
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))))
703
vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
704
parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
707
if len(routes) == 0 {
710
if _, f := routeMap[routeKey]; !f {
711
routeMap[routeKey] = make(map[string]*config.Config)
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 {
719
vs := cfg.Spec.(*istio.VirtualService)
720
vs.Http = append(vs.Http, routes...)
722
cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s",
723
cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
725
name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
726
routeMap[routeKey][h] = &config.Config{
728
CreationTimestamp: obj.CreationTimestamp,
729
GroupVersionKind: gvk.VirtualService,
731
Annotations: routeMeta(obj),
732
Namespace: obj.Namespace,
735
Spec: &istio.VirtualService{
737
Gateways: []string{parent.InternalName},
745
for _, vsByHost := range gatewayRoutes {
746
for _, cfg := range vsByHost {
747
vs := cfg.Spec.(*istio.VirtualService)
748
sortHTTPRoutes(vs.Http)
751
for _, vsByHost := range meshRoutes {
752
for _, cfg := range vsByHost {
753
vs := cfg.Spec.(*istio.VirtualService)
754
sortHTTPRoutes(vs.Http)
759
func routeMeta(obj config.Config) map[string]string {
760
m := parentMeta(obj, nil)
761
m[constants.InternalRouteSemantics] = constants.RouteSemanticsGateway
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 {
771
} else if len(routes[j].Match) == 0 {
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)
779
// 1: Exact/Prefix/Regex
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
792
return len(m1.QueryParams) > len(m2.QueryParams)
797
// getURIRank ranks a URI match type. Exact > Prefix > Regex
798
func getURIRank(match *istio.HTTPMatchRequest) int {
799
if match.Uri == nil {
802
switch match.Uri.MatchType.(type) {
803
case *istio.StringMatch_Exact:
805
case *istio.StringMatch_Prefix:
807
case *istio.StringMatch_Regex:
814
func getURILength(match *istio.HTTPMatchRequest) int {
815
if match.Uri == nil {
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())
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)
835
return map[string]string{
836
constants.InternalParentNames: name,
840
func hostnameToStringList(h []k8s.Hostname) []string {
841
// In the Istio API, empty hostname is not allowed. In the Kubernetes API hosts means "any"
845
return slices.Map(h, func(e k8s.Hostname) string {
850
func toInternalParentReference(p k8s.ParentReference, localNamespace string) (parentKey, error) {
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
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 {
861
} else if kind == gvk.ServiceEntry.Kind && group == gvk.ServiceEntry.Group {
862
ik = gvk.ServiceEntry
864
return empty, fmt.Errorf("unsupported parentKey: %v/%v", p.Group, kind)
866
// Unset namespace means "same namespace"
867
ns = ptr.OrDefault((*string)(p.Namespace), localNamespace)
870
Name: string(p.Name),
875
func referenceAllowed(
877
routeKind config.GroupVersionKind,
878
parentRef parentReference,
879
hostnames []k8s.Hostname,
882
if parentRef.Kind == gvk.Service || parentRef.Kind == gvk.ServiceEntry {
883
// TODO: check if the service reference is valid
886
Reason: ParentErrorParentRefConflict,
887
Message: fmt.Sprintf("parent service: %q is invalid", parentRef.Name),
891
// First, check section and port apply. This must come first
892
if parentRef.Port != 0 && parentRef.Port != parent.Port {
894
Reason: ParentErrorNotAccepted,
895
Message: fmt.Sprintf("port %v not found", parentRef.Port),
898
if len(parentRef.SectionName) > 0 && parentRef.SectionName != parent.SectionName {
900
Reason: ParentErrorNotAccepted,
901
Message: fmt.Sprintf("sectionName %q not found", parentRef.SectionName),
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
909
if len(hostnames) == 0 {
910
hostnames = []k8s.Hostname{"*"}
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
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 {
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,
942
Reason: ParentErrorNoHostname,
943
Message: fmt.Sprintf(
944
"no hostnames matched parent hostname %q",
945
parent.OriginalHostname,
951
// Also make sure this route kind is allowed
953
for _, ak := range parent.AllowedKinds {
954
if string(ak.Kind) == routeKind.Kind && ptr.OrDefault((*string)(ak.Group), gvk.GatewayClass.Group) == routeKind.Group {
961
Reason: ParentErrorNotAllowed,
962
Message: fmt.Sprintf("kind %v is not allowed", routeKind),
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)
975
// Cannot handle the reference. Maybe it is for another controller, so we just ignore it
978
pk := parentReference{
980
SectionName: ptr.OrEmpty(ref.SectionName),
981
Port: ptr.OrEmpty(ref.Port),
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,
991
if rpi.DeniedReason == nil {
992
// Record that we were able to bind to the parent
995
parentRefs = append(parentRefs, rpi)
998
if ir.Kind == gvk.Service || ir.Kind == gvk.ServiceEntry {
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)
1006
// Ensure stable order
1007
slices.SortBy(parentRefs, func(a routeParentReference) string {
1008
return parentRefString(a.OriginalReference)
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)
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)
1024
type conversionResult struct {
1026
routes []*istio.TCPRoute
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
1035
return conversionResult{error: err}
1037
// Got an error but also routes
1041
res.routes = append(res.routes, vs)
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,
1053
res.RouteError = meshResult.error
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)
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))))
1072
vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s",
1073
parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)}
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{
1082
CreationTimestamp: obj.CreationTimestamp,
1083
GroupVersionKind: gvk.VirtualService,
1085
Annotations: routeMeta(obj),
1086
Namespace: obj.Namespace,
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},
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)
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)
1113
type conversionResult struct {
1115
routes []*istio.TLSRoute
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
1124
return conversionResult{error: err}
1126
// Got an error but also routes
1130
res.routes = append(res.routes, vs)
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,
1142
res.RouteError = meshResult.error
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))))
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}
1162
routes = augmentTLSPortMatch(routes, parent.OriginalReference.Port, vsHosts)
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)
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{
1175
CreationTimestamp: obj.CreationTimestamp,
1176
GroupVersionKind: gvk.VirtualService,
1178
Annotations: routeMeta(obj),
1179
Namespace: obj.Namespace,
1182
Spec: &istio.VirtualService{
1183
Hosts: []string{host},
1184
Gateways: []string{parent.InternalName},
1185
Tls: filteredRoutes,
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},
1208
dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TCPRoute)
1212
return &istio.TCPRoute{
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},
1232
dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TLSRoute)
1236
return &istio.TLSRoute{
1237
Match: buildTLSMatch(obj.Spec.(*k8s.TLSRouteSpec).Hostnames),
1242
func buildTCPDestination(
1244
forwardTo []k8s.BackendRef,
1246
enforceRefGrant bool,
1247
k config.GroupVersionKind,
1248
) ([]*istio.RouteDestination, *ConfigError, *ConfigError) {
1249
if forwardTo == nil {
1250
return nil, nil, nil
1254
action := []k8s.BackendRef{}
1255
for _, w := range forwardTo {
1256
wt := int(ptr.OrDefault(w.Weight, 1))
1260
action = append(action, w)
1261
weights = append(weights, wt)
1263
if len(weights) == 1 {
1267
var invalidBackendErr *ConfigError
1268
res := []*istio.RouteDestination{}
1269
for i, fwd := range action {
1270
dst, err := buildDestination(ctx, fwd, ns, enforceRefGrant, k)
1272
if isInvalidBackend(err) {
1273
invalidBackendErr = err
1274
// keep going, we will gracefully drop invalid backends
1276
return nil, nil, err
1279
res = append(res, &istio.RouteDestination{
1281
Weight: int32(weights[i]),
1284
return res, invalidBackendErr, nil
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),
1294
func hostnamesToStringListWithWildcard(h []k8s.Hostname) []string {
1296
return []string{"*"}
1298
res := make([]string, 0, len(h))
1299
for _, i := range h {
1300
res = append(res, string(i))
1305
func weightSum(forwardTo []k8s.HTTPBackendRef) int {
1307
for _, w := range forwardTo {
1308
sum += ptr.OrDefault(w.Weight, 1)
1313
func grpcWeightSum(forwardTo []k8s.GRPCBackendRef) int {
1315
for _, w := range forwardTo {
1316
sum += ptr.OrDefault(w.Weight, 1)
1321
func tcpWeightSum(forwardTo []k8s.BackendRef) int {
1323
for _, w := range forwardTo {
1324
sum += ptr.OrDefault(w.Weight, 1)
1329
func buildHTTPDestination(
1331
forwardTo []k8s.HTTPBackendRef,
1333
enforceRefGrant bool,
1334
) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) {
1335
if forwardTo == nil {
1336
return nil, nil, nil
1339
action := []k8s.HTTPBackendRef{}
1340
for _, w := range forwardTo {
1341
wt := int(ptr.OrDefault(w.Weight, 1))
1345
action = append(action, w)
1346
weights = append(weights, wt)
1348
if len(weights) == 1 {
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)
1357
if isInvalidBackend(err) {
1358
invalidBackendErr = err
1359
// keep going, we will gracefully drop invalid backends
1361
return nil, nil, err
1364
rd := &istio.HTTPRouteDestination{
1366
Weight: int32(weights[i]),
1368
for _, filter := range fwd.Filters {
1369
switch filter.Type {
1370
case k8sv1.HTTPRouteFilterRequestHeaderModifier:
1371
h := createHeadersFilter(filter.RequestHeaderModifier)
1375
if rd.Headers == nil {
1376
rd.Headers = &istio.Headers{}
1378
rd.Headers.Request = h
1379
case k8sv1.HTTPRouteFilterResponseHeaderModifier:
1380
h := createHeadersFilter(filter.ResponseHeaderModifier)
1384
if rd.Headers == nil {
1385
rd.Headers = &istio.Headers{}
1387
rd.Headers.Response = h
1389
return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
1392
res = append(res, rd)
1394
return res, invalidBackendErr, nil
1397
func buildGRPCDestination(
1399
forwardTo []k8s.GRPCBackendRef,
1401
enforceRefGrant bool,
1402
) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) {
1403
if forwardTo == nil {
1404
return nil, nil, nil
1407
action := []k8s.GRPCBackendRef{}
1408
for _, w := range forwardTo {
1409
wt := int(ptr.OrDefault(w.Weight, 1))
1413
action = append(action, w)
1414
weights = append(weights, wt)
1416
if len(weights) == 1 {
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)
1425
if isInvalidBackend(err) {
1426
invalidBackendErr = err
1427
// keep going, we will gracefully drop invalid backends
1429
return nil, nil, err
1432
rd := &istio.HTTPRouteDestination{
1434
Weight: int32(weights[i]),
1436
for _, filter := range fwd.Filters {
1437
switch filter.Type {
1438
case k8s.GRPCRouteFilterRequestHeaderModifier:
1439
h := createHeadersFilter(filter.RequestHeaderModifier)
1443
if rd.Headers == nil {
1444
rd.Headers = &istio.Headers{}
1446
rd.Headers.Request = h
1447
case k8s.GRPCRouteFilterResponseHeaderModifier:
1448
h := createHeadersFilter(filter.ResponseHeaderModifier)
1452
if rd.Headers == nil {
1453
rd.Headers = &istio.Headers{}
1455
rd.Headers.Response = h
1457
return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
1460
res = append(res, rd)
1462
return res, invalidBackendErr, nil
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),
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) {
1484
// "Port is required when the referent is a Kubernetes Service."
1485
return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
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."}
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)}
1494
return &istio.Destination{
1495
// TODO: implement ReferencePolicy for cross namespace
1497
Port: &istio.PortSelector{Number: uint32(*to.Port)},
1498
}, invalidBackendErr
1500
if nilOrEqual((*string)(to.Group), features.MCSAPIGroup) && nilOrEqual((*string)(to.Kind), "ServiceImport") {
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)
1509
// We don't know where to send without port
1510
return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
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."}
1515
if ctx.Context.GetService(hostname, namespace) == nil {
1516
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
1518
return &istio.Destination{
1520
Port: &istio.PortSelector{Number: uint32(*to.Port)},
1521
}, invalidBackendErr
1523
if nilOrEqual((*string)(to.Group), gvk.ServiceEntry.Group) && nilOrEqual((*string)(to.Kind), "Hostname") {
1524
// Hostname synthetic type
1526
// We don't know where to send without port
1527
return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
1529
if to.Namespace != nil {
1530
return nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"}
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)}
1536
return &istio.Destination{
1537
Host: string(to.Name),
1538
Port: &istio.PortSelector{Number: uint32(*to.Port)},
1539
}, invalidBackendErr
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)),
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
1554
func headerListToMap(hl []k8s.HTTPHeader) map[string]string {
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"
1570
func createMirrorFilter(ctx configContext, filter *k8s.HTTPRequestMirrorFilter, ns string,
1571
enforceRefGrant bool, k config.GroupVersionKind,
1572
) (*istio.HTTPMirrorPolicy, *ConfigError) {
1576
var weightOne int32 = 1
1577
dst, err := buildDestination(ctx, k8s.BackendRef{
1578
BackendObjectReference: filter.BackendRef,
1580
}, ns, enforceRefGrant, k)
1584
return &istio.HTTPMirrorPolicy{Destination: dst}, nil
1587
func createRewriteFilter(filter *k8s.HTTPURLRewriteFilter) *istio.HTTPRewrite {
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
1600
case k8sv1.FullPathHTTPPathModifier:
1601
rewrite.UriRegexRewrite = &istio.RegexRewrite{
1603
Rewrite: *filter.Path.ReplaceFullPath,
1607
if filter.Hostname != nil {
1608
rewrite.Authority = string(*filter.Hostname)
1611
if rewrite.Uri == "" && rewrite.UriRegexRewrite == nil && rewrite.Authority == "" {
1617
func createRedirectFilter(filter *k8s.HTTPRequestRedirectFilter) *istio.HTTPRedirect {
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)
1627
if filter.Hostname != nil {
1628
resp.Authority = string(*filter.Hostname)
1630
if filter.Scheme != nil {
1631
// Both allow http and https
1632
resp.Scheme = *filter.Scheme
1634
if filter.Port != nil {
1635
resp.RedirectPort = &istio.HTTPRedirect_Port{Port: uint32(*filter.Port)}
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}
1642
resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_REQUEST_PORT}
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)
1656
func createHeadersFilter(filter *k8s.HTTPHeaderFilter) *istio.Headers_HeaderOperations {
1660
return &istio.Headers_HeaderOperations{
1661
Add: headerListToMap(filter.Add),
1662
Remove: filter.Remove,
1663
Set: headerListToMap(filter.Set),
1668
func createMethodMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
1669
if match.Method == nil {
1672
return &istio.StringMatch{
1673
MatchType: &istio.StringMatch_Exact{Exact: string(*match.Method)},
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
1685
case k8sv1.QueryParamMatchExact:
1686
res[string(qp.Name)] = &istio.StringMatch{
1687
MatchType: &istio.StringMatch_Exact{Exact: qp.Value},
1689
case k8sv1.QueryParamMatchRegularExpression:
1690
res[string(qp.Name)] = &istio.StringMatch{
1691
MatchType: &istio.StringMatch_Regex{Regex: qp.Value},
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)}
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 {
1713
case k8sv1.HeaderMatchExact:
1714
res[string(header.Name)] = &istio.StringMatch{
1715
MatchType: &istio.StringMatch_Exact{Exact: header.Value},
1717
case k8sv1.HeaderMatchRegularExpression:
1718
res[string(header.Name)] = &istio.StringMatch{
1719
MatchType: &istio.StringMatch_Regex{Regex: header.Value},
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)}
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 {
1741
case k8sv1.HeaderMatchExact:
1742
res[string(header.Name)] = &istio.StringMatch{
1743
MatchType: &istio.StringMatch_Exact{Exact: header.Value},
1745
case k8sv1.HeaderMatchRegularExpression:
1746
res[string(header.Name)] = &istio.StringMatch{
1747
MatchType: &istio.StringMatch_Regex{Regex: header.Value},
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)}
1761
func createURIMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
1762
tp := k8sv1.PathMatchPathPrefix
1763
if match.Path.Type != nil {
1764
tp = *match.Path.Type
1767
if match.Path.Value != nil {
1768
dest = *match.Path.Value
1771
case k8sv1.PathMatchPathPrefix:
1772
// "When specified, a trailing `/` is ignored."
1774
dest = strings.TrimSuffix(dest, "/")
1776
return &istio.StringMatch{
1777
MatchType: &istio.StringMatch_Prefix{Prefix: dest},
1779
case k8sv1.PathMatchExact:
1780
return &istio.StringMatch{
1781
MatchType: &istio.StringMatch_Exact{Exact: dest},
1783
case k8sv1.PathMatchRegularExpression:
1784
return &istio.StringMatch{
1785
MatchType: &istio.StringMatch_Regex{Regex: dest},
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)}
1793
func createGRPCURIMatch(match k8s.GRPCRouteMatch) (*istio.StringMatch, *ConfigError) {
1798
tp := k8s.GRPCMethodMatchExact
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"}
1806
// gRPC format is /<Service>/<Method>. Since we don't natively understand this, convert to various string matches
1808
case k8s.GRPCMethodMatchExact:
1809
if m.Method == nil {
1810
return &istio.StringMatch{
1811
MatchType: &istio.StringMatch_Prefix{Prefix: fmt.Sprintf("/%s/", *m.Service)},
1814
if m.Service == nil {
1815
return &istio.StringMatch{
1816
MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)},
1819
return &istio.StringMatch{
1820
MatchType: &istio.StringMatch_Exact{Exact: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)},
1822
case k8s.GRPCMethodMatchRegularExpression:
1823
if m.Method == nil {
1824
return &istio.StringMatch{
1825
MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/.+", *m.Service)},
1828
if m.Service == nil {
1829
return &istio.StringMatch{
1830
MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)},
1833
return &istio.StringMatch{
1834
MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)},
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)}
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
1850
for _, obj := range r.GatewayClass {
1851
gwc := obj.Spec.(*k8s.GatewayClassSpec)
1852
_, known := classInfos[gwc.ControllerName]
1856
res[obj.Name] = gwc.ControllerName
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
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)
1876
// Namespace is the namespace of the resource
1880
type parentReference struct {
1883
SectionName k8s.SectionName
1884
Port k8sv1.PortNumber
1887
var meshGVK = config.GroupVersionKind{
1888
Group: gvk.KubernetesGateway.Group,
1889
Version: gvk.KubernetesGateway.Version,
1893
var meshParentKey = parentKey{
1898
type configContext struct {
1900
AllowedReferences AllowedReferences
1901
GatewayReferences map[parentKey][]*parentInfo
1903
// key: referenced resources(e.g. secrets), value: gateway-api resources(e.g. gateways)
1904
resourceReferences map[model.ConfigKey][]model.ConfigKey
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"
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
1917
// OriginalHostname is the unprocessed form of Hostnames; how it appeared in users' config
1918
OriginalHostname string
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
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"
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
1944
func (r routeParentReference) IsMesh() bool {
1945
return r.InternalName == "mesh"
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
1955
ret = append(ret, p)
1957
// To ensure deterministic order, sort them
1958
sort.Slice(ret, func(i, j int) bool {
1959
return ret[i].InternalName < ret[j].InternalName
1964
func getDefaultName(name string, kgw *k8s.GatewaySpec) string {
1965
return fmt.Sprintf("%v-%v", name, kgw.GatewayClassName)
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 {
1979
kgw := obj.Spec.(*k8s.GatewaySpec)
1980
controllerName, f := classes[string(kgw.GatewayClassName)]
1982
// No gateway class found, this may be meant for another controller; should be skipped.
1985
classInfo, f := classInfos[controllerName]
1989
if classInfo.disableRouteGeneration {
1990
// We found it, but don't want to handle this class
1994
servers := []*istio.Server{}
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)
2003
for i, l := range kgw.Listeners {
2005
namespaceLabelReferences.InsertAll(getNamespaceLabelReferences(l.AllowedRoutes)...)
2006
server, programmed := buildListener(r, obj, l, i, controllerName)
2008
servers = append(servers, server)
2009
if controllerName == constants.ManagedGatewayMeshController {
2010
// Waypoint doesn't actually convert the routes to VirtualServices
2013
meta := parentMeta(obj, &l.Name)
2014
meta[constants.InternalGatewaySemantics] = constants.GatewaySemanticsGateway
2015
meta[model.InternalGatewayServiceAnnotation] = strings.Join(gatewayServices, ",")
2017
// Each listener generates an Istio Gateway with a single Server. This allows binding to a specific listener.
2018
gatewayConfig := config.Config{
2020
CreationTimestamp: obj.CreationTimestamp,
2021
GroupVersionKind: gvk.Gateway,
2022
Name: kubeconfig.InternalGatewayName(obj.Name, string(l.Name)),
2024
Namespace: obj.Namespace,
2027
Spec: &istio.Gateway{
2028
Servers: []*istio.Server{server},
2032
Kind: gvk.KubernetesGateway,
2034
Namespace: obj.Namespace,
2036
if _, f := gwMap[ref]; !f {
2037
gwMap[ref] = []*parentInfo{}
2040
allowed, _ := generateSupportedKinds(l)
2042
InternalName: obj.Namespace + "/" + gatewayConfig.Name,
2043
AllowedKinds: allowed,
2044
Hostnames: server.Hosts,
2045
OriginalHostname: string(ptr.OrEmpty(l.Hostname)),
2046
SectionName: l.Name,
2049
pri.ReportAttachedRoutes = func() {
2050
reportListenerAttachedRoutes(i, obj, pri.AttachedRoutes)
2052
gwMap[ref] = append(gwMap[ref], pri)
2055
result = append(result, gatewayConfig)
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] != "" {
2066
Kind: gvk.KubernetesGateway,
2067
Name: obj.Annotations[gatewayAliasForAnnotationKey],
2068
Namespace: obj.Namespace,
2071
Kind: gvk.KubernetesGateway,
2073
Namespace: obj.Namespace,
2075
gwMap[ref] = gwMap[alias]
2078
reportGatewayStatus(r, obj, classInfo, gatewayServices, servers, err)
2080
// Insert a parent for Mesh references.
2081
gwMap[meshParentKey] = []*parentInfo{
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)},
2093
return result, gwMap, namespaceLabelReferences
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 {
2104
if l.Protocol != k8s.ProtocolType(protocol.HBONE) {
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 {
2118
func reportGatewayStatus(
2121
classInfo classInfo,
2122
gatewayServices []string,
2123
servers []*istio.Server,
2124
gatewayErr *ConfigError,
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)
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",
2139
string(k8sv1.GatewayConditionProgrammed): {
2140
reason: string(k8sv1.GatewayReasonProgrammed),
2141
message: "Resource programmed",
2145
if gatewayErr != nil {
2146
gatewayConditions[string(k8sv1.GatewayConditionAccepted)].error = gatewayErr
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
2154
if len(gatewayServices) == 0 {
2155
gatewayConditions[string(k8sv1.GatewayReasonProgrammed)].error = &ConfigError{
2156
Reason: InvalidAddress,
2157
Message: "Failed to assign to any requested addresses",
2159
} else if len(warnings) > 0 {
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, "; "))
2166
msg = fmt.Sprintf("Failed to assign to any requested addresses: %s", strings.Join(warnings, "; "))
2169
reason = string(k8sv1.GatewayReasonAddressNotAssigned)
2171
reason = string(k8sv1.GatewayReasonAddressNotUsable)
2173
gatewayConditions[string(k8sv1.GatewayConditionProgrammed)].error = &ConfigError{
2174
// TODO: this only checks Service ready, we should also check Deployment ready?
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)
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
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)
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
2208
addrType = k8s.HostnameAddressType
2210
gs.Addresses = append(gs.Addresses, k8sv1.GatewayStatusAddress{
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)
2225
gs.Listeners = listeners
2226
gs.Conditions = setConditions(obj.Generation, gs.Conditions, gatewayConditions)
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.
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
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,
2249
// which users can add to the Gateway.
2250
func IsManaged(gw *k8s.GatewaySpec) bool {
2251
if len(gw.Addresses) == 0 {
2254
if len(gw.Addresses) > 1 {
2257
if t := gw.Addresses[0].Type; t == nil || *t == k8s.IPAddressType {
2263
func extractGatewayServices(r GatewayResources, kgw *k8s.GatewaySpec, obj config.Config) ([]string, *ConfigError) {
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
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)
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
2280
if !strings.Contains(fqdn, ".") {
2281
// Short name, expand it
2282
fqdn = fmt.Sprintf("%s.%s.svc.%s", fqdn, obj.Namespace, r.Domain)
2284
gatewayServices = append(gatewayServices, fqdn)
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),
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),
2301
return gatewayServices, nil
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 {
2310
for k := range routes.Namespaces.Selector.MatchLabels {
2311
res = append(res, k)
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, "*")
2323
res = append(res, me.Key)
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",
2334
string(k8sv1.ListenerConditionProgrammed): {
2335
reason: string(k8sv1.ListenerReasonProgrammed),
2336
message: "No errors found",
2338
string(k8sv1.ListenerConditionConflicted): {
2339
reason: string(k8sv1.ListenerReasonNoConflicts),
2340
message: "No errors found",
2341
status: kstatus.StatusFalse,
2343
string(k8sv1.ListenerConditionResolvedRefs): {
2344
reason: string(k8sv1.ListenerReasonResolvedRefs),
2345
message: "No errors found",
2350
tls, err := buildTLS(r, l.TLS, obj, kube.IsAutoPassthrough(obj.Labels, l))
2352
listenerConditions[string(k8sv1.ListenerConditionResolvedRefs)].error = err
2353
listenerConditions[string(k8sv1.GatewayConditionProgrammed)].error = &ConfigError{
2354
Reason: string(k8sv1.GatewayReasonInvalid),
2355
Message: "Bad TLS configuration",
2359
hostnames := buildHostnameMatch(obj.Namespace, r.GatewayResources, l)
2360
server := &istio.Server{
2362
// Name is required. We only have one server per Gateway, so we can just name them all the same
2364
Number: uint32(l.Port),
2365
Protocol: listenerProtocolToIstio(l.Protocol),
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"`,
2379
reportListenerCondition(listenerIndex, l, obj, listenerConditions)
2383
func listenerProtocolToIstio(protocol k8s.ProtocolType) string {
2384
// Currently, all gateway-api protocols are valid Istio protocols.
2385
return string(protocol)
2388
func buildTLS(ctx configContext, tls *k8s.GatewayTLSConfig, gw config.Config, isAutoPassthrough bool) (*istio.ServerTLSSettings, *ConfigError) {
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,
2397
mode := k8sv1.TLSModeTerminate
2398
if tls.Mode != nil {
2401
namespace := gw.Namespace
2403
case k8sv1.TLSModeTerminate:
2404
out.Mode = istio.ServerTLSSettings_SIMPLE
2405
if tls.Options != nil {
2406
switch tls.Options[gatewayTLSTerminateModeKey] {
2408
out.Mode = istio.ServerTLSSettings_MUTUAL
2409
case "ISTIO_MUTUAL":
2410
out.Mode = istio.ServerTLSSettings_ISTIO_MUTUAL
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"}
2418
cred, err := buildSecretReference(ctx, tls.CertificateRefs[0], gw)
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,
2433
out.CredentialName = cred
2434
case k8sv1.TLSModePassthrough:
2435
out.Mode = istio.ServerTLSSettings_PASSTHROUGH
2436
if isAutoPassthrough {
2437
out.Mode = istio.ServerTLSSettings_AUTO_PASSTHROUGH
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))}
2448
secret := model.ConfigKey{
2450
Name: string(ref.Name),
2451
Namespace: ptr.OrDefault((*string)(ref.Namespace), gw.Namespace),
2454
ctx.resourceReferences[secret] = append(ctx.resourceReferences[secret], model.ConfigKey{
2455
Kind: kind.KubernetesGateway,
2456
Namespace: gw.Namespace,
2460
if ctx.Credentials != nil {
2461
if certInfo, err := ctx.Credentials.GetCertInfo(secret.Name, secret.Namespace); err != nil {
2462
return "", &ConfigError{
2464
Message: fmt.Sprintf("invalid certificate reference %v, %v", objectReferenceString(ref), err),
2466
} else if _, err = tls.X509KeyPair(certInfo.Cert, certInfo.Key); err != nil {
2467
return "", &ConfigError{
2469
Message: fmt.Sprintf("invalid certificate reference %v, the certificate is malformed: %v", objectReferenceString(ref), err),
2474
return creds.ToKubernetesGatewayResource(secret.Namespace, secret.Name), nil
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),
2482
ptr.OrEmpty(ref.Namespace))
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),
2490
ptr.OrEmpty(ref.SectionName),
2491
ptr.OrEmpty(ref.Port),
2492
ptr.OrEmpty(ref.Namespace))
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
2499
if l.Hostname != nil {
2500
hostname = string(*l.Hostname)
2504
for _, ns := range namespacesFromSelector(localNamespace, r, l.AllowedRoutes) {
2505
// This check is necessary to prevent adding a hostname with an invalid empty namespace
2507
resp = append(resp, fmt.Sprintf("%s/%s", ns, hostname))
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).
2515
return []string{"~/" + hostname}
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}
2526
if *lr.Namespaces.From == k8sv1.NamespacesFromAll {
2527
return []string{"*"}
2530
if lr.Namespaces.Selector == nil {
2531
// Should never happen, invalid config
2532
return []string{"*"}
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)
2542
namespaces := []string{}
2543
for _, ns := range r.Namespaces {
2544
if ls.Matches(toNamespaceSet(ns.Name, ns.Labels)) {
2545
namespaces = append(namespaces, ns.Name)
2548
// Ensure stable order
2549
sort.Strings(namespaces)
2553
func nilOrEqual(have *string, expected string) bool {
2554
return have == nil || *have == expected
2557
func humanReadableJoin(ss []string) string {
2564
return ss[0] + " and " + ss[1]
2566
return strings.Join(ss[:len(ss)-1], ", ") + ", and " + ss[len(ss)-1]
2570
// NamespaceNameLabel represents that label added automatically to namespaces is newer Kubernetes clusters
2571
const NamespaceNameLabel = "kubernetes.io/metadata.name"
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
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 {
2585
ret[NamespaceNameLabel] = name
2589
func (kr GatewayResources) FuzzValidate() bool {
2590
for _, gwc := range kr.GatewayClass {
2591
if gwc.Spec == nil {
2595
for _, rp := range kr.ReferenceGrant {
2600
for _, hr := range kr.HTTPRoute {
2605
for _, hr := range kr.GRPCRoute {
2610
for _, tr := range kr.TLSRoute {
2615
for _, g := range kr.Gateway {
2620
for _, tr := range kr.TCPRoute {