github.com/webmeshproj/webmesh-cni@v0.0.27/internal/types/install.go (about) 1 /* 2 Copyright 2023 Avi Zimmerman <avi.zimmerman@gmail.com>. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package types 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "flag" 23 "fmt" 24 "io" 25 "log" 26 "os" 27 "path/filepath" 28 "runtime" 29 "strings" 30 31 "github.com/mitchellh/mapstructure" 32 "k8s.io/client-go/rest" 33 "k8s.io/client-go/tools/clientcmd" 34 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 35 ctrl "sigs.k8s.io/controller-runtime" 36 ) 37 38 // InstallOptions are the options for the install component. 39 type InstallOptions struct { 40 // Kubeconfig is the kubeconfig to use for the plugin. 41 Kubeconfig string `json:"kubeconfig" mapstructure:"kubeconfig"` 42 // SourceBinary is the path to the source binary. 43 SourceBinary string `json:"sourceBinary" mapstructure:"sourceBinary"` 44 // BinaryDestBin is the destination directory for the CNI binaries. 45 BinaryDestBin string `json:"binaryDestBin" mapstructure:"binaryDestBin"` 46 // ConfDestDir is the destination directory for the CNI configuration. 47 ConfDestDir string `json:"confDestDir" mapstructure:"confDestDir"` 48 // ConfDestName is the name of the CNI configuration file. 49 ConfDestName string `json:"confDestName" mapstructure:"confDestName"` 50 // HostLocalNetDir is the directory containing host-local IPAM allocations. 51 // We release these when we start for the first time. 52 HostLocalNetDir string `json:"hostLocalNetDir" mapstructure:"hostLocalNetDir"` 53 // NetConfTemplate is the template for the CNI configuration. 54 NetConfTemplate string `json:"netConfTemplate" mapstructure:"netConfTemplate"` 55 // NodeName is the name of the node we are running on. 56 NodeName string `json:"nodeName" mapstructure:"nodeName"` 57 // Namespace is the namespace to use for the plugin. 58 Namespace string `json:"namespace" mapstructure:"namespace"` 59 // DryRun is whether or not to run in dry run mode. 60 DryRun bool `json:"dryRun" mapstructure:"dryRun"` 61 } 62 63 // String returns a string representation of the install options. 64 func (i *InstallOptions) String() string { 65 mapstruct := map[string]any{} 66 err := mapstructure.Decode(i, &mapstruct) 67 if err != nil { 68 return fmt.Sprintf("error decoding install options: %s", err.Error()) 69 } 70 delete(mapstruct, "netConfTemplate") 71 confTempl := map[string]any{} 72 err = json.Unmarshal([]byte(i.NetConfTemplate), &confTempl) 73 if err == nil { 74 mapstruct["netConfTemplate"] = confTempl 75 } else { 76 mapstruct["netConfTemplate"] = "error parsing netconf template: " + err.Error() 77 } 78 out, _ := json.MarshalIndent(mapstruct, "", " ") 79 return string(out) 80 } 81 82 // BindFlags binds the install options to the given flag set. 83 func (i *InstallOptions) BindFlags(fs *flag.FlagSet) { 84 fs.BoolVar(&i.DryRun, "dry-run", i.DryRun, "whether or not to run in dry run mode") 85 fs.StringVar(&i.SourceBinary, "source-binary", i.SourceBinary, "path to the source binary (default: current executable)") 86 fs.StringVar(&i.BinaryDestBin, "binary-dest-bin", i.BinaryDestBin, "destination directory for the CNI binaries") 87 fs.StringVar(&i.ConfDestDir, "conf-dest-dir", i.ConfDestDir, "destination directory for the CNI configuration") 88 fs.StringVar(&i.ConfDestName, "conf-dest-name", i.ConfDestName, "name of the CNI configuration file") 89 fs.StringVar(&i.HostLocalNetDir, "host-local-net-dir", i.HostLocalNetDir, "directory containing host-local IPAM allocations to clear, leave this empty to disable") 90 fs.StringVar(&i.NodeName, "node-name", i.NodeName, "name of the node we are running on") 91 fs.StringVar(&i.Namespace, "namespace", i.Namespace, "namespace to use for the plugin") 92 fs.Func("netconf-template", "template file for the CNI configuration", func(fname string) error { 93 data, err := os.ReadFile(fname) 94 if err != nil { 95 return fmt.Errorf("read file: %w", err) 96 } 97 i.NetConfTemplate = string(data) 98 return nil 99 }) 100 } 101 102 // getExecutable is the function for retrieving the current executable. 103 // This is overridden in tests. 104 var getExecutable = os.Executable 105 106 // LoadInstallOptionsFromEnv loads the install options from the environment. 107 func LoadInstallOptionsFromEnv() *InstallOptions { 108 var opts InstallOptions 109 opts.Kubeconfig = envOrDefault(KubeconfigEnvVar, "") 110 opts.NodeName = envOrDefault(NodeNameEnvVar, "") 111 opts.HostLocalNetDir = envOrDefault(CNINetDirEnvVar, os.Getenv(CNINetDirEnvVar)) 112 opts.Namespace = envOrDefault(PodNamespaceEnvVar, DefaultNamespace) 113 opts.BinaryDestBin = envOrDefault(DestBinEnvVar, DefaultDestBin) 114 opts.ConfDestDir = envOrDefault(DestConfEnvVar, DefaultDestConfDir) 115 opts.ConfDestName = envOrDefault(DestConfFileNameEnvVar, DefaultDestConfFilename) 116 opts.NetConfTemplate = os.Getenv(NetConfTemplateEnvVar) 117 opts.SourceBinary, _ = getExecutable() 118 if dryrun, ok := os.LookupEnv(DryRunEnvVar); ok { 119 opts.DryRun = dryrun == "true" || dryrun == "1" 120 } 121 return &opts 122 } 123 124 func envOrDefault(env, def string) string { 125 if val, ok := os.LookupEnv(env); ok { 126 return val 127 } 128 return def 129 } 130 131 func (i *InstallOptions) Default() { 132 if i.SourceBinary == "" { 133 i.SourceBinary, _ = getExecutable() 134 } 135 if i.BinaryDestBin == "" { 136 i.BinaryDestBin = DefaultDestBin 137 } 138 if i.ConfDestDir == "" { 139 i.ConfDestDir = DefaultDestConfDir 140 } 141 if i.ConfDestName == "" { 142 i.ConfDestName = DefaultDestConfFilename 143 } 144 if i.Namespace == "" { 145 i.Namespace, _ = GetInClusterNamespace() 146 } 147 } 148 149 func (i *InstallOptions) Validate() error { 150 i.Default() 151 if i.SourceBinary == "" { 152 return fmt.Errorf("source binary not set") 153 } 154 if i.BinaryDestBin == "" { 155 return fmt.Errorf("binary destination directory not set") 156 } 157 if i.ConfDestDir == "" { 158 return fmt.Errorf("configuration destination directory not set") 159 } 160 if i.ConfDestName == "" { 161 return fmt.Errorf("configuration destination name not set") 162 } 163 if i.NetConfTemplate == "" { 164 return fmt.Errorf("CNI configuration template not set") 165 } 166 if i.NodeName == "" { 167 return fmt.Errorf("node name not set") 168 } 169 if i.Namespace == "" { 170 return fmt.Errorf("%s not set and unable to get in-cluster namespace", PodNamespaceEnvVar) 171 } 172 err := json.Unmarshal([]byte(i.NetConfTemplate), &struct{}{}) 173 if err != nil { 174 return fmt.Errorf("CNI configuration template is not proper JSON: %w", err) 175 } 176 return nil 177 } 178 179 // getInstallRestConfig is the function for retrieving the REST config during installation. 180 // This is overridden in tests. 181 var getInstallRestConfig = ctrl.GetConfig 182 183 // RunInstall is an alias for running all install steps. 184 func (i *InstallOptions) RunInstall() error { 185 var apicfg *rest.Config 186 var err error 187 if i.Kubeconfig == "" { 188 log.Println("no kubeconfig provided, trying to auto-detect") 189 apicfg, err = getInstallRestConfig() 190 if err != nil { 191 log.Println("error getting kubeconfig:", err) 192 return err 193 } 194 } else { 195 log.Println("using kubeconfig provided at", i.Kubeconfig) 196 apicfg, err = clientcmd.BuildConfigFromKubeconfigGetter("", func() (*clientcmdapi.Config, error) { 197 return clientcmd.LoadFromFile(i.Kubeconfig) 198 }) 199 if err != nil { 200 log.Println("error getting kubeconfig:", err) 201 return err 202 } 203 } 204 // Clear any local host IPAM allocations that already exist. 205 if i.HostLocalNetDir != "" { 206 log.Println("clearing host-local IPAM allocations from", i.HostLocalNetDir) 207 if !i.DryRun { 208 err = i.ClearHostLocalIPAMAllocations() 209 if err != nil { 210 log.Println("error clearing host-local IPAM allocations:", err) 211 return err 212 } 213 } 214 } 215 pluginBin := filepath.Join(i.BinaryDestBin, PluginBinaryName) 216 log.Println("installing plugin binary to -> ", pluginBin) 217 if !i.DryRun { 218 err = i.InstallPlugin(pluginBin) 219 if err != nil { 220 log.Println("error installing plugin:", err) 221 return err 222 } 223 } 224 kubeconfigPath := filepath.Join(i.ConfDestDir, PluginKubeconfigName) 225 log.Println("installing kubeconfig to destination -> ", kubeconfigPath) 226 if !i.DryRun { 227 err = i.InstallKubeconfig(kubeconfigPath) 228 if err != nil { 229 log.Println("error writing kubeconfig:", err) 230 return err 231 } 232 } 233 log.Println("rendering CNI configuration") 234 netConf := i.RenderNetConf(apicfg.Host, strings.TrimPrefix(kubeconfigPath, "/host")) 235 log.Println("effective CNI configuration ->\n", netConf) 236 confPath := filepath.Join(i.ConfDestDir, i.ConfDestName) 237 log.Println("installing CNI configuration to destination -> ", confPath) 238 if !i.DryRun { 239 err = i.InstallNetConf(confPath, netConf) 240 if err != nil { 241 log.Println("error writing netconf:", err) 242 return err 243 } 244 } 245 return nil 246 } 247 248 // ClearHostLocalIPAMAllocations removes any host-local CNI plugins from the CNI configuration. 249 func (i *InstallOptions) ClearHostLocalIPAMAllocations() error { 250 dir, err := os.ReadDir(i.HostLocalNetDir) 251 if err != nil { 252 if os.IsNotExist(err) { 253 return nil 254 } 255 return fmt.Errorf("error reading host-local CNI directory: %w", err) 256 } 257 for _, file := range dir { 258 // Skip parent directory. 259 if file.Name() == filepath.Base(i.HostLocalNetDir) { 260 continue 261 } 262 err = os.RemoveAll(filepath.Join(i.HostLocalNetDir, file.Name())) 263 if err != nil { 264 return fmt.Errorf("error removing host-local CNI plugin: %w", err) 265 } 266 } 267 return nil 268 } 269 270 // InstallPlugin installs the plugin. 271 func (i *InstallOptions) InstallPlugin(dest string) error { 272 if err := installPluginBinary(i.SourceBinary, dest); err != nil { 273 log.Printf("error installing binary to %s: %v", dest, err) 274 return err 275 } 276 return nil 277 } 278 279 // InstallNetConf installs the CNI configuration. 280 func (i *InstallOptions) InstallNetConf(path string, config string) error { 281 if err := os.WriteFile(path, []byte(config), 0644); err != nil { 282 log.Println("error writing CNI configuration:", err) 283 return err 284 } 285 return nil 286 } 287 288 // RenderNetConf renders the CNI configuration. 289 func (i *InstallOptions) RenderNetConf(apiEndpoint string, kubeconfig string) string { 290 conf := i.NetConfTemplate 291 conf = strings.Replace(conf, NodeNameReplaceStr, i.NodeName, -1) 292 conf = strings.Replace(conf, PodNamespaceReplaceStr, i.Namespace, -1) 293 conf = strings.Replace(conf, APIEndpointReplaceStr, apiEndpoint, -1) 294 conf = strings.Replace(conf, KubeconfigFilepathReplaceStr, kubeconfig, -1) 295 return conf 296 } 297 298 // InstallKubeconfig writes the kubeconfig file for the plugin. 299 func (i *InstallOptions) InstallKubeconfig(kubeconfigPath string) error { 300 kubeconfig, err := i.GetKubeconfig() 301 if err != nil { 302 return fmt.Errorf("error getting kubeconfig: %w", err) 303 } 304 if err := clientcmd.WriteToFile(kubeconfig, kubeconfigPath); err != nil { 305 log.Println("error writing kubeconfig:", err) 306 return err 307 } 308 return nil 309 } 310 311 // GetKubeconfig tries to build a kubeconfig from the current in cluster 312 // configuration. 313 func (i *InstallOptions) GetKubeconfig() (clientcmdapi.Config, error) { 314 // If our cert data is empty, convert it to the contents of the cert file. 315 cfg, err := getInstallRestConfig() 316 if err != nil { 317 return clientcmdapi.Config{}, fmt.Errorf("error getting config: %w", err) 318 } 319 return KubeconfigFromRestConfig(cfg, i.Namespace) 320 } 321 322 // KubeconfigFromRestConfig returns a kubeconfig from the given rest config. 323 // It reads in any files and encodes them as base64 in the final configuration. 324 // GetKubeconfig tries to build a kubeconfig from the current in cluster 325 // configuration. 326 func KubeconfigFromRestConfig(cfg *rest.Config, namespace string) (clientcmdapi.Config, error) { 327 if cfg.CAFile != "" { 328 caData, err := os.ReadFile(cfg.CAFile) 329 if err != nil { 330 return clientcmdapi.Config{}, fmt.Errorf("error reading certificate authority data: %w", err) 331 } 332 cfg.CAData = caData 333 } 334 // If our bearer token is a file, convert it to the contents of the file. 335 if cfg.BearerTokenFile != "" { 336 token, err := os.ReadFile(cfg.BearerTokenFile) 337 if err != nil { 338 return clientcmdapi.Config{}, fmt.Errorf("error reading bearer token: %w", err) 339 } 340 cfg.BearerToken = string(token) 341 } 342 // If our client certificate is a file, convert it to the contents of the file. 343 if cfg.CertFile != "" { 344 cert, err := os.ReadFile(cfg.CertFile) 345 if err != nil { 346 log.Println("error reading client certificate:", err) 347 return clientcmdapi.Config{}, fmt.Errorf("error reading client certificate: %w", err) 348 } 349 cfg.CertData = cert 350 } 351 // Same for any key 352 if cfg.KeyFile != "" { 353 key, err := os.ReadFile(cfg.KeyFile) 354 if err != nil { 355 log.Println("error reading client key:", err) 356 return clientcmdapi.Config{}, fmt.Errorf("error reading client key: %w", err) 357 } 358 cfg.KeyData = key 359 } 360 return clientcmdapi.Config{ 361 Kind: "Config", 362 APIVersion: "v1", 363 Clusters: map[string]*clientcmdapi.Cluster{ 364 KubeconfigContextName: { 365 Server: cfg.Host, 366 TLSServerName: cfg.ServerName, 367 InsecureSkipTLSVerify: cfg.Insecure, 368 CertificateAuthorityData: cfg.CAData, 369 }, 370 }, 371 AuthInfos: map[string]*clientcmdapi.AuthInfo{ 372 KubeconfigContextName: { 373 ClientCertificateData: cfg.CertData, 374 ClientKeyData: cfg.KeyData, 375 Token: cfg.BearerToken, 376 Impersonate: cfg.Impersonate.UserName, 377 ImpersonateGroups: cfg.Impersonate.Groups, 378 }, 379 }, 380 Contexts: map[string]*clientcmdapi.Context{ 381 KubeconfigContextName: { 382 Cluster: KubeconfigContextName, 383 AuthInfo: KubeconfigContextName, 384 Namespace: namespace, 385 }, 386 }, 387 CurrentContext: KubeconfigContextName, 388 }, nil 389 } 390 391 var setSuidBit = setSuidBitToFile 392 393 // installPluginBinary copies the binary to the destination directory. 394 func installPluginBinary(src, dest string) error { 395 f, err := os.Open(src) 396 if err != nil { 397 return fmt.Errorf("error opening binary: %w", err) 398 } 399 defer f.Close() 400 // Create the destination directory if it doesn't exist. 401 if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { 402 return fmt.Errorf("error creating destination directory: %w", err) 403 } 404 // Create the destination file. 405 out, err := os.Create(dest) 406 if err != nil { 407 return fmt.Errorf("error creating destination file: %w", err) 408 } 409 // Copy the binary to the destination file. 410 if _, err := io.Copy(out, f); err != nil { 411 return fmt.Errorf("error copying binary: %w", err) 412 } 413 err = out.Close() 414 if err != nil { 415 return fmt.Errorf("error closing destination file: %w", err) 416 } 417 // Make the destination file executable. 418 if err := os.Chmod(dest, 0755); err != nil { 419 return fmt.Errorf("error making destination file executable: %w", err) 420 } 421 return setSuidBit(dest) 422 } 423 424 func setSuidBitToFile(file string) error { 425 if runtime.GOOS == "windows" { 426 // chmod doesn't work on windows 427 log.Println("chmod doesn't work on windows, skipping setSuidBit()") 428 return nil 429 } 430 fi, err := os.Stat(file) 431 if err != nil { 432 return fmt.Errorf("failed to stat file: %s", err) 433 } 434 err = os.Chmod(file, fi.Mode()|os.FileMode(uint32(8388608))) 435 if err != nil { 436 return fmt.Errorf("failed to chmod file: %s", err) 437 } 438 return nil 439 } 440 441 // inClusterNamespacePath is the path to the namespace file in the pod. 442 // Declared as a variable for testing. 443 var inClusterNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" 444 445 // GetInClusterNamespace returns the namespace of the pod we are running in. 446 func GetInClusterNamespace() (string, error) { 447 // Load the namespace file and return its content 448 namespace, err := os.ReadFile(inClusterNamespacePath) 449 if err != nil { 450 if os.IsNotExist(err) { 451 return "", fmt.Errorf("namespace file does not exist, not running in-cluster") 452 } 453 return "", fmt.Errorf("error reading namespace file: %w", err) 454 } 455 return string(bytes.TrimSpace(namespace)), nil 456 }