blob: 016317615ccd84da4dc765a64850b470c3029246 [file] [log] [blame]
// 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 gateway
import (
"fmt"
"sort"
"strings"
)
import (
"istio.io/api/label"
istio "istio.io/api/networking/v1alpha3"
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/v1alpha2"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/features"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model/credentials"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model/kstatus"
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/constants"
"github.com/apache/dubbo-go-pixiu/pkg/config/host"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk"
"github.com/apache/dubbo-go-pixiu/pkg/util/sets"
)
const (
DefaultClassName = "istio"
ControllerName = "istio.io/gateway-controller"
gatewayAliasForAnnotationKey = "gateway.istio.io/alias-for"
)
// KubernetesResources stores all inputs to our conversion
type KubernetesResources struct {
GatewayClass []config.Config
Gateway []config.Config
HTTPRoute []config.Config
TCPRoute []config.Config
TLSRoute []config.Config
ReferencePolicy []config.Config
// Namespaces stores all namespace in the cluster, keyed by name
Namespaces map[string]*corev1.Namespace
// Domain for the cluster. Typically, cluster.local
Domain string
Context model.GatewayContext
}
// OutputResources stores all outputs of our conversion
type OutputResources struct {
Gateway []config.Config
VirtualService []config.Config
// AllowedReferences stores all allowed references, from Reference -> to Reference(s)
AllowedReferences map[Reference]map[Reference]*AllowedReferences
// ReferencedNamespaceKeys stores the label key of all namespace selections. This allows us to quickly
// determine if a namespace update could have impacted any Gateways. See namespaceEvent.
ReferencedNamespaceKeys sets.Set
}
// Reference stores a reference to a namespaced GVK, as used by ReferencePolicy
type Reference struct {
Kind config.GroupVersionKind
Namespace k8s.Namespace
}
// convertResources is the top level entrypoint to our conversion logic, computing the full state based
// on KubernetesResources inputs.
func convertResources(r *KubernetesResources) OutputResources {
// sort HTTPRoutes by creation timestamp and namespace/name
sort.Slice(r.HTTPRoute, func(i, j int) bool {
if r.HTTPRoute[i].CreationTimestamp.Equal(r.HTTPRoute[j].CreationTimestamp) {
in := r.HTTPRoute[i].Namespace + "/" + r.HTTPRoute[i].Name
jn := r.HTTPRoute[j].Namespace + "/" + r.HTTPRoute[j].Name
return in < jn
}
return r.HTTPRoute[i].CreationTimestamp.Before(r.HTTPRoute[j].CreationTimestamp)
})
result := OutputResources{}
gw, gwMap, nsReferences := convertGateways(r)
result.Gateway = gw
result.VirtualService = convertVirtualService(r, gwMap)
// Once we have gone through all route computation, we will know how many routes bound to each gateway.
// Report this in the status.
for _, dm := range gwMap {
for _, pri := range dm {
if pri.ReportAttachedRoutes != nil {
pri.ReportAttachedRoutes()
}
}
}
result.AllowedReferences = convertReferencePolicies(r)
result.ReferencedNamespaceKeys = nsReferences
return result
}
type AllowedReferences struct {
AllowAll bool
AllowedNames sets.Set
}
// convertReferencePolicies extracts all ReferencePolicy into an easily accessibly index.
// The currently supported references are:
// * Gateway -> Secret
func convertReferencePolicies(r *KubernetesResources) map[Reference]map[Reference]*AllowedReferences {
res := map[Reference]map[Reference]*AllowedReferences{}
for _, obj := range r.ReferencePolicy {
rp := obj.Spec.(*k8s.ReferencePolicySpec)
for _, from := range rp.From {
fromKey := Reference{
Namespace: from.Namespace,
}
if string(from.Group) == gvk.KubernetesGateway.Group && string(from.Kind) == gvk.KubernetesGateway.Kind {
fromKey.Kind = gvk.KubernetesGateway
} else {
// Not supported type. Not an error; may be for another controller
continue
}
for _, to := range rp.To {
toKey := Reference{
Namespace: k8s.Namespace(obj.Namespace),
}
if to.Group == "" && string(to.Kind) == gvk.Secret.Kind {
toKey.Kind = gvk.Secret
} else {
// Not supported type. Not an error; may be for another controller
continue
}
if _, f := res[fromKey]; !f {
res[fromKey] = map[Reference]*AllowedReferences{}
}
if _, f := res[fromKey][toKey]; !f {
res[fromKey][toKey] = &AllowedReferences{
AllowedNames: sets.New(),
}
}
if to.Name != nil {
res[fromKey][toKey].AllowedNames.Insert(string(*to.Name))
} else {
res[fromKey][toKey].AllowAll = true
}
}
}
}
return res
}
// convertVirtualService takes all xRoute types and generates corresponding VirtualServices.
func convertVirtualService(r *KubernetesResources, gatewayMap map[parentKey]map[k8s.SectionName]*parentInfo) []config.Config {
result := []config.Config{}
for _, obj := range r.TCPRoute {
if vsConfig := buildTCPVirtualService(obj, gatewayMap, r.Domain); vsConfig != nil {
result = append(result, *vsConfig)
}
}
for _, obj := range r.TLSRoute {
result = append(result, buildTLSVirtualService(obj, gatewayMap, r.Domain)...)
}
// for gateway routes, build one VS per gateway+host
gatewayRoutes := make(map[string]map[string]*config.Config)
// for mesh routes, build one VS per namespace+host
meshRoutes := make(map[string]map[string]*config.Config)
for _, obj := range r.HTTPRoute {
buildHTTPVirtualServices(obj, gatewayMap, r.Domain, gatewayRoutes, meshRoutes)
}
for _, vsByHost := range gatewayRoutes {
for _, vsConfig := range vsByHost {
result = append(result, *vsConfig)
}
}
for _, vsByHost := range meshRoutes {
for _, vsConfig := range vsByHost {
result = append(result, *vsConfig)
}
}
return result
}
func buildHTTPVirtualServices(obj config.Config, gateways map[parentKey]map[k8s.SectionName]*parentInfo, domain string,
gatewayRoutes map[string]map[string]*config.Config, meshRoutes map[string]map[string]*config.Config) {
route := obj.Spec.(*k8s.HTTPRouteSpec)
for _, r := range route.Rules {
if len(r.Matches) > 1 {
// if a rule has multiple matches, make a deep copy to avoid impacting the obj when splitting the rule
route = route.DeepCopy()
break
}
}
ns := obj.Namespace
parentRefs := extractParentReferenceInfo(gateways, route.ParentRefs, route.Hostnames, gvk.HTTPRoute, ns)
reportError := func(routeErr *ConfigError) {
obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
rs := s.(*k8s.HTTPRouteStatus)
rs.Parents = createRouteStatus(parentRefs, obj, rs.Parents, routeErr)
return rs
})
}
httproutes := []*istio.HTTPRoute{}
hosts := hostnameToStringList(route.Hostnames)
for i := 0; i < len(route.Rules); i++ {
r := route.Rules[i]
if len(r.Matches) > 1 {
// split the rule to make sure each rule has up to one match
for _, match := range r.Matches {
splitRule := r
splitRule.Matches = []k8s.HTTPRouteMatch{match}
route.Rules = append(route.Rules, splitRule)
}
continue
}
// TODO: implement rewrite, timeout, mirror, corspolicy, retries
vs := &istio.HTTPRoute{}
for _, match := range r.Matches {
uri, err := createURIMatch(match)
if err != nil {
reportError(err)
return
}
headers, err := createHeadersMatch(match)
if err != nil {
reportError(err)
return
}
qp, err := createQueryParamsMatch(match)
if err != nil {
reportError(err)
return
}
method, err := createMethodMatch(match)
if err != nil {
reportError(err)
return
}
vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
Uri: uri,
Headers: headers,
QueryParams: qp,
Method: method,
})
}
for _, filter := range r.Filters {
switch filter.Type {
case k8s.HTTPRouteFilterRequestHeaderModifier:
vs.Headers = createHeadersFilter(filter.RequestHeaderModifier)
case k8s.HTTPRouteFilterRequestRedirect:
vs.Redirect = createRedirectFilter(filter.RequestRedirect)
case k8s.HTTPRouteFilterRequestMirror:
mirror, err := createMirrorFilter(filter.RequestMirror, ns, domain)
if err != nil {
reportError(err)
return
}
vs.Mirror = mirror
default:
reportError(&ConfigError{
Reason: InvalidFilter,
Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
})
return
}
}
zero := true
for _, w := range r.BackendRefs {
if w.Weight == nil || (w.Weight != nil && int(*w.Weight) != 0) {
zero = false
break
}
}
if zero && vs.Redirect == nil {
// The spec requires us to 503 when there are no >0 weight backends
vs.Fault = &istio.HTTPFaultInjection{Abort: &istio.HTTPFaultInjection_Abort{
Percentage: &istio.Percent{
Value: 100,
},
ErrorType: &istio.HTTPFaultInjection_Abort_HttpStatus{
HttpStatus: 503,
},
}}
}
route, err := buildHTTPDestination(r.BackendRefs, ns, domain, zero)
if err != nil {
reportError(err)
return
}
vs.Route = route
httproutes = append(httproutes, vs)
}
reportError(nil)
gatewayNames := referencesToInternalNames(parentRefs)
if len(gatewayNames) == 0 {
return
}
count := 0
for _, gw := range gatewayNames {
// for gateway routes, build one VS per gateway+host
routeMap := gatewayRoutes
routeKey := gw
if gw == "mesh" {
// for mesh routes, build one VS per namespace+host
routeMap = meshRoutes
routeKey = ns
}
if _, f := routeMap[routeKey]; !f {
routeMap[routeKey] = make(map[string]*config.Config)
}
// Create one VS per hostname with a single hostname.
// This ensures we can treat each hostname independently, as the spec requires
for _, host := range hosts {
if cfg := routeMap[routeKey][host]; cfg != nil {
// merge http routes
vs := cfg.Spec.(*istio.VirtualService)
vs.Http = append(vs.Http, httproutes...)
} else {
name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
routeMap[routeKey][host] = &config.Config{
Meta: config.Meta{
CreationTimestamp: obj.CreationTimestamp,
GroupVersionKind: gvk.VirtualService,
Name: name,
Annotations: routeMeta(obj),
Namespace: ns,
Domain: domain,
},
Spec: &istio.VirtualService{
Hosts: []string{host},
Gateways: []string{gw},
Http: httproutes,
},
}
count++
}
}
}
for _, vsByHost := range gatewayRoutes {
for _, cfg := range vsByHost {
vs := cfg.Spec.(*istio.VirtualService)
sortHTTPRoutes(vs.Http)
}
}
for _, vsByHost := range meshRoutes {
for _, cfg := range vsByHost {
vs := cfg.Spec.(*istio.VirtualService)
sortHTTPRoutes(vs.Http)
}
}
}
func routeMeta(obj config.Config) 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 len(routes[j].Match) != 0
}
if len(routes[j].Match) == 0 {
return false
}
m1, m2 := routes[i].Match[0], routes[j].Match[0]
len1, len2 := getURILength(m1), getURILength(m2)
if len1 == len2 {
if len(m1.Headers) == len(m2.Headers) {
return len(m1.QueryParams) > len(m2.QueryParams)
}
return len(m1.Headers) > len(m2.Headers)
}
return len1 > len2
})
}
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 parentMeta(obj config.Config, sectionName *k8s.SectionName) map[string]string {
name := fmt.Sprintf("%s/%s.%s", obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
if sectionName != nil {
name = fmt.Sprintf("%s/%s/%s.%s", obj.GroupVersionKind.Kind, obj.Name, *sectionName, obj.Namespace)
}
return map[string]string{
constants.InternalParentName: name,
}
}
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{"*"}
}
res := make([]string, 0, len(h))
for _, i := range h {
res = append(res, string(i))
}
return res
}
func toInternalParentReference(p k8s.ParentReference, localNamespace string) (parentKey, error) {
empty := parentKey{}
grp := defaultIfNil((*string)(p.Group), gvk.KubernetesGateway.Group)
kind := defaultIfNil((*string)(p.Kind), gvk.KubernetesGateway.Kind)
var ik config.GroupVersionKind
var ns string
// Currently supported types are Gateway and Mesh
if kind == gvk.KubernetesGateway.Kind && grp == gvk.KubernetesGateway.Group {
// Unset namespace means "same namespace"
ns = defaultIfNil((*string)(p.Namespace), localNamespace)
ik = gvk.KubernetesGateway
} else if kind == meshGVK.Kind && grp == meshGVK.Group {
ik = meshGVK
} else {
return empty, fmt.Errorf("unsupported parentKey: %v/%v", grp, kind)
}
return parentKey{
Kind: ik,
Name: string(p.Name),
Namespace: ns,
}, nil
}
func referenceAllowed(p *parentInfo, routeKind config.GroupVersionKind, parentKind config.GroupVersionKind, hostnames []k8s.Hostname, namespace string) error {
// First 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(p.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
for _, routeHostname := range hostnames {
for _, parentHostNamespace := range p.Hostnames {
spl := strings.Split(parentHostNamespace, "/")
parentNamespace, parentHostname := spl[0], spl[1]
hostnameMatch := host.Name(parentHostname).Matches(host.Name(routeHostname))
namespaceMatch := parentNamespace == "*" || parentNamespace == namespace
hostMatched = hostMatched || hostnameMatch
if hostnameMatch && namespaceMatch {
matched = true
break
}
}
}
if !matched {
if hostMatched {
return fmt.Errorf("hostnames matched parent hostname %q, but namespace %q is not allowed by the parent", p.OriginalHostname, namespace)
}
return fmt.Errorf("no hostnames matched parent hostname %q", p.OriginalHostname)
}
}
// Also make sure this route kind is allowed
if len(p.AllowedKinds) > 0 {
matched := false
for _, ak := range p.AllowedKinds {
if string(ak.Kind) == routeKind.Kind && defaultIfNil((*string)(ak.Group), gvk.GatewayClass.Group) == routeKind.Group {
matched = true
break
}
}
if !matched {
return fmt.Errorf("kind %v is not allowed", routeKind)
}
}
if parentKind == meshGVK {
for _, h := range hostnames {
if h == "*" {
return fmt.Errorf("mesh requires hostname to be set")
}
}
}
return nil
}
func extractParentReferenceInfo(gateways map[parentKey]map[k8s.SectionName]*parentInfo, routeRefs []k8s.ParentReference,
hostnames []k8s.Hostname, kind config.GroupVersionKind, localNamespace string) []routeParentReference {
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
}
appendParent := func(pr *parentInfo, pk parentKey) {
rpi := routeParentReference{
InternalName: pr.InternalName,
DeniedReason: referenceAllowed(pr, kind, pk.Kind, hostnames, localNamespace),
OriginalReference: ref,
}
if rpi.DeniedReason == nil {
// Record that we were able to bind to the parent
pr.AttachedRoutes++
}
parentRefs = append(parentRefs, rpi)
}
if ref.SectionName != nil {
// We are selecting a specific section, so attach just that section
if pr, f := gateways[ir][*ref.SectionName]; f {
appendParent(pr, ir)
}
} else {
// no section name set, match all sections
for _, pr := range gateways[ir] {
appendParent(pr, ir)
}
}
}
return parentRefs
}
func buildTCPVirtualService(obj config.Config, gateways map[parentKey]map[k8s.SectionName]*parentInfo, domain string) *config.Config {
route := obj.Spec.(*k8s.TCPRouteSpec)
parentRefs := extractParentReferenceInfo(gateways, route.ParentRefs, nil, gvk.TCPRoute, obj.Namespace)
reportError := func(routeErr *ConfigError) {
obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
rs := s.(*k8s.TCPRouteStatus)
rs.Parents = createRouteStatus(parentRefs, obj, rs.Parents, routeErr)
return rs
})
}
gatewayNames := referencesToInternalNames(parentRefs)
if len(gatewayNames) == 0 {
reportError(nil)
return nil
}
routes := []*istio.TCPRoute{}
for _, r := range route.Rules {
route, err := buildTCPDestination(r.BackendRefs, obj.Namespace, domain)
if err != nil {
reportError(err)
return nil
}
ir := &istio.TCPRoute{
Route: route,
}
routes = append(routes, ir)
}
reportError(nil)
vsConfig := config.Config{
Meta: config.Meta{
CreationTimestamp: obj.CreationTimestamp,
GroupVersionKind: gvk.VirtualService,
Name: fmt.Sprintf("%s-tcp-%s", obj.Name, constants.KubernetesGatewayName),
Annotations: routeMeta(obj),
Namespace: obj.Namespace,
Domain: domain,
},
Spec: &istio.VirtualService{
// We can use wildcard here since each listener can have at most one route bound to it, so we have
// a single VS per Gateway.
Hosts: []string{"*"},
Gateways: gatewayNames,
Tcp: routes,
},
}
return &vsConfig
}
func buildTLSVirtualService(obj config.Config, gateways map[parentKey]map[k8s.SectionName]*parentInfo, domain string) []config.Config {
route := obj.Spec.(*k8s.TLSRouteSpec)
parentRefs := extractParentReferenceInfo(gateways, route.ParentRefs, nil, gvk.TLSRoute, obj.Namespace)
reportError := func(routeErr *ConfigError) {
obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
rs := s.(*k8s.TLSRouteStatus)
rs.Parents = createRouteStatus(parentRefs, obj, rs.Parents, routeErr)
return rs
})
}
routes := []*istio.TLSRoute{}
for _, r := range route.Rules {
dest, err := buildTCPDestination(r.BackendRefs, obj.Namespace, domain)
if err != nil {
reportError(err)
return nil
}
if len(dest) == 0 {
return nil
}
ir := &istio.TLSRoute{
Match: buildTLSMatch(route.Hostnames),
Route: dest,
}
routes = append(routes, ir)
}
reportError(nil)
gatewayNames := referencesToInternalNames(parentRefs)
if len(gatewayNames) == 0 {
// TODO we need to properly return not admitted here
return nil
}
configs := make([]config.Config, 0, len(route.Hostnames))
for i, host := range hostnameToStringList(route.Hostnames) {
name := fmt.Sprintf("%s-tls-%d-%s", obj.Name, i, constants.KubernetesGatewayName)
// Create one VS per hostname with a single hostname.
// This ensures we can treat each hostname independently, as the spec requires
vsConfig := config.Config{
Meta: config.Meta{
CreationTimestamp: obj.CreationTimestamp,
GroupVersionKind: gvk.VirtualService,
Name: name,
Annotations: routeMeta(obj),
Namespace: obj.Namespace,
Domain: domain,
},
Spec: &istio.VirtualService{
Hosts: []string{host},
Gateways: gatewayNames,
Tls: routes,
},
}
configs = append(configs, vsConfig)
}
return configs
}
func buildTCPDestination(forwardTo []k8s.BackendRef, ns, domain string) ([]*istio.RouteDestination, *ConfigError) {
if forwardTo == nil {
return nil, nil
}
weights := []int{}
action := []k8s.BackendRef{}
for i, w := range forwardTo {
wt := 1
if w.Weight != nil {
wt = int(*w.Weight)
}
if wt == 0 {
continue
}
action = append(action, forwardTo[i])
weights = append(weights, wt)
}
weights = standardizeWeights(weights)
res := []*istio.RouteDestination{}
for i, fwd := range action {
dst, err := buildDestination(fwd, ns, domain)
if err != nil {
return nil, err
}
res = append(res, &istio.RouteDestination{
Destination: dst,
Weight: int32(weights[i]),
})
}
return res, 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 intSum(n []int) int {
r := 0
for _, i := range n {
r += i
}
return r
}
func buildHTTPDestination(forwardTo []k8s.HTTPBackendRef, ns string, domain string, totalZero bool) ([]*istio.HTTPRouteDestination, *ConfigError) {
if forwardTo == nil {
return nil, nil
}
weights := []int{}
action := []k8s.HTTPBackendRef{}
for i, w := range forwardTo {
wt := 1
if w.Weight != nil {
wt = int(*w.Weight)
}
// When total weight is zero, create destination to add falutInjection.
// When total weight is not zero, do not create the destination.
if wt == 0 && !totalZero {
continue
}
action = append(action, forwardTo[i])
weights = append(weights, wt)
}
weights = standardizeWeights(weights)
res := []*istio.HTTPRouteDestination{}
for i, fwd := range action {
dst, err := buildDestination(fwd.BackendRef, ns, domain)
if err != nil {
return nil, err
}
rd := &istio.HTTPRouteDestination{
Destination: dst,
Weight: int32(weights[i]),
}
for _, filter := range fwd.Filters {
switch filter.Type {
case k8s.HTTPRouteFilterRequestHeaderModifier:
rd.Headers = createHeadersFilter(filter.RequestHeaderModifier)
default:
return nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)}
}
}
res = append(res, rd)
}
return res, nil
}
func buildDestination(to k8s.BackendRef, ns, domain string) (*istio.Destination, *ConfigError) {
namespace := defaultIfNil((*string)(to.Namespace), ns)
if nilOrEqual((*string)(to.Group), "") && nilOrEqual((*string)(to.Kind), gvk.Service.Kind) {
// Service
if to.Port == nil {
// "Port is required when the referent is a Kubernetes Service."
return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
}
if strings.Contains(string(to.Name), ".") {
return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."}
}
return &istio.Destination{
// TODO: implement ReferencePolicy for cross namespace
Host: fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, domain),
Port: &istio.PortSelector{Number: uint32(*to.Port)},
}, nil
}
if nilOrEqual((*string)(to.Group), features.MCSAPIGroup) && nilOrEqual((*string)(to.Kind), "ServiceImport") {
// Service import
name := 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
name = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, domain)
}
if to.Port == nil {
// We don't know where to send without port
return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
}
if strings.Contains(string(to.Name), ".") {
return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."}
}
return &istio.Destination{
Host: name,
Port: &istio.PortSelector{Number: uint32(*to.Port)},
}, nil
}
if nilOrEqual((*string)(to.Group), gvk.ServiceEntry.Group) && nilOrEqual((*string)(to.Kind), "Hostname") {
// Hostname synthetic type
if to.Port == nil {
// We don't know where to send without port
return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"}
}
if to.Namespace != nil {
return nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"}
}
return &istio.Destination{
Host: string(to.Name),
Port: &istio.PortSelector{Number: uint32(*to.Port)},
}, nil
}
return nil, &ConfigError{
Reason: InvalidDestination,
Message: fmt.Sprintf("referencing unsupported backendRef: group %q kind %q", emptyIfNil((*string)(to.Group)), emptyIfNil((*string)(to.Kind))),
}
}
// standardizeWeights migrates a list of weights from relative weights, to weights out of 100
// In the event we cannot cleanly move to 100 denominator, we will round up weights in order. See test for details.
// TODO in the future we should probably just make VirtualService support relative weights directly
func standardizeWeights(weights []int) []int {
if len(weights) == 1 {
// Instead of setting weight=100 for a single destination, we will not set weight at all
return []int{0}
}
total := intSum(weights)
if total == 0 {
// All empty, fallback to even weight
for i := range weights {
weights[i] = 1
}
total = len(weights)
}
results := make([]int, 0, len(weights))
remainders := make([]float64, 0, len(weights))
for _, w := range weights {
perc := float64(w) / float64(total)
rounded := int(perc * 100)
remainders = append(remainders, (perc*100)-float64(rounded))
results = append(results, rounded)
}
remaining := 100 - intSum(results)
order := argsort(remainders)
for _, idx := range order {
if remaining <= 0 {
break
}
remaining--
results[idx]++
}
return results
}
type argSlice struct {
sort.Interface
idx []int
}
func (s argSlice) Swap(i, j int) {
s.Interface.Swap(i, j)
s.idx[i], s.idx[j] = s.idx[j], s.idx[i]
}
func argsort(n []float64) []int {
s := &argSlice{Interface: sort.Float64Slice(n), idx: make([]int, len(n))}
for i := range s.idx {
s.idx[i] = i
}
sort.Sort(sort.Reverse(s))
return s.idx
}
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(filter *k8s.HTTPRequestMirrorFilter, ns, domain string) (*istio.Destination, *ConfigError) {
if filter == nil {
return nil, nil
}
var weightOne int32 = 1
return buildDestination(k8s.BackendRef{
BackendObjectReference: filter.BackendRef,
Weight: &weightOne,
}, ns, domain)
}
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
resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_REQUEST_PORT}
}
return resp
}
func createHeadersFilter(filter *k8s.HTTPRequestHeaderFilter) *istio.Headers {
if filter == nil {
return nil
}
return &istio.Headers{
Request: &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[qp.Name] = &istio.StringMatch{
MatchType: &istio.StringMatch_Exact{Exact: qp.Value},
}
case k8s.QueryParamMatchRegularExpression:
res[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 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:
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)}
}
}
// getGatewayClass finds all gateway class that are owned by Istio
func getGatewayClasses(r *KubernetesResources) map[string]struct{} {
classes := map[string]struct{}{}
builtinClassExists := false
for _, obj := range r.GatewayClass {
gwc := obj.Spec.(*k8s.GatewayClassSpec)
if obj.Name == DefaultClassName {
builtinClassExists = true
}
if gwc.ControllerName == ControllerName {
// TODO we can add any settings we need here needed for the controller
// For now, we have none, so just add a struct
classes[obj.Name] = struct{}{}
obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
gcs := s.(*k8s.GatewayClassStatus)
gcs.Conditions = kstatus.UpdateConditionIfChanged(gcs.Conditions, metav1.Condition{
Type: string(k8s.GatewayClassConditionStatusAccepted),
Status: kstatus.StatusTrue,
ObservedGeneration: obj.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(k8s.GatewayClassConditionStatusAccepted),
Message: "Handled by Istio controller",
})
return gcs
})
}
}
if !builtinClassExists {
// Allow `istio` class without explicit GatewayClass. However, if it already exists then do not
// add it here, in case it points to a different controller.
classes[DefaultClassName] = struct{}{}
}
return classes
}
var meshGVK = config.GroupVersionKind{
Group: gvk.KubernetesGateway.Group,
Version: gvk.KubernetesGateway.Version,
Kind: "Mesh",
}
// parentKey holds info about a parentRef (ie 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 (ie Kubernetes Gateway name)
Name string
// Namespace is the namespace of the resource
Namespace string
}
// 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
Hostnames []string
// OriginalHostname is the unprocessed form of Hostnames; how it appeared in users' config
OriginalHostname string
// AttachedRoutes keeps track of how many routes are attached to this parent. This is tracked for status.
// Because this is mutate in the route generation, parentInfo must be passed as a pointer
AttachedRoutes int32
// ReportAttachedRoutes is a callback that should be triggered once all AttachedRoutes are computed, to
// actually store the attached route count in the status
ReportAttachedRoutes func()
}
// 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
// DeniedReason, if present, indicates why the reference was not valid
DeniedReason error
// OriginalReference contains the original reference
OriginalReference k8s.ParentReference
}
// referencesToInternalNames converts valid parent references to names that can be used in VirtualService
func referencesToInternalNames(parents []routeParentReference) []string {
ret := make([]string, 0, len(parents))
for _, p := range parents {
if p.DeniedReason != nil {
// We should filter this out
continue
}
ret = append(ret, p.InternalName)
}
// To ensure deterministic order, sort them
sort.Strings(ret)
return ret
}
func convertGateways(r *KubernetesResources) ([]config.Config, map[parentKey]map[k8s.SectionName]*parentInfo, sets.Set) {
// result stores our generated Istio Gateways
result := []config.Config{}
// gwMap stores an index to access parentInfo (which corresponds to a Kubernetes Gateway)
gwMap := map[parentKey]map[k8s.SectionName]*parentInfo{}
// namespaceLabelReferences keeps track of all namespace label keys referenced by Gateways. This is
// used to ensure we handle namespace updates for those keys.
namespaceLabelReferences := sets.New()
classes := getGatewayClasses(r)
for _, obj := range r.Gateway {
obj := obj
kgw := obj.Spec.(*k8s.GatewaySpec)
if _, f := classes[string(kgw.GatewayClassName)]; !f {
// No gateway class found, this may be meant for another controller; should be skipped.
continue
}
// Setup initial conditions to the success state. If we encounter errors, we will update this.
gatewayConditions := map[string]*condition{
string(k8s.GatewayConditionReady): {
reason: "ListenersValid",
message: "Listeners valid",
},
}
if isManaged(kgw) {
gatewayConditions[string(k8s.GatewayConditionScheduled)] = &condition{
error: &ConfigError{
Reason: "ResourcesPending",
Message: "Resources not yet deployed to the cluster",
},
setOnce: string(k8s.GatewayReasonNotReconciled), // Default reason
}
} else {
gatewayConditions[string(k8s.GatewayConditionScheduled)] = &condition{
reason: "ResourcesAvailable",
message: "Resources available",
}
}
servers := []*istio.Server{}
// Extract the addresses. A gateway will bind to a specific Service
gatewayServices, skippedAddresses := extractGatewayServices(r, kgw, obj)
invalidListeners := []k8s.SectionName{}
for i, l := range kgw.Listeners {
i := i
namespaceLabelReferences.InsertAll(getNamespaceLabelReferences(l.AllowedRoutes)...)
server, ok := buildListener(r, obj, l, i)
if !ok {
invalidListeners = append(invalidListeners, l.Name)
continue
}
meta := parentMeta(obj, &l.Name)
meta[model.InternalGatewayServiceAnnotation] = strings.Join(gatewayServices, ",")
// Each listener generates an Istio Gateway with a single Server. This allows binding to a specific listener.
gatewayConfig := config.Config{
Meta: config.Meta{
CreationTimestamp: obj.CreationTimestamp,
GroupVersionKind: gvk.Gateway,
Name: fmt.Sprintf("%s-%s-%s", obj.Name, constants.KubernetesGatewayName, l.Name),
Annotations: meta,
Namespace: obj.Namespace,
Domain: r.Domain,
},
Spec: &istio.Gateway{
Servers: []*istio.Server{server},
},
}
ref := parentKey{
Kind: gvk.KubernetesGateway,
Name: obj.Name,
Namespace: obj.Namespace,
}
if _, f := gwMap[ref]; !f {
gwMap[ref] = map[k8s.SectionName]*parentInfo{}
}
pri := &parentInfo{
InternalName: obj.Namespace + "/" + gatewayConfig.Name,
AllowedKinds: generateSupportedKinds(l),
Hostnames: server.Hosts,
OriginalHostname: emptyIfNil((*string)(l.Hostname)),
}
pri.ReportAttachedRoutes = func() {
reportListenerAttachedRoutes(i, obj, pri.AttachedRoutes)
}
gwMap[ref][l.Name] = pri
result = append(result, gatewayConfig)
servers = append(servers, server)
}
// If "gateway.istio.io/alias-for" annotation is present, any Route
// that binds to the gateway will bind to its alias instead.
// The typical usage is when the original gateway is not managed by the gateway controller
// but the ( generated ) alias is. This allows people to build their own
// gateway controllers on top of Istio Gateway Controller.
if obj.Annotations != nil && obj.Annotations[gatewayAliasForAnnotationKey] != "" {
ref := parentKey{
Kind: gvk.KubernetesGateway,
Name: obj.Annotations[gatewayAliasForAnnotationKey],
Namespace: obj.Namespace,
}
alias := parentKey{
Kind: gvk.KubernetesGateway,
Name: obj.Name,
Namespace: obj.Namespace,
}
gwMap[ref] = gwMap[alias]
}
internal, external, warnings := r.Context.ResolveGatewayInstances(obj.Namespace, gatewayServices, servers)
if len(skippedAddresses) > 0 {
warnings = append(warnings, fmt.Sprintf("Only Hostname is supported, ignoring %v", skippedAddresses))
}
if len(warnings) > 0 {
var msg 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, "; "))
}
gatewayConditions[string(k8s.GatewayConditionReady)].error = &ConfigError{
Reason: string(k8s.GatewayReasonAddressNotAssigned),
Message: msg,
}
} else if len(invalidListeners) > 0 {
gatewayConditions[string(k8s.GatewayConditionReady)].error = &ConfigError{
Reason: string(k8s.GatewayReasonListenersNotValid),
Message: fmt.Sprintf("Invalid listeners: %v", invalidListeners),
}
} else {
gatewayConditions[string(k8s.GatewayConditionReady)].message = fmt.Sprintf("Gateway valid, assigned to service(s) %s", humanReadableJoin(internal))
}
obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status {
gs := s.(*k8s.GatewayStatus)
addressesToReport := external
addrType := k8s.IPAddressType
if len(addressesToReport) == 0 {
// There are no external addresses, so report the internal ones
// TODO: should we always report both?
addressesToReport = internal
addrType = k8s.HostnameAddressType
}
gs.Addresses = make([]k8s.GatewayAddress, 0, len(addressesToReport))
for _, addr := range addressesToReport {
gs.Addresses = append(gs.Addresses, k8s.GatewayAddress{
Type: &addrType,
Value: addr,
})
}
return gs
})
reportGatewayCondition(obj, gatewayConditions)
}
// Insert a parent for Mesh references.
gwMap[parentKey{
Kind: meshGVK,
Name: "istio",
}] = map[k8s.SectionName]*parentInfo{
"": {
InternalName: "mesh",
},
}
return result, gwMap, namespaceLabelReferences
}
// 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.
func isManaged(gw *k8s.GatewaySpec) bool {
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
}
func extractGatewayServices(r *KubernetesResources, kgw *k8s.GatewaySpec, obj config.Config) ([]string, []string) {
if isManaged(kgw) {
return []string{fmt.Sprintf("%s.%s.svc.%v", obj.Name, obj.Namespace, r.Domain)}, nil
}
gatewayServices := []string{}
skippedAddresses := []string{}
for _, addr := range kgw.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, obj.Namespace, r.Domain)
}
gatewayServices = append(gatewayServices, fqdn)
}
return gatewayServices, skippedAddresses
}
// getNamespaceLabelReferences fetches all label keys used in namespace selectors. Return order may not be stable.
func getNamespaceLabelReferences(routes *k8s.AllowedRoutes) []string {
if routes == nil || routes.Namespaces == nil || routes.Namespaces.Selector == nil {
return nil
}
res := []string{}
for k := range routes.Namespaces.Selector.MatchLabels {
res = append(res, k)
}
for _, me := range routes.Namespaces.Selector.MatchExpressions {
res = append(res, me.Key)
}
return res
}
func buildListener(r *KubernetesResources, obj config.Config, l k8s.Listener, listenerIndex int) (*istio.Server, bool) {
listenerConditions := map[string]*condition{
string(k8s.ListenerConditionReady): {
reason: "ListenerReady",
message: "No errors found",
},
string(k8s.ListenerConditionDetached): {
reason: "ListenerReady",
message: "No errors found",
status: kstatus.StatusFalse,
},
string(k8s.ListenerConditionConflicted): {
reason: "ListenerReady",
message: "No errors found",
status: kstatus.StatusFalse,
},
string(k8s.ListenerConditionResolvedRefs): {
reason: "ListenerReady",
message: "No errors found",
},
}
defer reportListenerCondition(listenerIndex, l, obj, listenerConditions)
tls, err := buildTLS(l.TLS, obj.Namespace, isAutoPassthrough(obj, l))
if err != nil {
listenerConditions[string(k8s.ListenerConditionReady)].error = &ConfigError{
Reason: string(k8s.ListenerReasonInvalid),
Message: err.Message,
}
listenerConditions[string(k8s.ListenerConditionResolvedRefs)].error = &ConfigError{
Reason: string(k8s.ListenerReasonInvalidCertificateRef),
Message: err.Message,
}
return nil, false
}
hostnames := buildHostnameMatch(obj.Namespace, r, l)
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: listenerProtocolToIstio(l.Protocol),
},
Hosts: hostnames,
Tls: tls,
}
return server, true
}
// isAutoPassthrough determines if a listener should use auto passthrough mode. This is used for
// multi-network. In the Istio API, this is an explicit tls.Mode. However, this mode is not part of
// the gateway-api, and leaks implementation details. We already have an API to declare a Gateway as
// a multinetwork gateway, so we will use this as a signal.
// A user who wishes to expose multinetwork connectivity should create a listener with port 15443 (by default, overridable by label),
// and declare it as PASSTRHOUGH
func isAutoPassthrough(obj config.Config, l k8s.Listener) bool {
_, networkSet := obj.Labels[label.TopologyNetwork.Name]
if !networkSet {
return false
}
expectedPort := "15443"
if port, f := obj.Labels[label.NetworkingGatewayPort.Name]; f {
expectedPort = port
}
return fmt.Sprint(l.Port) == expectedPort
}
func listenerProtocolToIstio(protocol k8s.ProtocolType) string {
// Currently, all gateway-api protocols are valid Istio protocols.
return string(protocol)
}
func buildTLS(tls *k8s.GatewayTLSConfig, namespace string, 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
}
switch mode {
case k8s.TLSModeTerminate:
out.Mode = istio.ServerTLSSettings_SIMPLE
if len(tls.CertificateRefs) != 1 {
// This is required in the API, should be rejected in validation
return nil, &ConfigError{Reason: InvalidConfiguration, Message: "exactly 1 certificateRefs should be present for TLS termination"}
}
cred, err := buildSecretReference(*tls.CertificateRefs[0], namespace)
if err != nil {
return nil, err
}
out.CredentialName = cred
case k8s.TLSModePassthrough:
out.Mode = istio.ServerTLSSettings_PASSTHROUGH
if isAutoPassthrough {
out.Mode = istio.ServerTLSSettings_AUTO_PASSTHROUGH
}
}
return out, nil
}
func buildSecretReference(ref k8s.SecretObjectReference, defaultNamespace string) (string, *ConfigError) {
if !nilOrEqual((*string)(ref.Group), gvk.Secret.Group) || !nilOrEqual((*string)(ref.Kind), gvk.Secret.Kind) {
return "", &ConfigError{Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, only secret is allowed", objectReferenceString(ref))}
}
return credentials.ToKubernetesGatewayResource(defaultIfNil((*string)(ref.Namespace), defaultNamespace), string(ref.Name)), nil
}
func objectReferenceString(ref k8s.SecretObjectReference) string {
return fmt.Sprintf("%s/%s/%s.%s",
emptyIfNil((*string)(ref.Group)),
emptyIfNil((*string)(ref.Kind)),
ref.Name,
emptyIfNil((*string)(ref.Namespace)))
}
func parentRefString(ref k8s.ParentReference) string {
return fmt.Sprintf("%s/%s/%s/%s.%s",
emptyIfNil((*string)(ref.Group)),
emptyIfNil((*string)(ref.Kind)),
ref.Name,
emptyIfNil((*string)(ref.SectionName)),
emptyIfNil((*string)(ref.Namespace)))
}
// buildHostnameMatch generates a VirtualService.spec.hosts section from a listener
func buildHostnameMatch(localNamespace string, r *KubernetesResources, 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(localNamespace, r, l.AllowedRoutes) {
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(localNamespace string, r *KubernetesResources, 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{}
for _, ns := range r.Namespaces {
if ls.Matches(toNamespaceSet(ns.Name, ns.Labels)) {
namespaces = append(namespaces, ns.Name)
}
}
// Ensure stable order
sort.Strings(namespaces)
return namespaces
}
func emptyIfNil(s *string) string {
return defaultIfNil(s, "")
}
func defaultIfNil(s *string, d string) string {
if s != nil {
return *s
}
return d
}
func nilOrEqual(have *string, expected string) bool {
return have == nil || *have == expected
}
func StrPointer(s string) *string {
return &s
}
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
}