blob: 6f102cd6dd97cffa68edbe6793dac82711336f5c [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 model_test
import (
"encoding/json"
"reflect"
"testing"
"time"
)
import (
"google.golang.org/protobuf/types/known/durationpb"
structpb "google.golang.org/protobuf/types/known/structpb"
meshconfig "istio.io/api/mesh/v1alpha1"
"istio.io/api/networking/v1alpha3"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/serviceregistry/memory"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/serviceregistry/mock"
"github.com/apache/dubbo-go-pixiu/pkg/config/host"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/assert"
"github.com/apache/dubbo-go-pixiu/pkg/util/protomarshal"
)
func TestNodeMetadata(t *testing.T) {
tests := []struct {
name string
in model.BootstrapNodeMetadata
out string
inOut model.BootstrapNodeMetadata
}{
{
"empty",
model.BootstrapNodeMetadata{},
"{}",
model.BootstrapNodeMetadata{NodeMetadata: model.NodeMetadata{Raw: map[string]interface{}{}}},
},
{
"csvlists",
model.BootstrapNodeMetadata{NodeMetadata: model.NodeMetadata{InstanceIPs: []string{"abc", "1.2.3.4"}}},
`{"INSTANCE_IPS":"abc,1.2.3.4"}`,
model.BootstrapNodeMetadata{
NodeMetadata: model.NodeMetadata{
InstanceIPs: []string{"abc", "1.2.3.4"},
Raw: map[string]interface{}{
"INSTANCE_IPS": "abc,1.2.3.4",
},
},
},
},
{
"labels",
model.BootstrapNodeMetadata{NodeMetadata: model.NodeMetadata{Labels: map[string]string{"foo": "bar"}}},
`{"LABELS":{"foo":"bar"}}`,
model.BootstrapNodeMetadata{
NodeMetadata: model.NodeMetadata{
Labels: map[string]string{"foo": "bar"},
Raw: map[string]interface{}{
"LABELS": map[string]interface{}{
"foo": "bar",
},
},
},
},
},
{
"proxy config",
model.BootstrapNodeMetadata{
NodeMetadata: model.NodeMetadata{
ProxyConfig: (*model.NodeMetaProxyConfig)(&meshconfig.ProxyConfig{
ConfigPath: "foo",
DrainDuration: durationpb.New(time.Second * 5),
ControlPlaneAuthPolicy: meshconfig.AuthenticationPolicy_MUTUAL_TLS,
EnvoyAccessLogService: &meshconfig.RemoteService{
Address: "address",
TlsSettings: &v1alpha3.ClientTLSSettings{
SubjectAltNames: []string{"san"},
},
},
}),
},
},
// nolint: lll
`{"PROXY_CONFIG":{"configPath":"foo","drainDuration":"5s","controlPlaneAuthPolicy":"MUTUAL_TLS","envoyAccessLogService":{"address":"address","tlsSettings":{"subjectAltNames":["san"]}}}}`,
model.BootstrapNodeMetadata{
NodeMetadata: model.NodeMetadata{
ProxyConfig: (*model.NodeMetaProxyConfig)(&meshconfig.ProxyConfig{
ConfigPath: "foo",
DrainDuration: durationpb.New(time.Second * 5),
ControlPlaneAuthPolicy: meshconfig.AuthenticationPolicy_MUTUAL_TLS,
EnvoyAccessLogService: &meshconfig.RemoteService{
Address: "address",
TlsSettings: &v1alpha3.ClientTLSSettings{
SubjectAltNames: []string{"san"},
},
},
}),
Raw: map[string]interface{}{
"PROXY_CONFIG": map[string]interface{}{
"drainDuration": "5s",
"configPath": "foo",
"controlPlaneAuthPolicy": "MUTUAL_TLS",
"envoyAccessLogService": map[string]interface{}{
"address": "address",
"tlsSettings": map[string]interface{}{
"subjectAltNames": []interface{}{"san"},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
j, err := json.Marshal(tt.in)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
if string(j) != tt.out {
t.Errorf("Got json '%s', expected '%s'", string(j), tt.out)
}
var meta model.BootstrapNodeMetadata
if err := json.Unmarshal(j, &meta); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
assert.Equal(t, (*meshconfig.ProxyConfig)(meta.NodeMetadata.ProxyConfig), (*meshconfig.ProxyConfig)(tt.inOut.NodeMetadata.ProxyConfig))
// cmp cannot handle the type-alias in the metadata, so check them separately.
meta.NodeMetadata.ProxyConfig = nil
tt.inOut.NodeMetadata.ProxyConfig = nil
assert.Equal(t, meta, tt.inOut)
})
}
}
func TestStringList(t *testing.T) {
cases := []struct {
in string
expect model.StringList
noRoundTrip bool
}{
{in: `"a,b,c"`, expect: []string{"a", "b", "c"}},
{in: `"a"`, expect: []string{"a"}},
{in: `""`, expect: []string{}},
{in: `"123,@#$#,abcdef"`, expect: []string{"123", "@#$#", "abcdef"}},
{in: `1`, expect: []string{}, noRoundTrip: true},
}
for _, tt := range cases {
t.Run(tt.in, func(t *testing.T) {
var out model.StringList
if err := json.Unmarshal([]byte(tt.in), &out); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(out, tt.expect) {
t.Fatalf("Expected %v, got %v", tt.expect, out)
}
b, err := json.Marshal(out)
if err != nil {
t.Fatal(err)
}
if tt.noRoundTrip {
return
}
if !reflect.DeepEqual(string(b), tt.in) {
t.Fatalf("Expected %v, got %v", tt.in, string(b))
}
})
}
}
func TestPodPortList(t *testing.T) {
cases := []struct {
name string
in string
expect model.PodPortList
expUnmarshalErr string
}{
{"no port", `"[]"`, model.PodPortList{}, ""},
{"one port", `"[{\"name\":\"foo\",\"containerPort\":9080,\"protocol\":\"TCP\"}]"`, model.PodPortList{{"foo", 9080, "TCP"}}, ""},
{
"two ports",
`"[{\"name\":\"foo\",\"containerPort\":9080,\"protocol\":\"TCP\"},{\"containerPort\":8888,\"protocol\":\"TCP\"}]"`,
model.PodPortList{{"foo", 9080, "TCP"}, {ContainerPort: 8888, Protocol: "TCP"}},
"",
},
{"invalid syntax", `[]`, nil, "invalid syntax"},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
var out model.PodPortList
if err := json.Unmarshal([]byte(tt.in), &out); err != nil {
if tt.expUnmarshalErr == "" {
t.Fatal(err)
}
if out != nil {
t.Fatalf("%s: Expected null unmarshal output but obtained a non-null one.", tt.name)
}
if err.Error() != tt.expUnmarshalErr {
t.Fatalf("%s: Expected error: %s but got error: %s.", tt.name, tt.expUnmarshalErr, err.Error())
}
return
}
if !reflect.DeepEqual(out, tt.expect) {
t.Fatalf("%s: Expected %v, got %v", tt.name, tt.expect, out)
}
b, err := json.Marshal(out)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(string(b), tt.in) {
t.Fatalf("%s: Expected %v, got %v", tt.name, tt.in, string(b))
}
})
}
}
func TestStringBool(t *testing.T) {
cases := []struct {
name string
in string
expect string
}{
{"1", `"1"`, `"true"`},
{"0", `"0"`, `"false"`},
{"false", `"false"`, `"false"`},
{"true", `"true"`, `"true"`},
{"invalid input", `"foo"`, ``},
{"no quotes", `true`, ``},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
var out model.StringBool
if err := json.Unmarshal([]byte(tt.in), &out); err != nil {
if tt.expect == "" {
return
}
t.Fatal(err)
}
b, err := json.Marshal(out)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(string(b), tt.expect) {
t.Fatalf("Expected %v, got %v", tt.expect, string(b))
}
})
}
}
func TestServiceNode(t *testing.T) {
cases := []struct {
in *model.Proxy
out string
}{
{
in: &mock.HelloProxyV0,
out: "sidecar~10.1.1.0~v0.default~default.svc.cluster.local",
},
{
in: &model.Proxy{
Type: model.Router,
ID: "random",
IPAddresses: []string{"10.3.3.3"},
DNSDomain: "local",
Metadata: &model.NodeMetadata{},
IstioVersion: model.MaxIstioVersion,
},
out: "router~10.3.3.3~random~local",
},
{
in: &model.Proxy{
Type: model.SidecarProxy,
ID: "random",
IPAddresses: []string{"10.3.3.3", "10.4.4.4", "10.5.5.5", "10.6.6.6"},
DNSDomain: "local",
Metadata: &model.NodeMetadata{
InstanceIPs: []string{"10.3.3.3", "10.4.4.4", "10.5.5.5", "10.6.6.6"},
},
IstioVersion: model.MaxIstioVersion,
},
out: "sidecar~10.3.3.3~random~local",
},
}
for _, node := range cases {
out := node.in.ServiceNode()
if out != node.out {
t.Errorf("%#v.ServiceNode() => Got %s, want %s", node.in, out, node.out)
}
in, err := model.ParseServiceNodeWithMetadata(node.out, node.in.Metadata)
if err != nil {
t.Errorf("ParseServiceNode(%q) => Got error %v", node.out, err)
}
if !reflect.DeepEqual(in, node.in) {
t.Errorf("ParseServiceNode(%q) => Got %#v, want %#v", node.out, in, node.in)
}
}
}
func TestParseMetadata(t *testing.T) {
cases := []struct {
name string
metadata map[string]interface{}
out *model.Proxy
}{
{
name: "Basic Case",
out: &model.Proxy{
Type: "sidecar", IPAddresses: []string{"1.1.1.1"}, DNSDomain: "domain", ID: "id", IstioVersion: model.MaxIstioVersion,
Metadata: &model.NodeMetadata{Raw: map[string]interface{}{}},
},
},
{
name: "Capture Arbitrary Metadata",
metadata: map[string]interface{}{"foo": "bar"},
out: &model.Proxy{
Type: "sidecar", IPAddresses: []string{"1.1.1.1"}, DNSDomain: "domain", ID: "id", IstioVersion: model.MaxIstioVersion,
Metadata: &model.NodeMetadata{
Raw: map[string]interface{}{
"foo": "bar",
},
},
},
},
{
name: "Capture Labels",
metadata: map[string]interface{}{
"LABELS": map[string]string{
"foo": "bar",
},
},
out: &model.Proxy{
Type: "sidecar", IPAddresses: []string{"1.1.1.1"}, DNSDomain: "domain", ID: "id", IstioVersion: model.MaxIstioVersion,
Metadata: &model.NodeMetadata{
Raw: map[string]interface{}{
"LABELS": map[string]interface{}{"foo": "bar"},
},
Labels: map[string]string{"foo": "bar"},
},
},
},
{
name: "Capture Pod Ports",
metadata: map[string]interface{}{
"POD_PORTS": `[{"name":"http","containerPort":8080,"protocol":"TCP"},{"name":"grpc","containerPort":8079,"protocol":"TCP"}]`,
},
out: &model.Proxy{
Type: "sidecar", IPAddresses: []string{"1.1.1.1"}, DNSDomain: "domain", ID: "id", IstioVersion: model.MaxIstioVersion,
Metadata: &model.NodeMetadata{
Raw: map[string]interface{}{
"POD_PORTS": `[{"name":"http","containerPort":8080,"protocol":"TCP"},{"name":"grpc","containerPort":8079,"protocol":"TCP"}]`,
},
PodPorts: []model.PodPort{
{"http", 8080, "TCP"},
{"grpc", 8079, "TCP"},
},
},
},
},
}
nodeID := "sidecar~1.1.1.1~id~domain"
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
meta, err := mapToStruct(tt.metadata)
if err != nil {
t.Fatalf("failed to setup metadata: %v", err)
}
parsed, err := model.ParseMetadata(meta)
if err != nil {
t.Fatalf("failed to parse service node: %v", err)
}
node, err := model.ParseServiceNodeWithMetadata(nodeID, parsed)
if err != nil {
t.Fatalf("failed to parse service node: %v", err)
}
if !reflect.DeepEqual(*tt.out.Metadata, *node.Metadata) {
t.Errorf("Got \n%v, want \n%v", *node.Metadata, *tt.out.Metadata)
}
})
}
}
func mapToStruct(msg map[string]interface{}) (*structpb.Struct, error) {
if msg == nil {
return &structpb.Struct{}, nil
}
b, err := json.Marshal(msg)
if err != nil {
return nil, err
}
pbs := &structpb.Struct{}
if err := protomarshal.Unmarshal(b, pbs); err != nil {
return nil, err
}
return pbs, nil
}
func TestParsePort(t *testing.T) {
if port := model.ParsePort("localhost:3000"); port != 3000 {
t.Errorf("ParsePort(localhost:3000) => Got %d, want 3000", port)
}
if port := model.ParsePort("localhost"); port != 0 {
t.Errorf("ParsePort(localhost) => Got %d, want 0", port)
}
if port := model.ParsePort("127.0.0.1:3000"); port != 3000 {
t.Errorf("ParsePort(127.0.0.1:3000) => Got %d, want 3000", port)
}
if port := model.ParsePort("127.0.0.1"); port != 0 {
t.Errorf("ParsePort(127.0.0.1) => Got %d, want 0", port)
}
if port := model.ParsePort("[::1]:3000"); port != 3000 {
t.Errorf("ParsePort([::1]:3000) => Got %d, want 3000", port)
}
if port := model.ParsePort("::1"); port != 0 {
t.Errorf("ParsePort(::1) => Got %d, want 0", port)
}
if port := model.ParsePort("[2001:4860:0:2001::68]:3000"); port != 3000 {
t.Errorf("ParsePort([2001:4860:0:2001::68]:3000) => Got %d, want 3000", port)
}
if port := model.ParsePort("2001:4860:0:2001::68"); port != 0 {
t.Errorf("ParsePort(2001:4860:0:2001::68) => Got %d, want 0", port)
}
}
func TestGetOrDefault(t *testing.T) {
assert.Equal(t, "a", model.GetOrDefault("a", "b"))
assert.Equal(t, "b", model.GetOrDefault("", "b"))
}
func TestProxyVersion_Compare(t *testing.T) {
type fields struct {
Major int
Minor int
Patch int
}
type args struct {
inv *model.IstioVersion
}
tests := []struct {
name string
fields fields
args args
want int
}{
{
name: "greater major",
fields: fields{Major: 2, Minor: 1, Patch: 1},
args: args{&model.IstioVersion{Major: 1, Minor: 2, Patch: 1}},
want: 1,
},
{
name: "equal at minor",
fields: fields{Major: 2, Minor: 1, Patch: 1},
args: args{&model.IstioVersion{Major: 2, Minor: 1, Patch: -1}},
want: 0,
},
{
name: "less at patch",
fields: fields{Major: 2, Minor: 1, Patch: 0},
args: args{&model.IstioVersion{Major: 2, Minor: 1, Patch: 1}},
want: -1,
},
{
name: "ignore minor",
fields: fields{Major: 2, Minor: 1, Patch: 11},
args: args{&model.IstioVersion{Major: 2, Minor: -1, Patch: 1}},
want: 0,
},
{
name: "ignore patch",
fields: fields{Major: 2, Minor: 1, Patch: 11},
args: args{&model.IstioVersion{Major: 2, Minor: 1, Patch: -1}},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pversion := &model.IstioVersion{
Major: tt.fields.Major,
Minor: tt.fields.Minor,
Patch: tt.fields.Patch,
}
if got := pversion.Compare(tt.args.inv); got != tt.want {
t.Errorf("ProxyVersion.Compare() = %v, want %v", got, tt.want)
}
})
}
}
func Test_parseIstioVersion(t *testing.T) {
type args struct {
ver string
}
tests := []struct {
name string
args args
want *model.IstioVersion
}{
{
name: "major.minor.patch",
args: args{ver: "1.2.3"},
want: &model.IstioVersion{Major: 1, Minor: 2, Patch: 3},
},
{
name: "major.minor",
args: args{ver: "1.2"},
want: &model.IstioVersion{Major: 1, Minor: 2, Patch: 65535},
},
{
name: "dev",
args: args{ver: "1.5-alpha.f70faea2aa817eeec0b08f6cc3b5078e5dcf3beb"},
want: &model.IstioVersion{Major: 1, Minor: 5, Patch: 65535},
},
{
name: "release-major.minor-date",
args: args{ver: "release-1.2-123214234"},
want: &model.IstioVersion{Major: 1, Minor: 2, Patch: 65535},
},
{
name: "master-date",
args: args{ver: "master-123214234"},
want: model.MaxIstioVersion,
},
{
name: "master-sha",
args: args{ver: "master-0b94e017f5b6c7c4598a4da42ea9d45eeb099e5f"},
want: model.MaxIstioVersion,
},
{
name: "junk-major.minor.patch",
args: args{ver: "junk-1.2.3214234"},
want: model.MaxIstioVersion,
},
{
name: "junk-garbage",
args: args{ver: "junk-garbage"},
want: model.MaxIstioVersion,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := model.ParseIstioVersion(tt.args.ver); !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseIstioVersion() = %v, want %v", got, tt.want)
}
})
}
}
func TestSetServiceInstances(t *testing.T) {
tnow := time.Now()
instances := []*model.ServiceInstance{
{
Service: &model.Service{
CreationTime: tnow.Add(1 * time.Second),
Hostname: host.Name("test1.com"),
},
},
{
Service: &model.Service{
CreationTime: tnow,
Hostname: host.Name("test3.com"),
},
},
{
Service: &model.Service{
CreationTime: tnow,
Hostname: host.Name("test2.com"),
},
},
}
serviceDiscovery := memory.NewServiceDiscovery()
serviceDiscovery.WantGetProxyServiceInstances = instances
env := &model.Environment{
ServiceDiscovery: serviceDiscovery,
}
proxy := &model.Proxy{}
proxy.SetServiceInstances(env)
assert.Equal(t, len(proxy.ServiceInstances), 3)
assert.Equal(t, proxy.ServiceInstances[0].Service.Hostname, host.Name("test2.com"))
assert.Equal(t, proxy.ServiceInstances[1].Service.Hostname, host.Name("test3.com"))
assert.Equal(t, proxy.ServiceInstances[2].Service.Hostname, host.Name("test1.com"))
}
func TestGlobalUnicastIP(t *testing.T) {
cases := []struct {
name string
in []string
expect string
}{
{
name: "single IPv4 (k8s)",
in: []string{"10.0.4.16"},
expect: "10.0.4.16",
},
{
name: "single IPv6 (k8s)",
in: []string{"fc00:f853:ccd:e793::1"},
expect: "fc00:f853:ccd:e793::1",
},
{
name: "multi IPv4 [1st] (VM)",
in: []string{"10.128.0.51", "fc00:f853:ccd:e793::1", "172.17.0.1", "fe80::42:35ff:fec1:7436", "fe80::345d:33ff:fe54:5c8e"},
expect: "10.128.0.51",
},
{
name: "multi IPv6 [1st] (VM)",
in: []string{"fc00:f853:ccd:e793::1", "172.17.0.1", "fe80::42:35ff:fec1:7436", "10.128.0.51", "fe80::345d:33ff:fe54:5c8e"},
expect: "fc00:f853:ccd:e793::1",
},
{
name: "multi IPv4 [2nd] (VM)",
in: []string{"127.0.0.1", "10.128.0.51", "fc00:f853:ccd:e793::1", "172.17.0.1", "fe80::42:35ff:fec1:7436", "fe80::345d:33ff:fe54:5c8e"},
expect: "10.128.0.51",
},
{
name: "multi IPv6 [2nd] (VM)",
in: []string{"fe80::42:35ff:fec1:7436", "fc00:f853:ccd:e793::1", "172.17.0.1", "10.128.0.51", "fe80::345d:33ff:fe54:5c8e"},
expect: "fc00:f853:ccd:e793::1",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
var node model.Proxy
node.IPAddresses = tt.in
node.DiscoverIPMode()
if got := node.GlobalUnicastIP; got != tt.expect {
t.Errorf("GlobalUnicastIP = %v, want %v", got, tt.expect)
}
})
}
}