blob: beb41169561893e22ba312c92ebe5bda9d84c93e [file] [log] [blame]
// Copyright Istio 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 gateway
import (
"fmt"
"os"
"reflect"
"regexp"
"sort"
"strings"
"testing"
)
import (
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s "sigs.k8s.io/gateway-api/apis/v1alpha2"
"sigs.k8s.io/yaml"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/config/kube/crd"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model/kstatus"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/core/v1alpha3"
"github.com/apache/dubbo-go-pixiu/pilot/test/util"
"github.com/apache/dubbo-go-pixiu/pkg/cluster"
"github.com/apache/dubbo-go-pixiu/pkg/config"
crdvalidation "github.com/apache/dubbo-go-pixiu/pkg/config/crd"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk"
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/assert"
"github.com/apache/dubbo-go-pixiu/pkg/util/sets"
)
func TestConvertResources(t *testing.T) {
validator := crdvalidation.NewIstioValidator(t)
cases := []struct {
name string
}{
{"http"},
{"tcp"},
{"tls"},
{"mismatch"},
{"weighted"},
{"zero"},
{"mesh"},
{"invalid"},
{"multi-gateway"},
{"delegated"},
//{"route-binding"},
{"reference-policy-tls"},
{"serviceentry"},
{"eastwest"},
{"alias"},
{"mcs"},
{"route-precedence"},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
input := readConfig(t, fmt.Sprintf("testdata/%s.yaml", tt.name), validator)
// Setup a few preconfigured services
ports := []*model.Port{
{
Name: "http",
Port: 80,
Protocol: "HTTP",
},
{
Name: "tcp",
Port: 34000,
Protocol: "TCP",
},
}
ingressSvc := &model.Service{
Attributes: model.ServiceAttributes{
Name: "istio-ingressgateway",
Namespace: "dubbo-system",
ClusterExternalAddresses: model.AddressMap{
Addresses: map[cluster.ID][]string{
"Kubernetes": {"1.2.3.4"},
},
},
},
Ports: ports,
Hostname: "istio-ingressgateway.dubbo-system.svc.domain.suffix",
}
altIngressSvc := &model.Service{
Attributes: model.ServiceAttributes{
Namespace: "dubbo-system",
},
Ports: ports,
Hostname: "example.com",
}
cg := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{
Services: []*model.Service{ingressSvc, altIngressSvc},
Instances: []*model.ServiceInstance{
{Service: ingressSvc, ServicePort: ingressSvc.Ports[0], Endpoint: &model.IstioEndpoint{EndpointPort: 8080}},
{Service: ingressSvc, ServicePort: ingressSvc.Ports[1], Endpoint: &model.IstioEndpoint{}},
{Service: altIngressSvc, ServicePort: altIngressSvc.Ports[0], Endpoint: &model.IstioEndpoint{}},
{Service: altIngressSvc, ServicePort: altIngressSvc.Ports[1], Endpoint: &model.IstioEndpoint{}},
},
},
)
kr := splitInput(input)
kr.Context = model.NewGatewayContext(cg.PushContext())
output := convertResources(kr)
output.AllowedReferences = nil // Not tested here
output.ReferencedNamespaceKeys = nil // Not tested here
goldenFile := fmt.Sprintf("testdata/%s.yaml.golden", tt.name)
if util.Refresh() {
res := append(output.Gateway, output.VirtualService...)
if err := os.WriteFile(goldenFile, marshalYaml(t, res), 0o644); err != nil {
t.Fatal(err)
}
}
golden := splitOutput(readConfig(t, goldenFile, validator))
// sort virtual services to make the order deterministic
sort.Slice(golden.VirtualService, func(i, j int) bool {
return golden.VirtualService[i].Namespace+"/"+golden.VirtualService[i].Name < golden.VirtualService[j].Namespace+"/"+golden.VirtualService[j].Name
})
sort.Slice(output.VirtualService, func(i, j int) bool {
return output.VirtualService[i].Namespace+"/"+output.VirtualService[i].Name < output.VirtualService[j].Namespace+"/"+output.VirtualService[j].Name
})
assert.Equal(t, golden, output)
outputStatus := getStatus(t, kr.GatewayClass, kr.Gateway, kr.HTTPRoute, kr.TLSRoute, kr.TCPRoute)
goldenStatusFile := fmt.Sprintf("testdata/%s.status.yaml.golden", tt.name)
if util.Refresh() {
if err := os.WriteFile(goldenStatusFile, outputStatus, 0o644); err != nil {
t.Fatal(err)
}
}
goldenStatus, err := os.ReadFile(goldenStatusFile)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(goldenStatus), string(outputStatus)); diff != "" {
t.Fatalf("Diff:\n%s", diff)
}
})
}
}
func TestReferencePolicy(t *testing.T) {
validator := crdvalidation.NewIstioValidator(t)
type res struct {
name, namespace string
allowed bool
}
cases := []struct {
name string
config string
expectations []res
}{
{
name: "simple",
config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferencePolicy
metadata:
name: allow-gateways-to-ref-secrets
namespace: default
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: dubbo-system
to:
- group: ""
kind: Secret
`,
expectations: []res{
// allow cross namespace
{"kubernetes-gateway://default/wildcard-example-com-cert", "dubbo-system", true},
// denied same namespace. We do not implicitly allow (in this code - higher level code does)
{"kubernetes-gateway://default/wildcard-example-com-cert", "default", false},
// denied namespace
{"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false},
},
},
{
name: "multiple in one",
config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferencePolicy
metadata:
name: allow-gateways-to-ref-secrets
namespace: default
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: ns-1
- group: gateway.networking.k8s.io
kind: Gateway
namespace: ns-2
to:
- group: ""
kind: Secret
`,
expectations: []res{
{"kubernetes-gateway://default/wildcard-example-com-cert", "ns-1", true},
{"kubernetes-gateway://default/wildcard-example-com-cert", "ns-2", true},
{"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false},
},
},
{
name: "multiple",
config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferencePolicy
metadata:
name: ns1
namespace: default
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: ns-1
to:
- group: ""
kind: Secret
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferencePolicy
metadata:
name: ns2
namespace: default
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: ns-2
to:
- group: ""
kind: Secret
`,
expectations: []res{
{"kubernetes-gateway://default/wildcard-example-com-cert", "ns-1", true},
{"kubernetes-gateway://default/wildcard-example-com-cert", "ns-2", true},
{"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false},
},
},
{
name: "same namespace",
config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferencePolicy
metadata:
name: allow-gateways-to-ref-secrets
namespace: default
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: default
to:
- group: ""
kind: Secret
`,
expectations: []res{
{"kubernetes-gateway://default/wildcard-example-com-cert", "dubbo-system", false},
{"kubernetes-gateway://default/wildcard-example-com-cert", "default", true},
{"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false},
},
},
{
name: "same name",
config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferencePolicy
metadata:
name: allow-gateways-to-ref-secrets
namespace: default
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: default
to:
- group: ""
kind: Secret
name: public
`,
expectations: []res{
{"kubernetes-gateway://default/public", "dubbo-system", false},
{"kubernetes-gateway://default/public", "default", true},
{"kubernetes-gateway://default/private", "default", false},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
input := readConfigString(t, tt.config, validator)
cg := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{})
kr := splitInput(input)
kr.Context = model.NewGatewayContext(cg.PushContext())
output := convertResources(kr)
c := &Controller{
state: output,
}
for _, sc := range tt.expectations {
t.Run(fmt.Sprintf("%v/%v", sc.name, sc.namespace), func(t *testing.T) {
got := c.SecretAllowed(sc.name, sc.namespace)
if got != sc.allowed {
t.Fatalf("expected allowed=%v, got allowed=%v", sc.allowed, got)
}
})
}
})
}
}
func getStatus(t test.Failer, acfgs ...[]config.Config) []byte {
cfgs := []config.Config{}
for _, cl := range acfgs {
cfgs = append(cfgs, cl...)
}
for i, c := range cfgs {
c = c.DeepCopy()
c.Spec = nil
c.Labels = nil
c.Annotations = nil
if c.Status.(*kstatus.WrappedStatus) != nil {
c.Status = c.Status.(*kstatus.WrappedStatus).Status
}
cfgs[i] = c
}
return timestampRegex.ReplaceAll(marshalYaml(t, cfgs), []byte("lastTransitionTime: fake"))
}
var timestampRegex = regexp.MustCompile(`lastTransitionTime:.*`)
func splitOutput(configs []config.Config) OutputResources {
out := OutputResources{
Gateway: []config.Config{},
VirtualService: []config.Config{},
}
for _, c := range configs {
c.Domain = "domain.suffix"
switch c.GroupVersionKind {
case gvk.Gateway:
out.Gateway = append(out.Gateway, c)
case gvk.VirtualService:
out.VirtualService = append(out.VirtualService, c)
}
}
return out
}
func splitInput(configs []config.Config) *KubernetesResources {
out := &KubernetesResources{}
namespaces := sets.New()
for _, c := range configs {
namespaces.Insert(c.Namespace)
switch c.GroupVersionKind {
case gvk.GatewayClass:
out.GatewayClass = append(out.GatewayClass, c)
case gvk.KubernetesGateway:
out.Gateway = append(out.Gateway, c)
case gvk.HTTPRoute:
out.HTTPRoute = append(out.HTTPRoute, c)
case gvk.TCPRoute:
out.TCPRoute = append(out.TCPRoute, c)
case gvk.TLSRoute:
out.TLSRoute = append(out.TLSRoute, c)
case gvk.ReferencePolicy:
out.ReferencePolicy = append(out.ReferencePolicy, c)
}
}
out.Namespaces = map[string]*corev1.Namespace{}
for ns := range namespaces {
out.Namespaces[ns] = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: ns,
Labels: map[string]string{
"istio.io/test-name-part": strings.Split(ns, "-")[0],
},
},
}
}
out.Domain = "domain.suffix"
return out
}
func readConfig(t *testing.T, filename string, validator *crdvalidation.Validator) []config.Config {
t.Helper()
data, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("failed to read input yaml file: %v", err)
}
return readConfigString(t, string(data), validator)
}
func readConfigString(t *testing.T, data string, validator *crdvalidation.Validator) []config.Config {
if err := validator.ValidateCustomResourceYAML(data); err != nil {
t.Error(err)
}
c, _, err := crd.ParseInputs(data)
if err != nil {
t.Fatalf("failed to parse CRD: %v", err)
}
return insertDefaults(c)
}
// insertDefaults sets default values that would be present when reading from Kubernetes but not from
// files
func insertDefaults(cfgs []config.Config) []config.Config {
res := make([]config.Config, 0, len(cfgs))
for _, c := range cfgs {
switch c.GroupVersionKind {
case gvk.GatewayClass:
c.Status = kstatus.Wrap(&k8s.GatewayClassStatus{})
case gvk.KubernetesGateway:
c.Status = kstatus.Wrap(&k8s.GatewayStatus{})
case gvk.HTTPRoute:
c.Status = kstatus.Wrap(&k8s.HTTPRouteStatus{})
case gvk.TCPRoute:
c.Status = kstatus.Wrap(&k8s.TCPRouteStatus{})
case gvk.TLSRoute:
c.Status = kstatus.Wrap(&k8s.TLSRouteStatus{})
}
res = append(res, c)
}
return res
}
// Print as YAML
func marshalYaml(t test.Failer, cl []config.Config) []byte {
t.Helper()
result := []byte{}
separator := []byte("---\n")
for _, config := range cl {
obj, err := crd.ConvertConfig(config)
if err != nil {
t.Fatalf("Could not decode %v: %v", config.Name, err)
}
bytes, err := yaml.Marshal(obj)
if err != nil {
t.Fatalf("Could not convert %v to YAML: %v", config, err)
}
result = append(result, bytes...)
result = append(result, separator...)
}
return result
}
func TestStandardizeWeight(t *testing.T) {
tests := []struct {
name string
input []int
output []int
}{
{"single", []int{1}, []int{0}},
{"double", []int{1, 1}, []int{50, 50}},
{"zero", []int{1, 0}, []int{100, 0}},
{"overflow", []int{1, 1, 1}, []int{34, 33, 33}},
{"skewed", []int{9, 1}, []int{90, 10}},
{"multiple overflow", []int{1, 1, 1, 1, 1, 1}, []int{17, 17, 17, 17, 16, 16}},
{"skewed overflow", []int{1, 1, 1, 3}, []int{17, 17, 16, 50}},
{"skewed overflow 2", []int{1, 1, 1, 1, 2}, []int{17, 17, 17, 16, 33}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := standardizeWeights(tt.input)
if !reflect.DeepEqual(tt.output, got) {
t.Errorf("standardizeWeights() = %v, want %v", got, tt.output)
}
if len(tt.output) > 1 && intSum(tt.output) != 100 {
t.Errorf("invalid weights, should sum to 100: %v", got)
}
})
}
}
func TestHumanReadableJoin(t *testing.T) {
tests := []struct {
input []string
want string
}{
{[]string{"a"}, "a"},
{[]string{"a", "b"}, "a and b"},
{[]string{"a", "b", "c"}, "a, b, and c"},
}
for _, tt := range tests {
t.Run(strings.Join(tt.input, "_"), func(t *testing.T) {
if got := humanReadableJoin(tt.input); !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}