pkg/ingress/kube/common/tool.go (329 lines of code) (raw):
// 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/cluster"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/kube"
v1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
"k8s.io/apimachinery/pkg/util/version"
netv1 "github.com/alibaba/higress/client/pkg/apis/networking/v1"
. "github.com/alibaba/higress/pkg/ingress/log"
)
func ValidateBackendResource(resource *v1.TypedLocalObjectReference) bool {
if resource == nil || resource.APIGroup == nil ||
*resource.APIGroup != netv1.SchemeGroupVersion.Group ||
resource.Kind != "McpBridge" || resource.Name != "default" {
return false
}
return true
}
// 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 {
// Consider the new ingress package is available as default
return true
}
runningVersion, err := version.ParseGeneric(serverVersion.String())
if err != nil {
// Consider the new ingress package is available as default
IngressLog.Errorf("unexpected error parsing running Kubernetes version: %v", err)
return true
}
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.String()
out[RawClusterIdAnnotation] = options.RawClusterId
return out
}
func GetClusterId(annotations map[string]string) cluster.ID {
if len(annotations) == 0 {
return ""
}
if value, exist := annotations[ClusterIdAnnotation]; exist {
return cluster.ID(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 ""
}
// Istio requires that the name of the gateway must conform to the DNS label.
// For details, you can view: https://github.com/istio/istio/blob/2d5c40ad5e9cceebe64106005aa38381097da2ba/pkg/config/validation/validation.go#L478
func convertToDNSLabelValid(input string) string {
hasher := md5.New()
hasher.Write([]byte(input))
hash := hasher.Sum(nil)
return hex.EncodeToString(hash[4:12])
}
// CleanHost follow the format of mse-ops for host.
func CleanHost(host string) string {
return convertToDNSLabelValid(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 == "/" {
if route.HTTPRoute.Match == nil {
return true
}
match := route.HTTPRoute.Match[0]
if len(match.Headers) == 0 && len(match.QueryParams) == 0 && match.Method == nil {
return true
}
}
return false
}
// default backend,user specified root path => path type => path length =>
// methods => header => query param
// refer https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec
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 {
if in, jn := len(routes[i].OriginPath), len(routes[j].OriginPath); in != jn {
return in > jn
}
match1, match2 := routes[i].HTTPRoute.Match[0], routes[j].HTTPRoute.Match[0]
// methods
if in, jn := len(match1.Method.GetRegex()), len(match2.Method.GetRegex()); in != jn {
if in != 0 && jn != 0 {
return in < jn
}
return in != 0
}
// headers
if in, jn := len(match1.Headers), len(match2.Headers); in != jn {
return in > jn
}
// query params
if in, jn := len(match1.QueryParams), len(match2.QueryParams); in != jn {
return in > jn
}
return false
}
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(defaultNs string, route *WrapperHTTPRoute) string {
if route.WrapperConfig.Config.Namespace == defaultNs {
return route.WrapperConfig.Config.Name
}
return route.Meta()
}
func GenerateUniqueRouteNameWithSuffix(defaultNs string, route *WrapperHTTPRoute, suffix string) string {
return CreateConvertedName(GenerateUniqueRouteName(defaultNs, route), 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, ".")
service := model.BackendService{
Namespace: parts[1],
Name: parts[0],
Weight: routeDestination.Weight,
}
if routeDestination.Destination.Port != nil {
service.Port = routeDestination.Destination.Port.Number
}
return service
}
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
}
func SortLbIngressListV1(lbi []networkingv1.IngressLoadBalancerIngress) func(int, int) bool {
return func(i int, j int) bool {
return lbi[i].IP < lbi[j].IP
}
}
func GetLbStatusListV1(svcList []*v1.Service) []networkingv1.IngressLoadBalancerIngress {
svcIpList := getSvcIpList(svcList)
lbi := make([]networkingv1.IngressLoadBalancerIngress, 0, len(svcIpList))
for _, ep := range svcIpList {
lbi = append(lbi, networkingv1.IngressLoadBalancerIngress{IP: ep})
}
sort.SliceStable(lbi, SortLbIngressListV1(lbi))
return lbi
}
func SortLbIngressListV1Beta1(lbi []networkingv1beta1.IngressLoadBalancerIngress) func(int, int) bool {
return func(i int, j int) bool {
return lbi[i].IP < lbi[j].IP
}
}
func GetLbStatusListV1Beta1(svcList []*v1.Service) []networkingv1beta1.IngressLoadBalancerIngress {
svcIpList := getSvcIpList(svcList)
lbi := make([]networkingv1beta1.IngressLoadBalancerIngress, 0, len(svcIpList))
for _, ep := range svcIpList {
lbi = append(lbi, networkingv1beta1.IngressLoadBalancerIngress{IP: ep})
}
sort.SliceStable(lbi, SortLbIngressListV1Beta1(lbi))
return lbi
}