Move codes to pkg (#46)

This commit is contained in:
Yang
2022-11-09 20:37:40 +08:00
committed by GitHub
parent b09b68c1e0
commit ecba3a0265
54 changed files with 62 additions and 65 deletions

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"strings"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
gatewaytool "istio.io/istio/pkg/config/gateway"
listerv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
)
type ServiceKey struct {
Namespace string
Name string
Port int32
}
type WrapperConfig struct {
Config *config.Config
AnnotationsConfig *annotations.Ingress
}
type WrapperGateway struct {
Gateway *networking.Gateway
WrapperConfig *WrapperConfig
ClusterId string
Host string
}
func (w *WrapperGateway) IsHTTPS() bool {
if w.Gateway == nil || len(w.Gateway.Servers) == 0 {
return false
}
for _, server := range w.Gateway.Servers {
if gatewaytool.IsTLSServer(server) {
return true
}
}
return false
}
type WrapperHTTPRoute struct {
HTTPRoute *networking.HTTPRoute
WrapperConfig *WrapperConfig
RawClusterId string
ClusterId string
ClusterName string
Host string
OriginPath string
OriginPathType PathType
WeightTotal int32
IsDefaultBackend bool
}
func (w *WrapperHTTPRoute) Meta() string {
return strings.Join([]string{w.WrapperConfig.Config.Namespace, w.WrapperConfig.Config.Name}, "-")
}
func (w *WrapperHTTPRoute) BasePathFormat() string {
return strings.Join([]string{w.Host, w.OriginPath}, "-")
}
func (w *WrapperHTTPRoute) PathFormat() string {
return strings.Join([]string{w.Host, string(w.OriginPathType), w.OriginPath}, "-")
}
type WrapperVirtualService struct {
VirtualService *networking.VirtualService
WrapperConfig *WrapperConfig
ConfiguredDefaultBackend bool
AppRoot string
}
type WrapperTrafficPolicy struct {
TrafficPolicy *networking.TrafficPolicy_PortTrafficPolicy
WrapperConfig *WrapperConfig
}
type WrapperDestinationRule struct {
DestinationRule *networking.DestinationRule
WrapperConfig *WrapperConfig
ServiceKey ServiceKey
}
type IngressController interface {
// RegisterEventHandler adds a handler to receive config update events for a
// configuration type
RegisterEventHandler(kind config.GroupVersionKind, handler model.EventHandler)
List() []config.Config
ServiceLister() listerv1.ServiceLister
SecretLister() listerv1.SecretLister
ConvertGateway(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
ConvertHTTPRoute(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
ApplyDefaultBackend(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
ApplyCanaryIngress(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
ConvertTrafficPolicy(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
// Run until a signal is received
Run(stop <-chan struct{})
SetWatchErrorHandler(func(r *cache.Reflector, err error)) error
// HasSynced returns true after initial cache synchronization is complete
HasSynced() bool
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import "istio.io/pkg/monitoring"
type Event string
const (
Normal Event = "normal"
Unknown Event = "unknown"
EmptyRule Event = "empty-rule"
MissingSecret Event = "missing-secret"
InvalidBackendService Event = "invalid-backend-service"
DuplicatedRoute Event = "duplicated-route"
DuplicatedTls Event = "duplicated-tls"
PortNameResolveError Event = "port-name-resolve-error"
)
var (
clusterTag = monitoring.MustCreateLabel("cluster")
invalidType = monitoring.MustCreateLabel("type")
// totalIngresses tracks the total number of ingress
totalIngresses = monitoring.NewGauge(
"pilot_total_ingresses",
"Total ingresses known to pilot.",
monitoring.WithLabels(clusterTag),
)
totalInvalidIngress = monitoring.NewSum(
"pilot_total_invalid_ingresses",
"Total invalid ingresses known to pilot.",
monitoring.WithLabels(clusterTag, invalidType),
)
)
func init() {
monitoring.MustRegister(totalIngresses)
monitoring.MustRegister(totalInvalidIngress)
}
func RecordIngressNumber(cluster string, number int) {
totalIngresses.With(clusterTag.Value(cluster)).Record(float64(number))
}
func IncrementInvalidIngress(cluster string, event Event) {
totalInvalidIngress.With(clusterTag.Value(cluster), invalidType.Value(string(event))).Increment()
}

View File

@@ -0,0 +1,411 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/collection"
"istio.io/istio/pkg/config/schema/collections"
"k8s.io/apimachinery/pkg/labels"
. "github.com/alibaba/higress/pkg/ingress/log"
)
type PathType string
const (
prefixAnnotation = "internal.higress.io/"
ClusterIdAnnotation = prefixAnnotation + "cluster-id"
RawClusterIdAnnotation = prefixAnnotation + "raw-cluster-id"
HostAnnotation = prefixAnnotation + "host"
// PrefixMatchRegex optionally matches "/..." at the end of a path.
// regex taken from https://github.com/projectcontour/contour/blob/2b3376449bedfea7b8cea5fbade99fb64009c0f6/internal/envoy/v3/route.go#L59
PrefixMatchRegex = `((\/).*)?`
DefaultHost = "*"
DefaultPath = "/"
// DefaultIngressClass defines the default class used in the nginx ingress controller.
// For compatible ingress nginx case, istio controller will watch ingresses whose ingressClass is
// nginx, empty string or unset.
DefaultIngressClass = "nginx"
Exact PathType = "exact"
Prefix PathType = "prefix"
Regex PathType = "regex"
DefaultStatusUpdateInterval = 10 * time.Second
AppKey = "app"
AppValue = "higress-gateway"
SvcHostNameSuffix = ".multiplenic"
)
var (
ErrUnsupportedOp = errors.New("unsupported operation: the ingress config store is a read-only view")
ErrNotFound = errors.New("item not found")
Schemas = collection.SchemasFor(
collections.IstioNetworkingV1Alpha3Virtualservices,
collections.IstioNetworkingV1Alpha3Gateways,
collections.IstioNetworkingV1Alpha3Destinationrules,
collections.IstioNetworkingV1Alpha3Envoyfilters,
)
clusterPrefix string
SvcLabelSelector labels.Selector
)
func init() {
set := labels.Set{
AppKey: AppValue,
}
SvcLabelSelector = labels.SelectorFromSet(set)
}
type Options struct {
Enable bool
ClusterId string
IngressClass string
WatchNamespace string
RawClusterId string
EnableStatus bool
SystemNamespace string
GatewaySelectorKey string
GatewaySelectorValue string
}
type BasicAuthRules struct {
Rules []*Rule `json:"_rules_"`
}
type Rule struct {
Realm string `json:"realm"`
MatchRoute []string `json:"_match_route_"`
Credentials []string `json:"credentials"`
Encrypted bool `json:"encrypted"`
}
type IngressDomainCache struct {
// host as key
Valid map[string]*IngressDomainBuilder
Invalid []model.IngressDomain
}
func NewIngressDomainCache() *IngressDomainCache {
return &IngressDomainCache{
Valid: map[string]*IngressDomainBuilder{},
}
}
func (i *IngressDomainCache) Extract() model.IngressDomainCollection {
var valid []model.IngressDomain
for _, builder := range i.Valid {
valid = append(valid, builder.Build())
}
return model.IngressDomainCollection{
Valid: valid,
Invalid: i.Invalid,
}
}
type ConvertOptions struct {
HostWithRule2Ingress map[string]*config.Config
HostWithTls2Ingress map[string]*config.Config
Gateways map[string]*WrapperGateway
IngressDomainCache *IngressDomainCache
HostAndPath2Ingress map[string]*config.Config
// Record valid/invalid routes from ingress
IngressRouteCache *IngressRouteCache
VirtualServices map[string]*WrapperVirtualService
// host -> routes
HTTPRoutes map[string][]*WrapperHTTPRoute
CanaryIngresses []*WrapperConfig
Service2TrafficPolicy map[ServiceKey]*WrapperTrafficPolicy
HasDefaultBackend bool
}
// CreateOptions obtain options from cluster id.
// The cluster id format is k8sClusterId ingressClass watchNamespace EnableStatus, delimited by _.
func CreateOptions(clusterId cluster.ID) Options {
parts := strings.Split(clusterId.String(), "_")
// Old cluster key
if len(parts) < 3 {
out := Options{
RawClusterId: clusterId.String(),
}
if len(parts) > 0 {
out.ClusterId = parts[0]
}
return out
}
options := Options{
Enable: true,
ClusterId: parts[0],
IngressClass: parts[1],
WatchNamespace: parts[2],
RawClusterId: clusterId.String(),
// The status switch is enabled by default.
EnableStatus: true,
}
if len(parts) == 4 {
if enable, err := strconv.ParseBool(parts[3]); err == nil {
options.EnableStatus = enable
}
}
return options
}
type IngressRouteCache struct {
routes map[string]*IngressRouteBuilder
invalid []model.IngressRoute
}
func NewIngressRouteCache() *IngressRouteCache {
return &IngressRouteCache{
routes: map[string]*IngressRouteBuilder{},
}
}
func (i *IngressRouteCache) New(route *WrapperHTTPRoute) *IngressRouteBuilder {
return &IngressRouteBuilder{
ClusterId: route.ClusterId,
RouteName: route.HTTPRoute.Name,
Path: route.OriginPath,
PathType: string(route.OriginPathType),
Host: route.Host,
Event: Normal,
Ingress: route.WrapperConfig.Config,
}
}
func (i *IngressRouteCache) NewAndAdd(route *WrapperHTTPRoute) {
routeBuilder := &IngressRouteBuilder{
ClusterId: route.ClusterId,
RouteName: route.HTTPRoute.Name,
Path: route.OriginPath,
PathType: string(route.OriginPathType),
Host: route.Host,
Event: Normal,
Ingress: route.WrapperConfig.Config,
}
// Only care about the first destination
destination := route.HTTPRoute.Route[0].Destination
svcName, namespace, _ := SplitServiceFQDN(destination.Host)
routeBuilder.ServiceList = []model.BackendService{
{
Name: svcName,
Namespace: namespace,
Port: destination.Port.Number,
Weight: route.HTTPRoute.Route[0].Weight,
},
}
i.routes[route.HTTPRoute.Name] = routeBuilder
}
func (i *IngressRouteCache) Add(builder *IngressRouteBuilder) {
if builder.Event != Normal {
builder.RouteName = "invalid-route"
i.invalid = append(i.invalid, builder.Build())
return
}
i.routes[builder.RouteName] = builder
}
func (i *IngressRouteCache) Update(route *WrapperHTTPRoute) {
oldBuilder, exist := i.routes[route.HTTPRoute.Name]
if !exist {
// Never happen
IngressLog.Errorf("ingress route builder %s not found.", route.HTTPRoute.Name)
return
}
var serviceList []model.BackendService
for _, routeDestination := range route.HTTPRoute.Route {
serviceList = append(serviceList, ConvertBackendService(routeDestination))
}
oldBuilder.ServiceList = serviceList
}
func (i *IngressRouteCache) Delete(route *WrapperHTTPRoute) {
delete(i.routes, route.HTTPRoute.Name)
}
func (i *IngressRouteCache) Extract() model.IngressRouteCollection {
var valid []model.IngressRoute
for _, builder := range i.routes {
valid = append(valid, builder.Build())
}
return model.IngressRouteCollection{
Valid: valid,
Invalid: i.invalid,
}
}
type IngressRouteBuilder struct {
ClusterId string
RouteName string
Host string
PathType string
Path string
ServiceList []model.BackendService
PortName string
Event Event
Ingress *config.Config
PreIngress *config.Config
}
func (i *IngressRouteBuilder) Build() model.IngressRoute {
errorMsg := ""
switch i.Event {
case DuplicatedRoute:
preClusterId := GetClusterId(i.PreIngress.Annotations)
errorMsg = fmt.Sprintf("host %s and path %s in ingress %s/%s within cluster %s is already defined in ingress %s/%s within cluster %s",
i.Host,
i.Path,
i.Ingress.Namespace,
i.Ingress.Name,
i.ClusterId,
i.PreIngress.Namespace,
i.PreIngress.Name,
preClusterId)
case InvalidBackendService:
errorMsg = fmt.Sprintf("backend service of host %s and path %s is invalid defined in ingress %s/%s within cluster %s",
i.Host,
i.Path,
i.Ingress.Namespace,
i.Ingress.Name,
i.ClusterId,
)
case PortNameResolveError:
errorMsg = fmt.Sprintf("service port name %s of host %s and path %s resolves error defined in ingress %s/%s within cluster %s",
i.PortName,
i.Host,
i.Path,
i.Ingress.Namespace,
i.Ingress.Name,
i.ClusterId,
)
}
ingressRoute := model.IngressRoute{
Name: i.RouteName,
Host: i.Host,
Path: i.Path,
PathType: i.PathType,
DestinationType: model.Single,
ServiceList: i.ServiceList,
Error: errorMsg,
}
// backward compatibility
if len(ingressRoute.ServiceList) > 0 {
ingressRoute.ServiceName = i.ServiceList[0].Name
}
if len(ingressRoute.ServiceList) > 1 {
ingressRoute.DestinationType = model.Multiple
}
return ingressRoute
}
type Protocol string
const (
HTTP Protocol = "HTTP"
HTTPS Protocol = "HTTPS"
)
type IngressDomainBuilder struct {
ClusterId string
Host string
Protocol Protocol
Event Event
// format is cluster id/namespace/name
SecretName string
Ingress *config.Config
PreIngress *config.Config
}
func (i *IngressDomainBuilder) Build() model.IngressDomain {
errorMsg := ""
switch i.Event {
case MissingSecret:
errorMsg = fmt.Sprintf("tls field of host %s defined in ingress %s/%s within cluster %s misses secret",
i.Host,
i.Ingress.Namespace,
i.Ingress.Name,
i.ClusterId,
)
case DuplicatedTls:
preClusterId := GetClusterId(i.PreIngress.Annotations)
errorMsg = fmt.Sprintf("tls field of host %s defined in ingress %s/%s within cluster %s "+
"is conflicted with ingress %s/%s within cluster %s",
i.Host,
i.Ingress.Namespace,
i.Ingress.Name,
i.ClusterId,
i.PreIngress.Namespace,
i.PreIngress.Name,
preClusterId,
)
}
return model.IngressDomain{
Host: i.Host,
Protocol: string(i.Protocol),
SecretName: i.SecretName,
CreationTime: i.Ingress.CreationTimestamp,
Error: errorMsg,
}
}

View File

@@ -0,0 +1,365 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"crypto/md5"
"encoding/hex"
"net"
"sort"
"strings"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/kube"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/version"
. "github.com/alibaba/higress/pkg/ingress/log"
)
// V1Available check if the "networking/v1" Ingress is available.
func V1Available(client kube.Client) bool {
// check kubernetes version to use new ingress package or not
version119, _ := version.ParseGeneric("v1.19.0")
serverVersion, err := client.GetKubernetesVersion()
if err != nil {
return false
}
runningVersion, err := version.ParseGeneric(serverVersion.String())
if err != nil {
IngressLog.Errorf("unexpected error parsing running Kubernetes version: %v", err)
return false
}
return runningVersion.AtLeast(version119)
}
// NetworkingIngressAvailable check if the "networking" group Ingress is available.
func NetworkingIngressAvailable(client kube.Client) bool {
// check kubernetes version to use new ingress package or not
version118, _ := version.ParseGeneric("v1.18.0")
serverVersion, err := client.GetKubernetesVersion()
if err != nil {
return false
}
runningVersion, err := version.ParseGeneric(serverVersion.String())
if err != nil {
IngressLog.Errorf("unexpected error parsing running Kubernetes version: %v", err)
return false
}
return runningVersion.AtLeast(version118)
}
// SortIngressByCreationTime sorts the list of config objects in ascending order by their creation time (if available).
func SortIngressByCreationTime(configs []config.Config) {
sort.Slice(configs, func(i, j int) bool {
if configs[i].CreationTimestamp == configs[j].CreationTimestamp {
in := configs[i].Name + "." + configs[i].Namespace
jn := configs[j].Name + "." + configs[j].Namespace
return in < jn
}
return configs[i].CreationTimestamp.Before(configs[j].CreationTimestamp)
})
}
func CreateOrUpdateAnnotations(annotations map[string]string, options Options) map[string]string {
out := make(map[string]string, len(annotations))
for key, value := range annotations {
out[key] = value
}
out[ClusterIdAnnotation] = options.ClusterId
out[RawClusterIdAnnotation] = options.RawClusterId
return out
}
func GetClusterId(annotations map[string]string) string {
if len(annotations) == 0 {
return ""
}
if value, exist := annotations[ClusterIdAnnotation]; exist {
return value
}
return ""
}
func GetRawClusterId(annotations map[string]string) string {
if len(annotations) == 0 {
return ""
}
if value, exist := annotations[RawClusterIdAnnotation]; exist {
return value
}
return ""
}
func GetHost(annotations map[string]string) string {
if len(annotations) == 0 {
return ""
}
if value, exist := annotations[HostAnnotation]; exist {
return value
}
return ""
}
// CleanHost follow the format of mse-ops for host.
func CleanHost(host string) string {
if host == "*" {
return "global"
}
if strings.HasPrefix(host, "*") {
host = strings.ReplaceAll(host, "*", "global-")
}
return strings.ReplaceAll(host, ".", "-")
}
func CreateConvertedName(items ...string) string {
for i := len(items) - 1; i >= 0; i-- {
if items[i] == "" {
items = append(items[:i], items[i+1:]...)
}
}
return strings.Join(items, "-")
}
// SortHTTPRoutes sort routes base on path type and path length
func SortHTTPRoutes(routes []*WrapperHTTPRoute) {
isDefaultBackend := func(route *WrapperHTTPRoute) bool {
return route.IsDefaultBackend
}
isAllCatch := func(route *WrapperHTTPRoute) bool {
if route.OriginPathType == Prefix && route.OriginPath == "/" {
return true
}
return false
}
sort.SliceStable(routes, func(i, j int) bool {
// Move default backend to end
if isDefaultBackend(routes[i]) {
return false
}
if isDefaultBackend(routes[j]) {
return true
}
// Move user specified root path match to end
if isAllCatch(routes[i]) {
return false
}
if isAllCatch(routes[j]) {
return true
}
if routes[i].OriginPathType == routes[j].OriginPathType {
return len(routes[i].OriginPath) > len(routes[j].OriginPath)
}
if routes[i].OriginPathType == Exact {
return true
}
if routes[i].OriginPathType != Exact &&
routes[j].OriginPathType != Exact {
return routes[i].OriginPathType == Prefix
}
return false
})
}
func constructRouteName(route *WrapperHTTPRoute) string {
var builder strings.Builder
// host-pathType-path
base := route.PathFormat()
builder.WriteString(base)
var mappings []string
var headerMappings []string
var queryMappings []string
if len(route.HTTPRoute.Match) > 0 {
match := route.HTTPRoute.Match[0]
if len(match.Headers) != 0 {
for k, v := range match.Headers {
var mapping string
switch v.GetMatchType().(type) {
case *networking.StringMatch_Exact:
mapping = CreateConvertedName("exact", k, v.GetExact())
case *networking.StringMatch_Prefix:
mapping = CreateConvertedName("prefix", k, v.GetPrefix())
case *networking.StringMatch_Regex:
mapping = CreateConvertedName("regex", k, v.GetRegex())
}
headerMappings = append(headerMappings, mapping)
}
sort.SliceStable(headerMappings, func(i, j int) bool {
return headerMappings[i] < headerMappings[j]
})
}
if len(match.QueryParams) != 0 {
for k, v := range match.QueryParams {
var mapping string
switch v.GetMatchType().(type) {
case *networking.StringMatch_Exact:
mapping = strings.Join([]string{"exact", k, v.GetExact()}, ":")
case *networking.StringMatch_Prefix:
mapping = strings.Join([]string{"prefix", k, v.GetPrefix()}, ":")
case *networking.StringMatch_Regex:
mapping = strings.Join([]string{"regex", k, v.GetRegex()}, ":")
}
queryMappings = append(queryMappings, mapping)
}
sort.SliceStable(queryMappings, func(i, j int) bool {
return queryMappings[i] < queryMappings[j]
})
}
}
mappings = append(mappings, headerMappings...)
mappings = append(mappings, queryMappings...)
if len(mappings) == 0 {
return CreateConvertedName(base)
}
return CreateConvertedName(base, CreateConvertedName(mappings...))
}
func partMd5(raw string) string {
hash := md5.Sum([]byte(raw))
encoded := hex.EncodeToString(hash[:])
return encoded[:4] + encoded[len(encoded)-4:]
}
func GenerateUniqueRouteName(route *WrapperHTTPRoute) string {
raw := constructRouteName(route)
// meta-part-clusterId
// meta: ingressNamespace-ingressName
meta := route.Meta()
// host-pathType-path-header-queryParam, md5, then before 4 char and end 4 char
part := partMd5(raw)
routeName := CreateConvertedName(meta, part, route.ClusterId)
if route.WrapperConfig.AnnotationsConfig.IsCanary() {
return routeName + "-canary"
}
return routeName
}
func GenerateUniqueRouteNameWithSuffix(route *WrapperHTTPRoute, suffix string) string {
raw := constructRouteName(route)
// meta-part-clusterId
// meta: ingressNamespace-ingressName
meta := route.Meta()
// host-pathType-path-header-queryParam, md5, then before 4 char and end 4 char
part := partMd5(raw)
return CreateConvertedName(meta, part, route.ClusterId, suffix)
}
func SplitServiceFQDN(fqdn string) (string, string, bool) {
parts := strings.Split(fqdn, ".")
if len(parts) > 1 {
return parts[0], parts[1], true
}
return "", "", false
}
func ConvertBackendService(routeDestination *networking.HTTPRouteDestination) model.BackendService {
parts := strings.Split(routeDestination.Destination.Host, ".")
return model.BackendService{
Namespace: parts[1],
Name: parts[0],
Port: routeDestination.Destination.Port.Number,
Weight: routeDestination.Weight,
}
}
func getLoadBalancerIp(svc *v1.Service) []string {
var out []string
for _, ingress := range svc.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
out = append(out, ingress.IP)
}
if ingress.Hostname != "" {
hostName := strings.TrimSuffix(ingress.Hostname, SvcHostNameSuffix)
if net.ParseIP(hostName) != nil {
out = append(out, hostName)
}
}
}
return out
}
func getSvcIpList(svcList []*v1.Service) []string {
var targetSvcList []*v1.Service
for _, svc := range svcList {
if svc.Spec.Type == v1.ServiceTypeLoadBalancer &&
strings.HasPrefix(svc.Name, clusterPrefix) {
targetSvcList = append(targetSvcList, svc)
}
}
var out []string
for _, svc := range targetSvcList {
out = append(out, getLoadBalancerIp(svc)...)
}
return out
}
func SortLbIngressList(lbi []v1.LoadBalancerIngress) func(int, int) bool {
return func(i int, j int) bool {
return lbi[i].IP < lbi[j].IP
}
}
func GetLbStatusList(svcList []*v1.Service) []v1.LoadBalancerIngress {
svcIpList := getSvcIpList(svcList)
lbi := make([]v1.LoadBalancerIngress, 0, len(svcIpList))
for _, ep := range svcIpList {
lbi = append(lbi, v1.LoadBalancerIngress{IP: ep})
}
sort.SliceStable(lbi, SortLbIngressList(lbi))
return lbi
}

View File

@@ -0,0 +1,473 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"testing"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
)
func TestConstructRouteName(t *testing.T) {
testCases := []struct {
input *WrapperHTTPRoute
expect string
}{
{
input: &WrapperHTTPRoute{
Host: "test.com",
OriginPathType: Exact,
OriginPath: "/test",
HTTPRoute: &networking.HTTPRoute{},
},
expect: "test.com-exact-/test",
},
{
input: &WrapperHTTPRoute{
Host: "*.test.com",
OriginPathType: Regex,
OriginPath: "/test/(.*)/?[0-9]",
HTTPRoute: &networking.HTTPRoute{},
},
expect: "*.test.com-regex-/test/(.*)/?[0-9]",
},
{
input: &WrapperHTTPRoute{
Host: "test.com",
OriginPathType: Exact,
OriginPath: "/test",
HTTPRoute: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
Headers: map[string]*networking.StringMatch{
"b": {
MatchType: &networking.StringMatch_Regex{
Regex: "a?c.*",
},
},
"a": {
MatchType: &networking.StringMatch_Exact{
Exact: "hello",
},
},
},
},
},
},
},
expect: "test.com-exact-/test-exact-a-hello-regex-b-a?c.*",
},
{
input: &WrapperHTTPRoute{
Host: "test.com",
OriginPathType: Prefix,
OriginPath: "/test",
HTTPRoute: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
QueryParams: map[string]*networking.StringMatch{
"b": {
MatchType: &networking.StringMatch_Regex{
Regex: "a?c.*",
},
},
"a": {
MatchType: &networking.StringMatch_Exact{
Exact: "hello",
},
},
},
},
},
},
},
expect: "test.com-prefix-/test-exact:a:hello-regex:b:a?c.*",
},
{
input: &WrapperHTTPRoute{
Host: "test.com",
OriginPathType: Prefix,
OriginPath: "/test",
HTTPRoute: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
Headers: map[string]*networking.StringMatch{
"f": {
MatchType: &networking.StringMatch_Regex{
Regex: "abc?",
},
},
"e": {
MatchType: &networking.StringMatch_Exact{
Exact: "bye",
},
},
},
QueryParams: map[string]*networking.StringMatch{
"b": {
MatchType: &networking.StringMatch_Regex{
Regex: "a?c.*",
},
},
"a": {
MatchType: &networking.StringMatch_Exact{
Exact: "hello",
},
},
},
},
},
},
},
expect: "test.com-prefix-/test-exact-e-bye-regex-f-abc?-exact:a:hello-regex:b:a?c.*",
},
}
for _, c := range testCases {
t.Run("", func(t *testing.T) {
out := constructRouteName(c.input)
if out != c.expect {
t.Fatalf("Expect %s, but is %s", c.expect, out)
}
})
}
}
func TestGenerateUniqueRouteName(t *testing.T) {
inputWithoutCanary := &WrapperHTTPRoute{
WrapperConfig: &WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "bar",
},
},
AnnotationsConfig: &annotations.Ingress{},
},
Host: "test.com",
OriginPathType: Prefix,
OriginPath: "/test",
ClusterId: "cluster1",
HTTPRoute: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
Headers: map[string]*networking.StringMatch{
"f": {
MatchType: &networking.StringMatch_Regex{
Regex: "abc?",
},
},
"e": {
MatchType: &networking.StringMatch_Exact{
Exact: "bye",
},
},
},
QueryParams: map[string]*networking.StringMatch{
"b": {
MatchType: &networking.StringMatch_Regex{
Regex: "a?c.*",
},
},
"a": {
MatchType: &networking.StringMatch_Exact{
Exact: "hello",
},
},
},
},
},
},
}
withoutCanary := GenerateUniqueRouteName(inputWithoutCanary)
t.Log(withoutCanary)
inputWithCanary := &WrapperHTTPRoute{
WrapperConfig: &WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "bar",
},
},
AnnotationsConfig: &annotations.Ingress{
Canary: &annotations.CanaryConfig{
Enabled: true,
},
},
},
Host: "test.com",
OriginPathType: Prefix,
OriginPath: "/test",
ClusterId: "cluster1",
HTTPRoute: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
Headers: map[string]*networking.StringMatch{
"f": {
MatchType: &networking.StringMatch_Regex{
Regex: "abc?",
},
},
"e": {
MatchType: &networking.StringMatch_Exact{
Exact: "bye",
},
},
},
QueryParams: map[string]*networking.StringMatch{
"b": {
MatchType: &networking.StringMatch_Regex{
Regex: "a?c.*",
},
},
"a": {
MatchType: &networking.StringMatch_Exact{
Exact: "hello",
},
},
},
},
},
},
}
withCanary := GenerateUniqueRouteName(inputWithCanary)
t.Log(withCanary)
if withCanary != withoutCanary+"-canary" {
t.Fatalf("Expect %s, but actual is %s", withCanary, withoutCanary+"-canary")
}
}
func TestGetLbStatusList(t *testing.T) {
clusterPrefix = "gw-123-"
svcName := clusterPrefix
svcList := []*v1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
IP: "2.2.2.2",
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: "1.1.1.1" + SvcHostNameSuffix,
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
Hostname: "4.4.4.4" + SvcHostNameSuffix,
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
IP: "3.3.3.3",
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeClusterIP,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{
IP: "5.5.5.5",
},
},
},
},
},
}
lbiList := GetLbStatusList(svcList)
if len(lbiList) != 4 {
t.Fatal("len should be 4")
}
if lbiList[0].IP != "1.1.1.1" {
t.Fatal("should be 1.1.1.1")
}
if lbiList[3].IP != "4.4.4.4" {
t.Fatal("should be 4.4.4.4")
}
}
func TestSortRoutes(t *testing.T) {
input := []*WrapperHTTPRoute{
{
WrapperConfig: &WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "bar",
},
},
AnnotationsConfig: &annotations.Ingress{},
},
Host: "test.com",
OriginPathType: Prefix,
OriginPath: "/",
ClusterId: "cluster1",
HTTPRoute: &networking.HTTPRoute{
Name: "test-1",
},
},
{
WrapperConfig: &WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "bar",
},
},
AnnotationsConfig: &annotations.Ingress{},
},
Host: "test.com",
OriginPathType: Prefix,
OriginPath: "/a",
ClusterId: "cluster1",
HTTPRoute: &networking.HTTPRoute{
Name: "test-2",
},
},
{
WrapperConfig: &WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "bar",
},
},
AnnotationsConfig: &annotations.Ingress{},
},
HTTPRoute: &networking.HTTPRoute{
Name: "test-3",
},
IsDefaultBackend: true,
},
{
WrapperConfig: &WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "bar",
},
},
AnnotationsConfig: &annotations.Ingress{},
},
Host: "test.com",
OriginPathType: Exact,
OriginPath: "/b",
ClusterId: "cluster1",
HTTPRoute: &networking.HTTPRoute{
Name: "test-4",
},
},
{
WrapperConfig: &WrapperConfig{
Config: &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "bar",
},
},
AnnotationsConfig: &annotations.Ingress{},
},
Host: "test.com",
OriginPathType: Regex,
OriginPath: "/d(.*)",
ClusterId: "cluster1",
HTTPRoute: &networking.HTTPRoute{
Name: "test-5",
},
},
}
SortHTTPRoutes(input)
if (input[0].HTTPRoute.Name) != "test-4" {
t.Fatal("should be test-4")
}
if (input[1].HTTPRoute.Name) != "test-2" {
t.Fatal("should be test-2")
}
if (input[2].HTTPRoute.Name) != "test-5" {
t.Fatal("should be test-5")
}
if (input[3].HTTPRoute.Name) != "test-1" {
t.Fatal("should be test-1")
}
if (input[4].HTTPRoute.Name) != "test-3" {
t.Fatal("should be test-3")
}
}