istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/test/install_cni.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 "bytes" 19 "context" 20 "errors" 21 "fmt" 22 "os" 23 "path/filepath" 24 "strings" 25 "sync" 26 "sync/atomic" 27 "testing" 28 "time" 29 30 "github.com/google/go-cmp/cmp" 31 "github.com/spf13/viper" 32 33 "istio.io/istio/cni/pkg/config" 34 "istio.io/istio/cni/pkg/constants" 35 "istio.io/istio/cni/pkg/install" 36 "istio.io/istio/cni/pkg/util" 37 "istio.io/istio/pkg/file" 38 "istio.io/istio/pkg/slices" 39 "istio.io/istio/pkg/test/env" 40 "istio.io/istio/pkg/test/util/retry" 41 ) 42 43 const ( 44 cniConfSubDir = "/testdata/pre/" 45 k8sSvcAcctSubDir = "/testdata/k8s_svcacct/" 46 47 defaultFileMode = 0o644 48 ) 49 50 func getEnv(key, fallback string) string { 51 if value, ok := os.LookupEnv(key); ok { 52 return value 53 } 54 return fallback 55 } 56 57 func mktemp(dir, prefix string, t *testing.T) string { 58 t.Helper() 59 tempDir, err := os.MkdirTemp(dir, prefix) 60 if err != nil { 61 t.Fatalf("Couldn't get current working directory, err: %v", err) 62 } 63 t.Logf("Created temporary dir: %v", tempDir) 64 return tempDir 65 } 66 67 func ls(dir string, t *testing.T) []string { 68 files, err := os.ReadDir(dir) 69 t.Helper() 70 if err != nil { 71 t.Fatalf("Failed to list files, err: %v", err) 72 } 73 return slices.Map(files, func(e os.DirEntry) string { 74 return e.Name() 75 }) 76 } 77 78 func cp(src, dest string, t *testing.T) { 79 t.Helper() 80 data, err := os.ReadFile(src) 81 if err != nil { 82 t.Fatalf("Failed to read file %v, err: %v", src, err) 83 } 84 if err = os.WriteFile(dest, data, os.FileMode(defaultFileMode)); err != nil { 85 t.Fatalf("Failed to write file %v, err: %v", dest, err) 86 } 87 } 88 89 func rmDir(dir string, t *testing.T) { 90 t.Helper() 91 err := os.RemoveAll(dir) 92 if err != nil { 93 t.Fatalf("Failed to remove dir %v, err: %v", dir, err) 94 } 95 } 96 97 // Removes Istio CNI's config from the CNI config file 98 func rmCNIConfig(cniConfigFilepath string, t *testing.T) { 99 t.Helper() 100 101 // Read JSON from CNI config file 102 cniConfigMap, err := util.ReadCNIConfigMap(cniConfigFilepath) 103 if err != nil { 104 t.Fatal(err) 105 } 106 107 // Find Istio CNI and remove from plugin list 108 plugins, err := util.GetPlugins(cniConfigMap) 109 if err != nil { 110 t.Fatal(err) 111 } 112 for i, rawPlugin := range plugins { 113 plugin, err := util.GetPlugin(rawPlugin) 114 if err != nil { 115 t.Fatal(err) 116 } 117 if plugin["type"] == "istio-cni" { 118 cniConfigMap["plugins"] = append(plugins[:i], plugins[i+1:]...) 119 break 120 } 121 } 122 123 cniConfig, err := util.MarshalCNIConfig(cniConfigMap) 124 if err != nil { 125 t.Fatal(err) 126 } 127 128 if err = file.AtomicWrite(cniConfigFilepath, cniConfig, os.FileMode(0o644)); err != nil { 129 t.Fatal(err) 130 } 131 } 132 133 // populateTempDirs populates temporary test directories with golden files and 134 // other related configuration. 135 func populateTempDirs(wd string, cniDirOrderedFiles []string, tempCNIConfDir, tempK8sSvcAcctDir string, t *testing.T) { 136 t.Helper() 137 t.Logf("Pre-populating working dirs") 138 for i, f := range cniDirOrderedFiles { 139 destFilenm := fmt.Sprintf("0%d-%s", i, f) 140 t.Logf("Copying %v into temp config dir %v/%s", f, tempCNIConfDir, destFilenm) 141 cp(wd+cniConfSubDir+f, tempCNIConfDir+"/"+destFilenm, t) 142 } 143 for _, f := range ls(wd+k8sSvcAcctSubDir, t) { 144 t.Logf("Copying %v into temp k8s serviceaccount dir %v", f, tempK8sSvcAcctDir) 145 cp(wd+k8sSvcAcctSubDir+f, tempK8sSvcAcctDir+"/"+f, t) 146 } 147 t.Logf("Finished pre-populating working dirs") 148 } 149 150 // create an install server instance and run it, blocking until it gets terminated 151 // via context cancellation 152 func startInstallServer(ctx context.Context, serverConfig *config.Config, t *testing.T) { 153 readyFlag := &atomic.Value{} 154 installer := install.NewInstaller(&serverConfig.InstallConfig, readyFlag) 155 156 t.Logf("CNI installer created, watching...") 157 // installer.Run() will block indefinitely, and attempt to permanently "keep" 158 // the CNI binary installed. 159 if err := installer.Run(ctx); err != nil { 160 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 161 // Error was caused by interrupt/termination signal 162 t.Logf("installer complete: %v", err) 163 } else { 164 t.Errorf("installer failed: %v", err) 165 } 166 } 167 168 if cleanErr := installer.Cleanup(); cleanErr != nil { 169 t.Errorf("Error during test CNI installer cleanup, error was: %s", cleanErr) 170 } 171 } 172 173 // checkResult checks if resultFile is equal to expectedFile at each tick until timeout 174 func checkResult(result, expected string) error { 175 resultFile, err := os.ReadFile(result) 176 if err != nil { 177 return fmt.Errorf("couldn't read result: %v", err) 178 } 179 expectedFile, err := os.ReadFile(expected) 180 if err != nil { 181 return fmt.Errorf("couldn't read expected: %v", err) 182 } 183 if !bytes.Equal(resultFile, expectedFile) { 184 return fmt.Errorf("expected != result. Diff: %v", cmp.Diff(string(expectedFile), string(resultFile))) 185 } 186 return nil 187 } 188 189 // compareConfResult does a string compare of 2 test files. 190 func compareConfResult(result, expected string, t *testing.T) { 191 t.Helper() 192 retry.UntilSuccessOrFail(t, func() error { 193 return checkResult(result, expected) 194 }, retry.Delay(time.Millisecond*10), retry.Timeout(time.Second*3)) 195 } 196 197 // checkBinDir verifies the presence/absence of test files. 198 func checkBinDir(t *testing.T, tempCNIBinDir, op string, files ...string) error { 199 t.Helper() 200 for _, f := range files { 201 if _, err := os.Stat(tempCNIBinDir + "/" + f); !os.IsNotExist(err) { 202 if op == "add" { 203 t.Logf("PASS: File %v was added to %v", f, tempCNIBinDir) 204 return nil 205 } else if op == "del" { 206 return fmt.Errorf("FAIL: File %v was not removed from %v", f, tempCNIBinDir) 207 } 208 } else { 209 if op == "add" { 210 return fmt.Errorf("FAIL: File %v was not added to %v", f, tempCNIBinDir) 211 } else if op == "del" { 212 t.Logf("PASS: File %v was removed from %v", f, tempCNIBinDir) 213 return nil 214 } 215 } 216 } 217 218 return fmt.Errorf("no files, or unrecognized op") 219 } 220 221 // checkTempFilesCleaned verifies that all temporary files have been cleaned up 222 func checkTempFilesCleaned(tempCNIConfDir string, t *testing.T) { 223 t.Helper() 224 files, err := os.ReadDir(tempCNIConfDir) 225 if err != nil { 226 t.Fatalf("Failed to list files, err: %v", err) 227 } 228 for _, f := range files { 229 if strings.Contains(f.Name(), ".tmp") { 230 t.Fatalf("FAIL: Temporary file not cleaned in %v: %v", tempCNIConfDir, f.Name()) 231 } 232 } 233 t.Logf("PASS: All temporary files removed from %v", tempCNIConfDir) 234 } 235 236 // doTest sets up necessary environment variables, runs the Docker installation 237 // container and verifies output file correctness. 238 func doTest(t *testing.T, chainedCNIPlugin bool, wd, preConfFile, resultFileName, delayedConfFile, expectedOutputFile, 239 expectedPostCleanFile, tempCNIConfDir, tempCNIBinDir, tempK8sSvcAcctDir string, 240 ) { 241 t.Logf("prior cni-conf='%v', expected result='%v'", preConfFile, resultFileName) 242 243 // disable monitoring & repair 244 viper.Set(constants.MonitoringPort, 0) 245 viper.Set(constants.RepairEnabled, false) 246 247 // Don't set the CNI conf file env var if preConfFile is not set 248 var envPreconf string 249 if preConfFile != "" { 250 envPreconf = preConfFile 251 } else { 252 preConfFile = resultFileName 253 } 254 255 ztunnelAddr := "/tmp/ztfoo" 256 cniEventAddr := "/tmp/cnieventfoo" 257 defer os.Remove(ztunnelAddr) 258 defer os.Remove(cniEventAddr) 259 260 installConfig := config.Config{ 261 InstallConfig: config.InstallConfig{ 262 CNIEventAddress: cniEventAddr, 263 ZtunnelUDSAddress: ztunnelAddr, 264 MountedCNINetDir: tempCNIConfDir, 265 CNIBinSourceDir: filepath.Join(env.IstioSrc, "cni/test/testdata/bindir"), 266 CNIBinTargetDirs: []string{tempCNIBinDir}, 267 K8sServicePort: "443", 268 K8sServiceHost: "10.110.0.1", 269 MonitoringPort: 0, 270 LogUDSAddress: "", 271 KubeconfigFilename: "ZZZ-istio-cni-kubeconfig", 272 CNINetDir: "/etc/cni/net.d", 273 ChainedCNIPlugin: chainedCNIPlugin, 274 LogLevel: "debug", 275 ExcludeNamespaces: "istio-system", 276 KubeconfigMode: constants.DefaultKubeconfigMode, 277 CNIConfName: envPreconf, 278 K8sServiceAccountPath: tempK8sSvcAcctDir, 279 }, 280 } 281 282 ctx, cancel := context.WithCancel(context.Background()) 283 wg := sync.WaitGroup{} 284 285 wg.Add(1) 286 defer func() { 287 cancel() 288 wg.Wait() 289 }() 290 go func() { 291 startInstallServer(ctx, &installConfig, t) 292 wg.Done() 293 }() 294 295 resultFile := tempCNIConfDir + "/" + resultFileName 296 if chainedCNIPlugin && delayedConfFile != "" { 297 retry.UntilSuccessOrFail(t, func() error { 298 if err := checkResult(resultFile, expectedOutputFile); err == nil { 299 // We should have waited for the delayed conf 300 return fmt.Errorf("did not wait for valid config file") 301 } 302 return nil 303 }, retry.Delay(time.Millisecond), retry.Timeout(time.Millisecond*250)) 304 var destFilenm string 305 if preConfFile != "" { 306 destFilenm = preConfFile 307 } else { 308 destFilenm = delayedConfFile 309 } 310 cp(delayedConfFile, tempCNIConfDir+"/"+destFilenm, t) 311 } 312 313 retry.UntilSuccessOrFail(t, func() error { 314 return checkBinDir(t, tempCNIBinDir, "add", "istio-cni") 315 }, retry.Delay(time.Millisecond*10), retry.Timeout(time.Second*5)) 316 317 compareConfResult(resultFile, expectedOutputFile, t) 318 319 // Test script restart by removing configuration 320 if chainedCNIPlugin { 321 rmCNIConfig(resultFile, t) 322 } else if err := os.Remove(resultFile); err != nil { 323 t.Fatalf("error removing CNI config file: %s", resultFile) 324 } 325 // Verify configuration is still valid after removal 326 compareConfResult(resultFile, expectedOutputFile, t) 327 t.Log("PASS: Istio CNI configuration still valid after removal") 328 329 // Shutdown the install-cni 330 cancel() 331 wg.Wait() 332 333 t.Logf("Check the cleanup worked") 334 if chainedCNIPlugin { 335 if len(expectedPostCleanFile) == 0 { 336 compareConfResult(resultFile, wd+cniConfSubDir+preConfFile, t) 337 } else { 338 compareConfResult(resultFile, expectedPostCleanFile, t) 339 } 340 } else { 341 if file.Exists(resultFile) { 342 t.Logf("FAIL: Istio CNI config file was not removed: %s", resultFile) 343 } 344 } 345 retry.UntilSuccessOrFail(t, func() error { 346 return checkBinDir(t, tempCNIBinDir, "del", "istio-cni") 347 }, retry.Delay(time.Millisecond*10), retry.Timeout(time.Second*5)) 348 349 checkTempFilesCleaned(tempCNIConfDir, t) 350 } 351 352 // RunInstallCNITest sets up temporary directories and runs the test. 353 // 354 // Doing a go test install_cni.go by itself will not execute the test as the 355 // file doesn't have a _test.go suffix, and this func doesn't start with a Test 356 // prefix. This func is only meant to be invoked programmatically. A separate 357 // install_cni_test.go file exists for executing this test. 358 func RunInstallCNITest(t *testing.T, chainedCNIPlugin bool, preConfFile, resultFileName, delayedConfFile, expectedOutputFile, 359 expectedPostCleanFile string, cniConfDirOrderedFiles []string, 360 ) { 361 wd := env.IstioSrc + "/cni/test" 362 testWorkRootDir := getEnv("TEST_WORK_ROOTDIR", "/tmp") 363 364 tempCNIConfDir := mktemp(testWorkRootDir, "cni-conf-", t) 365 defer rmDir(tempCNIConfDir, t) 366 tempCNIBinDir := mktemp(testWorkRootDir, "cni-bin-", t) 367 defer rmDir(tempCNIBinDir, t) 368 tempK8sSvcAcctDir := mktemp(testWorkRootDir, "kube-svcacct-", t) 369 defer rmDir(tempK8sSvcAcctDir, t) 370 371 populateTempDirs(wd, cniConfDirOrderedFiles, tempCNIConfDir, tempK8sSvcAcctDir, t) 372 doTest(t, chainedCNIPlugin, wd, preConfFile, resultFileName, delayedConfFile, expectedOutputFile, 373 expectedPostCleanFile, tempCNIConfDir, tempCNIBinDir, tempK8sSvcAcctDir) 374 }