istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/pkg/install/install_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 "sync/atomic" 22 "testing" 23 "time" 24 25 "istio.io/istio/cni/pkg/config" 26 testutils "istio.io/istio/pilot/test/util" 27 "istio.io/istio/pkg/file" 28 "istio.io/istio/pkg/test/util/assert" 29 "istio.io/istio/pkg/util/sets" 30 ) 31 32 func TestCheckInstall(t *testing.T) { 33 cases := []struct { 34 name string 35 expectedFailure bool 36 cniConfigFilename string 37 cniConfName string 38 chainedCNIPlugin bool 39 existingConfFiles map[string]string // {srcFilename: targetFilename, ...} 40 }{ 41 { 42 name: "preempted config", 43 expectedFailure: true, 44 cniConfigFilename: "list.conflist", 45 chainedCNIPlugin: true, 46 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "list.conflist.golden": "list.conflist"}, 47 }, 48 { 49 name: "intentional preempted config invalid", 50 expectedFailure: true, 51 cniConfigFilename: "invalid-arr.conflist", 52 cniConfName: "invalid-arr.conflist", 53 chainedCNIPlugin: true, 54 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "invalid-arr.conflist": "invalid-arr.conflist"}, 55 }, 56 { 57 name: "intentional preempted config", 58 cniConfigFilename: "list.conflist", 59 cniConfName: "list.conflist", 60 chainedCNIPlugin: true, 61 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf", "list.conflist.golden": "list.conflist"}, 62 }, 63 { 64 name: "CNI config file removed", 65 expectedFailure: true, 66 cniConfigFilename: "file-removed.conflist", 67 }, 68 { 69 name: "istio-cni config removed from CNI config file", 70 expectedFailure: true, 71 cniConfigFilename: "list.conflist", 72 chainedCNIPlugin: true, 73 existingConfFiles: map[string]string{"list.conflist": "list.conflist"}, 74 }, 75 { 76 name: "chained CNI plugin", 77 cniConfigFilename: "list.conflist", 78 chainedCNIPlugin: true, 79 existingConfFiles: map[string]string{"list.conflist.golden": "list.conflist"}, 80 }, 81 { 82 name: "standalone CNI plugin istio-cni config not in CNI config file", 83 expectedFailure: true, 84 cniConfigFilename: "bridge.conf", 85 existingConfFiles: map[string]string{"bridge.conf": "bridge.conf"}, 86 }, 87 { 88 name: "standalone CNI plugin", 89 cniConfigFilename: "istio-cni.conf", 90 existingConfFiles: map[string]string{"istio-cni.conf": "istio-cni.conf"}, 91 }, 92 } 93 94 for _, c := range cases { 95 t.Run(c.name, func(t *testing.T) { 96 // Create temp directory for files 97 tempDir := t.TempDir() 98 99 // Create existing config files if specified in test case 100 for srcFilename, targetFilename := range c.existingConfFiles { 101 if err := file.AtomicCopy(filepath.Join("testdata", srcFilename), tempDir, targetFilename); err != nil { 102 t.Fatal(err) 103 } 104 } 105 106 cfg := &config.InstallConfig{ 107 MountedCNINetDir: tempDir, 108 CNIConfName: c.cniConfName, 109 ChainedCNIPlugin: c.chainedCNIPlugin, 110 } 111 err := checkValidCNIConfig(cfg, filepath.Join(tempDir, c.cniConfigFilename)) 112 if (c.expectedFailure && err == nil) || (!c.expectedFailure && err != nil) { 113 t.Fatalf("expected failure: %t, got %v", c.expectedFailure, err) 114 } 115 }) 116 } 117 } 118 119 func TestSleepCheckInstall(t *testing.T) { 120 cases := []struct { 121 name string 122 chainedCNIPlugin bool 123 cniConfigFilename string 124 invalidConfigFilename string 125 validConfigFilename string 126 saFilename string 127 saNewFilename string 128 }{ 129 { 130 name: "chained CNI plugin", 131 chainedCNIPlugin: true, 132 cniConfigFilename: "plugins.conflist", 133 invalidConfigFilename: "list.conflist", 134 validConfigFilename: "list.conflist.golden", 135 saFilename: "token-foo", 136 }, 137 { 138 name: "standalone CNI plugin", 139 cniConfigFilename: "istio-cni.conf", 140 validConfigFilename: "istio-cni.conf", 141 saFilename: "token-foo", 142 saNewFilename: "token-bar", 143 }, 144 } 145 146 for _, c := range cases { 147 t.Run(c.name, func(t *testing.T) { 148 // Create temp directory for files 149 tempDir := t.TempDir() 150 151 // Initialize parameters 152 ctx, cancel := context.WithCancel(context.Background()) 153 defer cancel() 154 cfg := &config.InstallConfig{ 155 MountedCNINetDir: tempDir, 156 ChainedCNIPlugin: c.chainedCNIPlugin, 157 } 158 cniConfigFilepath := filepath.Join(tempDir, c.cniConfigFilename) 159 isReady := &atomic.Value{} 160 setNotReady(isReady) 161 in := NewInstaller(cfg, isReady) 162 in.cniConfigFilepath = cniConfigFilepath 163 164 if err := file.AtomicCopy(filepath.Join("testdata", c.saFilename), tempDir, c.saFilename); err != nil { 165 t.Fatal(err) 166 } 167 168 if len(c.invalidConfigFilename) > 0 { 169 // Copy an invalid config file into tempDir 170 if err := file.AtomicCopy(filepath.Join("testdata", c.invalidConfigFilename), tempDir, c.cniConfigFilename); err != nil { 171 t.Fatal(err) 172 } 173 } 174 175 t.Log("Expecting an invalid configuration log:") 176 err := in.sleepWatchInstall(ctx, sets.String{}) 177 if err != nil { 178 t.Fatalf("error should be nil due to invalid config, got: %v", err) 179 } 180 assert.Equal(t, isReady.Load(), false) 181 182 if len(c.invalidConfigFilename) > 0 { 183 if err := os.Remove(cniConfigFilepath); err != nil { 184 t.Fatal(err) 185 } 186 } 187 188 // Copy a valid config file into tempDir 189 if err := file.AtomicCopy(filepath.Join("testdata", c.validConfigFilename), tempDir, c.cniConfigFilename); err != nil { 190 t.Fatal(err) 191 } 192 193 // Listen for isReady to be set to true 194 ticker := time.NewTicker(500 * time.Millisecond) 195 defer ticker.Stop() 196 readyChan := make(chan bool) 197 go func(ctx context.Context, tick <-chan time.Time) { 198 for { 199 select { 200 case <-ctx.Done(): 201 return 202 case <-tick: 203 if isReady.Load().(bool) { 204 readyChan <- true 205 } 206 } 207 } 208 }(ctx, ticker.C) 209 210 // Listen to sleepWatchInstall return value 211 // Should detect a valid configuration and wait indefinitely for a file modification 212 errChan := make(chan error) 213 go func(ctx context.Context) { 214 errChan <- in.sleepWatchInstall(ctx, sets.String{}) 215 }(ctx) 216 217 select { 218 case <-readyChan: 219 assert.Equal(t, isReady.Load(), true) 220 case err := <-errChan: 221 if err == nil { 222 t.Fatal("invalid configuration detected") 223 } 224 t.Fatal(err) 225 case <-time.After(5 * time.Second): 226 t.Fatal("timed out waiting for isReady to be set to true") 227 } 228 229 // Change SA token 230 if len(c.saNewFilename) > 0 { 231 t.Log("Expecting detect changes to the SA token") 232 if err := file.AtomicCopy(filepath.Join("testdata", c.saNewFilename), tempDir, c.saFilename); err != nil { 233 t.Fatal(err) 234 } 235 236 select { 237 case err := <-errChan: 238 if err != nil { 239 // A change in SA token should return nil 240 t.Fatal(err) 241 } 242 assert.Equal(t, isReady.Load(), false) 243 case <-time.After(5 * time.Second): 244 t.Fatal("timed out waiting for invalid configuration to be detected") 245 } 246 247 // Revert valid SA 248 if err := file.AtomicCopy(filepath.Join("testdata", c.saFilename), tempDir, c.saFilename); err != nil { 249 t.Fatal(err) 250 } 251 252 // Run sleepWatchInstall 253 go func(ctx context.Context, in *Installer) { 254 errChan <- in.sleepWatchInstall(ctx, sets.String{}) 255 }(ctx, in) 256 } 257 258 // Remove Istio CNI's config 259 t.Log("Expecting an invalid configuration log:") 260 if len(c.invalidConfigFilename) > 0 { 261 if err := file.AtomicCopy(filepath.Join("testdata", c.invalidConfigFilename), tempDir, c.cniConfigFilename); err != nil { 262 t.Fatal(err) 263 } 264 } else { 265 if err := os.Remove(cniConfigFilepath); err != nil { 266 t.Fatal(err) 267 } 268 } 269 270 select { 271 case err := <-errChan: 272 if err != nil { 273 // An invalid configuration should return nil 274 // Either an invalid config did not return nil (which is an issue) or an unexpected error occurred 275 t.Fatal(err) 276 } 277 assert.Equal(t, isReady.Load(), false) 278 case <-time.After(5 * time.Second): 279 t.Fatal("timed out waiting for invalid configuration to be detected") 280 } 281 }) 282 } 283 } 284 285 func TestCleanup(t *testing.T) { 286 cases := []struct { 287 name string 288 expectedFailure bool 289 chainedCNIPlugin bool 290 configFilename string 291 existingConfigFilename string 292 expectedConfigFilename string 293 }{ 294 { 295 name: "chained CNI plugin", 296 chainedCNIPlugin: true, 297 configFilename: "list.conflist", 298 existingConfigFilename: "list-with-istio.conflist", 299 expectedConfigFilename: "list-no-istio.conflist", 300 }, 301 { 302 name: "standalone CNI plugin", 303 configFilename: "istio-cni.conf", 304 existingConfigFilename: "istio-cni.conf", 305 }, 306 } 307 308 for _, c := range cases { 309 t.Run(c.name, func(t *testing.T) { 310 // Create temp directory for files 311 cniNetDir := t.TempDir() 312 cniBinDir := t.TempDir() 313 314 // Create existing config file if specified in test case 315 cniConfigFilePath := filepath.Join(cniNetDir, c.configFilename) 316 if err := file.AtomicCopy(filepath.Join("testdata", c.existingConfigFilename), cniNetDir, c.configFilename); err != nil { 317 t.Fatal(err) 318 } 319 320 // Create existing binary files 321 if err := os.WriteFile(filepath.Join(cniBinDir, "istio-cni"), []byte{1, 2, 3}, 0o755); err != nil { 322 t.Fatal(err) 323 } 324 325 // Create kubeconfig 326 kubeConfigFilePath := filepath.Join(cniNetDir, "kubeconfig") 327 if err := os.WriteFile(kubeConfigFilePath, []byte{1, 2, 3}, 0o755); err != nil { 328 t.Fatal(err) 329 } 330 331 cfg := &config.InstallConfig{ 332 MountedCNINetDir: cniNetDir, 333 ChainedCNIPlugin: c.chainedCNIPlugin, 334 CNIBinTargetDirs: []string{cniBinDir}, 335 } 336 337 isReady := &atomic.Value{} 338 isReady.Store(false) 339 installer := NewInstaller(cfg, isReady) 340 installer.cniConfigFilepath = cniConfigFilePath 341 installer.kubeconfigFilepath = kubeConfigFilePath 342 err := installer.Cleanup() 343 if (c.expectedFailure && err == nil) || (!c.expectedFailure && err != nil) { 344 t.Fatalf("expected failure: %t, got %v", c.expectedFailure, err) 345 } 346 347 // check if conf file is deleted/conflist file is updated 348 if c.chainedCNIPlugin { 349 resultConfig := testutils.ReadFile(t, cniConfigFilePath) 350 351 goldenFilepath := filepath.Join("testdata", c.expectedConfigFilename) 352 goldenConfig := testutils.ReadFile(t, goldenFilepath) 353 testutils.CompareBytes(t, resultConfig, goldenConfig, goldenFilepath) 354 } else if file.Exists(cniConfigFilePath) { 355 t.Fatalf("file %s was not deleted", c.configFilename) 356 } 357 358 // check if kubeconfig is deleted 359 if file.Exists(kubeConfigFilePath) { 360 t.Fatal("kubeconfig was not deleted") 361 } 362 363 // check if binaries are deleted 364 if file.Exists(filepath.Join(cniBinDir, "istio-cni")) { 365 t.Fatalf("File %s was not deleted", "istio-cni") 366 } 367 }) 368 } 369 }