mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 20:57:32 +08:00
feat: support KnativeIngress (#524)
This commit is contained in:
552
pkg/ingress/config/kingress_config.go
Normal file
552
pkg/ingress/config/kingress_config.go
Normal file
@@ -0,0 +1,552 @@
|
||||
// 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 (
|
||||
"sync"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"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"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/kingress"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/secret"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
"github.com/alibaba/higress/registry/reconcile"
|
||||
)
|
||||
|
||||
var (
|
||||
_ model.ConfigStoreCache = &KIngressConfig{}
|
||||
_ model.IngressStore = &KIngressConfig{}
|
||||
)
|
||||
|
||||
type KIngressConfig struct {
|
||||
// key: cluster id
|
||||
remoteIngressControllers map[string]common.KIngressController
|
||||
mutex sync.RWMutex
|
||||
|
||||
ingressRouteCache model.IngressRouteCollection
|
||||
ingressDomainCache model.IngressDomainCollection
|
||||
|
||||
localKubeClient kube.Client
|
||||
virtualServiceHandlers []model.EventHandler
|
||||
gatewayHandlers []model.EventHandler
|
||||
envoyFilterHandlers []model.EventHandler
|
||||
WatchErrorHandler cache.WatchErrorHandler
|
||||
|
||||
cachedEnvoyFilters []config.Config
|
||||
|
||||
watchedSecretSet sets.Set
|
||||
|
||||
RegistryReconciler *reconcile.Reconciler
|
||||
|
||||
XDSUpdater model.XDSUpdater
|
||||
|
||||
annotationHandler annotations.AnnotationHandler
|
||||
|
||||
globalGatewayName string
|
||||
|
||||
namespace string
|
||||
|
||||
clusterId string
|
||||
}
|
||||
|
||||
func NewKIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater, namespace, clusterId string) *KIngressConfig {
|
||||
if localKubeClient.KIngressInformer() == nil {
|
||||
return nil
|
||||
}
|
||||
if clusterId == "Kubernetes" {
|
||||
clusterId = ""
|
||||
}
|
||||
config := &KIngressConfig{
|
||||
remoteIngressControllers: make(map[string]common.KIngressController),
|
||||
localKubeClient: localKubeClient,
|
||||
XDSUpdater: XDSUpdater,
|
||||
annotationHandler: annotations.NewAnnotationHandlerManager(),
|
||||
clusterId: clusterId,
|
||||
globalGatewayName: namespace + "/" +
|
||||
common.CreateConvertedName(clusterId, "global"),
|
||||
watchedSecretSet: sets.NewSet(),
|
||||
namespace: namespace,
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
|
||||
IngressLog.Infof("register resource %v", kind)
|
||||
switch kind {
|
||||
case gvk.VirtualService:
|
||||
m.virtualServiceHandlers = append(m.virtualServiceHandlers, f)
|
||||
|
||||
case gvk.Gateway:
|
||||
m.gatewayHandlers = append(m.gatewayHandlers, f)
|
||||
|
||||
case gvk.EnvoyFilter:
|
||||
m.envoyFilterHandlers = append(m.envoyFilterHandlers, f)
|
||||
}
|
||||
|
||||
for _, remoteIngressController := range m.remoteIngressControllers {
|
||||
remoteIngressController.RegisterEventHandler(kind, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) AddLocalCluster(options common.Options) common.KIngressController {
|
||||
secretController := secret.NewController(m.localKubeClient, options.ClusterId)
|
||||
secretController.AddEventHandler(m.ReflectSecretChanges)
|
||||
|
||||
var ingressController common.KIngressController
|
||||
|
||||
ingressController = kingress.NewController(m.localKubeClient, m.localKubeClient, options, secretController)
|
||||
|
||||
m.remoteIngressControllers[options.ClusterId] = ingressController
|
||||
return ingressController
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) InitializeCluster(ingressController common.KIngressController, stop <-chan struct{}) error {
|
||||
_ = ingressController.SetWatchErrorHandler(m.WatchErrorHandler)
|
||||
go ingressController.Run(stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) List(typ config.GroupVersionKind, namespace string) ([]config.Config, error) {
|
||||
if typ == gvk.EnvoyFilter || typ == gvk.DestinationRule || typ == gvk.WasmPlugin || typ == gvk.ServiceEntry {
|
||||
return nil, nil
|
||||
}
|
||||
if typ != gvk.Gateway && typ != gvk.VirtualService {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) 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 *KIngressConfig) 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 *KIngressConfig) convertVirtualService(configs []common.WrapperConfig) []config.Config {
|
||||
convertOptions := common.ConvertOptions{
|
||||
IngressRouteCache: common.NewIngressRouteCache(),
|
||||
VirtualServices: map[string]*common.WrapperVirtualService{},
|
||||
HTTPRoutes: map[string][]*common.WrapperHTTPRoute{},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize weighted cluster to make sure the sum of weight is 100.
|
||||
for _, host := range convertOptions.HTTPRoutes {
|
||||
for _, route := range host {
|
||||
normalizeWeightedKCluster(convertOptions.IngressRouteCache, route)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply annotation on virtual services Only IP-control and do nothing
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Make sure that the sum of traffic split ratio is 100, if it is not 100, it will be normalized
|
||||
func normalizeWeightedKCluster(cache *common.IngressRouteCache, route *common.WrapperHTTPRoute) {
|
||||
if len(route.HTTPRoute.Route) == 1 {
|
||||
route.HTTPRoute.Route[0].Weight = 100
|
||||
return
|
||||
}
|
||||
|
||||
var weightTotal int32 = 0
|
||||
for _, routeDestination := range route.HTTPRoute.Route {
|
||||
weightTotal += routeDestination.Weight
|
||||
}
|
||||
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 *KIngressConfig) 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 *KIngressConfig) 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 *KIngressConfig) 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 (m *KIngressConfig) Run(stop <-chan struct{}) {}
|
||||
|
||||
func (m *KIngressConfig) HasSynced() bool {
|
||||
IngressLog.Info("In Kingress Synced.")
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for _, remoteIngressController := range m.remoteIngressControllers {
|
||||
IngressLog.Info("In Kingress Synced.", remoteIngressController)
|
||||
if !remoteIngressController.HasSynced() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
IngressLog.Info("Ingress config controller synced.")
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) SetWatchErrorHandler(f func(r *cache.Reflector, err error)) error {
|
||||
m.WatchErrorHandler = f
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) GetIngressRoutes() model.IngressRouteCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.ingressRouteCache
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) GetIngressDomains() model.IngressDomainCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.ingressDomainCache
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Schemas() collection.Schemas {
|
||||
return common.IngressIR
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Get(config.GroupVersionKind, string, string) *config.Config {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Create(config.Config) (revision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Update(config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) UpdateStatus(config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Patch(config.Config, config.PatchFunc) (string, error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Delete(config.GroupVersionKind, string, string, *string) error {
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
481
pkg/ingress/config/kingress_config_test.go
Normal file
481
pkg/ingress/config/kingress_config_test.go
Normal file
@@ -0,0 +1,481 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
kcontrollerv1 "github.com/alibaba/higress/pkg/ingress/kube/kingress"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
func TestNormalizeKWeightedCluster(t *testing.T) {
|
||||
validate := func(route *common.WrapperHTTPRoute) int32 {
|
||||
var total int32
|
||||
fmt.Print("----------------------------")
|
||||
for _, routeDestination := range route.HTTPRoute.Route {
|
||||
total += routeDestination.Weight
|
||||
fmt.Print(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) {
|
||||
normalizeWeightedKCluster(nil, route)
|
||||
if validate(route) != 100 {
|
||||
t.Fatalf("Weight sum should be 100, but actual is %d", validate(route))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertGatewaysForKIngress(t *testing.T) {
|
||||
fake := kube.NewFakeClient()
|
||||
v1Options := common.Options{
|
||||
Enable: true,
|
||||
ClusterId: "kingress",
|
||||
RawClusterId: "kingress__",
|
||||
}
|
||||
kingressV1Controller := kcontrollerv1.NewController(fake, fake, v1Options, nil)
|
||||
m := NewKIngressConfig(fake, nil, "wakanda", "gw-123-istio")
|
||||
m.remoteIngressControllers = map[string]common.KIngressController{
|
||||
"kingress": kingressV1Controller,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputConfig []common.WrapperConfig
|
||||
expect map[string]config.Config
|
||||
}{
|
||||
{
|
||||
name: "kingress",
|
||||
inputConfig: []common.WrapperConfig{
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-1",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
},
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
HTTPOption: ingress.HTTPOptionEnabled,
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com",
|
||||
},
|
||||
},
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-2",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
},
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
HTTPOption: ingress.HTTPOptionRedirected,
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
SecretName: "foo-com",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com-2",
|
||||
},
|
||||
},
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"bar.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-3",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
},
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
HTTPOption: ingress.HTTPOptionEnabled,
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
SecretName: "foo-com",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com-3",
|
||||
},
|
||||
},
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"bar.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
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: "kingress",
|
||||
common.HostAnnotation: "foo.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-kingress-wakanda-test-1-foo-com",
|
||||
},
|
||||
Hosts: []string{"foo.com"},
|
||||
//Tls: &networking.ServerTLSSettings{
|
||||
// HttpsRedirect: true,
|
||||
//},
|
||||
},
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
Name: "https-443-ingress-kingress-wakanda-test-2-foo-com",
|
||||
},
|
||||
Hosts: []string{"foo.com"},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://kingress__/wakanda/foo-com",
|
||||
//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: "kingress",
|
||||
common.HostAnnotation: "test.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-kingress-wakanda-test-1-test-com",
|
||||
},
|
||||
Hosts: []string{"test.com"},
|
||||
//Tls: &networking.ServerTLSSettings{
|
||||
// HttpsRedirect: true,
|
||||
//},
|
||||
},
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
Name: "https-443-ingress-kingress-wakanda-test-1-test-com",
|
||||
},
|
||||
Hosts: []string{"test.com"},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://kingress__/wakanda/test-com",
|
||||
//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: "kingress",
|
||||
common.HostAnnotation: "bar.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-kingress-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)
|
||||
fmt.Print(item)
|
||||
target[host] = item
|
||||
}
|
||||
assert.Equal(t, testCase.expect, target)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -139,3 +139,27 @@ type IngressController interface {
|
||||
// HasSynced returns true after initial cache synchronization is complete
|
||||
HasSynced() bool
|
||||
}
|
||||
|
||||
type KIngressController 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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
751
pkg/ingress/kube/kingress/controller.go
Normal file
751
pkg/ingress/kube/kingress/controller.go
Normal file
@@ -0,0 +1,751 @@
|
||||
// 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 kingress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/model/credentials"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/protocol"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
kset "k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
networkingv1alpha1 "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/kingress/resources"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/secret"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
var (
|
||||
_ common.KIngressController = &controller{}
|
||||
)
|
||||
|
||||
const (
|
||||
// ClassAnnotationKey points to the annotation for the class of this resource.
|
||||
ClassAnnotationKey = "networking.knative.dev/ingress.class"
|
||||
IngressClassName = "higress"
|
||||
)
|
||||
|
||||
type controller struct {
|
||||
queue workqueue.RateLimitingInterface
|
||||
virtualServiceHandlers []model.EventHandler
|
||||
gatewayHandlers []model.EventHandler
|
||||
envoyFilterHandlers []model.EventHandler
|
||||
|
||||
options common.Options
|
||||
|
||||
mutex sync.RWMutex
|
||||
// key: namespace/name
|
||||
ingresses map[string]*ingress.Ingress
|
||||
|
||||
ingressInformer cache.SharedInformer
|
||||
ingressLister networkingv1alpha1.IngressLister
|
||||
serviceInformer cache.SharedInformer
|
||||
serviceLister listerv1.ServiceLister
|
||||
secretController secret.SecretController
|
||||
statusSyncer *statusSyncer
|
||||
}
|
||||
|
||||
// NewController creates a new Kubernetes controller
|
||||
func NewController(localKubeClient, client kube.Client, options common.Options,
|
||||
secretController secret.SecretController) common.KIngressController {
|
||||
q := workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter())
|
||||
|
||||
//var namespace string = "default"
|
||||
ingressInformer := client.KIngressInformer().Networking().V1alpha1().Ingresses()
|
||||
serviceInformer := client.KubeInformer().Core().V1().Services()
|
||||
|
||||
c := &controller{
|
||||
options: options,
|
||||
queue: q,
|
||||
ingresses: make(map[string]*ingress.Ingress),
|
||||
ingressInformer: ingressInformer.Informer(),
|
||||
ingressLister: ingressInformer.Lister(),
|
||||
serviceInformer: serviceInformer.Informer(),
|
||||
serviceLister: serviceInformer.Lister(),
|
||||
secretController: secretController,
|
||||
}
|
||||
|
||||
handler := controllers.LatestVersionHandlerFuncs(controllers.EnqueueForSelf(q))
|
||||
c.ingressInformer.AddEventHandler(handler)
|
||||
|
||||
if options.EnableStatus {
|
||||
c.statusSyncer = newStatusSyncer(localKubeClient, client, c, options.SystemNamespace)
|
||||
} else {
|
||||
IngressLog.Infof("Disable status update for cluster %s", options.ClusterId)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *controller) ServiceLister() listerv1.ServiceLister {
|
||||
return c.serviceLister
|
||||
}
|
||||
|
||||
func (c *controller) SecretLister() listerv1.SecretLister {
|
||||
return c.secretController.Lister()
|
||||
}
|
||||
|
||||
func (c *controller) Run(stop <-chan struct{}) {
|
||||
if c.statusSyncer != nil {
|
||||
go c.statusSyncer.run(stop)
|
||||
}
|
||||
go c.secretController.Run(stop)
|
||||
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.queue.ShutDown()
|
||||
|
||||
if !cache.WaitForCacheSync(stop, c.HasSynced) {
|
||||
IngressLog.Errorf("Failed to sync ingress controller cache for cluster %s", c.options.ClusterId)
|
||||
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)
|
||||
if err := c.onEvent(ingressNamespacedName); err != nil {
|
||||
IngressLog.Errorf("error processing ingress item (%v) (retrying): %v, cluster: %s", key, err, c.options.ClusterId)
|
||||
c.queue.AddRateLimited(key)
|
||||
} else {
|
||||
c.queue.Forget(key)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
event := model.EventUpdate
|
||||
ing, err := c.ingressLister.Ingresses(namespacedName.Namespace).Get(namespacedName.Name)
|
||||
ing.Status.InitializeConditions()
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
event = model.EventDelete
|
||||
c.mutex.Lock()
|
||||
ing = c.ingresses[namespacedName.String()]
|
||||
delete(c.ingresses, namespacedName.String())
|
||||
c.mutex.Unlock()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ingress deleted, and it is not processed before
|
||||
if ing == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we should check need process only when event is not delete,
|
||||
// if it is delete event, and previously processed, we need to process too.
|
||||
if event != model.EventDelete {
|
||||
shouldProcess, err := c.shouldProcessIngressUpdate(ing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !shouldProcess {
|
||||
IngressLog.Infof("no need process, ingress %s", namespacedName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
vsmetadata := config.Meta{
|
||||
Name: ing.Name + "-" + "virtualservice",
|
||||
Namespace: ing.Namespace,
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
efmetadata := config.Meta{
|
||||
Name: ing.Name + "-" + "envoyfilter",
|
||||
Namespace: ing.Namespace,
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
gatewaymetadata := config.Meta{
|
||||
Name: ing.Name + "-" + "gateway",
|
||||
Namespace: ing.Namespace,
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
|
||||
for _, f := range c.virtualServiceHandlers {
|
||||
f(config.Config{Meta: vsmetadata}, config.Config{Meta: vsmetadata}, event)
|
||||
}
|
||||
|
||||
for _, f := range c.envoyFilterHandlers {
|
||||
f(config.Config{Meta: efmetadata}, config.Config{Meta: efmetadata}, event)
|
||||
}
|
||||
|
||||
for _, f := range c.gatewayHandlers {
|
||||
f(config.Config{Meta: gatewaymetadata}, config.Config{Meta: gatewaymetadata}, event)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
|
||||
switch kind {
|
||||
case gvk.VirtualService:
|
||||
c.virtualServiceHandlers = append(c.virtualServiceHandlers, f)
|
||||
case gvk.Gateway:
|
||||
c.gatewayHandlers = append(c.gatewayHandlers, f)
|
||||
case gvk.EnvoyFilter:
|
||||
c.envoyFilterHandlers = append(c.envoyFilterHandlers, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) SetWatchErrorHandler(handler func(r *cache.Reflector, err error)) error {
|
||||
var errs error
|
||||
if err := c.serviceInformer.SetWatchErrorHandler(handler); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
if err := c.ingressInformer.SetWatchErrorHandler(handler); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
if err := c.secretController.Informer().SetWatchErrorHandler(handler); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *controller) HasSynced() bool {
|
||||
return c.ingressInformer.HasSynced() && c.serviceInformer.HasSynced() && c.secretController.HasSynced()
|
||||
}
|
||||
|
||||
func (c *controller) List() []config.Config {
|
||||
c.mutex.RLock()
|
||||
out := make([]config.Config, 0, len(c.ingresses))
|
||||
c.mutex.RUnlock()
|
||||
|
||||
for _, raw := range c.ingressInformer.GetStore().List() {
|
||||
ing, ok := raw.(*ingress.Ingress)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if should, err := c.shouldProcessIngress(ing); !should || err != nil {
|
||||
continue
|
||||
}
|
||||
copiedConfig := ing.DeepCopy()
|
||||
|
||||
outConfig := config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: copiedConfig.Name,
|
||||
Namespace: copiedConfig.Namespace,
|
||||
Annotations: common.CreateOrUpdateAnnotations(copiedConfig.Annotations, c.options),
|
||||
Labels: copiedConfig.Labels,
|
||||
CreationTimestamp: copiedConfig.CreationTimestamp.Time,
|
||||
},
|
||||
Spec: copiedConfig.Spec,
|
||||
}
|
||||
|
||||
out = append(out, outConfig)
|
||||
}
|
||||
|
||||
common.RecordIngressNumber(c.options.ClusterId, len(out))
|
||||
return out
|
||||
}
|
||||
|
||||
func extractTLSSecretName(host string, tls []ingress.IngressTLS) string {
|
||||
if len(tls) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, t := range tls {
|
||||
match := false
|
||||
for _, h := range t.Hosts {
|
||||
if h == host {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
return t.SecretName
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
|
||||
if convertOptions == nil {
|
||||
return fmt.Errorf("convertOptions is nil")
|
||||
}
|
||||
if wrapper == nil {
|
||||
return fmt.Errorf("wrapperConfig is nil")
|
||||
}
|
||||
|
||||
cfg := wrapper.Config
|
||||
kingressv1alpha1, ok := cfg.Spec.(ingress.IngressSpec)
|
||||
|
||||
if !ok {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, common.Unknown)
|
||||
return fmt.Errorf("convert type is invalid in cluster %s", c.options.ClusterId)
|
||||
}
|
||||
if len(kingressv1alpha1.Rules) == 0 {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, common.EmptyRule)
|
||||
return fmt.Errorf("invalid ingress rule %s:%s in cluster %s, `rules` must be specified", cfg.Namespace, cfg.Name, c.options.ClusterId)
|
||||
}
|
||||
|
||||
for _, rule := range kingressv1alpha1.Rules {
|
||||
for _, ruleHost := range rule.Hosts {
|
||||
cleanHost := common.CleanHost(ruleHost)
|
||||
// Need create builder for every rule.
|
||||
domainBuilder := &common.IngressDomainBuilder{
|
||||
ClusterId: c.options.ClusterId,
|
||||
Protocol: common.HTTP,
|
||||
Host: ruleHost,
|
||||
Ingress: cfg,
|
||||
Event: common.Normal,
|
||||
}
|
||||
// Extract the previous gateway and builder
|
||||
wrapperGateway, exist := convertOptions.Gateways[ruleHost]
|
||||
preDomainBuilder, _ := convertOptions.IngressDomainCache.Valid[ruleHost]
|
||||
if !exist {
|
||||
wrapperGateway = &common.WrapperGateway{
|
||||
Gateway: &networking.Gateway{},
|
||||
WrapperConfig: wrapper,
|
||||
ClusterId: c.options.ClusterId,
|
||||
Host: ruleHost,
|
||||
}
|
||||
if c.options.GatewaySelectorKey != "" {
|
||||
wrapperGateway.Gateway.Selector = map[string]string{c.options.GatewaySelectorKey: c.options.GatewaySelectorValue}
|
||||
}
|
||||
if rule.Visibility == ingress.IngressVisibilityClusterLocal {
|
||||
wrapperGateway.Gateway.Servers = append(wrapperGateway.Gateway.Servers, &networking.Server{
|
||||
Port: &networking.Port{
|
||||
Number: 8081,
|
||||
Protocol: string(protocol.HTTP),
|
||||
Name: common.CreateConvertedName("http-8081-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
|
||||
},
|
||||
Hosts: []string{ruleHost},
|
||||
})
|
||||
|
||||
} else {
|
||||
wrapperGateway.Gateway.Servers = append(wrapperGateway.Gateway.Servers, &networking.Server{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: string(protocol.HTTP),
|
||||
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
|
||||
},
|
||||
Hosts: []string{ruleHost},
|
||||
})
|
||||
}
|
||||
|
||||
// Add new gateway, builder
|
||||
convertOptions.Gateways[ruleHost] = wrapperGateway
|
||||
convertOptions.IngressDomainCache.Valid[ruleHost] = domainBuilder
|
||||
} else {
|
||||
// Fallback to get downstream tls from current ingress.
|
||||
if wrapperGateway.WrapperConfig.AnnotationsConfig.DownstreamTLS == nil {
|
||||
wrapperGateway.WrapperConfig.AnnotationsConfig.DownstreamTLS = wrapper.AnnotationsConfig.DownstreamTLS
|
||||
}
|
||||
}
|
||||
//Redirect option
|
||||
if isIngressPublic(&kingressv1alpha1) && (kingressv1alpha1.HTTPOption == ingress.HTTPOptionRedirected) {
|
||||
for _, server := range wrapperGateway.Gateway.Servers {
|
||||
if protocol.Parse(server.Port.Protocol).IsHTTP() {
|
||||
server.Tls = &networking.ServerTLSSettings{
|
||||
HttpsRedirect: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if isIngressPublic(&kingressv1alpha1) && (kingressv1alpha1.HTTPOption == ingress.HTTPOptionEnabled) {
|
||||
for _, server := range wrapperGateway.Gateway.Servers {
|
||||
if protocol.Parse(server.Port.Protocol).IsHTTP() {
|
||||
server.Tls = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are no tls settings, so just skip.
|
||||
if len(kingressv1alpha1.TLS) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get tls secret matching the rule host
|
||||
secretName := extractTLSSecretName(ruleHost, kingressv1alpha1.TLS)
|
||||
if secretName == "" {
|
||||
// There no matching secret, so just skip.
|
||||
continue
|
||||
}
|
||||
|
||||
domainBuilder.Protocol = common.HTTPS
|
||||
domainBuilder.SecretName = path.Join(c.options.ClusterId, cfg.Namespace, secretName)
|
||||
|
||||
// There is a matching secret and the gateway has already a tls secret.
|
||||
// We should report the duplicated tls secret event.
|
||||
if wrapperGateway.IsHTTPS() {
|
||||
domainBuilder.Event = common.DuplicatedTls
|
||||
domainBuilder.PreIngress = preDomainBuilder.Ingress
|
||||
convertOptions.IngressDomainCache.Invalid = append(convertOptions.IngressDomainCache.Invalid,
|
||||
domainBuilder.Build())
|
||||
continue
|
||||
}
|
||||
|
||||
// Append https server
|
||||
wrapperGateway.Gateway.Servers = append(wrapperGateway.Gateway.Servers, &networking.Server{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: string(protocol.HTTPS),
|
||||
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
|
||||
},
|
||||
Hosts: []string{ruleHost},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: credentials.ToKubernetesIngressResource(c.options.RawClusterId, cfg.Namespace, secretName),
|
||||
},
|
||||
})
|
||||
|
||||
// Update domain builder
|
||||
convertOptions.IngressDomainCache.Valid[ruleHost] = domainBuilder
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
|
||||
if convertOptions == nil {
|
||||
return fmt.Errorf("convertOptions is nil")
|
||||
}
|
||||
if wrapper == nil {
|
||||
return fmt.Errorf("wrapperConfig is nil")
|
||||
}
|
||||
|
||||
cfg := wrapper.Config
|
||||
KingressV1, ok := cfg.Spec.(ingress.IngressSpec)
|
||||
if !ok {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, common.Unknown)
|
||||
return fmt.Errorf("convert type is invalid in cluster %s", c.options.ClusterId)
|
||||
}
|
||||
if len(KingressV1.Rules) == 0 {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, common.EmptyRule)
|
||||
return fmt.Errorf("invalid ingress rule %s:%s in cluster %s, `rules` must be specified", cfg.Namespace, cfg.Name, c.options.ClusterId)
|
||||
}
|
||||
convertOptions.HasDefaultBackend = false
|
||||
// In one ingress, we will limit the rule conflict.
|
||||
// When the host, pathType, path of two rule are same, we think there is a conflict event.
|
||||
definedRules := sets.NewSet()
|
||||
|
||||
var (
|
||||
// But in across ingresses case, we will restrict this limit.
|
||||
// When the {host, path, headers, method, params} of two rule in different ingress are same, we think there is a conflict event.
|
||||
tempRuleKey []string
|
||||
)
|
||||
|
||||
for _, rule := range KingressV1.Rules {
|
||||
for _, rulehost := range rule.Hosts {
|
||||
if rule.HTTP == nil || len(rule.HTTP.Paths) == 0 {
|
||||
IngressLog.Warnf("invalid ingress rule %s:%s for host %q in cluster %s, no paths defined", cfg.Namespace, cfg.Name, rulehost, c.options.ClusterId)
|
||||
continue
|
||||
}
|
||||
wrapperVS, exist := convertOptions.VirtualServices[rulehost]
|
||||
if !exist {
|
||||
wrapperVS = &common.WrapperVirtualService{
|
||||
VirtualService: &networking.VirtualService{
|
||||
Hosts: []string{rulehost},
|
||||
},
|
||||
WrapperConfig: wrapper,
|
||||
}
|
||||
convertOptions.VirtualServices[rulehost] = wrapperVS
|
||||
}
|
||||
wrapperHttpRoutes := make([]*common.WrapperHTTPRoute, 0, len(rule.HTTP.Paths))
|
||||
for _, httpPath := range rule.HTTP.Paths {
|
||||
wrapperHttpRoute := &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{},
|
||||
WrapperConfig: wrapper,
|
||||
Host: rulehost,
|
||||
ClusterId: c.options.ClusterId,
|
||||
}
|
||||
|
||||
var pathType common.PathType
|
||||
originPath := httpPath.Path
|
||||
pathType = common.Prefix
|
||||
wrapperHttpRoute.OriginPath = originPath
|
||||
wrapperHttpRoute.OriginPathType = pathType
|
||||
wrapperHttpRoute.HTTPRoute = resources.MakeVirtualServiceRoute(transformHosts(rulehost), &httpPath)
|
||||
wrapperHttpRoute.HTTPRoute.Name = common.GenerateUniqueRouteName(c.options.SystemNamespace, wrapperHttpRoute)
|
||||
ingressRouteBuilder := convertOptions.IngressRouteCache.New(wrapperHttpRoute)
|
||||
hostAndPath := wrapperHttpRoute.PathFormat()
|
||||
key := createRuleKey(cfg.Annotations, hostAndPath)
|
||||
wrapperHttpRoute.RuleKey = key
|
||||
if WrapPreIngress, exist := convertOptions.Route2Ingress[key]; exist {
|
||||
ingressRouteBuilder.PreIngress = WrapPreIngress.Config
|
||||
ingressRouteBuilder.Event = common.DuplicatedRoute
|
||||
}
|
||||
tempRuleKey = append(tempRuleKey, key)
|
||||
|
||||
// Two duplicated rules in the same ingress.
|
||||
if ingressRouteBuilder.Event == common.Normal {
|
||||
pathFormat := wrapperHttpRoute.PathFormat()
|
||||
if definedRules.Contains(pathFormat) {
|
||||
ingressRouteBuilder.PreIngress = cfg
|
||||
ingressRouteBuilder.Event = common.DuplicatedRoute
|
||||
}
|
||||
definedRules.Insert(pathFormat)
|
||||
}
|
||||
|
||||
// backend service check
|
||||
var event common.Event
|
||||
destinationConfig := wrapper.AnnotationsConfig.Destination
|
||||
event = c.IngressRouteBuilderServicesCheck(&httpPath, cfg.Namespace, ingressRouteBuilder, destinationConfig)
|
||||
|
||||
if destinationConfig != nil {
|
||||
wrapperHttpRoute.WeightTotal = int32(destinationConfig.WeightSum)
|
||||
}
|
||||
|
||||
if ingressRouteBuilder.Event != common.Normal {
|
||||
event = ingressRouteBuilder.Event
|
||||
}
|
||||
|
||||
if event != common.Normal {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, event)
|
||||
ingressRouteBuilder.Event = event
|
||||
} else {
|
||||
wrapperHttpRoutes = append(wrapperHttpRoutes, wrapperHttpRoute)
|
||||
}
|
||||
convertOptions.IngressRouteCache.Add(ingressRouteBuilder)
|
||||
}
|
||||
|
||||
for idx, item := range tempRuleKey {
|
||||
if val, exist := convertOptions.Route2Ingress[item]; !exist || strings.Compare(val.RuleKey, tempRuleKey[idx]) != 0 {
|
||||
convertOptions.Route2Ingress[item] = &common.WrapperConfigWithRuleKey{
|
||||
Config: cfg,
|
||||
RuleKey: tempRuleKey[idx],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
old, f := convertOptions.HTTPRoutes[rulehost]
|
||||
if f {
|
||||
old = append(old, wrapperHttpRoutes...)
|
||||
convertOptions.HTTPRoutes[rulehost] = old
|
||||
} else {
|
||||
convertOptions.HTTPRoutes[rulehost] = wrapperHttpRoutes
|
||||
}
|
||||
|
||||
// Sort, exact -> prefix -> regex
|
||||
routes := convertOptions.HTTPRoutes[rulehost]
|
||||
IngressLog.Debugf("routes of host %s is %v", rulehost, routes)
|
||||
common.SortHTTPRoutes(routes)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *controller) IngressRouteBuilderServicesCheck(httppath *ingress.HTTPIngressPath, namespace string,
|
||||
builder *common.IngressRouteBuilder, config *annotations.DestinationConfig) common.Event {
|
||||
|
||||
//backend check
|
||||
if httppath.Splits == nil {
|
||||
return common.InvalidBackendService
|
||||
}
|
||||
for _, split := range httppath.Splits {
|
||||
if split.ServiceName == "" {
|
||||
return common.InvalidBackendService
|
||||
}
|
||||
backendService := model.BackendService{
|
||||
Namespace: namespace,
|
||||
Name: split.ServiceName,
|
||||
Port: uint32(split.ServicePort.IntValue()),
|
||||
Weight: int32(split.Percent),
|
||||
}
|
||||
builder.ServiceList = append(builder.ServiceList, backendService)
|
||||
}
|
||||
return common.Normal
|
||||
}
|
||||
|
||||
func (c *controller) shouldProcessIngressWithClass(ing *ingress.Ingress) bool {
|
||||
if classValue, found := ing.GetAnnotations()[ClassAnnotationKey]; !found || classValue != IngressClassName {
|
||||
IngressLog.Debugf("Ingress class %s does not match knative IngressCLassName %s.", classValue, IngressClassName)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *controller) shouldProcessIngress(i *ingress.Ingress) (bool, error) {
|
||||
//check namespace
|
||||
if c.shouldProcessIngressWithClass(i) {
|
||||
switch c.options.WatchNamespace {
|
||||
case "":
|
||||
return true, nil
|
||||
default:
|
||||
return c.options.WatchNamespace == i.Namespace, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
|
||||
}
|
||||
|
||||
// shouldProcessIngressUpdate checks whether we should renotify registered handlers about an update event
|
||||
func (c *controller) shouldProcessIngressUpdate(ing *ingress.Ingress) (bool, error) {
|
||||
shouldProcess, err := c.shouldProcessIngress(ing)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
namespacedName := ing.Namespace + "/" + ing.Name
|
||||
if shouldProcess {
|
||||
// record processed ingress
|
||||
c.mutex.Lock()
|
||||
preConfig, exist := c.ingresses[namespacedName]
|
||||
c.ingresses[namespacedName] = ing
|
||||
c.mutex.Unlock()
|
||||
|
||||
// We only care about annotations, labels and spec.
|
||||
if exist {
|
||||
if !reflect.DeepEqual(preConfig.Annotations, ing.Annotations) {
|
||||
IngressLog.Debugf("Annotations of ingress %s changed, should process.", namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
if !reflect.DeepEqual(preConfig.Labels, ing.Labels) {
|
||||
IngressLog.Debugf("Labels of ingress %s changed, should process.", namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
if !reflect.DeepEqual(preConfig.Spec, ing.Spec) {
|
||||
IngressLog.Debugf("Spec of ingress %s changed, should process.", namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
IngressLog.Debugf("First receive relative ingress %s, should process.", namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
_, preProcessed := c.ingresses[namespacedName]
|
||||
// previous processed but should not currently, delete it
|
||||
if preProcessed && !shouldProcess {
|
||||
delete(c.ingresses, namespacedName)
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
return preProcessed, nil
|
||||
}
|
||||
|
||||
// createRuleKey according to the pathType, path, methods, headers, params of rules
|
||||
func createRuleKey(annots map[string]string, hostAndPath string) string {
|
||||
var (
|
||||
headers [][2]string
|
||||
params [][2]string
|
||||
sb strings.Builder
|
||||
)
|
||||
|
||||
sep := "\n\n"
|
||||
|
||||
// path
|
||||
sb.WriteString(hostAndPath)
|
||||
sb.WriteString(sep)
|
||||
|
||||
// methods
|
||||
if str, ok := annots[annotations.HigressAnnotationsPrefix+"/"+annotations.MatchMethod]; ok {
|
||||
sb.WriteString(str)
|
||||
}
|
||||
sb.WriteString(sep)
|
||||
|
||||
start := len(annotations.HigressAnnotationsPrefix) + 1 // example: higress.io/exact-match-header-key: value
|
||||
// headers && params
|
||||
for k, val := range annots {
|
||||
if idx := strings.Index(k, annotations.MatchHeader); idx != -1 {
|
||||
key := k[start:idx] + k[idx+len(annotations.MatchHeader)+1:]
|
||||
headers = append(headers, [2]string{key, val})
|
||||
}
|
||||
if idx := strings.Index(k, annotations.MatchQuery); idx != -1 {
|
||||
key := k[start:idx] + k[idx+len(annotations.MatchQuery)+1:]
|
||||
params = append(params, [2]string{key, val})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(headers, func(i, j int) bool {
|
||||
return headers[i][0] < headers[j][0]
|
||||
})
|
||||
sort.SliceStable(params, func(i, j int) bool {
|
||||
return params[i][0] < params[j][0]
|
||||
})
|
||||
for idx := range headers {
|
||||
if idx != 0 {
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
sb.WriteString(headers[idx][0])
|
||||
sb.WriteByte('\t')
|
||||
sb.WriteString(headers[idx][1])
|
||||
}
|
||||
sb.WriteString(sep)
|
||||
for idx := range params {
|
||||
if idx != 0 {
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
sb.WriteString(params[idx][0])
|
||||
sb.WriteByte('\t')
|
||||
sb.WriteString(params[idx][1])
|
||||
}
|
||||
sb.WriteString(sep)
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func transformHosts(host string) kset.String {
|
||||
hosts := []string{host}
|
||||
out := kset.NewString()
|
||||
out.Insert(hosts...)
|
||||
return out
|
||||
}
|
||||
|
||||
func isIngressPublic(ingSpec *ingress.IngressSpec) bool {
|
||||
for _, rule := range ingSpec.Rules {
|
||||
if rule.Visibility == ingress.IngressVisibilityExternalIP {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
604
pkg/ingress/kube/kingress/controller_test.go
Normal file
604
pkg/ingress/kube/kingress/controller_test.go
Normal file
@@ -0,0 +1,604 @@
|
||||
// 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 kingress
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
istiov1alpha3 "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"knative.dev/networking/pkg/apis/networking"
|
||||
"knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
"knative.dev/pkg/kmeta"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/secret"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
testNS = "testNS"
|
||||
IstioIngressClassNametest = "higress"
|
||||
)
|
||||
|
||||
var (
|
||||
ingressRules = []v1alpha1.IngressRule{{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &v1alpha1.HTTPIngressRuleValue{
|
||||
Paths: []v1alpha1.HTTPIngressPath{{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: testNS,
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: v1alpha1.IngressVisibilityExternalIP,
|
||||
}, {
|
||||
Hosts: []string{
|
||||
"host-tls.test-ns.svc.cluster.local",
|
||||
},
|
||||
HTTP: &v1alpha1.HTTPIngressRuleValue{
|
||||
Paths: []v1alpha1.HTTPIngressPath{{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: testNS,
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: v1alpha1.IngressVisibilityClusterLocal,
|
||||
}}
|
||||
|
||||
ingressTLS = []v1alpha1.IngressTLS{{
|
||||
Hosts: []string{"host-tls.example.com"},
|
||||
SecretName: "secret0",
|
||||
SecretNamespace: "istio-system",
|
||||
}}
|
||||
|
||||
// The gateway server according to ingressTLS.
|
||||
ingressTLSServer = &istiov1alpha3.Server{
|
||||
Hosts: []string{"host-tls.example.com"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "test-ns/reconciling-ingress:0",
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &istiov1alpha3.ServerTLSSettings{
|
||||
Mode: istiov1alpha3.ServerTLSSettings_SIMPLE,
|
||||
ServerCertificate: "tls.crt",
|
||||
PrivateKey: "tls.key",
|
||||
CredentialName: "secret0",
|
||||
},
|
||||
}
|
||||
|
||||
ingressHTTPServer = &istiov1alpha3.Server{
|
||||
Hosts: []string{"host-tls.example.com"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "http-server",
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
}
|
||||
|
||||
ingressHTTPRedirectServer = &istiov1alpha3.Server{
|
||||
Hosts: []string{"*"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "http-server",
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
Tls: &istiov1alpha3.ServerTLSSettings{
|
||||
HttpsRedirect: true,
|
||||
},
|
||||
}
|
||||
|
||||
// The gateway server irrelevant to ingressTLS.
|
||||
irrelevantServer = &istiov1alpha3.Server{
|
||||
Hosts: []string{"host-tls.example.com", "host-tls.test-ns.svc.cluster.local"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "test:0",
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &istiov1alpha3.ServerTLSSettings{
|
||||
Mode: istiov1alpha3.ServerTLSSettings_SIMPLE,
|
||||
ServerCertificate: "tls.crt",
|
||||
PrivateKey: "tls.key",
|
||||
CredentialName: "other-secret",
|
||||
},
|
||||
}
|
||||
irrelevantServer1 = &istiov1alpha3.Server{
|
||||
Hosts: []string{"*"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "http-server",
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
}
|
||||
|
||||
deletionTime = metav1.NewTime(time.Unix(1e9, 0))
|
||||
)
|
||||
|
||||
func TestKIngressControllerConventions(t *testing.T) {
|
||||
fakeClient := kube.NewFakeClient()
|
||||
localKubeClient, client := fakeClient, fakeClient
|
||||
|
||||
options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true}
|
||||
|
||||
secretController := secret.NewController(localKubeClient, options.ClusterId)
|
||||
ingressController := NewController(localKubeClient, client, options, secretController)
|
||||
|
||||
testcases := map[string]func(*testing.T, common.KIngressController){
|
||||
"test convert HTTPRoute": testConvertHTTPRoute,
|
||||
}
|
||||
for name, tc := range testcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc(t, ingressController)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testConvertHTTPRoute(t *testing.T, c common.KIngressController) {
|
||||
testcases := []struct {
|
||||
description string
|
||||
input struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}
|
||||
expectNoError bool
|
||||
}{
|
||||
{
|
||||
description: "convertOptions is nil",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: nil,
|
||||
wrapperConfig: nil,
|
||||
},
|
||||
expectNoError: false,
|
||||
}, {
|
||||
description: "convertOptions is not nil but empty",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{},
|
||||
wrapperConfig: &common.WrapperConfig{
|
||||
Config: &config.Config{},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: false,
|
||||
}, {
|
||||
description: "valid httpRoute convention,invalid backend",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{
|
||||
IngressDomainCache: &common.IngressDomainCache{
|
||||
Valid: make(map[string]*common.IngressDomainBuilder),
|
||||
Invalid: make([]model.IngressDomain, 0),
|
||||
},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
VirtualServices: make(map[string]*common.WrapperVirtualService),
|
||||
Gateways: make(map[string]*common.WrapperGateway),
|
||||
IngressRouteCache: &common.IngressRouteCache{},
|
||||
HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute),
|
||||
},
|
||||
wrapperConfig: &common.WrapperConfig{Config: &config.Config{
|
||||
Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test1", "test2"},
|
||||
SecretName: "test",
|
||||
},
|
||||
}},
|
||||
}, AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: true,
|
||||
}, {
|
||||
description: "valid httpRoute convention,invalid split",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{
|
||||
IngressDomainCache: &common.IngressDomainCache{
|
||||
Valid: make(map[string]*common.IngressDomainBuilder),
|
||||
Invalid: make([]model.IngressDomain, 0),
|
||||
},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
VirtualServices: make(map[string]*common.WrapperVirtualService),
|
||||
Gateways: make(map[string]*common.WrapperGateway),
|
||||
IngressRouteCache: &common.IngressRouteCache{},
|
||||
HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute),
|
||||
},
|
||||
wrapperConfig: &common.WrapperConfig{Config: &config.Config{
|
||||
Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []ingress.IngressBackendSplit{{}},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test1", "test2"},
|
||||
SecretName: "test",
|
||||
},
|
||||
}},
|
||||
}, AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: true,
|
||||
},
|
||||
{
|
||||
description: "valid httpRoute convention, vaild ingress",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{
|
||||
IngressDomainCache: &common.IngressDomainCache{
|
||||
Valid: make(map[string]*common.IngressDomainBuilder),
|
||||
Invalid: make([]model.IngressDomain, 0),
|
||||
},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
VirtualServices: make(map[string]*common.WrapperVirtualService),
|
||||
Gateways: make(map[string]*common.WrapperGateway),
|
||||
IngressRouteCache: common.NewIngressRouteCache(),
|
||||
HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute),
|
||||
},
|
||||
wrapperConfig: &common.WrapperConfig{Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "host-tls-test",
|
||||
Namespace: testNS,
|
||||
},
|
||||
Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: testNS,
|
||||
ServiceName: "v1-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test1", "test2"},
|
||||
SecretName: "test",
|
||||
},
|
||||
}},
|
||||
}, AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: true,
|
||||
}, {
|
||||
description: "valid httpRoute convention, Spec Rule All open Ingress",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{
|
||||
IngressDomainCache: &common.IngressDomainCache{
|
||||
Valid: make(map[string]*common.IngressDomainBuilder),
|
||||
Invalid: make([]model.IngressDomain, 0),
|
||||
},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
VirtualServices: make(map[string]*common.WrapperVirtualService),
|
||||
Gateways: make(map[string]*common.WrapperGateway),
|
||||
IngressRouteCache: common.NewIngressRouteCache(),
|
||||
HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute),
|
||||
},
|
||||
wrapperConfig: &common.WrapperConfig{Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "host-kingress-all-open-test",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"hello.default",
|
||||
"hello.default.svc",
|
||||
"hello.default.svc.cluster.local",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Path: "/pet/",
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
AppendHeaders: map[string]string{
|
||||
"Knative-Serving-Namespace": "default",
|
||||
"Knative-Serving-Revision": "hello-00002",
|
||||
},
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "default",
|
||||
ServiceName: "hello-00002",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 90,
|
||||
}, {
|
||||
AppendHeaders: map[string]string{
|
||||
"Knative-Serving-Namespace": "default",
|
||||
"Knative-Serving-Revision": "hello-00001",
|
||||
},
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "default",
|
||||
ServiceName: "hello-00001",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 10,
|
||||
}},
|
||||
AppendHeaders: map[string]string{
|
||||
"ugh": "blah",
|
||||
},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityClusterLocal,
|
||||
}, {
|
||||
Hosts: []string{
|
||||
"hello.default.zwj.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
AppendHeaders: map[string]string{
|
||||
"Knative-Serving-Namespace": "default",
|
||||
"Knative-Serving-Revision": "hello-00002",
|
||||
},
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "default",
|
||||
ServiceName: "hello-00002",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 90,
|
||||
}, {
|
||||
AppendHeaders: map[string]string{
|
||||
"Knative-Serving-Namespace": "default",
|
||||
"Knative-Serving-Revision": "hello-00001",
|
||||
},
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "default",
|
||||
ServiceName: "hello-00001",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 10,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test1", "test2"},
|
||||
SecretName: "test",
|
||||
},
|
||||
}},
|
||||
}, AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
err := c.ConvertHTTPRoute(testcase.input.options, testcase.input.wrapperConfig)
|
||||
if err != nil {
|
||||
require.Equal(t, testcase.expectNoError, false)
|
||||
} else {
|
||||
require.Equal(t, testcase.expectNoError, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTLSSecretName(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input struct {
|
||||
host string
|
||||
tls []ingress.IngressTLS
|
||||
}
|
||||
expect string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
input: struct {
|
||||
host string
|
||||
tls []ingress.IngressTLS
|
||||
}{
|
||||
host: "",
|
||||
tls: nil,
|
||||
},
|
||||
expect: "",
|
||||
description: "both are nil",
|
||||
},
|
||||
{
|
||||
input: struct {
|
||||
host string
|
||||
tls []ingress.IngressTLS
|
||||
}{
|
||||
host: "test",
|
||||
tls: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test"},
|
||||
SecretName: "test-secret",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test1"},
|
||||
SecretName: "test1-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: "test-secret",
|
||||
description: "found secret name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
actual := extractTLSSecretName(testcase.input.host, testcase.input.tls)
|
||||
require.Equal(t, testcase.expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldProcessIngressUpdate(t *testing.T) {
|
||||
c := controller{
|
||||
options: common.Options{},
|
||||
ingresses: make(map[string]*ingress.Ingress),
|
||||
}
|
||||
ingress1 := &ingress.Ingress{
|
||||
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-1",
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "testNs",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
addAnnotations(ingress1, map[string]string{networking.IngressClassAnnotationKey: IstioIngressClassNametest})
|
||||
|
||||
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")
|
||||
}
|
||||
ingress4 := ingress1.DeepCopy()
|
||||
addAnnotations(ingress4, map[string]string{networking.IngressClassAnnotationKey: "fake-classname"})
|
||||
should, _ = c.shouldProcessIngressUpdate(ingress4)
|
||||
if should {
|
||||
t.Fatal("should be false")
|
||||
}
|
||||
//可能有坑,annotation更新可能会引起ingress资源的反复处理。
|
||||
|
||||
}
|
||||
|
||||
func addAnnotations(ing *ingress.Ingress, annos map[string]string) *ingress.Ingress {
|
||||
// UnionMaps(a, b) where value from b wins. Use annos for second arg.
|
||||
ing.ObjectMeta.Annotations = kmeta.UnionMaps(ing.ObjectMeta.Annotations, annos)
|
||||
return ing
|
||||
}
|
||||
|
||||
func TestCreateRuleKey(t *testing.T) {
|
||||
sep := "\n\n"
|
||||
wrapperHttpRoute := &common.WrapperHTTPRoute{
|
||||
Host: "higress.com",
|
||||
OriginPathType: common.Prefix,
|
||||
OriginPath: "/foo",
|
||||
}
|
||||
|
||||
annots := annotations.Annotations{
|
||||
buildHigressAnnotationKey(annotations.MatchMethod): "GET PUT",
|
||||
buildHigressAnnotationKey("exact-" + annotations.MatchHeader + "-abc"): "123",
|
||||
buildHigressAnnotationKey("prefix-" + annotations.MatchHeader + "-def"): "456",
|
||||
buildHigressAnnotationKey("exact-" + annotations.MatchQuery + "-region"): "beijing",
|
||||
buildHigressAnnotationKey("prefix-" + annotations.MatchQuery + "-user-id"): "user-",
|
||||
}
|
||||
expect := "higress.com-prefix-/foo" + sep + //host-pathType-path
|
||||
"GET PUT" + sep + // method
|
||||
"exact-abc\t123" + "\n" + "prefix-def\t456" + sep + // header
|
||||
"exact-region\tbeijing" + "\n" + "prefix-user-id\tuser-" + sep // params
|
||||
|
||||
key := createRuleKey(annots, wrapperHttpRoute.PathFormat())
|
||||
if diff := cmp.Diff(expect, key); diff != "" {
|
||||
|
||||
t.Errorf("CreateRuleKey() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func buildHigressAnnotationKey(key string) string {
|
||||
return annotations.HigressAnnotationsPrefix + "/" + key
|
||||
}
|
||||
19
pkg/ingress/kube/kingress/resources/doc.go
Normal file
19
pkg/ingress/kube/kingress/resources/doc.go
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2019 The Knative Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package resources holds simple functions for synthesizing child resources from
|
||||
// an Ingress resource and any relevant Ingress controller configuration.
|
||||
package resources
|
||||
148
pkg/ingress/kube/kingress/resources/virtual_service.go
Normal file
148
pkg/ingress/kube/kingress/resources/virtual_service.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
Copyright 2019 The Knative Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
istiov1alpha3 "istio.io/api/networking/v1alpha3"
|
||||
"knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
"knative.dev/pkg/network"
|
||||
)
|
||||
|
||||
func MakeVirtualServiceRoute(hosts sets.String, http *v1alpha1.HTTPIngressPath) *istiov1alpha3.HTTPRoute {
|
||||
matches := []*istiov1alpha3.HTTPMatchRequest{}
|
||||
// Deduplicate hosts to avoid excessive matches, which cause a combinatorial expansion in Istio
|
||||
|
||||
for _, host := range hosts.List() {
|
||||
matches = append(matches, makeMatch(host, http.Path, http.Headers))
|
||||
}
|
||||
|
||||
weights := []*istiov1alpha3.HTTPRouteDestination{}
|
||||
for _, split := range http.Splits {
|
||||
var h *istiov1alpha3.Headers
|
||||
if len(split.AppendHeaders) > 0 {
|
||||
h = &istiov1alpha3.Headers{
|
||||
Request: &istiov1alpha3.Headers_HeaderOperations{
|
||||
Set: split.AppendHeaders,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
weights = append(weights, &istiov1alpha3.HTTPRouteDestination{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: network.GetServiceHostname(
|
||||
split.ServiceName, split.ServiceNamespace),
|
||||
Port: &istiov1alpha3.PortSelector{
|
||||
Number: uint32(split.ServicePort.IntValue()),
|
||||
},
|
||||
},
|
||||
Weight: int32(split.Percent),
|
||||
Headers: h,
|
||||
})
|
||||
}
|
||||
|
||||
var h *istiov1alpha3.Headers
|
||||
if len(http.AppendHeaders) > 0 {
|
||||
h = &istiov1alpha3.Headers{
|
||||
Request: &istiov1alpha3.Headers_HeaderOperations{
|
||||
Set: http.AppendHeaders,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var rewrite *istiov1alpha3.HTTPRewrite
|
||||
if http.RewriteHost != "" {
|
||||
rewrite = &istiov1alpha3.HTTPRewrite{
|
||||
Authority: http.RewriteHost,
|
||||
}
|
||||
}
|
||||
|
||||
route := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{}, // Override default istio behaviour of retrying twice.
|
||||
Match: matches,
|
||||
Route: weights,
|
||||
Rewrite: rewrite,
|
||||
Headers: h,
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
// getDistinctHostPrefixes deduplicate a set of prefix matches. For example, the set {a, aabb} can be
|
||||
// reduced to {a}, as a prefix match on {a} accepts all the same inputs as {a, aabb}.
|
||||
func getDistinctHostPrefixes(hosts sets.String) sets.String {
|
||||
// First we sort the list. This ensures that we always process the smallest elements (which match against
|
||||
// the most patterns, as they are less specific) first.
|
||||
all := hosts.List()
|
||||
ns := sets.NewString()
|
||||
for _, h := range all {
|
||||
prefixExists := false
|
||||
h = hostPrefix(h)
|
||||
// For each element, check if any existing elements are a prefix. We only insert if none are
|
||||
// // For example, if we already have {a} and we are looking at "ab", we would not add it as it has a prefix of "a"
|
||||
for e := range ns {
|
||||
if strings.HasPrefix(h, e) {
|
||||
prefixExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !prefixExists {
|
||||
ns.Insert(h)
|
||||
}
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
func makeMatch(host, path string, headers map[string]v1alpha1.HeaderMatch) *istiov1alpha3.HTTPMatchRequest {
|
||||
match := &istiov1alpha3.HTTPMatchRequest{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
// Do not use Regex as Istio 1.4 or later has 100 bytes limitation.
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: host},
|
||||
},
|
||||
}
|
||||
// Empty path is considered match all path. We only need to consider path
|
||||
// when it's non-empty.
|
||||
if path != "" {
|
||||
match.Uri = &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: path},
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
match.Headers = map[string]*istiov1alpha3.StringMatch{
|
||||
k: {
|
||||
MatchType: &istiov1alpha3.StringMatch_Exact{
|
||||
Exact: v.Exact,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
// hostPrefix returns an host to match either host or host:<any port>.
|
||||
// For clusterLocalHost, it trims .svc.<local domain> from the host to match short host.
|
||||
func hostPrefix(host string) string {
|
||||
localDomainSuffix := ".svc." + network.GetClusterDomainName()
|
||||
if !strings.HasSuffix(host, localDomainSuffix) {
|
||||
return host
|
||||
}
|
||||
return strings.TrimSuffix(host, localDomainSuffix)
|
||||
}
|
||||
258
pkg/ingress/kube/kingress/resources/virtual_service_test.go
Normal file
258
pkg/ingress/kube/kingress/resources/virtual_service_test.go
Normal file
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
Copyright 2019 The Knative Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
istiov1alpha3 "istio.io/api/networking/v1alpha3"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
"knative.dev/pkg/system"
|
||||
_ "knative.dev/pkg/system/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultIngressRuleValue = &v1alpha1.HTTPIngressRuleValue{
|
||||
Paths: []v1alpha1.HTTPIngressPath{{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
Percent: 100,
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test",
|
||||
ServiceName: "test.svc.cluster.local",
|
||||
ServicePort: intstr.FromInt(8080),
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
defaultIngress = v1alpha1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ingress",
|
||||
Namespace: system.Namespace(),
|
||||
},
|
||||
Spec: v1alpha1.IngressSpec{Rules: []v1alpha1.IngressRule{{
|
||||
Hosts: []string{
|
||||
"test-route.test-ns.svc.cluster.local",
|
||||
},
|
||||
HTTP: defaultIngressRuleValue,
|
||||
}}},
|
||||
}
|
||||
defaultVSCmpOpts = protocmp.Transform()
|
||||
)
|
||||
|
||||
func TestMakeVirtualServiceRoute_RewriteHost(t *testing.T) {
|
||||
ingressPath := &v1alpha1.HTTPIngressPath{
|
||||
RewriteHost: "the.target.host",
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
Percent: 100,
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceName: "the-svc",
|
||||
ServiceNamespace: "the-ns",
|
||||
ServicePort: intstr.FromInt(8080),
|
||||
},
|
||||
}},
|
||||
}
|
||||
route := MakeVirtualServiceRoute(sets.NewString("a.vanity.url", "another.vanity.url"), ingressPath)
|
||||
expected := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{},
|
||||
Match: []*istiov1alpha3.HTTPMatchRequest{{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.vanity.url`},
|
||||
},
|
||||
}, {
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `another.vanity.url`},
|
||||
},
|
||||
}},
|
||||
Rewrite: &istiov1alpha3.HTTPRewrite{
|
||||
Authority: "the.target.host",
|
||||
},
|
||||
Route: []*istiov1alpha3.HTTPRouteDestination{{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "the-svc.the-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
Weight: 100,
|
||||
}},
|
||||
}
|
||||
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
|
||||
t.Error("Unexpected route (-want +got):", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// One active target.
|
||||
func TestMakeVirtualServiceRoute_Vanilla(t *testing.T) {
|
||||
ingressPath := &v1alpha1.HTTPIngressPath{
|
||||
Headers: map[string]v1alpha1.HeaderMatch{
|
||||
"my-header": {
|
||||
Exact: "my-header-value",
|
||||
},
|
||||
},
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test-ns",
|
||||
ServiceName: "revision-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}
|
||||
route := MakeVirtualServiceRoute(sets.NewString("a.com", "b.org"), ingressPath)
|
||||
expected := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{},
|
||||
Match: []*istiov1alpha3.HTTPMatchRequest{{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.com`},
|
||||
},
|
||||
Headers: map[string]*istiov1alpha3.StringMatch{
|
||||
"my-header": {
|
||||
MatchType: &istiov1alpha3.StringMatch_Exact{
|
||||
Exact: "my-header-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `b.org`},
|
||||
},
|
||||
Headers: map[string]*istiov1alpha3.StringMatch{
|
||||
"my-header": {
|
||||
MatchType: &istiov1alpha3.StringMatch_Exact{
|
||||
Exact: "my-header-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Route: []*istiov1alpha3.HTTPRouteDestination{{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "revision-service.test-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{Number: 80},
|
||||
},
|
||||
Weight: 100,
|
||||
}},
|
||||
}
|
||||
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
|
||||
t.Error("Unexpected route (-want +got):", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// One active target.
|
||||
func TestMakeVirtualServiceRoute_Internal(t *testing.T) {
|
||||
ingressPath := &v1alpha1.HTTPIngressPath{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test-ns",
|
||||
ServiceName: "revision-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}
|
||||
route := MakeVirtualServiceRoute(sets.NewString("a.default"), ingressPath)
|
||||
expected := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{},
|
||||
Match: []*istiov1alpha3.HTTPMatchRequest{{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.default`},
|
||||
},
|
||||
}},
|
||||
Route: []*istiov1alpha3.HTTPRouteDestination{{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "revision-service.test-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{Number: 80},
|
||||
},
|
||||
Weight: 100,
|
||||
}},
|
||||
}
|
||||
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
|
||||
t.Error("Unexpected route (-want +got):", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// Two active targets.
|
||||
func TestMakeVirtualServiceRoute_TwoTargets(t *testing.T) {
|
||||
ingressPath := &v1alpha1.HTTPIngressPath{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test-ns",
|
||||
ServiceName: "revision-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 90,
|
||||
}, {
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test-ns",
|
||||
ServiceName: "new-revision-service",
|
||||
ServicePort: intstr.FromInt(81),
|
||||
},
|
||||
Percent: 10,
|
||||
}},
|
||||
}
|
||||
route := MakeVirtualServiceRoute(sets.NewString("test.org"), ingressPath)
|
||||
expected := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{},
|
||||
Match: []*istiov1alpha3.HTTPMatchRequest{{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `test.org`},
|
||||
},
|
||||
}},
|
||||
Route: []*istiov1alpha3.HTTPRouteDestination{{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "revision-service.test-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{Number: 80},
|
||||
},
|
||||
Weight: 90,
|
||||
}, {
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "new-revision-service.test-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{Number: 81},
|
||||
},
|
||||
Weight: 10,
|
||||
}},
|
||||
}
|
||||
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
|
||||
t.Error("Unexpected route (-want +got):", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDistinctHostPrefixes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in sets.String
|
||||
out sets.String
|
||||
}{
|
||||
{"empty", sets.NewString(), sets.NewString()},
|
||||
{"single element", sets.NewString("a"), sets.NewString("a")},
|
||||
{"no overlap", sets.NewString("a", "b"), sets.NewString("a", "b")},
|
||||
{"overlap", sets.NewString("a", "ab", "abc"), sets.NewString("a")},
|
||||
{"multiple overlaps", sets.NewString("a", "ab", "abc", "xyz", "xy", "m"), sets.NewString("a", "xy", "m")},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := getDistinctHostPrefixes(tt.in)
|
||||
if !tt.out.Equal(got) {
|
||||
t.Fatalf("Expected %v, got %v", tt.out, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
127
pkg/ingress/kube/kingress/status.go
Normal file
127
pkg/ingress/kube/kingress/status.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 kingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
coreV1 "k8s.io/api/core/v1"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
kingressclient "knative.dev/networking/pkg/client/clientset/versioned"
|
||||
kingresslister "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
|
||||
|
||||
common2 "github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
// statusSyncer keeps the status IP in each Ingress resource updated
|
||||
type statusSyncer struct {
|
||||
client kingressclient.Interface
|
||||
controller *controller
|
||||
watchedNamespace string
|
||||
ingressLister kingresslister.IngressLister
|
||||
serviceLister listerv1.ServiceLister
|
||||
}
|
||||
|
||||
// newStatusSyncer creates a new instance
|
||||
func newStatusSyncer(localKubeClient, client kube.Client, controller *controller, namespace string) *statusSyncer {
|
||||
return &statusSyncer{
|
||||
client: client.KIngress(),
|
||||
controller: controller,
|
||||
watchedNamespace: namespace,
|
||||
ingressLister: client.KIngressInformer().Networking().V1alpha1().Ingresses().Lister(),
|
||||
serviceLister: localKubeClient.KubeInformer().Core().V1().Services().Lister(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statusSyncer) run(stopCh <-chan struct{}) {
|
||||
cache.WaitForCacheSync(stopCh, s.controller.HasSynced)
|
||||
|
||||
ticker := time.NewTicker(common2.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(common2.SvcLabelSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
IngressLog.Debugf("found number %d of svc", len(svcList))
|
||||
lbStatusList := common2.GetLbStatusList(svcList)
|
||||
return s.updateStatus(lbStatusList)
|
||||
}
|
||||
|
||||
func transportLoadBalancerIngress(status []coreV1.LoadBalancerIngress) []v1alpha1.LoadBalancerIngressStatus {
|
||||
var KnativeLBIngress []v1alpha1.LoadBalancerIngressStatus
|
||||
for _, addr := range status {
|
||||
KnativeIng := v1alpha1.LoadBalancerIngressStatus{
|
||||
IP: addr.IP,
|
||||
Domain: addr.Hostname,
|
||||
}
|
||||
KnativeLBIngress = append(KnativeLBIngress, KnativeIng)
|
||||
}
|
||||
return KnativeLBIngress
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
ingress.Status.MarkNetworkConfigured()
|
||||
KIngressStatus := transportLoadBalancerIngress(status)
|
||||
if ingress.Status.PublicLoadBalancer == nil || len(ingress.Status.PublicLoadBalancer.Ingress) != len(KIngressStatus) || reflect.DeepEqual(ingress.Status.PublicLoadBalancer.Ingress, KIngressStatus) {
|
||||
ingress.Status.ObservedGeneration = ingress.Generation
|
||||
ingress.Status.MarkLoadBalancerReady(KIngressStatus, KIngressStatus)
|
||||
IngressLog.Infof("Update Ingress %v/%v within cluster %s status", ingress.Namespace, ingress.Name, s.controller.options.ClusterId)
|
||||
}
|
||||
_, err = s.client.NetworkingV1alpha1().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
|
||||
}
|
||||
204
pkg/ingress/translation/translation.go
Normal file
204
pkg/ingress/translation/translation.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// 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 translation
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
ingressconfig "github.com/alibaba/higress/pkg/ingress/config"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
var (
|
||||
_ model.ConfigStoreCache = &IngressTranslation{}
|
||||
_ model.IngressStore = &IngressTranslation{}
|
||||
)
|
||||
|
||||
type IngressTranslation struct {
|
||||
ingressConfig *ingressconfig.IngressConfig
|
||||
kingressConfig *ingressconfig.KIngressConfig
|
||||
mutex sync.RWMutex
|
||||
higressRouteCache model.IngressRouteCollection
|
||||
higressDomainCache model.IngressDomainCollection
|
||||
}
|
||||
|
||||
func NewIngressTranslation(localKubeClient kube.Client, XDSUpdater model.XDSUpdater, namespace, clusterId string) *IngressTranslation {
|
||||
if clusterId == "Kubernetes" {
|
||||
clusterId = ""
|
||||
}
|
||||
Config := &IngressTranslation{
|
||||
ingressConfig: ingressconfig.NewIngressConfig(localKubeClient, XDSUpdater, namespace, clusterId),
|
||||
kingressConfig: ingressconfig.NewKIngressConfig(localKubeClient, XDSUpdater, namespace, clusterId),
|
||||
}
|
||||
return Config
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) AddLocalCluster(options common.Options) (common.IngressController, common.KIngressController) {
|
||||
if m.kingressConfig == nil {
|
||||
return m.ingressConfig.AddLocalCluster(options), nil
|
||||
}
|
||||
return m.ingressConfig.AddLocalCluster(options), m.kingressConfig.AddLocalCluster(options)
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) InitializeCluster(ingressController common.IngressController, kingressController common.KIngressController, stop <-chan struct{}) error {
|
||||
if err := m.ingressConfig.InitializeCluster(ingressController, stop); err != nil {
|
||||
return err
|
||||
}
|
||||
if kingressController == nil {
|
||||
return nil
|
||||
}
|
||||
if err := m.kingressConfig.InitializeCluster(kingressController, stop); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
|
||||
m.ingressConfig.RegisterEventHandler(kind, f)
|
||||
if m.kingressConfig != nil {
|
||||
m.kingressConfig.RegisterEventHandler(kind, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) HasSynced() bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
if !m.ingressConfig.HasSynced() {
|
||||
return false
|
||||
}
|
||||
if m.kingressConfig != nil {
|
||||
if !m.kingressConfig.HasSynced() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Run(stop <-chan struct{}) {
|
||||
go m.ingressConfig.Run(stop)
|
||||
if m.kingressConfig != nil {
|
||||
go m.kingressConfig.Run(stop)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) SetWatchErrorHandler(f func(r *cache.Reflector, err error)) error {
|
||||
m.ingressConfig.SetWatchErrorHandler(f)
|
||||
if m.kingressConfig != nil {
|
||||
m.kingressConfig.SetWatchErrorHandler(f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) GetIngressRoutes() model.IngressRouteCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
ingressRouteCache := m.ingressConfig.GetIngressRoutes()
|
||||
m.higressRouteCache = model.IngressRouteCollection{}
|
||||
m.higressRouteCache.Invalid = append(m.higressRouteCache.Invalid, ingressRouteCache.Invalid...)
|
||||
m.higressRouteCache.Valid = append(m.higressRouteCache.Valid, ingressRouteCache.Valid...)
|
||||
if m.kingressConfig != nil {
|
||||
kingressRouteCache := m.kingressConfig.GetIngressRoutes()
|
||||
m.higressRouteCache.Invalid = append(m.higressRouteCache.Invalid, kingressRouteCache.Invalid...)
|
||||
m.higressRouteCache.Valid = append(m.higressRouteCache.Valid, kingressRouteCache.Valid...)
|
||||
}
|
||||
|
||||
return m.higressRouteCache
|
||||
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) GetIngressDomains() model.IngressDomainCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
ingressDomainCache := m.ingressConfig.GetIngressDomains()
|
||||
|
||||
m.higressDomainCache = model.IngressDomainCollection{}
|
||||
m.higressDomainCache.Invalid = append(m.higressDomainCache.Invalid, ingressDomainCache.Invalid...)
|
||||
m.higressDomainCache.Valid = append(m.higressDomainCache.Valid, ingressDomainCache.Valid...)
|
||||
if m.kingressConfig != nil {
|
||||
kingressDomainCache := m.kingressConfig.GetIngressDomains()
|
||||
m.higressDomainCache.Invalid = append(m.higressDomainCache.Invalid, kingressDomainCache.Invalid...)
|
||||
m.higressDomainCache.Valid = append(m.higressDomainCache.Valid, kingressDomainCache.Valid...)
|
||||
}
|
||||
return m.higressDomainCache
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Schemas() collection.Schemas {
|
||||
return common.IngressIR
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Get(typ config.GroupVersionKind, name, namespace string) *config.Config {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) List(typ config.GroupVersionKind, namespace string) ([]config.Config, error) {
|
||||
if typ != gvk.Gateway &&
|
||||
typ != gvk.VirtualService &&
|
||||
typ != gvk.DestinationRule &&
|
||||
typ != gvk.EnvoyFilter &&
|
||||
typ != gvk.ServiceEntry &&
|
||||
typ != gvk.WasmPlugin {
|
||||
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
|
||||
}
|
||||
|
||||
ingressConfig, err := m.ingressConfig.List(typ, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var higressConfig []config.Config
|
||||
higressConfig = append(higressConfig, ingressConfig...)
|
||||
if m.kingressConfig != nil {
|
||||
kingressConfig, err := m.kingressConfig.List(typ, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
higressConfig = append(higressConfig, kingressConfig...)
|
||||
}
|
||||
return higressConfig, nil
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Create(config config.Config) (revision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Update(config config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) UpdateStatus(config config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Patch(orig config.Config, patchFn config.PatchFunc) (string, error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Delete(typ config.GroupVersionKind, name, namespace string, resourceVersion *string) error {
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
Reference in New Issue
Block a user