mirror of
https://github.com/alibaba/higress.git
synced 2026-03-04 08:30:48 +08:00
Add ingress (#18)
This commit is contained in:
846
ingress/config/ingress_config.go
Normal file
846
ingress/config/ingress_config.go
Normal file
@@ -0,0 +1,846 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
wasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
|
||||
httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
networkingutil "istio.io/istio/pilot/pkg/networking/util"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/ingress/kube/common"
|
||||
"github.com/alibaba/higress/ingress/kube/ingress"
|
||||
"github.com/alibaba/higress/ingress/kube/ingressv1"
|
||||
secretkube "github.com/alibaba/higress/ingress/kube/secret/kube"
|
||||
"github.com/alibaba/higress/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
)
|
||||
|
||||
var (
|
||||
_ model.ConfigStoreCache = &IngressConfig{}
|
||||
_ model.IngressStore = &IngressConfig{}
|
||||
)
|
||||
|
||||
type IngressConfig struct {
|
||||
// key: cluster id
|
||||
remoteIngressControllers map[string]common.IngressController
|
||||
mutex sync.RWMutex
|
||||
|
||||
ingressRouteCache model.IngressRouteCollection
|
||||
ingressDomainCache model.IngressDomainCollection
|
||||
|
||||
localKubeClient kube.Client
|
||||
|
||||
virtualServiceHandlers []model.EventHandler
|
||||
gatewayHandlers []model.EventHandler
|
||||
destinationRuleHandlers []model.EventHandler
|
||||
envoyFilterHandlers []model.EventHandler
|
||||
watchErrorHandler cache.WatchErrorHandler
|
||||
|
||||
cachedEnvoyFilters []config.Config
|
||||
|
||||
watchedSecretSet sets.Set
|
||||
|
||||
XDSUpdater model.XDSUpdater
|
||||
|
||||
annotationHandler annotations.AnnotationHandler
|
||||
|
||||
globalGatewayName string
|
||||
|
||||
namespace string
|
||||
|
||||
clusterId string
|
||||
}
|
||||
|
||||
func NewIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater, namespace, clusterId string) *IngressConfig {
|
||||
if clusterId == "Kubernetes" {
|
||||
clusterId = ""
|
||||
}
|
||||
return &IngressConfig{
|
||||
remoteIngressControllers: make(map[string]common.IngressController),
|
||||
localKubeClient: localKubeClient,
|
||||
XDSUpdater: XDSUpdater,
|
||||
annotationHandler: annotations.NewAnnotationHandlerManager(),
|
||||
clusterId: clusterId,
|
||||
globalGatewayName: namespace + "/" +
|
||||
common.CreateConvertedName(clusterId, "global"),
|
||||
watchedSecretSet: sets.NewSet(),
|
||||
namespace: namespace,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressConfig) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
|
||||
IngressLog.Infof("register resource %v", kind)
|
||||
if kind != gvk.VirtualService && kind != gvk.Gateway &&
|
||||
kind != gvk.DestinationRule && kind != gvk.EnvoyFilter {
|
||||
return
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case gvk.VirtualService:
|
||||
m.virtualServiceHandlers = append(m.virtualServiceHandlers, f)
|
||||
|
||||
case gvk.Gateway:
|
||||
m.gatewayHandlers = append(m.gatewayHandlers, f)
|
||||
|
||||
case gvk.DestinationRule:
|
||||
m.destinationRuleHandlers = append(m.destinationRuleHandlers, f)
|
||||
|
||||
case gvk.EnvoyFilter:
|
||||
m.envoyFilterHandlers = append(m.envoyFilterHandlers, f)
|
||||
}
|
||||
|
||||
for _, remoteIngressController := range m.remoteIngressControllers {
|
||||
remoteIngressController.RegisterEventHandler(kind, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressConfig) AddLocalCluster(options common.Options) common.IngressController {
|
||||
secretController := secretkube.NewController(m.localKubeClient, options)
|
||||
secretController.AddEventHandler(m.ReflectSecretChanges)
|
||||
|
||||
var ingressController common.IngressController
|
||||
v1 := common.V1Available(m.localKubeClient)
|
||||
if !v1 {
|
||||
ingressController = ingress.NewController(m.localKubeClient, m.localKubeClient, options, secretController)
|
||||
} else {
|
||||
ingressController = ingressv1.NewController(m.localKubeClient, m.localKubeClient, options, secretController)
|
||||
}
|
||||
|
||||
m.remoteIngressControllers[options.ClusterId] = ingressController
|
||||
return ingressController
|
||||
}
|
||||
|
||||
func (m *IngressConfig) InitializeCluster(ingressController common.IngressController, stop <-chan struct{}) error {
|
||||
for _, handler := range m.virtualServiceHandlers {
|
||||
ingressController.RegisterEventHandler(gvk.VirtualService, handler)
|
||||
}
|
||||
for _, handler := range m.gatewayHandlers {
|
||||
ingressController.RegisterEventHandler(gvk.Gateway, handler)
|
||||
}
|
||||
for _, handler := range m.destinationRuleHandlers {
|
||||
ingressController.RegisterEventHandler(gvk.DestinationRule, handler)
|
||||
}
|
||||
for _, handler := range m.envoyFilterHandlers {
|
||||
ingressController.RegisterEventHandler(gvk.EnvoyFilter, handler)
|
||||
}
|
||||
|
||||
_ = ingressController.SetWatchErrorHandler(m.watchErrorHandler)
|
||||
|
||||
go ingressController.Run(stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressConfig) List(typ config.GroupVersionKind, namespace string) ([]config.Config, error) {
|
||||
if typ != gvk.Gateway &&
|
||||
typ != gvk.VirtualService &&
|
||||
typ != gvk.DestinationRule &&
|
||||
typ != gvk.EnvoyFilter {
|
||||
return nil, common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
// Currently, only support list all namespaces gateways or virtualservices.
|
||||
if namespace != "" {
|
||||
IngressLog.Warnf("ingress store only support type %s of all namespace.", typ)
|
||||
return nil, common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
if typ == gvk.EnvoyFilter {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
IngressLog.Infof("resource type %s, configs number %d", typ, len(m.cachedEnvoyFilters))
|
||||
return m.cachedEnvoyFilters, nil
|
||||
}
|
||||
|
||||
var configs []config.Config
|
||||
m.mutex.RLock()
|
||||
for _, ingressController := range m.remoteIngressControllers {
|
||||
configs = append(configs, ingressController.List()...)
|
||||
}
|
||||
m.mutex.RUnlock()
|
||||
|
||||
common.SortIngressByCreationTime(configs)
|
||||
wrapperConfigs := m.createWrapperConfigs(configs)
|
||||
|
||||
IngressLog.Infof("resource type %s, configs number %d", typ, len(wrapperConfigs))
|
||||
switch typ {
|
||||
case gvk.Gateway:
|
||||
return m.convertGateways(wrapperConfigs), nil
|
||||
case gvk.VirtualService:
|
||||
return m.convertVirtualService(wrapperConfigs), nil
|
||||
case gvk.DestinationRule:
|
||||
return m.convertDestinationRule(wrapperConfigs), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *IngressConfig) createWrapperConfigs(configs []config.Config) []common.WrapperConfig {
|
||||
var wrapperConfigs []common.WrapperConfig
|
||||
|
||||
// Init global context
|
||||
clusterSecretListers := map[string]listersv1.SecretLister{}
|
||||
clusterServiceListers := map[string]listersv1.ServiceLister{}
|
||||
m.mutex.RLock()
|
||||
for clusterId, controller := range m.remoteIngressControllers {
|
||||
clusterSecretListers[clusterId] = controller.SecretLister()
|
||||
clusterServiceListers[clusterId] = controller.ServiceLister()
|
||||
}
|
||||
m.mutex.RUnlock()
|
||||
globalContext := &annotations.GlobalContext{
|
||||
WatchedSecrets: sets.NewSet(),
|
||||
ClusterSecretLister: clusterSecretListers,
|
||||
ClusterServiceList: clusterServiceListers,
|
||||
}
|
||||
|
||||
for idx := range configs {
|
||||
rawConfig := configs[idx]
|
||||
annotationsConfig := &annotations.Ingress{
|
||||
Meta: annotations.Meta{
|
||||
Namespace: rawConfig.Namespace,
|
||||
Name: rawConfig.Name,
|
||||
RawClusterId: common.GetRawClusterId(rawConfig.Annotations),
|
||||
ClusterId: common.GetClusterId(rawConfig.Annotations),
|
||||
},
|
||||
}
|
||||
_ = m.annotationHandler.Parse(rawConfig.Annotations, annotationsConfig, globalContext)
|
||||
wrapperConfigs = append(wrapperConfigs, common.WrapperConfig{
|
||||
Config: &rawConfig,
|
||||
AnnotationsConfig: annotationsConfig,
|
||||
})
|
||||
}
|
||||
|
||||
m.mutex.Lock()
|
||||
m.watchedSecretSet = globalContext.WatchedSecrets
|
||||
m.mutex.Unlock()
|
||||
|
||||
return wrapperConfigs
|
||||
}
|
||||
|
||||
func (m *IngressConfig) convertGateways(configs []common.WrapperConfig) []config.Config {
|
||||
convertOptions := common.ConvertOptions{
|
||||
IngressDomainCache: common.NewIngressDomainCache(),
|
||||
Gateways: map[string]*common.WrapperGateway{},
|
||||
}
|
||||
|
||||
for idx := range configs {
|
||||
cfg := configs[idx]
|
||||
clusterId := common.GetClusterId(cfg.Config.Annotations)
|
||||
m.mutex.RLock()
|
||||
ingressController := m.remoteIngressControllers[clusterId]
|
||||
m.mutex.RUnlock()
|
||||
if ingressController == nil {
|
||||
continue
|
||||
}
|
||||
if err := ingressController.ConvertGateway(&convertOptions, &cfg); err != nil {
|
||||
IngressLog.Errorf("Convert ingress %s/%s to gateway fail in cluster %s, err %v", cfg.Config.Namespace, cfg.Config.Name, clusterId, err)
|
||||
}
|
||||
}
|
||||
|
||||
// apply annotation
|
||||
for _, wrapperGateway := range convertOptions.Gateways {
|
||||
m.annotationHandler.ApplyGateway(wrapperGateway.Gateway, wrapperGateway.WrapperConfig.AnnotationsConfig)
|
||||
}
|
||||
|
||||
m.mutex.Lock()
|
||||
m.ingressDomainCache = convertOptions.IngressDomainCache.Extract()
|
||||
m.mutex.Unlock()
|
||||
|
||||
out := make([]config.Config, 0, len(convertOptions.Gateways))
|
||||
for _, gateway := range convertOptions.Gateways {
|
||||
cleanHost := common.CleanHost(gateway.Host)
|
||||
out = append(out, config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost),
|
||||
Namespace: m.namespace,
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: gateway.ClusterId,
|
||||
common.HostAnnotation: gateway.Host,
|
||||
},
|
||||
},
|
||||
Spec: gateway.Gateway,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []config.Config {
|
||||
convertOptions := common.ConvertOptions{
|
||||
HostAndPath2Ingress: map[string]*config.Config{},
|
||||
IngressRouteCache: common.NewIngressRouteCache(),
|
||||
VirtualServices: map[string]*common.WrapperVirtualService{},
|
||||
HTTPRoutes: map[string][]*common.WrapperHTTPRoute{},
|
||||
}
|
||||
|
||||
// convert http route
|
||||
for idx := range configs {
|
||||
cfg := configs[idx]
|
||||
clusterId := common.GetClusterId(cfg.Config.Annotations)
|
||||
m.mutex.RLock()
|
||||
ingressController := m.remoteIngressControllers[clusterId]
|
||||
m.mutex.RUnlock()
|
||||
if ingressController == nil {
|
||||
continue
|
||||
}
|
||||
if err := ingressController.ConvertHTTPRoute(&convertOptions, &cfg); err != nil {
|
||||
IngressLog.Errorf("Convert ingress %s/%s to HTTP route fail in cluster %s, err %v", cfg.Config.Namespace, cfg.Config.Name, clusterId, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply annotation on routes
|
||||
for _, routes := range convertOptions.HTTPRoutes {
|
||||
for _, route := range routes {
|
||||
m.annotationHandler.ApplyRoute(route.HTTPRoute, route.WrapperConfig.AnnotationsConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply canary ingress
|
||||
if len(configs) > len(convertOptions.CanaryIngresses) {
|
||||
m.applyCanaryIngresses(&convertOptions)
|
||||
}
|
||||
|
||||
// Normalize weighted cluster to make sure the sum of weight is 100.
|
||||
for _, host := range convertOptions.HTTPRoutes {
|
||||
for _, route := range host {
|
||||
normalizeWeightedCluster(convertOptions.IngressRouteCache, route)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply spec default backend.
|
||||
if convertOptions.HasDefaultBackend {
|
||||
for idx := range configs {
|
||||
cfg := configs[idx]
|
||||
clusterId := common.GetClusterId(cfg.Config.Annotations)
|
||||
m.mutex.RLock()
|
||||
ingressController := m.remoteIngressControllers[clusterId]
|
||||
m.mutex.RUnlock()
|
||||
if ingressController == nil {
|
||||
continue
|
||||
}
|
||||
if err := ingressController.ApplyDefaultBackend(&convertOptions, &cfg); err != nil {
|
||||
IngressLog.Errorf("Apply default backend on ingress %s/%s fail in cluster %s, err %v", cfg.Config.Namespace, cfg.Config.Name, clusterId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply annotation on virtual services
|
||||
for _, virtualService := range convertOptions.VirtualServices {
|
||||
m.annotationHandler.ApplyVirtualServiceHandler(virtualService.VirtualService, virtualService.WrapperConfig.AnnotationsConfig)
|
||||
}
|
||||
|
||||
// Apply app root for per host.
|
||||
m.applyAppRoot(&convertOptions)
|
||||
|
||||
// Apply internal active redirect for error page.
|
||||
m.applyInternalActiveRedirect(&convertOptions)
|
||||
|
||||
m.mutex.Lock()
|
||||
m.ingressRouteCache = convertOptions.IngressRouteCache.Extract()
|
||||
m.mutex.Unlock()
|
||||
|
||||
// Convert http route to virtual service
|
||||
out := make([]config.Config, 0, len(convertOptions.HTTPRoutes))
|
||||
for host, routes := range convertOptions.HTTPRoutes {
|
||||
if len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cleanHost := common.CleanHost(host)
|
||||
// namespace/name, name format: (istio cluster id)-host
|
||||
gateways := []string{m.namespace + "/" +
|
||||
common.CreateConvertedName(m.clusterId, cleanHost),
|
||||
common.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost)}
|
||||
if host != "*" {
|
||||
gateways = append(gateways, m.globalGatewayName)
|
||||
}
|
||||
|
||||
wrapperVS, exist := convertOptions.VirtualServices[host]
|
||||
if !exist {
|
||||
IngressLog.Warnf("virtual service for host %s does not exist.", host)
|
||||
}
|
||||
vs := wrapperVS.VirtualService
|
||||
vs.Gateways = gateways
|
||||
|
||||
for _, route := range routes {
|
||||
vs.Http = append(vs.Http, route.HTTPRoute)
|
||||
}
|
||||
|
||||
firstRoute := routes[0]
|
||||
out = append(out, config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, firstRoute.WrapperConfig.Config.Namespace, firstRoute.WrapperConfig.Config.Name, cleanHost),
|
||||
Namespace: m.namespace,
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: firstRoute.ClusterId,
|
||||
},
|
||||
},
|
||||
Spec: vs,
|
||||
})
|
||||
}
|
||||
|
||||
// We generate some specific envoy filter here to avoid duplicated computation.
|
||||
m.convertEnvoyFilter(&convertOptions)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions) {
|
||||
var envoyFilters []config.Config
|
||||
mappings := map[string]*common.Rule{}
|
||||
|
||||
for _, routes := range convertOptions.HTTPRoutes {
|
||||
for _, route := range routes {
|
||||
if strings.HasSuffix(route.HTTPRoute.Name, "app-root") {
|
||||
continue
|
||||
}
|
||||
|
||||
auth := route.WrapperConfig.AnnotationsConfig.Auth
|
||||
if auth == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
key := auth.AuthSecret.String() + "/" + auth.AuthRealm
|
||||
if rule, exist := mappings[key]; !exist {
|
||||
mappings[key] = &common.Rule{
|
||||
Realm: auth.AuthRealm,
|
||||
MatchRoute: []string{route.HTTPRoute.Name},
|
||||
Credentials: auth.Credentials,
|
||||
Encrypted: true,
|
||||
}
|
||||
} else {
|
||||
rule.MatchRoute = append(rule.MatchRoute, route.HTTPRoute.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IngressLog.Infof("Found %d number of basic auth", len(mappings))
|
||||
if len(mappings) > 0 {
|
||||
rules := &common.BasicAuthRules{}
|
||||
for _, rule := range mappings {
|
||||
rules.Rules = append(rules.Rules, rule)
|
||||
}
|
||||
|
||||
basicAuth, err := constructBasicAuthEnvoyFilter(rules, m.namespace)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Construct basic auth filter error %v", err)
|
||||
} else {
|
||||
envoyFilters = append(envoyFilters, *basicAuth)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Support other envoy filters
|
||||
|
||||
m.mutex.Lock()
|
||||
m.cachedEnvoyFilters = envoyFilters
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) []config.Config {
|
||||
convertOptions := common.ConvertOptions{
|
||||
Service2TrafficPolicy: map[common.ServiceKey]*common.WrapperTrafficPolicy{},
|
||||
}
|
||||
|
||||
// Convert destination from service within ingress rule.
|
||||
for idx := range configs {
|
||||
cfg := configs[idx]
|
||||
clusterId := common.GetClusterId(cfg.Config.Annotations)
|
||||
m.mutex.RLock()
|
||||
ingressController := m.remoteIngressControllers[clusterId]
|
||||
m.mutex.RUnlock()
|
||||
if ingressController == nil {
|
||||
continue
|
||||
}
|
||||
if err := ingressController.ConvertTrafficPolicy(&convertOptions, &cfg); err != nil {
|
||||
IngressLog.Errorf("Convert ingress %s/%s to destination rule fail in cluster %s, err %v", cfg.Config.Namespace, cfg.Config.Name, clusterId, err)
|
||||
}
|
||||
}
|
||||
|
||||
IngressLog.Debugf("traffic policy number %d", len(convertOptions.Service2TrafficPolicy))
|
||||
|
||||
for _, wrapperTrafficPolicy := range convertOptions.Service2TrafficPolicy {
|
||||
m.annotationHandler.ApplyTrafficPolicy(wrapperTrafficPolicy.TrafficPolicy, wrapperTrafficPolicy.WrapperConfig.AnnotationsConfig)
|
||||
}
|
||||
|
||||
// Merge multi-port traffic policy per service into one destination rule.
|
||||
destinationRules := map[string]*common.WrapperDestinationRule{}
|
||||
for key, wrapperTrafficPolicy := range convertOptions.Service2TrafficPolicy {
|
||||
serviceName := util.CreateServiceFQDN(key.Namespace, key.Name)
|
||||
dr, exist := destinationRules[serviceName]
|
||||
if !exist {
|
||||
dr = &common.WrapperDestinationRule{
|
||||
DestinationRule: &networking.DestinationRule{
|
||||
Host: serviceName,
|
||||
TrafficPolicy: &networking.TrafficPolicy{
|
||||
PortLevelSettings: []*networking.TrafficPolicy_PortTrafficPolicy{wrapperTrafficPolicy.TrafficPolicy},
|
||||
},
|
||||
},
|
||||
WrapperConfig: wrapperTrafficPolicy.WrapperConfig,
|
||||
ServiceKey: key,
|
||||
}
|
||||
} else {
|
||||
dr.DestinationRule.TrafficPolicy.PortLevelSettings = append(dr.DestinationRule.TrafficPolicy.PortLevelSettings, wrapperTrafficPolicy.TrafficPolicy)
|
||||
}
|
||||
|
||||
destinationRules[serviceName] = dr
|
||||
}
|
||||
|
||||
out := make([]config.Config, 0, len(destinationRules))
|
||||
for _, dr := range destinationRules {
|
||||
drName := util.CreateDestinationRuleName(m.clusterId, dr.ServiceKey.Namespace, dr.ServiceKey.Name)
|
||||
out = append(out, config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.DestinationRule,
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, drName),
|
||||
Namespace: m.namespace,
|
||||
},
|
||||
Spec: dr.DestinationRule,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *IngressConfig) applyAppRoot(convertOptions *common.ConvertOptions) {
|
||||
for host, wrapVS := range convertOptions.VirtualServices {
|
||||
if wrapVS.AppRoot != "" {
|
||||
route := &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Name: common.CreateConvertedName(host, "app-root"),
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Redirect: &networking.HTTPRedirect{
|
||||
RedirectCode: 302,
|
||||
Uri: wrapVS.AppRoot,
|
||||
},
|
||||
},
|
||||
WrapperConfig: wrapVS.WrapperConfig,
|
||||
ClusterId: wrapVS.WrapperConfig.AnnotationsConfig.ClusterId,
|
||||
}
|
||||
convertOptions.HTTPRoutes[host] = append([]*common.WrapperHTTPRoute{route}, convertOptions.HTTPRoutes[host]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressConfig) applyInternalActiveRedirect(convertOptions *common.ConvertOptions) {
|
||||
for host, routes := range convertOptions.HTTPRoutes {
|
||||
var tempRoutes []*common.WrapperHTTPRoute
|
||||
for _, route := range routes {
|
||||
tempRoutes = append(tempRoutes, route)
|
||||
if route.HTTPRoute.InternalActiveRedirect != nil {
|
||||
fallbackConfig := route.WrapperConfig.AnnotationsConfig.Fallback
|
||||
if fallbackConfig == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
typedNamespace := fallbackConfig.DefaultBackend
|
||||
internalRedirectRoute := route.HTTPRoute.DeepCopy()
|
||||
internalRedirectRoute.Name = internalRedirectRoute.Name + annotations.FallbackRouteNameSuffix
|
||||
internalRedirectRoute.InternalActiveRedirect = nil
|
||||
internalRedirectRoute.Match = []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "/",
|
||||
},
|
||||
},
|
||||
Headers: map[string]*networking.StringMatch{
|
||||
annotations.FallbackInjectHeaderRouteName: {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: internalRedirectRoute.Name,
|
||||
},
|
||||
},
|
||||
annotations.FallbackInjectFallbackService: {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: typedNamespace.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
internalRedirectRoute.Route = []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: util.CreateServiceFQDN(typedNamespace.Namespace, typedNamespace.Name),
|
||||
Port: &networking.PortSelector{
|
||||
Number: fallbackConfig.Port,
|
||||
},
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
|
||||
tempRoutes = append([]*common.WrapperHTTPRoute{{
|
||||
HTTPRoute: internalRedirectRoute,
|
||||
WrapperConfig: route.WrapperConfig,
|
||||
ClusterId: route.ClusterId,
|
||||
}}, tempRoutes...)
|
||||
}
|
||||
}
|
||||
convertOptions.HTTPRoutes[host] = tempRoutes
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressConfig) ReflectSecretChanges(clusterNamespacedName util.ClusterNamespacedName) {
|
||||
var hit bool
|
||||
m.mutex.RLock()
|
||||
if m.watchedSecretSet.Contains(clusterNamespacedName.String()) {
|
||||
hit = true
|
||||
}
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if hit {
|
||||
push := func(kind config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&model.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[model.ConfigKey]struct{}{{
|
||||
Kind: kind,
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
Reason: []model.TriggerReason{"auth-secret-change"},
|
||||
})
|
||||
}
|
||||
push(gvk.VirtualService)
|
||||
push(gvk.EnvoyFilter)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeWeightedCluster(cache *common.IngressRouteCache, route *common.WrapperHTTPRoute) {
|
||||
if len(route.HTTPRoute.Route) == 1 {
|
||||
route.HTTPRoute.Route[0].Weight = 100
|
||||
return
|
||||
}
|
||||
|
||||
var weightTotal int32 = 0
|
||||
for idx, routeDestination := range route.HTTPRoute.Route {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
weightTotal += routeDestination.Weight
|
||||
}
|
||||
|
||||
if weightTotal < route.WeightTotal {
|
||||
weightTotal = route.WeightTotal
|
||||
}
|
||||
|
||||
var sum int32
|
||||
for idx, routeDestination := range route.HTTPRoute.Route {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
weight := float32(routeDestination.Weight) / float32(weightTotal)
|
||||
routeDestination.Weight = int32(weight * 100)
|
||||
|
||||
sum += routeDestination.Weight
|
||||
}
|
||||
|
||||
route.HTTPRoute.Route[0].Weight = 100 - sum
|
||||
|
||||
// Update the recorded status in ingress builder
|
||||
if cache != nil {
|
||||
cache.Update(route)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressConfig) applyCanaryIngresses(convertOptions *common.ConvertOptions) {
|
||||
if len(convertOptions.CanaryIngresses) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
IngressLog.Infof("Found %d number of canary ingresses.", len(convertOptions.CanaryIngresses))
|
||||
for _, cfg := range convertOptions.CanaryIngresses {
|
||||
clusterId := common.GetClusterId(cfg.Config.Annotations)
|
||||
m.mutex.RLock()
|
||||
ingressController := m.remoteIngressControllers[clusterId]
|
||||
m.mutex.RUnlock()
|
||||
if ingressController == nil {
|
||||
continue
|
||||
}
|
||||
if err := ingressController.ApplyCanaryIngress(convertOptions, cfg); err != nil {
|
||||
IngressLog.Errorf("Apply canary ingress %s/%s fail in cluster %s, err %v", cfg.Config.Namespace, cfg.Config.Name, clusterId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func constructBasicAuthEnvoyFilter(rules *common.BasicAuthRules, namespace string) (*config.Config, error) {
|
||||
rulesStr, err := json.Marshal(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configuration := &wrappers.StringValue{
|
||||
Value: string(rulesStr),
|
||||
}
|
||||
|
||||
wasm := &wasm.Wasm{
|
||||
Config: &v3.PluginConfig{
|
||||
Name: "basic-auth",
|
||||
FailOpen: true,
|
||||
Vm: &v3.PluginConfig_VmConfig{
|
||||
VmConfig: &v3.VmConfig{
|
||||
Runtime: "envoy.wasm.runtime.null",
|
||||
Code: &corev3.AsyncDataSource{
|
||||
Specifier: &corev3.AsyncDataSource_Local{
|
||||
Local: &corev3.DataSource{
|
||||
Specifier: &corev3.DataSource_InlineString{
|
||||
InlineString: "envoy.wasm.basic_auth",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Configuration: networkingutil.MessageToAny(configuration),
|
||||
},
|
||||
}
|
||||
|
||||
wasmAny, err := anypb.New(wasm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
typedConfig := &httppb.HttpFilter{
|
||||
Name: "basic-auth",
|
||||
ConfigType: &httppb.HttpFilter_TypedConfig{
|
||||
TypedConfig: wasmAny,
|
||||
},
|
||||
}
|
||||
|
||||
gogoTypedConfig, err := util.MessageToGoGoStruct(typedConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "basic-auth"),
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.cors",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
|
||||
Value: gogoTypedConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *IngressConfig) Run(<-chan struct{}) {}
|
||||
|
||||
func (m *IngressConfig) HasSynced() bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
for _, remoteIngressController := range m.remoteIngressControllers {
|
||||
if !remoteIngressController.HasSynced() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
IngressLog.Info("Ingress config controller synced.")
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *IngressConfig) SetWatchErrorHandler(f func(r *cache.Reflector, err error)) error {
|
||||
m.watchErrorHandler = f
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressConfig) GetIngressRoutes() model.IngressRouteCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.ingressRouteCache
|
||||
}
|
||||
|
||||
func (m *IngressConfig) GetIngressDomains() model.IngressDomainCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.ingressDomainCache
|
||||
}
|
||||
|
||||
func (m *IngressConfig) Schemas() collection.Schemas {
|
||||
return common.Schemas
|
||||
}
|
||||
|
||||
func (m *IngressConfig) Get(config.GroupVersionKind, string, string) *config.Config {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressConfig) Create(config.Config) (revision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressConfig) Update(config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressConfig) UpdateStatus(config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressConfig) Patch(config.Config, config.PatchFunc) (string, error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressConfig) Delete(config.GroupVersionKind, string, string, *string) error {
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
613
ingress/config/ingress_config_test.go
Normal file
613
ingress/config/ingress_config_test.go
Normal file
@@ -0,0 +1,613 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/proto"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/config/xds"
|
||||
"istio.io/istio/pkg/kube"
|
||||
ingress "k8s.io/api/networking/v1"
|
||||
ingressv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/ingress/kube/common"
|
||||
controllerv1beta1 "github.com/alibaba/higress/ingress/kube/ingress"
|
||||
controllerv1 "github.com/alibaba/higress/ingress/kube/ingressv1"
|
||||
)
|
||||
|
||||
func TestNormalizeWeightedCluster(t *testing.T) {
|
||||
validate := func(route *common.WrapperHTTPRoute) int32 {
|
||||
var total int32
|
||||
for _, routeDestination := range route.HTTPRoute.Route {
|
||||
total += routeDestination.Weight
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
var testCases []*common.WrapperHTTPRoute
|
||||
testCases = append(testCases, &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Weight: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
testCases = append(testCases, &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Weight: 98,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
testCases = append(testCases, &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Weight: 0,
|
||||
},
|
||||
{
|
||||
Weight: 48,
|
||||
},
|
||||
{
|
||||
Weight: 48,
|
||||
},
|
||||
},
|
||||
},
|
||||
WeightTotal: 100,
|
||||
})
|
||||
|
||||
testCases = append(testCases, &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Weight: 0,
|
||||
},
|
||||
{
|
||||
Weight: 48,
|
||||
},
|
||||
{
|
||||
Weight: 48,
|
||||
},
|
||||
},
|
||||
},
|
||||
WeightTotal: 80,
|
||||
})
|
||||
|
||||
for _, route := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
normalizeWeightedCluster(nil, route)
|
||||
if validate(route) != 100 {
|
||||
t.Fatalf("Weight sum should be 100, but actual is %d", validate(route))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertGatewaysForIngress(t *testing.T) {
|
||||
fake := kube.NewFakeClient()
|
||||
v1Beta1Options := common.Options{
|
||||
Enable: true,
|
||||
ClusterId: "ingress-v1beta1",
|
||||
RawClusterId: "ingress-v1beta1__",
|
||||
}
|
||||
v1Options := common.Options{
|
||||
Enable: true,
|
||||
ClusterId: "ingress-v1",
|
||||
RawClusterId: "ingress-v1__",
|
||||
}
|
||||
ingressV1Beta1Controller := controllerv1beta1.NewController(fake, fake, v1Beta1Options, nil)
|
||||
ingressV1Controller := controllerv1.NewController(fake, fake, v1Options, nil)
|
||||
m := NewIngressConfig(fake, nil, "wakanda", "gw-123-istio")
|
||||
m.remoteIngressControllers = map[string]common.IngressController{
|
||||
"ingress-v1beta1": ingressV1Beta1Controller,
|
||||
"ingress-v1": ingressV1Controller,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputConfig []common.WrapperConfig
|
||||
expect map[string]config.Config
|
||||
}{
|
||||
{
|
||||
name: "ingress v1beta1",
|
||||
inputConfig: []common.WrapperConfig{
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-1",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1beta1",
|
||||
},
|
||||
},
|
||||
Spec: ingressv1beta1.IngressSpec{
|
||||
TLS: []ingressv1beta1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com",
|
||||
},
|
||||
},
|
||||
Rules: []ingressv1beta1.IngressRule{
|
||||
{
|
||||
Host: "foo.com",
|
||||
IngressRuleValue: ingressv1beta1.IngressRuleValue{
|
||||
HTTP: &ingressv1beta1.HTTPIngressRuleValue{
|
||||
Paths: []ingressv1beta1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Host: "test.com",
|
||||
IngressRuleValue: ingressv1beta1.IngressRuleValue{
|
||||
HTTP: &ingressv1beta1.HTTPIngressRuleValue{
|
||||
Paths: []ingressv1beta1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{
|
||||
DownstreamTLS: &annotations.DownstreamTLSConfig{
|
||||
TlsMinVersion: annotations.TLSProtocolVersion("TLSv1.1"),
|
||||
CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256", "AES256-SHA"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-2",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1beta1",
|
||||
},
|
||||
},
|
||||
Spec: ingressv1beta1.IngressSpec{
|
||||
TLS: []ingressv1beta1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
SecretName: "foo-com",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com-2",
|
||||
},
|
||||
},
|
||||
Rules: []ingressv1beta1.IngressRule{
|
||||
{
|
||||
Host: "foo.com",
|
||||
IngressRuleValue: ingressv1beta1.IngressRuleValue{
|
||||
HTTP: &ingressv1beta1.HTTPIngressRuleValue{
|
||||
Paths: []ingressv1beta1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Host: "bar.com",
|
||||
IngressRuleValue: ingressv1beta1.IngressRuleValue{
|
||||
HTTP: &ingressv1beta1.HTTPIngressRuleValue{
|
||||
Paths: []ingressv1beta1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Host: "test.com",
|
||||
IngressRuleValue: ingressv1beta1.IngressRuleValue{
|
||||
HTTP: &ingressv1beta1.HTTPIngressRuleValue{
|
||||
Paths: []ingressv1beta1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{
|
||||
DownstreamTLS: &annotations.DownstreamTLSConfig{
|
||||
TlsMinVersion: annotations.TLSProtocolVersion("TLSv1.2"),
|
||||
CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: map[string]config.Config{
|
||||
"foo.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-foo-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1beta1",
|
||||
common.HostAnnotation: "foo.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-ingress-v1beta1-wakanda-test-1-foo-com",
|
||||
},
|
||||
Hosts: []string{"foo.com"},
|
||||
},
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
Name: "https-443-ingress-ingress-v1beta1-wakanda-test-2-foo-com",
|
||||
},
|
||||
Hosts: []string{"foo.com"},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://ingress-v1beta1__/wakanda/foo-com",
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLSV1_1,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256", "AES256-SHA"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-test-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1beta1",
|
||||
common.HostAnnotation: "test.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-ingress-v1beta1-wakanda-test-1-test-com",
|
||||
},
|
||||
Hosts: []string{"test.com"},
|
||||
},
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
Name: "https-443-ingress-ingress-v1beta1-wakanda-test-1-test-com",
|
||||
},
|
||||
Hosts: []string{"test.com"},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://ingress-v1beta1__/wakanda/test-com",
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLSV1_1,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256", "AES256-SHA"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"bar.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-bar-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1beta1",
|
||||
common.HostAnnotation: "bar.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-ingress-v1beta1-wakanda-test-2-bar-com",
|
||||
},
|
||||
Hosts: []string{"bar.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ingress v1",
|
||||
inputConfig: []common.WrapperConfig{
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-1",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1",
|
||||
},
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com",
|
||||
},
|
||||
},
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Host: "foo.com",
|
||||
IngressRuleValue: ingress.IngressRuleValue{
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Host: "test.com",
|
||||
IngressRuleValue: ingress.IngressRuleValue{
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-2",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1",
|
||||
},
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
SecretName: "foo-com",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com-2",
|
||||
},
|
||||
},
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Host: "foo.com",
|
||||
IngressRuleValue: ingress.IngressRuleValue{
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Host: "bar.com",
|
||||
IngressRuleValue: ingress.IngressRuleValue{
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Host: "test.com",
|
||||
IngressRuleValue: ingress.IngressRuleValue{
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{
|
||||
DownstreamTLS: &annotations.DownstreamTLSConfig{
|
||||
TlsMinVersion: annotations.TLSProtocolVersion("TLSv1.2"),
|
||||
CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: map[string]config.Config{
|
||||
"foo.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-foo-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1",
|
||||
common.HostAnnotation: "foo.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-ingress-v1-wakanda-test-1-foo-com",
|
||||
},
|
||||
Hosts: []string{"foo.com"},
|
||||
},
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
Name: "https-443-ingress-ingress-v1-wakanda-test-2-foo-com",
|
||||
},
|
||||
Hosts: []string{"foo.com"},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://ingress-v1__/wakanda/foo-com",
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLSV1_2,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-test-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1",
|
||||
common.HostAnnotation: "test.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-ingress-v1-wakanda-test-1-test-com",
|
||||
},
|
||||
Hosts: []string{"test.com"},
|
||||
},
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
Name: "https-443-ingress-ingress-v1-wakanda-test-1-test-com",
|
||||
},
|
||||
Hosts: []string{"test.com"},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://ingress-v1__/wakanda/test-com",
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLSV1_2,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"bar.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-bar-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "ingress-v1",
|
||||
common.HostAnnotation: "bar.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-ingress-v1-wakanda-test-2-bar-com",
|
||||
},
|
||||
Hosts: []string{"bar.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
result := m.convertGateways(testCase.inputConfig)
|
||||
target := map[string]config.Config{}
|
||||
for _, item := range result {
|
||||
host := common.GetHost(item.Annotations)
|
||||
target[host] = item
|
||||
}
|
||||
assert.Equal(t, testCase.expect, target)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstructBasicAuthEnvoyFilter(t *testing.T) {
|
||||
rules := &common.BasicAuthRules{
|
||||
Rules: []*common.Rule{
|
||||
{
|
||||
Realm: "test",
|
||||
MatchRoute: []string{"route"},
|
||||
Credentials: []string{"user:password"},
|
||||
Encrypted: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config, err := constructBasicAuthEnvoyFilter(rules, "")
|
||||
if err != nil {
|
||||
t.Fatalf("construct error %v", err)
|
||||
}
|
||||
envoyFilter := config.Spec.(*networking.EnvoyFilter)
|
||||
pb, err := xds.BuildXDSObjectFromStruct(networking.EnvoyFilter_HTTP_FILTER, envoyFilter.ConfigPatches[0].Patch.Value, false)
|
||||
if err != nil {
|
||||
t.Fatalf("build object error %v", err)
|
||||
}
|
||||
target := proto.Clone(pb).(*httppb.HttpFilter)
|
||||
t.Log(target)
|
||||
}
|
||||
212
ingress/kube/annotations/annotations.go
Normal file
212
ingress/kube/annotations/annotations.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
)
|
||||
|
||||
type GlobalContext struct {
|
||||
// secret key is cluster/namespace/name
|
||||
WatchedSecrets sets.Set
|
||||
|
||||
ClusterSecretLister map[string]listersv1.SecretLister
|
||||
|
||||
ClusterServiceList map[string]listersv1.ServiceLister
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
Namespace string
|
||||
Name string
|
||||
RawClusterId string
|
||||
ClusterId string
|
||||
}
|
||||
|
||||
// Ingress defines the valid annotations present in one NGINX Ingress.
|
||||
type Ingress struct {
|
||||
Meta
|
||||
|
||||
Cors *CorsConfig
|
||||
|
||||
Rewrite *RewriteConfig
|
||||
|
||||
Redirect *RedirectConfig
|
||||
|
||||
UpstreamTLS *UpstreamTLSConfig
|
||||
|
||||
DownstreamTLS *DownstreamTLSConfig
|
||||
|
||||
Canary *CanaryConfig
|
||||
|
||||
IPAccessControl *IPAccessControlConfig
|
||||
|
||||
HeaderControl *HeaderControlConfig
|
||||
|
||||
Timeout *TimeoutConfig
|
||||
|
||||
Retry *RetryConfig
|
||||
|
||||
LoadBalance *LoadBalanceConfig
|
||||
|
||||
localRateLimit *localRateLimitConfig
|
||||
|
||||
Fallback *FallbackConfig
|
||||
|
||||
Auth *AuthConfig
|
||||
}
|
||||
|
||||
func (i *Ingress) NeedRegexMatch() bool {
|
||||
if i.Rewrite == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return i.Rewrite.RewriteTarget != "" || i.Rewrite.UseRegex
|
||||
}
|
||||
|
||||
func (i *Ingress) IsCanary() bool {
|
||||
if i.Canary == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return i.Canary.Enabled
|
||||
}
|
||||
|
||||
// CanaryKind return byHeader, byWeight
|
||||
func (i *Ingress) CanaryKind() (bool, bool) {
|
||||
if !i.IsCanary() {
|
||||
return false, false
|
||||
}
|
||||
|
||||
// first header, cookie
|
||||
if i.Canary.Header != "" || i.Canary.Cookie != "" {
|
||||
return true, false
|
||||
}
|
||||
|
||||
// then weight
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (i *Ingress) NeedTrafficPolicy() bool {
|
||||
return i.UpstreamTLS != nil ||
|
||||
i.LoadBalance != nil
|
||||
}
|
||||
|
||||
func (i *Ingress) MergeHostIPAccessControlIfNotExist(ac *IPAccessControlConfig) {
|
||||
if i.IPAccessControl != nil && i.IPAccessControl.Domain != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ac != nil && ac.Domain != nil {
|
||||
if i.IPAccessControl == nil {
|
||||
i.IPAccessControl = &IPAccessControlConfig{
|
||||
Domain: ac.Domain,
|
||||
}
|
||||
} else {
|
||||
i.IPAccessControl.Domain = ac.Domain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AnnotationHandler interface {
|
||||
Parser
|
||||
GatewayHandler
|
||||
VirtualServiceHandler
|
||||
RouteHandler
|
||||
TrafficPolicyHandler
|
||||
}
|
||||
|
||||
type AnnotationHandlerManager struct {
|
||||
parsers []Parser
|
||||
gatewayHandlers []GatewayHandler
|
||||
virtualServiceHandlers []VirtualServiceHandler
|
||||
routeHandlers []RouteHandler
|
||||
trafficPolicyHandlers []TrafficPolicyHandler
|
||||
}
|
||||
|
||||
func NewAnnotationHandlerManager() AnnotationHandler {
|
||||
return &AnnotationHandlerManager{
|
||||
parsers: []Parser{
|
||||
canary{},
|
||||
cors{},
|
||||
downstreamTLS{},
|
||||
redirect{},
|
||||
rewrite{},
|
||||
upstreamTLS{},
|
||||
ipAccessControl{},
|
||||
headerControl{},
|
||||
timeout{},
|
||||
retry{},
|
||||
loadBalance{},
|
||||
localRateLimit{},
|
||||
fallback{},
|
||||
auth{},
|
||||
},
|
||||
gatewayHandlers: []GatewayHandler{
|
||||
downstreamTLS{},
|
||||
},
|
||||
virtualServiceHandlers: []VirtualServiceHandler{
|
||||
ipAccessControl{},
|
||||
},
|
||||
routeHandlers: []RouteHandler{
|
||||
cors{},
|
||||
redirect{},
|
||||
rewrite{},
|
||||
ipAccessControl{},
|
||||
headerControl{},
|
||||
timeout{},
|
||||
retry{},
|
||||
localRateLimit{},
|
||||
fallback{},
|
||||
},
|
||||
trafficPolicyHandlers: []TrafficPolicyHandler{
|
||||
upstreamTLS{},
|
||||
loadBalance{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
for _, parser := range h.parsers {
|
||||
_ = parser.Parse(annotations, config, globalContext)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) ApplyGateway(gateway *networking.Gateway, config *Ingress) {
|
||||
for _, handler := range h.gatewayHandlers {
|
||||
handler.ApplyGateway(gateway, config)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) ApplyVirtualServiceHandler(virtualService *networking.VirtualService, config *Ingress) {
|
||||
for _, handler := range h.virtualServiceHandlers {
|
||||
handler.ApplyVirtualServiceHandler(virtualService, config)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
for _, handler := range h.routeHandlers {
|
||||
handler.ApplyRoute(route, config)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) ApplyTrafficPolicy(trafficPolicy *networking.TrafficPolicy_PortTrafficPolicy, config *Ingress) {
|
||||
for _, handler := range h.trafficPolicyHandlers {
|
||||
handler.ApplyTrafficPolicy(trafficPolicy, config)
|
||||
}
|
||||
}
|
||||
182
ingress/kube/annotations/annotations_test.go
Normal file
182
ingress/kube/annotations/annotations_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// 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 annotations
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNeedRegexMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *Ingress
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
input: &Ingress{},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Rewrite: &RewriteConfig{},
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
RewriteTarget: "/test",
|
||||
},
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
UseRegex: true,
|
||||
},
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
if testCase.input.NeedRegexMatch() != testCase.expect {
|
||||
t.Fatalf("Should be %t, but actual is %t", testCase.expect, testCase.input.NeedRegexMatch())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCanary(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *Ingress
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
input: &Ingress{},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{},
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
if testCase.input.IsCanary() != testCase.expect {
|
||||
t.Fatalf("Should be %t, but actual is %t", testCase.expect, testCase.input.IsCanary())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanaryKind(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *Ingress
|
||||
byHeader bool
|
||||
byWeight bool
|
||||
}{
|
||||
{
|
||||
input: &Ingress{},
|
||||
byHeader: false,
|
||||
byWeight: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{},
|
||||
},
|
||||
byHeader: false,
|
||||
byWeight: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
byHeader: false,
|
||||
byWeight: true,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
Header: "test",
|
||||
},
|
||||
},
|
||||
byHeader: true,
|
||||
byWeight: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
Cookie: "test",
|
||||
},
|
||||
},
|
||||
byHeader: true,
|
||||
byWeight: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
Weight: 2,
|
||||
},
|
||||
},
|
||||
byHeader: false,
|
||||
byWeight: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
byHeader, byWeight := testCase.input.CanaryKind()
|
||||
if byHeader != testCase.byHeader {
|
||||
t.Fatalf("Should be %t, but actual is %t", testCase.byHeader, byHeader)
|
||||
}
|
||||
|
||||
if byWeight != testCase.byWeight {
|
||||
t.Fatalf("Should be %t, but actual is %t", testCase.byWeight, byWeight)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedTrafficPolicy(t *testing.T) {
|
||||
config1 := &Ingress{}
|
||||
if config1.NeedTrafficPolicy() {
|
||||
t.Fatal("should be false")
|
||||
}
|
||||
|
||||
config2 := &Ingress{
|
||||
UpstreamTLS: &UpstreamTLSConfig{
|
||||
BackendProtocol: defaultBackendProtocol,
|
||||
},
|
||||
}
|
||||
if !config2.NeedTrafficPolicy() {
|
||||
t.Fatal("should be true")
|
||||
}
|
||||
}
|
||||
155
ingress/kube/annotations/auth.go
Normal file
155
ingress/kube/annotations/auth.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
authType = "auth-type"
|
||||
authRealm = "auth-realm"
|
||||
authSecretAnn = "auth-secret"
|
||||
authSecretTypeAnn = "auth-secret-type"
|
||||
|
||||
defaultAuthType = "basic"
|
||||
authFileKey = "auth"
|
||||
)
|
||||
|
||||
type authSecretType string
|
||||
|
||||
const (
|
||||
authFileAuthSecretType authSecretType = "auth-file"
|
||||
authMapAuthSecretType authSecretType = "auth-map"
|
||||
)
|
||||
|
||||
var _ Parser = auth{}
|
||||
|
||||
type AuthConfig struct {
|
||||
AuthType string
|
||||
AuthRealm string
|
||||
Credentials []string
|
||||
AuthSecret util.ClusterNamespacedName
|
||||
}
|
||||
|
||||
type auth struct{}
|
||||
|
||||
func (a auth) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
if !needAuthConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
authConfig := &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
}
|
||||
|
||||
// Check auth type
|
||||
authType, err := annotations.ParseStringASAP(authType)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Parse auth type error %v within ingress %/%s", err, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
if authType != defaultAuthType {
|
||||
IngressLog.Errorf("Auth type %s within ingress %/%s is not supported yet.", authType, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
secretName, _ := annotations.ParseStringASAP(authSecretAnn)
|
||||
namespaced := util.SplitNamespacedName(secretName)
|
||||
if namespaced.Name == "" {
|
||||
IngressLog.Errorf("Auth secret name within ingress %s/%s is invalid", config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
if namespaced.Namespace == "" {
|
||||
namespaced.Namespace = config.Namespace
|
||||
}
|
||||
|
||||
configKey := util.ClusterNamespacedName{
|
||||
NamespacedName: namespaced,
|
||||
ClusterId: config.ClusterId,
|
||||
}
|
||||
authConfig.AuthSecret = configKey
|
||||
|
||||
// Subscribe secret
|
||||
globalContext.WatchedSecrets.Insert(configKey.String())
|
||||
|
||||
secretType := authFileAuthSecretType
|
||||
if rawSecretType, err := annotations.ParseStringASAP(authSecretTypeAnn); err == nil {
|
||||
resultAuthSecretType := authSecretType(rawSecretType)
|
||||
if resultAuthSecretType == authFileAuthSecretType || resultAuthSecretType == authMapAuthSecretType {
|
||||
secretType = resultAuthSecretType
|
||||
}
|
||||
}
|
||||
|
||||
authConfig.AuthRealm, _ = annotations.ParseStringASAP(authRealm)
|
||||
|
||||
// Process credentials.
|
||||
secretLister, exist := globalContext.ClusterSecretLister[config.ClusterId]
|
||||
if !exist {
|
||||
IngressLog.Errorf("secret lister of cluster %s doesn't exist", config.ClusterId)
|
||||
return nil
|
||||
}
|
||||
authSecret, err := secretLister.Secrets(namespaced.Namespace).Get(namespaced.Name)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Secret %s within ingress %s/%s is not found",
|
||||
namespaced.String(), config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
credentials, err := convertCredentials(secretType, authSecret)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Parse auth secret fail, err %v", err)
|
||||
return nil
|
||||
}
|
||||
authConfig.Credentials = credentials
|
||||
|
||||
config.Auth = authConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertCredentials(secretType authSecretType, secret *corev1.Secret) ([]string, error) {
|
||||
var result []string
|
||||
switch secretType {
|
||||
case authFileAuthSecretType:
|
||||
users, exist := secret.Data[authFileKey]
|
||||
if !exist {
|
||||
return nil, errors.New("the auth file type must has auth key in secret data")
|
||||
}
|
||||
userList := strings.Split(string(users), "\n")
|
||||
for _, item := range userList {
|
||||
result = append(result, item)
|
||||
}
|
||||
case authMapAuthSecretType:
|
||||
for name, password := range secret.Data {
|
||||
result = append(result, name+":"+string(password))
|
||||
}
|
||||
}
|
||||
sort.SliceStable(result, func(i, j int) bool {
|
||||
return result[i] < result[j]
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func needAuthConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(authType) &&
|
||||
annotations.HasASAP(authSecretAnn)
|
||||
}
|
||||
196
ingress/kube/annotations/auth_test.go
Normal file
196
ingress/kube/annotations/auth_test.go
Normal file
@@ -0,0 +1,196 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/util"
|
||||
)
|
||||
|
||||
func TestAuthParse(t *testing.T) {
|
||||
auth := auth{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
secret *v1.Secret
|
||||
expect *AuthConfig
|
||||
watchedSecret string
|
||||
}{
|
||||
{
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): "digest",
|
||||
},
|
||||
expect: nil,
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildMSEAnnotationKey(authSecretAnn): "foo/bar",
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/foo/bar",
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildMSEAnnotationKey(authSecretAnn): "foo/bar",
|
||||
buildNginxAnnotationKey(authSecretTypeAnn): string(authMapAuthSecretType),
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"A": []byte("a"),
|
||||
"B": []byte("b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/foo/bar",
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildMSEAnnotationKey(authSecretAnn): "bar",
|
||||
buildNginxAnnotationKey(authSecretTypeAnn): string(authFileAuthSecretType),
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "default",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/default/bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "default",
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
}
|
||||
|
||||
globalContext, cancel := initGlobalContext(inputCase.secret)
|
||||
defer cancel()
|
||||
|
||||
_ = auth.Parse(inputCase.input, config, globalContext)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.Auth) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
|
||||
if inputCase.watchedSecret != "" {
|
||||
if !globalContext.WatchedSecrets.Contains(inputCase.watchedSecret) {
|
||||
t.Fatalf("Should watch secret %s", inputCase.watchedSecret)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initGlobalContext(secret *v1.Secret) (*GlobalContext, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
informerFactory := informers.NewSharedInformerFactory(client, time.Hour)
|
||||
secretInformer := informerFactory.Core().V1().Secrets()
|
||||
go secretInformer.Informer().Run(ctx.Done())
|
||||
cache.WaitForCacheSync(ctx.Done(), secretInformer.Informer().HasSynced)
|
||||
|
||||
return &GlobalContext{
|
||||
WatchedSecrets: sets.NewSet(),
|
||||
ClusterSecretLister: map[string]listerv1.SecretLister{
|
||||
"cluster": secretInformer.Lister(),
|
||||
},
|
||||
}, cancel
|
||||
}
|
||||
185
ingress/kube/annotations/canary.go
Normal file
185
ingress/kube/annotations/canary.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
enableCanary = "canary"
|
||||
canaryByHeader = "canary-by-header"
|
||||
canaryByHeaderValue = "canary-by-header-value"
|
||||
canaryByHeaderPattern = "canary-by-header-pattern"
|
||||
canaryByCookie = "canary-by-cookie"
|
||||
canaryWeight = "canary-weight"
|
||||
canaryWeightTotal = "canary-weight-total"
|
||||
|
||||
defaultCanaryWeightTotal = 100
|
||||
)
|
||||
|
||||
var _ Parser = &canary{}
|
||||
|
||||
type CanaryConfig struct {
|
||||
Enabled bool
|
||||
Header string
|
||||
HeaderValue string
|
||||
HeaderPattern string
|
||||
Cookie string
|
||||
Weight int
|
||||
WeightTotal int
|
||||
}
|
||||
|
||||
type canary struct{}
|
||||
|
||||
func (c canary) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needCanaryConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
canaryConfig := &CanaryConfig{
|
||||
WeightTotal: defaultCanaryWeightTotal,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
config.Canary = canaryConfig
|
||||
}()
|
||||
|
||||
canaryConfig.Enabled, _ = annotations.ParseBoolASAP(enableCanary)
|
||||
if !canaryConfig.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if header, err := annotations.ParseStringASAP(canaryByHeader); err == nil {
|
||||
canaryConfig.Header = header
|
||||
}
|
||||
|
||||
if headerValue, err := annotations.ParseStringASAP(canaryByHeaderValue); err == nil &&
|
||||
headerValue != "" {
|
||||
canaryConfig.HeaderValue = headerValue
|
||||
return nil
|
||||
}
|
||||
|
||||
if headerPattern, err := annotations.ParseStringASAP(canaryByHeaderPattern); err == nil &&
|
||||
headerPattern != "" {
|
||||
canaryConfig.HeaderPattern = headerPattern
|
||||
return nil
|
||||
}
|
||||
|
||||
if cookie, err := annotations.ParseStringASAP(canaryByCookie); err == nil &&
|
||||
cookie != "" {
|
||||
canaryConfig.Cookie = cookie
|
||||
return nil
|
||||
}
|
||||
|
||||
canaryConfig.Weight, _ = annotations.ParseIntASAP(canaryWeight)
|
||||
if weightTotal, err := annotations.ParseIntASAP(canaryWeightTotal); err == nil && weightTotal > 0 {
|
||||
canaryConfig.WeightTotal = weightTotal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApplyByWeight(canary, route *networking.HTTPRoute, canaryIngress *Ingress) {
|
||||
if len(route.Route) == 1 {
|
||||
// Move route level to destination level
|
||||
route.Route[0].Headers = route.Headers
|
||||
route.Headers = nil
|
||||
}
|
||||
|
||||
// Modify canary weighted cluster
|
||||
canary.Route[0].Weight = int32(canaryIngress.Canary.Weight)
|
||||
|
||||
// Append canary weight upstream service.
|
||||
// We will process total weight in the end.
|
||||
route.Route = append(route.Route, canary.Route[0])
|
||||
|
||||
// canary route use the header control applied on itself.
|
||||
headerControl{}.ApplyRoute(canary, canaryIngress)
|
||||
// Move route level to destination level
|
||||
canary.Route[0].Headers = canary.Headers
|
||||
|
||||
// First add normal route cluster
|
||||
canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
|
||||
route.Route[0].Destination.DeepCopy())
|
||||
// Second add fallback cluster of normal route cluster
|
||||
canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
|
||||
route.Route[0].FallbackClusters...)
|
||||
}
|
||||
|
||||
func ApplyByHeader(canary, route *networking.HTTPRoute, canaryIngress *Ingress) {
|
||||
canaryConfig := canaryIngress.Canary
|
||||
|
||||
// Copy canary http route
|
||||
temp := canary.DeepCopy()
|
||||
|
||||
// Inherit configuration from non-canary rule
|
||||
route.DeepCopyInto(canary)
|
||||
// Assign temp copied canary route match
|
||||
canary.Match = temp.Match
|
||||
// Assign temp copied canary route destination
|
||||
canary.Route = temp.Route
|
||||
|
||||
// Modified match base on by header
|
||||
if canaryConfig.Header != "" {
|
||||
canary.Match[0].Headers = map[string]*networking.StringMatch{
|
||||
canaryConfig.Header: {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "always",
|
||||
},
|
||||
},
|
||||
}
|
||||
if canaryConfig.HeaderValue != "" {
|
||||
canary.Match[0].Headers = map[string]*networking.StringMatch{
|
||||
canaryConfig.Header: {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "always|" + canaryConfig.HeaderValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if canaryConfig.HeaderPattern != "" {
|
||||
canary.Match[0].Headers = map[string]*networking.StringMatch{
|
||||
canaryConfig.Header: {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: canaryConfig.HeaderPattern,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
} else if canaryConfig.Cookie != "" {
|
||||
canary.Match[0].Headers = map[string]*networking.StringMatch{
|
||||
"cookie": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "^(.\\*?;)?(" + canaryConfig.Cookie + "=always)(;.\\*)?$",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
canary.Headers = nil
|
||||
// canary route use the header control applied on itself.
|
||||
headerControl{}.ApplyRoute(canary, canaryIngress)
|
||||
|
||||
// First add normal route cluster
|
||||
canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
|
||||
route.Route[0].Destination.DeepCopy())
|
||||
// Second add fallback cluster of normal route cluster
|
||||
canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
|
||||
route.Route[0].FallbackClusters...)
|
||||
}
|
||||
|
||||
func needCanaryConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(enableCanary)
|
||||
}
|
||||
254
ingress/kube/annotations/canary_test.go
Normal file
254
ingress/kube/annotations/canary_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestApplyWeight(t *testing.T) {
|
||||
route := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"normal": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
canary1 := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary1": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary1",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
canary2 := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary2": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary2",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ApplyByWeight(canary1, route, &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Weight: 30,
|
||||
},
|
||||
})
|
||||
|
||||
ApplyByWeight(canary2, route, &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Weight: 20,
|
||||
},
|
||||
})
|
||||
|
||||
expect := &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"normal": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary1",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary1": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Weight: 30,
|
||||
FallbackClusters: []*networking.Destination{
|
||||
{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary2",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary2": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Weight: 20,
|
||||
FallbackClusters: []*networking.Destination{
|
||||
{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(route, expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyHeader(t *testing.T) {
|
||||
route := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"normal": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
canary := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ApplyByHeader(canary, route, &Ingress{
|
||||
Canary: &CanaryConfig{},
|
||||
HeaderControl: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"canary": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary": "true",
|
||||
},
|
||||
},
|
||||
Response: &networking.Headers_HeaderOperations{},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
FallbackClusters: []*networking.Destination{
|
||||
{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(canary, expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
}
|
||||
200
ingress/kube/annotations/cors.go
Normal file
200
ingress/kube/annotations/cors.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
// annotation key
|
||||
enableCors = "enable-cors"
|
||||
allowOrigin = "cors-allow-origin"
|
||||
allowMethods = "cors-allow-methods"
|
||||
allowHeaders = "cors-allow-headers"
|
||||
exposeHeaders = "cors-expose-headers"
|
||||
allowCredentials = "cors-allow-credentials"
|
||||
maxAge = "cors-max-age"
|
||||
|
||||
// default annotation value
|
||||
defaultAllowOrigin = "*"
|
||||
defaultAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
||||
defaultAllowHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With," +
|
||||
"If-Modified-Since,Cache-Control,Content-Type,Authorization"
|
||||
defaultAllowCredentials = true
|
||||
defaultMaxAge = 1728000
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &cors{}
|
||||
_ RouteHandler = &cors{}
|
||||
)
|
||||
|
||||
type CorsConfig struct {
|
||||
Enabled bool
|
||||
AllowOrigin []string
|
||||
AllowMethods []string
|
||||
AllowHeaders []string
|
||||
ExposeHeaders []string
|
||||
AllowCredentials bool
|
||||
MaxAge int
|
||||
}
|
||||
|
||||
type cors struct{}
|
||||
|
||||
func (c cors) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needCorsConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// cors enable
|
||||
enable, _ := annotations.ParseBoolASAP(enableCors)
|
||||
if !enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
corsConfig := &CorsConfig{
|
||||
Enabled: enable,
|
||||
AllowOrigin: []string{defaultAllowOrigin},
|
||||
AllowMethods: splitStringWithSpaceTrim(defaultAllowMethods),
|
||||
AllowHeaders: splitStringWithSpaceTrim(defaultAllowHeaders),
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
config.Cors = corsConfig
|
||||
}()
|
||||
|
||||
// allow origin
|
||||
if origin, err := annotations.ParseStringASAP(allowOrigin); err == nil {
|
||||
corsConfig.AllowOrigin = splitStringWithSpaceTrim(origin)
|
||||
}
|
||||
|
||||
// allow methods
|
||||
if methods, err := annotations.ParseStringASAP(allowMethods); err == nil {
|
||||
corsConfig.AllowMethods = splitStringWithSpaceTrim(methods)
|
||||
}
|
||||
|
||||
// allow headers
|
||||
if headers, err := annotations.ParseStringASAP(allowHeaders); err == nil {
|
||||
corsConfig.AllowHeaders = splitStringWithSpaceTrim(headers)
|
||||
}
|
||||
|
||||
// expose headers
|
||||
if exposeHeaders, err := annotations.ParseStringASAP(exposeHeaders); err == nil {
|
||||
corsConfig.ExposeHeaders = splitStringWithSpaceTrim(exposeHeaders)
|
||||
}
|
||||
|
||||
// allow credentials
|
||||
if allowCredentials, err := annotations.ParseBoolASAP(allowCredentials); err == nil {
|
||||
corsConfig.AllowCredentials = allowCredentials
|
||||
}
|
||||
|
||||
// max age
|
||||
if age, err := annotations.ParseIntASAP(maxAge); err == nil {
|
||||
corsConfig.MaxAge = age
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c cors) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
corsConfig := config.Cors
|
||||
if corsConfig == nil || !corsConfig.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
corsPolicy := &networking.CorsPolicy{
|
||||
AllowMethods: corsConfig.AllowMethods,
|
||||
AllowHeaders: corsConfig.AllowHeaders,
|
||||
ExposeHeaders: corsConfig.ExposeHeaders,
|
||||
AllowCredentials: &types.BoolValue{
|
||||
Value: corsConfig.AllowCredentials,
|
||||
},
|
||||
MaxAge: &types.Duration{
|
||||
Seconds: int64(corsConfig.MaxAge),
|
||||
},
|
||||
}
|
||||
|
||||
var allowOrigins []*networking.StringMatch
|
||||
for _, origin := range corsConfig.AllowOrigin {
|
||||
if origin == "*" {
|
||||
allowOrigins = append(allowOrigins, &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: ".*",
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
if strings.Contains(origin, "*") {
|
||||
parsedURL, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(parsedURL.Host, "*") {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(".*")
|
||||
for idx, char := range parsedURL.Host {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '.' {
|
||||
sb.WriteString("\\")
|
||||
}
|
||||
|
||||
sb.WriteString(string(char))
|
||||
}
|
||||
|
||||
allowOrigins = append(allowOrigins, &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: sb.String(),
|
||||
},
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
allowOrigins = append(allowOrigins, &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: origin,
|
||||
},
|
||||
})
|
||||
}
|
||||
corsPolicy.AllowOrigins = allowOrigins
|
||||
|
||||
route.CorsPolicy = corsPolicy
|
||||
}
|
||||
|
||||
func needCorsConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(enableCors)
|
||||
}
|
||||
|
||||
func splitStringWithSpaceTrim(input string) []string {
|
||||
out := strings.Split(input, ",")
|
||||
for i, item := range out {
|
||||
converted := strings.TrimSpace(item)
|
||||
if converted == "*" {
|
||||
return []string{"*"}
|
||||
}
|
||||
out[i] = converted
|
||||
}
|
||||
return out
|
||||
}
|
||||
282
ingress/kube/annotations/cors_test.go
Normal file
282
ingress/kube/annotations/cors_test.go
Normal file
@@ -0,0 +1,282 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestSplitStringWithSpaceTrim(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expect []string
|
||||
}{
|
||||
{
|
||||
input: "*",
|
||||
expect: []string{"*"},
|
||||
},
|
||||
{
|
||||
input: "a, b, c",
|
||||
expect: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
input: "a, *, c",
|
||||
expect: []string{"*"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
result := splitStringWithSpaceTrim(testCase.input)
|
||||
if !reflect.DeepEqual(testCase.expect, result) {
|
||||
t.Fatalf("Must be equal, but got %s", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsParse(t *testing.T) {
|
||||
cors := cors{}
|
||||
testCases := []struct {
|
||||
input Annotations
|
||||
expect *CorsConfig
|
||||
}{
|
||||
{
|
||||
input: Annotations{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "false",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "true",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{defaultAllowOrigin},
|
||||
AllowMethods: splitStringWithSpaceTrim(defaultAllowMethods),
|
||||
AllowHeaders: splitStringWithSpaceTrim(defaultAllowHeaders),
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "true",
|
||||
buildNginxAnnotationKey(allowOrigin): "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: splitStringWithSpaceTrim(defaultAllowMethods),
|
||||
AllowHeaders: splitStringWithSpaceTrim(defaultAllowHeaders),
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "true",
|
||||
buildNginxAnnotationKey(allowOrigin): "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199",
|
||||
buildNginxAnnotationKey(allowMethods): "GET, PUT",
|
||||
buildNginxAnnotationKey(allowHeaders): "foo,bar",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "PUT"},
|
||||
AllowHeaders: []string{"foo", "bar"},
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "true",
|
||||
buildNginxAnnotationKey(allowOrigin): "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199",
|
||||
buildNginxAnnotationKey(allowMethods): "GET, PUT",
|
||||
buildNginxAnnotationKey(allowHeaders): "foo,bar",
|
||||
buildNginxAnnotationKey(allowCredentials): "false",
|
||||
buildNginxAnnotationKey(maxAge): "100",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "PUT"},
|
||||
AllowHeaders: []string{"foo", "bar"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildMSEAnnotationKey(enableCors): "true",
|
||||
buildNginxAnnotationKey(allowOrigin): "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199",
|
||||
buildMSEAnnotationKey(allowMethods): "GET, PUT",
|
||||
buildNginxAnnotationKey(allowHeaders): "foo,bar",
|
||||
buildNginxAnnotationKey(allowCredentials): "false",
|
||||
buildNginxAnnotationKey(maxAge): "100",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "PUT"},
|
||||
AllowHeaders: []string{"foo", "bar"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = cors.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(config.Cors, testCase.expect) {
|
||||
t.Fatalf("Must be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsApplyRoute(t *testing.T) {
|
||||
cors := cors{}
|
||||
testCases := []struct {
|
||||
route *networking.HTTPRoute
|
||||
config *Ingress
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
route: &networking.HTTPRoute{},
|
||||
config: &Ingress{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
route: &networking.HTTPRoute{},
|
||||
config: &Ingress{
|
||||
Cors: &CorsConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
route: &networking.HTTPRoute{},
|
||||
config: &Ingress{
|
||||
Cors: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowHeaders: []string{"test", "unique"},
|
||||
ExposeHeaders: []string{"hello", "bye"},
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
CorsPolicy: &networking.CorsPolicy{
|
||||
AllowOrigins: []*networking.StringMatch{
|
||||
{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "https://origin-site.com:4443",
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "http://origin-site.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "https://example.org:1199",
|
||||
},
|
||||
},
|
||||
},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowHeaders: []string{"test", "unique"},
|
||||
ExposeHeaders: []string{"hello", "bye"},
|
||||
AllowCredentials: &types.BoolValue{
|
||||
Value: true,
|
||||
},
|
||||
MaxAge: &types.Duration{
|
||||
Seconds: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
route: &networking.HTTPRoute{},
|
||||
config: &Ingress{
|
||||
Cors: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://*.origin-site.com:4443", "http://*.origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowHeaders: []string{"test", "unique"},
|
||||
ExposeHeaders: []string{"hello", "bye"},
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
CorsPolicy: &networking.CorsPolicy{
|
||||
AllowOrigins: []*networking.StringMatch{
|
||||
{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: ".*\\.origin-site\\.com:4443",
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: ".*\\.origin-site\\.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "https://example.org:1199",
|
||||
},
|
||||
},
|
||||
},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowHeaders: []string{"test", "unique"},
|
||||
ExposeHeaders: []string{"hello", "bye"},
|
||||
AllowCredentials: &types.BoolValue{
|
||||
Value: true,
|
||||
},
|
||||
MaxAge: &types.Duration{
|
||||
Seconds: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
cors.ApplyRoute(testCase.route, testCase.config)
|
||||
if !proto.Equal(testCase.route, testCase.expect) {
|
||||
t.Fatal("Must be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
151
ingress/kube/annotations/default_backend.go
Normal file
151
ingress/kube/annotations/default_backend.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
annDefaultBackend = "default-backend"
|
||||
customHTTPError = "custom-http-errors"
|
||||
|
||||
defaultRedirectUrl = "http://example.com/"
|
||||
FallbackRouteNameSuffix = "-fallback"
|
||||
FallbackInjectHeaderRouteName = "x-envoy-route-name"
|
||||
FallbackInjectFallbackService = "x-envoy-fallback-service"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = fallback{}
|
||||
_ RouteHandler = fallback{}
|
||||
)
|
||||
|
||||
type FallbackConfig struct {
|
||||
DefaultBackend model.NamespacedName
|
||||
Port uint32
|
||||
customHTTPErrors []uint32
|
||||
}
|
||||
|
||||
type fallback struct{}
|
||||
|
||||
func (f fallback) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
if !needFallback(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fallBackConfig := &FallbackConfig{}
|
||||
svcName, err := annotations.ParseStringASAP(annDefaultBackend)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Parse annotation default backend err: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
fallBackConfig.DefaultBackend = util.SplitNamespacedName(svcName)
|
||||
if fallBackConfig.DefaultBackend.Name == "" {
|
||||
IngressLog.Errorf("Annotation default backend within ingress %s/%s is invalid", config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
// Use ingress namespace instead, if user don't specify the namespace for default backend svc.
|
||||
if fallBackConfig.DefaultBackend.Namespace == "" {
|
||||
fallBackConfig.DefaultBackend.Namespace = config.Namespace
|
||||
}
|
||||
|
||||
serviceLister, exist := globalContext.ClusterServiceList[config.ClusterId]
|
||||
if !exist {
|
||||
IngressLog.Errorf("service lister of cluster %s doesn't exist", config.ClusterId)
|
||||
return nil
|
||||
}
|
||||
|
||||
fallbackSvc, err := serviceLister.Services(fallBackConfig.DefaultBackend.Namespace).Get(fallBackConfig.DefaultBackend.Name)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Fallback service %s/%s within ingress %s/%s is not found",
|
||||
fallBackConfig.DefaultBackend.Namespace, fallBackConfig.DefaultBackend.Name, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
if len(fallbackSvc.Spec.Ports) == 0 {
|
||||
IngressLog.Errorf("Fallback service %s/%s within ingress %s/%s haven't ports",
|
||||
fallBackConfig.DefaultBackend.Namespace, fallBackConfig.DefaultBackend.Name, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
// Use the first port like nginx ingress.
|
||||
fallBackConfig.Port = uint32(fallbackSvc.Spec.Ports[0].Port)
|
||||
|
||||
config.Fallback = fallBackConfig
|
||||
|
||||
if codes, err := annotations.ParseStringASAP(customHTTPError); err == nil {
|
||||
codesStr := splitBySeparator(codes, ",")
|
||||
var codesUint32 []uint32
|
||||
for _, rawCode := range codesStr {
|
||||
code, err := strconv.Atoi(rawCode)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Custom HTTP code %s within ingress %s/%s is invalid", rawCode, config.Namespace, config.Name)
|
||||
continue
|
||||
}
|
||||
codesUint32 = append(codesUint32, uint32(code))
|
||||
}
|
||||
fallBackConfig.customHTTPErrors = codesUint32
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f fallback) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
fallback := config.Fallback
|
||||
if fallback == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add fallback svc
|
||||
route.Route[0].FallbackClusters = []*networking.Destination{
|
||||
{
|
||||
Host: util.CreateServiceFQDN(fallback.DefaultBackend.Namespace, fallback.DefaultBackend.Name),
|
||||
Port: &networking.PortSelector{
|
||||
Number: fallback.Port,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(fallback.customHTTPErrors) > 0 {
|
||||
route.InternalActiveRedirect = &networking.HTTPInternalActiveRedirect{
|
||||
MaxInternalRedirects: 1,
|
||||
RedirectResponseCodes: fallback.customHTTPErrors,
|
||||
AllowCrossScheme: true,
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
FallbackInjectHeaderRouteName: route.Name + FallbackRouteNameSuffix,
|
||||
FallbackInjectFallbackService: fallback.DefaultBackend.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
RedirectUrlRewriteSpecifier: &networking.HTTPInternalActiveRedirect_RedirectUrl{
|
||||
RedirectUrl: defaultRedirectUrl,
|
||||
},
|
||||
ForcedUseOriginalHost: true,
|
||||
ForcedAddHeaderBeforeRouteMatcher: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func needFallback(annotations Annotations) bool {
|
||||
return annotations.HasASAP(annDefaultBackend)
|
||||
}
|
||||
229
ingress/kube/annotations/default_backend_test.go
Normal file
229
ingress/kube/annotations/default_backend_test.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
var (
|
||||
normalService = &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Ports: []v1.ServicePort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
abnormalService = &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app",
|
||||
Namespace: "foo",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestFallbackParse(t *testing.T) {
|
||||
fallback := fallback{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *FallbackConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(annDefaultBackend): "test/app",
|
||||
},
|
||||
expect: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(annDefaultBackend): "app",
|
||||
},
|
||||
expect: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(annDefaultBackend): "foo/app",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(annDefaultBackend): "test/app",
|
||||
buildNginxAnnotationKey(customHTTPError): "404,503",
|
||||
},
|
||||
expect: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
customHTTPErrors: []uint32{404, 503},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(annDefaultBackend): "test/app",
|
||||
buildNginxAnnotationKey(customHTTPError): "404,5ac",
|
||||
},
|
||||
expect: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
customHTTPErrors: []uint32{404},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "test",
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
}
|
||||
globalContext, cancel := initGlobalContextForService()
|
||||
defer cancel()
|
||||
|
||||
_ = fallback.Parse(inputCase.input, config, globalContext)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.Fallback) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackApplyRoute(t *testing.T) {
|
||||
fallback := fallback{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Fallback: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
customHTTPErrors: []uint32{404, 503},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{
|
||||
Name: "route",
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{},
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
Name: "route",
|
||||
InternalActiveRedirect: &networking.HTTPInternalActiveRedirect{
|
||||
MaxInternalRedirects: 1,
|
||||
RedirectResponseCodes: []uint32{404, 503},
|
||||
AllowCrossScheme: true,
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
FallbackInjectHeaderRouteName: "route" + FallbackRouteNameSuffix,
|
||||
FallbackInjectFallbackService: "test/app",
|
||||
},
|
||||
},
|
||||
},
|
||||
RedirectUrlRewriteSpecifier: &networking.HTTPInternalActiveRedirect_RedirectUrl{
|
||||
RedirectUrl: defaultRedirectUrl,
|
||||
},
|
||||
ForcedUseOriginalHost: true,
|
||||
ForcedAddHeaderBeforeRouteMatcher: true,
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
FallbackClusters: []*networking.Destination{
|
||||
{
|
||||
Host: "app.test.svc.cluster.local",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
fallback.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initGlobalContextForService() (*GlobalContext, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
client := fake.NewSimpleClientset(normalService, abnormalService)
|
||||
informerFactory := informers.NewSharedInformerFactory(client, time.Hour)
|
||||
serviceInformer := informerFactory.Core().V1().Services()
|
||||
go serviceInformer.Informer().Run(ctx.Done())
|
||||
cache.WaitForCacheSync(ctx.Done(), serviceInformer.Informer().HasSynced)
|
||||
|
||||
return &GlobalContext{
|
||||
ClusterServiceList: map[string]listerv1.ServiceLister{
|
||||
"cluster": serviceInformer.Lister(),
|
||||
},
|
||||
}, cancel
|
||||
}
|
||||
164
ingress/kube/annotations/downstreamtls.go
Normal file
164
ingress/kube/annotations/downstreamtls.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"github.com/alibaba/higress/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
"istio.io/istio/pilot/pkg/credentials/kube"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
gatewaytool "istio.io/istio/pkg/config/gateway"
|
||||
"istio.io/istio/pkg/config/security"
|
||||
)
|
||||
|
||||
const (
|
||||
authTLSSecret = "auth-tls-secret"
|
||||
tlsMinVersion = "tls-min-protocol-version"
|
||||
tlsMaxVersion = "tls-max-protocol-version"
|
||||
sslCipher = "ssl-cipher"
|
||||
)
|
||||
|
||||
type TLSProtocolVersion string
|
||||
|
||||
const (
|
||||
tlsV10 TLSProtocolVersion = "TLSv1.0"
|
||||
tlsV11 TLSProtocolVersion = "TLSv1.1"
|
||||
tlsV12 TLSProtocolVersion = "TLSv1.2"
|
||||
tlsV13 TLSProtocolVersion = "TLSv1.3"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &downstreamTLS{}
|
||||
_ GatewayHandler = &downstreamTLS{}
|
||||
|
||||
tlsProtocol = map[TLSProtocolVersion]networking.ServerTLSSettings_TLSProtocol{
|
||||
tlsV10: networking.ServerTLSSettings_TLSV1_0,
|
||||
tlsV11: networking.ServerTLSSettings_TLSV1_1,
|
||||
tlsV12: networking.ServerTLSSettings_TLSV1_2,
|
||||
tlsV13: networking.ServerTLSSettings_TLSV1_3,
|
||||
}
|
||||
)
|
||||
|
||||
func isValidTLSProtocolVersion(protocol string) bool {
|
||||
tls := TLSProtocolVersion(protocol)
|
||||
_, exist := tlsProtocol[tls]
|
||||
return exist
|
||||
}
|
||||
|
||||
func Convert(protocol string) networking.ServerTLSSettings_TLSProtocol {
|
||||
return tlsProtocol[TLSProtocolVersion(protocol)]
|
||||
}
|
||||
|
||||
type DownstreamTLSConfig struct {
|
||||
TlsMinVersion TLSProtocolVersion
|
||||
TlsMaxVersion TLSProtocolVersion
|
||||
CipherSuites []string
|
||||
Mode networking.ServerTLSSettings_TLSmode
|
||||
CASecretName model.NamespacedName
|
||||
}
|
||||
|
||||
type downstreamTLS struct{}
|
||||
|
||||
func (d downstreamTLS) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needDownstreamTLS(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
downstreamTLSConfig := &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
}
|
||||
defer func() {
|
||||
config.DownstreamTLS = downstreamTLSConfig
|
||||
}()
|
||||
|
||||
if secretName, err := annotations.ParseStringASAP(authTLSSecret); err == nil {
|
||||
namespacedName := util.SplitNamespacedName(secretName)
|
||||
if namespacedName.Name == "" {
|
||||
IngressLog.Errorf("CA secret name %s format is invalid.", secretName)
|
||||
} else {
|
||||
if namespacedName.Namespace == "" {
|
||||
namespacedName.Namespace = config.Namespace
|
||||
}
|
||||
downstreamTLSConfig.CASecretName = namespacedName
|
||||
downstreamTLSConfig.Mode = networking.ServerTLSSettings_MUTUAL
|
||||
}
|
||||
}
|
||||
|
||||
if minVersion, err := annotations.ParseStringForMSE(tlsMinVersion); err == nil &&
|
||||
isValidTLSProtocolVersion(minVersion) {
|
||||
downstreamTLSConfig.TlsMinVersion = TLSProtocolVersion(minVersion)
|
||||
}
|
||||
|
||||
if maxVersion, err := annotations.ParseStringForMSE(tlsMaxVersion); err == nil &&
|
||||
isValidTLSProtocolVersion(maxVersion) {
|
||||
downstreamTLSConfig.TlsMaxVersion = TLSProtocolVersion(maxVersion)
|
||||
}
|
||||
|
||||
if rawTlsCipherSuite, err := annotations.ParseStringASAP(sslCipher); err == nil {
|
||||
var validCipherSuite []string
|
||||
cipherList := strings.Split(rawTlsCipherSuite, ":")
|
||||
for _, cipher := range cipherList {
|
||||
if security.IsValidCipherSuite(cipher) {
|
||||
validCipherSuite = append(validCipherSuite, cipher)
|
||||
}
|
||||
}
|
||||
|
||||
downstreamTLSConfig.CipherSuites = validCipherSuite
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d downstreamTLS) ApplyGateway(gateway *networking.Gateway, config *Ingress) {
|
||||
if config.DownstreamTLS == nil {
|
||||
return
|
||||
}
|
||||
|
||||
downstreamTLSConfig := config.DownstreamTLS
|
||||
for _, server := range gateway.Servers {
|
||||
if gatewaytool.IsTLSServer(server) {
|
||||
if downstreamTLSConfig.CASecretName.Name != "" {
|
||||
serverCert := extraSecret(server.Tls.CredentialName)
|
||||
if downstreamTLSConfig.CASecretName.Namespace != serverCert.Namespace ||
|
||||
(downstreamTLSConfig.CASecretName.Name != serverCert.Name &&
|
||||
downstreamTLSConfig.CASecretName.Name != serverCert.Name+kube.GatewaySdsCaSuffix) {
|
||||
IngressLog.Errorf("CA secret %s is invalid", downstreamTLSConfig.CASecretName.String())
|
||||
} else {
|
||||
server.Tls.Mode = downstreamTLSConfig.Mode
|
||||
}
|
||||
}
|
||||
|
||||
if downstreamTLSConfig.TlsMinVersion != "" {
|
||||
server.Tls.MinProtocolVersion = tlsProtocol[downstreamTLSConfig.TlsMinVersion]
|
||||
}
|
||||
if downstreamTLSConfig.TlsMaxVersion != "" {
|
||||
server.Tls.MaxProtocolVersion = tlsProtocol[downstreamTLSConfig.TlsMaxVersion]
|
||||
}
|
||||
if len(downstreamTLSConfig.CipherSuites) != 0 {
|
||||
server.Tls.CipherSuites = downstreamTLSConfig.CipherSuites
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func needDownstreamTLS(annotations Annotations) bool {
|
||||
return annotations.HasMSE(tlsMinVersion) ||
|
||||
annotations.HasMSE(tlsMaxVersion) ||
|
||||
annotations.HasASAP(sslCipher) ||
|
||||
annotations.HasASAP(authTLSSecret)
|
||||
}
|
||||
351
ingress/kube/annotations/downstreamtls_test.go
Normal file
351
ingress/kube/annotations/downstreamtls_test.go
Normal file
@@ -0,0 +1,351 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
)
|
||||
|
||||
var parser = downstreamTLS{}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input map[string]string
|
||||
expect *DownstreamTLSConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "TLSv1.0",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
TlsMinVersion: tlsV10,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "TLSv1.3",
|
||||
DefaultAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
TlsMinVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "xxx",
|
||||
DefaultAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "xxx",
|
||||
MSEAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authTLSSecret): "test",
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "xxx",
|
||||
MSEAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "test",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(authTLSSecret): "test/foo",
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "TLSv1.3",
|
||||
DefaultAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "foo",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
TlsMinVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "foo",
|
||||
},
|
||||
}
|
||||
_ = parser.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(testCase.expect, config.DownstreamTLS) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGateway(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *networking.Gateway
|
||||
config *Ingress
|
||||
expect *networking.Gateway
|
||||
}{
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
TlsMinVersion: tlsV10,
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
TlsMinVersion: tlsV12,
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLSV1_2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
TlsMaxVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
TlsMaxVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar-cacert",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
TlsMaxVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "bar",
|
||||
Name: "foo",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
TlsMaxVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
parser.ApplyGateway(testCase.input, testCase.config)
|
||||
if !reflect.DeepEqual(testCase.input, testCase.expect) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
160
ingress/kube/annotations/header_control.go
Normal file
160
ingress/kube/annotations/header_control.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// request
|
||||
requestHeaderAdd = "request-header-control-add"
|
||||
requestHeaderUpdate = "request-header-control-update"
|
||||
requestHeaderRemove = "request-header-control-remove"
|
||||
|
||||
// response
|
||||
responseHeaderAdd = "response-header-control-add"
|
||||
responseHeaderUpdate = "response-header-control-update"
|
||||
responseHeaderRemove = "response-header-control-remove"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = headerControl{}
|
||||
_ RouteHandler = headerControl{}
|
||||
)
|
||||
|
||||
type HeaderOperation struct {
|
||||
Add map[string]string
|
||||
Update map[string]string
|
||||
Remove []string
|
||||
}
|
||||
|
||||
// HeaderControlConfig enforces header operations on route level.
|
||||
// Note: Canary route don't use header control applied on the normal route.
|
||||
type HeaderControlConfig struct {
|
||||
Request *HeaderOperation
|
||||
Response *HeaderOperation
|
||||
}
|
||||
|
||||
type headerControl struct{}
|
||||
|
||||
func (h headerControl) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needHeaderControlConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
config.HeaderControl = &HeaderControlConfig{}
|
||||
|
||||
var requestAdd map[string]string
|
||||
var requestUpdate map[string]string
|
||||
var requestRemove []string
|
||||
if add, err := annotations.ParseStringForMSE(requestHeaderAdd); err == nil {
|
||||
requestAdd = convertAddOrUpdate(add)
|
||||
}
|
||||
if update, err := annotations.ParseStringForMSE(requestHeaderUpdate); err == nil {
|
||||
requestUpdate = convertAddOrUpdate(update)
|
||||
}
|
||||
if remove, err := annotations.ParseStringForMSE(requestHeaderRemove); err == nil {
|
||||
requestRemove = splitBySeparator(remove, ",")
|
||||
}
|
||||
if len(requestAdd) > 0 || len(requestUpdate) > 0 || len(requestRemove) > 0 {
|
||||
config.HeaderControl.Request = &HeaderOperation{
|
||||
Add: requestAdd,
|
||||
Update: requestUpdate,
|
||||
Remove: requestRemove,
|
||||
}
|
||||
}
|
||||
|
||||
var responseAdd map[string]string
|
||||
var responseUpdate map[string]string
|
||||
var responseRemove []string
|
||||
if add, err := annotations.ParseStringForMSE(responseHeaderAdd); err == nil {
|
||||
responseAdd = convertAddOrUpdate(add)
|
||||
}
|
||||
if update, err := annotations.ParseStringForMSE(responseHeaderUpdate); err == nil {
|
||||
responseUpdate = convertAddOrUpdate(update)
|
||||
}
|
||||
if remove, err := annotations.ParseStringForMSE(responseHeaderRemove); err == nil {
|
||||
responseRemove = splitBySeparator(remove, ",")
|
||||
}
|
||||
if len(responseAdd) > 0 || len(responseUpdate) > 0 || len(responseRemove) > 0 {
|
||||
config.HeaderControl.Response = &HeaderOperation{
|
||||
Add: responseAdd,
|
||||
Update: responseUpdate,
|
||||
Remove: responseRemove,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h headerControl) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
headerControlConfig := config.HeaderControl
|
||||
if headerControlConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
headers := &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{},
|
||||
Response: &networking.Headers_HeaderOperations{},
|
||||
}
|
||||
if headerControlConfig.Request != nil {
|
||||
headers.Request.Add = headerControlConfig.Request.Add
|
||||
headers.Request.Set = headerControlConfig.Request.Update
|
||||
headers.Request.Remove = headerControlConfig.Request.Remove
|
||||
}
|
||||
|
||||
if headerControlConfig.Response != nil {
|
||||
headers.Response.Add = headerControlConfig.Response.Add
|
||||
headers.Response.Set = headerControlConfig.Response.Update
|
||||
headers.Response.Remove = headerControlConfig.Response.Remove
|
||||
}
|
||||
|
||||
route.Headers = headers
|
||||
}
|
||||
|
||||
func needHeaderControlConfig(annotations Annotations) bool {
|
||||
return annotations.HasMSE(requestHeaderAdd) ||
|
||||
annotations.HasMSE(requestHeaderUpdate) ||
|
||||
annotations.HasMSE(requestHeaderRemove) ||
|
||||
annotations.HasMSE(responseHeaderAdd) ||
|
||||
annotations.HasMSE(responseHeaderUpdate) ||
|
||||
annotations.HasMSE(responseHeaderRemove)
|
||||
}
|
||||
|
||||
func convertAddOrUpdate(headers string) map[string]string {
|
||||
result := map[string]string{}
|
||||
parts := strings.Split(headers, "\n")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
keyValue := strings.Fields(part)
|
||||
if len(keyValue) != 2 {
|
||||
IngressLog.Infof("Header format %s is invalid.", keyValue)
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(keyValue[0])
|
||||
value := strings.TrimSpace(keyValue[1])
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
235
ingress/kube/annotations/header_control_test.go
Normal file
235
ingress/kube/annotations/header_control_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestHeaderControlParse(t *testing.T) {
|
||||
headerControl := &headerControl{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *HeaderControlConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(requestHeaderAdd): "one 1",
|
||||
buildMSEAnnotationKey(responseHeaderAdd): "A a",
|
||||
},
|
||||
expect: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"one": "1",
|
||||
},
|
||||
},
|
||||
Response: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(requestHeaderAdd): "one 1\n two 2\nthree 3 \n",
|
||||
buildMSEAnnotationKey(requestHeaderUpdate): "two 2",
|
||||
buildMSEAnnotationKey(requestHeaderRemove): "one, two,three\n",
|
||||
buildMSEAnnotationKey(responseHeaderAdd): "A a\nB b\n",
|
||||
buildMSEAnnotationKey(responseHeaderUpdate): "X x\nY y\n",
|
||||
buildMSEAnnotationKey(responseHeaderRemove): "x",
|
||||
},
|
||||
expect: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"three": "3",
|
||||
},
|
||||
Update: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
Response: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Update: map[string]string{
|
||||
"X": "x",
|
||||
"Y": "y",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = headerControl.Parse(inputCase.input, config, nil)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.HeaderControl) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderControlApplyRoute(t *testing.T) {
|
||||
headerControl := headerControl{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
HeaderControl: &HeaderControlConfig{},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{},
|
||||
Response: &networking.Headers_HeaderOperations{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
HeaderControl: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"three": "3",
|
||||
},
|
||||
Update: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"three": "3",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
Response: &networking.Headers_HeaderOperations{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
HeaderControl: &HeaderControlConfig{
|
||||
Response: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Update: map[string]string{
|
||||
"X": "x",
|
||||
"Y": "y",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{},
|
||||
Response: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"X": "x",
|
||||
"Y": "y",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
HeaderControl: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Update: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
Response: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Set: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
Response: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
headerControl.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
42
ingress/kube/annotations/interface.go
Normal file
42
ingress/kube/annotations/interface.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 annotations
|
||||
|
||||
import networking "istio.io/api/networking/v1alpha3"
|
||||
|
||||
type Parser interface {
|
||||
// Parse parses ingress annotations and puts result on config
|
||||
Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error
|
||||
}
|
||||
|
||||
type GatewayHandler interface {
|
||||
// ApplyGateway parsed ingress annotation config reflected on gateway
|
||||
ApplyGateway(gateway *networking.Gateway, config *Ingress)
|
||||
}
|
||||
|
||||
type VirtualServiceHandler interface {
|
||||
// ApplyVirtualServiceHandler parsed ingress annotation config reflected on virtual host
|
||||
ApplyVirtualServiceHandler(virtualService *networking.VirtualService, config *Ingress)
|
||||
}
|
||||
|
||||
type RouteHandler interface {
|
||||
// ApplyRoute parsed ingress annotation config reflected on route
|
||||
ApplyRoute(route *networking.HTTPRoute, config *Ingress)
|
||||
}
|
||||
|
||||
type TrafficPolicyHandler interface {
|
||||
// ApplyTrafficPolicy parsed ingress annotation config reflected on traffic policy
|
||||
ApplyTrafficPolicy(trafficPolicy *networking.TrafficPolicy_PortTrafficPolicy, config *Ingress)
|
||||
}
|
||||
144
ingress/kube/annotations/ip_access_control.go
Normal file
144
ingress/kube/annotations/ip_access_control.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/networking/core/v1alpha3/mseingress"
|
||||
)
|
||||
|
||||
const (
|
||||
domainWhitelist = "domain-whitelist-source-range"
|
||||
domainBlacklist = "domain-blacklist-source-range"
|
||||
whitelist = "whitelist-source-range"
|
||||
blacklist = "blacklist-source-range"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &ipAccessControl{}
|
||||
_ RouteHandler = &ipAccessControl{}
|
||||
)
|
||||
|
||||
type IPAccessControl struct {
|
||||
isWhite bool
|
||||
remoteIp []string
|
||||
}
|
||||
|
||||
type IPAccessControlConfig struct {
|
||||
Domain *IPAccessControl
|
||||
Route *IPAccessControl
|
||||
}
|
||||
|
||||
type ipAccessControl struct{}
|
||||
|
||||
func (i ipAccessControl) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needIPAccessControlConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ipConfig := &IPAccessControlConfig{}
|
||||
defer func() {
|
||||
config.IPAccessControl = ipConfig
|
||||
}()
|
||||
|
||||
var domain *IPAccessControl
|
||||
rawWhitelist, err := annotations.ParseStringForMSE(domainWhitelist)
|
||||
if err == nil {
|
||||
domain = &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: splitStringWithSpaceTrim(rawWhitelist),
|
||||
}
|
||||
} else {
|
||||
if rawBlacklist, err := annotations.ParseStringForMSE(domainBlacklist); err == nil {
|
||||
domain = &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: splitStringWithSpaceTrim(rawBlacklist),
|
||||
}
|
||||
}
|
||||
}
|
||||
if domain != nil {
|
||||
ipConfig.Domain = domain
|
||||
}
|
||||
|
||||
var route *IPAccessControl
|
||||
rawWhitelist, err = annotations.ParseStringASAP(whitelist)
|
||||
if err == nil {
|
||||
route = &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: splitStringWithSpaceTrim(rawWhitelist),
|
||||
}
|
||||
} else {
|
||||
if rawBlacklist, err := annotations.ParseStringForMSE(blacklist); err == nil {
|
||||
route = &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: splitStringWithSpaceTrim(rawBlacklist),
|
||||
}
|
||||
}
|
||||
}
|
||||
if route != nil {
|
||||
ipConfig.Route = route
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i ipAccessControl) ApplyVirtualServiceHandler(virtualService *networking.VirtualService, config *Ingress) {
|
||||
ac := config.IPAccessControl
|
||||
if ac == nil || ac.Domain == nil {
|
||||
return
|
||||
}
|
||||
|
||||
filter := &networking.IPAccessControl{}
|
||||
if ac.Domain.isWhite {
|
||||
filter.RemoteIpBlocks = ac.Domain.remoteIp
|
||||
} else {
|
||||
filter.NotRemoteIpBlocks = ac.Domain.remoteIp
|
||||
}
|
||||
|
||||
virtualService.HostHTTPFilters = append(virtualService.HostHTTPFilters, &networking.HTTPFilter{
|
||||
Name: mseingress.IPAccessControl,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: filter,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (i ipAccessControl) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
ac := config.IPAccessControl
|
||||
if ac == nil || ac.Route == nil {
|
||||
return
|
||||
}
|
||||
|
||||
filter := &networking.IPAccessControl{}
|
||||
if ac.Route.isWhite {
|
||||
filter.RemoteIpBlocks = ac.Route.remoteIp
|
||||
} else {
|
||||
filter.NotRemoteIpBlocks = ac.Route.remoteIp
|
||||
}
|
||||
|
||||
route.RouteHTTPFilters = append(route.RouteHTTPFilters, &networking.HTTPFilter{
|
||||
Name: mseingress.IPAccessControl,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: filter,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func needIPAccessControlConfig(annotations Annotations) bool {
|
||||
return annotations.HasMSE(domainWhitelist) ||
|
||||
annotations.HasMSE(domainBlacklist) ||
|
||||
annotations.HasASAP(whitelist) ||
|
||||
annotations.HasMSE(blacklist)
|
||||
}
|
||||
241
ingress/kube/annotations/ip_access_control_test.go
Normal file
241
ingress/kube/annotations/ip_access_control_test.go
Normal file
@@ -0,0 +1,241 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestIPAccessControlParse(t *testing.T) {
|
||||
parser := ipAccessControl{}
|
||||
|
||||
testCases := []struct {
|
||||
input map[string]string
|
||||
expect *IPAccessControlConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
DefaultAnnotationsPrefix + "/" + whitelist: "1.1.1.1",
|
||||
MSEAnnotationsPrefix + "/" + blacklist: "2.2.2.2",
|
||||
},
|
||||
expect: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + blacklist: "2.2.2.2",
|
||||
},
|
||||
expect: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + domainWhitelist: "1.1.1.1",
|
||||
},
|
||||
expect: &IPAccessControlConfig{
|
||||
Domain: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + whitelist: "1.1.1.1, 3.3.3.3",
|
||||
MSEAnnotationsPrefix + "/" + domainBlacklist: "2.2.2.2",
|
||||
},
|
||||
expect: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1", "3.3.3.3"},
|
||||
},
|
||||
Domain: &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = parser.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(testCase.expect, config.IPAccessControl) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIpAccessControl_ApplyVirtualServiceHandler(t *testing.T) {
|
||||
parser := ipAccessControl{}
|
||||
|
||||
testCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.VirtualService
|
||||
expect *networking.HTTPFilter
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.VirtualService{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
IPAccessControl: &IPAccessControlConfig{
|
||||
Domain: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.VirtualService{},
|
||||
expect: &networking.HTTPFilter{
|
||||
Name: "ip-access-control",
|
||||
Disable: false,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: &networking.IPAccessControl{
|
||||
RemoteIpBlocks: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
IPAccessControl: &IPAccessControlConfig{
|
||||
Domain: &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.VirtualService{},
|
||||
expect: &networking.HTTPFilter{
|
||||
Name: "ip-access-control",
|
||||
Disable: false,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: &networking.IPAccessControl{
|
||||
NotRemoteIpBlocks: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
parser.ApplyVirtualServiceHandler(testCase.input, testCase.config)
|
||||
if testCase.config.IPAccessControl == nil {
|
||||
if len(testCase.input.HostHTTPFilters) != 0 {
|
||||
t.Fatalf("Should be empty")
|
||||
}
|
||||
} else {
|
||||
if len(testCase.input.HostHTTPFilters) == 0 {
|
||||
t.Fatalf("Should be not empty")
|
||||
}
|
||||
if !reflect.DeepEqual(testCase.expect, testCase.input.HostHTTPFilters[0]) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIpAccessControl_ApplyRoute(t *testing.T) {
|
||||
parser := ipAccessControl{}
|
||||
|
||||
testCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPFilter
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
IPAccessControl: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPFilter{
|
||||
Name: "ip-access-control",
|
||||
Disable: false,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: &networking.IPAccessControl{
|
||||
RemoteIpBlocks: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
IPAccessControl: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPFilter{
|
||||
Name: "ip-access-control",
|
||||
Disable: false,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: &networking.IPAccessControl{
|
||||
NotRemoteIpBlocks: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
parser.ApplyRoute(testCase.input, testCase.config)
|
||||
if testCase.config.IPAccessControl == nil {
|
||||
if len(testCase.input.RouteHTTPFilters) != 0 {
|
||||
t.Fatalf("Should be empty")
|
||||
}
|
||||
} else {
|
||||
if len(testCase.input.RouteHTTPFilters) == 0 {
|
||||
t.Fatalf("Should be not empty")
|
||||
}
|
||||
if !reflect.DeepEqual(testCase.expect, testCase.input.RouteHTTPFilters[0]) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
212
ingress/kube/annotations/loadbalance.go
Normal file
212
ingress/kube/annotations/loadbalance.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
loadBalanceAnnotation = "load-balance"
|
||||
upstreamHashBy = "upstream-hash-by"
|
||||
// affinity in nginx/mse ingress always be cookie
|
||||
affinity = "affinity"
|
||||
// affinityMode in mse ingress always be balanced
|
||||
affinityMode = "affinity-mode"
|
||||
// affinityCanaryBehavior in mse ingress always be legacy
|
||||
affinityCanaryBehavior = "affinity-canary-behavior"
|
||||
sessionCookieName = "session-cookie-name"
|
||||
sessionCookiePath = "session-cookie-path"
|
||||
sessionCookieMaxAge = "session-cookie-max-age"
|
||||
sessionCookieExpires = "session-cookie-expires"
|
||||
warmup = "warmup"
|
||||
|
||||
varIndicator = "$"
|
||||
headerIndicator = "$http_"
|
||||
queryParamIndicator = "$arg_"
|
||||
|
||||
defaultAffinityCookieName = "INGRESSCOOKIE"
|
||||
defaultAffinityCookiePath = "/"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = loadBalance{}
|
||||
_ TrafficPolicyHandler = loadBalance{}
|
||||
|
||||
headersMapping = map[string]string{
|
||||
"$request_uri": ":path",
|
||||
"$host": ":authority",
|
||||
"$remote_addr": "x-envoy-external-address",
|
||||
}
|
||||
)
|
||||
|
||||
type consistentHashByOther struct {
|
||||
header string
|
||||
queryParam string
|
||||
}
|
||||
|
||||
type consistentHashByCookie struct {
|
||||
name string
|
||||
path string
|
||||
age *types.Duration
|
||||
}
|
||||
|
||||
type LoadBalanceConfig struct {
|
||||
simple networking.LoadBalancerSettings_SimpleLB
|
||||
warmup *types.Duration
|
||||
other *consistentHashByOther
|
||||
cookie *consistentHashByCookie
|
||||
}
|
||||
|
||||
type loadBalance struct{}
|
||||
|
||||
func (l loadBalance) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needLoadBalanceConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
loadBalanceConfig := &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_ROUND_ROBIN,
|
||||
}
|
||||
defer func() {
|
||||
config.LoadBalance = loadBalanceConfig
|
||||
}()
|
||||
|
||||
if isCookieAffinity(annotations) {
|
||||
loadBalanceConfig.cookie = &consistentHashByCookie{
|
||||
name: defaultAffinityCookieName,
|
||||
path: defaultAffinityCookiePath,
|
||||
age: &types.Duration{},
|
||||
}
|
||||
if name, err := annotations.ParseStringASAP(sessionCookieName); err == nil {
|
||||
loadBalanceConfig.cookie.name = name
|
||||
}
|
||||
if path, err := annotations.ParseStringASAP(sessionCookiePath); err == nil {
|
||||
loadBalanceConfig.cookie.path = path
|
||||
}
|
||||
if age, err := annotations.ParseIntASAP(sessionCookieMaxAge); err == nil {
|
||||
loadBalanceConfig.cookie.age = &types.Duration{
|
||||
Seconds: int64(age),
|
||||
}
|
||||
} else if age, err = annotations.ParseIntASAP(sessionCookieExpires); err == nil {
|
||||
loadBalanceConfig.cookie.age = &types.Duration{
|
||||
Seconds: int64(age),
|
||||
}
|
||||
}
|
||||
} else if isOtherAffinity(annotations) {
|
||||
if key, err := annotations.ParseStringASAP(upstreamHashBy); err == nil &&
|
||||
strings.HasPrefix(key, varIndicator) {
|
||||
value, exist := headersMapping[key]
|
||||
if exist {
|
||||
loadBalanceConfig.other = &consistentHashByOther{
|
||||
header: value,
|
||||
}
|
||||
} else {
|
||||
if strings.HasPrefix(key, headerIndicator) {
|
||||
loadBalanceConfig.other = &consistentHashByOther{
|
||||
header: strings.TrimPrefix(key, headerIndicator),
|
||||
}
|
||||
} else if strings.HasPrefix(key, queryParamIndicator) {
|
||||
loadBalanceConfig.other = &consistentHashByOther{
|
||||
queryParam: strings.TrimPrefix(key, queryParamIndicator),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if lb, err := annotations.ParseStringASAP(loadBalanceAnnotation); err == nil {
|
||||
lb = strings.ToUpper(lb)
|
||||
loadBalanceConfig.simple = networking.LoadBalancerSettings_SimpleLB(networking.LoadBalancerSettings_SimpleLB_value[lb])
|
||||
}
|
||||
|
||||
if warmup, err := annotations.ParseIntForMSE(warmup); err == nil && warmup != 0 {
|
||||
loadBalanceConfig.warmup = &types.Duration{
|
||||
Seconds: int64(warmup),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l loadBalance) ApplyTrafficPolicy(trafficPolicy *networking.TrafficPolicy_PortTrafficPolicy, config *Ingress) {
|
||||
loadBalanceConfig := config.LoadBalance
|
||||
if loadBalanceConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if loadBalanceConfig.cookie != nil {
|
||||
trafficPolicy.LoadBalancer = &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie{
|
||||
HttpCookie: &networking.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{
|
||||
Name: loadBalanceConfig.cookie.name,
|
||||
Path: loadBalanceConfig.cookie.path,
|
||||
Ttl: loadBalanceConfig.cookie.age,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if loadBalanceConfig.other != nil {
|
||||
var consistentHash *networking.LoadBalancerSettings_ConsistentHashLB
|
||||
if loadBalanceConfig.other.header != "" {
|
||||
consistentHash = &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpHeaderName{
|
||||
HttpHeaderName: loadBalanceConfig.other.header,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
consistentHash = &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpQueryParameterName{
|
||||
HttpQueryParameterName: loadBalanceConfig.other.queryParam,
|
||||
},
|
||||
}
|
||||
}
|
||||
trafficPolicy.LoadBalancer = &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: consistentHash,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
trafficPolicy.LoadBalancer = &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_Simple{
|
||||
Simple: loadBalanceConfig.simple,
|
||||
},
|
||||
}
|
||||
trafficPolicy.LoadBalancer.WarmupDurationSecs = loadBalanceConfig.warmup
|
||||
}
|
||||
}
|
||||
|
||||
func isCookieAffinity(annotations Annotations) bool {
|
||||
return annotations.HasASAP(affinity) ||
|
||||
annotations.HasASAP(sessionCookieName) ||
|
||||
annotations.HasASAP(sessionCookiePath)
|
||||
}
|
||||
|
||||
func isOtherAffinity(annotations Annotations) bool {
|
||||
return annotations.HasASAP(upstreamHashBy)
|
||||
}
|
||||
|
||||
func needLoadBalanceConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(loadBalanceAnnotation) ||
|
||||
annotations.HasMSE(warmup) ||
|
||||
isCookieAffinity(annotations) ||
|
||||
isOtherAffinity(annotations)
|
||||
}
|
||||
294
ingress/kube/annotations/loadbalance_test.go
Normal file
294
ingress/kube/annotations/loadbalance_test.go
Normal file
@@ -0,0 +1,294 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestLoadBalanceParse(t *testing.T) {
|
||||
loadBalance := loadBalance{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *LoadBalanceConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(affinity): "cookie",
|
||||
buildNginxAnnotationKey(affinityMode): "balanced",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
cookie: &consistentHashByCookie{
|
||||
name: defaultAffinityCookieName,
|
||||
path: defaultAffinityCookiePath,
|
||||
age: &types.Duration{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(affinity): "cookie",
|
||||
buildNginxAnnotationKey(affinityMode): "balanced",
|
||||
buildNginxAnnotationKey(sessionCookieName): "test",
|
||||
buildNginxAnnotationKey(sessionCookiePath): "/test",
|
||||
buildNginxAnnotationKey(sessionCookieMaxAge): "100",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
cookie: &consistentHashByCookie{
|
||||
name: "test",
|
||||
path: "/test",
|
||||
age: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(affinity): "cookie",
|
||||
buildNginxAnnotationKey(affinityMode): "balanced",
|
||||
buildNginxAnnotationKey(sessionCookieName): "test",
|
||||
buildNginxAnnotationKey(sessionCookieExpires): "10",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
cookie: &consistentHashByCookie{
|
||||
name: "test",
|
||||
path: defaultAffinityCookiePath,
|
||||
age: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$request_uri",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: ":path",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$host",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: ":authority",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$remote_addr",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: "x-envoy-external-address",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$http_test",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$arg_query",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
queryParam: "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(warmup): "100",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_ROUND_ROBIN,
|
||||
warmup: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(loadBalanceAnnotation): "LEAST_CONN",
|
||||
buildMSEAnnotationKey(warmup): "100",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_LEAST_CONN,
|
||||
warmup: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(loadBalanceAnnotation): "random",
|
||||
buildMSEAnnotationKey(warmup): "100",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_RANDOM,
|
||||
warmup: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = loadBalance.Parse(inputCase.input, config, nil)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.LoadBalance) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalanceApplyTrafficPolicy(t *testing.T) {
|
||||
loadBalance := loadBalance{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.TrafficPolicy_PortTrafficPolicy
|
||||
expect *networking.TrafficPolicy_PortTrafficPolicy
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
LoadBalance: &LoadBalanceConfig{
|
||||
cookie: &consistentHashByCookie{
|
||||
name: "test",
|
||||
path: "/",
|
||||
age: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
LoadBalancer: &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie{
|
||||
HttpCookie: &networking.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{
|
||||
Name: "test",
|
||||
Path: "/",
|
||||
Ttl: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
LoadBalance: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: ":authority",
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
LoadBalancer: &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpHeaderName{
|
||||
HttpHeaderName: ":authority",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
LoadBalance: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
queryParam: "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
LoadBalancer: &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpQueryParameterName{
|
||||
HttpQueryParameterName: "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
LoadBalance: &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_ROUND_ROBIN,
|
||||
warmup: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
LoadBalancer: &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_Simple{
|
||||
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
|
||||
},
|
||||
WarmupDurationSecs: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
loadBalance.ApplyTrafficPolicy(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
110
ingress/kube/annotations/local_rate_limit.go
Normal file
110
ingress/kube/annotations/local_rate_limit.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/networking/core/v1alpha3/mseingress"
|
||||
)
|
||||
|
||||
const (
|
||||
limitRPM = "route-limit-rpm"
|
||||
limitRPS = "route-limit-rps"
|
||||
limitBurstMultiplier = "route-limit-burst-multiplier"
|
||||
|
||||
defaultBurstMultiplier = 5
|
||||
defaultStatusCode = 503
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = localRateLimit{}
|
||||
_ RouteHandler = localRateLimit{}
|
||||
|
||||
second = &types.Duration{
|
||||
Seconds: 1,
|
||||
}
|
||||
|
||||
minute = &types.Duration{
|
||||
Seconds: 60,
|
||||
}
|
||||
)
|
||||
|
||||
type localRateLimitConfig struct {
|
||||
TokensPerFill uint32
|
||||
MaxTokens uint32
|
||||
FillInterval *types.Duration
|
||||
}
|
||||
|
||||
type localRateLimit struct{}
|
||||
|
||||
func (l localRateLimit) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needLocalRateLimitConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var local *localRateLimitConfig
|
||||
defer func() {
|
||||
config.localRateLimit = local
|
||||
}()
|
||||
|
||||
multiplier := defaultBurstMultiplier
|
||||
if m, err := annotations.ParseIntForMSE(limitBurstMultiplier); err == nil {
|
||||
multiplier = m
|
||||
}
|
||||
|
||||
if rpm, err := annotations.ParseIntForMSE(limitRPM); err == nil {
|
||||
local = &localRateLimitConfig{
|
||||
MaxTokens: uint32(rpm * multiplier),
|
||||
TokensPerFill: uint32(rpm),
|
||||
FillInterval: minute,
|
||||
}
|
||||
} else if rps, err := annotations.ParseIntForMSE(limitRPS); err == nil {
|
||||
local = &localRateLimitConfig{
|
||||
MaxTokens: uint32(rps * multiplier),
|
||||
TokensPerFill: uint32(rps),
|
||||
FillInterval: second,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l localRateLimit) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
localRateLimitConfig := config.localRateLimit
|
||||
if localRateLimitConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
route.RouteHTTPFilters = append(route.RouteHTTPFilters, &networking.HTTPFilter{
|
||||
Name: mseingress.LocalRateLimit,
|
||||
Filter: &networking.HTTPFilter_LocalRateLimit{
|
||||
LocalRateLimit: &networking.LocalRateLimit{
|
||||
TokenBucket: &networking.TokenBucket{
|
||||
MaxTokens: localRateLimitConfig.MaxTokens,
|
||||
TokensPefFill: localRateLimitConfig.TokensPerFill,
|
||||
FillInterval: localRateLimitConfig.FillInterval,
|
||||
},
|
||||
StatusCode: defaultStatusCode,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func needLocalRateLimitConfig(annotations Annotations) bool {
|
||||
return annotations.HasMSE(limitRPM) ||
|
||||
annotations.HasMSE(limitRPS)
|
||||
}
|
||||
127
ingress/kube/annotations/local_rate_limit_test.go
Normal file
127
ingress/kube/annotations/local_rate_limit_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/networking/core/v1alpha3/mseingress"
|
||||
)
|
||||
|
||||
func TestLocalRateLimitParse(t *testing.T) {
|
||||
localRateLimit := localRateLimit{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *localRateLimitConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(limitRPM): "2",
|
||||
},
|
||||
expect: &localRateLimitConfig{
|
||||
MaxTokens: 10,
|
||||
TokensPerFill: 2,
|
||||
FillInterval: minute,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(limitRPM): "2",
|
||||
buildMSEAnnotationKey(limitRPS): "3",
|
||||
buildMSEAnnotationKey(limitBurstMultiplier): "10",
|
||||
},
|
||||
expect: &localRateLimitConfig{
|
||||
MaxTokens: 20,
|
||||
TokensPerFill: 2,
|
||||
FillInterval: minute,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(limitRPS): "3",
|
||||
buildMSEAnnotationKey(limitBurstMultiplier): "10",
|
||||
},
|
||||
expect: &localRateLimitConfig{
|
||||
MaxTokens: 30,
|
||||
TokensPerFill: 3,
|
||||
FillInterval: second,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = localRateLimit.Parse(inputCase.input, config, nil)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.localRateLimit) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRateLimitApplyRoute(t *testing.T) {
|
||||
localRateLimit := localRateLimit{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
localRateLimit: &localRateLimitConfig{
|
||||
MaxTokens: 60,
|
||||
TokensPerFill: 20,
|
||||
FillInterval: second,
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
RouteHTTPFilters: []*networking.HTTPFilter{
|
||||
{
|
||||
Name: mseingress.LocalRateLimit,
|
||||
Filter: &networking.HTTPFilter_LocalRateLimit{
|
||||
LocalRateLimit: &networking.LocalRateLimit{
|
||||
TokenBucket: &networking.TokenBucket{
|
||||
MaxTokens: 60,
|
||||
TokensPefFill: 20,
|
||||
FillInterval: second,
|
||||
},
|
||||
StatusCode: defaultStatusCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
localRateLimit.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
216
ingress/kube/annotations/parser.go
Normal file
216
ingress/kube/annotations/parser.go
Normal file
@@ -0,0 +1,216 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller
|
||||
DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
|
||||
|
||||
// MSEAnnotationsPrefix defines the common prefix used in the mse ingress controller
|
||||
MSEAnnotationsPrefix = "mse.ingress.kubernetes.io"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingAnnotations the ingress rule does not contain annotations
|
||||
// This is an error only when annotations are being parsed
|
||||
ErrMissingAnnotations = errors.New("ingress rule without annotations")
|
||||
|
||||
// ErrInvalidAnnotationName the ingress rule does contain an invalid
|
||||
// annotation name
|
||||
ErrInvalidAnnotationName = errors.New("invalid annotation name")
|
||||
|
||||
// ErrInvalidAnnotationValue the ingress rule does contain an invalid
|
||||
// annotation value
|
||||
ErrInvalidAnnotationValue = errors.New("invalid annotation value")
|
||||
)
|
||||
|
||||
// IsMissingAnnotations checks if the error is an error which
|
||||
// indicates the ingress does not contain annotations
|
||||
func IsMissingAnnotations(e error) bool {
|
||||
return e == ErrMissingAnnotations
|
||||
}
|
||||
|
||||
type Annotations map[string]string
|
||||
|
||||
func (a Annotations) ParseBool(key string) (bool, error) {
|
||||
if len(a) == 0 {
|
||||
return false, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildNginxAnnotationKey(key)]
|
||||
if ok {
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return false, ErrInvalidAnnotationValue
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
return false, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseBoolForMSE(key string) (bool, error) {
|
||||
if len(a) == 0 {
|
||||
return false, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildMSEAnnotationKey(key)]
|
||||
if ok {
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return false, ErrInvalidAnnotationValue
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
return false, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseBoolASAP(key string) (bool, error) {
|
||||
if result, err := a.ParseBool(key); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
return a.ParseBoolForMSE(key)
|
||||
}
|
||||
|
||||
func (a Annotations) ParseString(key string) (string, error) {
|
||||
if len(a) == 0 {
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildNginxAnnotationKey(key)]
|
||||
if ok {
|
||||
s := normalizeString(val)
|
||||
if s == "" {
|
||||
return "", ErrInvalidAnnotationValue
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseStringForMSE(key string) (string, error) {
|
||||
if len(a) == 0 {
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildMSEAnnotationKey(key)]
|
||||
if ok {
|
||||
s := normalizeString(val)
|
||||
if s == "" {
|
||||
return "", ErrInvalidAnnotationValue
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
// ParseStringASAP will first extra config from nginx annotation, then will
|
||||
// try to extra config from mse annotation if the first step fails.
|
||||
func (a Annotations) ParseStringASAP(key string) (string, error) {
|
||||
if result, err := a.ParseString(key); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
return a.ParseStringForMSE(key)
|
||||
}
|
||||
|
||||
func (a Annotations) ParseInt(key string) (int, error) {
|
||||
if len(a) == 0 {
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildNginxAnnotationKey(key)]
|
||||
if ok {
|
||||
i, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidAnnotationValue
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseIntForMSE(key string) (int, error) {
|
||||
if len(a) == 0 {
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildMSEAnnotationKey(key)]
|
||||
if ok {
|
||||
i, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidAnnotationValue
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseIntASAP(key string) (int, error) {
|
||||
if result, err := a.ParseInt(key); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
return a.ParseIntForMSE(key)
|
||||
}
|
||||
|
||||
func (a Annotations) Has(key string) bool {
|
||||
if len(a) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
_, exist := a[buildNginxAnnotationKey(key)]
|
||||
return exist
|
||||
}
|
||||
|
||||
func (a Annotations) HasMSE(key string) bool {
|
||||
if len(a) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
_, exist := a[buildMSEAnnotationKey(key)]
|
||||
return exist
|
||||
}
|
||||
|
||||
func (a Annotations) HasASAP(key string) bool {
|
||||
if a.Has(key) {
|
||||
return true
|
||||
}
|
||||
return a.HasMSE(key)
|
||||
}
|
||||
|
||||
func buildNginxAnnotationKey(key string) string {
|
||||
return DefaultAnnotationsPrefix + "/" + key
|
||||
}
|
||||
|
||||
func buildMSEAnnotationKey(key string) string {
|
||||
return MSEAnnotationsPrefix + "/" + key
|
||||
}
|
||||
|
||||
func normalizeString(input string) string {
|
||||
var trimmedContent []string
|
||||
for _, line := range strings.Split(input, "\n") {
|
||||
trimmedContent = append(trimmedContent, strings.TrimSpace(line))
|
||||
}
|
||||
|
||||
return strings.Join(trimmedContent, "\n")
|
||||
}
|
||||
152
ingress/kube/annotations/redirect.go
Normal file
152
ingress/kube/annotations/redirect.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
appRoot = "app-root"
|
||||
temporalRedirect = "temporal-redirect"
|
||||
permanentRedirect = "permanent-redirect"
|
||||
permanentRedirectCode = "permanent-redirect-code"
|
||||
sslRedirect = "ssl-redirect"
|
||||
forceSSLRedirect = "force-ssl-redirect"
|
||||
|
||||
defaultPermanentRedirectCode = 301
|
||||
defaultTemporalRedirectCode = 302
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &redirect{}
|
||||
_ RouteHandler = &redirect{}
|
||||
)
|
||||
|
||||
type RedirectConfig struct {
|
||||
AppRoot string
|
||||
|
||||
URL string
|
||||
|
||||
Code int
|
||||
|
||||
httpsRedirect bool
|
||||
}
|
||||
|
||||
type redirect struct{}
|
||||
|
||||
func (r redirect) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needRedirectConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
redirectConfig := &RedirectConfig{
|
||||
Code: defaultPermanentRedirectCode,
|
||||
}
|
||||
config.Redirect = redirectConfig
|
||||
|
||||
redirectConfig.AppRoot, _ = annotations.ParseStringASAP(appRoot)
|
||||
|
||||
httpsRedirect, _ := annotations.ParseBoolASAP(sslRedirect)
|
||||
forceHTTPSRedirect, _ := annotations.ParseBoolASAP(forceSSLRedirect)
|
||||
if httpsRedirect || forceHTTPSRedirect {
|
||||
redirectConfig.httpsRedirect = true
|
||||
}
|
||||
|
||||
// temporal redirect is firstly applied.
|
||||
tr, err := annotations.ParseStringASAP(temporalRedirect)
|
||||
if err != nil && !IsMissingAnnotations(err) {
|
||||
return nil
|
||||
}
|
||||
if tr != "" && isValidURL(tr) == nil {
|
||||
redirectConfig.URL = tr
|
||||
redirectConfig.Code = defaultTemporalRedirectCode
|
||||
return nil
|
||||
}
|
||||
|
||||
// permanent redirect
|
||||
// url
|
||||
pr, err := annotations.ParseStringASAP(permanentRedirect)
|
||||
if err != nil && !IsMissingAnnotations(err) {
|
||||
return nil
|
||||
}
|
||||
if pr != "" && isValidURL(pr) == nil {
|
||||
redirectConfig.URL = pr
|
||||
}
|
||||
// code
|
||||
if prc, err := annotations.ParseIntASAP(permanentRedirectCode); err == nil {
|
||||
if prc < http.StatusMultipleChoices || prc > http.StatusPermanentRedirect {
|
||||
prc = defaultPermanentRedirectCode
|
||||
}
|
||||
redirectConfig.Code = prc
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r redirect) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
redirectConfig := config.Redirect
|
||||
if redirectConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var redirectPolicy *networking.HTTPRedirect
|
||||
if redirectConfig.URL != "" {
|
||||
parseURL, err := url.Parse(redirectConfig.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
redirectPolicy = &networking.HTTPRedirect{
|
||||
Scheme: parseURL.Scheme,
|
||||
Authority: parseURL.Host,
|
||||
Uri: parseURL.Path,
|
||||
RedirectCode: uint32(redirectConfig.Code),
|
||||
}
|
||||
} else if redirectConfig.httpsRedirect {
|
||||
redirectPolicy = &networking.HTTPRedirect{
|
||||
Scheme: "https",
|
||||
// 308 is the default code for ssl redirect
|
||||
RedirectCode: 308,
|
||||
}
|
||||
}
|
||||
|
||||
route.Redirect = redirectPolicy
|
||||
}
|
||||
|
||||
func needRedirectConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(temporalRedirect) ||
|
||||
annotations.HasASAP(permanentRedirect) ||
|
||||
annotations.HasASAP(sslRedirect) ||
|
||||
annotations.HasASAP(forceSSLRedirect) ||
|
||||
annotations.HasASAP(appRoot)
|
||||
}
|
||||
|
||||
func isValidURL(s string) error {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(u.Scheme, "http") {
|
||||
return fmt.Errorf("only http and https are valid protocols (%v)", u.Scheme)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
134
ingress/kube/annotations/retry.go
Normal file
134
ingress/kube/annotations/retry.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
retryCount = "proxy-next-upstream-tries"
|
||||
perRetryTimeout = "proxy-next-upstream-timeout"
|
||||
retryOn = "proxy-next-upstream"
|
||||
|
||||
defaultRetryCount = 3
|
||||
defaultRetryOn = "5xx"
|
||||
retryStatusCode = "retriable-status-codes"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = retry{}
|
||||
_ RouteHandler = retry{}
|
||||
)
|
||||
|
||||
type RetryConfig struct {
|
||||
retryCount int32
|
||||
perRetryTimeout *types.Duration
|
||||
retryOn string
|
||||
}
|
||||
|
||||
type retry struct{}
|
||||
|
||||
func (r retry) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needRetryConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
retryConfig := &RetryConfig{
|
||||
retryCount: defaultRetryCount,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: defaultRetryOn,
|
||||
}
|
||||
defer func() {
|
||||
config.Retry = retryConfig
|
||||
}()
|
||||
|
||||
if count, err := annotations.ParseIntASAP(retryCount); err == nil {
|
||||
retryConfig.retryCount = int32(count)
|
||||
}
|
||||
|
||||
if timeout, err := annotations.ParseIntASAP(perRetryTimeout); err == nil {
|
||||
retryConfig.perRetryTimeout = &types.Duration{
|
||||
Seconds: int64(timeout),
|
||||
}
|
||||
}
|
||||
|
||||
if retryOn, err := annotations.ParseStringASAP(retryOn); err == nil {
|
||||
conditions := toSet(splitBySeparator(retryOn, ","))
|
||||
if len(conditions) > 0 {
|
||||
if conditions.Contains("off") {
|
||||
retryConfig.retryCount = 0
|
||||
} else {
|
||||
var stringBuilder strings.Builder
|
||||
// Convert error, timeout, invalid_header to 5xx
|
||||
if conditions.Contains("error") ||
|
||||
conditions.Contains("timeout") ||
|
||||
conditions.Contains("invalid_header") {
|
||||
stringBuilder.WriteString(defaultRetryOn + ",")
|
||||
}
|
||||
// Just use the raw.
|
||||
if conditions.Contains("non_idempotent") {
|
||||
stringBuilder.WriteString("non_idempotent,")
|
||||
}
|
||||
// Append the status codes.
|
||||
statusCodes := convertStatusCodes(conditions)
|
||||
if len(statusCodes) > 0 {
|
||||
stringBuilder.WriteString(retryStatusCode + ",")
|
||||
for _, code := range statusCodes {
|
||||
stringBuilder.WriteString(code + ",")
|
||||
}
|
||||
}
|
||||
|
||||
retryConfig.retryOn = strings.TrimSuffix(stringBuilder.String(), ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r retry) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
retryConfig := config.Retry
|
||||
if retryConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
route.Retries = &networking.HTTPRetry{
|
||||
Attempts: retryConfig.retryCount,
|
||||
PerTryTimeout: retryConfig.perRetryTimeout,
|
||||
RetryOn: retryConfig.retryOn,
|
||||
}
|
||||
}
|
||||
|
||||
func needRetryConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(retryCount) ||
|
||||
annotations.HasASAP(perRetryTimeout) ||
|
||||
annotations.HasASAP(retryOn)
|
||||
}
|
||||
|
||||
func convertStatusCodes(set sets.Set) []string {
|
||||
var result []string
|
||||
for condition := range set {
|
||||
if strings.HasPrefix(condition, "http_") {
|
||||
result = append(result, strings.TrimPrefix(condition, "http_"))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
147
ingress/kube/annotations/retry_test.go
Normal file
147
ingress/kube/annotations/retry_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestRetryParse(t *testing.T) {
|
||||
retry := retry{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *RetryConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryCount): "1",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 1,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(perRetryTimeout): "10",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 3,
|
||||
perRetryTimeout: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
retryOn: "5xx",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryCount): "2",
|
||||
buildNginxAnnotationKey(retryOn): "off",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 0,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryCount): "2",
|
||||
buildNginxAnnotationKey(retryOn): "error,timeout",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 2,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryOn): "timeout,non_idempotent",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 3,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx,non_idempotent",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryOn): "timeout,http_503,http_502,http_404",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 3,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx,retriable-status-codes,503,502,404",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = retry.Parse(inputCase.input, config, nil)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.Retry) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryApplyRoute(t *testing.T) {
|
||||
retry := retry{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Retry: &RetryConfig{
|
||||
retryCount: 3,
|
||||
retryOn: "test",
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Retries: &networking.HTTPRetry{
|
||||
Attempts: 3,
|
||||
RetryOn: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
retry.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
106
ingress/kube/annotations/rewrite.go
Normal file
106
ingress/kube/annotations/rewrite.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
rewriteTarget = "rewrite-target"
|
||||
useRegex = "use-regex"
|
||||
upstreamVhost = "upstream-vhost"
|
||||
|
||||
re2Regex = "\\$[0-9]"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &rewrite{}
|
||||
_ RouteHandler = &rewrite{}
|
||||
)
|
||||
|
||||
type RewriteConfig struct {
|
||||
RewriteTarget string
|
||||
UseRegex bool
|
||||
RewriteHost string
|
||||
}
|
||||
|
||||
type rewrite struct{}
|
||||
|
||||
func (r rewrite) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needRewriteConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
rewriteConfig := &RewriteConfig{}
|
||||
rewriteConfig.RewriteTarget, _ = annotations.ParseStringASAP(rewriteTarget)
|
||||
rewriteConfig.UseRegex, _ = annotations.ParseBoolASAP(useRegex)
|
||||
rewriteConfig.RewriteHost, _ = annotations.ParseStringASAP(upstreamVhost)
|
||||
|
||||
if rewriteConfig.RewriteTarget != "" {
|
||||
// When rewrite target is present and not empty,
|
||||
// we will enforce regex match on all rules in this ingress.
|
||||
rewriteConfig.UseRegex = true
|
||||
|
||||
// We should convert nginx regex rule to envoy regex rule.
|
||||
rewriteConfig.RewriteTarget = convertToRE2(rewriteConfig.RewriteTarget)
|
||||
}
|
||||
|
||||
config.Rewrite = rewriteConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r rewrite) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
rewriteConfig := config.Rewrite
|
||||
if rewriteConfig == nil || (rewriteConfig.RewriteTarget == "" &&
|
||||
rewriteConfig.RewriteHost == "") {
|
||||
return
|
||||
}
|
||||
|
||||
route.Rewrite = &networking.HTTPRewrite{}
|
||||
if rewriteConfig.RewriteTarget != "" {
|
||||
route.Rewrite.UriRegex = &networking.RegexMatchAndSubstitute{
|
||||
Pattern: route.Match[0].Uri.GetRegex(),
|
||||
Substitution: rewriteConfig.RewriteTarget,
|
||||
}
|
||||
}
|
||||
|
||||
if rewriteConfig.RewriteHost != "" {
|
||||
route.Rewrite.Authority = rewriteConfig.RewriteHost
|
||||
}
|
||||
}
|
||||
|
||||
func convertToRE2(target string) string {
|
||||
if match, err := regexp.MatchString(re2Regex, target); err != nil || !match {
|
||||
return target
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(target, "$", "\\")
|
||||
}
|
||||
|
||||
func NeedRegexMatch(annotations map[string]string) bool {
|
||||
target, _ := Annotations(annotations).ParseStringASAP(rewriteTarget)
|
||||
regex, _ := Annotations(annotations).ParseBoolASAP(useRegex)
|
||||
|
||||
return regex || target != ""
|
||||
}
|
||||
|
||||
func needRewriteConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(rewriteTarget) || annotations.HasASAP(useRegex) ||
|
||||
annotations.HasASAP(upstreamVhost)
|
||||
}
|
||||
254
ingress/kube/annotations/rewrite_test.go
Normal file
254
ingress/kube/annotations/rewrite_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestConvertToRE2(t *testing.T) {
|
||||
useCases := []struct {
|
||||
input string
|
||||
except string
|
||||
}{
|
||||
{
|
||||
input: "/test",
|
||||
except: "/test",
|
||||
},
|
||||
{
|
||||
input: "/test/app",
|
||||
except: "/test/app",
|
||||
},
|
||||
{
|
||||
input: "/$1",
|
||||
except: "/\\1",
|
||||
},
|
||||
{
|
||||
input: "/$2/$1",
|
||||
except: "/\\2/\\1",
|
||||
},
|
||||
{
|
||||
input: "/$test/$a",
|
||||
except: "/$test/$a",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range useCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
if convertToRE2(c.input) != c.except {
|
||||
t.Fatalf("input %s is not equal to except %s.", c.input, c.except)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteParse(t *testing.T) {
|
||||
rewrite := rewrite{}
|
||||
testCases := []struct {
|
||||
input Annotations
|
||||
expect *RewriteConfig
|
||||
}{
|
||||
{
|
||||
input: nil,
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
input: Annotations{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(rewriteTarget): "/test",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
RewriteTarget: "/test",
|
||||
UseRegex: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(rewriteTarget): "",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
UseRegex: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(rewriteTarget): "/$2/$1",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
RewriteTarget: "/\\2/\\1",
|
||||
UseRegex: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(useRegex): "true",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
UseRegex: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(upstreamVhost): "test.com",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
RewriteHost: "test.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(rewriteTarget): "/$2/$1",
|
||||
buildNginxAnnotationKey(upstreamVhost): "test.com",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
RewriteTarget: "/\\2/\\1",
|
||||
UseRegex: true,
|
||||
RewriteHost: "test.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = rewrite.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(config.Rewrite, testCase.expect) {
|
||||
t.Fatalf("Must be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteApplyRoute(t *testing.T) {
|
||||
rewrite := rewrite{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Rewrite: &RewriteConfig{},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
RewriteTarget: "/test",
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "/hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "/hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Rewrite: &networking.HTTPRewrite{
|
||||
UriRegex: &networking.RegexMatchAndSubstitute{
|
||||
Pattern: "/hello",
|
||||
Substitution: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
RewriteHost: "test.com",
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Rewrite: &networking.HTTPRewrite{
|
||||
Authority: "test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
RewriteTarget: "/test",
|
||||
RewriteHost: "test.com",
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "/hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "/hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Rewrite: &networking.HTTPRewrite{
|
||||
UriRegex: &networking.RegexMatchAndSubstitute{
|
||||
Pattern: "/hello",
|
||||
Substitution: "/test",
|
||||
},
|
||||
Authority: "test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
rewrite.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
62
ingress/kube/annotations/timeout.go
Normal file
62
ingress/kube/annotations/timeout.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const timeoutAnnotation = "timeout"
|
||||
|
||||
var (
|
||||
_ Parser = timeout{}
|
||||
_ RouteHandler = timeout{}
|
||||
)
|
||||
|
||||
type TimeoutConfig struct {
|
||||
time *types.Duration
|
||||
}
|
||||
|
||||
type timeout struct{}
|
||||
|
||||
func (t timeout) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needTimeoutConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if time, err := annotations.ParseIntForMSE(timeoutAnnotation); err == nil {
|
||||
config.Timeout = &TimeoutConfig{
|
||||
time: &types.Duration{
|
||||
Seconds: int64(time),
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t timeout) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
timeout := config.Timeout
|
||||
if timeout == nil || timeout.time == nil || timeout.time.Seconds == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
route.Timeout = timeout.time
|
||||
}
|
||||
|
||||
func needTimeoutConfig(annotations Annotations) bool {
|
||||
return annotations.HasMSE(timeoutAnnotation)
|
||||
}
|
||||
121
ingress/kube/annotations/timeout_test.go
Normal file
121
ingress/kube/annotations/timeout_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestTimeoutParse(t *testing.T) {
|
||||
timeout := timeout{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *TimeoutConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + timeoutAnnotation: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + timeoutAnnotation: "0",
|
||||
},
|
||||
expect: &TimeoutConfig{
|
||||
time: &types.Duration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + timeoutAnnotation: "10",
|
||||
},
|
||||
expect: &TimeoutConfig{
|
||||
time: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = timeout.Parse(c.input, config, nil)
|
||||
if !reflect.DeepEqual(c.expect, config.Timeout) {
|
||||
t.Fatalf("Should be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutApplyRoute(t *testing.T) {
|
||||
timeout := timeout{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Timeout: &TimeoutConfig{},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Timeout: &TimeoutConfig{
|
||||
time: &types.Duration{},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Timeout: &TimeoutConfig{
|
||||
time: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Timeout: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
timeout.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
188
ingress/kube/annotations/upstreamtls.go
Normal file
188
ingress/kube/annotations/upstreamtls.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model/credentials"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/util"
|
||||
)
|
||||
|
||||
const (
|
||||
backendProtocol = "backend-protocol"
|
||||
proxySSLSecret = "proxy-ssl-secret"
|
||||
proxySSLVerify = "proxy-ssl-verify"
|
||||
proxySSLName = "proxy-ssl-name"
|
||||
proxySSLServerName = "proxy-ssl-server-name"
|
||||
|
||||
defaultBackendProtocol = "HTTP"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &upstreamTLS{}
|
||||
_ TrafficPolicyHandler = &upstreamTLS{}
|
||||
|
||||
validProtocols = regexp.MustCompile(`^(HTTP|HTTP2|HTTPS|GRPC|GRPCS)$`)
|
||||
|
||||
OnOffRegex = regexp.MustCompile(`^(on|off)$`)
|
||||
)
|
||||
|
||||
type UpstreamTLSConfig struct {
|
||||
BackendProtocol string
|
||||
|
||||
SecretName string
|
||||
SSLVerify bool
|
||||
SNI string
|
||||
EnableSNI bool
|
||||
}
|
||||
|
||||
type upstreamTLS struct{}
|
||||
|
||||
func (u upstreamTLS) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needUpstreamTLSConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
upstreamTLSConfig := &UpstreamTLSConfig{
|
||||
BackendProtocol: defaultBackendProtocol,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
config.UpstreamTLS = upstreamTLSConfig
|
||||
}()
|
||||
|
||||
if proto, err := annotations.ParseStringASAP(backendProtocol); err == nil {
|
||||
proto = strings.TrimSpace(strings.ToUpper(proto))
|
||||
if validProtocols.MatchString(proto) {
|
||||
upstreamTLSConfig.BackendProtocol = proto
|
||||
}
|
||||
}
|
||||
|
||||
secretName, _ := annotations.ParseStringASAP(proxySSLSecret)
|
||||
namespacedName := util.SplitNamespacedName(secretName)
|
||||
if namespacedName.Name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if namespacedName.Namespace == "" {
|
||||
namespacedName.Namespace = config.Namespace
|
||||
}
|
||||
upstreamTLSConfig.SecretName = namespacedName.String()
|
||||
|
||||
if sslVerify, err := annotations.ParseStringASAP(proxySSLVerify); err == nil {
|
||||
if OnOffRegex.MatchString(sslVerify) {
|
||||
upstreamTLSConfig.SSLVerify = onOffToBool(sslVerify)
|
||||
}
|
||||
}
|
||||
|
||||
upstreamTLSConfig.SNI, _ = annotations.ParseStringASAP(proxySSLName)
|
||||
|
||||
if enableSNI, err := annotations.ParseStringASAP(proxySSLServerName); err == nil {
|
||||
if OnOffRegex.MatchString(enableSNI) {
|
||||
upstreamTLSConfig.SSLVerify = onOffToBool(enableSNI)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u upstreamTLS) ApplyTrafficPolicy(trafficPolicy *networking.TrafficPolicy_PortTrafficPolicy, config *Ingress) {
|
||||
if config.UpstreamTLS == nil {
|
||||
return
|
||||
}
|
||||
|
||||
upstreamTLSConfig := config.UpstreamTLS
|
||||
|
||||
if isH2(upstreamTLSConfig.BackendProtocol) {
|
||||
trafficPolicy.ConnectionPool = &networking.ConnectionPoolSettings{
|
||||
Http: &networking.ConnectionPoolSettings_HTTPSettings{
|
||||
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_UPGRADE,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var tls *networking.ClientTLSSettings
|
||||
if upstreamTLSConfig.SecretName != "" {
|
||||
// MTLS
|
||||
tls = processMTLS(config)
|
||||
} else if isHTTPS(upstreamTLSConfig.BackendProtocol) {
|
||||
tls = processSimple(config)
|
||||
}
|
||||
|
||||
trafficPolicy.Tls = tls
|
||||
}
|
||||
|
||||
func processMTLS(config *Ingress) *networking.ClientTLSSettings {
|
||||
namespacedName := util.SplitNamespacedName(config.UpstreamTLS.SecretName)
|
||||
if namespacedName.Name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
tls := &networking.ClientTLSSettings{
|
||||
Mode: networking.ClientTLSSettings_MUTUAL,
|
||||
CredentialName: credentials.ToKubernetesIngressResource(config.RawClusterId, namespacedName.Namespace, namespacedName.Name),
|
||||
}
|
||||
|
||||
if !config.UpstreamTLS.SSLVerify {
|
||||
// This api InsecureSkipVerify hasn't been support yet.
|
||||
// Until this pr https://github.com/istio/istio/pull/35357.
|
||||
tls.InsecureSkipVerify = &types.BoolValue{
|
||||
Value: false,
|
||||
}
|
||||
}
|
||||
|
||||
if config.UpstreamTLS.EnableSNI && config.UpstreamTLS.SNI != "" {
|
||||
tls.Sni = config.UpstreamTLS.SNI
|
||||
}
|
||||
|
||||
return tls
|
||||
}
|
||||
|
||||
func processSimple(config *Ingress) *networking.ClientTLSSettings {
|
||||
tls := &networking.ClientTLSSettings{
|
||||
Mode: networking.ClientTLSSettings_SIMPLE,
|
||||
}
|
||||
|
||||
if config.UpstreamTLS.EnableSNI && config.UpstreamTLS.SNI != "" {
|
||||
tls.Sni = config.UpstreamTLS.SNI
|
||||
}
|
||||
|
||||
return tls
|
||||
}
|
||||
|
||||
func needUpstreamTLSConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(backendProtocol) ||
|
||||
annotations.HasASAP(proxySSLSecret)
|
||||
}
|
||||
|
||||
func onOffToBool(onOff string) bool {
|
||||
return onOff == "on"
|
||||
}
|
||||
|
||||
func isH2(protocol string) bool {
|
||||
return protocol == "HTTP2" ||
|
||||
protocol == "GRPC" ||
|
||||
protocol == "GRPCS"
|
||||
}
|
||||
|
||||
func isHTTPS(protocol string) bool {
|
||||
return protocol == "HTTPS" ||
|
||||
protocol == "GRPCS"
|
||||
}
|
||||
54
ingress/kube/annotations/util.go
Normal file
54
ingress/kube/annotations/util.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/model/credentials"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
)
|
||||
|
||||
func extraSecret(name string) model.NamespacedName {
|
||||
result := model.NamespacedName{}
|
||||
res := strings.TrimPrefix(name, credentials.KubernetesIngressSecretTypeURI)
|
||||
split := strings.Split(res, "/")
|
||||
if len(split) != 3 {
|
||||
return result
|
||||
}
|
||||
|
||||
return model.NamespacedName{
|
||||
Namespace: split[1],
|
||||
Name: split[2],
|
||||
}
|
||||
}
|
||||
|
||||
func splitBySeparator(content, separator string) []string {
|
||||
var result []string
|
||||
parts := strings.Split(content, separator)
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, part)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toSet(slice []string) sets.Set {
|
||||
return sets.NewSet(slice...)
|
||||
}
|
||||
53
ingress/kube/annotations/util_test.go
Normal file
53
ingress/kube/annotations/util_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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 annotations
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
)
|
||||
|
||||
func TestExtraSecret(t *testing.T) {
|
||||
inputCases := []struct {
|
||||
input string
|
||||
expect model.NamespacedName
|
||||
}{
|
||||
{
|
||||
input: "test/test",
|
||||
expect: model.NamespacedName{},
|
||||
},
|
||||
{
|
||||
input: "kubernetes-ingress://test/test",
|
||||
expect: model.NamespacedName{},
|
||||
},
|
||||
{
|
||||
input: "kubernetes-ingress://cluster/foo/bar",
|
||||
expect: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
if !reflect.DeepEqual(inputCase.expect, extraSecret(inputCase.input)) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
133
ingress/kube/common/controller.go
Normal file
133
ingress/kube/common/controller.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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 (
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
gatewaytool "istio.io/istio/pkg/config/gateway"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/annotations"
|
||||
)
|
||||
|
||||
type ServiceKey struct {
|
||||
Namespace string
|
||||
Name string
|
||||
Port int32
|
||||
}
|
||||
|
||||
type WrapperConfig struct {
|
||||
Config *config.Config
|
||||
AnnotationsConfig *annotations.Ingress
|
||||
}
|
||||
|
||||
type WrapperGateway struct {
|
||||
Gateway *networking.Gateway
|
||||
WrapperConfig *WrapperConfig
|
||||
ClusterId string
|
||||
Host string
|
||||
}
|
||||
|
||||
func (w *WrapperGateway) IsHTTPS() bool {
|
||||
if w.Gateway == nil || len(w.Gateway.Servers) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, server := range w.Gateway.Servers {
|
||||
if gatewaytool.IsTLSServer(server) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type WrapperHTTPRoute struct {
|
||||
HTTPRoute *networking.HTTPRoute
|
||||
WrapperConfig *WrapperConfig
|
||||
RawClusterId string
|
||||
ClusterId string
|
||||
ClusterName string
|
||||
Host string
|
||||
OriginPath string
|
||||
OriginPathType PathType
|
||||
WeightTotal int32
|
||||
IsDefaultBackend bool
|
||||
}
|
||||
|
||||
func (w *WrapperHTTPRoute) Meta() string {
|
||||
return strings.Join([]string{w.WrapperConfig.Config.Namespace, w.WrapperConfig.Config.Name}, "-")
|
||||
}
|
||||
|
||||
func (w *WrapperHTTPRoute) BasePathFormat() string {
|
||||
return strings.Join([]string{w.Host, w.OriginPath}, "-")
|
||||
}
|
||||
|
||||
func (w *WrapperHTTPRoute) PathFormat() string {
|
||||
return strings.Join([]string{w.Host, string(w.OriginPathType), w.OriginPath}, "-")
|
||||
}
|
||||
|
||||
type WrapperVirtualService struct {
|
||||
VirtualService *networking.VirtualService
|
||||
WrapperConfig *WrapperConfig
|
||||
ConfiguredDefaultBackend bool
|
||||
AppRoot string
|
||||
}
|
||||
|
||||
type WrapperTrafficPolicy struct {
|
||||
TrafficPolicy *networking.TrafficPolicy_PortTrafficPolicy
|
||||
WrapperConfig *WrapperConfig
|
||||
}
|
||||
|
||||
type WrapperDestinationRule struct {
|
||||
DestinationRule *networking.DestinationRule
|
||||
WrapperConfig *WrapperConfig
|
||||
ServiceKey ServiceKey
|
||||
}
|
||||
|
||||
type IngressController interface {
|
||||
// RegisterEventHandler adds a handler to receive config update events for a
|
||||
// configuration type
|
||||
RegisterEventHandler(kind config.GroupVersionKind, handler model.EventHandler)
|
||||
|
||||
List() []config.Config
|
||||
|
||||
ServiceLister() listerv1.ServiceLister
|
||||
|
||||
SecretLister() listerv1.SecretLister
|
||||
|
||||
ConvertGateway(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
|
||||
|
||||
ConvertHTTPRoute(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
|
||||
|
||||
ApplyDefaultBackend(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
|
||||
|
||||
ApplyCanaryIngress(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
|
||||
|
||||
ConvertTrafficPolicy(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
|
||||
|
||||
// Run until a signal is received
|
||||
Run(stop <-chan struct{})
|
||||
|
||||
SetWatchErrorHandler(func(r *cache.Reflector, err error)) error
|
||||
|
||||
// HasSynced returns true after initial cache synchronization is complete
|
||||
HasSynced() bool
|
||||
}
|
||||
68
ingress/kube/common/metrics.go
Normal file
68
ingress/kube/common/metrics.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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 "istio.io/pkg/monitoring"
|
||||
|
||||
type Event string
|
||||
|
||||
const (
|
||||
Normal Event = "normal"
|
||||
|
||||
Unknown Event = "unknown"
|
||||
|
||||
EmptyRule Event = "empty-rule"
|
||||
|
||||
MissingSecret Event = "missing-secret"
|
||||
|
||||
InvalidBackendService Event = "invalid-backend-service"
|
||||
|
||||
DuplicatedRoute Event = "duplicated-route"
|
||||
|
||||
DuplicatedTls Event = "duplicated-tls"
|
||||
|
||||
PortNameResolveError Event = "port-name-resolve-error"
|
||||
)
|
||||
|
||||
var (
|
||||
clusterTag = monitoring.MustCreateLabel("cluster")
|
||||
invalidType = monitoring.MustCreateLabel("type")
|
||||
|
||||
// totalIngresses tracks the total number of ingress
|
||||
totalIngresses = monitoring.NewGauge(
|
||||
"pilot_total_ingresses",
|
||||
"Total ingresses known to pilot.",
|
||||
monitoring.WithLabels(clusterTag),
|
||||
)
|
||||
|
||||
totalInvalidIngress = monitoring.NewSum(
|
||||
"pilot_total_invalid_ingresses",
|
||||
"Total invalid ingresses known to pilot.",
|
||||
monitoring.WithLabels(clusterTag, invalidType),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
monitoring.MustRegister(totalIngresses)
|
||||
monitoring.MustRegister(totalInvalidIngress)
|
||||
}
|
||||
|
||||
func RecordIngressNumber(cluster string, number int) {
|
||||
totalIngresses.With(clusterTag.Value(cluster)).Record(float64(number))
|
||||
}
|
||||
|
||||
func IncrementInvalidIngress(cluster string, event Event) {
|
||||
totalInvalidIngress.With(clusterTag.Value(cluster), invalidType.Value(string(event))).Increment()
|
||||
}
|
||||
411
ingress/kube/common/model.go
Normal file
411
ingress/kube/common/model.go
Normal file
@@ -0,0 +1,411 @@
|
||||
// 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"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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/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"
|
||||
|
||||
Regex PathType = "regex"
|
||||
|
||||
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.IstioNetworkingV1Alpha3Virtualservices,
|
||||
collections.IstioNetworkingV1Alpha3Gateways,
|
||||
collections.IstioNetworkingV1Alpha3Destinationrules,
|
||||
collections.IstioNetworkingV1Alpha3Envoyfilters,
|
||||
)
|
||||
|
||||
clusterPrefix string
|
||||
SvcLabelSelector labels.Selector
|
||||
)
|
||||
|
||||
func init() {
|
||||
set := labels.Set{
|
||||
AppKey: AppValue,
|
||||
}
|
||||
SvcLabelSelector = labels.SelectorFromSet(set)
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Enable bool
|
||||
ClusterId string
|
||||
IngressClass string
|
||||
WatchNamespace string
|
||||
RawClusterId string
|
||||
EnableStatus bool
|
||||
SystemNamespace string
|
||||
GatewaySelectorKey string
|
||||
GatewaySelectorValue string
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
HostAndPath2Ingress map[string]*config.Config
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// CreateOptions obtain options from cluster id.
|
||||
// The cluster id format is k8sClusterId ingressClass watchNamespace EnableStatus, delimited by _.
|
||||
func CreateOptions(clusterId cluster.ID) Options {
|
||||
parts := strings.Split(clusterId.String(), "_")
|
||||
// Old cluster key
|
||||
if len(parts) < 3 {
|
||||
out := Options{
|
||||
RawClusterId: clusterId.String(),
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
out.ClusterId = parts[0]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
options := Options{
|
||||
Enable: true,
|
||||
ClusterId: parts[0],
|
||||
IngressClass: parts[1],
|
||||
WatchNamespace: parts[2],
|
||||
RawClusterId: clusterId.String(),
|
||||
// The status switch is enabled by default.
|
||||
EnableStatus: true,
|
||||
}
|
||||
|
||||
if len(parts) == 4 {
|
||||
if enable, err := strconv.ParseBool(parts[3]); err == nil {
|
||||
options.EnableStatus = enable
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
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 string
|
||||
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 string
|
||||
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,
|
||||
}
|
||||
}
|
||||
365
ingress/kube/common/tool.go
Normal file
365
ingress/kube/common/tool.go
Normal file
@@ -0,0 +1,365 @@
|
||||
// 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 (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/kube"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
)
|
||||
|
||||
// V1Available check if the "networking/v1" Ingress is available.
|
||||
func V1Available(client kube.Client) bool {
|
||||
// check kubernetes version to use new ingress package or not
|
||||
version119, _ := version.ParseGeneric("v1.19.0")
|
||||
|
||||
serverVersion, err := client.GetKubernetesVersion()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
runningVersion, err := version.ParseGeneric(serverVersion.String())
|
||||
if err != nil {
|
||||
IngressLog.Errorf("unexpected error parsing running Kubernetes version: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return runningVersion.AtLeast(version119)
|
||||
}
|
||||
|
||||
// NetworkingIngressAvailable check if the "networking" group Ingress is available.
|
||||
func NetworkingIngressAvailable(client kube.Client) bool {
|
||||
// check kubernetes version to use new ingress package or not
|
||||
version118, _ := version.ParseGeneric("v1.18.0")
|
||||
|
||||
serverVersion, err := client.GetKubernetesVersion()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
runningVersion, err := version.ParseGeneric(serverVersion.String())
|
||||
if err != nil {
|
||||
IngressLog.Errorf("unexpected error parsing running Kubernetes version: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return runningVersion.AtLeast(version118)
|
||||
}
|
||||
|
||||
// SortIngressByCreationTime sorts the list of config objects in ascending order by their creation time (if available).
|
||||
func SortIngressByCreationTime(configs []config.Config) {
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
if configs[i].CreationTimestamp == configs[j].CreationTimestamp {
|
||||
in := configs[i].Name + "." + configs[i].Namespace
|
||||
jn := configs[j].Name + "." + configs[j].Namespace
|
||||
return in < jn
|
||||
}
|
||||
return configs[i].CreationTimestamp.Before(configs[j].CreationTimestamp)
|
||||
})
|
||||
}
|
||||
|
||||
func CreateOrUpdateAnnotations(annotations map[string]string, options Options) map[string]string {
|
||||
out := make(map[string]string, len(annotations))
|
||||
|
||||
for key, value := range annotations {
|
||||
out[key] = value
|
||||
}
|
||||
|
||||
out[ClusterIdAnnotation] = options.ClusterId
|
||||
out[RawClusterIdAnnotation] = options.RawClusterId
|
||||
return out
|
||||
}
|
||||
|
||||
func GetClusterId(annotations map[string]string) string {
|
||||
if len(annotations) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if value, exist := annotations[ClusterIdAnnotation]; exist {
|
||||
return value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetRawClusterId(annotations map[string]string) string {
|
||||
if len(annotations) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if value, exist := annotations[RawClusterIdAnnotation]; exist {
|
||||
return value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetHost(annotations map[string]string) string {
|
||||
if len(annotations) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if value, exist := annotations[HostAnnotation]; exist {
|
||||
return value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// CleanHost follow the format of mse-ops for host.
|
||||
func CleanHost(host string) string {
|
||||
if host == "*" {
|
||||
return "global"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(host, "*") {
|
||||
host = strings.ReplaceAll(host, "*", "global-")
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(host, ".", "-")
|
||||
}
|
||||
|
||||
func CreateConvertedName(items ...string) string {
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
if items[i] == "" {
|
||||
items = append(items[:i], items[i+1:]...)
|
||||
}
|
||||
}
|
||||
return strings.Join(items, "-")
|
||||
}
|
||||
|
||||
// SortHTTPRoutes sort routes base on path type and path length
|
||||
func SortHTTPRoutes(routes []*WrapperHTTPRoute) {
|
||||
isDefaultBackend := func(route *WrapperHTTPRoute) bool {
|
||||
return route.IsDefaultBackend
|
||||
}
|
||||
|
||||
isAllCatch := func(route *WrapperHTTPRoute) bool {
|
||||
if route.OriginPathType == Prefix && route.OriginPath == "/" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
sort.SliceStable(routes, func(i, j int) bool {
|
||||
// Move default backend to end
|
||||
if isDefaultBackend(routes[i]) {
|
||||
return false
|
||||
}
|
||||
if isDefaultBackend(routes[j]) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Move user specified root path match to end
|
||||
if isAllCatch(routes[i]) {
|
||||
return false
|
||||
}
|
||||
if isAllCatch(routes[j]) {
|
||||
return true
|
||||
}
|
||||
|
||||
if routes[i].OriginPathType == routes[j].OriginPathType {
|
||||
return len(routes[i].OriginPath) > len(routes[j].OriginPath)
|
||||
}
|
||||
|
||||
if routes[i].OriginPathType == Exact {
|
||||
return true
|
||||
}
|
||||
|
||||
if routes[i].OriginPathType != Exact &&
|
||||
routes[j].OriginPathType != Exact {
|
||||
return routes[i].OriginPathType == Prefix
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func constructRouteName(route *WrapperHTTPRoute) string {
|
||||
var builder strings.Builder
|
||||
// host-pathType-path
|
||||
base := route.PathFormat()
|
||||
builder.WriteString(base)
|
||||
|
||||
var mappings []string
|
||||
var headerMappings []string
|
||||
var queryMappings []string
|
||||
if len(route.HTTPRoute.Match) > 0 {
|
||||
match := route.HTTPRoute.Match[0]
|
||||
if len(match.Headers) != 0 {
|
||||
for k, v := range match.Headers {
|
||||
var mapping string
|
||||
switch v.GetMatchType().(type) {
|
||||
case *networking.StringMatch_Exact:
|
||||
mapping = CreateConvertedName("exact", k, v.GetExact())
|
||||
case *networking.StringMatch_Prefix:
|
||||
mapping = CreateConvertedName("prefix", k, v.GetPrefix())
|
||||
case *networking.StringMatch_Regex:
|
||||
mapping = CreateConvertedName("regex", k, v.GetRegex())
|
||||
}
|
||||
|
||||
headerMappings = append(headerMappings, mapping)
|
||||
}
|
||||
|
||||
sort.SliceStable(headerMappings, func(i, j int) bool {
|
||||
return headerMappings[i] < headerMappings[j]
|
||||
})
|
||||
}
|
||||
|
||||
if len(match.QueryParams) != 0 {
|
||||
for k, v := range match.QueryParams {
|
||||
var mapping string
|
||||
switch v.GetMatchType().(type) {
|
||||
case *networking.StringMatch_Exact:
|
||||
mapping = strings.Join([]string{"exact", k, v.GetExact()}, ":")
|
||||
case *networking.StringMatch_Prefix:
|
||||
mapping = strings.Join([]string{"prefix", k, v.GetPrefix()}, ":")
|
||||
case *networking.StringMatch_Regex:
|
||||
mapping = strings.Join([]string{"regex", k, v.GetRegex()}, ":")
|
||||
}
|
||||
|
||||
queryMappings = append(queryMappings, mapping)
|
||||
}
|
||||
|
||||
sort.SliceStable(queryMappings, func(i, j int) bool {
|
||||
return queryMappings[i] < queryMappings[j]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mappings = append(mappings, headerMappings...)
|
||||
mappings = append(mappings, queryMappings...)
|
||||
|
||||
if len(mappings) == 0 {
|
||||
return CreateConvertedName(base)
|
||||
}
|
||||
|
||||
return CreateConvertedName(base, CreateConvertedName(mappings...))
|
||||
}
|
||||
|
||||
func partMd5(raw string) string {
|
||||
hash := md5.Sum([]byte(raw))
|
||||
encoded := hex.EncodeToString(hash[:])
|
||||
return encoded[:4] + encoded[len(encoded)-4:]
|
||||
}
|
||||
|
||||
func GenerateUniqueRouteName(route *WrapperHTTPRoute) string {
|
||||
raw := constructRouteName(route)
|
||||
|
||||
// meta-part-clusterId
|
||||
// meta: ingressNamespace-ingressName
|
||||
meta := route.Meta()
|
||||
// host-pathType-path-header-queryParam, md5, then before 4 char and end 4 char
|
||||
part := partMd5(raw)
|
||||
routeName := CreateConvertedName(meta, part, route.ClusterId)
|
||||
|
||||
if route.WrapperConfig.AnnotationsConfig.IsCanary() {
|
||||
return routeName + "-canary"
|
||||
}
|
||||
|
||||
return routeName
|
||||
}
|
||||
|
||||
func GenerateUniqueRouteNameWithSuffix(route *WrapperHTTPRoute, suffix string) string {
|
||||
raw := constructRouteName(route)
|
||||
|
||||
// meta-part-clusterId
|
||||
// meta: ingressNamespace-ingressName
|
||||
meta := route.Meta()
|
||||
// host-pathType-path-header-queryParam, md5, then before 4 char and end 4 char
|
||||
part := partMd5(raw)
|
||||
return CreateConvertedName(meta, part, route.ClusterId, suffix)
|
||||
}
|
||||
|
||||
func SplitServiceFQDN(fqdn string) (string, string, bool) {
|
||||
parts := strings.Split(fqdn, ".")
|
||||
if len(parts) > 1 {
|
||||
return parts[0], parts[1], true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func ConvertBackendService(routeDestination *networking.HTTPRouteDestination) model.BackendService {
|
||||
parts := strings.Split(routeDestination.Destination.Host, ".")
|
||||
return model.BackendService{
|
||||
Namespace: parts[1],
|
||||
Name: parts[0],
|
||||
Port: routeDestination.Destination.Port.Number,
|
||||
Weight: routeDestination.Weight,
|
||||
}
|
||||
}
|
||||
|
||||
func getLoadBalancerIp(svc *v1.Service) []string {
|
||||
var out []string
|
||||
|
||||
for _, ingress := range svc.Status.LoadBalancer.Ingress {
|
||||
if ingress.IP != "" {
|
||||
out = append(out, ingress.IP)
|
||||
}
|
||||
|
||||
if ingress.Hostname != "" {
|
||||
hostName := strings.TrimSuffix(ingress.Hostname, SvcHostNameSuffix)
|
||||
if net.ParseIP(hostName) != nil {
|
||||
out = append(out, hostName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func getSvcIpList(svcList []*v1.Service) []string {
|
||||
var targetSvcList []*v1.Service
|
||||
for _, svc := range svcList {
|
||||
if svc.Spec.Type == v1.ServiceTypeLoadBalancer &&
|
||||
strings.HasPrefix(svc.Name, clusterPrefix) {
|
||||
targetSvcList = append(targetSvcList, svc)
|
||||
}
|
||||
}
|
||||
|
||||
var out []string
|
||||
for _, svc := range targetSvcList {
|
||||
out = append(out, getLoadBalancerIp(svc)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func SortLbIngressList(lbi []v1.LoadBalancerIngress) func(int, int) bool {
|
||||
return func(i int, j int) bool {
|
||||
return lbi[i].IP < lbi[j].IP
|
||||
}
|
||||
}
|
||||
|
||||
func GetLbStatusList(svcList []*v1.Service) []v1.LoadBalancerIngress {
|
||||
svcIpList := getSvcIpList(svcList)
|
||||
lbi := make([]v1.LoadBalancerIngress, 0, len(svcIpList))
|
||||
for _, ep := range svcIpList {
|
||||
lbi = append(lbi, v1.LoadBalancerIngress{IP: ep})
|
||||
}
|
||||
|
||||
sort.SliceStable(lbi, SortLbIngressList(lbi))
|
||||
return lbi
|
||||
}
|
||||
473
ingress/kube/common/tool_test.go
Normal file
473
ingress/kube/common/tool_test.go
Normal file
@@ -0,0 +1,473 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/annotations"
|
||||
)
|
||||
|
||||
func TestConstructRouteName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *WrapperHTTPRoute
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
input: &WrapperHTTPRoute{
|
||||
Host: "test.com",
|
||||
OriginPathType: Exact,
|
||||
OriginPath: "/test",
|
||||
HTTPRoute: &networking.HTTPRoute{},
|
||||
},
|
||||
expect: "test.com-exact-/test",
|
||||
},
|
||||
{
|
||||
input: &WrapperHTTPRoute{
|
||||
Host: "*.test.com",
|
||||
OriginPathType: Regex,
|
||||
OriginPath: "/test/(.*)/?[0-9]",
|
||||
HTTPRoute: &networking.HTTPRoute{},
|
||||
},
|
||||
expect: "*.test.com-regex-/test/(.*)/?[0-9]",
|
||||
},
|
||||
{
|
||||
input: &WrapperHTTPRoute{
|
||||
Host: "test.com",
|
||||
OriginPathType: Exact,
|
||||
OriginPath: "/test",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Headers: map[string]*networking.StringMatch{
|
||||
"b": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "a?c.*",
|
||||
},
|
||||
},
|
||||
"a": {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: "test.com-exact-/test-exact-a-hello-regex-b-a?c.*",
|
||||
},
|
||||
{
|
||||
input: &WrapperHTTPRoute{
|
||||
Host: "test.com",
|
||||
OriginPathType: Prefix,
|
||||
OriginPath: "/test",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
QueryParams: map[string]*networking.StringMatch{
|
||||
"b": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "a?c.*",
|
||||
},
|
||||
},
|
||||
"a": {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: "test.com-prefix-/test-exact:a:hello-regex:b:a?c.*",
|
||||
},
|
||||
{
|
||||
input: &WrapperHTTPRoute{
|
||||
Host: "test.com",
|
||||
OriginPathType: Prefix,
|
||||
OriginPath: "/test",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Headers: map[string]*networking.StringMatch{
|
||||
"f": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "abc?",
|
||||
},
|
||||
},
|
||||
"e": {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "bye",
|
||||
},
|
||||
},
|
||||
},
|
||||
QueryParams: map[string]*networking.StringMatch{
|
||||
"b": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "a?c.*",
|
||||
},
|
||||
},
|
||||
"a": {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: "test.com-prefix-/test-exact-e-bye-regex-f-abc?-exact:a:hello-regex:b:a?c.*",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
out := constructRouteName(c.input)
|
||||
if out != c.expect {
|
||||
t.Fatalf("Expect %s, but is %s", c.expect, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateUniqueRouteName(t *testing.T) {
|
||||
inputWithoutCanary := &WrapperHTTPRoute{
|
||||
WrapperConfig: &WrapperConfig{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
Host: "test.com",
|
||||
OriginPathType: Prefix,
|
||||
OriginPath: "/test",
|
||||
ClusterId: "cluster1",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Headers: map[string]*networking.StringMatch{
|
||||
"f": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "abc?",
|
||||
},
|
||||
},
|
||||
"e": {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "bye",
|
||||
},
|
||||
},
|
||||
},
|
||||
QueryParams: map[string]*networking.StringMatch{
|
||||
"b": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "a?c.*",
|
||||
},
|
||||
},
|
||||
"a": {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
withoutCanary := GenerateUniqueRouteName(inputWithoutCanary)
|
||||
t.Log(withoutCanary)
|
||||
|
||||
inputWithCanary := &WrapperHTTPRoute{
|
||||
WrapperConfig: &WrapperConfig{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{
|
||||
Canary: &annotations.CanaryConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Host: "test.com",
|
||||
OriginPathType: Prefix,
|
||||
OriginPath: "/test",
|
||||
ClusterId: "cluster1",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Headers: map[string]*networking.StringMatch{
|
||||
"f": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "abc?",
|
||||
},
|
||||
},
|
||||
"e": {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "bye",
|
||||
},
|
||||
},
|
||||
},
|
||||
QueryParams: map[string]*networking.StringMatch{
|
||||
"b": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "a?c.*",
|
||||
},
|
||||
},
|
||||
"a": {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
withCanary := GenerateUniqueRouteName(inputWithCanary)
|
||||
t.Log(withCanary)
|
||||
|
||||
if withCanary != withoutCanary+"-canary" {
|
||||
t.Fatalf("Expect %s, but actual is %s", withCanary, withoutCanary+"-canary")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLbStatusList(t *testing.T) {
|
||||
clusterPrefix = "gw-123-"
|
||||
svcName := clusterPrefix
|
||||
svcList := []*v1.Service{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "2.2.2.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: "1.1.1.1" + SvcHostNameSuffix,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: "4.4.4.4" + SvcHostNameSuffix,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "3.3.3.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "5.5.5.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lbiList := GetLbStatusList(svcList)
|
||||
if len(lbiList) != 4 {
|
||||
t.Fatal("len should be 4")
|
||||
}
|
||||
|
||||
if lbiList[0].IP != "1.1.1.1" {
|
||||
t.Fatal("should be 1.1.1.1")
|
||||
}
|
||||
|
||||
if lbiList[3].IP != "4.4.4.4" {
|
||||
t.Fatal("should be 4.4.4.4")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortRoutes(t *testing.T) {
|
||||
input := []*WrapperHTTPRoute{
|
||||
{
|
||||
WrapperConfig: &WrapperConfig{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
Host: "test.com",
|
||||
OriginPathType: Prefix,
|
||||
OriginPath: "/",
|
||||
ClusterId: "cluster1",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Name: "test-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
WrapperConfig: &WrapperConfig{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
Host: "test.com",
|
||||
OriginPathType: Prefix,
|
||||
OriginPath: "/a",
|
||||
ClusterId: "cluster1",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Name: "test-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
WrapperConfig: &WrapperConfig{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Name: "test-3",
|
||||
},
|
||||
IsDefaultBackend: true,
|
||||
},
|
||||
{
|
||||
WrapperConfig: &WrapperConfig{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
Host: "test.com",
|
||||
OriginPathType: Exact,
|
||||
OriginPath: "/b",
|
||||
ClusterId: "cluster1",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Name: "test-4",
|
||||
},
|
||||
},
|
||||
{
|
||||
WrapperConfig: &WrapperConfig{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
Host: "test.com",
|
||||
OriginPathType: Regex,
|
||||
OriginPath: "/d(.*)",
|
||||
ClusterId: "cluster1",
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Name: "test-5",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
SortHTTPRoutes(input)
|
||||
if (input[0].HTTPRoute.Name) != "test-4" {
|
||||
t.Fatal("should be test-4")
|
||||
}
|
||||
if (input[1].HTTPRoute.Name) != "test-2" {
|
||||
t.Fatal("should be test-2")
|
||||
}
|
||||
if (input[2].HTTPRoute.Name) != "test-5" {
|
||||
t.Fatal("should be test-5")
|
||||
}
|
||||
if (input[3].HTTPRoute.Name) != "test-1" {
|
||||
t.Fatal("should be test-1")
|
||||
}
|
||||
if (input[4].HTTPRoute.Name) != "test-3" {
|
||||
t.Fatal("should be test-3")
|
||||
}
|
||||
}
|
||||
1159
ingress/kube/ingress/controller.go
Normal file
1159
ingress/kube/ingress/controller.go
Normal file
File diff suppressed because it is too large
Load Diff
78
ingress/kube/ingress/controller_test.go
Normal file
78
ingress/kube/ingress/controller_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 ingress
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/networking/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/common"
|
||||
)
|
||||
|
||||
func TestShouldProcessIngressUpdate(t *testing.T) {
|
||||
c := controller{
|
||||
options: common.Options{
|
||||
IngressClass: "mse",
|
||||
},
|
||||
ingresses: make(map[string]*v1beta1.Ingress),
|
||||
}
|
||||
|
||||
ingressClass := "mse"
|
||||
|
||||
ingress1 := &v1beta1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-1",
|
||||
},
|
||||
Spec: v1beta1.IngressSpec{
|
||||
IngressClassName: &ingressClass,
|
||||
Rules: []v1beta1.IngressRule{
|
||||
{
|
||||
Host: "test.com",
|
||||
IngressRuleValue: v1beta1.IngressRuleValue{
|
||||
HTTP: &v1beta1.HTTPIngressRuleValue{
|
||||
Paths: []v1beta1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
should, _ := c.shouldProcessIngressUpdate(ingress1)
|
||||
if !should {
|
||||
t.Fatal("should be true")
|
||||
}
|
||||
|
||||
ingress2 := *ingress1
|
||||
should, _ = c.shouldProcessIngressUpdate(&ingress2)
|
||||
if should {
|
||||
t.Fatal("should be false")
|
||||
}
|
||||
|
||||
ingress3 := *ingress1
|
||||
ingress3.Annotations = map[string]string{
|
||||
"test": "true",
|
||||
}
|
||||
should, _ = c.shouldProcessIngressUpdate(&ingress3)
|
||||
if !should {
|
||||
t.Fatal("should be true")
|
||||
}
|
||||
}
|
||||
133
ingress/kube/ingress/status.go
Normal file
133
ingress/kube/ingress/status.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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 ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
kubelib "istio.io/istio/pkg/kube"
|
||||
coreV1 "k8s.io/api/core/v1"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
ingresslister "k8s.io/client-go/listers/networking/v1beta1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/common"
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
)
|
||||
|
||||
// statusSyncer keeps the status IP in each Ingress resource updated
|
||||
type statusSyncer struct {
|
||||
client kubernetes.Interface
|
||||
controller *controller
|
||||
|
||||
watchedNamespace string
|
||||
|
||||
ingressLister ingresslister.IngressLister
|
||||
ingressClassLister ingresslister.IngressClassLister
|
||||
// search service in the mse vpc
|
||||
serviceLister listerv1.ServiceLister
|
||||
}
|
||||
|
||||
// newStatusSyncer creates a new instance
|
||||
func newStatusSyncer(localKubeClient, client kubelib.Client, controller *controller, namespace string) *statusSyncer {
|
||||
return &statusSyncer{
|
||||
client: client,
|
||||
controller: controller,
|
||||
watchedNamespace: namespace,
|
||||
ingressLister: client.KubeInformer().Networking().V1beta1().Ingresses().Lister(),
|
||||
ingressClassLister: client.KubeInformer().Networking().V1beta1().IngressClasses().Lister(),
|
||||
// search service in the mse vpc
|
||||
serviceLister: localKubeClient.KubeInformer().Core().V1().Services().Lister(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statusSyncer) run(stopCh <-chan struct{}) {
|
||||
cache.WaitForCacheSync(stopCh, s.controller.HasSynced)
|
||||
|
||||
ticker := time.NewTicker(common.DefaultStatusUpdateInterval)
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := s.runUpdateStatus(); err != nil {
|
||||
IngressLog.Errorf("update status task fail, err %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statusSyncer) runUpdateStatus() error {
|
||||
svcList, err := s.serviceLister.Services(s.watchedNamespace).List(common.SvcLabelSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
IngressLog.Debugf("found number %d of svc", len(svcList))
|
||||
|
||||
lbStatusList := common.GetLbStatusList(svcList)
|
||||
if len(lbStatusList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.updateStatus(lbStatusList)
|
||||
}
|
||||
|
||||
// updateStatus updates ingress status with the list of IP
|
||||
func (s *statusSyncer) updateStatus(status []coreV1.LoadBalancerIngress) error {
|
||||
ingressList, err := s.ingressLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ingress := range ingressList {
|
||||
shouldTarget, err := s.controller.shouldProcessIngress(ingress)
|
||||
if err != nil {
|
||||
IngressLog.Warnf("error determining whether should target ingress %s/%s within cluster %s for status update: %v",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !shouldTarget {
|
||||
continue
|
||||
}
|
||||
|
||||
curIPs := ingress.Status.LoadBalancer.Ingress
|
||||
sort.SliceStable(curIPs, common.SortLbIngressList(curIPs))
|
||||
|
||||
if reflect.DeepEqual(status, curIPs) {
|
||||
IngressLog.Debugf("skipping update of Ingress %v/%v within cluster %s (no change)",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId)
|
||||
continue
|
||||
}
|
||||
|
||||
ingress.Status.LoadBalancer.Ingress = status
|
||||
IngressLog.Infof("Update Ingress %v/%v within cluster %s status",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId)
|
||||
_, err = s.client.NetworkingV1beta1().Ingresses(ingress.Namespace).UpdateStatus(context.TODO(), ingress, metaV1.UpdateOptions{})
|
||||
if err != nil {
|
||||
IngressLog.Warnf("error updating ingress %s/%s within cluster %s status: %v",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1158
ingress/kube/ingressv1/controller.go
Normal file
1158
ingress/kube/ingressv1/controller.go
Normal file
File diff suppressed because it is too large
Load Diff
78
ingress/kube/ingressv1/controller_test.go
Normal file
78
ingress/kube/ingressv1/controller_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 ingressv1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/common"
|
||||
)
|
||||
|
||||
func TestShouldProcessIngressUpdate(t *testing.T) {
|
||||
c := controller{
|
||||
options: common.Options{
|
||||
IngressClass: "mse",
|
||||
},
|
||||
ingresses: make(map[string]*v1.Ingress),
|
||||
}
|
||||
|
||||
ingressClass := "mse"
|
||||
|
||||
ingress1 := &v1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-1",
|
||||
},
|
||||
Spec: v1.IngressSpec{
|
||||
IngressClassName: &ingressClass,
|
||||
Rules: []v1.IngressRule{
|
||||
{
|
||||
Host: "test.com",
|
||||
IngressRuleValue: v1.IngressRuleValue{
|
||||
HTTP: &v1.HTTPIngressRuleValue{
|
||||
Paths: []v1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
should, _ := c.shouldProcessIngressUpdate(ingress1)
|
||||
if !should {
|
||||
t.Fatal("should be true")
|
||||
}
|
||||
|
||||
ingress2 := *ingress1
|
||||
should, _ = c.shouldProcessIngressUpdate(&ingress2)
|
||||
if should {
|
||||
t.Fatal("should be false")
|
||||
}
|
||||
|
||||
ingress3 := *ingress1
|
||||
ingress3.Annotations = map[string]string{
|
||||
"test": "true",
|
||||
}
|
||||
should, _ = c.shouldProcessIngressUpdate(&ingress3)
|
||||
if !should {
|
||||
t.Fatal("should be true")
|
||||
}
|
||||
}
|
||||
133
ingress/kube/ingressv1/status.go
Normal file
133
ingress/kube/ingressv1/status.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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 ingressv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
kubelib "istio.io/istio/pkg/kube"
|
||||
coreV1 "k8s.io/api/core/v1"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
ingresslister "k8s.io/client-go/listers/networking/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/common"
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
)
|
||||
|
||||
// statusSyncer keeps the status IP in each Ingress resource updated
|
||||
type statusSyncer struct {
|
||||
client kubernetes.Interface
|
||||
controller *controller
|
||||
|
||||
watchedNamespace string
|
||||
|
||||
ingressLister ingresslister.IngressLister
|
||||
ingressClassLister ingresslister.IngressClassLister
|
||||
// search service in the mse vpc
|
||||
serviceLister listerv1.ServiceLister
|
||||
}
|
||||
|
||||
// newStatusSyncer creates a new instance
|
||||
func newStatusSyncer(localKubeClient, client kubelib.Client, controller *controller, namespace string) *statusSyncer {
|
||||
return &statusSyncer{
|
||||
client: client,
|
||||
controller: controller,
|
||||
watchedNamespace: namespace,
|
||||
ingressLister: client.KubeInformer().Networking().V1().Ingresses().Lister(),
|
||||
ingressClassLister: client.KubeInformer().Networking().V1().IngressClasses().Lister(),
|
||||
// search service in the mse vpc
|
||||
serviceLister: localKubeClient.KubeInformer().Core().V1().Services().Lister(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statusSyncer) run(stopCh <-chan struct{}) {
|
||||
cache.WaitForCacheSync(stopCh, s.controller.HasSynced)
|
||||
|
||||
ticker := time.NewTicker(common.DefaultStatusUpdateInterval)
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := s.runUpdateStatus(); err != nil {
|
||||
IngressLog.Errorf("update status task fail, err %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statusSyncer) runUpdateStatus() error {
|
||||
svcList, err := s.serviceLister.Services(s.watchedNamespace).List(common.SvcLabelSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
IngressLog.Debugf("found number %d of svc", len(svcList))
|
||||
|
||||
lbStatusList := common.GetLbStatusList(svcList)
|
||||
if len(lbStatusList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.updateStatus(lbStatusList)
|
||||
}
|
||||
|
||||
// updateStatus updates ingress status with the list of IP
|
||||
func (s *statusSyncer) updateStatus(status []coreV1.LoadBalancerIngress) error {
|
||||
ingressList, err := s.ingressLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ingress := range ingressList {
|
||||
shouldTarget, err := s.controller.shouldProcessIngress(ingress)
|
||||
if err != nil {
|
||||
IngressLog.Warnf("error determining whether should target ingress %s/%s within cluster %s for status update: %v",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !shouldTarget {
|
||||
continue
|
||||
}
|
||||
|
||||
curIPs := ingress.Status.LoadBalancer.Ingress
|
||||
sort.SliceStable(curIPs, common.SortLbIngressList(curIPs))
|
||||
|
||||
if reflect.DeepEqual(status, curIPs) {
|
||||
IngressLog.Debugf("skipping update of Ingress %v/%v within cluster %s (no change)",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId)
|
||||
continue
|
||||
}
|
||||
|
||||
ingress.Status.LoadBalancer.Ingress = status
|
||||
IngressLog.Infof("Update Ingress %v/%v within cluster %s status",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId)
|
||||
_, err = s.client.NetworkingV1().Ingresses(ingress.Namespace).UpdateStatus(context.TODO(), ingress, metaV1.UpdateOptions{})
|
||||
if err != nil {
|
||||
IngressLog.Warnf("error updating ingress %s/%s within cluster %s status: %v",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
148
ingress/kube/secret/kube/controller.go
Normal file
148
ingress/kube/secret/kube/controller.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// 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 kube
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
kubeclient "istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
informersv1 "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/common"
|
||||
"github.com/alibaba/higress/ingress/kube/secret"
|
||||
"github.com/alibaba/higress/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/ingress/log"
|
||||
)
|
||||
|
||||
var _ secret.Controller = &controller{}
|
||||
|
||||
type controller struct {
|
||||
queue workqueue.RateLimitingInterface
|
||||
informer cache.SharedIndexInformer
|
||||
lister listersv1.SecretLister
|
||||
handler func(util.ClusterNamespacedName)
|
||||
clusterId string
|
||||
}
|
||||
|
||||
// NewController is copied from NewCredentialsController.
|
||||
func NewController(client kubeclient.Client, options common.Options) secret.Controller {
|
||||
q := workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter())
|
||||
|
||||
informer := client.KubeInformer().InformerFor(&v1.Secret{}, func(k kubernetes.Interface, resync time.Duration) cache.SharedIndexInformer {
|
||||
return informersv1.NewFilteredSecretInformer(
|
||||
k, metav1.NamespaceAll, resync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||
func(options *metav1.ListOptions) {
|
||||
options.FieldSelector = fields.AndSelectors(
|
||||
fields.OneTermNotEqualSelector("type", "helm.sh/release.v1"),
|
||||
fields.OneTermNotEqualSelector("type", string(v1.SecretTypeServiceAccountToken)),
|
||||
).String()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
handler := controllers.LatestVersionHandlerFuncs(controllers.EnqueueForSelf(q))
|
||||
informer.AddEventHandler(handler)
|
||||
|
||||
return &controller{
|
||||
queue: q,
|
||||
informer: informer,
|
||||
lister: listersv1.NewSecretLister(informer.GetIndexer()),
|
||||
clusterId: options.ClusterId,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) Lister() listersv1.SecretLister {
|
||||
return c.lister
|
||||
}
|
||||
|
||||
func (c *controller) Informer() cache.SharedIndexInformer {
|
||||
return c.informer
|
||||
}
|
||||
|
||||
func (c *controller) AddEventHandler(f func(util.ClusterNamespacedName)) {
|
||||
c.handler = f
|
||||
}
|
||||
|
||||
func (c *controller) Run(stop <-chan struct{}) {
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.queue.ShutDown()
|
||||
|
||||
if !cache.WaitForCacheSync(stop, c.HasSynced) {
|
||||
IngressLog.Errorf("Failed to sync secret controller cache")
|
||||
return
|
||||
}
|
||||
go wait.Until(c.worker, time.Second, stop)
|
||||
<-stop
|
||||
}
|
||||
|
||||
func (c *controller) worker() {
|
||||
for c.processNextWorkItem() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) processNextWorkItem() bool {
|
||||
key, quit := c.queue.Get()
|
||||
if quit {
|
||||
return false
|
||||
}
|
||||
defer c.queue.Done(key)
|
||||
ingressNamespacedName := key.(types.NamespacedName)
|
||||
IngressLog.Debugf("secret %s push to queue", ingressNamespacedName)
|
||||
if err := c.onEvent(ingressNamespacedName); err != nil {
|
||||
IngressLog.Errorf("error processing secret item (%v) (retrying): %v", key, err)
|
||||
c.queue.AddRateLimited(key)
|
||||
} else {
|
||||
c.queue.Forget(key)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
_, err := c.lister.Secrets(namespacedName.Namespace).Get(namespacedName.Name)
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// We only care about add or update event.
|
||||
c.handler(util.ClusterNamespacedName{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: namespacedName.Namespace,
|
||||
Name: namespacedName.Name,
|
||||
},
|
||||
ClusterId: c.clusterId,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) HasSynced() bool {
|
||||
return c.informer.HasSynced()
|
||||
}
|
||||
34
ingress/kube/secret/model.go
Normal file
34
ingress/kube/secret/model.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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 secret
|
||||
|
||||
import (
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/ingress/kube/util"
|
||||
)
|
||||
|
||||
type Controller interface {
|
||||
AddEventHandler(func(util.ClusterNamespacedName))
|
||||
|
||||
Run(stop <-chan struct{})
|
||||
|
||||
HasSynced() bool
|
||||
|
||||
Lister() listerv1.SecretLister
|
||||
|
||||
Informer() cache.SharedIndexInformer
|
||||
}
|
||||
84
ingress/kube/util/util.go
Normal file
84
ingress/kube/util/util.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
)
|
||||
|
||||
const DefaultDomainSuffix = "cluster.local"
|
||||
|
||||
type ClusterNamespacedName struct {
|
||||
model.NamespacedName
|
||||
ClusterId string
|
||||
}
|
||||
|
||||
func (c ClusterNamespacedName) String() string {
|
||||
return c.ClusterId + "/" + c.NamespacedName.String()
|
||||
}
|
||||
|
||||
func SplitNamespacedName(name string) model.NamespacedName {
|
||||
nsName := strings.Split(name, "/")
|
||||
if len(nsName) == 2 {
|
||||
return model.NamespacedName{
|
||||
Namespace: nsName[0],
|
||||
Name: nsName[1],
|
||||
}
|
||||
}
|
||||
|
||||
return model.NamespacedName{
|
||||
Name: nsName[0],
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDestinationRuleName create the same format of DR name with ops.
|
||||
func CreateDestinationRuleName(istioCluster, namespace, name string) string {
|
||||
format := path.Join(istioCluster, namespace, name)
|
||||
hash := md5.Sum([]byte(format))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func MessageToGoGoStruct(msg proto.Message) (*types.Struct, error) {
|
||||
if msg == nil {
|
||||
return nil, errors.New("nil message")
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := (&jsonpb.Marshaler{OrigName: true}).Marshal(buf, msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pbs := &types.Struct{}
|
||||
if err := jsonpb.Unmarshal(buf, pbs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pbs, nil
|
||||
}
|
||||
|
||||
func CreateServiceFQDN(namespace, name string) string {
|
||||
return fmt.Sprintf("%s.%s.svc.%s", name, namespace, DefaultDomainSuffix)
|
||||
}
|
||||
108
ingress/kube/util/util_test.go
Normal file
108
ingress/kube/util/util_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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 util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
wasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
|
||||
v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
|
||||
any "google.golang.org/protobuf/types/known/anypb"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
)
|
||||
|
||||
func TestSplitNamespacedName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expect model.NamespacedName
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
},
|
||||
{
|
||||
input: "a/",
|
||||
expect: model.NamespacedName{
|
||||
Namespace: "a",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "a/b",
|
||||
expect: model.NamespacedName{
|
||||
Namespace: "a",
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "/b",
|
||||
expect: model.NamespacedName{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "b",
|
||||
expect: model.NamespacedName{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
result := SplitNamespacedName(testCase.input)
|
||||
if result != testCase.expect {
|
||||
t.Fatalf("expect is %v, but actual is %v", testCase.expect, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDestinationRuleName(t *testing.T) {
|
||||
istioCluster := "gw-123-istio"
|
||||
namespace := "default"
|
||||
serviceName := "go-httpbin-v1"
|
||||
t.Log(CreateDestinationRuleName(istioCluster, namespace, serviceName))
|
||||
}
|
||||
|
||||
func TestMessageToGoGoStruct(t *testing.T) {
|
||||
bytes := []byte("test")
|
||||
wasm := &wasm.Wasm{
|
||||
Config: &v3.PluginConfig{
|
||||
Name: "basic-auth",
|
||||
FailOpen: true,
|
||||
Vm: &v3.PluginConfig_VmConfig{
|
||||
VmConfig: &v3.VmConfig{
|
||||
Runtime: "envoy.wasm.runtime.null",
|
||||
Code: &corev3.AsyncDataSource{
|
||||
Specifier: &corev3.AsyncDataSource_Local{
|
||||
Local: &corev3.DataSource{
|
||||
Specifier: &corev3.DataSource_InlineString{
|
||||
InlineString: "envoy.wasm.basic_auth",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Configuration: &any.Any{
|
||||
TypeUrl: "type.googleapis.com/google.protobuf.StringValue",
|
||||
Value: bytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gogoStruct, _ := MessageToGoGoStruct(wasm)
|
||||
t.Log(gogoStruct)
|
||||
}
|
||||
19
ingress/log/log.go
Normal file
19
ingress/log/log.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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 log
|
||||
|
||||
import "istio.io/pkg/log"
|
||||
|
||||
var IngressLog = log.RegisterScope("ingress", "Higress Ingress process.", 0)
|
||||
223
ingress/mcp/generator.go
Normal file
223
ingress/mcp/generator.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// 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 mcp
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/golang/protobuf/ptypes/any"
|
||||
extensions "istio.io/api/extensions/v1alpha1"
|
||||
mcp "istio.io/api/mcp/v1alpha1"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/xds"
|
||||
)
|
||||
|
||||
type VirtualServiceGenerator struct {
|
||||
Server *xds.DiscoveryServer
|
||||
}
|
||||
|
||||
func (c VirtualServiceGenerator) Generate(proxy *model.Proxy, push *model.PushContext, w *model.WatchedResource,
|
||||
updates *model.PushRequest) ([]*any.Any, model.XdsLogDetails, error) {
|
||||
resources := make([]*any.Any, 0)
|
||||
configs := push.AllVirtualServices
|
||||
for _, config := range configs {
|
||||
body, err := types.MarshalAny(config.Spec.(*networking.VirtualService))
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
createTime, err := types.TimestampProto(config.CreationTimestamp)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resource := &mcp.Resource{
|
||||
Body: body,
|
||||
Metadata: &mcp.Metadata{
|
||||
Name: path.Join(config.Namespace, config.Name),
|
||||
CreateTime: createTime,
|
||||
},
|
||||
}
|
||||
mcpAny, err := ptypes.MarshalAny(resource)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resources = append(resources, mcpAny)
|
||||
}
|
||||
return resources, model.DefaultXdsLogDetails, nil
|
||||
}
|
||||
|
||||
func (c VirtualServiceGenerator) GenerateDeltas(proxy *model.Proxy, push *model.PushContext, updates *model.PushRequest,
|
||||
w *model.WatchedResource) ([]*any.Any, []string, model.XdsLogDetails, bool, error) {
|
||||
// TODO: delta implement
|
||||
return nil, nil, model.DefaultXdsLogDetails, false, nil
|
||||
}
|
||||
|
||||
type DestinationRuleGenerator struct {
|
||||
Server *xds.DiscoveryServer
|
||||
}
|
||||
|
||||
func (c DestinationRuleGenerator) Generate(proxy *model.Proxy, push *model.PushContext, w *model.WatchedResource,
|
||||
updates *model.PushRequest) ([]*any.Any, model.XdsLogDetails, error) {
|
||||
resources := make([]*any.Any, 0)
|
||||
configs := push.AllDestinationRules
|
||||
for _, config := range configs {
|
||||
body, err := types.MarshalAny(config.Spec.(*networking.DestinationRule))
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
createTime, err := types.TimestampProto(config.CreationTimestamp)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resource := &mcp.Resource{
|
||||
Body: body,
|
||||
Metadata: &mcp.Metadata{
|
||||
Name: path.Join(config.Namespace, config.Name),
|
||||
CreateTime: createTime,
|
||||
},
|
||||
}
|
||||
mcpAny, err := ptypes.MarshalAny(resource)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resources = append(resources, mcpAny)
|
||||
}
|
||||
return resources, model.DefaultXdsLogDetails, nil
|
||||
}
|
||||
|
||||
func (c DestinationRuleGenerator) GenerateDeltas(proxy *model.Proxy, push *model.PushContext, updates *model.PushRequest,
|
||||
w *model.WatchedResource) ([]*any.Any, []string, model.XdsLogDetails, bool, error) {
|
||||
// TODO: delta implement
|
||||
return nil, nil, model.DefaultXdsLogDetails, false, nil
|
||||
}
|
||||
|
||||
type EnvoyFilterGenerator struct {
|
||||
Server *xds.DiscoveryServer
|
||||
}
|
||||
|
||||
func (c EnvoyFilterGenerator) Generate(proxy *model.Proxy, push *model.PushContext, w *model.WatchedResource,
|
||||
updates *model.PushRequest) ([]*any.Any, model.XdsLogDetails, error) {
|
||||
resources := make([]*any.Any, 0)
|
||||
configs := push.AllEnvoyFilters
|
||||
for _, config := range configs {
|
||||
body, err := types.MarshalAny(config.Spec.(*networking.EnvoyFilter))
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
createTime, err := types.TimestampProto(config.CreationTimestamp)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resource := &mcp.Resource{
|
||||
Body: body,
|
||||
Metadata: &mcp.Metadata{
|
||||
Name: path.Join(config.Namespace, config.Name),
|
||||
CreateTime: createTime,
|
||||
},
|
||||
}
|
||||
mcpAny, err := ptypes.MarshalAny(resource)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resources = append(resources, mcpAny)
|
||||
}
|
||||
return resources, model.DefaultXdsLogDetails, nil
|
||||
}
|
||||
|
||||
func (c EnvoyFilterGenerator) GenerateDeltas(proxy *model.Proxy, push *model.PushContext, updates *model.PushRequest,
|
||||
w *model.WatchedResource) ([]*any.Any, []string, model.XdsLogDetails, bool, error) {
|
||||
// TODO: delta implement
|
||||
return nil, nil, model.DefaultXdsLogDetails, false, nil
|
||||
}
|
||||
|
||||
type GatewayGenerator struct {
|
||||
Server *xds.DiscoveryServer
|
||||
}
|
||||
|
||||
func (c GatewayGenerator) Generate(proxy *model.Proxy, push *model.PushContext, w *model.WatchedResource,
|
||||
updates *model.PushRequest) ([]*any.Any, model.XdsLogDetails, error) {
|
||||
resources := make([]*any.Any, 0)
|
||||
configs := push.AllGateways
|
||||
for _, config := range configs {
|
||||
body, err := types.MarshalAny(config.Spec.(*networking.Gateway))
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
createTime, err := types.TimestampProto(config.CreationTimestamp)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resource := &mcp.Resource{
|
||||
Body: body,
|
||||
Metadata: &mcp.Metadata{
|
||||
Name: path.Join(config.Namespace, config.Name),
|
||||
CreateTime: createTime,
|
||||
},
|
||||
}
|
||||
mcpAny, err := ptypes.MarshalAny(resource)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resources = append(resources, mcpAny)
|
||||
}
|
||||
return resources, model.DefaultXdsLogDetails, nil
|
||||
}
|
||||
|
||||
func (c GatewayGenerator) GenerateDeltas(proxy *model.Proxy, push *model.PushContext, updates *model.PushRequest,
|
||||
w *model.WatchedResource) ([]*any.Any, []string, model.XdsLogDetails, bool, error) {
|
||||
// TODO: delta implement
|
||||
return nil, nil, model.DefaultXdsLogDetails, false, nil
|
||||
}
|
||||
|
||||
type WasmpluginGenerator struct {
|
||||
Server *xds.DiscoveryServer
|
||||
}
|
||||
|
||||
func (c WasmpluginGenerator) Generate(proxy *model.Proxy, push *model.PushContext, w *model.WatchedResource,
|
||||
updates *model.PushRequest) ([]*any.Any, model.XdsLogDetails, error) {
|
||||
resources := make([]*any.Any, 0)
|
||||
configs := push.AllWasmplugins
|
||||
for _, config := range configs {
|
||||
body, err := types.MarshalAny(config.Spec.(*extensions.WasmPlugin))
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
createTime, err := types.TimestampProto(config.CreationTimestamp)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resource := &mcp.Resource{
|
||||
Body: body,
|
||||
Metadata: &mcp.Metadata{
|
||||
Name: path.Join(config.Namespace, config.Name),
|
||||
CreateTime: createTime,
|
||||
},
|
||||
}
|
||||
mcpAny, err := ptypes.MarshalAny(resource)
|
||||
if err != nil {
|
||||
return nil, model.DefaultXdsLogDetails, err
|
||||
}
|
||||
resources = append(resources, mcpAny)
|
||||
}
|
||||
return resources, model.DefaultXdsLogDetails, nil
|
||||
}
|
||||
|
||||
func (c WasmpluginGenerator) GenerateDeltas(proxy *model.Proxy, push *model.PushContext, updates *model.PushRequest,
|
||||
w *model.WatchedResource) ([]*any.Any, []string, model.XdsLogDetails, bool, error) {
|
||||
// TODO: delta implement
|
||||
return nil, nil, model.DefaultXdsLogDetails, false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user