istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/analyze_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  	"encoding/json"
    22  	"fmt"
    23  	"strings"
    24  	"testing"
    25  
    26  	. "github.com/onsi/gomega"
    27  
    28  	"istio.io/istio/istioctl/pkg/analyze"
    29  	"istio.io/istio/pkg/config/analysis/diag"
    30  	"istio.io/istio/pkg/config/analysis/msg"
    31  	"istio.io/istio/pkg/test"
    32  	"istio.io/istio/pkg/test/framework"
    33  	"istio.io/istio/pkg/test/framework/components/istioctl"
    34  	"istio.io/istio/pkg/test/framework/components/namespace"
    35  	"istio.io/istio/tests/integration/helm"
    36  )
    37  
    38  const (
    39  	gatewayFile          = "testdata/gateway.yaml"
    40  	jsonGatewayFile      = "testdata/gateway.json"
    41  	destinationRuleFile  = "testdata/destinationrule.yaml"
    42  	virtualServiceFile   = "testdata/virtualservice.yaml"
    43  	invalidFile          = "testdata/invalid.yaml"
    44  	invalidExtensionFile = "testdata/invalid.md"
    45  	dirWithConfig        = "testdata/some-dir/"
    46  	jsonOutput           = "-ojson"
    47  )
    48  
    49  var analyzerFoundIssuesError = analyze.AnalyzerFoundIssuesError{}
    50  
    51  func TestEmptyCluster(t *testing.T) {
    52  	// nolint: staticcheck
    53  	framework.
    54  		NewTest(t).
    55  		RequiresSingleCluster().
    56  		Run(func(t framework.TestContext) {
    57  			g := NewWithT(t)
    58  
    59  			ns := namespace.NewOrFail(t, t, namespace.Config{
    60  				Prefix: "istioctl-analyze",
    61  				Inject: true,
    62  			})
    63  
    64  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
    65  
    66  			// For a clean istio install with injection enabled, expect no validation errors
    67  			output, err := istioctlSafe(t, istioCtl, ns.Name(), true)
    68  			expectNoMessages(t, g, output)
    69  			g.Expect(err).To(BeNil())
    70  		})
    71  }
    72  
    73  func TestFileOnly(t *testing.T) {
    74  	// nolint: staticcheck
    75  	framework.
    76  		NewTest(t).
    77  		RequiresSingleCluster().
    78  		Run(func(t framework.TestContext) {
    79  			g := NewWithT(t)
    80  
    81  			ns := namespace.NewOrFail(t, t, namespace.Config{
    82  				Prefix: "istioctl-analyze",
    83  				Inject: true,
    84  			})
    85  
    86  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
    87  
    88  			// Validation error if we have a virtual service with subset not defined.
    89  			output, err := istioctlSafe(t, istioCtl, ns.Name(), false, virtualServiceFile)
    90  			expectMessages(t, g, output, msg.ReferencedResourceNotFound)
    91  			g.Expect(err).To(BeIdenticalTo(analyzerFoundIssuesError))
    92  
    93  			// Error goes away if we define the subset in the destination rule.
    94  			output, err = istioctlSafe(t, istioCtl, ns.Name(), false, destinationRuleFile)
    95  			expectNoMessages(t, g, output)
    96  			g.Expect(err).To(BeNil())
    97  		})
    98  }
    99  
   100  func TestDirectoryWithoutRecursion(t *testing.T) {
   101  	// nolint: staticcheck
   102  	framework.
   103  		NewTest(t).
   104  		RequiresSingleCluster().
   105  		Run(func(t framework.TestContext) {
   106  			g := NewWithT(t)
   107  
   108  			ns := namespace.NewOrFail(t, t, namespace.Config{
   109  				Prefix: "istioctl-analyze",
   110  				Inject: true,
   111  			})
   112  
   113  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   114  
   115  			// Recursive is false, so we should only analyze
   116  			// testdata/some-dir/missing-gateway.yaml and get a
   117  			// SchemaValidationError (if we did recurse, we'd get a
   118  			// UnknownAnnotation as well).
   119  			output, err := istioctlSafe(t, istioCtl, ns.Name(), false, "--recursive=false", dirWithConfig)
   120  			expectMessages(t, g, output, msg.SchemaValidationError)
   121  			g.Expect(err).To(BeIdenticalTo(analyzerFoundIssuesError))
   122  		})
   123  }
   124  
   125  func TestDirectoryWithRecursion(t *testing.T) {
   126  	// nolint: staticcheck
   127  	framework.
   128  		NewTest(t).
   129  		RequiresSingleCluster().
   130  		Run(func(t framework.TestContext) {
   131  			g := NewWithT(t)
   132  
   133  			ns := namespace.NewOrFail(t, t, namespace.Config{
   134  				Prefix: "istioctl-analyze",
   135  				Inject: true,
   136  			})
   137  
   138  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   139  
   140  			// Recursive is true, so we should see one error (SchemaValidationError).
   141  			output, err := istioctlSafe(t, istioCtl, ns.Name(), false, "--recursive=true", dirWithConfig)
   142  			expectMessages(t, g, output, msg.SchemaValidationError)
   143  			g.Expect(err).To(BeIdenticalTo(analyzerFoundIssuesError))
   144  		})
   145  }
   146  
   147  func TestInvalidFileError(t *testing.T) {
   148  	// nolint: staticcheck
   149  	framework.
   150  		NewTest(t).
   151  		RequiresSingleCluster().
   152  		Run(func(t framework.TestContext) {
   153  			g := NewWithT(t)
   154  
   155  			ns := namespace.NewOrFail(t, t, namespace.Config{
   156  				Prefix: "istioctl-analyze",
   157  				Inject: true,
   158  			})
   159  
   160  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   161  
   162  			// Skip the file with invalid extension and produce no errors.
   163  			output, err := istioctlSafe(t, istioCtl, ns.Name(), false, invalidExtensionFile)
   164  			g.Expect(output[0]).To(ContainSubstring(fmt.Sprintf("Skipping file %v, recognized file extensions are: [.json .yaml .yml]", invalidExtensionFile)))
   165  			g.Expect(err).To(BeNil())
   166  
   167  			// Parse error as the yaml file itself is not valid yaml.
   168  			output, err = istioctlSafe(t, istioCtl, ns.Name(), false, invalidFile)
   169  			g.Expect(strings.Join(output, "\n")).To(ContainSubstring("Error(s) adding files"))
   170  			g.Expect(strings.Join(output, "\n")).To(ContainSubstring(fmt.Sprintf("errors parsing content \"%s\"", invalidFile)))
   171  
   172  			g.Expect(err).To(MatchError(analyze.FileParseError{}))
   173  
   174  			// Parse error as the yaml file itself is not valid yaml, but ignore.
   175  			output, err = istioctlSafe(t, istioCtl, ns.Name(), false, invalidFile, "--ignore-unknown=true")
   176  			g.Expect(strings.Join(output, "\n")).To(ContainSubstring("Error(s) adding files"))
   177  			g.Expect(strings.Join(output, "\n")).To(ContainSubstring(fmt.Sprintf("errors parsing content \"%s\"", invalidFile)))
   178  
   179  			g.Expect(err).To(BeNil())
   180  		})
   181  }
   182  
   183  func TestJsonInputFile(t *testing.T) {
   184  	// nolint: staticcheck
   185  	framework.
   186  		NewTest(t).
   187  		RequiresSingleCluster().
   188  		Run(func(t framework.TestContext) {
   189  			g := NewWithT(t)
   190  
   191  			ns := namespace.NewOrFail(t, t, namespace.Config{
   192  				Prefix: "istioctl-analyze",
   193  				Inject: true,
   194  			})
   195  
   196  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   197  
   198  			// Validation error if we have a gateway with invalid selector.
   199  			applyFileOrFail(t, ns.Name(), jsonGatewayFile)
   200  			output, err := istioctlSafe(t, istioCtl, ns.Name(), true)
   201  			expectMessages(t, g, output, msg.ReferencedResourceNotFound)
   202  			g.Expect(err).To(BeIdenticalTo(analyzerFoundIssuesError))
   203  		})
   204  }
   205  
   206  func TestJsonOutput(t *testing.T) {
   207  	// nolint: staticcheck
   208  	framework.
   209  		NewTest(t).
   210  		RequiresSingleCluster().
   211  		Run(func(t framework.TestContext) {
   212  			g := NewWithT(t)
   213  
   214  			ns := namespace.NewOrFail(t, t, namespace.Config{
   215  				Prefix: "istioctl-analyze",
   216  				Inject: true,
   217  			})
   218  
   219  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   220  
   221  			t.NewSubTest("no other output except analysis json output").Run(func(t framework.TestContext) {
   222  				applyFileOrFail(t, ns.Name(), jsonGatewayFile)
   223  				stdout, _, err := istioctlWithStderr(t, istioCtl, ns.Name(), true, jsonOutput)
   224  				expectJSONMessages(t, g, stdout, msg.ReferencedResourceNotFound)
   225  				g.Expect(err).To(BeNil())
   226  			})
   227  
   228  			t.NewSubTest("invalid file does not output error in stdout").Run(func(t framework.TestContext) {
   229  				stdout, _, err := istioctlWithStderr(t, istioCtl, ns.Name(), false, invalidExtensionFile, jsonOutput)
   230  				expectJSONMessages(t, g, stdout)
   231  				g.Expect(err).To(BeNil())
   232  			})
   233  		})
   234  }
   235  
   236  func TestKubeOnly(t *testing.T) {
   237  	// nolint: staticcheck
   238  	framework.
   239  		NewTest(t).
   240  		RequiresSingleCluster().
   241  		Run(func(t framework.TestContext) {
   242  			g := NewWithT(t)
   243  
   244  			ns := namespace.NewOrFail(t, t, namespace.Config{
   245  				Prefix: "istioctl-analyze",
   246  				Inject: true,
   247  			})
   248  
   249  			applyFileOrFail(t, ns.Name(), gatewayFile)
   250  
   251  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   252  
   253  			// Validation error if we have a gateway with invalid selector.
   254  			output, err := istioctlSafe(t, istioCtl, ns.Name(), true)
   255  			expectMessages(t, g, output, msg.ReferencedResourceNotFound)
   256  			g.Expect(err).To(BeIdenticalTo(analyzerFoundIssuesError))
   257  		})
   258  }
   259  
   260  func TestFileAndKubeCombined(t *testing.T) {
   261  	// nolint: staticcheck
   262  	framework.
   263  		NewTest(t).
   264  		RequiresSingleCluster().
   265  		Run(func(t framework.TestContext) {
   266  			g := NewWithT(t)
   267  
   268  			ns := namespace.NewOrFail(t, t, namespace.Config{
   269  				Prefix: "istioctl-analyze",
   270  				Inject: true,
   271  			})
   272  
   273  			applyFileOrFail(t, ns.Name(), virtualServiceFile)
   274  
   275  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   276  
   277  			// Simulating applying the destination rule that defines the subset, we should
   278  			// fix the error and thus see no message
   279  			output, err := istioctlSafe(t, istioCtl, ns.Name(), true, destinationRuleFile)
   280  			expectNoMessages(t, g, output)
   281  			g.Expect(err).To(BeNil())
   282  		})
   283  }
   284  
   285  func TestAllNamespaces(t *testing.T) {
   286  	// nolint: staticcheck
   287  	framework.
   288  		NewTest(t).
   289  		RequiresSingleCluster().
   290  		Run(func(t framework.TestContext) {
   291  			g := NewWithT(t)
   292  
   293  			ns1 := namespace.NewOrFail(t, t, namespace.Config{
   294  				Prefix: "istioctl-analyze-1",
   295  				Inject: true,
   296  			})
   297  			ns2 := namespace.NewOrFail(t, t, namespace.Config{
   298  				Prefix: "istioctl-analyze-2",
   299  				Inject: true,
   300  			})
   301  
   302  			applyFileOrFail(t, ns1.Name(), gatewayFile)
   303  			applyFileOrFail(t, ns2.Name(), gatewayFile)
   304  
   305  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   306  
   307  			// If we look at one namespace, we should successfully run and see one message (and not anything from any other namespace)
   308  			output, _ := istioctlSafe(t, istioCtl, ns1.Name(), true)
   309  			expectMessages(t, g, output, msg.ReferencedResourceNotFound, msg.ConflictingGateways)
   310  
   311  			// If we use --all-namespaces, we should successfully run and see a message from each namespace
   312  			output, _ = istioctlSafe(t, istioCtl, "", true, "--all-namespaces")
   313  			// Since this test runs in a cluster with lots of other namespaces we don't actually care about, only look for ns1 and ns2
   314  			foundCount := 0
   315  			for _, line := range output {
   316  				if strings.Contains(line, ns1.Name()) {
   317  					if strings.Contains(line, msg.ReferencedResourceNotFound.Code()) {
   318  						g.Expect(line).To(ContainSubstring(msg.ReferencedResourceNotFound.Code()))
   319  						foundCount++
   320  					}
   321  					// There are 2 conflictings can be detected, A to B and B to A
   322  					if strings.Contains(line, msg.ConflictingGateways.Code()) {
   323  						g.Expect(line).To(ContainSubstring(msg.ConflictingGateways.Code()))
   324  						foundCount++
   325  					}
   326  				}
   327  				if strings.Contains(line, ns2.Name()) {
   328  					if strings.Contains(line, msg.ReferencedResourceNotFound.Code()) {
   329  						g.Expect(line).To(ContainSubstring(msg.ReferencedResourceNotFound.Code()))
   330  						foundCount++
   331  					}
   332  					// There are 2 conflictings can be detected, B to A and A to B
   333  					if strings.Contains(line, msg.ConflictingGateways.Code()) {
   334  						g.Expect(line).To(ContainSubstring(msg.ConflictingGateways.Code()))
   335  						foundCount++
   336  					}
   337  				}
   338  			}
   339  			g.Expect(foundCount).To(Equal(6))
   340  		})
   341  }
   342  
   343  func TestTimeout(t *testing.T) {
   344  	t.Skip("https://github.com/istio/istio/issues/25893")
   345  	// nolint: staticcheck
   346  	framework.
   347  		NewTest(t).
   348  		RequiresSingleCluster().
   349  		Run(func(t framework.TestContext) {
   350  			g := NewWithT(t)
   351  
   352  			ns := namespace.NewOrFail(t, t, namespace.Config{
   353  				Prefix: "istioctl-analyze",
   354  				Inject: true,
   355  			})
   356  
   357  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   358  
   359  			// We should time out immediately.
   360  			_, err := istioctlSafe(t, istioCtl, ns.Name(), true, "--timeout=0s")
   361  			g.Expect(err.Error()).To(ContainSubstring("timed out"))
   362  		})
   363  }
   364  
   365  // Verify the error line number in the message is correct
   366  func TestErrorLine(t *testing.T) {
   367  	// nolint: staticcheck
   368  	framework.
   369  		NewTest(t).
   370  		RequiresSingleCluster().
   371  		Run(func(t framework.TestContext) {
   372  			g := NewWithT(t)
   373  
   374  			ns := namespace.NewOrFail(t, t, namespace.Config{
   375  				Prefix: "istioctl-analyze",
   376  				Inject: true,
   377  			})
   378  
   379  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
   380  
   381  			// Validation error if we have a gateway with invalid selector.
   382  			output, err := istioctlSafe(t, istioCtl, ns.Name(), true, gatewayFile, virtualServiceFile)
   383  
   384  			g.Expect(strings.Join(output, "\n")).To(ContainSubstring("testdata/gateway.yaml:9"))
   385  			g.Expect(strings.Join(output, "\n")).To(ContainSubstring("testdata/virtualservice.yaml:11"))
   386  			g.Expect(err).To(BeIdenticalTo(analyzerFoundIssuesError))
   387  		})
   388  }
   389  
   390  // Verify the output contains messages of the expected type, in order, followed by boilerplate lines
   391  func expectMessages(t test.Failer, g *GomegaWithT, outputLines []string, expected ...*diag.MessageType) {
   392  	t.Helper()
   393  
   394  	// The boilerplate lines that appear if any issues are found
   395  	boilerplateLines := strings.Split(analyzerFoundIssuesError.Error(), "\n")
   396  
   397  	g.Expect(outputLines).To(HaveLen(len(expected) + len(boilerplateLines)))
   398  
   399  	for i, line := range outputLines {
   400  		if i < len(expected) {
   401  			g.Expect(line).To(ContainSubstring(expected[i].Code()))
   402  		} else {
   403  			g.Expect(line).To(ContainSubstring(boilerplateLines[i-len(expected)]))
   404  		}
   405  	}
   406  }
   407  
   408  func expectNoMessages(t test.Failer, g *GomegaWithT, output []string) {
   409  	t.Helper()
   410  	g.Expect(output).To(HaveLen(1))
   411  	g.Expect(output[0]).To(ContainSubstring("No validation issues found when analyzing"))
   412  }
   413  
   414  func expectJSONMessages(t test.Failer, g *GomegaWithT, output string, expected ...*diag.MessageType) {
   415  	t.Helper()
   416  
   417  	var j []map[string]any
   418  	if err := json.Unmarshal([]byte(output), &j); err != nil {
   419  		t.Fatal(err, output)
   420  	}
   421  
   422  	g.Expect(j).To(HaveLen(len(expected)))
   423  
   424  	for i, m := range j {
   425  		g.Expect(m["level"]).To(Equal(expected[i].Level().String()))
   426  		g.Expect(m["code"]).To(Equal(expected[i].Code()))
   427  	}
   428  }
   429  
   430  // istioctlSafe calls istioctl analyze with certain flags set. Stdout and Stderr are merged
   431  func istioctlSafe(t test.Failer, i istioctl.Instance, ns string, useKube bool, extraArgs ...string) ([]string, error) {
   432  	output, stderr, err := istioctlWithStderr(t, i, ns, useKube, extraArgs...)
   433  	return strings.Split(strings.TrimSpace(output+stderr), "\n"), err
   434  }
   435  
   436  func istioctlWithStderr(t test.Failer, i istioctl.Instance, ns string, useKube bool, extraArgs ...string) (string, string, error) {
   437  	t.Helper()
   438  
   439  	args := []string{"analyze"}
   440  	if ns != "" {
   441  		args = append(args, "--namespace", ns)
   442  	}
   443  	// Suppress some cluster-wide checks. This ensures we do not fail tests when running on clusters that trigger
   444  	// analyzers we didn't intended to test.
   445  	args = append(args, fmt.Sprintf("--use-kube=%t", useKube), "--suppress=IST0139=*", "--suppress=IST0002=CustomResourceDefinition *")
   446  	args = append(args, extraArgs...)
   447  
   448  	return i.Invoke(args)
   449  }
   450  
   451  // applyFileOrFail applys the given yaml file and deletes it during context cleanup
   452  func applyFileOrFail(t framework.TestContext, ns, filename string) {
   453  	t.Helper()
   454  	if err := t.Clusters().Default().ApplyYAMLFiles(ns, filename); err != nil {
   455  		t.Fatal(err)
   456  	}
   457  	t.Cleanup(func() {
   458  		_ = t.Clusters().Default().DeleteYAMLFiles(ns, filename)
   459  	})
   460  }
   461  
   462  func TestMultiCluster(t *testing.T) {
   463  	// nolint: staticcheck
   464  	framework.
   465  		NewTest(t).
   466  		Run(func(t framework.TestContext) {
   467  			if len(t.Environment().Clusters()) < 2 {
   468  				t.Skip("skipping test, need at least 2 clusters")
   469  			}
   470  
   471  			g := NewWithT(t)
   472  
   473  			ns := namespace.NewOrFail(t, t, namespace.Config{
   474  				Prefix: "istioctl-analyze",
   475  				Inject: true,
   476  			})
   477  
   478  			// create remote secrets for analysis
   479  			secrets := map[string]string{}
   480  			for _, c := range t.Environment().Clusters() {
   481  				istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{
   482  					Cluster: c,
   483  				})
   484  				secret, _, err := createRemoteSecret(t, istioCtl, c.Name())
   485  				g.Expect(err).To(BeNil())
   486  				secrets[c.Name()] = secret
   487  			}
   488  			for ind, c := range t.Environment().Clusters() {
   489  				// apply remote secret to be used for analysis
   490  				for sc, secret := range secrets {
   491  					if c.Name() == sc {
   492  						continue
   493  					}
   494  					err := c.ApplyYAMLFiles(helm.IstioNamespace, secret)
   495  					g.Expect(err).To(BeNil())
   496  				}
   497  
   498  				svc := fmt.Sprintf(`
   499  apiVersion: v1
   500  kind: Service
   501  metadata:
   502    name: reviews
   503  spec:
   504    selector:
   505      app: reviews
   506    type: ClusterIP
   507    ports:
   508    - name: http-%d
   509      port: 8080
   510      protocol: TCP
   511      targetPort: 8080
   512  `, ind)
   513  				// apply inconsistent services
   514  				err := c.ApplyYAMLFiles(ns.Name(), svc)
   515  				g.Expect(err).To(BeNil())
   516  			}
   517  
   518  			istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{Cluster: t.Clusters().Configs().Default()})
   519  			output, _ := istioctlSafe(t, istioCtl, "", true, "--all-namespaces")
   520  			g.Expect(strings.Join(output, "\n")).To(ContainSubstring("is inconsistent across clusters"))
   521  		})
   522  }
   523  
   524  func createRemoteSecret(t test.Failer, i istioctl.Instance, cluster string) (string, string, error) {
   525  	t.Helper()
   526  
   527  	args := []string{"create-remote-secret"}
   528  	args = append(args, "--name", cluster)
   529  
   530  	return i.Invoke(args)
   531  }