feat: support KnativeIngress (#524)

This commit is contained in:
Vikizhao
2023-09-22 09:32:02 +08:00
committed by GitHub
parent fab734d39a
commit 2da1c62c69
16 changed files with 3431 additions and 218 deletions

View File

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

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

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

View 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

View 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)
}

View 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)
}
})
}
}

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