github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/playground/kubeconfig.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package playground
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"time"
    27  
    28  	"k8s.io/client-go/tools/clientcmd"
    29  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    30  	"k8s.io/klog/v2"
    31  )
    32  
    33  // writeKubeConfigOptions provides a set of options for writing a KubeConfig file
    34  type writeKubeConfigOptions struct {
    35  	UpdateExisting       bool
    36  	UpdateCurrentContext bool
    37  	OverwriteExisting    bool
    38  }
    39  
    40  // kubeConfigWrite writes the kubeconfig to the specified output
    41  func kubeConfigWrite(kubeConfigStr string, output string, options writeKubeConfigOptions) error {
    42  	var err error
    43  
    44  	// convert the kubeconfig string to a kubeconfig object
    45  	kubeConfig, err := clientcmd.Load([]byte(kubeConfigStr))
    46  	if err != nil {
    47  		return fmt.Errorf("failed to load kubeconfig: %w", err)
    48  	}
    49  
    50  	// if output is not specified, use the default kubeconfig path
    51  	if output == "" {
    52  		output, err = kubeConfigGetDefaultPath()
    53  		if err != nil {
    54  			return fmt.Errorf("failed to get default kubeconfig path: %w", err)
    55  		}
    56  	}
    57  
    58  	// simply write the kubeconfig to the output path, ignoring existing contents
    59  	if options.OverwriteExisting || output == "-" {
    60  		return kubeConfigWriteToPath(kubeConfig, output)
    61  	}
    62  
    63  	var existingKubeConfig *clientcmdapi.Config
    64  	firstRun := true
    65  	for {
    66  		existingKubeConfig, err = clientcmd.LoadFromFile(output)
    67  		if err == nil {
    68  			break
    69  		}
    70  
    71  		// the output file does not exist, try to create it and try again
    72  		if os.IsNotExist(err) && firstRun {
    73  			klog.V(1).Infof("Output path '%s' does not exist, try to create it", output)
    74  
    75  			// create directory path
    76  			if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
    77  				return fmt.Errorf("failed to create output directory '%s': %w", filepath.Dir(output), err)
    78  			}
    79  
    80  			// create output file
    81  			f, err := os.Create(output)
    82  			if err != nil {
    83  				return fmt.Errorf("failed to create output file '%s': %w", output, err)
    84  			}
    85  			f.Close()
    86  
    87  			// try again
    88  			firstRun = false
    89  			continue
    90  		}
    91  		return fmt.Errorf("failed to load kubeconfig from output path '%s': %w", output, err)
    92  	}
    93  	return kubeConfigMerge(kubeConfig, existingKubeConfig, output, options)
    94  }
    95  
    96  // kubeConfigGetDefaultPath returns the path of the default kubeconfig, print errors
    97  // if the KUBECONFIG env var specifies more than one file
    98  func kubeConfigGetDefaultPath() (string, error) {
    99  	defaultKubeConfigLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
   100  	if len(defaultKubeConfigLoadingRules.GetLoadingPrecedence()) > 1 {
   101  		return "", fmt.Errorf("multiple kubeconfigs specified via KUBECONFIG env var: Please reduce to one entry, unset KUBECONFIG or explicitly choose one")
   102  	}
   103  	return defaultKubeConfigLoadingRules.GetDefaultFilename(), nil
   104  }
   105  
   106  // kubeConfigMerge merges the kubeconfig into the existing kubeconfig and writes it to the output path
   107  func kubeConfigMerge(kubeConfig, existingKubeConfig *clientcmdapi.Config, output string, options writeKubeConfigOptions) error {
   108  	klog.V(1).Infof("Merging new kubeconfig:\n%+v\n>>> into existing Kubeconfig:\n%+v", kubeConfig, existingKubeConfig)
   109  
   110  	// overwrite values in existing kubeconfig with values from new kubeconfig
   111  	for k, v := range kubeConfig.Clusters {
   112  		if _, ok := existingKubeConfig.Clusters[k]; ok && !options.UpdateExisting {
   113  			return fmt.Errorf("cluster \"%s\" already exists in target kubeconfig", k)
   114  		}
   115  		existingKubeConfig.Clusters[k] = v
   116  	}
   117  
   118  	for k, v := range kubeConfig.AuthInfos {
   119  		if _, ok := existingKubeConfig.AuthInfos[k]; ok && !options.UpdateExisting {
   120  			return fmt.Errorf("user '%s' already exists in target KubeConfig", k)
   121  		}
   122  		existingKubeConfig.AuthInfos[k] = v
   123  	}
   124  
   125  	for k, v := range kubeConfig.Contexts {
   126  		if _, ok := existingKubeConfig.Contexts[k]; ok && !options.UpdateExisting {
   127  			return fmt.Errorf("context '%s' already exists in target KubeConfig", k)
   128  		}
   129  		existingKubeConfig.Contexts[k] = v
   130  	}
   131  
   132  	// set current context if it is not set, or we want to update it
   133  	if existingKubeConfig.CurrentContext == "" || options.UpdateCurrentContext {
   134  		klog.V(1).Infof("Setting new current-context '%s'", kubeConfig.CurrentContext)
   135  		existingKubeConfig.CurrentContext = kubeConfig.CurrentContext
   136  	}
   137  
   138  	return kubeConfigAtomicWrite(existingKubeConfig, output)
   139  }
   140  
   141  // kubeConfigWriteToPath takes a kubeconfig and writes it to some path, which can be '-' for os.Stdout
   142  func kubeConfigWriteToPath(kubeconfig *clientcmdapi.Config, path string) error {
   143  	var output *os.File
   144  	defer output.Close()
   145  	var err error
   146  
   147  	if path == "-" {
   148  		output = os.Stdout
   149  	} else {
   150  		output, err = os.Create(path)
   151  		if err != nil {
   152  			return fmt.Errorf("failed to create file '%s': %w", path, err)
   153  		}
   154  		defer output.Close()
   155  	}
   156  
   157  	kubeconfigBytes, err := clientcmd.Write(*kubeconfig)
   158  	if err != nil {
   159  		return fmt.Errorf("failed to write kubeconfig: %w", err)
   160  	}
   161  
   162  	_, err = output.Write(kubeconfigBytes)
   163  	if err != nil {
   164  		return fmt.Errorf("failed to write file '%s': %w", output.Name(), err)
   165  	}
   166  
   167  	klog.V(1).Infof("Wrote kubeconfig to '%s'", output.Name())
   168  
   169  	return nil
   170  }
   171  
   172  // kubeConfigWrite writes a kubeconfig to a path atomically
   173  func kubeConfigAtomicWrite(kubeconfig *clientcmdapi.Config, path string) error {
   174  	tempPath := fmt.Sprintf("%s.kb_playground_%s", path, time.Now().Format("20060102_150405.000000"))
   175  	if err := clientcmd.WriteToFile(*kubeconfig, tempPath); err != nil {
   176  		return fmt.Errorf("failed to write merged kubeconfig to temporary file '%s': %w", tempPath, err)
   177  	}
   178  
   179  	// Move temporary file over existing KubeConfig
   180  	if err := os.Rename(tempPath, path); err != nil {
   181  		return fmt.Errorf("failed to overwrite existing KubeConfig '%s' with new kubeconfig '%s': %w", path, tempPath, err)
   182  	}
   183  
   184  	klog.V(1).Infof("Wrote kubeconfig to '%s'", path)
   185  
   186  	return nil
   187  }
   188  
   189  // kubeConfigRemove removes the specified kubeconfig from the specified cfgPath path
   190  func kubeConfigRemove(kubeConfigStr string, cfgPath string) error {
   191  	// convert the kubeconfig string to a kubeconfig object
   192  	kubeConfig, err := clientcmd.Load([]byte(kubeConfigStr))
   193  	if err != nil {
   194  		return fmt.Errorf("failed to load kubeconfig: %w", err)
   195  	}
   196  
   197  	// get the existing kubeconfig
   198  	existingKubeConfig, err := clientcmd.LoadFromFile(cfgPath)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	for k := range kubeConfig.Clusters {
   204  		delete(existingKubeConfig.Clusters, k)
   205  	}
   206  
   207  	for k := range kubeConfig.AuthInfos {
   208  		delete(existingKubeConfig.AuthInfos, k)
   209  	}
   210  
   211  	for k := range kubeConfig.Contexts {
   212  		delete(existingKubeConfig.Contexts, k)
   213  	}
   214  
   215  	return kubeConfigAtomicWrite(existingKubeConfig, cfgPath)
   216  }
   217  
   218  func kubeConfigCurrentContext(kubeConfigStr string) (string, error) {
   219  	kubeConfig, err := clientcmd.Load([]byte(kubeConfigStr))
   220  	if err != nil {
   221  		return "", fmt.Errorf("failed to load kubeconfig: %w", err)
   222  	}
   223  	return kubeConfig.CurrentContext, nil
   224  }
   225  
   226  func kubeConfigCurrentContextFromFile(kubeConfigPath string) (string, error) {
   227  	kubeConfig, err := clientcmd.LoadFromFile(kubeConfigPath)
   228  	if err != nil {
   229  		return "", fmt.Errorf("failed to load kubeconfig: %w", err)
   230  	}
   231  	return kubeConfig.CurrentContext, nil
   232  }