pkg/ingress/kube/common/model.go (286 lines of code) (raw):

// Copyright (c) 2022 Alibaba Group Holding Ltd. // // 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 common import ( "errors" "fmt" "time" "istio.io/istio/pilot/pkg/model" "istio.io/istio/pkg/cluster" "istio.io/istio/pkg/config" "istio.io/istio/pkg/config/schema/collection" "istio.io/istio/pkg/config/schema/collections" "k8s.io/apimachinery/pkg/labels" . "github.com/alibaba/higress/pkg/ingress/log" ) type PathType string const ( prefixAnnotation = "internal.higress.io/" ClusterIdAnnotation = prefixAnnotation + "cluster-id" RawClusterIdAnnotation = prefixAnnotation + "raw-cluster-id" HostAnnotation = prefixAnnotation + "host" // PrefixMatchRegex optionally matches "/..." at the end of a path. // regex taken from https://github.com/projectcontour/contour/blob/2b3376449bedfea7b8cea5fbade99fb64009c0f6/internal/envoy/v3/route.go#L59 PrefixMatchRegex = `((\/).*)?` DefaultHost = "*" DefaultPath = "/" // DefaultIngressClass defines the default class used in the nginx ingress controller. // For compatible ingress nginx case, istio controller will watch ingresses whose ingressClass is // nginx, empty string or unset. DefaultIngressClass = "nginx" Exact PathType = "exact" Prefix PathType = "prefix" // PrefixRegex :if PathType is PrefixRegex, then the /foo/bar/[A-Z0-9]{3} is actually ^/foo/bar/[A-Z0-9]{3}.* PrefixRegex PathType = "prefixRegex" // FullPathRegex :if PathType is FullPathRegex, then the /foo/bar/[A-Z0-9]{3} is actually ^/foo/bar/[A-Z0-9]{3}$ FullPathRegex PathType = "fullPathRegex" DefaultStatusUpdateInterval = 10 * time.Second AppKey = "app" AppValue = "higress-gateway" SvcHostNameSuffix = ".multiplenic" ) var ( ErrUnsupportedOp = errors.New("unsupported operation: the ingress config store is a read-only view") ErrNotFound = errors.New("item not found") Schemas = collection.SchemasFor( collections.VirtualService, collections.Gateway, collections.DestinationRule, collections.EnvoyFilter, ) clusterPrefix string SvcLabelSelector labels.Selector ) func init() { set := labels.Set{ AppKey: AppValue, } SvcLabelSelector = labels.SelectorFromSet(set) } type Options struct { Enable bool ClusterId cluster.ID IngressClass string GatewayClass string WatchNamespace string RawClusterId string EnableStatus bool SystemNamespace string GatewaySelectorKey string GatewaySelectorValue string GatewayHttpPort uint32 GatewayHttpsPort uint32 } type BasicAuthRules struct { Rules []*Rule `json:"_rules_"` } type Rule struct { Realm string `json:"realm"` MatchRoute []string `json:"_match_route_"` Credentials []string `json:"credentials"` Encrypted bool `json:"encrypted"` } type IngressDomainCache struct { // host as key Valid map[string]*IngressDomainBuilder Invalid []model.IngressDomain } func NewIngressDomainCache() *IngressDomainCache { return &IngressDomainCache{ Valid: map[string]*IngressDomainBuilder{}, } } func (i *IngressDomainCache) Extract() model.IngressDomainCollection { var valid []model.IngressDomain for _, builder := range i.Valid { valid = append(valid, builder.Build()) } return model.IngressDomainCollection{ Valid: valid, Invalid: i.Invalid, } } type ConvertOptions struct { HostWithRule2Ingress map[string]*config.Config HostWithTls2Ingress map[string]*config.Config Gateways map[string]*WrapperGateway IngressDomainCache *IngressDomainCache // the host, path, headers, params of rule => ingress Route2Ingress map[string]*WrapperConfigWithRuleKey // Record valid/invalid routes from ingress IngressRouteCache *IngressRouteCache VirtualServices map[string]*WrapperVirtualService // host -> routes HTTPRoutes map[string][]*WrapperHTTPRoute CanaryIngresses []*WrapperConfig Service2TrafficPolicy map[ServiceKey]*WrapperTrafficPolicy HasDefaultBackend bool } type IngressRouteCache struct { routes map[string]*IngressRouteBuilder invalid []model.IngressRoute } func NewIngressRouteCache() *IngressRouteCache { return &IngressRouteCache{ routes: map[string]*IngressRouteBuilder{}, } } func (i *IngressRouteCache) New(route *WrapperHTTPRoute) *IngressRouteBuilder { return &IngressRouteBuilder{ ClusterId: route.ClusterId, RouteName: route.HTTPRoute.Name, Path: route.OriginPath, PathType: string(route.OriginPathType), Host: route.Host, Event: Normal, Ingress: route.WrapperConfig.Config, } } func (i *IngressRouteCache) NewAndAdd(route *WrapperHTTPRoute) { routeBuilder := &IngressRouteBuilder{ ClusterId: route.ClusterId, RouteName: route.HTTPRoute.Name, Path: route.OriginPath, PathType: string(route.OriginPathType), Host: route.Host, Event: Normal, Ingress: route.WrapperConfig.Config, } // Only care about the first destination destination := route.HTTPRoute.Route[0].Destination svcName, namespace, _ := SplitServiceFQDN(destination.Host) routeBuilder.ServiceList = []model.BackendService{ { Name: svcName, Namespace: namespace, Port: destination.Port.Number, Weight: route.HTTPRoute.Route[0].Weight, }, } i.routes[route.HTTPRoute.Name] = routeBuilder } func (i *IngressRouteCache) Add(builder *IngressRouteBuilder) { if builder.Event != Normal { builder.RouteName = "invalid-route" i.invalid = append(i.invalid, builder.Build()) return } i.routes[builder.RouteName] = builder } func (i *IngressRouteCache) Update(route *WrapperHTTPRoute) { oldBuilder, exist := i.routes[route.HTTPRoute.Name] if !exist { // Never happen IngressLog.Errorf("ingress route builder %s not found.", route.HTTPRoute.Name) return } var serviceList []model.BackendService for _, routeDestination := range route.HTTPRoute.Route { serviceList = append(serviceList, ConvertBackendService(routeDestination)) } oldBuilder.ServiceList = serviceList } func (i *IngressRouteCache) Delete(route *WrapperHTTPRoute) { delete(i.routes, route.HTTPRoute.Name) } func (i *IngressRouteCache) Extract() model.IngressRouteCollection { var valid []model.IngressRoute for _, builder := range i.routes { valid = append(valid, builder.Build()) } return model.IngressRouteCollection{ Valid: valid, Invalid: i.invalid, } } type IngressRouteBuilder struct { ClusterId cluster.ID RouteName string Host string PathType string Path string ServiceList []model.BackendService PortName string Event Event Ingress *config.Config PreIngress *config.Config } func (i *IngressRouteBuilder) Build() model.IngressRoute { errorMsg := "" switch i.Event { case DuplicatedRoute: preClusterId := GetClusterId(i.PreIngress.Annotations) errorMsg = fmt.Sprintf("host %s and path %s in ingress %s/%s within cluster %s is already defined in ingress %s/%s within cluster %s", i.Host, i.Path, i.Ingress.Namespace, i.Ingress.Name, i.ClusterId, i.PreIngress.Namespace, i.PreIngress.Name, preClusterId) case InvalidBackendService: errorMsg = fmt.Sprintf("backend service of host %s and path %s is invalid defined in ingress %s/%s within cluster %s", i.Host, i.Path, i.Ingress.Namespace, i.Ingress.Name, i.ClusterId, ) case PortNameResolveError: errorMsg = fmt.Sprintf("service port name %s of host %s and path %s resolves error defined in ingress %s/%s within cluster %s", i.PortName, i.Host, i.Path, i.Ingress.Namespace, i.Ingress.Name, i.ClusterId, ) } ingressRoute := model.IngressRoute{ Name: i.RouteName, Host: i.Host, Path: i.Path, PathType: i.PathType, DestinationType: model.Single, ServiceList: i.ServiceList, Error: errorMsg, } // backward compatibility if len(ingressRoute.ServiceList) > 0 { ingressRoute.ServiceName = i.ServiceList[0].Name } if len(ingressRoute.ServiceList) > 1 { ingressRoute.DestinationType = model.Multiple } return ingressRoute } type Protocol string const ( HTTP Protocol = "HTTP" HTTPS Protocol = "HTTPS" ) type IngressDomainBuilder struct { ClusterId cluster.ID Host string Protocol Protocol Event Event // format is cluster id/namespace/name SecretName string Ingress *config.Config PreIngress *config.Config } func (i *IngressDomainBuilder) Build() model.IngressDomain { errorMsg := "" switch i.Event { case MissingSecret: errorMsg = fmt.Sprintf("tls field of host %s defined in ingress %s/%s within cluster %s misses secret", i.Host, i.Ingress.Namespace, i.Ingress.Name, i.ClusterId, ) case DuplicatedTls: preClusterId := GetClusterId(i.PreIngress.Annotations) errorMsg = fmt.Sprintf("tls field of host %s defined in ingress %s/%s within cluster %s "+ "is conflicted with ingress %s/%s within cluster %s", i.Host, i.Ingress.Namespace, i.Ingress.Name, i.ClusterId, i.PreIngress.Namespace, i.PreIngress.Name, preClusterId, ) } return model.IngressDomain{ Host: i.Host, Protocol: string(i.Protocol), SecretName: i.SecretName, CreationTime: i.Ingress.CreationTimestamp, Error: errorMsg, } }