blob: b7942985a94991acde9a6aefafcea49282502d3b [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"
"strings"
"testing"
)
import (
"github.com/google/go-cmp/cmp"
apiannotation "istio.io/api/annotation"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s_labels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
)
import (
"github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/configdump"
"github.com/apache/dubbo-go-pixiu/pilot/test/util"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/assert"
)
// execAndK8sConfigTestCase lets a test case hold some Envoy, Istio, and Kubernetes configuration
type execAndK8sConfigTestCase struct {
k8sConfigs []runtime.Object // Canned K8s configuration
namespace string
args []string
// Typically use one of the three
expectedOutput string // Expected constant output
expectedString string // String output is expected to contain
goldenFilename string // Expected output stored in golden file
wantException bool
}
// Tests Pilot /debug
func TestDescribe(t *testing.T) {
cases := []execAndK8sConfigTestCase{
{ // case 0
args: strings.Split("experimental describe", " "),
expectedString: "Describe resource and related Istio configuration",
},
{ // case 1 short name 'i'
args: strings.Split("x des", " "),
expectedString: "Describe resource and related Istio configuration",
},
{ // case 2 no pod
args: strings.Split("experimental describe pod", " "),
expectedString: "Error: expecting pod name",
wantException: true, // "istioctl experimental inspect pod" should fail
},
{ // case 3 unknown pod
args: strings.Split("experimental describe po not-a-pod", " "),
expectedString: "pods \"not-a-pod\" not found",
wantException: true, // "istioctl experimental describe pod not-a-pod" should fail
},
{ // case 8 unknown service
args: strings.Split("experimental describe service not-a-service", " "),
expectedString: "services \"not-a-service\" not found",
wantException: true, // "istioctl experimental describe service not-a-service" should fail
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("case %d %s", i, strings.Join(c.args, " ")), func(t *testing.T) {
verifyExecAndK8sConfigTestCaseTestOutput(t, c)
})
}
}
func TestGetRevisionFromPodAnnotation(t *testing.T) {
cases := []struct {
anno k8s_labels.Set
expected string
}{
{
anno: k8s_labels.Set{
apiannotation.SidecarStatus.Name: "",
},
expected: "",
},
{
anno: k8s_labels.Set{},
expected: "",
},
{
anno: k8s_labels.Set{
apiannotation.SidecarStatus.Name: `
{
"initContainers": [
"istio-init"
],
"containers": [
"istio-proxy"
],
"volumes": [
"istio-envoy",
"istio-data",
"istio-podinfo",
"istio-token",
"istiod-ca-cert"
],
"imagePullSecrets": null,
"revision": "1-13-2"
}`,
},
expected: "1-13-2",
},
}
for _, tc := range cases {
t.Run("", func(t *testing.T) {
got := getRevisionFromPodAnnotation(tc.anno)
assert.Equal(t, tc.expected, got)
})
}
}
func TestFindProtocolForPort(t *testing.T) {
http := "HTTP"
cases := []struct {
port v1.ServicePort
expectedProtocol string
}{
{
port: v1.ServicePort{
Name: "http",
Protocol: v1.ProtocolTCP,
},
expectedProtocol: "HTTP",
},
{
port: v1.ServicePort{
Name: "GRPC-port",
Protocol: v1.ProtocolTCP,
},
expectedProtocol: "GRPC",
},
{
port: v1.ServicePort{
AppProtocol: &http,
Protocol: v1.ProtocolTCP,
},
expectedProtocol: "HTTP",
},
{
port: v1.ServicePort{
Protocol: v1.ProtocolTCP,
Port: 80,
},
expectedProtocol: "auto-detect",
},
{
port: v1.ServicePort{
Protocol: v1.ProtocolUDP,
Port: 80,
},
expectedProtocol: "UDP",
},
}
for _, tc := range cases {
protocol := findProtocolForPort(&tc.port)
if protocol != tc.expectedProtocol {
t.Fatalf("Output didn't match for the port protocol: got %s want %s", protocol, tc.expectedProtocol)
}
}
}
func verifyExecAndK8sConfigTestCaseTestOutput(t *testing.T, c execAndK8sConfigTestCase) {
t.Helper()
// Override the Istio config factory
configStoreFactory = mockClientFactoryGenerator()
// Override the K8s config factory
interfaceFactory = mockInterfaceFactoryGenerator(c.k8sConfigs)
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.expectedOutput != "" && c.expectedOutput != output {
t.Fatalf("Unexpected output for 'istioctl %s'\n got: %q\nwant: %q", strings.Join(c.args, " "), output, c.expectedOutput)
}
if c.expectedString != "" && !strings.Contains(output, c.expectedString) {
t.Fatalf("Output didn't match for 'istioctl %s'\n got %v\nwant: %v", strings.Join(c.args, " "), output, c.expectedString)
}
if c.goldenFilename != "" {
util.CompareContent(t, []byte(output), c.goldenFilename)
}
if c.wantException {
if fErr == nil {
t.Fatalf("Wanted an exception for 'istioctl %s', didn't get one, output was %q",
strings.Join(c.args, " "), output)
}
} else {
if fErr != nil {
t.Fatalf("Unwanted exception for 'istioctl %s': %v", strings.Join(c.args, " "), fErr)
}
}
}
func mockInterfaceFactoryGenerator(k8sConfigs []runtime.Object) func(kubeconfig string) (kubernetes.Interface, error) {
outFactory := func(_ string) (kubernetes.Interface, error) {
client := fake.NewSimpleClientset(k8sConfigs...)
return client, nil
}
return outFactory
}
func TestGetIstioVirtualServicePathForSvcFromRoute(t *testing.T) {
tests := []struct {
name string
inputConfig string
inputService v1.Service
inputPort int32
expected string
}{
{
name: "test tls config",
inputConfig: "testdata/describe/tls_config.json",
inputService: v1.Service{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "productpage",
Namespace: "default",
},
Spec: v1.ServiceSpec{},
Status: v1.ServiceStatus{},
},
inputPort: int32(9080),
expected: "/apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/bookinfo",
},
{
name: "test http config",
inputConfig: "testdata/describe/http_config.json",
inputService: v1.Service{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "productpage",
Namespace: "default",
},
Spec: v1.ServiceSpec{},
Status: v1.ServiceStatus{},
},
inputPort: int32(9080),
expected: "/apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/bookinfo",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ic, err := readFile(test.inputConfig)
if err != nil {
t.Fatalf("unable to open file in directory: %s", test.inputConfig)
}
cd := configdump.Wrapper{}
err = cd.UnmarshalJSON(ic)
if err != nil {
t.Fatal(err)
}
out, err := getIstioVirtualServicePathForSvcFromRoute(&cd, test.inputService, test.inputPort)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(test.expected, out); diff != "" {
t.Fatalf("Diff:\n%s", diff)
}
})
}
}