blob: c04f86e80408efc7c0ce2203408b93b01aa0491e [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 cmd
import (
"bytes"
"fmt"
"reflect"
"strings"
"testing"
)
import (
appsv1 "k8s.io/api/apps/v1"
coreV1 "k8s.io/api/core/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/fake"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/collections"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/assert"
"github.com/apache/dubbo-go-pixiu/pkg/url"
)
type testcase struct {
description string
expectedException bool
args []string
k8sConfigs []runtime.Object
dynamicConfigs []runtime.Object
expectedOutput string
namespace string
}
var (
one = int32(1)
cannedK8sConfigs = []runtime.Object{
&coreV1.ConfigMapList{Items: []coreV1.ConfigMap{}},
&appsv1.DeploymentList{Items: []appsv1.Deployment{
{
ObjectMeta: metaV1.ObjectMeta{
Name: "details-v1",
Namespace: "default",
Labels: map[string]string{
"app": "details",
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &one,
Selector: &metaV1.LabelSelector{
MatchLabels: map[string]string{"app": "details"},
},
Template: coreV1.PodTemplateSpec{
ObjectMeta: metaV1.ObjectMeta{
Labels: map[string]string{"app": "details"},
},
Spec: coreV1.PodSpec{
Containers: []coreV1.Container{
{Name: "details", Image: "docker.io/istio/examples-bookinfo-details-v1:1.15.0"},
},
},
},
},
},
}},
&coreV1.ServiceList{Items: []coreV1.Service{
{
ObjectMeta: metaV1.ObjectMeta{
Name: "details",
Namespace: "default",
},
Spec: coreV1.ServiceSpec{
Ports: []coreV1.ServicePort{
{
Port: 9080,
Name: "http",
},
},
Selector: map[string]string{"app": "details"},
},
},
{
ObjectMeta: metaV1.ObjectMeta{
Name: "dummyservice",
Namespace: "default",
},
Spec: coreV1.ServiceSpec{
Ports: []coreV1.ServicePort{
{
Port: 9080,
Name: "http",
},
},
Selector: map[string]string{"app": "dummy"},
},
},
}},
}
cannedDynamicConfigs = []runtime.Object{
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": collections.IstioNetworkingV1Alpha3Serviceentries.Resource().APIVersion(),
"kind": collections.IstioNetworkingV1Alpha3Serviceentries.Resource().Kind(),
"metadata": map[string]interface{}{
"namespace": "default",
"name": "mesh-expansion-vmtest",
},
},
},
}
)
func TestAddToMesh(t *testing.T) {
cases := []testcase{
{
description: "Invalid command args - missing service name",
args: strings.Split("experimental add-to-mesh service", " "),
expectedException: true,
expectedOutput: "Error: expecting service name\n",
},
{
description: "Invalid command args - missing deployment name",
args: strings.Split("experimental add-to-mesh deployment", " "),
expectedException: true,
expectedOutput: "Error: expecting deployment name\n",
},
{
description: "valid case - add service into mesh",
args: strings.Split("experimental add-to-mesh service details --meshConfigFile testdata/mesh-config.yaml"+
" --injectConfigFile testdata/inject-config.yaml"+
" --valuesFile testdata/inject-values.yaml", " "),
expectedException: false,
k8sConfigs: cannedK8sConfigs,
expectedOutput: "deployment details-v1.default updated successfully with Istio sidecar injected.\n" +
"Next Step: Add related labels to the deployment to align with Istio's requirement: " + url.DeploymentRequirements + "\n",
namespace: "default",
},
{
description: "valid case - add deployment into mesh",
args: strings.Split("experimental add-to-mesh deployment details-v1 --meshConfigFile testdata/mesh-config.yaml"+
" --injectConfigFile testdata/inject-config.yaml"+
" --valuesFile testdata/inject-values.yaml", " "),
expectedException: false,
k8sConfigs: cannedK8sConfigs,
expectedOutput: "deployment details-v1.default updated successfully with Istio sidecar injected.\n" +
"Next Step: Add related labels to the deployment to align with Istio's requirement: " + url.DeploymentRequirements + "\n",
namespace: "default",
},
{
description: "service does not exist",
args: strings.Split("experimental add-to-mesh service test --meshConfigFile testdata/mesh-config.yaml"+
" --injectConfigFile testdata/inject-config.yaml"+
" --valuesFile testdata/inject-values.yaml", " "),
expectedException: true,
k8sConfigs: cannedK8sConfigs,
expectedOutput: "Error: services \"test\" not found\n",
},
{
description: "deployment does not exist",
args: strings.Split("experimental add-to-mesh deployment test --meshConfigFile testdata/mesh-config.yaml"+
" --injectConfigFile testdata/inject-config.yaml"+
" --valuesFile testdata/inject-values.yaml", " "),
expectedException: true,
k8sConfigs: cannedK8sConfigs,
expectedOutput: "Error: deployment \"test\" does not exist\n",
},
{
description: "service does not exist (with short syntax)",
args: strings.Split("x add svc test --meshConfigFile testdata/mesh-config.yaml"+
" --injectConfigFile testdata/inject-config.yaml"+
" --valuesFile testdata/inject-values.yaml", " "),
expectedException: true,
k8sConfigs: cannedK8sConfigs,
expectedOutput: "Error: services \"test\" not found\n",
},
{
description: "deployment does not exist (with short syntax)",
args: strings.Split("x add deploy test --meshConfigFile testdata/mesh-config.yaml"+
" --injectConfigFile testdata/inject-config.yaml"+
" --valuesFile testdata/inject-values.yaml", " "),
expectedException: true,
k8sConfigs: cannedK8sConfigs,
expectedOutput: "Error: deployment \"test\" does not exist\n",
},
{
description: "service without deployment",
args: strings.Split("experimental add-to-mesh service dummyservice --meshConfigFile testdata/mesh-config.yaml"+
" --injectConfigFile testdata/inject-config.yaml"+
" --valuesFile testdata/inject-values.yaml", " "),
namespace: "default",
expectedException: false,
k8sConfigs: cannedK8sConfigs,
expectedOutput: "No deployments found for service dummyservice.default\n",
},
{
description: "Invalid command args - missing service name",
args: strings.Split("experimental add-to-mesh service", " "),
expectedException: true,
expectedOutput: "Error: expecting service name\n",
},
{
description: "Invalid command args - missing service IP",
args: strings.Split("experimental add-to-mesh external-service test tcp:12345", " "),
expectedException: true,
expectedOutput: "Error: provide service name, IP and Port List\n",
},
{
description: "Invalid command args - missing service Ports",
args: strings.Split("experimental add-to-mesh external-service test 172.186.15.123", " "),
expectedException: true,
expectedOutput: "Error: provide service name, IP and Port List\n",
},
{
description: "Invalid command args - invalid port protocol",
args: strings.Split("experimental add-to-mesh external-service test 172.186.15.123 tcp1:12345", " "),
expectedException: true,
expectedOutput: "Error: protocol tcp1 is not supported by Istio\n",
},
{
description: "service already exists",
args: strings.Split("experimental add-to-mesh external-service dummyservice 11.11.11.11 tcp:12345", " "),
expectedException: true,
k8sConfigs: cannedK8sConfigs,
dynamicConfigs: cannedDynamicConfigs,
namespace: "default",
expectedOutput: "Error: service \"dummyservice\" already exists, skip\n",
},
{
description: "service already exists (with short syntax)",
args: strings.Split("x add es dummyservice 11.11.11.11 tcp:12345", " "),
expectedException: true,
k8sConfigs: cannedK8sConfigs,
dynamicConfigs: cannedDynamicConfigs,
namespace: "default",
expectedOutput: "Error: service \"dummyservice\" already exists, skip\n",
},
{
description: "ServiceEntry already exists",
args: strings.Split("experimental add-to-mesh external-service vmtest 11.11.11.11 tcp:12345", " "),
expectedException: true,
k8sConfigs: cannedK8sConfigs,
dynamicConfigs: cannedDynamicConfigs,
namespace: "default",
expectedOutput: "Error: service entry \"mesh-expansion-vmtest\" already exists, skip\n",
},
{
description: "external service banana namespace",
args: strings.Split("experimental add-to-mesh external-service vmtest 11.11.11.11 tcp:12345 tcp:12346", " "),
k8sConfigs: cannedK8sConfigs,
dynamicConfigs: cannedDynamicConfigs,
namespace: "banana",
expectedOutput: `ServiceEntry "mesh-expansion-vmtest.banana" has been created in the Istio service mesh for the external service "vmtest"
Kubernetes Service "vmtest.banana" has been created in the Istio service mesh for the external service "vmtest"
`,
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d %s", i, c.description), func(t *testing.T) {
verifyAddToMeshOutput(t, c)
})
}
}
func verifyAddToMeshOutput(t *testing.T, c testcase) {
t.Helper()
interfaceFactory = mockInterfaceFactoryGenerator(c.k8sConfigs)
crdFactory = mockDynamicClientGenerator(c.dynamicConfigs)
var out bytes.Buffer
rootCmd := GetRootCmd(c.args)
rootCmd.SetOut(&out)
rootCmd.SetErr(&out)
if c.namespace != "" {
namespace = c.namespace
}
fErr := rootCmd.Execute()
output := out.String()
if c.expectedException {
if fErr == nil {
t.Fatalf("Wanted an exception, "+
"didn't get one, output was %q", output)
}
} else {
if fErr != nil {
t.Fatalf("Unwanted exception: %v", fErr)
}
}
if c.expectedOutput != "" && c.expectedOutput != output {
assert.Equal(t, c.expectedOutput, output)
t.Fatalf("Unexpected output for 'istioctl %s'\n got: %q\nwant: %q", strings.Join(c.args, " "), output, c.expectedOutput)
}
}
func mockDynamicClientGenerator(dynamicConfigs []runtime.Object) func(kubeconfig string) (dynamic.Interface, error) {
outFactory := func(_ string) (dynamic.Interface, error) {
types := runtime.NewScheme()
client := fake.NewSimpleDynamicClient(types, dynamicConfigs...)
return client, nil
}
return outFactory
}
func TestSplitEqual(t *testing.T) {
tests := []struct {
arg string
wantKey string
wantValue string
}{
{arg: "key=value", wantKey: "key", wantValue: "value"},
{arg: "key==value", wantKey: "key", wantValue: "=value"},
{arg: "key=", wantKey: "key", wantValue: ""},
{arg: "key", wantKey: "key", wantValue: ""},
{arg: "", wantKey: "", wantValue: ""},
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
gotKey, gotValue := splitEqual(tt.arg)
if gotKey != tt.wantKey {
t.Errorf("splitEqual(%v) got = %v, want %v", tt.arg, gotKey, tt.wantKey)
}
if gotValue != tt.wantValue {
t.Errorf("splitEqual(%v) got1 = %v, want %v", tt.arg, gotValue, tt.wantValue)
}
})
}
}
func TestConvertToMap(t *testing.T) {
tests := []struct {
name string
arg []string
want map[string]string
}{
{name: "empty", arg: []string{""}, want: map[string]string{"": ""}},
{name: "one-valid", arg: []string{"key=value"}, want: map[string]string{"key": "value"}},
{name: "one-valid-double-equals", arg: []string{"key==value"}, want: map[string]string{"key": "=value"}},
{name: "one-key-only", arg: []string{"key"}, want: map[string]string{"key": ""}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := convertToStringMap(tt.arg); !reflect.DeepEqual(got, tt.want) {
t.Errorf("convertToStringMap() = %v, want %v", got, tt.want)
}
})
}
}
func TestStr2NamedPort(t *testing.T) {
tests := []struct {
input string // input
expVal namedPort // output
expErr bool // error
}{
// Good cases:
{"http:5555", namedPort{5555, "http"}, false},
{"80", namedPort{80, "http"}, false},
{"443", namedPort{443, "https"}, false},
{"1234", namedPort{1234, "1234"}, false},
// Error cases:
{"", namedPort{0, ""}, true},
{"foo:bar", namedPort{0, "foo"}, true},
}
for _, tst := range tests {
actVal, actErr := str2NamedPort(tst.input)
if tst.expVal != actVal {
t.Errorf("Got '%+v', expecting '%+v' for Str2NamedPort('%s')", actVal, tst.expVal, tst.input)
}
if tst.expErr {
if actErr == nil {
t.Errorf("Got no error when expecting an error for Str2NamedPort('%s')", tst.input)
}
} else {
if actErr != nil {
t.Errorf("Got unexpected error '%+v' when expecting none for Str2NamedPort('%s')", actErr, tst.input)
}
}
}
}