mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 12:47:28 +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