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 }