istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/pkg/install/cniconfig.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  	"encoding/json"
    20  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"sort"
    24  	"strings"
    25  
    26  	"github.com/containernetworking/cni/libcni"
    27  
    28  	"istio.io/istio/cni/pkg/config"
    29  	"istio.io/istio/cni/pkg/plugin"
    30  	"istio.io/istio/cni/pkg/util"
    31  	"istio.io/istio/pkg/file"
    32  )
    33  
    34  func createCNIConfigFile(ctx context.Context, cfg *config.InstallConfig) (string, error) {
    35  	pluginConfig := plugin.Config{
    36  		LogLevel:        cfg.LogLevel,
    37  		LogUDSAddress:   cfg.LogUDSAddress,
    38  		CNIEventAddress: cfg.CNIEventAddress,
    39  		AmbientEnabled:  cfg.AmbientEnabled,
    40  		Kubernetes: plugin.Kubernetes{
    41  			Kubeconfig:        filepath.Join(cfg.CNINetDir, cfg.KubeconfigFilename),
    42  			ExcludeNamespaces: strings.Split(cfg.ExcludeNamespaces, ","),
    43  		},
    44  	}
    45  
    46  	pluginConfig.Name = "istio-cni"
    47  	pluginConfig.Type = "istio-cni"
    48  	pluginConfig.CNIVersion = "0.3.1"
    49  
    50  	marshalledJSON, err := json.MarshalIndent(pluginConfig, "", "  ")
    51  	if err != nil {
    52  		return "", err
    53  	}
    54  	marshalledJSON = append(marshalledJSON, "\n"...)
    55  
    56  	return writeCNIConfig(ctx, marshalledJSON, cfg)
    57  }
    58  
    59  // writeCNIConfig will
    60  // 1. read in the existing CNI config file
    61  // 2. append the `istio`-specific entry
    62  // 3. write the combined result back out to the same path, overwriting the original.
    63  func writeCNIConfig(ctx context.Context, pluginConfig []byte, cfg *config.InstallConfig) (string, error) {
    64  	cniConfigFilepath, err := getCNIConfigFilepath(ctx, cfg.CNIConfName, cfg.MountedCNINetDir, cfg.ChainedCNIPlugin)
    65  	if err != nil {
    66  		return "", err
    67  	}
    68  
    69  	if cfg.ChainedCNIPlugin {
    70  		if !file.Exists(cniConfigFilepath) {
    71  			return "", fmt.Errorf("CNI config file %s removed during configuration", cniConfigFilepath)
    72  		}
    73  		// This section overwrites an existing plugins list entry for istio-cni
    74  		existingCNIConfig, err := os.ReadFile(cniConfigFilepath)
    75  		if err != nil {
    76  			return "", err
    77  		}
    78  		pluginConfig, err = insertCNIConfig(pluginConfig, existingCNIConfig)
    79  		if err != nil {
    80  			return "", err
    81  		}
    82  	}
    83  
    84  	if err = file.AtomicWrite(cniConfigFilepath, pluginConfig, os.FileMode(0o644)); err != nil {
    85  		installLog.Errorf("Failed to write CNI config file %v: %v", cniConfigFilepath, err)
    86  		return cniConfigFilepath, err
    87  	}
    88  
    89  	if cfg.ChainedCNIPlugin && strings.HasSuffix(cniConfigFilepath, ".conf") {
    90  		// If the old CNI config filename ends with .conf, rename it to .conflist, because it has to be changed to a list
    91  		installLog.Infof("Renaming %s extension to .conflist", cniConfigFilepath)
    92  		err = os.Rename(cniConfigFilepath, cniConfigFilepath+"list")
    93  		if err != nil {
    94  			installLog.Errorf("Failed to rename CNI config file %v: %v", cniConfigFilepath, err)
    95  			return cniConfigFilepath, err
    96  		}
    97  		cniConfigFilepath += "list"
    98  	}
    99  
   100  	installLog.Infof("Created CNI config %s", cniConfigFilepath)
   101  	installLog.Debugf("CNI config: %s", pluginConfig)
   102  	return cniConfigFilepath, nil
   103  }
   104  
   105  // If configured as chained CNI plugin, waits indefinitely for a main CNI config file to exist before returning
   106  // Or until cancelled by parent context
   107  func getCNIConfigFilepath(ctx context.Context, cniConfName, mountedCNINetDir string, chained bool) (string, error) {
   108  	if !chained {
   109  		if len(cniConfName) == 0 {
   110  			cniConfName = "YYY-istio-cni.conf"
   111  		}
   112  		return filepath.Join(mountedCNINetDir, cniConfName), nil
   113  	}
   114  
   115  	watcher, err := util.CreateFileWatcher(mountedCNINetDir)
   116  	if err != nil {
   117  		return "", err
   118  	}
   119  	defer watcher.Close()
   120  
   121  	for len(cniConfName) == 0 {
   122  		cniConfName, err = getDefaultCNINetwork(mountedCNINetDir)
   123  		if err == nil {
   124  			break
   125  		}
   126  		installLog.Warnf("Istio CNI is configured as chained plugin, but cannot find existing CNI network config: %v", err)
   127  		installLog.Infof("Waiting for CNI network config file to be written in %v...", mountedCNINetDir)
   128  		if err := watcher.Wait(ctx); err != nil {
   129  			return "", err
   130  		}
   131  	}
   132  
   133  	cniConfigFilepath := filepath.Join(mountedCNINetDir, cniConfName)
   134  
   135  	for !file.Exists(cniConfigFilepath) {
   136  		if strings.HasSuffix(cniConfigFilepath, ".conf") && file.Exists(cniConfigFilepath+"list") {
   137  			installLog.Infof("%s doesn't exist, but %[1]slist does; Using it as the CNI config file instead.", cniConfigFilepath)
   138  			cniConfigFilepath += "list"
   139  		} else if strings.HasSuffix(cniConfigFilepath, ".conflist") && file.Exists(cniConfigFilepath[:len(cniConfigFilepath)-4]) {
   140  			installLog.Infof("%s doesn't exist, but %s does; Using it as the CNI config file instead.", cniConfigFilepath, cniConfigFilepath[:len(cniConfigFilepath)-4])
   141  			cniConfigFilepath = cniConfigFilepath[:len(cniConfigFilepath)-4]
   142  		} else {
   143  			installLog.Infof("CNI config file %s does not exist. Waiting for file to be written...", cniConfigFilepath)
   144  			if err := watcher.Wait(ctx); err != nil {
   145  				return "", err
   146  			}
   147  		}
   148  	}
   149  
   150  	installLog.Infof("CNI config file %s exists. Proceeding.", cniConfigFilepath)
   151  
   152  	return cniConfigFilepath, err
   153  }
   154  
   155  // Follows the same semantics as kubelet
   156  // https://github.com/kubernetes/kubernetes/blob/954996e231074dc7429f7be1256a579bedd8344c/pkg/kubelet/dockershim/network/cni/cni.go#L144-L184
   157  func getDefaultCNINetwork(confDir string) (string, error) {
   158  	files, err := libcni.ConfFiles(confDir, []string{".conf", ".conflist"})
   159  	switch {
   160  	case err != nil:
   161  		return "", err
   162  	case len(files) == 0:
   163  		return "", fmt.Errorf("no networks found in %s", confDir)
   164  	}
   165  
   166  	sort.Strings(files)
   167  	for _, confFile := range files {
   168  		var confList *libcni.NetworkConfigList
   169  		if strings.HasSuffix(confFile, ".conflist") {
   170  			confList, err = libcni.ConfListFromFile(confFile)
   171  			if err != nil {
   172  				installLog.Warnf("Error loading CNI config list file %s: %v", confFile, err)
   173  				continue
   174  			}
   175  		} else {
   176  			conf, err := libcni.ConfFromFile(confFile)
   177  			if err != nil {
   178  				installLog.Warnf("Error loading CNI config file %s: %v", confFile, err)
   179  				continue
   180  			}
   181  			// Ensure the config has a "type" so we know what plugin to run.
   182  			// Also catches the case where somebody put a conflist into a conf file.
   183  			if conf.Network.Type == "" {
   184  				installLog.Warnf("Error loading CNI config file %s: no 'type'; perhaps this is a .conflist?", confFile)
   185  				continue
   186  			}
   187  
   188  			confList, err = libcni.ConfListFromConf(conf)
   189  			if err != nil {
   190  				installLog.Warnf("Error converting CNI config file %s to list: %v", confFile, err)
   191  				continue
   192  			}
   193  		}
   194  		if len(confList.Plugins) == 0 {
   195  			installLog.Warnf("CNI config list %s has no networks, skipping", confList.Name)
   196  			continue
   197  		}
   198  
   199  		return filepath.Base(confFile), nil
   200  	}
   201  
   202  	return "", fmt.Errorf("no valid networks found in %s", confDir)
   203  }
   204  
   205  // insertCNIConfig will append newCNIConfig to existingCNIConfig
   206  func insertCNIConfig(newCNIConfig, existingCNIConfig []byte) ([]byte, error) {
   207  	var istioMap map[string]any
   208  	err := json.Unmarshal(newCNIConfig, &istioMap)
   209  	if err != nil {
   210  		return nil, fmt.Errorf("error loading Istio CNI config (JSON error): %v", err)
   211  	}
   212  
   213  	var existingMap map[string]any
   214  	err = json.Unmarshal(existingCNIConfig, &existingMap)
   215  	if err != nil {
   216  		return nil, fmt.Errorf("error loading existing CNI config (JSON error): %v", err)
   217  	}
   218  
   219  	delete(istioMap, "cniVersion")
   220  
   221  	var newMap map[string]any
   222  
   223  	if _, ok := existingMap["type"]; ok {
   224  		// Assume it is a regular network conf file
   225  		delete(existingMap, "cniVersion")
   226  
   227  		plugins := make([]map[string]any, 2)
   228  		plugins[0] = existingMap
   229  		plugins[1] = istioMap
   230  
   231  		newMap = map[string]any{
   232  			"name":       "k8s-pod-network",
   233  			"cniVersion": "0.3.1",
   234  			"plugins":    plugins,
   235  		}
   236  	} else {
   237  		// Assume it is a network list file
   238  		newMap = existingMap
   239  		plugins, err := util.GetPlugins(newMap)
   240  		if err != nil {
   241  			return nil, fmt.Errorf("existing CNI config: %v", err)
   242  		}
   243  
   244  		for i, rawPlugin := range plugins {
   245  			plugin, err := util.GetPlugin(rawPlugin)
   246  			if err != nil {
   247  				return nil, fmt.Errorf("existing CNI plugin: %v", err)
   248  			}
   249  			if plugin["type"] == "istio-cni" {
   250  				plugins = append(plugins[:i], plugins[i+1:]...)
   251  				break
   252  			}
   253  		}
   254  
   255  		newMap["plugins"] = append(plugins, istioMap)
   256  	}
   257  
   258  	return util.MarshalCNIConfig(newMap)
   259  }