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 }