istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/pkg/install/cniconfig_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 install 16 17 import ( 18 "context" 19 "os" 20 "path/filepath" 21 "testing" 22 "time" 23 24 "istio.io/istio/cni/pkg/config" 25 testutils "istio.io/istio/pilot/test/util" 26 "istio.io/istio/pkg/file" 27 "istio.io/istio/pkg/test/util/assert" 28 ) 29 30 func TestGetDefaultCNINetwork(t *testing.T) { 31 tempDir := t.TempDir() 32 33 cases := []struct { 34 name string 35 dir string 36 inFilename string 37 outFilename string 38 fileContents string 39 expectedFailure bool 40 }{ 41 { 42 name: "inexistent directory", 43 dir: "/inexistent/directory", 44 expectedFailure: true, 45 }, 46 { 47 name: "empty directory", 48 dir: tempDir, 49 expectedFailure: true, 50 }, 51 { 52 // Only .conf and .conflist files are detectable 53 name: "undetectable file", 54 dir: tempDir, 55 expectedFailure: true, 56 inFilename: "undetectable.file", 57 fileContents: ` 58 { 59 "cniVersion": "0.3.1", 60 "name": "istio-cni", 61 "type": "istio-cni" 62 }`, 63 }, 64 { 65 name: "empty file", 66 dir: tempDir, 67 expectedFailure: true, 68 inFilename: "empty.conf", 69 }, 70 { 71 name: "regular file", 72 dir: tempDir, 73 expectedFailure: false, 74 inFilename: "regular.conf", 75 outFilename: "regular.conf", 76 fileContents: ` 77 { 78 "cniVersion": "0.3.1", 79 "name": "istio-cni", 80 "type": "istio-cni" 81 }`, 82 }, 83 { 84 name: "another regular file", 85 dir: tempDir, 86 expectedFailure: false, 87 inFilename: "regular2.conf", 88 outFilename: "regular.conf", 89 fileContents: ` 90 { 91 "cniVersion": "0.3.1", 92 "name": "istio-cni", 93 "type": "istio-cni" 94 }`, 95 }, 96 } 97 98 for _, c := range cases { 99 t.Run(c.name, func(t *testing.T) { 100 if c.fileContents != "" { 101 err := os.WriteFile(filepath.Join(c.dir, c.inFilename), []byte(c.fileContents), 0o644) 102 if err != nil { 103 t.Fatal(err) 104 } 105 } 106 107 result, err := getDefaultCNINetwork(c.dir) 108 if (c.expectedFailure && err == nil) || (!c.expectedFailure && err != nil) { 109 t.Fatalf("expected failure: %t, got %v", c.expectedFailure, err) 110 } 111 112 if c.fileContents != "" { 113 if c.outFilename != result { 114 t.Fatalf("expected %s, got %s", c.outFilename, result) 115 } 116 } 117 }) 118 } 119 } 120 121 func TestGetCNIConfigFilepath(t *testing.T) { 122 cases := []struct { 123 name string 124 chainedCNIPlugin bool 125 specifiedConfName string 126 delayedConfName string 127 expectedConfName string 128 existingConfFiles []string 129 }{ 130 { 131 name: "unspecified existing CNI config file", 132 chainedCNIPlugin: true, 133 expectedConfName: "bridge.conf", 134 existingConfFiles: []string{"bridge.conf", "list.conflist"}, 135 }, 136 { 137 name: "unspecified delayed CNI config file", 138 chainedCNIPlugin: true, 139 delayedConfName: "bridge.conf", 140 expectedConfName: "bridge.conf", 141 }, 142 { 143 name: "unspecified CNI config file never created", 144 chainedCNIPlugin: true, 145 }, 146 { 147 name: "specified existing CNI config file", 148 chainedCNIPlugin: true, 149 specifiedConfName: "list.conflist", 150 expectedConfName: "list.conflist", 151 existingConfFiles: []string{"bridge.conf", "list.conflist"}, 152 }, 153 { 154 name: "specified existing CNI config file (.conf to .conflist)", 155 chainedCNIPlugin: true, 156 specifiedConfName: "list.conf", 157 expectedConfName: "list.conflist", 158 existingConfFiles: []string{"bridge.conf", "list.conflist"}, 159 }, 160 { 161 name: "specified existing CNI config file (.conflist to .conf)", 162 chainedCNIPlugin: true, 163 specifiedConfName: "bridge.conflist", 164 expectedConfName: "bridge.conf", 165 existingConfFiles: []string{"bridge.conf", "list.conflist"}, 166 }, 167 { 168 name: "specified delayed CNI config file", 169 chainedCNIPlugin: true, 170 specifiedConfName: "bridge.conf", 171 delayedConfName: "bridge.conf", 172 expectedConfName: "bridge.conf", 173 }, 174 { 175 name: "specified CNI config file never created", 176 chainedCNIPlugin: true, 177 specifiedConfName: "never-created.conf", 178 existingConfFiles: []string{"bridge.conf", "list.conflist"}, 179 }, 180 { 181 name: "standalone CNI plugin unspecified CNI config file", 182 expectedConfName: "YYY-istio-cni.conf", 183 }, 184 { 185 name: "standalone CNI plugin specified CNI config file", 186 specifiedConfName: "specific-name.conf", 187 expectedConfName: "specific-name.conf", 188 }, 189 } 190 191 for _, c := range cases { 192 t.Run(c.name, func(t *testing.T) { 193 // Create temp directory for files 194 tempDir := t.TempDir() 195 196 // Create existing config files if specified in test case 197 for _, filename := range c.existingConfFiles { 198 if err := file.AtomicCopy(filepath.Join("testdata", filepath.Base(filename)), tempDir, filepath.Base(filename)); err != nil { 199 t.Fatal(err) 200 } 201 } 202 203 var expectedFilepath string 204 if len(c.expectedConfName) > 0 { 205 expectedFilepath = filepath.Join(tempDir, c.expectedConfName) 206 } 207 208 if !c.chainedCNIPlugin { 209 // Standalone CNI plugin 210 parent := context.Background() 211 ctx1, cancel := context.WithTimeout(parent, 100*time.Millisecond) 212 defer cancel() 213 result, err := getCNIConfigFilepath(ctx1, c.specifiedConfName, tempDir, c.chainedCNIPlugin) 214 if err != nil { 215 assert.Equal(t, result, "") 216 if err == context.DeadlineExceeded { 217 t.Fatalf("timed out waiting for expected %s", expectedFilepath) 218 } 219 t.Fatal(err) 220 } 221 if result != expectedFilepath { 222 t.Fatalf("expected %s, got %s", expectedFilepath, result) 223 } 224 // Successful test case 225 return 226 } 227 228 // Handle chained CNI plugin cases 229 // Call with goroutine to test fsnotify watcher 230 parent, cancel := context.WithCancel(context.Background()) 231 defer cancel() 232 resultChan, errChan := make(chan string, 1), make(chan error, 1) 233 go func(resultChan chan string, errChan chan error, ctx context.Context, cniConfName, mountedCNINetDir string, chained bool) { 234 result, err := getCNIConfigFilepath(ctx, cniConfName, mountedCNINetDir, chained) 235 if err != nil { 236 errChan <- err 237 return 238 } 239 resultChan <- result 240 }(resultChan, errChan, parent, c.specifiedConfName, tempDir, c.chainedCNIPlugin) 241 242 select { 243 case result := <-resultChan: 244 if len(c.delayedConfName) > 0 { 245 // Delayed case 246 t.Fatalf("did not expect to retrieve a CNI config file %s", result) 247 } else if result != expectedFilepath { 248 if len(expectedFilepath) > 0 { 249 t.Fatalf("expected %s, got %s", expectedFilepath, result) 250 } 251 t.Fatalf("did not expect to retrieve a CNI config file %s", result) 252 } 253 // Successful test for non-delayed cases 254 return 255 case err := <-errChan: 256 t.Fatal(err) 257 case <-time.After(250 * time.Millisecond): 258 if len(c.delayedConfName) > 0 { 259 // Delayed case 260 // Write delayed CNI config file 261 data, err := os.ReadFile(filepath.Join("testdata", c.delayedConfName)) 262 if err != nil { 263 t.Fatal(err) 264 } 265 err = os.WriteFile(filepath.Join(tempDir, c.delayedConfName), data, 0o644) 266 if err != nil { 267 t.Fatal(err) 268 } 269 t.Logf("delayed write to %v", filepath.Join(tempDir, c.delayedConfName)) 270 } else if len(c.expectedConfName) > 0 { 271 t.Fatalf("timed out waiting for expected %s", expectedFilepath) 272 } else { 273 // Successful test for test cases where CNI config file is never created 274 return 275 } 276 } 277 278 // Only for delayed cases 279 select { 280 case result := <-resultChan: 281 if result != expectedFilepath { 282 if len(expectedFilepath) > 0 { 283 t.Fatalf("expected %s, got %s", expectedFilepath, result) 284 } 285 t.Fatalf("did not expect to retrieve a CNI config file %s", result) 286 } 287 case err := <-errChan: 288 t.Fatal(err) 289 case <-time.After(250 * time.Millisecond): 290 t.Fatalf("timed out waiting for expected %s", expectedFilepath) 291 } 292 }) 293 } 294 } 295 296 func TestInsertCNIConfig(t *testing.T) { 297 cases := []struct { 298 name string 299 expectedFailure bool 300 existingConfFilename string 301 newConfFilename string 302 }{ 303 { 304 name: "invalid existing config format (map)", 305 expectedFailure: true, 306 existingConfFilename: "invalid-map.conflist", 307 newConfFilename: "istio-cni.conf", 308 }, 309 { 310 name: "invalid new config format (arr)", 311 expectedFailure: true, 312 existingConfFilename: "list.conflist", 313 newConfFilename: "invalid-arr.conflist", 314 }, 315 { 316 name: "invalid existing config format (arr)", 317 expectedFailure: true, 318 existingConfFilename: "invalid-arr.conflist", 319 newConfFilename: "istio-cni.conf", 320 }, 321 { 322 name: "regular network file", 323 existingConfFilename: "bridge.conf", 324 newConfFilename: "istio-cni.conf", 325 }, 326 { 327 name: "list network file", 328 existingConfFilename: "list.conflist", 329 newConfFilename: "istio-cni.conf", 330 }, 331 { 332 name: "list network file with existing istio", 333 existingConfFilename: "list-with-istio.conflist", 334 newConfFilename: "istio-cni.conf", 335 }, 336 } 337 338 for _, c := range cases { 339 t.Run(c.name, func(t *testing.T) { 340 istioConf := testutils.ReadFile(t, filepath.Join("testdata", c.newConfFilename)) 341 existingConfFilepath := filepath.Join("testdata", c.existingConfFilename) 342 existingConf := testutils.ReadFile(t, existingConfFilepath) 343 344 output, err := insertCNIConfig(istioConf, existingConf) 345 if err != nil { 346 if !c.expectedFailure { 347 t.Fatal(err) 348 } 349 return 350 } 351 352 goldenFilepath := existingConfFilepath + ".golden" 353 goldenConfig := testutils.ReadFile(t, goldenFilepath) 354 testutils.CompareBytes(t, output, goldenConfig, goldenFilepath) 355 }) 356 } 357 } 358 359 const ( 360 // For testing purposes, set kubeconfigFilename equivalent to the path in the test files and use __KUBECONFIG_FILENAME__ 361 // CreateCNIConfigFile joins the MountedCNINetDir and KubeconfigFilename if __KUBECONFIG_FILEPATH__ was used 362 kubeconfigFilename = "/path/to/kubeconfig" 363 cniNetworkConfigFile = "testdata/istio-cni.conf.template" 364 cniNetworkConfig = `{ 365 "cniVersion": "0.3.1", 366 "name": "istio-cni", 367 "type": "istio-cni", 368 "log_level": "__LOG_LEVEL__", 369 "kubernetes": { 370 "kubeconfig": "__KUBECONFIG_FILENAME__", 371 "cni_bin_dir": "/path/cni/bin" 372 } 373 } 374 ` 375 ) 376 377 func TestCreateCNIConfigFile(t *testing.T) { 378 cases := []struct { 379 name string 380 chainedCNIPlugin bool 381 specifiedConfName string 382 expectedConfName string 383 goldenConfName string 384 existingConfFiles map[string]string // {srcFilename: targetFilename, ...} 385 }{ 386 { 387 name: "unspecified existing CNI config file (existing .conf to conflist)", 388 chainedCNIPlugin: true, 389 expectedConfName: "bridge.conflist", 390 goldenConfName: "bridge.conf.golden", 391 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "list.conflist": "list.conflist"}, 392 }, 393 { 394 name: "unspecified CNI config file never created", 395 chainedCNIPlugin: true, 396 }, 397 { 398 name: "specified existing CNI config file", 399 chainedCNIPlugin: true, 400 specifiedConfName: "list.conflist", 401 expectedConfName: "list.conflist", 402 goldenConfName: "list.conflist.golden", 403 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "list.conflist": "list.conflist"}, 404 }, 405 { 406 name: "specified existing CNI config file (specified .conf to .conflist)", 407 chainedCNIPlugin: true, 408 specifiedConfName: "list.conf", 409 expectedConfName: "list.conflist", 410 goldenConfName: "list.conflist.golden", 411 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "list.conflist": "list.conflist"}, 412 }, 413 { 414 name: "specified existing CNI config file (existing .conf to .conflist)", 415 chainedCNIPlugin: true, 416 specifiedConfName: "bridge.conflist", 417 expectedConfName: "bridge.conflist", 418 goldenConfName: "bridge.conf.golden", 419 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "list.conflist": "list.conflist"}, 420 }, 421 { 422 name: "specified CNI config file never created", 423 chainedCNIPlugin: true, 424 specifiedConfName: "never-created.conf", 425 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "list.conflist": "list.conflist"}, 426 }, 427 { 428 name: "specified CNI config file undetectable", 429 chainedCNIPlugin: true, 430 specifiedConfName: "undetectable.file", 431 expectedConfName: "undetectable.file", 432 goldenConfName: "list.conflist.golden", 433 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "list.conflist": "undetectable.file"}, 434 }, 435 { 436 name: "standalone CNI plugin unspecified CNI config file", 437 expectedConfName: "YYY-istio-cni.conf", 438 goldenConfName: "istio-cni.conf", 439 }, 440 { 441 name: "standalone CNI plugin specified CNI config file", 442 specifiedConfName: "specific-name.conf", 443 expectedConfName: "specific-name.conf", 444 goldenConfName: "istio-cni.conf", 445 }, 446 } 447 448 for _, c := range cases { 449 cfgFile := config.InstallConfig{ 450 CNIConfName: c.specifiedConfName, 451 ChainedCNIPlugin: c.chainedCNIPlugin, 452 LogLevel: "debug", 453 KubeconfigFilename: kubeconfigFilename, 454 } 455 456 cfg := config.InstallConfig{ 457 CNIConfName: c.specifiedConfName, 458 ChainedCNIPlugin: c.chainedCNIPlugin, 459 LogLevel: "debug", 460 KubeconfigFilename: kubeconfigFilename, 461 } 462 test := func(cfg config.InstallConfig) func(t *testing.T) { 463 return func(t *testing.T) { 464 // Create temp directory for files 465 tempDir := t.TempDir() 466 467 // Create existing config files if specified in test case 468 for srcFilename, targetFilename := range c.existingConfFiles { 469 if err := file.AtomicCopy(filepath.Join("testdata", srcFilename), tempDir, targetFilename); err != nil { 470 t.Fatal(err) 471 } 472 } 473 474 cfg.MountedCNINetDir = tempDir 475 476 var expectedFilepath string 477 if len(c.expectedConfName) > 0 { 478 expectedFilepath = filepath.Join(tempDir, c.expectedConfName) 479 } 480 481 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 482 defer cancel() 483 resultFilepath, err := createCNIConfigFile(ctx, &cfg) 484 if err != nil { 485 assert.Equal(t, resultFilepath, "") 486 if err == context.DeadlineExceeded { 487 if len(c.expectedConfName) > 0 { 488 t.Fatalf("timed out waiting for expected %s", expectedFilepath) 489 } 490 // Successful test for never-created config file 491 return 492 } 493 t.Fatal(err) 494 } 495 496 if resultFilepath != expectedFilepath { 497 if len(expectedFilepath) > 0 { 498 t.Fatalf("expected %s, got %s", expectedFilepath, resultFilepath) 499 } 500 t.Fatalf("did not expect to retrieve a CNI config file %s", resultFilepath) 501 } 502 503 resultConfig := testutils.ReadFile(t, resultFilepath) 504 505 goldenFilepath := filepath.Join("testdata", c.goldenConfName) 506 goldenConfig := testutils.ReadFile(t, goldenFilepath) 507 testutils.CompareBytes(t, resultConfig, goldenConfig, goldenFilepath) 508 } 509 } 510 t.Run("network-config-file "+c.name, test(cfgFile)) 511 t.Run(c.name, test(cfg)) 512 } 513 }