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  }