istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/workload/workload_test.go (about) 1 // Copyright 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 15 package workload 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "os" 22 "path" 23 "reflect" 24 "strings" 25 "testing" 26 27 "github.com/spf13/cobra" 28 v1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 31 "istio.io/istio/istioctl/pkg/cli" 32 "istio.io/istio/pilot/test/util" 33 "istio.io/istio/pkg/config/constants" 34 "istio.io/istio/pkg/kube" 35 "istio.io/istio/pkg/test/util/assert" 36 ) 37 38 var fakeCACert = []byte("fake-CA-cert") 39 40 var ( 41 defaultYAML = `apiVersion: networking.istio.io/v1alpha3 42 kind: WorkloadGroup 43 metadata: 44 name: foo 45 namespace: bar 46 spec: 47 metadata: {} 48 template: 49 serviceAccount: default 50 ` 51 52 customYAML = `apiVersion: networking.istio.io/v1alpha3 53 kind: WorkloadGroup 54 metadata: 55 name: foo 56 namespace: bar 57 spec: 58 metadata: 59 annotations: 60 annotation: foobar 61 labels: 62 app: foo 63 bar: baz 64 template: 65 ports: 66 grpc: 3550 67 http: 8080 68 serviceAccount: test 69 ` 70 ) 71 72 type testcase struct { 73 description string 74 expectedException bool 75 args []string 76 expectedOutput string 77 namespace string 78 } 79 80 func TestWorkloadGroupCreate(t *testing.T) { 81 cases := []testcase{ 82 { 83 description: "Invalid command args - missing service name and namespace", 84 args: strings.Split("group create", " "), 85 expectedException: true, 86 expectedOutput: "Error: expecting a workload name\n", 87 }, 88 { 89 description: "Invalid command args - missing service name", 90 args: strings.Split("group create -n bar", " "), 91 expectedException: true, 92 expectedOutput: "Error: expecting a workload name\n", 93 }, 94 { 95 description: "Invalid command args - missing service namespace", 96 args: strings.Split("group create --name foo", " "), 97 expectedException: true, 98 expectedOutput: "Error: expecting a workload namespace\n", 99 }, 100 { 101 description: "valid case - minimal flags, infer defaults", 102 args: strings.Split("group create --name foo --namespace bar", " "), 103 expectedException: false, 104 expectedOutput: defaultYAML, 105 }, 106 { 107 description: "valid case - create full workload group", 108 args: strings.Split("group create --name foo --namespace bar --labels app=foo,bar=baz "+ 109 " --annotations annotation=foobar --ports grpc=3550,http=8080 --serviceAccount test", " "), 110 expectedException: false, 111 expectedOutput: customYAML, 112 }, 113 { 114 description: "valid case - create full workload group with shortnames", 115 args: strings.Split("group create --name foo -n bar -l app=foo,bar=baz -p grpc=3550,http=8080"+ 116 " -a annotation=foobar --serviceAccount test", " "), 117 expectedException: false, 118 expectedOutput: customYAML, 119 }, 120 } 121 122 for i, c := range cases { 123 t.Run(fmt.Sprintf("case %d %s", i, c.description), func(t *testing.T) { 124 verifyTestcaseOutput(t, Cmd(cli.NewFakeContext(nil)), c) 125 }) 126 } 127 } 128 129 func verifyTestcaseOutput(t *testing.T, cmd *cobra.Command, c testcase) { 130 t.Helper() 131 132 var out bytes.Buffer 133 cmd.SetArgs(c.args) 134 cmd.SetOut(&out) 135 cmd.SetErr(&out) 136 cmd.SilenceUsage = true 137 if c.namespace != "" { 138 namespace = c.namespace 139 } 140 141 fErr := cmd.Execute() 142 output := out.String() 143 144 if c.expectedException { 145 if fErr == nil { 146 t.Fatalf("Wanted an exception, "+ 147 "didn't get one, output was %q", output) 148 } 149 } else { 150 if fErr != nil { 151 t.Fatalf("Unwanted exception: %v", fErr) 152 } 153 } 154 155 if c.expectedOutput != "" && c.expectedOutput != output { 156 assert.Equal(t, c.expectedOutput, output) 157 t.Fatalf("Unexpected output for 'istioctl %s'\n got: %q\nwant: %q", strings.Join(c.args, " "), output, c.expectedOutput) 158 } 159 } 160 161 func TestWorkloadEntryConfigureInvalidArgs(t *testing.T) { 162 cases := []testcase{ 163 { 164 description: "Invalid command args - missing valid input spec", 165 args: strings.Split("entry configure --name foo -o temp --clusterID cid", " "), 166 expectedException: true, 167 expectedOutput: "Error: expecting a WorkloadGroup artifact file or the name and namespace of an existing WorkloadGroup\n", 168 }, 169 { 170 description: "Invalid command args - missing valid input spec", 171 args: strings.Split("entry configure -n bar -o temp --clusterID cid", " "), 172 expectedException: true, 173 expectedOutput: "Error: expecting a WorkloadGroup artifact file or the name and namespace of an existing WorkloadGroup\n", 174 }, 175 { 176 description: "Invalid command args - valid filename input but missing output filename", 177 args: strings.Split("entry configure -f file --clusterID cid", " "), 178 expectedException: true, 179 expectedOutput: "Error: expecting an output directory\n", 180 }, 181 { 182 description: "Invalid command args - valid kubectl input but missing output filename", 183 args: strings.Split("entry configure --name foo -n bar --clusterID cid", " "), 184 expectedException: true, 185 expectedOutput: "Error: expecting an output directory\n", 186 }, 187 } 188 189 for i, c := range cases { 190 t.Run(fmt.Sprintf("case %d %s", i, c.description), func(t *testing.T) { 191 verifyTestcaseOutput(t, Cmd(cli.NewFakeContext(nil)), c) 192 }) 193 } 194 } 195 196 var generated = map[string]bool{ 197 "hosts": true, 198 "istio-token": true, 199 "mesh.yaml": true, 200 "root-cert.pem": true, 201 "cluster.env": true, 202 } 203 204 const goldenSuffix = ".golden" 205 206 // TestWorkloadEntryConfigure enumerates test cases based on subdirectories of testdata/vmconfig. 207 // Each subdirectory contains two input files: workloadgroup.yaml and meshconfig.yaml that are used 208 // to generate golden outputs from the VM command. 209 func TestWorkloadEntryConfigure(t *testing.T) { 210 noClusterID := "failed to automatically determine the --clusterID" 211 files, err := os.ReadDir("testdata/vmconfig") 212 if err != nil { 213 t.Fatal(err) 214 } 215 testCases := map[string]map[string]string{ 216 "ipv4": { 217 "internalIP": "10.10.10.10", 218 "ingressIP": "10.10.10.11", 219 }, 220 "ipv6": { 221 "internalIP": "fd00:10:96::1", 222 "ingressIP": "fd00:10:96::2", 223 }, 224 } 225 for _, dir := range files { 226 if !dir.IsDir() { 227 continue 228 } 229 testdir := path.Join("testdata/vmconfig", dir.Name()) 230 t.Cleanup(func() { 231 for k := range generated { 232 os.Remove(path.Join(testdir, k)) 233 } 234 }) 235 t.Run(dir.Name(), func(t *testing.T) { 236 createClientFunc := func(client kube.CLIClient) { 237 client.Kube().CoreV1().ServiceAccounts("bar").Create(context.Background(), &v1.ServiceAccount{ 238 ObjectMeta: metav1.ObjectMeta{Namespace: "bar", Name: "vm-serviceaccount"}, 239 Secrets: []v1.ObjectReference{{Name: "test"}}, 240 }, metav1.CreateOptions{}) 241 client.Kube().CoreV1().ConfigMaps("bar").Create(context.Background(), &v1.ConfigMap{ 242 ObjectMeta: metav1.ObjectMeta{Namespace: "bar", Name: "istio-ca-root-cert"}, 243 Data: map[string]string{"root-cert.pem": string(fakeCACert)}, 244 }, metav1.CreateOptions{}) 245 client.Kube().CoreV1().ConfigMaps("istio-system").Create(context.Background(), &v1.ConfigMap{ 246 ObjectMeta: metav1.ObjectMeta{Namespace: "istio-system", Name: "istio-rev-1"}, 247 Data: map[string]string{ 248 "mesh": string(util.ReadFile(t, path.Join(testdir, "meshconfig.yaml"))), 249 }, 250 }, metav1.CreateOptions{}) 251 client.Kube().CoreV1().Secrets("bar").Create(context.Background(), &v1.Secret{ 252 ObjectMeta: metav1.ObjectMeta{Namespace: "bar", Name: "test"}, 253 Data: map[string][]byte{ 254 "token": {}, 255 }, 256 }, metav1.CreateOptions{}) 257 } 258 259 cmdWithClusterID := []string{ 260 "entry", "configure", 261 "-f", path.Join("testdata/vmconfig", dir.Name(), "workloadgroup.yaml"), 262 "--internalIP", testCases[dir.Name()]["internalIP"], 263 "--ingressIP", testCases[dir.Name()]["ingressIP"], 264 "--clusterID", constants.DefaultClusterName, 265 "--revision", "rev-1", 266 "-o", testdir, 267 } 268 if _, err := runTestCmd(t, createClientFunc, "rev-1", cmdWithClusterID); err != nil { 269 t.Fatal(err) 270 } 271 272 cmdNoClusterID := []string{ 273 "entry", "configure", 274 "-f", path.Join("testdata/vmconfig", dir.Name(), "workloadgroup.yaml"), 275 "--internalIP", testCases[dir.Name()]["internalIP"], 276 "--revision", "rev-1", 277 "-o", testdir, 278 } 279 if output, err := runTestCmd(t, createClientFunc, "rev-1", cmdNoClusterID); err != nil { 280 if !strings.Contains(output, noClusterID) { 281 t.Fatal(err) 282 } 283 } 284 285 checkFiles := map[string]bool{ 286 // inputs that we allow to exist, if other files seep in unexpectedly we fail the test 287 ".gitignore": false, "meshconfig.yaml": false, "workloadgroup.yaml": false, 288 } 289 for k, v := range generated { 290 checkFiles[k] = v 291 } 292 293 checkOutputFiles(t, testdir, checkFiles) 294 }) 295 } 296 } 297 298 func TestWorkloadEntryToPodPortsMeta(t *testing.T) { 299 cases := []struct { 300 description string 301 ports map[string]uint32 302 want string 303 }{ 304 { 305 description: "test json marshal", 306 ports: map[string]uint32{ 307 "HTTP": 80, 308 "HTTPS": 443, 309 }, 310 want: `[{"name":"HTTP","containerPort":80,"protocol":""},{"name":"HTTPS","containerPort":443,"protocol":""}]`, 311 }, 312 } 313 for i, c := range cases { 314 t.Run(fmt.Sprintf("case %d %s", i, c.description), func(t *testing.T) { 315 str := marshalWorkloadEntryPodPorts(c.ports) 316 if c.want != str { 317 t.Errorf("want %s, got %s", c.want, str) 318 } 319 }) 320 } 321 } 322 323 // TestWorkloadEntryConfigureNilProxyMetadata tests a particular use case when the 324 // proxyMetadata is nil, no metadata would be generated at all. 325 func TestWorkloadEntryConfigureNilProxyMetadata(t *testing.T) { 326 testdir := "testdata/vmconfig-nil-proxy-metadata" 327 noClusterID := "failed to automatically determine the --clusterID" 328 329 t.Cleanup(func() { 330 for k := range generated { 331 os.Remove(path.Join(testdir, k)) 332 } 333 }) 334 335 createClientFunc := func(client kube.CLIClient) { 336 client.Kube().CoreV1().ServiceAccounts("bar").Create(context.Background(), &v1.ServiceAccount{ 337 ObjectMeta: metav1.ObjectMeta{Namespace: "bar", Name: "vm-serviceaccount"}, 338 Secrets: []v1.ObjectReference{{Name: "test"}}, 339 }, metav1.CreateOptions{}) 340 client.Kube().CoreV1().ConfigMaps("bar").Create(context.Background(), &v1.ConfigMap{ 341 ObjectMeta: metav1.ObjectMeta{Namespace: "bar", Name: "istio-ca-root-cert"}, 342 Data: map[string]string{"root-cert.pem": string(fakeCACert)}, 343 }, metav1.CreateOptions{}) 344 client.Kube().CoreV1().ConfigMaps("istio-system").Create(context.Background(), &v1.ConfigMap{ 345 ObjectMeta: metav1.ObjectMeta{Namespace: "istio-system", Name: "istio"}, 346 Data: map[string]string{ 347 "mesh": "defaultConfig: {}", 348 }, 349 }, metav1.CreateOptions{}) 350 client.Kube().CoreV1().Secrets("bar").Create(context.Background(), &v1.Secret{ 351 ObjectMeta: metav1.ObjectMeta{Namespace: "bar", Name: "test"}, 352 Data: map[string][]byte{ 353 "token": {}, 354 }, 355 }, metav1.CreateOptions{}) 356 } 357 358 cmdWithClusterID := []string{ 359 "entry", "configure", 360 "-f", path.Join(testdir, "workloadgroup.yaml"), 361 "--internalIP", "10.10.10.10", 362 "--clusterID", constants.DefaultClusterName, 363 "-o", testdir, 364 } 365 if output, err := runTestCmd(t, createClientFunc, "", cmdWithClusterID); err != nil { 366 t.Logf("output: %v", output) 367 t.Fatal(err) 368 } 369 370 cmdNoClusterID := []string{ 371 "entry", "configure", 372 "-f", path.Join(testdir, "workloadgroup.yaml"), 373 "--internalIP", "10.10.10.10", 374 "-o", testdir, 375 } 376 if output, err := runTestCmd(t, createClientFunc, "", cmdNoClusterID); err != nil { 377 if !strings.Contains(output, noClusterID) { 378 t.Fatal(err) 379 } 380 } 381 382 checkFiles := map[string]bool{ 383 // inputs that we allow to exist, if other files seep in unexpectedly we fail the test 384 ".gitignore": false, "workloadgroup.yaml": false, 385 } 386 for k, v := range generated { 387 checkFiles[k] = v 388 } 389 390 checkOutputFiles(t, testdir, checkFiles) 391 } 392 393 func runTestCmd(t *testing.T, createResourceFunc func(client kube.CLIClient), rev string, args []string) (string, error) { 394 t.Helper() 395 // TODO there is already probably something else that does this 396 var out bytes.Buffer 397 ctx := cli.NewFakeContext(&cli.NewFakeContextOption{ 398 IstioNamespace: "istio-system", 399 }) 400 rootCmd := Cmd(ctx) 401 rootCmd.SetArgs(args) 402 client, err := ctx.CLIClientWithRevision(rev) 403 if err != nil { 404 return "", err 405 } 406 createResourceFunc(client) 407 408 rootCmd.SetOut(&out) 409 rootCmd.SetErr(&out) 410 err = rootCmd.Execute() 411 output := out.String() 412 return output, err 413 } 414 415 func checkOutputFiles(t *testing.T, testdir string, checkFiles map[string]bool) { 416 t.Helper() 417 418 outputFiles, err := os.ReadDir(testdir) 419 if err != nil { 420 t.Fatal(err) 421 } 422 423 for _, f := range outputFiles { 424 checkGolden, ok := checkFiles[f.Name()] 425 if !ok { 426 if checkGolden, ok := checkFiles[f.Name()[:len(f.Name())-len(goldenSuffix)]]; !(checkGolden && ok) { 427 t.Errorf("unexpected file in output dir: %s", f.Name()) 428 } 429 continue 430 } 431 if checkGolden { 432 t.Run(f.Name(), func(t *testing.T) { 433 contents := util.ReadFile(t, path.Join(testdir, f.Name())) 434 goldenFile := path.Join(testdir, f.Name()+goldenSuffix) 435 util.RefreshGoldenFile(t, contents, goldenFile) 436 util.CompareContent(t, contents, goldenFile) 437 }) 438 } 439 } 440 } 441 442 func TestConvertToMap(t *testing.T) { 443 tests := []struct { 444 name string 445 arg []string 446 want map[string]string 447 }{ 448 {name: "empty", arg: []string{""}, want: map[string]string{"": ""}}, 449 {name: "one-valid", arg: []string{"key=value"}, want: map[string]string{"key": "value"}}, 450 {name: "one-valid-double-equals", arg: []string{"key==value"}, want: map[string]string{"key": "=value"}}, 451 {name: "one-key-only", arg: []string{"key"}, want: map[string]string{"key": ""}}, 452 } 453 for _, tt := range tests { 454 t.Run(tt.name, func(t *testing.T) { 455 if got := convertToStringMap(tt.arg); !reflect.DeepEqual(got, tt.want) { 456 t.Errorf("convertToStringMap() = %v, want %v", got, tt.want) 457 } 458 }) 459 } 460 } 461 462 func TestSplitEqual(t *testing.T) { 463 tests := []struct { 464 arg string 465 wantKey string 466 wantValue string 467 }{ 468 {arg: "key=value", wantKey: "key", wantValue: "value"}, 469 {arg: "key==value", wantKey: "key", wantValue: "=value"}, 470 {arg: "key=", wantKey: "key", wantValue: ""}, 471 {arg: "key", wantKey: "key", wantValue: ""}, 472 {arg: "", wantKey: "", wantValue: ""}, 473 } 474 for _, tt := range tests { 475 t.Run(tt.arg, func(t *testing.T) { 476 gotKey, gotValue := splitEqual(tt.arg) 477 if gotKey != tt.wantKey { 478 t.Errorf("splitEqual(%v) got = %v, want %v", tt.arg, gotKey, tt.wantKey) 479 } 480 if gotValue != tt.wantValue { 481 t.Errorf("splitEqual(%v) got1 = %v, want %v", tt.arg, gotValue, tt.wantValue) 482 } 483 }) 484 } 485 }