1// Copyright 2019 Istio Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package pilot
16
17import (
18	"encoding/json"
19	"fmt"
20	"regexp"
21	"strings"
22	"testing"
23
24	"github.com/onsi/gomega"
25
26	"istio.io/istio/pkg/test/framework"
27	"istio.io/istio/pkg/test/framework/components/echo"
28	"istio.io/istio/pkg/test/framework/components/echo/echoboot"
29	"istio.io/istio/pkg/test/framework/components/galley"
30	"istio.io/istio/pkg/test/framework/components/istioctl"
31	"istio.io/istio/pkg/test/framework/components/namespace"
32	"istio.io/istio/pkg/test/framework/components/pilot"
33	"istio.io/istio/pkg/test/framework/resource/environment"
34	"istio.io/istio/pkg/test/util/file"
35)
36
37const (
38	describeSvcAOutput = `Service: a\..*
39   Port: grpc 7070/GRPC targets pod port 7070
40   Port: http 80/HTTP targets pod port 8090
417070 DestinationRule: a\..* for "a"
42   Matching subsets: v1
43   No Traffic Policy
447070 VirtualService: a\..*
45   when headers are end-user=jason
467070 RBAC policies: ns\[.*\]-policy\[integ-test\]-rule\[0\]
4780 DestinationRule: a\..* for "a"
48   Matching subsets: v1
49   No Traffic Policy
5080 VirtualService: a\..*
51   when headers are end-user=jason
52`
53
54	describePodAOutput = `Pod: .*
55   Pod Ports: 7070 \(app\), 8090 \(app\), 8080 \(app\), 3333 \(app\), 15090 \(istio-proxy\)
56--------------------
57Service: a\..*
58   Port: grpc 7070\/GRPC targets pod port 7070
59   Port: http 80\/HTTP targets pod port 8090
607070 DestinationRule: a\..* for "a"
61   Matching subsets: v1
62   No Traffic Policy
637070 VirtualService: a\..*
64   when headers are end-user=jason
657070 RBAC policies: ns\[.*\]-policy\[integ-test\]-rule\[0\]
6680 DestinationRule: a\..* for "a"
67   Matching subsets: v1
68   No Traffic Policy
6980 VirtualService: a\..*
70   when headers are end-user=jason
71`
72
73	addToMeshPodAOutput = `deployment .* updated successfully with Istio sidecar injected.
74Next Step: Add related labels to the deployment to align with Istio's requirement: https://istio.io/docs/setup/kubernetes/additional-setup/requirements/
75`
76	removeFromMeshPodAOutput = `deployment .* updated successfully with Istio sidecar un-injected.`
77)
78
79func TestWait(t *testing.T) {
80	framework.NewTest(t).
81		RequiresEnvironment(environment.Kube).
82		Run(func(ctx framework.TestContext) {
83			ns := namespace.NewOrFail(t, ctx, namespace.Config{
84				Prefix: "default",
85				Inject: true,
86			})
87			g.ApplyConfigOrFail(t, ns, `
88apiVersion: networking.istio.io/v1alpha3
89kind: VirtualService
90metadata:
91  name: reviews
92spec:
93  gateways: [missing-gw]
94  hosts:
95  - reviews
96  http:
97  - route:
98    - destination:
99        host: reviews
100`)
101			istioCtl := istioctl.NewOrFail(ctx, ctx, istioctl.Config{})
102			istioCtl.InvokeOrFail(t, []string{"x", "wait", "VirtualService", "reviews." + ns.Name()})
103		})
104}
105
106// This test requires `--istio.test.env=kube` because it tests istioctl doing PodExec
107// TestVersion does "istioctl version --remote=true" to verify the CLI understands the data plane version data
108func TestVersion(t *testing.T) {
109	framework.
110		NewTest(t).
111		RequiresEnvironment(environment.Kube).
112		Run(func(ctx framework.TestContext) {
113			g := galley.NewOrFail(t, ctx, galley.Config{})
114			_ = pilot.NewOrFail(t, ctx, pilot.Config{Galley: g})
115			cfg := i.Settings()
116
117			istioCtl := istioctl.NewOrFail(ctx, ctx, istioctl.Config{})
118
119			args := []string{"version", "--remote=true", fmt.Sprintf("--istioNamespace=%s", cfg.SystemNamespace)}
120
121			output, _ := istioCtl.InvokeOrFail(t, args)
122
123			// istioctl will return a single "control plane version" if all control plane versions match
124			controlPlaneRegex := regexp.MustCompile(`control plane version: [a-z0-9\-]*`)
125			if controlPlaneRegex.MatchString(output) {
126				return
127			}
128
129			ctx.Logf("Did not find control plane version. This may mean components have different versions.")
130
131			// At this point, we expect the version for each component
132			expectedRegexps := []*regexp.Regexp{
133				regexp.MustCompile(`citadel version: [a-z0-9\-]*`),
134				regexp.MustCompile(`client version: [a-z0-9\-]*`),
135				regexp.MustCompile(`egressgateway version: [a-z0-9\-]*`),
136				regexp.MustCompile(`ingressgateway version: [a-z0-9\-]*`),
137				regexp.MustCompile(`istiod version: [a-z0-9\-]*`),
138				regexp.MustCompile(`galley version: [a-z0-9\-]*`),
139				regexp.MustCompile(`policy version: [a-z0-9\-]*`),
140				regexp.MustCompile(`sidecar-injector version: [a-z0-9\-]*`),
141				regexp.MustCompile(`telemetry version: [a-z0-9\-]*`),
142			}
143			for _, regexp := range expectedRegexps {
144				if !regexp.MatchString(output) {
145					ctx.Fatalf("Output didn't match for 'istioctl %s'\n got %v\nwant: %v",
146						strings.Join(args, " "), output, regexp)
147				}
148			}
149		})
150}
151
152func TestDescribe(t *testing.T) {
153	framework.NewTest(t).
154		RequiresEnvironment(environment.Kube).
155		RunParallel(func(ctx framework.TestContext) {
156			ns := namespace.NewOrFail(ctx, ctx, namespace.Config{
157				Prefix: "istioctl-describe",
158				Inject: true,
159			})
160
161			deployment := file.AsStringOrFail(t, "../istioctl/testdata/a.yaml")
162			g.ApplyConfigOrFail(t, ns, deployment)
163
164			var a echo.Instance
165			echoboot.NewBuilderOrFail(ctx, ctx).
166				With(&a, echoConfig(ns, "a")).
167				BuildOrFail(ctx)
168
169			if err := a.WaitUntilCallable(a); err != nil {
170				t.Fatal(err)
171			}
172			istioCtl := istioctl.NewOrFail(ctx, ctx, istioctl.Config{})
173
174			podID, err := getPodID(a)
175			if err != nil {
176				ctx.Fatalf("Could not get Pod ID: %v", err)
177			}
178
179			var output string
180			var args []string
181			g := gomega.NewGomegaWithT(t)
182
183			// When this test passed the namespace through --namespace it was flakey
184			// because istioctl uses a global variable for namespace, and this test may
185			// run in parallel.
186			args = []string{"--namespace=dummy",
187				"x", "describe", "pod", fmt.Sprintf("%s.%s", podID, ns.Name())}
188			output, _ = istioCtl.InvokeOrFail(t, args)
189			g.Expect(output).To(gomega.MatchRegexp(describePodAOutput))
190
191			args = []string{"--namespace=dummy",
192				"x", "describe", "svc", fmt.Sprintf("a.%s", ns.Name())}
193			output, _ = istioCtl.InvokeOrFail(t, args)
194			g.Expect(output).To(gomega.MatchRegexp(describeSvcAOutput))
195		})
196}
197
198func getPodID(i echo.Instance) (string, error) {
199	wls, err := i.Workloads()
200	if err != nil {
201		return "", nil
202	}
203
204	for _, wl := range wls {
205		hostname := strings.Split(wl.Sidecar().NodeID(), "~")[2]
206		podID := strings.Split(hostname, ".")[0]
207		return podID, nil
208	}
209
210	return "", fmt.Errorf("no workloads")
211}
212
213func TestAddToAndRemoveFromMesh(t *testing.T) {
214	framework.NewTest(t).
215		RequiresEnvironment(environment.Kube).
216		RunParallel(func(ctx framework.TestContext) {
217			ns := namespace.NewOrFail(t, ctx, namespace.Config{
218				Prefix: "istioctl-add-to-mesh",
219				Inject: true,
220			})
221
222			var a echo.Instance
223			echoboot.NewBuilderOrFail(ctx, ctx).
224				With(&a, echoConfig(ns, "a")).
225				BuildOrFail(ctx)
226
227			istioCtl := istioctl.NewOrFail(ctx, ctx, istioctl.Config{})
228
229			var output string
230			var args []string
231			g := gomega.NewGomegaWithT(t)
232
233			// able to remove from mesh when the deployment is auto injected
234			args = []string{fmt.Sprintf("--namespace=%s", ns.Name()),
235				"x", "remove-from-mesh", "service", "a"}
236			output, _ = istioCtl.InvokeOrFail(t, args)
237			g.Expect(output).To(gomega.MatchRegexp(removeFromMeshPodAOutput))
238
239			// remove from mesh should be clean
240			// users can add it back to mesh successfully
241			if err := a.WaitUntilCallable(a); err != nil {
242				t.Fatal(err)
243			}
244
245			args = []string{fmt.Sprintf("--namespace=%s", ns.Name()),
246				"x", "add-to-mesh", "service", "a"}
247			output, _ = istioCtl.InvokeOrFail(t, args)
248			g.Expect(output).To(gomega.MatchRegexp(addToMeshPodAOutput))
249		})
250}
251
252func TestProxyConfig(t *testing.T) {
253	framework.NewTest(t).
254		RequiresEnvironment(environment.Kube).
255		Run(func(ctx framework.TestContext) {
256			ns := namespace.NewOrFail(ctx, ctx, namespace.Config{
257				Prefix: "istioctl-pc",
258				Inject: true,
259			})
260
261			var a echo.Instance
262			echoboot.NewBuilderOrFail(ctx, ctx).
263				With(&a, echoConfig(ns, "a")).
264				BuildOrFail(ctx)
265
266			istioCtl := istioctl.NewOrFail(ctx, ctx, istioctl.Config{})
267
268			podID, err := getPodID(a)
269			if err != nil {
270				ctx.Fatalf("Could not get Pod ID: %v", err)
271			}
272
273			var output string
274			var args []string
275			g := gomega.NewGomegaWithT(t)
276
277			args = []string{"--namespace=dummy",
278				"pc", "bootstrap", fmt.Sprintf("%s.%s", podID, ns.Name())}
279			output, _ = istioCtl.InvokeOrFail(t, args)
280			jsonOutput := jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
281			g.Expect(jsonOutput).To(gomega.HaveKey("bootstrap"))
282
283			args = []string{"--namespace=dummy",
284				"pc", "cluster", fmt.Sprintf("%s.%s", podID, ns.Name()), "-o", "json"}
285			output, _ = istioCtl.InvokeOrFail(t, args)
286			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
287			g.Expect(jsonOutput).To(gomega.Not(gomega.BeEmpty()))
288
289			args = []string{"--namespace=dummy",
290				"pc", "endpoint", fmt.Sprintf("%s.%s", podID, ns.Name()), "-o", "json"}
291			output, _ = istioCtl.InvokeOrFail(t, args)
292			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
293			g.Expect(jsonOutput).To(gomega.Not(gomega.BeEmpty()))
294
295			args = []string{"--namespace=dummy",
296				"pc", "listener", fmt.Sprintf("%s.%s", podID, ns.Name()), "-o", "json"}
297			output, _ = istioCtl.InvokeOrFail(t, args)
298			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
299			g.Expect(jsonOutput).To(gomega.Not(gomega.BeEmpty()))
300
301			args = []string{"--namespace=dummy",
302				"pc", "route", fmt.Sprintf("%s.%s", podID, ns.Name()), "-o", "json"}
303			output, _ = istioCtl.InvokeOrFail(t, args)
304			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
305			g.Expect(jsonOutput).To(gomega.Not(gomega.BeEmpty()))
306
307			args = []string{"--namespace=dummy",
308				"pc", "secret", fmt.Sprintf("%s.%s", podID, ns.Name()), "-o", "json"}
309			output, _ = istioCtl.InvokeOrFail(t, args)
310			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
311			g.Expect(jsonOutput).To(gomega.HaveKey("dynamicActiveSecrets"))
312		})
313}
314
315func jsonUnmarshallOrFail(t *testing.T, context, s string) interface{} {
316	t.Helper()
317	var val interface{}
318
319	// this is guarded by prettyPrint
320	if err := json.Unmarshal([]byte(s), &val); err != nil {
321		t.Fatalf("Could not unmarshal %s response %s", context, s)
322	}
323	return val
324}
325