istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/istioctl_test.go (about)

     1  //go:build integ
     2  // +build integ
     3  
     4  // Copyright Istio Authors
     5  //
     6  // Licensed under the Apache License, Version 2.0 (the "License");
     7  // you may not use this file except in compliance with the License.
     8  // You may obtain a copy of the License at
     9  //
    10  //     http://www.apache.org/licenses/LICENSE-2.0
    11  //
    12  // Unless required by applicable law or agreed to in writing, software
    13  // distributed under the License is distributed on an "AS IS" BASIS,
    14  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  // See the License for the specific language governing permissions and
    16  // limitations under the License.
    17  
    18  package pilot
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	admin "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
    32  	. "github.com/onsi/gomega"
    33  
    34  	"istio.io/istio/pkg/test"
    35  	"istio.io/istio/pkg/test/framework"
    36  	"istio.io/istio/pkg/test/framework/components/echo"
    37  	commonDeployment "istio.io/istio/pkg/test/framework/components/echo/common/deployment"
    38  	"istio.io/istio/pkg/test/framework/components/istioctl"
    39  	"istio.io/istio/pkg/test/util/retry"
    40  	"istio.io/istio/pkg/util/protomarshal"
    41  )
    42  
    43  var (
    44  	// The full describe output is much larger, but testing for it requires a change anytime the test
    45  	// app changes which is tedious. Instead, just check a minimum subset; unit test cover the
    46  	// details.
    47  	describeSvcAOutput = regexp.MustCompile(`(?s)Service: a\..*
    48     Port: http 80/HTTP targets pod port 18080
    49  .*
    50  80:
    51     DestinationRule: a\..* for "a"
    52        Matching subsets: v1
    53        No Traffic Policy
    54  `)
    55  
    56  	describePodAOutput = describeSvcAOutput
    57  )
    58  
    59  // This test requires `--istio.test.env=kube` because it tests istioctl doing PodExec
    60  // TestVersion does "istioctl version --remote=true" to verify the CLI understands the data plane version data
    61  func TestVersion(t *testing.T) {
    62  	// nolint: staticcheck
    63  	framework.
    64  		NewTest(t).RequiresSingleCluster().
    65  		Run(func(t framework.TestContext) {
    66  			cfg := i.Settings()
    67  
    68  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{Cluster: t.Environment().Clusters()[0]})
    69  			args := []string{"version", "--remote=true", fmt.Sprintf("--istioNamespace=%s", cfg.SystemNamespace)}
    70  
    71  			output, _ := istioCtl.InvokeOrFail(t, args)
    72  
    73  			// istioctl will return a single "control plane version" if all control plane versions match
    74  			controlPlaneRegex := regexp.MustCompile(`control plane version: [a-z0-9\-]*`)
    75  			if controlPlaneRegex.MatchString(output) {
    76  				return
    77  			}
    78  
    79  			t.Fatalf("Did not find control plane version: %v", output)
    80  		})
    81  }
    82  
    83  // This test requires `--istio.test.env=kube` because it tests istioctl doing PodExec
    84  // TestVersion does "istioctl version --remote=true" to verify the CLI understands the data plane version data
    85  func TestXdsVersion(t *testing.T) {
    86  	// nolint: staticcheck
    87  	framework.
    88  		NewTest(t).RequiresSingleCluster().
    89  		RequireIstioVersion("1.10.0").
    90  		Run(func(t framework.TestContext) {
    91  			cfg := i.Settings()
    92  
    93  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{Cluster: t.Clusters().Default()})
    94  			args := []string{"x", "version", "--remote=true", fmt.Sprintf("--istioNamespace=%s", cfg.SystemNamespace)}
    95  
    96  			output, _ := istioCtl.InvokeOrFail(t, args)
    97  
    98  			// istioctl will return a single "control plane version" if all control plane versions match.
    99  			// This test accepts any version with a "." (period) in it -- we mostly want to fail on "MISSING CP VERSION"
   100  			controlPlaneRegex := regexp.MustCompile(`control plane version: [a-z0-9\-]+\.[a-z0-9\-]+`)
   101  			if controlPlaneRegex.MatchString(output) {
   102  				return
   103  			}
   104  
   105  			t.Fatalf("Did not find valid control plane version: %v", output)
   106  		})
   107  }
   108  
   109  func TestDescribe(t *testing.T) {
   110  	// nolint: staticcheck
   111  	framework.NewTest(t).RequiresSingleCluster().
   112  		Run(func(t framework.TestContext) {
   113  			t.ConfigIstio().File(apps.Namespace.Name(), "testdata/a.yaml").ApplyOrFail(t)
   114  
   115  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   116  
   117  			// When this test passed the namespace through --namespace it was flakey
   118  			// because istioctl uses a global variable for namespace, and this test may
   119  			// run in parallel.
   120  			retry.UntilSuccessOrFail(t, func() error {
   121  				args := []string{
   122  					"--namespace=dummy",
   123  					"x", "describe", "svc", fmt.Sprintf("%s.%s", commonDeployment.ASvc, apps.Namespace.Name()),
   124  				}
   125  				output, _, err := istioCtl.Invoke(args)
   126  				if err != nil {
   127  					return err
   128  				}
   129  				if !describeSvcAOutput.MatchString(output) {
   130  					return fmt.Errorf("output:\n%v\n does not match regex:\n%v", output, describeSvcAOutput)
   131  				}
   132  				return nil
   133  			}, retry.Timeout(time.Second*20))
   134  
   135  			retry.UntilSuccessOrFail(t, func() error {
   136  				podID, err := getPodID(apps.A[0])
   137  				if err != nil {
   138  					return fmt.Errorf("could not get Pod ID: %v", err)
   139  				}
   140  				args := []string{
   141  					"--namespace=dummy",
   142  					"x", "describe", "pod", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()),
   143  				}
   144  				output, _, err := istioCtl.Invoke(args)
   145  				if err != nil {
   146  					return err
   147  				}
   148  				if !describePodAOutput.MatchString(output) {
   149  					return fmt.Errorf("output:\n%v\n does not match regex:\n%v", output, describePodAOutput)
   150  				}
   151  				return nil
   152  			}, retry.Timeout(time.Second*20))
   153  		})
   154  }
   155  
   156  func getPodID(i echo.Instance) (string, error) {
   157  	wls, err := i.Workloads()
   158  	if err != nil {
   159  		return "", nil
   160  	}
   161  
   162  	for _, wl := range wls {
   163  		return wl.PodName(), nil
   164  	}
   165  
   166  	return "", fmt.Errorf("no workloads")
   167  }
   168  
   169  func TestProxyConfig(t *testing.T) {
   170  	// nolint: staticcheck
   171  	framework.NewTest(t).RequiresSingleCluster().
   172  		Run(func(t framework.TestContext) {
   173  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   174  
   175  			podID, err := getPodID(apps.A[0])
   176  			if err != nil {
   177  				t.Fatalf("Could not get Pod ID: %v", err)
   178  			}
   179  
   180  			var output string
   181  			var args []string
   182  			g := NewWithT(t)
   183  
   184  			args = []string{
   185  				"--namespace=dummy",
   186  				"pc", "bootstrap", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()),
   187  			}
   188  			output, _ = istioCtl.InvokeOrFail(t, args)
   189  			jsonOutput := jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
   190  			g.Expect(jsonOutput).To(HaveKey("bootstrap"))
   191  
   192  			args = []string{
   193  				"--namespace=dummy",
   194  				"pc", "cluster", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()), "-o", "json",
   195  			}
   196  			output, _ = istioCtl.InvokeOrFail(t, args)
   197  			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
   198  			g.Expect(jsonOutput).To(Not(BeEmpty()))
   199  
   200  			args = []string{
   201  				"--namespace=dummy",
   202  				"pc", "endpoint", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()), "-o", "json",
   203  			}
   204  			output, _ = istioCtl.InvokeOrFail(t, args)
   205  			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
   206  			g.Expect(jsonOutput).To(Not(BeEmpty()))
   207  
   208  			args = []string{
   209  				"--namespace=dummy",
   210  				"pc", "listener", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()), "-o", "json",
   211  			}
   212  			output, _ = istioCtl.InvokeOrFail(t, args)
   213  			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
   214  			g.Expect(jsonOutput).To(Not(BeEmpty()))
   215  
   216  			args = []string{
   217  				"--namespace=dummy",
   218  				"pc", "route", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()), "-o", "json",
   219  			}
   220  			output, _ = istioCtl.InvokeOrFail(t, args)
   221  			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
   222  			g.Expect(jsonOutput).To(Not(BeEmpty()))
   223  
   224  			args = []string{
   225  				"--namespace=dummy",
   226  				"pc", "all", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()), "-o", "json",
   227  			}
   228  			output, _ = istioCtl.InvokeOrFail(t, args)
   229  			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
   230  			dumpAll, ok := jsonOutput.(map[string]any)
   231  			if !ok {
   232  				t.Fatalf("Failed to parse istioctl %s config dump to top level map", strings.Join(args, " "))
   233  			}
   234  			rawConfigs, ok := dumpAll["configs"].([]any)
   235  			if !ok {
   236  				t.Fatalf("Failed to parse istioctl %s config dump to slice of any", strings.Join(args, " "))
   237  			}
   238  			hasEndpoints := false
   239  			for _, rawConfig := range rawConfigs {
   240  				configDump, ok := rawConfig.(map[string]any)
   241  				if !ok {
   242  					t.Fatalf("Failed to parse istioctl %s raw config dump element to map of any", strings.Join(args, " "))
   243  				}
   244  				if configDump["@type"] == "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump" {
   245  					hasEndpoints = true
   246  					break
   247  				}
   248  			}
   249  
   250  			g.Expect(hasEndpoints).To(BeTrue())
   251  
   252  			args = []string{
   253  				"--namespace=dummy",
   254  				"pc", "secret", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()), "-o", "json",
   255  			}
   256  			output, _ = istioCtl.InvokeOrFail(t, args)
   257  			jsonOutput = jsonUnmarshallOrFail(t, strings.Join(args, " "), output)
   258  			g.Expect(jsonOutput).To(HaveKey("dynamicActiveSecrets"))
   259  			dump := &admin.SecretsConfigDump{}
   260  			if err := protomarshal.Unmarshal([]byte(output), dump); err != nil {
   261  				t.Fatal(err)
   262  			}
   263  			if len(dump.DynamicWarmingSecrets) > 0 {
   264  				t.Fatalf("found warming secrets: %v", output)
   265  			}
   266  			if len(dump.DynamicActiveSecrets) != 2 {
   267  				// If the config for the SDS does not align in all locations, we may get duplicates.
   268  				// This check ensures we do not. If this is failing, check to ensure the bootstrap config matches
   269  				// the XDS response.
   270  				t.Fatalf("found unexpected secrets, should have only default and ROOTCA: %v", output)
   271  			}
   272  		})
   273  }
   274  
   275  func jsonUnmarshallOrFail(t test.Failer, context, s string) any {
   276  	t.Helper()
   277  	var val any
   278  
   279  	// this is guarded by prettyPrint
   280  	if err := json.Unmarshal([]byte(s), &val); err != nil {
   281  		t.Fatalf("Could not unmarshal %s response %s", context, s)
   282  	}
   283  	return val
   284  }
   285  
   286  func TestProxyStatus(t *testing.T) {
   287  	// nolint: staticcheck
   288  	framework.NewTest(t).RequiresSingleCluster().
   289  		RequiresLocalControlPlane(). // https://github.com/istio/istio/issues/37051
   290  		Run(func(t framework.TestContext) {
   291  			const timeoutFlag = "--timeout=10s"
   292  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   293  
   294  			podID, err := getPodID(apps.A[0])
   295  			if err != nil {
   296  				t.Fatalf("Could not get Pod ID: %v", err)
   297  			}
   298  
   299  			var output string
   300  			var args []string
   301  
   302  			expectSubstrings := func(have string, wants ...string) error {
   303  				for _, want := range wants {
   304  					if !strings.Contains(have, want) {
   305  						return fmt.Errorf("substring %q not found; have %q", want, have)
   306  					}
   307  				}
   308  				return nil
   309  			}
   310  			retry.UntilSuccessOrFail(t, func() error {
   311  				args = []string{"proxy-status", timeoutFlag}
   312  				output, _ = istioCtl.InvokeOrFail(t, args)
   313  				return expectSubstrings(output, fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()))
   314  			})
   315  
   316  			retry.UntilSuccessOrFail(t, func() error {
   317  				args = []string{
   318  					"proxy-status", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()), timeoutFlag,
   319  				}
   320  				output, _, err := istioCtl.Invoke(args)
   321  				if err != nil {
   322  					return err
   323  				}
   324  				return expectSubstrings(output, "Clusters Match", "Listeners Match", "Routes Match")
   325  			})
   326  
   327  			// test the --file param
   328  			retry.UntilSuccessOrFail(t, func() error {
   329  				d := t.TempDir()
   330  				filename := filepath.Join(d, "ps-configdump.json")
   331  				cs := t.Clusters().Default()
   332  				dump, err := cs.EnvoyDo(context.TODO(), podID, apps.Namespace.Name(), "GET", "config_dump")
   333  				if err != nil {
   334  					return err
   335  				}
   336  				err = os.WriteFile(filename, dump, os.ModePerm)
   337  				if err != nil {
   338  					return err
   339  				}
   340  				args = []string{
   341  					"proxy-status", fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()), "--file", filename, timeoutFlag,
   342  				}
   343  				output, _, err = istioCtl.Invoke(args)
   344  				if err != nil {
   345  					return err
   346  				}
   347  				return expectSubstrings(output, "Clusters Match", "Listeners Match", "Routes Match")
   348  			})
   349  
   350  			// test namespace filtering
   351  			retry.UntilSuccessOrFail(t, func() error {
   352  				args = []string{"proxy-status", "-n", apps.Namespace.Name(), timeoutFlag}
   353  				output, _ = istioCtl.InvokeOrFail(t, args)
   354  				return expectSubstrings(output, fmt.Sprintf("%s.%s", podID, apps.Namespace.Name()))
   355  			})
   356  		})
   357  }
   358  
   359  func TestAuthZCheck(t *testing.T) {
   360  	// nolint: staticcheck
   361  	framework.NewTest(t).RequiresSingleCluster().
   362  		Run(func(t framework.TestContext) {
   363  			istioLabel := "ingressgateway"
   364  			if labelOverride := i.Settings().IngressGatewayIstioLabel; labelOverride != "" {
   365  				istioLabel = labelOverride
   366  			}
   367  			t.ConfigIstio().File(apps.Namespace.Name(), "testdata/authz-a.yaml").ApplyOrFail(t)
   368  			t.ConfigIstio().EvalFile(i.Settings().SystemNamespace, map[string]any{
   369  				"GatewayIstioLabel": istioLabel,
   370  			}, "testdata/authz-b.yaml").ApplyOrFail(t)
   371  
   372  			gwPod, err := i.IngressFor(t.Clusters().Default()).PodID(0)
   373  			if err != nil {
   374  				t.Fatalf("Could not get Pod ID: %v", err)
   375  			}
   376  			appPod, err := getPodID(apps.A[0])
   377  			if err != nil {
   378  				t.Fatalf("Could not get Pod ID: %v", err)
   379  			}
   380  
   381  			cases := []struct {
   382  				name  string
   383  				pod   string
   384  				wants []*regexp.Regexp
   385  			}{
   386  				{
   387  					name: "ingressgateway",
   388  					pod:  fmt.Sprintf("%s.%s", gwPod, i.Settings().SystemNamespace),
   389  					wants: []*regexp.Regexp{
   390  						regexp.MustCompile(fmt.Sprintf(`DENY\s+deny-policy\.%s\s+2`, i.Settings().SystemNamespace)),
   391  						regexp.MustCompile(fmt.Sprintf(`ALLOW\s+allow-policy\.%s\s+1`, i.Settings().SystemNamespace)),
   392  					},
   393  				},
   394  				{
   395  					name: "workload",
   396  					pod:  fmt.Sprintf("%s.%s", appPod, apps.Namespace.Name()),
   397  					wants: []*regexp.Regexp{
   398  						regexp.MustCompile(fmt.Sprintf(`DENY\s+deny-policy\.%s\s+2`, apps.Namespace.Name())),
   399  						regexp.MustCompile(`ALLOW\s+_anonymous_match_nothing_\s+1`),
   400  						regexp.MustCompile(fmt.Sprintf(`ALLOW\s+allow-policy\.%s\s+1`, apps.Namespace.Name())),
   401  					},
   402  				},
   403  			}
   404  
   405  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{Cluster: t.Clusters().Default()})
   406  			for _, c := range cases {
   407  				args := []string{"experimental", "authz", "check", c.pod}
   408  				t.NewSubTest(c.name).Run(func(t framework.TestContext) {
   409  					// Verify the output matches the expected text, which is the policies loaded above.
   410  					retry.UntilSuccessOrFail(t, func() error {
   411  						output, _, err := istioCtl.Invoke(args)
   412  						if err != nil {
   413  							return err
   414  						}
   415  						for _, want := range c.wants {
   416  							if !want.MatchString(output) {
   417  								return fmt.Errorf("%v did not match %v", output, want)
   418  							}
   419  						}
   420  						return nil
   421  					}, retry.Timeout(time.Second*30))
   422  				})
   423  			}
   424  		})
   425  }
   426  
   427  func TestKubeInject(t *testing.T) {
   428  	// nolint: staticcheck
   429  	framework.NewTest(t).RequiresSingleCluster().
   430  		Run(func(t framework.TestContext) {
   431  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   432  			var output string
   433  			args := []string{"kube-inject", "-f", "testdata/hello.yaml", "--revision=" + t.Settings().Revisions.Default()}
   434  			output, _ = istioCtl.InvokeOrFail(t, args)
   435  			if !strings.Contains(output, "istio-proxy") {
   436  				t.Fatal("istio-proxy has not been injected")
   437  			}
   438  		})
   439  }
   440  
   441  func TestRemoteClusters(t *testing.T) {
   442  	// nolint: staticcheck
   443  	framework.NewTest(t).RequiresMinClusters(2).
   444  		Run(func(t framework.TestContext) {
   445  			for _, cluster := range t.Clusters().Primaries() {
   446  				cluster := cluster
   447  				t.NewSubTest(cluster.StableName()).Run(func(t framework.TestContext) {
   448  					istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{Cluster: cluster})
   449  					var output string
   450  					args := []string{"remote-clusters"}
   451  					output, _ = istioCtl.InvokeOrFail(t, args)
   452  					for _, otherName := range t.Clusters().Exclude(cluster).Names() {
   453  						if !strings.Contains(output, otherName) {
   454  							t.Fatalf("remote-clusters output did not contain %s; got:\n%s", otherName, output)
   455  						}
   456  					}
   457  				})
   458  			}
   459  		})
   460  }