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 }