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  }