pilot/pkg/config/kube/gateway/conversion.go (1,324 lines of code) (raw):

// 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 }