istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/pkg/install/install.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  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"sync/atomic"
    23  
    24  	"istio.io/istio/cni/pkg/config"
    25  	"istio.io/istio/cni/pkg/util"
    26  	"istio.io/istio/pkg/file"
    27  	"istio.io/istio/pkg/log"
    28  	"istio.io/istio/pkg/util/sets"
    29  )
    30  
    31  // TODO this should share parent scope, in practice it isn't useful to hide it within its own granular scope.
    32  var installLog = log.RegisterScope("install", "CNI install")
    33  
    34  type Installer struct {
    35  	cfg                *config.InstallConfig
    36  	isReady            *atomic.Value
    37  	kubeconfigFilepath string
    38  	cniConfigFilepath  string
    39  }
    40  
    41  // NewInstaller returns an instance of Installer with the given config
    42  func NewInstaller(cfg *config.InstallConfig, isReady *atomic.Value) *Installer {
    43  	return &Installer{
    44  		cfg:                cfg,
    45  		kubeconfigFilepath: filepath.Join(cfg.MountedCNINetDir, cfg.KubeconfigFilename),
    46  		isReady:            isReady,
    47  	}
    48  }
    49  
    50  func (in *Installer) installAll(ctx context.Context) (sets.String, error) {
    51  	// Install binaries
    52  	// Currently we _always_ do this, since the binaries do not live in a shared location
    53  	// and we harm no one by doing so.
    54  	copiedFiles, err := copyBinaries(in.cfg.CNIBinSourceDir, in.cfg.CNIBinTargetDirs)
    55  	if err != nil {
    56  		cniInstalls.With(resultLabel.Value(resultCopyBinariesFailure)).Increment()
    57  		return copiedFiles, fmt.Errorf("copy binaries: %v", err)
    58  	}
    59  
    60  	// Install kubeconfig (if needed) - we write/update this in the shared node CNI netdir,
    61  	// which may be watched by other CNIs, and so we don't want to trigger writes to this file
    62  	// unless it's missing or the contents are not what we expect.
    63  	if err := maybeWriteKubeConfigFile(in.cfg); err != nil {
    64  		cniInstalls.With(resultLabel.Value(resultCreateKubeConfigFailure)).Increment()
    65  		return copiedFiles, fmt.Errorf("write kubeconfig: %v", err)
    66  	}
    67  
    68  	// Install CNI netdir config (if needed) - we write/update this in the shared node CNI netdir,
    69  	// which may be watched by other CNIs, and so we don't want to trigger writes to this file
    70  	// unless it's missing or the contents are not what we expect.
    71  	if err := checkValidCNIConfig(in.cfg, in.cniConfigFilepath); err != nil {
    72  		installLog.Infof("missing (or invalid) configuration detected, (re)writing CNI config file at %s", in.cniConfigFilepath)
    73  		cfgPath, err := createCNIConfigFile(ctx, in.cfg)
    74  		if err != nil {
    75  			cniInstalls.With(resultLabel.Value(resultCreateCNIConfigFailure)).Increment()
    76  			return copiedFiles, fmt.Errorf("create CNI config file: %v", err)
    77  		}
    78  		in.cniConfigFilepath = cfgPath
    79  	} else {
    80  		installLog.Infof("valid Istio config present in node-level CNI file %s, not modifying", in.cniConfigFilepath)
    81  	}
    82  
    83  	return copiedFiles, nil
    84  }
    85  
    86  // Run starts the installation process, verifies the configuration, then sleeps.
    87  // If the configuration is invalid, a full redeployal of config, binaries, and svcAcct credentials to the
    88  // shared node CNI dir will be attempted.
    89  //
    90  // If changes occurred but the config is still valid, only the binaries and (optionally) svcAcct credentials
    91  // will be redeployed.
    92  func (in *Installer) Run(ctx context.Context) error {
    93  	installedBins, err := in.installAll(ctx)
    94  	if err != nil {
    95  		return err
    96  	}
    97  	installLog.Info("Installation succeed, start watching for re-installation.")
    98  
    99  	for {
   100  		// if sleepWatchInstall yields without error, that means the config might have been modified in some fashion.
   101  		// so we rerun `install`, which will update the modified config if it has fallen out of sync with
   102  		// our desired state
   103  		err := in.sleepWatchInstall(ctx, installedBins)
   104  		if err != nil {
   105  			installLog.Error("error watching node CNI config")
   106  			return err
   107  		}
   108  		installLog.Info("Detected changes to the node-level CNI setup, checking to see if configs or binaries need redeploying")
   109  		// We don't support (or want) to silently (re)deploy any binaries that were not in the initial "snapshot"
   110  		// so we intentionally discard/do not update the list of installedBins on redeploys.
   111  		if _, err := in.installAll(ctx); err != nil {
   112  			return err
   113  		}
   114  		installLog.Info("Istio CNI configuration and binaries validated/reinstalled.")
   115  	}
   116  }
   117  
   118  // Cleanup remove Istio CNI's config, kubeconfig file, and binaries.
   119  func (in *Installer) Cleanup() error {
   120  	installLog.Info("Cleaning up.")
   121  	if len(in.cniConfigFilepath) > 0 && file.Exists(in.cniConfigFilepath) {
   122  		if in.cfg.ChainedCNIPlugin {
   123  			installLog.Infof("Removing Istio CNI config from CNI config file: %s", in.cniConfigFilepath)
   124  
   125  			// Read JSON from CNI config file
   126  			cniConfigMap, err := util.ReadCNIConfigMap(in.cniConfigFilepath)
   127  			if err != nil {
   128  				return err
   129  			}
   130  			// Find Istio CNI and remove from plugin list
   131  			plugins, err := util.GetPlugins(cniConfigMap)
   132  			if err != nil {
   133  				return fmt.Errorf("%s: %w", in.cniConfigFilepath, err)
   134  			}
   135  			for i, rawPlugin := range plugins {
   136  				plugin, err := util.GetPlugin(rawPlugin)
   137  				if err != nil {
   138  					return fmt.Errorf("%s: %w", in.cniConfigFilepath, err)
   139  				}
   140  				if plugin["type"] == "istio-cni" {
   141  					cniConfigMap["plugins"] = append(plugins[:i], plugins[i+1:]...)
   142  					break
   143  				}
   144  			}
   145  
   146  			cniConfig, err := util.MarshalCNIConfig(cniConfigMap)
   147  			if err != nil {
   148  				return err
   149  			}
   150  			if err = file.AtomicWrite(in.cniConfigFilepath, cniConfig, os.FileMode(0o644)); err != nil {
   151  				return err
   152  			}
   153  		} else {
   154  			installLog.Infof("Removing Istio CNI config file: %s", in.cniConfigFilepath)
   155  			if err := os.Remove(in.cniConfigFilepath); err != nil {
   156  				return err
   157  			}
   158  		}
   159  	}
   160  
   161  	if len(in.kubeconfigFilepath) > 0 && file.Exists(in.kubeconfigFilepath) {
   162  		installLog.Infof("Removing Istio CNI kubeconfig file: %s", in.kubeconfigFilepath)
   163  		if err := os.Remove(in.kubeconfigFilepath); err != nil {
   164  			return err
   165  		}
   166  	}
   167  
   168  	for _, targetDir := range in.cfg.CNIBinTargetDirs {
   169  		if istioCNIBin := filepath.Join(targetDir, "istio-cni"); file.Exists(istioCNIBin) {
   170  			installLog.Infof("Removing binary: %s", istioCNIBin)
   171  			if err := os.Remove(istioCNIBin); err != nil {
   172  				return err
   173  			}
   174  		}
   175  	}
   176  	return nil
   177  }
   178  
   179  // sleepWatchInstall blocks until any file change for the binaries or config are detected.
   180  // At that point, the func yields so the caller can recheck the validity of the install.
   181  // If an error occurs or context is canceled, the function will return an error.
   182  func (in *Installer) sleepWatchInstall(ctx context.Context, installedBinFiles sets.String) error {
   183  	// Watch our specific binaries, in each configured binary dir.
   184  	// We may or may not be the only CNI plugin in play, and if we are not
   185  	// we shouldn't fire events for binaries that are not ours.
   186  	var binPaths []string
   187  	for _, bindir := range in.cfg.CNIBinTargetDirs {
   188  		for _, binary := range installedBinFiles.UnsortedList() {
   189  			binPaths = append(binPaths, filepath.Join(bindir, binary))
   190  		}
   191  	}
   192  	targets := append(
   193  		binPaths,
   194  		in.cfg.MountedCNINetDir,
   195  		in.cfg.K8sServiceAccountPath,
   196  	)
   197  	// Create file watcher before checking for installation
   198  	// so that no file modifications are missed while and after checking
   199  	// note: we create a file watcher for each invocation, otherwise when we write to the directories
   200  	// we would get infinite looping of events
   201  	//
   202  	// Additionally, fsnotify will lose existing watches on atomic copies (due to overwrite/rename),
   203  	// so we have to re-watch after re-copy to make sure we always have fresh watches.
   204  	watcher, err := util.CreateFileWatcher(targets...)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	defer func() {
   209  		setNotReady(in.isReady)
   210  		watcher.Close()
   211  	}()
   212  
   213  	// Before we process whether any file events have been triggered, we must check that the file is correct
   214  	// at this moment, and if not, yield. This is to catch other CNIs which might have mutated the file between
   215  	// the (theoretical) window after we initially install/write, but before we actually start the filewatch.
   216  	if err := checkValidCNIConfig(in.cfg, in.cniConfigFilepath); err != nil {
   217  		return nil
   218  	}
   219  
   220  	// If a file we are watching has a change event, yield and let caller check validity
   221  	select {
   222  	case <-watcher.Events:
   223  		// Something changed, and we must yield
   224  		return nil
   225  	case err := <-watcher.Errors:
   226  		// We had a watch error - that's no good
   227  		return err
   228  	case <-ctx.Done():
   229  		return ctx.Err()
   230  	default:
   231  		// Valid configuration; set isReady to true and wait for modifications before checking again
   232  		setReady(in.isReady)
   233  		cniInstalls.With(resultLabel.Value(resultSuccess)).Increment()
   234  		// Pod set to "NotReady" before termination
   235  		return watcher.Wait(ctx)
   236  	}
   237  }
   238  
   239  // checkValidCNIConfig returns an error if an invalid CNI configuration is detected
   240  func checkValidCNIConfig(cfg *config.InstallConfig, cniConfigFilepath string) error {
   241  	defaultCNIConfigFilename, err := getDefaultCNINetwork(cfg.MountedCNINetDir)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	defaultCNIConfigFilepath := filepath.Join(cfg.MountedCNINetDir, defaultCNIConfigFilename)
   246  	if defaultCNIConfigFilepath != cniConfigFilepath {
   247  		if len(cfg.CNIConfName) > 0 || !cfg.ChainedCNIPlugin {
   248  			// Install was run with overridden CNI config file so don't error out on preempt check
   249  			// Likely the only use for this is testing the script
   250  			installLog.Warnf("CNI config file %s preempted by %s", cniConfigFilepath, defaultCNIConfigFilepath)
   251  		} else {
   252  			return fmt.Errorf("CNI config file %s preempted by %s", cniConfigFilepath, defaultCNIConfigFilepath)
   253  		}
   254  	}
   255  
   256  	if !file.Exists(cniConfigFilepath) {
   257  		return fmt.Errorf("CNI config file removed: %s", cniConfigFilepath)
   258  	}
   259  
   260  	if cfg.ChainedCNIPlugin {
   261  		// Verify that Istio CNI config exists in the CNI config plugin list
   262  		cniConfigMap, err := util.ReadCNIConfigMap(cniConfigFilepath)
   263  		if err != nil {
   264  			return err
   265  		}
   266  		plugins, err := util.GetPlugins(cniConfigMap)
   267  		if err != nil {
   268  			return fmt.Errorf("%s: %w", cniConfigFilepath, err)
   269  		}
   270  		for _, rawPlugin := range plugins {
   271  			plugin, err := util.GetPlugin(rawPlugin)
   272  			if err != nil {
   273  				return fmt.Errorf("%s: %w", cniConfigFilepath, err)
   274  			}
   275  			if plugin["type"] == "istio-cni" {
   276  				return nil
   277  			}
   278  		}
   279  
   280  		return fmt.Errorf("istio-cni CNI config removed from CNI config file: %s", cniConfigFilepath)
   281  	}
   282  	// Verify that Istio CNI config exists as a standalone plugin
   283  	cniConfigMap, err := util.ReadCNIConfigMap(cniConfigFilepath)
   284  	if err != nil {
   285  		return err
   286  	}
   287  
   288  	if cniConfigMap["type"] != "istio-cni" {
   289  		return fmt.Errorf("istio-cni CNI config file modified: %s", cniConfigFilepath)
   290  	}
   291  	return nil
   292  }
   293  
   294  // Sets isReady to true.
   295  func setReady(isReady *atomic.Value) {
   296  	installReady.Record(1)
   297  	isReady.Store(true)
   298  }
   299  
   300  // Sets isReady to false.
   301  func setNotReady(isReady *atomic.Value) {
   302  	installReady.Record(0)
   303  	isReady.Store(false)
   304  }