Files
higress/pkg/ingress/kube/gateway/istio/conversion.go
2025-11-26 10:15:00 +08:00

2698 lines
87 KiB
Go

// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package istio
import (
"cmp"
"crypto/tls"
"crypto/x509"
"fmt"
higressconfig "github.com/alibaba/higress/v2/pkg/config"
"github.com/alibaba/higress/v2/pkg/ingress/kube/util"
"istio.io/istio/pilot/pkg/credentials"
"net"
"path"
inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1"
"sort"
"strconv"
"strings"
"time"
"google.golang.org/protobuf/types/known/durationpb"
wrappers "google.golang.org/protobuf/types/known/wrapperspb"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
klabels "k8s.io/apimachinery/pkg/labels"
k8s "sigs.k8s.io/gateway-api/apis/v1"
k8salpha "sigs.k8s.io/gateway-api/apis/v1alpha2"
k8sbeta "sigs.k8s.io/gateway-api/apis/v1beta1"
gatewayx "sigs.k8s.io/gateway-api/apisx/v1alpha1"
"istio.io/api/label"
istio "istio.io/api/networking/v1alpha3"
kubecreds "istio.io/istio/pilot/pkg/credentials/kube"
"istio.io/istio/pilot/pkg/features"
"istio.io/istio/pilot/pkg/model"
creds "istio.io/istio/pilot/pkg/model/credentials"
"istio.io/istio/pilot/pkg/model/kstatus"
"istio.io/istio/pilot/pkg/serviceregistry/kube"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/constants"
"istio.io/istio/pkg/config/host"
"istio.io/istio/pkg/config/protocol"
"istio.io/istio/pkg/config/schema/collections"
"istio.io/istio/pkg/config/schema/gvk"
"istio.io/istio/pkg/config/schema/kind"
schematypes "istio.io/istio/pkg/config/schema/kubetypes"
"istio.io/istio/pkg/kube/controllers"
"istio.io/istio/pkg/kube/krt"
"istio.io/istio/pkg/ptr"
"istio.io/istio/pkg/slices"
"istio.io/istio/pkg/util/sets"
)
const (
gatewayTLSTerminateModeKey = "gateway.higress.io/tls-terminate-mode"
addressTypeOverride = "networking.higress.io/address-type"
gatewayClassDefaults = "gateway.higress.io/defaults-for-class"
gatewayNameOverride = "gateway.higress.io/name-override"
serviceTypeOverride = "networking.istio.io/service-type"
)
func sortConfigByCreationTime(configs []config.Config) {
sort.Slice(configs, func(i, j int) bool {
if r := configs[i].CreationTimestamp.Compare(configs[j].CreationTimestamp); r != 0 {
return r == -1 // -1 means i is less than j, so return true
}
if r := cmp.Compare(configs[i].Namespace, configs[j].Namespace); r != 0 {
return r == -1
}
return cmp.Compare(configs[i].Name, configs[j].Name) == -1
})
}
func sortRoutesByCreationTime(configs []RouteWithKey) {
sort.Slice(configs, func(i, j int) bool {
if r := configs[i].CreationTimestamp.Compare(configs[j].CreationTimestamp); r != 0 {
return r == -1 // -1 means i is less than j, so return true
}
if r := cmp.Compare(configs[i].Namespace, configs[j].Namespace); r != 0 {
return r == -1
}
return cmp.Compare(configs[i].Name, configs[j].Name) == -1
})
}
func sortedConfigByCreationTime(configs []config.Config) []config.Config {
sortConfigByCreationTime(configs)
return configs
}
func convertHTTPRoute(ctx RouteContext, r k8s.HTTPRouteRule,
obj *k8sbeta.HTTPRoute, pos int, enforceRefGrant bool,
) (*istio.HTTPRoute, *inferencePoolConfig, *ConfigError) {
vs := &istio.HTTPRoute{}
// Start - Modified by Higress
//if r.Name != nil {
// vs.Name = string(*r.Name)
//} else {
// // Auto-name the route. If upstream defines an explicit name, will use it instead
// // The position within the route is unique
// vs.Name = obj.Namespace + "." + obj.Name + "." + strconv.Itoa(pos) // format: %s.%s.%d
//}
// The best practice for Higress is to configure one HTTP route per route match.
vs.Name = generateRouteName(obj, "HTTP")
// End - Modified by Higress
for _, match := range r.Matches {
uri, err := createURIMatch(match)
if err != nil {
return nil, nil, err
}
headers, err := createHeadersMatch(match)
if err != nil {
return nil, nil, err
}
qp, err := createQueryParamsMatch(match)
if err != nil {
return nil, nil, err
}
method, err := createMethodMatch(match)
if err != nil {
return nil, nil, err
}
vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
Uri: uri,
Headers: headers,
QueryParams: qp,
Method: method,
})
}
var mirrorBackendErr *ConfigError
for _, filter := range r.Filters {
switch filter.Type {
case k8s.HTTPRouteFilterRequestHeaderModifier:
h := createHeadersFilter(filter.RequestHeaderModifier)
if h == nil {
continue
}
if vs.Headers == nil {
vs.Headers = &istio.Headers{}
}
vs.Headers.Request = h
case k8s.HTTPRouteFilterResponseHeaderModifier:
h := createHeadersFilter(filter.ResponseHeaderModifier)
if h == nil {
continue
}
if vs.Headers == nil {
vs.Headers = &istio.Headers{}
}
vs.Headers.Response = h
case k8s.HTTPRouteFilterRequestRedirect:
vs.Redirect = createRedirectFilter(filter.RequestRedirect)
case k8s.HTTPRouteFilterRequestMirror:
mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.HTTPRoute)
if err != nil {
mirrorBackendErr = err
} else {
vs.Mirrors = append(vs.Mirrors, mirror)
}
case k8s.HTTPRouteFilterURLRewrite:
vs.Rewrite = createRewriteFilter(filter.URLRewrite)
case k8s.HTTPRouteFilterCORS:
vs.CorsPolicy = createCorsFilter(filter.CORS)
default:
return nil, nil, &ConfigError{
Reason: InvalidFilter,
Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
}
}
}
if r.Retry != nil {
// "Implementations SHOULD retry on connection errors (disconnect, reset, timeout,
// TCP failure) if a retry stanza is configured."
retryOn := []string{"connect-failure", "refused-stream", "unavailable", "cancelled"}
for _, codes := range r.Retry.Codes {
retryOn = append(retryOn, strconv.Itoa(int(codes)))
}
vs.Retries = &istio.HTTPRetry{
// If unset, default is implementation specific.
// VirtualService.retry has no default when set -- users are expected to set it if they customize `retry`.
// However, the default retry if none are set is "2", so we use that as the default.
Attempts: int32(ptr.OrDefault(r.Retry.Attempts, 2)),
PerTryTimeout: nil,
RetryOn: strings.Join(retryOn, ","),
}
if vs.Retries.Attempts == 0 {
// Invalid to set this when there are no attempts
vs.Retries.RetryOn = ""
}
if r.Retry.Backoff != nil {
retrybackOff, _ := time.ParseDuration(string(*r.Retry.Backoff))
vs.Retries.Backoff = durationpb.New(retrybackOff)
}
}
if r.Timeouts != nil {
if r.Timeouts.Request != nil {
request, _ := time.ParseDuration(string(*r.Timeouts.Request))
if request != 0 {
vs.Timeout = durationpb.New(request)
}
}
if r.Timeouts.BackendRequest != nil {
backendRequest, _ := time.ParseDuration(string(*r.Timeouts.BackendRequest))
if backendRequest != 0 {
timeout := durationpb.New(backendRequest)
if vs.Retries != nil {
vs.Retries.PerTryTimeout = timeout
} else {
vs.Timeout = timeout
}
}
}
}
if weightSum(r.BackendRefs) == 0 && vs.Redirect == nil {
// The spec requires us to return 500 when there are no >0 weight backends
vs.DirectResponse = &istio.HTTPDirectResponse{
Status: 500,
}
} else {
route, ipCfg, backendErr, err := buildHTTPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant)
if err != nil {
return nil, nil, err
}
vs.Route = route
return vs, ipCfg, joinErrors(backendErr, mirrorBackendErr)
}
return vs, nil, mirrorBackendErr
}
func joinErrors(a *ConfigError, b *ConfigError) *ConfigError {
if b == nil {
return a
}
if a == nil {
return b
}
a.Message += "; " + b.Message
return a
}
func convertGRPCRoute(ctx RouteContext, r k8s.GRPCRouteRule,
obj *k8s.GRPCRoute, pos int, enforceRefGrant bool,
) (*istio.HTTPRoute, *ConfigError) { // Assuming GRPCRoute doesn't need inferencePoolConfig for now
vs := &istio.HTTPRoute{}
// Start - Modified by Higress
//if r.Name != nil {
// vs.Name = string(*r.Name)
//} else {
// // Auto-name the route. If upstream defines an explicit name, will use it instead
// // The position within the route is unique
// vs.Name = obj.Namespace + "." + obj.Name + "." + strconv.Itoa(pos) // format:%s.%s.%d
//}
// The best practice for Higress is to configure one HTTP route per route match.
vs.Name = generateRouteName(obj, "GRPC")
// End - Modified by Higress
for _, match := range r.Matches {
uri, err := createGRPCURIMatch(match)
if err != nil {
return nil, err
}
headers, err := createGRPCHeadersMatch(match)
if err != nil {
return nil, err
}
vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
Uri: uri,
Headers: headers,
})
}
for _, filter := range r.Filters {
switch filter.Type {
case k8s.GRPCRouteFilterRequestHeaderModifier:
h := createHeadersFilter(filter.RequestHeaderModifier)
if h == nil {
continue
}
if vs.Headers == nil {
vs.Headers = &istio.Headers{}
}
vs.Headers.Request = h
case k8s.GRPCRouteFilterResponseHeaderModifier:
h := createHeadersFilter(filter.ResponseHeaderModifier)
if h == nil {
continue
}
if vs.Headers == nil {
vs.Headers = &istio.Headers{}
}
vs.Headers.Response = h
case k8s.GRPCRouteFilterRequestMirror:
mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.GRPCRoute)
if err != nil {
return nil, err
}
vs.Mirrors = append(vs.Mirrors, mirror)
default:
return nil, &ConfigError{
Reason: InvalidFilter,
Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
}
}
}
if grpcWeightSum(r.BackendRefs) == 0 && vs.Redirect == nil {
// The spec requires us to return 500 when there are no >0 weight backends
vs.DirectResponse = &istio.HTTPDirectResponse{
Status: 500,
}
} else {
route, backendErr, err := buildGRPCDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant)
if err != nil {
return nil, err
}
vs.Route = route
return vs, backendErr
}
return vs, nil
}
func parentTypes(rpi []routeParentReference) (mesh, gateway bool) {
for _, r := range rpi {
if r.IsMesh() {
mesh = true
} else {
gateway = true
}
}
return mesh, gateway
}
func augmentPortMatch(routes []*istio.HTTPRoute, port k8s.PortNumber) []*istio.HTTPRoute {
res := make([]*istio.HTTPRoute, 0, len(routes))
for _, r := range routes {
r = r.DeepCopy()
for _, m := range r.Match {
m.Port = uint32(port)
}
if len(r.Match) == 0 {
r.Match = []*istio.HTTPMatchRequest{{
Port: uint32(port),
}}
}
res = append(res, r)
}
return res
}
func augmentTCPPortMatch(routes []*istio.TCPRoute, port k8s.PortNumber) []*istio.TCPRoute {
res := make([]*istio.TCPRoute, 0, len(routes))
for _, r := range routes {
r = r.DeepCopy()
for _, m := range r.Match {
m.Port = uint32(port)
}
if len(r.Match) == 0 {
r.Match = []*istio.L4MatchAttributes{{
Port: uint32(port),
}}
}
res = append(res, r)
}
return res
}
func augmentTLSPortMatch(routes []*istio.TLSRoute, port *k8s.PortNumber, parentHosts []string) []*istio.TLSRoute {
res := make([]*istio.TLSRoute, 0, len(routes))
for _, r := range routes {
r = r.DeepCopy()
if len(r.Match) == 1 && slices.Equal(r.Match[0].SniHosts, []string{"*"}) {
// For mesh, we use parent hosts for SNI if TLSRroute.hostnames were not specified.
r.Match[0].SniHosts = parentHosts
}
for _, m := range r.Match {
if port != nil {
m.Port = uint32(*port)
}
}
res = append(res, r)
}
return res
}
func compatibleRoutesForHost(routes []*istio.TLSRoute, parentHost string) []*istio.TLSRoute {
res := make([]*istio.TLSRoute, 0, len(routes))
for _, r := range routes {
if len(r.Match) == 1 && len(r.Match[0].SniHosts) > 1 {
r = r.DeepCopy()
sniHosts := []string{}
for _, h := range r.Match[0].SniHosts {
if host.Name(parentHost).Matches(host.Name(h)) {
sniHosts = append(sniHosts, h)
}
}
r.Match[0].SniHosts = sniHosts
}
res = append(res, r)
}
return res
}
func routeMeta(obj controllers.Object) map[string]string {
m := parentMeta(obj, nil)
m[constants.InternalRouteSemantics] = constants.RouteSemanticsGateway
return m
}
// sortHTTPRoutes sorts generated vs routes to meet gateway-api requirements
// see https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRouteRule
func sortHTTPRoutes(routes []*istio.HTTPRoute) {
sort.SliceStable(routes, func(i, j int) bool {
if len(routes[i].Match) == 0 {
return false
} else if len(routes[j].Match) == 0 {
return true
}
// Start - Added by Higress
if isCatchAllMatch(routes[i].Match[0]) {
return false
} else if isCatchAllMatch(routes[j].Match[0]) {
return true
}
// End - Added by Higress
// Only look at match[0], we always generate only one match
m1, m2 := routes[i].Match[0], routes[j].Match[0]
r1, r2 := getURIRank(m1), getURIRank(m2)
len1, len2 := getURILength(m1), getURILength(m2)
switch {
// 1: Exact/Prefix/Regex
case r1 != r2:
return r1 > r2
case len1 != len2:
return len1 > len2
// 2: method math
case (m1.Method == nil) != (m2.Method == nil):
return m1.Method != nil
// 3: number of header matches
case len(m1.Headers) != len(m2.Headers):
return len(m1.Headers) > len(m2.Headers)
// 4: number of query matches
default:
return len(m1.QueryParams) > len(m2.QueryParams)
}
})
}
func parentMeta(obj controllers.Object, sectionName *k8s.SectionName) map[string]string {
name := fmt.Sprintf("%s/%s.%s", schematypes.GvkFromObject(obj).Kind, obj.GetName(), obj.GetNamespace())
if sectionName != nil {
name = fmt.Sprintf("%s/%s/%s.%s", schematypes.GvkFromObject(obj).Kind, obj.GetName(), *sectionName, obj.GetNamespace())
}
return map[string]string{
constants.InternalParentNames: name,
}
}
// getURIRank ranks a URI match type. Exact > Prefix > Regex
func getURIRank(match *istio.HTTPMatchRequest) int {
if match.Uri == nil {
return -1
}
switch match.Uri.MatchType.(type) {
case *istio.StringMatch_Exact:
return 3
case *istio.StringMatch_Prefix:
return 2
case *istio.StringMatch_Regex:
return 1
}
// should not happen
return -1
}
func getURILength(match *istio.HTTPMatchRequest) int {
if match.Uri == nil {
return 0
}
switch match.Uri.MatchType.(type) {
case *istio.StringMatch_Prefix:
return len(match.Uri.GetPrefix())
case *istio.StringMatch_Exact:
return len(match.Uri.GetExact())
case *istio.StringMatch_Regex:
return len(match.Uri.GetRegex())
}
// should not happen
return -1
}
func hostnameToStringList(h []k8s.Hostname) []string {
// In the Istio API, empty hostname is not allowed. In the Kubernetes API hosts means "any"
if len(h) == 0 {
return []string{"*"}
}
return slices.Map(h, func(e k8s.Hostname) string {
return string(e)
})
}
var allowedParentReferences = sets.New(
gvk.KubernetesGateway,
gvk.Service,
gvk.ServiceEntry,
gvk.XListenerSet,
)
func toInternalParentReference(p k8s.ParentReference, localNamespace string) (parentKey, error) {
ref := normalizeReference(p.Group, p.Kind, gvk.KubernetesGateway)
if !allowedParentReferences.Contains(ref) {
return parentKey{}, fmt.Errorf("unsupported parent: %v/%v", p.Group, p.Kind)
}
return parentKey{
Kind: ref,
Name: string(p.Name),
// Unset namespace means "same namespace"
Namespace: defaultString(p.Namespace, localNamespace),
}, nil
}
// waypointConfigured returns true if a waypoint is configured via expected label's key-value pair.
func waypointConfigured(labels map[string]string) bool {
if val, ok := labels[label.IoIstioUseWaypoint.Name]; ok && len(val) > 0 && !strings.EqualFold(val, "none") {
return true
}
return false
}
func referenceAllowed(
ctx RouteContext,
parent *parentInfo,
routeKind config.GroupVersionKind,
parentRef parentReference,
hostnames []k8s.Hostname,
localNamespace string,
) (*ParentError, *WaypointError) {
if parentRef.Kind == gvk.Service {
key := parentRef.Namespace + "/" + parentRef.Name
svc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key)))
// check that the referenced svc exists
if svc == nil {
return &ParentError{
Reason: ParentErrorNotAccepted,
Message: fmt.Sprintf("parent service: %q not found", parentRef.Name),
}, &WaypointError{
Reason: WaypointErrorReasonNoMatchingParent,
Message: WaypointErrorMsgNoMatchingParent,
}
}
// check that the reference has the use-waypoint label
if !waypointConfigured(svc.Labels) {
// if reference does not have use-waypoint label, check the namespace of the reference
ns := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Namespaces, krt.FilterKey(svc.Namespace)))
if ns != nil {
if !waypointConfigured(ns.Labels) {
return nil, &WaypointError{
Reason: WaypointErrorReasonMissingLabel,
Message: WaypointErrorMsgMissingLabel,
}
}
}
}
} else if parentRef.Kind == gvk.ServiceEntry {
// check that the referenced svc entry exists
key := parentRef.Namespace + "/" + parentRef.Name
svcEntry := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(key)))
if svcEntry == nil {
return &ParentError{
Reason: ParentErrorNotAccepted,
Message: fmt.Sprintf("parent service entry: %q not found", parentRef.Name),
}, &WaypointError{
Reason: WaypointErrorReasonNoMatchingParent,
Message: WaypointErrorMsgNoMatchingParent,
}
}
// check that the reference has the use-waypoint label
if !waypointConfigured(svcEntry.Labels) {
// if reference does not have use-waypoint label, check the namespace of the reference
ns := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Namespaces, krt.FilterKey(parentRef.Namespace)))
if ns != nil {
if !waypointConfigured(ns.Labels) {
return nil, &WaypointError{
Reason: WaypointErrorReasonMissingLabel,
Message: WaypointErrorMsgMissingLabel,
}
}
}
}
} else {
// First, check section and port apply. This must come first
if parentRef.Port != 0 && parentRef.Port != parent.Port {
return &ParentError{
Reason: ParentErrorNotAccepted,
Message: fmt.Sprintf("port %v not found", parentRef.Port),
}, nil
}
if len(parentRef.SectionName) > 0 && parentRef.SectionName != parent.SectionName {
return &ParentError{
Reason: ParentErrorNotAccepted,
Message: fmt.Sprintf("sectionName %q not found", parentRef.SectionName),
}, nil
}
// Next check the hostnames are a match. This is a bi-directional wildcard match. Only one route
// hostname must match for it to be allowed (but the others will be filtered at runtime)
// If either is empty its treated as a wildcard which always matches
if len(hostnames) == 0 {
hostnames = []k8s.Hostname{"*"}
}
if len(parent.Hostnames) > 0 {
// TODO: the spec actually has a label match, not a string match. That is, *.com does not match *.apple.com
// We are doing a string match here
matched := false
hostMatched := false
out:
for _, routeHostname := range hostnames {
for _, parentHostNamespace := range parent.Hostnames {
var parentNamespace, parentHostname string
// When parentHostNamespace lacks a '/', it was likely sanitized from '*/host' to 'host'
// by sanitizeServerHostNamespace. Set parentNamespace to '*' to reflect the wildcard namespace
// and parentHostname to the sanitized host to prevent an index out of range panic.
if strings.Contains(parentHostNamespace, "/") {
spl := strings.Split(parentHostNamespace, "/")
parentNamespace, parentHostname = spl[0], spl[1]
} else {
parentNamespace, parentHostname = "*", parentHostNamespace
}
hostnameMatch := host.Name(parentHostname).Matches(host.Name(routeHostname))
namespaceMatch := parentNamespace == "*" || parentNamespace == localNamespace
hostMatched = hostMatched || hostnameMatch
if hostnameMatch && namespaceMatch {
matched = true
break out
}
}
}
if !matched {
if hostMatched {
return &ParentError{
Reason: ParentErrorNotAllowed,
Message: fmt.Sprintf(
"hostnames matched parent hostname %q, but namespace %q is not allowed by the parent",
parent.OriginalHostname, localNamespace,
),
}, nil
}
return &ParentError{
Reason: ParentErrorNoHostname,
Message: fmt.Sprintf(
"no hostnames matched parent hostname %q",
parent.OriginalHostname,
),
}, nil
}
}
}
// Also make sure this route kind is allowed
matched := false
for _, ak := range parent.AllowedKinds {
if string(ak.Kind) == routeKind.Kind && ptr.OrDefault((*string)(ak.Group), gvk.GatewayClass.Group) == routeKind.Group {
matched = true
break
}
}
if !matched {
return &ParentError{
Reason: ParentErrorNotAllowed,
Message: fmt.Sprintf("kind %v is not allowed", routeKind),
}, nil
}
return nil, nil
}
func extractParentReferenceInfo(ctx RouteContext, parents RouteParents, obj controllers.Object) []routeParentReference {
routeRefs, hostnames, kind := GetCommonRouteInfo(obj)
localNamespace := obj.GetNamespace()
parentRefs := []routeParentReference{}
for _, ref := range routeRefs {
ir, err := toInternalParentReference(ref, localNamespace)
if err != nil {
// Cannot handle the reference. Maybe it is for another controller, so we just ignore it
continue
}
pk := parentReference{
parentKey: ir,
SectionName: ptr.OrEmpty(ref.SectionName),
Port: ptr.OrEmpty(ref.Port),
}
gk := ir
if ir.Kind == gvk.Service || ir.Kind == gvk.ServiceEntry {
gk = meshParentKey
}
currentParents := parents.fetch(ctx.Krt, gk)
appendParent := func(pr *parentInfo, pk parentReference) {
bannedHostnames := sets.New[string]()
for _, gw := range currentParents {
if gw == pr {
continue // do not ban ourself
}
if gw.Port != pr.Port {
// We only care about listeners on the same port
continue
}
if gw.Protocol != pr.Protocol {
// We only care about listeners on the same protocol
continue
}
bannedHostnames.Insert(gw.OriginalHostname)
}
deniedReason, waypointError := referenceAllowed(ctx, pr, kind, pk, hostnames, localNamespace)
rpi := routeParentReference{
InternalName: pr.InternalName,
InternalKind: ir.Kind,
Hostname: pr.OriginalHostname,
DeniedReason: deniedReason,
OriginalReference: ref,
BannedHostnames: bannedHostnames.Copy().Delete(pr.OriginalHostname),
ParentKey: ir,
ParentSection: pr.SectionName,
WaypointError: waypointError,
}
parentRefs = append(parentRefs, rpi)
}
for _, gw := range currentParents {
// Append all matches. Note we may be adding mismatch section or ports; this is handled later
appendParent(gw, pk)
}
}
// Ensure stable order
slices.SortBy(parentRefs, func(a routeParentReference) string {
return parentRefString(a.OriginalReference, localNamespace)
})
return parentRefs
}
func convertTCPRoute(ctx RouteContext, r k8salpha.TCPRouteRule, obj *k8salpha.TCPRoute, enforceRefGrant bool) (*istio.TCPRoute, *ConfigError) {
if tcpWeightSum(r.BackendRefs) == 0 {
// The spec requires us to reject connections when there are no >0 weight backends
// We don't have a great way to do it. TODO: add a fault injection API for TCP?
return &istio.TCPRoute{
Route: []*istio.RouteDestination{{
Destination: &istio.Destination{
Host: "internal.cluster.local",
Subset: "zero-weight",
Port: &istio.PortSelector{Number: 65535},
},
Weight: 0,
}},
}, nil
}
dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TCPRoute)
if err != nil {
return nil, err
}
return &istio.TCPRoute{
Route: dest,
}, backendErr
}
func convertTLSRoute(ctx RouteContext, r k8salpha.TLSRouteRule, obj *k8salpha.TLSRoute, enforceRefGrant bool) (*istio.TLSRoute, *ConfigError) {
if tcpWeightSum(r.BackendRefs) == 0 {
// The spec requires us to reject connections when there are no >0 weight backends
// We don't have a great way to do it. TODO: add a fault injection API for TCP?
return &istio.TLSRoute{
Route: []*istio.RouteDestination{{
Destination: &istio.Destination{
Host: "internal.cluster.local",
Subset: "zero-weight",
Port: &istio.PortSelector{Number: 65535},
},
Weight: 0,
}},
}, nil
}
dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TLSRoute)
if err != nil {
return nil, err
}
return &istio.TLSRoute{
Match: buildTLSMatch(obj.Spec.Hostnames),
Route: dest,
}, backendErr
}
func buildTCPDestination(
ctx RouteContext,
forwardTo []k8s.BackendRef,
ns string,
enforceRefGrant bool,
k config.GroupVersionKind,
) ([]*istio.RouteDestination, *ConfigError, *ConfigError) {
if forwardTo == nil {
return nil, nil, nil
}
weights := []int{}
action := []k8s.BackendRef{}
for _, w := range forwardTo {
wt := int(ptr.OrDefault(w.Weight, 1))
if wt == 0 {
continue
}
action = append(action, w)
weights = append(weights, wt)
}
if len(weights) == 1 {
weights = []int{0}
}
var invalidBackendErr *ConfigError
res := []*istio.RouteDestination{}
for i, fwd := range action {
dst, _, err := buildDestination(ctx, fwd, ns, enforceRefGrant, k)
if err != nil {
if isInvalidBackend(err) {
invalidBackendErr = err
// keep going, we will gracefully drop invalid backends
} else {
return nil, nil, err
}
}
res = append(res, &istio.RouteDestination{
Destination: dst,
Weight: int32(weights[i]),
})
}
return res, invalidBackendErr, nil
}
func buildTLSMatch(hostnames []k8s.Hostname) []*istio.TLSMatchAttributes {
// Currently, the spec only supports extensions beyond hostname, which are not currently implemented by Istio.
return []*istio.TLSMatchAttributes{{
SniHosts: hostnamesToStringListWithWildcard(hostnames),
}}
}
func hostnamesToStringListWithWildcard(h []k8s.Hostname) []string {
if len(h) == 0 {
return []string{"*"}
}
res := make([]string, 0, len(h))
for _, i := range h {
res = append(res, string(i))
}
return res
}
func weightSum(forwardTo []k8s.HTTPBackendRef) int {
sum := int32(0)
for _, w := range forwardTo {
sum += ptr.OrDefault(w.Weight, 1)
}
return int(sum)
}
func grpcWeightSum(forwardTo []k8s.GRPCBackendRef) int {
sum := int32(0)
for _, w := range forwardTo {
sum += ptr.OrDefault(w.Weight, 1)
}
return int(sum)
}
func tcpWeightSum(forwardTo []k8s.BackendRef) int {
sum := int32(0)
for _, w := range forwardTo {
sum += ptr.OrDefault(w.Weight, 1)
}
return int(sum)
}
func buildHTTPDestination(
ctx RouteContext,
forwardTo []k8s.HTTPBackendRef,
ns string,
enforceRefGrant bool,
) ([]*istio.HTTPRouteDestination, *inferencePoolConfig, *ConfigError, *ConfigError) {
if forwardTo == nil {
return nil, nil, nil, nil
}
weights := []int{}
action := []k8s.HTTPBackendRef{}
for _, w := range forwardTo {
wt := int(ptr.OrDefault(w.Weight, 1))
if wt == 0 {
continue
}
action = append(action, w)
weights = append(weights, wt)
}
if len(weights) == 1 {
weights = []int{0}
}
var invalidBackendErr *ConfigError
var ipCfg *inferencePoolConfig
res := []*istio.HTTPRouteDestination{}
for i, fwd := range action {
dst, ipconfig, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.HTTPRoute)
ipCfg = ipconfig
if err != nil {
if isInvalidBackend(err) {
invalidBackendErr = err
// keep going, we will gracefully drop invalid backends
} else {
return nil, ipCfg, nil, err
}
}
rd := &istio.HTTPRouteDestination{
Destination: dst,
Weight: int32(weights[i]),
}
for _, filter := range fwd.Filters {
switch filter.Type {
case k8s.HTTPRouteFilterRequestHeaderModifier:
h := createHeadersFilter(filter.RequestHeaderModifier)
if h == nil {
continue
}
if rd.Headers == nil {
rd.Headers = &istio.Headers{}
}
rd.Headers.Request = h
case k8s.HTTPRouteFilterResponseHeaderModifier:
h := createHeadersFilter(filter.ResponseHeaderModifier)
if h == nil {
continue
}
if rd.Headers == nil {
rd.Headers = &istio.Headers{}
}
rd.Headers.Response = h
default:
return nil, ipCfg, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
}
}
res = append(res, rd)
}
return res, ipCfg, invalidBackendErr, nil
}
func buildGRPCDestination(
ctx RouteContext,
forwardTo []k8s.GRPCBackendRef,
ns string,
enforceRefGrant bool,
) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) {
if forwardTo == nil {
return nil, nil, nil
}
weights := []int{}
action := []k8s.GRPCBackendRef{}
for _, w := range forwardTo {
wt := int(ptr.OrDefault(w.Weight, 1))
if wt == 0 {
continue
}
action = append(action, w)
weights = append(weights, wt)
}
if len(weights) == 1 {
weights = []int{0}
}
var invalidBackendErr *ConfigError
res := []*istio.HTTPRouteDestination{}
for i, fwd := range action {
dst, _, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.GRPCRoute)
if err != nil {
if isInvalidBackend(err) {
invalidBackendErr = err
// keep going, we will gracefully drop invalid backends
} else {
return nil, nil, err
}
}
rd := &istio.HTTPRouteDestination{
Destination: dst,
Weight: int32(weights[i]),
}
for _, filter := range fwd.Filters {
switch filter.Type {
case k8s.GRPCRouteFilterRequestHeaderModifier:
h := createHeadersFilter(filter.RequestHeaderModifier)
if h == nil {
continue
}
if rd.Headers == nil {
rd.Headers = &istio.Headers{}
}
rd.Headers.Request = h
case k8s.GRPCRouteFilterResponseHeaderModifier:
h := createHeadersFilter(filter.ResponseHeaderModifier)
if h == nil {
continue
}
if rd.Headers == nil {
rd.Headers = &istio.Headers{}
}
rd.Headers.Response = h
default:
return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
}
}
res = append(res, rd)
}
return res, invalidBackendErr, nil
}
type inferencePoolConfig struct {
enableExtProc bool
endpointPickerDst string
endpointPickerPort string
endpointPickerFailureMode string
}
func buildDestination(ctx RouteContext, to k8s.BackendRef, ns string,
enforceRefGrant bool, k config.GroupVersionKind,
) (*istio.Destination, *inferencePoolConfig, *ConfigError) {
ref := normalizeReference(to.Group, to.Kind, gvk.Service)
// check if the reference is allowed
if enforceRefGrant {
if toNs := to.Namespace; toNs != nil && string(*toNs) != ns {
if !ctx.Grants.BackendAllowed(ctx.Krt, k, ref, to.Name, *toNs, ns) {
return &istio.Destination{}, nil, &ConfigError{
Reason: InvalidDestinationPermit,
Message: fmt.Sprintf("backendRef %v/%v not accessible to a %s in namespace %q (missing a ReferenceGrant?)", to.Name, *toNs, k.Kind, ns),
}
}
}
}
namespace := ptr.OrDefault((*string)(to.Namespace), ns)
var invalidBackendErr *ConfigError
var hostname string
switch ref {
case gvk.Service:
if strings.Contains(string(to.Name), ".") {
return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "service name invalid; the name of the Service must be used, not the hostname."}
}
hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.DomainSuffix)
// Start - Updated by Higress
//key := namespace + "/" + string(to.Name)
//svc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key)))
svc := ctx.LookupHostname(hostname, namespace, "Service")
// End - Updated by Higress
if svc == nil {
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
}
case config.GroupVersionKind{Group: gvk.ServiceEntry.Group, Kind: "Hostname"}:
if to.Namespace != nil {
return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"}
}
hostname = string(to.Name)
// Start - Updated by Higress
if ctx.LookupHostname(hostname, namespace, "Hostname") == nil {
// End - Updated by Higress
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
}
case config.GroupVersionKind{Group: features.MCSAPIGroup, Kind: "ServiceImport"}:
hostname = fmt.Sprintf("%s.%s.svc.clusterset.local", to.Name, namespace)
if !features.EnableMCSHost {
// They asked for ServiceImport, but actually don't have full support enabled...
// No problem, we can just treat it as Service, which is already cross-cluster in this mode anyways
hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.DomainSuffix)
}
// TODO: currently we are always looking for Service. We should be looking for ServiceImport when features.EnableMCSHost
// Start - Updated by Higress
//key := namespace + "/" + string(to.Name)
//svc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key)))
svc := ctx.LookupHostname(hostname, namespace, "ServiceImport")
// End - Updated by Higress
if svc == nil {
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
}
case gvk.InferencePool:
if !features.EnableGatewayAPIInferenceExtension {
return &istio.Destination{}, nil, &ConfigError{
Reason: InvalidDestinationKind,
Message: "InferencePool is not enabled. To enable, set ENABLE_GATEWAY_API_INFERENCE_EXTENSION to true in istiod",
}
}
if strings.Contains(string(to.Name), ".") {
return nil, nil, &ConfigError{
Reason: InvalidDestination,
Message: "InferencePool.Name invalid; the name of the InferencePool must be used, not the hostname.",
}
}
infPool := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.InferencePools, krt.FilterKey(namespace+"/"+string(to.Name))))
if infPool == nil {
// Inference pool doesn't exist
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", to.Name)}
return &istio.Destination{}, nil, invalidBackendErr
}
inferencePoolServiceName, _ := InferencePoolServiceName(string(to.Name))
hostname := fmt.Sprintf("%s.%s.svc.%s", inferencePoolServiceName, namespace, ctx.DomainSuffix)
svc := ctx.LookupHostname(hostname, namespace, "InferencePool")
if svc == nil {
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
return &istio.Destination{}, nil, invalidBackendErr
}
if svc.Attributes.Labels == nil {
invalidBackendErr = &ConfigError{Reason: InvalidDestination, Message: "InferencePool service invalid, extensionRef labels not found"}
return &istio.Destination{}, nil, invalidBackendErr
}
ipCfg := &inferencePoolConfig{
enableExtProc: true,
}
if dst, ok := svc.Attributes.Labels[InferencePoolExtensionRefSvc]; ok {
ipCfg.endpointPickerDst = fmt.Sprintf("%s.%s.svc.%s", dst, infPool.Namespace, ctx.DomainSuffix)
}
if p, ok := svc.Attributes.Labels[InferencePoolExtensionRefPort]; ok {
ipCfg.endpointPickerPort = p
}
if fm, ok := svc.Attributes.Labels[InferencePoolExtensionRefFailureMode]; ok {
ipCfg.endpointPickerFailureMode = fm
}
if ipCfg.endpointPickerDst == "" || ipCfg.endpointPickerPort == "" || ipCfg.endpointPickerFailureMode == "" {
invalidBackendErr = &ConfigError{Reason: InvalidDestination, Message: "InferencePool service invalid, extensionRef labels not found"}
}
return &istio.Destination{
Host: hostname,
// Port: &istio.PortSelector{Number: uint32(*to.Port)},
}, ipCfg, invalidBackendErr
default:
return &istio.Destination{}, nil, &ConfigError{
Reason: InvalidDestinationKind,
Message: fmt.Sprintf("referencing unsupported backendRef: group %q kind %q", ptr.OrEmpty(to.Group), ptr.OrEmpty(to.Kind)),
}
}
// Start - Added by Higress
if equal((*string)(to.Group), "networking.higress.io") && nilOrEqual((*string)(to.Kind), "Service") {
var port *istio.PortSelector
if to.Port != nil {
port = &istio.PortSelector{Number: uint32(*to.Port)}
}
return &istio.Destination{
Host: string(to.Name),
Port: port,
}, nil, nil
}
// End - Added by Higress
// All types currently require a Port, so we do this for everything; consider making this per-type if we have future types
// that do not require port.
if to.Port == nil {
// "Port is required when the referent is a Kubernetes Service."
return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
}
return &istio.Destination{
Host: hostname,
Port: &istio.PortSelector{Number: uint32(*to.Port)},
}, nil, invalidBackendErr
}
// https://github.com/kubernetes-sigs/gateway-api/blob/cea484e38e078a2c1997d8c7a62f410a1540f519/apis/v1beta1/httproute_types.go#L207-L212
func isInvalidBackend(err *ConfigError) bool {
return err.Reason == InvalidDestinationPermit ||
err.Reason == InvalidDestinationNotFound ||
err.Reason == InvalidDestinationKind
}
func headerListToMap(hl []k8s.HTTPHeader) map[string]string {
if len(hl) == 0 {
return nil
}
res := map[string]string{}
for _, e := range hl {
k := strings.ToLower(string(e.Name))
if _, f := res[k]; f {
// "Subsequent entries with an equivalent header name MUST be ignored"
continue
}
res[k] = e.Value
}
return res
}
func createMirrorFilter(ctx RouteContext, filter *k8s.HTTPRequestMirrorFilter, ns string,
enforceRefGrant bool, k config.GroupVersionKind,
) (*istio.HTTPMirrorPolicy, *ConfigError) {
if filter == nil {
return nil, nil
}
var weightOne int32 = 1
dst, _, err := buildDestination(ctx, k8s.BackendRef{
BackendObjectReference: filter.BackendRef,
Weight: &weightOne,
}, ns, enforceRefGrant, k)
if err != nil {
return nil, err
}
var percent *istio.Percent
if f := filter.Fraction; f != nil {
percent = &istio.Percent{Value: (100 * float64(f.Numerator)) / float64(ptr.OrDefault(f.Denominator, int32(100)))}
} else if p := filter.Percent; p != nil {
percent = &istio.Percent{Value: float64(*p)}
}
return &istio.HTTPMirrorPolicy{Destination: dst, Percentage: percent}, nil
}
func createRewriteFilter(filter *k8s.HTTPURLRewriteFilter) *istio.HTTPRewrite {
if filter == nil {
return nil
}
rewrite := &istio.HTTPRewrite{}
if filter.Path != nil {
switch filter.Path.Type {
case k8s.PrefixMatchHTTPPathModifier:
rewrite.Uri = strings.TrimSuffix(*filter.Path.ReplacePrefixMatch, "/")
if rewrite.Uri == "" {
// `/` means removing the prefix
rewrite.Uri = "/"
}
case k8s.FullPathHTTPPathModifier:
rewrite.UriRegexRewrite = &istio.RegexRewrite{
Match: "/.*",
Rewrite: *filter.Path.ReplaceFullPath,
}
}
}
if filter.Hostname != nil {
rewrite.Authority = string(*filter.Hostname)
}
// Nothing done
if rewrite.Uri == "" && rewrite.UriRegexRewrite == nil && rewrite.Authority == "" {
return nil
}
return rewrite
}
func createCorsFilter(filter *k8s.HTTPCORSFilter) *istio.CorsPolicy {
if filter == nil {
return nil
}
res := &istio.CorsPolicy{}
for _, r := range filter.AllowOrigins {
rs := string(r)
if len(rs) == 0 {
continue // Not valid anyways, but double check
}
// TODO: support wildcards (https://github.com/kubernetes-sigs/gateway-api/issues/3648)
res.AllowOrigins = append(res.AllowOrigins, &istio.StringMatch{
MatchType: &istio.StringMatch_Exact{Exact: string(r)},
})
}
if ptr.OrEmpty(filter.AllowCredentials) {
res.AllowCredentials = wrappers.Bool(true)
}
for _, r := range filter.AllowMethods {
res.AllowMethods = append(res.AllowMethods, string(r))
}
for _, r := range filter.AllowHeaders {
res.AllowHeaders = append(res.AllowHeaders, string(r))
}
for _, r := range filter.ExposeHeaders {
res.ExposeHeaders = append(res.ExposeHeaders, string(r))
}
if filter.MaxAge > 0 {
res.MaxAge = durationpb.New(time.Duration(filter.MaxAge) * time.Second)
}
return res
}
func createRedirectFilter(filter *k8s.HTTPRequestRedirectFilter) *istio.HTTPRedirect {
if filter == nil {
return nil
}
resp := &istio.HTTPRedirect{}
if filter.StatusCode != nil {
// Istio allows 301, 302, 303, 307, 308.
// Gateway allows only 301 and 302.
resp.RedirectCode = uint32(*filter.StatusCode)
}
if filter.Hostname != nil {
resp.Authority = string(*filter.Hostname)
}
if filter.Scheme != nil {
// Both allow http and https
resp.Scheme = *filter.Scheme
}
if filter.Port != nil {
resp.RedirectPort = &istio.HTTPRedirect_Port{Port: uint32(*filter.Port)}
} else {
// "When empty, port (if specified) of the request is used."
// this differs from Istio default
if filter.Scheme != nil {
resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_PROTOCOL_DEFAULT}
} else {
resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_REQUEST_PORT}
}
}
if filter.Path != nil {
switch filter.Path.Type {
case k8s.FullPathHTTPPathModifier:
resp.Uri = *filter.Path.ReplaceFullPath
case k8s.PrefixMatchHTTPPathModifier:
resp.Uri = fmt.Sprintf("%%PREFIX()%%%s", *filter.Path.ReplacePrefixMatch)
}
}
return resp
}
func createHeadersFilter(filter *k8s.HTTPHeaderFilter) *istio.Headers_HeaderOperations {
if filter == nil {
return nil
}
return &istio.Headers_HeaderOperations{
Add: headerListToMap(filter.Add),
Remove: filter.Remove,
Set: headerListToMap(filter.Set),
}
}
// nolint: unparam
func createMethodMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
if match.Method == nil {
return nil, nil
}
return &istio.StringMatch{
MatchType: &istio.StringMatch_Exact{Exact: string(*match.Method)},
}, nil
}
func createQueryParamsMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
res := map[string]*istio.StringMatch{}
for _, qp := range match.QueryParams {
tp := k8s.QueryParamMatchExact
if qp.Type != nil {
tp = *qp.Type
}
switch tp {
case k8s.QueryParamMatchExact:
res[string(qp.Name)] = &istio.StringMatch{
MatchType: &istio.StringMatch_Exact{Exact: qp.Value},
}
case k8s.QueryParamMatchRegularExpression:
res[string(qp.Name)] = &istio.StringMatch{
MatchType: &istio.StringMatch_Regex{Regex: qp.Value},
}
default:
// Should never happen, unless a new field is added
return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported QueryParams type", tp)}
}
}
if len(res) == 0 {
return nil, nil
}
return res, nil
}
func createHeadersMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
res := map[string]*istio.StringMatch{}
for _, header := range match.Headers {
tp := k8s.HeaderMatchExact
if header.Type != nil {
tp = *header.Type
}
switch tp {
case k8s.HeaderMatchExact:
res[string(header.Name)] = &istio.StringMatch{
MatchType: &istio.StringMatch_Exact{Exact: header.Value},
}
case k8s.HeaderMatchRegularExpression:
res[string(header.Name)] = &istio.StringMatch{
MatchType: &istio.StringMatch_Regex{Regex: header.Value},
}
default:
// Should never happen, unless a new field is added
return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)}
}
}
if len(res) == 0 {
return nil, nil
}
return res, nil
}
func createGRPCHeadersMatch(match k8s.GRPCRouteMatch) (map[string]*istio.StringMatch, *ConfigError) {
res := map[string]*istio.StringMatch{}
for _, header := range match.Headers {
tp := k8s.GRPCHeaderMatchExact
if header.Type != nil {
tp = *header.Type
}
switch tp {
case k8s.GRPCHeaderMatchExact:
res[string(header.Name)] = &istio.StringMatch{
MatchType: &istio.StringMatch_Exact{Exact: header.Value},
}
case k8s.GRPCHeaderMatchRegularExpression:
res[string(header.Name)] = &istio.StringMatch{
MatchType: &istio.StringMatch_Regex{Regex: header.Value},
}
default:
// Should never happen, unless a new field is added
return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)}
}
}
if len(res) == 0 {
return nil, nil
}
return res, nil
}
func createURIMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) {
tp := k8s.PathMatchPathPrefix
if match.Path.Type != nil {
tp = *match.Path.Type
}
dest := "/"
if match.Path.Value != nil {
dest = *match.Path.Value
}
switch tp {
case k8s.PathMatchPathPrefix:
// "When specified, a trailing `/` is ignored."
if dest != "/" {
dest = strings.TrimSuffix(dest, "/")
}
return &istio.StringMatch{
MatchType: &istio.StringMatch_Prefix{Prefix: dest},
}, nil
case k8s.PathMatchExact:
return &istio.StringMatch{
MatchType: &istio.StringMatch_Exact{Exact: dest},
}, nil
case k8s.PathMatchRegularExpression:
return &istio.StringMatch{
MatchType: &istio.StringMatch_Regex{Regex: dest},
}, nil
default:
// Should never happen, unless a new field is added
return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)}
}
}
func createGRPCURIMatch(match k8s.GRPCRouteMatch) (*istio.StringMatch, *ConfigError) {
m := match.Method
if m == nil {
return nil, nil
}
tp := k8s.GRPCMethodMatchExact
if m.Type != nil {
tp = *m.Type
}
if m.Method == nil && m.Service == nil {
// Should never happen, invalid per spec
return nil, &ConfigError{Reason: InvalidConfiguration, Message: "gRPC match must have method or service defined"}
}
// gRPC format is /<Service>/<Method>. Since we don't natively understand this, convert to various string matches
switch tp {
case k8s.GRPCMethodMatchExact:
if m.Method == nil {
return &istio.StringMatch{
MatchType: &istio.StringMatch_Prefix{Prefix: fmt.Sprintf("/%s/", *m.Service)},
}, nil
}
if m.Service == nil {
return &istio.StringMatch{
MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)},
}, nil
}
return &istio.StringMatch{
MatchType: &istio.StringMatch_Exact{Exact: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)},
}, nil
case k8s.GRPCMethodMatchRegularExpression:
if m.Method == nil {
return &istio.StringMatch{
MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/.+", *m.Service)},
}, nil
}
if m.Service == nil {
return &istio.StringMatch{
MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)},
}, nil
}
return &istio.StringMatch{
MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)},
}, nil
default:
// Should never happen, unless a new field is added
return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)}
}
}
// parentKey holds info about a parentRef (eg route binding to a Gateway). This is a mirror of
// k8s.ParentReference in a form that can be stored in a map
type parentKey struct {
Kind config.GroupVersionKind
// Name is the original name of the resource (eg Kubernetes Gateway name)
Name string
// Namespace is the namespace of the resource
Namespace string
}
func (p parentKey) String() string {
return p.Kind.String() + "/" + p.Namespace + "/" + p.Name
}
type parentReference struct {
parentKey
SectionName k8s.SectionName
Port k8s.PortNumber
}
func (p parentReference) String() string {
return p.parentKey.String() + "/" + string(p.SectionName) + "/" + fmt.Sprint(p.Port)
}
var meshGVK = config.GroupVersionKind{
Group: gvk.KubernetesGateway.Group,
Version: gvk.KubernetesGateway.Version,
Kind: "Mesh",
}
var meshParentKey = parentKey{
Kind: meshGVK,
Name: "istio",
}
// parentInfo holds info about a "parent" - something that can be referenced as a ParentRef in the API.
// Today, this is just Gateway and Mesh.
type parentInfo struct {
// InternalName refers to the internal name we can reference it by. For example, "mesh" or "my-ns/my-gateway"
InternalName string
// AllowedKinds indicates which kinds can be admitted by this parent
AllowedKinds []k8s.RouteGroupKind
// Hostnames is the hostnames that must be match to reference to the parent. For gateway this is listener hostname
// Format is ns/hostname or just hostname, which is equivalent to */hostname
Hostnames []string
// OriginalHostname is the unprocessed form of Hostnames; how it appeared in users' config
OriginalHostname string
SectionName k8s.SectionName
Port k8s.PortNumber
Protocol k8s.ProtocolType
}
// routeParentReference holds information about a route's parent reference
type routeParentReference struct {
// InternalName refers to the internal name of the parent we can reference it by. For example, "mesh" or "my-ns/my-gateway"
InternalName string
// InternalKind is the Group/Kind of the parent
InternalKind config.GroupVersionKind
// DeniedReason, if present, indicates why the reference was not valid
DeniedReason *ParentError
// OriginalReference contains the original reference
OriginalReference k8s.ParentReference
// Hostname is the hostname match of the parent, if any
Hostname string
BannedHostnames sets.Set[string]
ParentKey parentKey
ParentSection k8s.SectionName
// WaypointError, if present, indicates why the reference does not have valid configuration for generating a Waypoint
WaypointError *WaypointError
}
func (r routeParentReference) IsMesh() bool {
return r.InternalName == "mesh"
}
func (r routeParentReference) hostnameAllowedByIsolation(rawRouteHost string) bool {
routeHost := host.Name(rawRouteHost)
ourListener := host.Name(r.Hostname)
if len(ourListener) > 0 && !ourListener.IsWildCarded() {
// Short circuit: this logic only applies to wildcards
// Not required for correctness, just an optimization
return true
}
if len(ourListener) > 0 && !routeHost.Matches(ourListener) {
return false
}
for checkListener := range r.BannedHostnames {
// We have 3 hostnames here:
// * routeHost, the hostname in the route entry
// * ourListener, the hostname of the listener the route is bound to
// * checkListener, the hostname of the other listener we are comparing to
// We want to return false if checkListener would match the routeHost and it would be a more exact match
if len(ourListener) > len(checkListener) {
// If our hostname is longer, it must be more exact than the check
continue
}
// Ours is shorter. If it matches the checkListener, then it should ONLY match that one
// Note protocol, port, etc are already considered when we construct bannedHostnames
if routeHost.SubsetOf(host.Name(checkListener)) {
return false
}
}
return true
}
func filteredReferences(parents []routeParentReference) []routeParentReference {
ret := make([]routeParentReference, 0, len(parents))
for _, p := range parents {
if p.DeniedReason != nil {
// We should filter this out
continue
}
ret = append(ret, p)
}
// To ensure deterministic order, sort them
sort.Slice(ret, func(i, j int) bool {
return ret[i].InternalName < ret[j].InternalName
})
return ret
}
func getDefaultName(name string, kgw *k8s.GatewaySpec, disableNameSuffix bool) string {
if disableNameSuffix {
return name
}
return fmt.Sprintf("%v-%v", name, kgw.GatewayClassName)
}
// Gateway currently requires a listener (https://github.com/kubernetes-sigs/gateway-api/pull/1596).
// We don't *really* care about the listener, but it may make sense to add a warning if users do not
// configure it in an expected way so that we have consistency and can make changes in the future as needed.
// We could completely reject but that seems more likely to cause pain.
func unexpectedWaypointListener(l k8s.Listener) bool {
if l.Port != 15008 {
return true
}
if l.Protocol != k8s.ProtocolType(protocol.HBONE) {
return true
}
return false
}
func unexpectedEastWestWaypointListener(l k8s.Listener) bool {
if l.Port != 15008 {
return true
}
if l.Protocol != k8s.ProtocolType(protocol.HBONE) {
return true
}
if l.TLS == nil || *l.TLS.Mode != k8s.TLSModeTerminate {
return true
}
// TODO: Should we check that there aren't more things set
return false
}
func getListenerNames(spec *k8s.GatewaySpec) sets.Set[k8s.SectionName] {
res := sets.New[k8s.SectionName]()
for _, l := range spec.Listeners {
res.Insert(l.Name)
}
return res
}
func reportGatewayStatus(
r *GatewayContext,
obj *k8sbeta.Gateway,
gs *k8sbeta.GatewayStatus,
gatewayServices []string,
servers []*istio.Server,
listenerSetCount int,
gatewayErr *ConfigError,
) {
// TODO: we lose address if servers is empty due to an error
internal, external, pending, warnings, allUsable := r.ResolveGatewayInstances(obj.Namespace, gatewayServices, servers)
// Setup initial conditions to the success state. If we encounter errors, we will update this.
// We have two status
// Accepted: is the configuration valid. We only have errors in listeners, and the status is not supposed to
// be tied to listeners, so this is always accepted
// Programmed: is the data plane "ready" (note: eventually consistent)
gatewayConditions := map[string]*condition{
string(k8s.GatewayConditionAccepted): {
reason: string(k8s.GatewayReasonAccepted),
message: "Resource accepted",
},
string(k8s.GatewayConditionProgrammed): {
reason: string(k8s.GatewayReasonProgrammed),
message: "Resource programmed",
},
}
if gatewayErr != nil {
gatewayConditions[string(k8s.GatewayConditionAccepted)].error = gatewayErr
}
// Not defined in upstream API
const AttachedListenerSets = "AttachedListenerSets"
if obj.Spec.AllowedListeners != nil {
gatewayConditions[AttachedListenerSets] = &condition{
reason: "ListenersAttached",
message: "At least one ListenerSet is attached",
}
if !features.EnableAlphaGatewayAPI {
gatewayConditions[AttachedListenerSets].error = &ConfigError{
Reason: "Unsupported",
Message: fmt.Sprintf("AllowedListeners is configured, but ListenerSets are not enabled (set %v=true)",
features.EnableAlphaGatewayAPIName),
}
} else if listenerSetCount == 0 {
gatewayConditions[AttachedListenerSets].error = &ConfigError{
Reason: "NoListenersAttached",
Message: "AllowedListeners is configured, but no ListenerSets are attached",
}
}
}
setProgrammedCondition(gatewayConditions, internal, gatewayServices, warnings, allUsable)
addressesToReport := external
addrType := k8s.IPAddressType
if len(addressesToReport) == 0 {
addrType = k8s.HostnameAddressType
for _, hostport := range internal {
svchost, _, _ := net.SplitHostPort(hostport)
if !slices.Contains(pending, svchost) && !slices.Contains(addressesToReport, svchost) {
addressesToReport = append(addressesToReport, svchost)
}
}
}
gs.Addresses = make([]k8s.GatewayStatusAddress, 0, len(addressesToReport))
for _, addr := range addressesToReport {
gs.Addresses = append(gs.Addresses, k8s.GatewayStatusAddress{
Value: addr,
Type: &addrType,
})
}
// Prune listeners that have been removed
haveListeners := getListenerNames(&obj.Spec)
listeners := make([]k8s.ListenerStatus, 0, len(gs.Listeners))
for _, l := range gs.Listeners {
if haveListeners.Contains(l.Name) {
haveListeners.Delete(l.Name)
listeners = append(listeners, l)
}
}
gs.Listeners = listeners
gs.Conditions = setConditions(obj.Generation, gs.Conditions, gatewayConditions)
}
func reportListenerSetStatus(
r *GatewayContext,
parentGwObj *k8sbeta.Gateway,
obj *gatewayx.XListenerSet,
gs *gatewayx.ListenerSetStatus,
gatewayServices []string,
servers []*istio.Server,
gatewayErr *ConfigError,
) {
internal, _, _, warnings, allUsable := r.ResolveGatewayInstances(parentGwObj.Namespace, gatewayServices, servers)
// Setup initial conditions to the success state. If we encounter errors, we will update this.
// We have two status
// Accepted: is the configuration valid. We only have errors in listeners, and the status is not supposed to
// be tied to listeners, so this is always accepted
// Programmed: is the data plane "ready" (note: eventually consistent)
gatewayConditions := map[string]*condition{
string(k8s.GatewayConditionAccepted): {
reason: string(k8s.GatewayReasonAccepted),
message: "Resource accepted",
},
string(k8s.GatewayConditionProgrammed): {
reason: string(k8s.GatewayReasonProgrammed),
message: "Resource programmed",
},
}
if gatewayErr != nil {
gatewayErr.Message = "Parent not accepted: " + gatewayErr.Message
gatewayConditions[string(k8s.GatewayConditionAccepted)].error = gatewayErr
}
setProgrammedCondition(gatewayConditions, internal, gatewayServices, warnings, allUsable)
gs.Conditions = setConditions(obj.Generation, gs.Conditions, gatewayConditions)
}
func setProgrammedCondition(gatewayConditions map[string]*condition, internal []string, gatewayServices []string, warnings []string, allUsable bool) {
if len(internal) > 0 {
msg := fmt.Sprintf("Resource programmed, assigned to service(s) %s", humanReadableJoin(internal))
gatewayConditions[string(k8s.GatewayConditionProgrammed)].message = msg
}
if len(gatewayServices) == 0 {
gatewayConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{
Reason: InvalidAddress,
Message: "Failed to assign to any requested addresses",
}
} else if len(warnings) > 0 {
// Start - Updated by Higress
var msg string
//var reason string
if len(internal) != 0 {
msg = fmt.Sprintf("Assigned to service(s) %s, but failed to assign to all requested addresses: %s",
humanReadableJoin(internal), strings.Join(warnings, "; "))
} else {
msg = fmt.Sprintf("Failed to assign to any requested addresses: %s", strings.Join(warnings, "; "))
}
//
//if allUsable {
// reason = string(k8s.GatewayReasonAddressNotAssigned)
//} else {
// reason = string(k8s.GatewayReasonAddressNotUsable)
//}
// End - Updated by Higress
gatewayConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{
// TODO: this only checks Service ready, we should also check Deployment ready?
Reason: string(k8s.GatewayReasonInvalid),
Message: msg,
}
}
}
// reportUnmanagedGatewayStatus reports a status message for an unmanaged gateway.
// For these gateways, we don't deploy them. However, all gateways ought to have a status message, even if its basically
// just to say something read it
func reportUnmanagedGatewayStatus(
status *k8sbeta.GatewayStatus,
obj *k8sbeta.Gateway,
) {
gatewayConditions := map[string]*condition{
string(k8s.GatewayConditionAccepted): {
reason: string(k8s.GatewayReasonAccepted),
message: "Resource accepted",
},
string(k8s.GatewayConditionProgrammed): {
reason: string(k8s.GatewayReasonProgrammed),
// Set to true anyway since this is basically declaring it as valid
message: "This Gateway is remote; Istio will not program it",
},
}
status.Addresses = slices.Map(obj.Spec.Addresses, func(e k8s.GatewaySpecAddress) k8s.GatewayStatusAddress {
return k8s.GatewayStatusAddress(e)
})
status.Listeners = nil
status.Conditions = setConditions(obj.Generation, status.Conditions, gatewayConditions)
}
// reportUnsupportedListenerSet reports a status message for a ListenerSet that is not supported
func reportUnsupportedListenerSet(class string, status *gatewayx.ListenerSetStatus, obj *gatewayx.XListenerSet) {
gatewayConditions := map[string]*condition{
string(k8s.GatewayConditionAccepted): {
reason: string(k8s.GatewayReasonAccepted),
error: &ConfigError{
Reason: string(gatewayx.ListenerSetReasonNotAllowed),
Message: fmt.Sprintf("The %q GatewayClass does not support ListenerSet", class),
},
},
string(k8s.GatewayConditionProgrammed): {
reason: string(k8s.GatewayReasonProgrammed),
error: &ConfigError{
Reason: string(gatewayx.ListenerSetReasonNotAllowed),
Message: fmt.Sprintf("The %q GatewayClass does not support ListenerSet", class),
},
},
}
status.Listeners = nil
status.Conditions = setConditions(obj.Generation, status.Conditions, gatewayConditions)
}
// reportNotAllowedListenerSet reports a status message for a ListenerSet that is not allowed to be selected
func reportNotAllowedListenerSet(status *gatewayx.ListenerSetStatus, obj *gatewayx.XListenerSet) {
gatewayConditions := map[string]*condition{
string(k8s.GatewayConditionAccepted): {
reason: string(k8s.GatewayReasonAccepted),
error: &ConfigError{
Reason: string(gatewayx.ListenerSetReasonNotAllowed),
Message: "The parent Gateway does not allow this reference; check the 'spec.allowedRoutes'",
},
},
string(k8s.GatewayConditionProgrammed): {
reason: string(k8s.GatewayReasonProgrammed),
error: &ConfigError{
Reason: string(gatewayx.ListenerSetReasonNotAllowed),
Message: "The parent Gateway does not allow this reference; check the 'spec.allowedRoutes'",
},
},
}
status.Listeners = nil
status.Conditions = setConditions(obj.Generation, status.Conditions, gatewayConditions)
}
// IsManaged checks if a Gateway is managed (ie we create the Deployment and Service) or unmanaged.
// This is based on the address field of the spec. If address is set with a Hostname type, it should point to an existing
// 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.
// If there is an IP, we will set the `loadBalancerIP` type.
// 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.
// So far, this mirrors how out of clusters work (address set means to use existing IP, unset means to provision one),
// and there has been growing consensus on this model for in cluster deployments.
//
// Currently, the supported options are:
// * 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.
// * 1 IP address. This is managed, with IP explicit
// * Nothing. This is managed, with IP auto assigned
//
// Not supported:
// Multiple hostname/IP - It is feasible but preference is to create multiple Gateways. This would also break the 1:1 mapping of GW:Service
// Mixed hostname and IP - doesn't make sense; user should define the IP in service
// NamedAddress - Service has no concept of named address. For cloud's that have named addresses they can be configured by annotations,
//
// which users can add to the Gateway.
//
// If manual deployments are disabled, IsManaged() always returns true.
func IsManaged(gw *k8s.GatewaySpec) bool {
if !features.EnableGatewayAPIManualDeployment {
return true
}
if len(gw.Addresses) == 0 {
return true
}
if len(gw.Addresses) > 1 {
return false
}
if t := gw.Addresses[0].Type; t == nil || *t == k8s.IPAddressType {
return true
}
return false
}
// Start - Added by Higress
// UseDefaultService checks if a Gateway shall be bound to the default gateway service
// This is based on the addresses field of the spec
// If addresses field contains any item with a Hostname type, it should point to the existing
// Services that handles the gateway traffic
// If it is not set, or all items refer to only a single IP, we will consider it pointed to the default data plane service.
// 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.
func UseDefaultService(gw *k8s.GatewaySpec) bool {
if len(gw.Addresses) == 0 {
return true
}
for _, addr := range gw.Addresses {
if t := addr.Type; t == nil || *t == k8s.HostnameAddressType {
return false
}
}
return true
}
// End - Added by Higress
func extractGatewayServices(domainSuffix string, kgw *k8sbeta.Gateway, info classInfo) ([]string, bool, *ConfigError) {
// Start - Updated by Higress
if UseDefaultService(&kgw.Spec) {
// name := model.GetOrDefault(obj.Annotations[gatewayNameOverride], getDefaultName(obj.Name, kgw))
// return []string{fmt.Sprintf("%s.%s.svc.%v", name, obj.Namespace, r.Domain)}, true, nil
name := kgw.Annotations[gatewayNameOverride]
if len(name) > 0 {
return []string{fmt.Sprintf("%s.%s.svc.%v", name, kgw.Namespace, domainSuffix)}, false, nil
}
return []string{fmt.Sprintf("%s.%s.svc.%s", higressconfig.GatewayName, higressconfig.PodNamespace, util.GetDomainSuffix())}, true, nil
}
gatewayServices := []string{}
skippedAddresses := []string{}
for _, addr := range kgw.Spec.Addresses {
if addr.Type != nil && *addr.Type != k8s.HostnameAddressType {
// We only support HostnameAddressType. Keep track of invalid ones so we can report in status.
skippedAddresses = append(skippedAddresses, addr.Value)
continue
}
// TODO: For now we are using Addresses. There has been some discussion of allowing inline
// parameters on the class field like a URL, in which case we will probably just use that. See
// https://github.com/kubernetes-sigs/gateway-api/pull/614
fqdn := addr.Value
if !strings.Contains(fqdn, ".") {
// Short name, expand it
fqdn = fmt.Sprintf("%s.%s.svc.%s", fqdn, kgw.Namespace, domainSuffix)
}
gatewayServices = append(gatewayServices, fqdn)
}
if len(skippedAddresses) > 0 {
// Give error but return services, this is a soft failure
return gatewayServices, false, &ConfigError{
Reason: InvalidAddress,
Message: fmt.Sprintf("only Hostname is supported, ignoring %v", skippedAddresses),
}
}
if _, f := kgw.Annotations[serviceTypeOverride]; f {
// Give error but return services, this is a soft failure
// Remove entirely in 1.20
return gatewayServices, false, &ConfigError{
Reason: DeprecateFieldUsage,
Message: fmt.Sprintf("annotation %v is deprecated, use Spec.Infrastructure.Routeability", serviceTypeOverride),
}
}
return gatewayServices, false, nil
}
func buildListener(
ctx krt.HandlerContext,
configMaps krt.Collection[*corev1.ConfigMap],
secrets krt.Collection[*corev1.Secret],
grants ReferenceGrants,
namespaces krt.Collection[*corev1.Namespace],
obj controllers.Object,
status []k8s.ListenerStatus,
gw k8s.GatewaySpec,
l k8s.Listener,
listenerIndex int,
controllerName k8s.GatewayController,
portErr error,
) (*istio.Server, []k8s.ListenerStatus, bool) {
listenerConditions := map[string]*condition{
string(k8s.ListenerConditionAccepted): {
reason: string(k8s.ListenerReasonAccepted),
message: "No errors found",
},
string(k8s.ListenerConditionProgrammed): {
reason: string(k8s.ListenerReasonProgrammed),
message: "No errors found",
},
string(k8s.ListenerConditionConflicted): {
reason: string(k8s.ListenerReasonNoConflicts),
message: "No errors found",
status: kstatus.StatusFalse,
},
string(k8s.ListenerConditionResolvedRefs): {
reason: string(k8s.ListenerReasonResolvedRefs),
message: "No errors found",
},
}
ok := true
tls, err := buildTLS(ctx, configMaps, secrets, grants, resolveGatewayTLS(l.Port, gw.TLS), l.TLS, obj, kube.IsAutoPassthrough(obj.GetLabels(), l))
if err != nil {
listenerConditions[string(k8s.ListenerConditionResolvedRefs)].error = err
listenerConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{
Reason: string(k8s.GatewayReasonInvalid),
Message: "Bad TLS configuration",
}
ok = false
}
hostnames := buildHostnameMatch(ctx, obj.GetNamespace(), namespaces, l)
if portErr != nil {
listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{
Reason: string(k8s.ListenerReasonUnsupportedProtocol),
Message: portErr.Error(),
}
ok = false
}
protocol, perr := listenerProtocolToIstio(controllerName, l.Protocol)
if perr != nil {
listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{
Reason: string(k8s.ListenerReasonUnsupportedProtocol),
Message: perr.Error(),
}
ok = false
}
if controllerName == constants.ManagedGatewayMeshController {
if unexpectedWaypointListener(l) {
listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{
Reason: string(k8s.ListenerReasonUnsupportedProtocol),
Message: `Expected a single listener on port 15008 with protocol "HBONE"`,
}
}
}
if controllerName == constants.ManagedGatewayEastWestController {
if unexpectedEastWestWaypointListener(l) {
listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{
Reason: string(k8s.ListenerReasonUnsupportedProtocol),
Message: `Expected a single listener on port 15008 with protocol "HBONE" and TLS.Mode == Terminate`,
}
}
}
server := &istio.Server{
Port: &istio.Port{
// Name is required. We only have one server per Gateway, so we can just name them all the same
Name: "default",
Number: uint32(l.Port),
Protocol: protocol,
},
Hosts: hostnames,
Tls: tls,
}
updatedStatus := reportListenerCondition(listenerIndex, l, obj, status, listenerConditions)
return server, updatedStatus, ok
}
var supportedProtocols = sets.New(
k8s.HTTPProtocolType,
k8s.HTTPSProtocolType,
k8s.TLSProtocolType,
k8s.TCPProtocolType,
k8s.ProtocolType(protocol.HBONE))
func listenerProtocolToIstio(name k8s.GatewayController, p k8s.ProtocolType) (string, error) {
switch p {
// Standard protocol types
case k8s.HTTPProtocolType:
return string(p), nil
case k8s.HTTPSProtocolType:
return string(p), nil
case k8s.TLSProtocolType, k8s.TCPProtocolType:
if !features.EnableAlphaGatewayAPI {
return "", fmt.Errorf("protocol %q is supported, but only when %v=true is configured", p, features.EnableAlphaGatewayAPIName)
}
return string(p), nil
// Our own custom types
case k8s.ProtocolType(protocol.HBONE):
if name != constants.ManagedGatewayMeshController && name != constants.ManagedGatewayEastWestController {
return "", fmt.Errorf("protocol %q is only supported for waypoint proxies", p)
}
return string(p), nil
}
up := k8s.ProtocolType(strings.ToUpper(string(p)))
if supportedProtocols.Contains(up) {
return "", fmt.Errorf("protocol %q is unsupported. hint: %q (uppercase) may be supported", p, up)
}
// Note: the k8s.UDPProtocolType is explicitly left to hit this path
return "", fmt.Errorf("protocol %q is unsupported", p)
}
func resolveGatewayTLS(port k8s.PortNumber, gw *k8s.GatewayTLSConfig) *k8s.TLSConfig {
if gw == nil || gw.Frontend == nil {
return nil
}
f := gw.Frontend
pp := slices.FindFunc(f.PerPort, func(portConfig k8s.TLSPortConfig) bool {
return portConfig.Port == port
})
if pp != nil {
return &pp.TLS
}
return &f.Default
}
func buildTLS(
ctx krt.HandlerContext,
configMaps krt.Collection[*corev1.ConfigMap],
secrets krt.Collection[*corev1.Secret],
grants ReferenceGrants,
gatewayTLS *k8s.TLSConfig,
tls *k8s.ListenerTLSConfig,
gw controllers.Object,
isAutoPassthrough bool,
) (*istio.ServerTLSSettings, *ConfigError) {
if tls == nil {
return nil, nil
}
// Explicitly not supported: file mounted
// Not yet implemented: TLS mode, https redirect, max protocol version, SANs, CipherSuites, VerifyCertificate
out := &istio.ServerTLSSettings{
HttpsRedirect: false,
}
mode := k8s.TLSModeTerminate
if tls.Mode != nil {
mode = *tls.Mode
}
namespace := gw.GetNamespace()
switch mode {
case k8s.TLSModeTerminate:
out.Mode = istio.ServerTLSSettings_SIMPLE
if tls.Options != nil {
switch tls.Options[gatewayTLSTerminateModeKey] {
case "MUTUAL":
out.Mode = istio.ServerTLSSettings_MUTUAL
case "OPTIONAL_MUTUAL":
out.Mode = istio.ServerTLSSettings_OPTIONAL_MUTUAL
case "ISTIO_SIMPLE":
// Simple TLS but with builtin workload certificate.
// equivalent to `credentialName: builtin://
out.Mode = istio.ServerTLSSettings_SIMPLE
out.CredentialName = creds.BuiltinGatewaySecretTypeURI
return out, nil
case "ISTIO_MUTUAL":
out.Mode = istio.ServerTLSSettings_ISTIO_MUTUAL
return out, nil
}
}
if len(tls.CertificateRefs) > 2 {
return out, &ConfigError{
Reason: InvalidTLS,
Message: "TLS mode can only support up to 2 server certificates",
}
}
credNames := make([]string, len(tls.CertificateRefs))
validCertCount := 0
var combinedErr *ConfigError
for i, certRef := range tls.CertificateRefs {
cred, err := buildSecretReference(ctx, certRef, gw, secrets)
if err != nil {
combinedErr = joinErrors(combinedErr, err)
continue
}
credNs := ptr.OrDefault((*string)(certRef.Namespace), namespace)
sameNamespace := credNs == namespace
objectKind := schematypes.GvkFromObject(gw)
if !sameNamespace && !grants.SecretAllowed(ctx, objectKind, creds.ToResourceName(cred), namespace) {
combinedErr = joinErrors(combinedErr, &ConfigError{
Reason: InvalidListenerRefNotPermitted,
Message: fmt.Sprintf(
"certificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)",
certRef.Name, credNs, namespace,
),
})
continue
}
credNames[i] = cred
validCertCount++
}
if validCertCount == 0 {
// If we have no valid certificates, return an error
return out, combinedErr
}
if validCertCount == 1 {
out.CredentialName = credNames[0]
} else {
out.CredentialNames = credNames
}
if gatewayTLS != nil && gatewayTLS.Validation != nil && len(gatewayTLS.Validation.CACertificateRefs) > 0 {
// TODO: add 'Mode'
if len(gatewayTLS.Validation.CACertificateRefs) > 1 {
return out, &ConfigError{
Reason: InvalidTLS,
Message: "only one caCertificateRef is supported",
}
}
caCertRef := gatewayTLS.Validation.CACertificateRefs[0]
cred, err := buildCaCertificateReference(ctx, caCertRef, gw, configMaps, secrets)
if err != nil {
return out, err
}
if cred.Namespace != namespace && !grants.SecretAllowed(ctx, schematypes.GvkFromObject(gw), cred.ResourceName, namespace) {
return out, &ConfigError{
Reason: InvalidListenerRefNotPermitted,
Message: fmt.Sprintf(
"caCertificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)",
cred.Namespace, caCertRef.Name, namespace,
),
}
}
out.Mode = istio.ServerTLSSettings_MUTUAL
//out.CaCertCredentialName = cred.ResourceName
}
case k8s.TLSModePassthrough:
out.Mode = istio.ServerTLSSettings_PASSTHROUGH
if isAutoPassthrough {
out.Mode = istio.ServerTLSSettings_AUTO_PASSTHROUGH
}
}
return out, nil
}
func buildSecretReference(
ctx krt.HandlerContext,
ref k8s.SecretObjectReference,
gw controllers.Object,
secrets krt.Collection[*corev1.Secret],
) (string, *ConfigError) {
if normalizeReference(ref.Group, ref.Kind, gvk.Secret) != gvk.Secret {
return "", &ConfigError{Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, only secret is allowed", secretObjectReferenceString(ref))}
}
secret := model.ConfigKey{
Kind: kind.Secret,
Name: string(ref.Name),
Namespace: ptr.OrDefault((*string)(ref.Namespace), gw.GetNamespace()),
}
key := secret.Namespace + "/" + secret.Name
scrt := ptr.Flatten(krt.FetchOne(ctx, secrets, krt.FilterKey(key)))
if scrt == nil {
return "", &ConfigError{
Reason: InvalidTLS,
Message: fmt.Sprintf("invalid certificate reference %v, secret %v not found", secretObjectReferenceString(ref), key),
}
}
certInfo, err := kubecreds.ExtractCertInfo(scrt)
if err != nil {
return "", &ConfigError{
Reason: InvalidTLS,
Message: fmt.Sprintf("invalid certificate reference %v, %v", secretObjectReferenceString(ref), err),
}
}
if _, err = tls.X509KeyPair(certInfo.Cert, certInfo.Key); err != nil {
return "", &ConfigError{
Reason: InvalidTLS,
Message: fmt.Sprintf("invalid certificate reference %v, the certificate is malformed: %v", secretObjectReferenceString(ref), err),
}
}
return creds.ToKubernetesGatewayResource(secret.Namespace, secret.Name), nil
}
func buildCaCertificateReference(
ctx krt.HandlerContext,
ref k8s.ObjectReference,
gw controllers.Object,
configMaps krt.Collection[*corev1.ConfigMap],
secrets krt.Collection[*corev1.Secret],
) (*creds.SecretResource, *ConfigError) {
var resourceType string
var resourceKind kind.Kind
var certInfo *credentials.CertInfo
var certInfoErr error
namespace := ptr.OrDefault((*string)(ref.Namespace), gw.GetNamespace())
name := string(ref.Name)
switch normalizeReference(&ref.Group, &ref.Kind, config.GroupVersionKind{}) {
case gvk.ConfigMap:
resourceType = creds.KubernetesConfigMapType
resourceKind = kind.ConfigMap
key := namespace + "/" + name
cm := ptr.Flatten(krt.FetchOne(ctx, configMaps, krt.FilterKey(key)))
if cm == nil {
return nil, &ConfigError{
Reason: InvalidTLS,
Message: fmt.Sprintf("invalid CA certificate reference %v, configmap %v not found", objectReferenceString(ref), key),
}
}
certInfo, certInfoErr = kubecreds.ExtractRootFromString(cm.Data)
case gvk.Secret:
resourceType = creds.KubernetesGatewaySecretType
resourceKind = kind.Secret
key := namespace + "/" + name
scrt := ptr.Flatten(krt.FetchOne(ctx, secrets, krt.FilterKey(key)))
if scrt == nil {
return nil, &ConfigError{
Reason: InvalidTLS,
Message: fmt.Sprintf("invalid CA certificate reference %v, secret %v not found", objectReferenceString(ref), key),
}
}
certInfo, certInfoErr = kubecreds.ExtractRoot(scrt.Data)
default:
return nil, &ConfigError{
Reason: InvalidTLS,
Message: fmt.Sprintf("invalid CA certificate reference %v, only secret and configmap are allowed", objectReferenceString(ref)),
}
}
if certInfoErr != nil {
return nil, &ConfigError{
Reason: InvalidTLS,
Message: fmt.Sprintf("invalid CA certificate reference %v, %v", objectReferenceString(ref), certInfoErr),
}
}
if !x509.NewCertPool().AppendCertsFromPEM(certInfo.Cert) {
return nil, &ConfigError{
Reason: InvalidTLS,
Message: fmt.Sprintf("invalid CA certificate reference %v, the bundle is malformed", objectReferenceString(ref)),
}
}
log.Warnf("buildCaCertificateReference %s://%s/%s%s", resourceType, namespace, ref.Name, creds.SdsCaSuffix)
return &creds.SecretResource{
ResourceType: resourceType,
ResourceKind: resourceKind,
Name: name + creds.SdsCaSuffix,
Namespace: namespace,
ResourceName: fmt.Sprintf("%s://%s/%s%s", resourceType, namespace, ref.Name, creds.SdsCaSuffix),
Cluster: "",
}, nil
}
func objectReferenceString(ref k8s.ObjectReference) string {
return fmt.Sprintf("%s/%s/%s.%s", ref.Group, ref.Kind, ref.Name, ptr.OrEmpty(ref.Namespace))
}
func secretObjectReferenceString(ref k8s.SecretObjectReference) string {
return fmt.Sprintf("%s/%s/%s.%s",
ptr.OrEmpty(ref.Group),
ptr.OrEmpty(ref.Kind),
ref.Name,
ptr.OrEmpty(ref.Namespace))
}
func parentRefString(ref k8s.ParentReference, objectNamespace string) string {
return fmt.Sprintf("%s/%s/%s/%s/%d.%s",
defaultString(ref.Group, gvk.KubernetesGateway.Group),
defaultString(ref.Kind, gvk.KubernetesGateway.Kind),
ref.Name,
ptr.OrEmpty(ref.SectionName),
ptr.OrEmpty(ref.Port),
defaultString(ref.Namespace, objectNamespace))
}
// buildHostnameMatch generates a Gateway.spec.servers.hosts section from a listener
func buildHostnameMatch(ctx krt.HandlerContext, localNamespace string, namespaces krt.Collection[*corev1.Namespace], l k8s.Listener) []string {
// We may allow all hostnames or a specific one
hostname := "*"
if l.Hostname != nil {
hostname = string(*l.Hostname)
}
resp := []string{}
for _, ns := range namespacesFromSelector(ctx, localNamespace, namespaces, l.AllowedRoutes) {
// This check is necessary to prevent adding a hostname with an invalid empty namespace
if len(ns) > 0 {
resp = append(resp, fmt.Sprintf("%s/%s", ns, hostname))
}
}
// If nothing matched use ~ namespace (match nothing). We need this since its illegal to have an
// empty hostname list, but we still need the Gateway provisioned to ensure status is properly set and
// SNI matches are established; we just don't want to actually match any routing rules (yet).
if len(resp) == 0 {
return []string{"~/" + hostname}
}
return resp
}
// namespacesFromSelector determines a list of allowed namespaces for a given AllowedRoutes
func namespacesFromSelector(ctx krt.HandlerContext, localNamespace string, namespaceCol krt.Collection[*corev1.Namespace], lr *k8s.AllowedRoutes) []string {
// Default is to allow only the same namespace
if lr == nil || lr.Namespaces == nil || lr.Namespaces.From == nil || *lr.Namespaces.From == k8s.NamespacesFromSame {
return []string{localNamespace}
}
if *lr.Namespaces.From == k8s.NamespacesFromAll {
return []string{"*"}
}
if lr.Namespaces.Selector == nil {
// Should never happen, invalid config
return []string{"*"}
}
// gateway-api has selectors, but Istio Gateway just has a list of names. We will run the selector
// against all namespaces and get a list of matching namespaces that can be converted into a list
// Istio can handle.
ls, err := metav1.LabelSelectorAsSelector(lr.Namespaces.Selector)
if err != nil {
return nil
}
namespaces := []string{}
namespaceObjects := krt.Fetch(ctx, namespaceCol)
for _, ns := range namespaceObjects {
if ls.Matches(toNamespaceSet(ns.Name, ns.Labels)) {
namespaces = append(namespaces, ns.Name)
}
}
// Ensure stable order
sort.Strings(namespaces)
return namespaces
}
// namespaceAcceptedByAllowListeners determines a list of allowed namespaces for a given AllowedListener
func namespaceAcceptedByAllowListeners(localNamespace string, parent *k8sbeta.Gateway, lookupNamespace func(string) *corev1.Namespace) bool {
lr := parent.Spec.AllowedListeners
// Default allows none
if lr == nil || lr.Namespaces == nil {
return false
}
n := *lr.Namespaces
if n.From != nil {
switch *n.From {
case k8s.NamespacesFromAll:
return true
case k8s.NamespacesFromSame:
return localNamespace == parent.Namespace
case k8s.NamespacesFromNone:
return false
default:
// Unknown?
return false
}
}
if lr.Namespaces.Selector == nil {
// Should never happen, invalid config
return false
}
ls, err := metav1.LabelSelectorAsSelector(lr.Namespaces.Selector)
if err != nil {
return false
}
localNamespaceObject := lookupNamespace(localNamespace)
if localNamespaceObject == nil {
// Couldn't find the namespace
return false
}
return ls.Matches(toNamespaceSet(localNamespaceObject.Name, localNamespaceObject.Labels))
}
func humanReadableJoin(ss []string) string {
switch len(ss) {
case 0:
return ""
case 1:
return ss[0]
case 2:
return ss[0] + " and " + ss[1]
default:
return strings.Join(ss[:len(ss)-1], ", ") + ", and " + ss[len(ss)-1]
}
}
// NamespaceNameLabel represents that label added automatically to namespaces is newer Kubernetes clusters
const NamespaceNameLabel = "kubernetes.io/metadata.name"
// toNamespaceSet converts a set of namespace labels to a Set that can be used to select against.
func toNamespaceSet(name string, labels map[string]string) klabels.Set {
// If namespace label is not set, implicitly insert it to support older Kubernetes versions
if labels[NamespaceNameLabel] == name {
// Already set, avoid copies
return labels
}
// First we need a copy to not modify the underlying object
ret := make(map[string]string, len(labels)+1)
for k, v := range labels {
ret[k] = v
}
ret[NamespaceNameLabel] = name
return ret
}
func GetCommonRouteInfo(spec any) ([]k8s.ParentReference, []k8s.Hostname, config.GroupVersionKind) {
switch t := spec.(type) {
case *k8salpha.TCPRoute:
return t.Spec.ParentRefs, nil, gvk.TCPRoute
case *k8salpha.TLSRoute:
return t.Spec.ParentRefs, t.Spec.Hostnames, gvk.TLSRoute
case *k8sbeta.HTTPRoute:
return t.Spec.ParentRefs, t.Spec.Hostnames, gvk.HTTPRoute
case *k8s.GRPCRoute:
return t.Spec.ParentRefs, t.Spec.Hostnames, gvk.GRPCRoute
default:
log.Fatalf("unknown type %T", t)
return nil, nil, config.GroupVersionKind{}
}
}
func GetCommonRouteStateParents(spec any) []k8s.RouteParentStatus {
switch t := spec.(type) {
case *k8salpha.TCPRoute:
return t.Status.Parents
case *k8salpha.TLSRoute:
return t.Status.Parents
case *k8sbeta.HTTPRoute:
return t.Status.Parents
case *k8s.GRPCRoute:
return t.Status.Parents
default:
log.Fatalf("unknown type %T", t)
return nil
}
}
// normalizeReference takes a generic Group/Kind (the API uses a few variations) and converts to a known GroupVersionKind.
// Defaults for the group/kind are also passed.
func normalizeReference[G ~string, K ~string](group *G, kind *K, def config.GroupVersionKind) config.GroupVersionKind {
k := def.Kind
if kind != nil {
k = string(*kind)
}
g := def.Group
if group != nil {
g = string(*group)
}
gk := config.GroupVersionKind{
Group: g,
Kind: k,
}
s, f := collections.All.FindByGroupKind(gk)
if f {
return s.GroupVersionKind()
}
return gk
}
func defaultString[T ~string](s *T, def string) string {
if s == nil {
return def
}
return string(*s)
}
func toRouteKind(g config.GroupVersionKind) k8s.RouteGroupKind {
return k8s.RouteGroupKind{Group: (*k8s.Group)(&g.Group), Kind: k8s.Kind(g.Kind)}
}
func routeGroupKindEqual(rgk1, rgk2 k8s.RouteGroupKind) bool {
return rgk1.Kind == rgk2.Kind && getGroup(rgk1) == getGroup(rgk2)
}
func getGroup(rgk k8s.RouteGroupKind) k8s.Group {
return ptr.OrDefault(rgk.Group, k8s.Group(gvk.KubernetesGateway.Group))
}
func GetStatus[I, IS any](spec I) IS {
switch t := any(spec).(type) {
case *k8salpha.TCPRoute:
return any(t.Status).(IS)
case *k8salpha.TLSRoute:
return any(t.Status).(IS)
case *k8sbeta.HTTPRoute:
return any(t.Status).(IS)
case *k8s.GRPCRoute:
return any(t.Status).(IS)
case *k8sbeta.Gateway:
return any(t.Status).(IS)
case *k8sbeta.GatewayClass:
return any(t.Status).(IS)
case *gatewayx.XBackendTrafficPolicy:
return any(t.Status).(IS)
case *k8s.BackendTLSPolicy:
return any(t.Status).(IS)
case *gatewayx.XListenerSet:
return any(t.Status).(IS)
case *inferencev1.InferencePool:
return any(t.Status).(IS)
default:
log.Fatalf("unknown type %T", t)
return ptr.Empty[IS]()
}
}
func GetBackendRef[I any](spec I) (config.GroupVersionKind, *k8s.Namespace, k8s.ObjectName) {
switch t := any(spec).(type) {
case k8s.HTTPBackendRef:
return normalizeReference(t.Group, t.Kind, gvk.Service), t.Namespace, t.Name
case k8s.GRPCBackendRef:
return normalizeReference(t.Group, t.Kind, gvk.Service), t.Namespace, t.Name
case k8s.BackendRef:
return normalizeReference(t.Group, t.Kind, gvk.Service), t.Namespace, t.Name
default:
log.Fatalf("unknown GetBackendRef type %T", t)
return config.GroupVersionKind{}, nil, ""
}
}
// Start - Added by Higress
// isCatchAll returns true if HTTPMatchRequest is a catchall match otherwise
// false. Note - this may not be exactly "catch all" as we don't know the full
// class of possible inputs As such, this is used only for optimization.
func isCatchAllMatch(m *istio.HTTPMatchRequest) bool {
catchall := false
if m.Uri != nil {
switch m := m.Uri.MatchType.(type) {
case *istio.StringMatch_Prefix:
catchall = m.Prefix == "/"
case *istio.StringMatch_Regex:
catchall = m.Regex == "*"
}
}
// A Match is catch all if and only if it has no match set
// and URI has a prefix / or regex *.
return catchall &&
len(m.Headers) == 0 &&
len(m.QueryParams) == 0 &&
len(m.SourceLabels) == 0 &&
len(m.WithoutHeaders) == 0 &&
len(m.Gateways) == 0 &&
m.Method == nil &&
m.Scheme == nil &&
m.Port == 0 &&
m.Authority == nil &&
m.SourceNamespace == ""
}
func equal(have *string, expected string) bool {
return have != nil && *have == expected
}
func nilOrEqual(have *string, expected string) bool {
return have == nil || *have == expected
}
func generateRouteName(obj config.Namer, routeType string) string {
routeName := obj.GetName()
if obj.GetNamespace() != higressconfig.PodNamespace {
routeName = path.Join(obj.GetNamespace(), obj.GetName())
}
if routeType != "HTTP" {
routeName = path.Join(routeType, routeName)
}
return routeName
}
// End - Added by Higress