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  }