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 }