Files
higress/pkg/ingress/kube/common/tool_test.go

1071 lines
23 KiB
Go

// 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/pilot/pkg/model"
"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"
"github.com/stretchr/testify/assert"
)
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: PrefixRegex,
OriginPath: "/test/(.*)/?[0-9]",
HTTPRoute: &networking.HTTPRoute{},
},
expect: "*.test.com-prefixRegex-/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) {
input := &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",
},
},
},
},
},
},
}
assert.Equal(t, "bar/foo", GenerateUniqueRouteName("xxx", input))
assert.Equal(t, "foo", GenerateUniqueRouteName("bar", input))
}
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: PrefixRegex,
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")
}
}
// TestSortHTTPRoutesWithMoreRules include headers, query params, methods
func TestSortHTTPRoutesWithMoreRules(t *testing.T) {
input := []struct {
order string
pathType PathType
path string
method *networking.StringMatch
header map[string]*networking.StringMatch
queryParam map[string]*networking.StringMatch
}{
{
order: "1",
pathType: Exact,
path: "/bar",
},
{
order: "2",
pathType: Prefix,
path: "/bar",
},
{
order: "3",
pathType: Prefix,
path: "/bar",
method: &networking.StringMatch{
MatchType: &networking.StringMatch_Regex{Regex: "GET|PUT"},
},
},
{
order: "4",
pathType: Prefix,
path: "/bar",
method: &networking.StringMatch{
MatchType: &networking.StringMatch_Regex{Regex: "GET"},
},
},
{
order: "5",
pathType: Prefix,
path: "/bar",
header: map[string]*networking.StringMatch{
"foo": {
MatchType: &networking.StringMatch_Exact{Exact: "bar"},
},
},
},
{
order: "6",
pathType: Prefix,
path: "/bar",
header: map[string]*networking.StringMatch{
"foo": {
MatchType: &networking.StringMatch_Exact{Exact: "bar"},
},
"bar": {
MatchType: &networking.StringMatch_Exact{Exact: "foo"},
},
},
},
{
order: "7",
pathType: Prefix,
path: "/bar",
queryParam: map[string]*networking.StringMatch{
"foo": {
MatchType: &networking.StringMatch_Exact{Exact: "bar"},
},
},
},
{
order: "8",
pathType: Prefix,
path: "/bar",
queryParam: map[string]*networking.StringMatch{
"foo": {
MatchType: &networking.StringMatch_Exact{Exact: "bar"},
},
"bar": {
MatchType: &networking.StringMatch_Exact{Exact: "foo"},
},
},
},
{
order: "9",
pathType: Prefix,
path: "/bar",
method: &networking.StringMatch{
MatchType: &networking.StringMatch_Regex{Regex: "GET"},
},
queryParam: map[string]*networking.StringMatch{
"foo": {
MatchType: &networking.StringMatch_Exact{Exact: "bar"},
},
},
},
{
order: "10",
pathType: Prefix,
path: "/bar",
method: &networking.StringMatch{
MatchType: &networking.StringMatch_Regex{Regex: "GET"},
},
queryParam: map[string]*networking.StringMatch{
"bar": {
MatchType: &networking.StringMatch_Exact{Exact: "foo"},
},
},
},
}
origin := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}
expect := []string{"1", "9", "10", "4", "3", "6", "5", "8", "7", "2"}
var list []*WrapperHTTPRoute
for idx, val := range input {
list = append(list, &WrapperHTTPRoute{
OriginPath: val.path,
OriginPathType: val.pathType,
HTTPRoute: &networking.HTTPRoute{
Name: origin[idx],
Match: []*networking.HTTPMatchRequest{
{
Method: val.method,
Headers: val.header,
QueryParams: val.queryParam,
},
},
},
})
}
SortHTTPRoutes(list)
for idx, val := range list {
if val.HTTPRoute.Name != expect[idx] {
t.Fatalf("should be %s, but got %s", expect[idx], val.HTTPRoute.Name)
}
}
}
func TestValidateBackendResource(t *testing.T) {
groupStr := "networking.higress.io"
testCases := []struct {
name string
resource *v1.TypedLocalObjectReference
expected bool
}{
{
name: "nil resource",
resource: nil,
expected: false,
},
{
name: "nil APIGroup",
resource: &v1.TypedLocalObjectReference{
APIGroup: nil,
Kind: "McpBridge",
Name: "default",
},
expected: false,
},
{
name: "wrong APIGroup",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "McpBridge",
Name: "wrong-name",
},
expected: false,
},
{
name: "wrong Kind",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "WrongKind",
Name: "default",
},
expected: false,
},
{
name: "valid resource",
resource: &v1.TypedLocalObjectReference{
APIGroup: &groupStr,
Kind: "McpBridge",
Name: "default",
},
expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ValidateBackendResource(tc.resource)
assert.Equal(t, tc.expected, result)
})
}
}
func TestCreateOrUpdateAnnotations(t *testing.T) {
testCases := []struct {
name string
annotations map[string]string
options Options
expected map[string]string
}{
{
name: "empty annotations",
annotations: map[string]string{},
options: Options{
ClusterId: "test-cluster",
RawClusterId: "raw-test-cluster",
},
expected: map[string]string{
ClusterIdAnnotation: "test-cluster",
RawClusterIdAnnotation: "raw-test-cluster",
},
},
{
name: "existing annotations",
annotations: map[string]string{
"key1": "value1",
"key2": "value2",
},
options: Options{
ClusterId: "test-cluster",
RawClusterId: "raw-test-cluster",
},
expected: map[string]string{
"key1": "value1",
"key2": "value2",
ClusterIdAnnotation: "test-cluster",
RawClusterIdAnnotation: "raw-test-cluster",
},
},
{
name: "overwrite existing cluster annotations",
annotations: map[string]string{
ClusterIdAnnotation: "old-cluster",
RawClusterIdAnnotation: "old-raw-cluster",
"key1": "value1",
},
options: Options{
ClusterId: "new-cluster",
RawClusterId: "new-raw-cluster",
},
expected: map[string]string{
ClusterIdAnnotation: "new-cluster",
RawClusterIdAnnotation: "new-raw-cluster",
"key1": "value1",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := CreateOrUpdateAnnotations(tc.annotations, tc.options)
assert.Equal(t, tc.expected, result)
})
}
}
func TestGetClusterId(t *testing.T) {
testCases := []struct {
name string
annotations map[string]string
expected string
}{
{
name: "nil annotations",
annotations: nil,
expected: "",
},
{
name: "empty annotations",
annotations: map[string]string{},
expected: "",
},
{
name: "with cluster id",
annotations: map[string]string{
ClusterIdAnnotation: "test-cluster",
},
expected: "test-cluster",
},
{
name: "with other annotations",
annotations: map[string]string{
"key1": "value1",
"key2": "value2",
},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := GetClusterId(tc.annotations)
assert.Equal(t, tc.expected, string(result))
})
}
}
func TestConvertToDNSLabelValidAndCleanHost(t *testing.T) {
testCases := []struct {
name string
input string
}{
{
name: "simple host",
input: "example.com",
},
{
name: "wildcard host",
input: "*.example.com",
},
{
name: "long host",
input: "very-long-subdomain.example-service.my-namespace.svc.cluster.local",
},
{
name: "empty host",
input: "",
},
{
name: "ip address",
input: "192.168.1.1",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test internal convertToDNSLabelValid function (through CleanHost)
result := CleanHost(tc.input)
// Validate result
assert.NotEmpty(t, result)
assert.Equal(t, 16, len(result)) // MD5 hash format is fixed length of 16 bytes
// Consistency check - same input should produce same output
result2 := CleanHost(tc.input)
assert.Equal(t, result, result2)
})
}
}
func TestSplitServiceFQDN(t *testing.T) {
testCases := []struct {
name string
fqdn string
expectedSvc string
expectedNs string
expectedValid bool
}{
{
name: "simple fqdn",
fqdn: "service.namespace",
expectedSvc: "service",
expectedNs: "namespace",
expectedValid: true,
},
{
name: "full k8s fqdn",
fqdn: "service.namespace.svc.cluster.local",
expectedSvc: "service",
expectedNs: "namespace",
expectedValid: true,
},
{
name: "just service name",
fqdn: "service",
expectedSvc: "",
expectedNs: "",
expectedValid: false,
},
{
name: "empty string",
fqdn: "",
expectedSvc: "",
expectedNs: "",
expectedValid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
svc, ns, valid := SplitServiceFQDN(tc.fqdn)
assert.Equal(t, tc.expectedSvc, svc)
assert.Equal(t, tc.expectedNs, ns)
assert.Equal(t, tc.expectedValid, valid)
})
}
}
func TestConvertBackendService(t *testing.T) {
testCases := []struct {
name string
dest *networking.HTTPRouteDestination
expected model.BackendService
}{
{
name: "simple service",
dest: &networking.HTTPRouteDestination{
Destination: &networking.Destination{
Host: "service.namespace",
Port: &networking.PortSelector{
Number: 80,
},
},
Weight: 100,
},
expected: model.BackendService{
Name: "service",
Namespace: "namespace",
Port: 80,
Weight: 100,
},
},
{
name: "full k8s FQDN",
dest: &networking.HTTPRouteDestination{
Destination: &networking.Destination{
Host: "service.namespace.svc.cluster.local",
Port: &networking.PortSelector{
Number: 8080,
},
},
Weight: 50,
},
expected: model.BackendService{
Name: "service",
Namespace: "namespace",
Port: 8080,
Weight: 50,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ConvertBackendService(tc.dest)
assert.Equal(t, tc.expected.Name, result.Name)
assert.Equal(t, tc.expected.Namespace, result.Namespace)
assert.Equal(t, tc.expected.Port, result.Port)
assert.Equal(t, tc.expected.Weight, result.Weight)
})
}
}
func TestCreateConvertedName(t *testing.T) {
testCases := []struct {
name string
items []string
expected string
}{
{
name: "empty slice",
items: []string{},
expected: "",
},
{
name: "single item",
items: []string{"example"},
expected: "example",
},
{
name: "multiple items",
items: []string{"part1", "part2", "part3"},
expected: "part1-part2-part3",
},
{
name: "with empty strings",
items: []string{"part1", "", "part3"},
expected: "part1-part3",
},
{
name: "all empty strings",
items: []string{"", "", ""},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := CreateConvertedName(tc.items...)
assert.Equal(t, tc.expected, result)
})
}
}
func TestSortIngressByCreationTime(t *testing.T) {
configs := []config.Config{
{
Meta: config.Meta{
Name: "c-ingress",
Namespace: "ns1",
},
},
{
Meta: config.Meta{
Name: "a-ingress",
Namespace: "ns1",
},
},
{
Meta: config.Meta{
Name: "b-ingress",
Namespace: "ns1",
},
},
}
expected := []string{"a-ingress", "b-ingress", "c-ingress"}
SortIngressByCreationTime(configs)
var actual []string
for _, cfg := range configs {
actual = append(actual, cfg.Name)
}
assert.Equal(t, expected, actual, "When the timestamps are the same, the configuration should be sorted by name")
sameNamespaceConfigs := []config.Config{
{
Meta: config.Meta{
Name: "same-name",
Namespace: "c-ns",
},
},
{
Meta: config.Meta{
Name: "same-name",
Namespace: "a-ns",
},
},
{
Meta: config.Meta{
Name: "same-name",
Namespace: "b-ns",
},
},
}
expectedNamespace := []string{"a-ns", "b-ns", "c-ns"}
SortIngressByCreationTime(sameNamespaceConfigs)
var actualNamespace []string
for _, cfg := range sameNamespaceConfigs {
actualNamespace = append(actualNamespace, cfg.Namespace)
}
assert.Equal(t, expectedNamespace, actualNamespace, "When the names are the same, the configuration should be sorted by namespace")
}
func TestPartMd5(t *testing.T) {
testCases := []struct {
name string
input string
length int
}{
{
name: "empty string",
input: "",
length: 8,
},
{
name: "simple string",
input: "test",
length: 8,
},
{
name: "complex string",
input: "this-is-a-long-string-with-special-chars-!@#$%^&*()",
length: 8,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := partMd5(tc.input)
// Check result format
assert.Equal(t, tc.length, len(result), "MD5 hash excerpt should be 8 characters")
// Run twice to ensure deterministic output
result2 := partMd5(tc.input)
assert.Equal(t, result, result2, "partMd5 function should be deterministic")
})
}
}
func TestGetLbStatusListV1AndV1Beta1(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,
},
},
},
},
},
}
// Test the V1 version
t.Run("GetLbStatusListV1", func(t *testing.T) {
lbiList := GetLbStatusListV1(svcList)
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
})
// Test the V1Beta1 version
t.Run("GetLbStatusListV1Beta1", func(t *testing.T) {
lbiList := GetLbStatusListV1Beta1(svcList)
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
})
}