k8s.io/client-go@v0.31.1/tools/clientcmd/config.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     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 clientcmd
    18  
    19  import (
    20  	"errors"
    21  	"os"
    22  	"path/filepath"
    23  	"reflect"
    24  	"sort"
    25  
    26  	"k8s.io/klog/v2"
    27  
    28  	restclient "k8s.io/client-go/rest"
    29  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    30  )
    31  
    32  // ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files
    33  type ConfigAccess interface {
    34  	// GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config
    35  	GetLoadingPrecedence() []string
    36  	// GetStartingConfig returns the config that subcommands should being operating against.  It may or may not be merged depending on loading rules
    37  	GetStartingConfig() (*clientcmdapi.Config, error)
    38  	// GetDefaultFilename returns the name of the file you should write into (create if necessary), if you're trying to create a new stanza as opposed to updating an existing one.
    39  	GetDefaultFilename() string
    40  	// IsExplicitFile indicates whether or not this command is interested in exactly one file.  This implementation only ever does that  via a flag, but implementations that handle local, global, and flags may have more
    41  	IsExplicitFile() bool
    42  	// GetExplicitFile returns the particular file this command is operating against.  This implementation only ever has one, but implementations that handle local, global, and flags may have more
    43  	GetExplicitFile() string
    44  }
    45  
    46  type PathOptions struct {
    47  	// GlobalFile is the full path to the file to load as the global (final) option
    48  	GlobalFile string
    49  	// EnvVar is the env var name that points to the list of kubeconfig files to load
    50  	EnvVar string
    51  	// ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file
    52  	ExplicitFileFlag string
    53  
    54  	// GlobalFileSubpath is an optional value used for displaying help
    55  	GlobalFileSubpath string
    56  
    57  	LoadingRules *ClientConfigLoadingRules
    58  }
    59  
    60  var (
    61  	// UseModifyConfigLock ensures that access to kubeconfig file using ModifyConfig method
    62  	// is being guarded by a lock file.
    63  	// This variable is intentionaly made public so other consumers of this library
    64  	// can modify its default behavior, but be caution when disabling it since
    65  	// this will make your code not threadsafe.
    66  	UseModifyConfigLock = true
    67  )
    68  
    69  func (o *PathOptions) GetEnvVarFiles() []string {
    70  	if len(o.EnvVar) == 0 {
    71  		return []string{}
    72  	}
    73  
    74  	envVarValue := os.Getenv(o.EnvVar)
    75  	if len(envVarValue) == 0 {
    76  		return []string{}
    77  	}
    78  
    79  	fileList := filepath.SplitList(envVarValue)
    80  	// prevent the same path load multiple times
    81  	return deduplicate(fileList)
    82  }
    83  
    84  func (o *PathOptions) GetLoadingPrecedence() []string {
    85  	if o.IsExplicitFile() {
    86  		return []string{o.GetExplicitFile()}
    87  	}
    88  
    89  	if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
    90  		return envVarFiles
    91  	}
    92  	return []string{o.GlobalFile}
    93  }
    94  
    95  func (o *PathOptions) GetStartingConfig() (*clientcmdapi.Config, error) {
    96  	// don't mutate the original
    97  	loadingRules := *o.LoadingRules
    98  	loadingRules.Precedence = o.GetLoadingPrecedence()
    99  
   100  	clientConfig := NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &ConfigOverrides{})
   101  	rawConfig, err := clientConfig.RawConfig()
   102  	if os.IsNotExist(err) {
   103  		return clientcmdapi.NewConfig(), nil
   104  	}
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	return &rawConfig, nil
   110  }
   111  
   112  func (o *PathOptions) GetDefaultFilename() string {
   113  	if o.IsExplicitFile() {
   114  		return o.GetExplicitFile()
   115  	}
   116  
   117  	if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
   118  		if len(envVarFiles) == 1 {
   119  			return envVarFiles[0]
   120  		}
   121  
   122  		// if any of the envvar files already exists, return it
   123  		for _, envVarFile := range envVarFiles {
   124  			if _, err := os.Stat(envVarFile); err == nil {
   125  				return envVarFile
   126  			}
   127  		}
   128  
   129  		// otherwise, return the last one in the list
   130  		return envVarFiles[len(envVarFiles)-1]
   131  	}
   132  
   133  	return o.GlobalFile
   134  }
   135  
   136  func (o *PathOptions) IsExplicitFile() bool {
   137  	return len(o.LoadingRules.ExplicitPath) > 0
   138  }
   139  
   140  func (o *PathOptions) GetExplicitFile() string {
   141  	return o.LoadingRules.ExplicitPath
   142  }
   143  
   144  func NewDefaultPathOptions() *PathOptions {
   145  	ret := &PathOptions{
   146  		GlobalFile:       RecommendedHomeFile,
   147  		EnvVar:           RecommendedConfigPathEnvVar,
   148  		ExplicitFileFlag: RecommendedConfigPathFlag,
   149  
   150  		GlobalFileSubpath: filepath.Join(RecommendedHomeDir, RecommendedFileName),
   151  
   152  		LoadingRules: NewDefaultClientConfigLoadingRules(),
   153  	}
   154  	ret.LoadingRules.DoNotResolvePaths = true
   155  
   156  	return ret
   157  }
   158  
   159  // ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or
   160  // uses the default destination file to write the results into.  This results in multiple file reads, but it's very easy to follow.
   161  // Preferences and CurrentContext should always be set in the default destination file.  Since we can't distinguish between empty and missing values
   162  // (no nil strings), we're forced have separate handling for them.  In the kubeconfig cases, newConfig should have at most one difference,
   163  // that means that this code will only write into a single file.  If you want to relativizePaths, you must provide a fully qualified path in any
   164  // modified element.
   165  func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error {
   166  	if UseModifyConfigLock {
   167  		possibleSources := configAccess.GetLoadingPrecedence()
   168  		// sort the possible kubeconfig files so we always "lock" in the same order
   169  		// to avoid deadlock (note: this can fail w/ symlinks, but... come on).
   170  		sort.Strings(possibleSources)
   171  		for _, filename := range possibleSources {
   172  			if err := lockFile(filename); err != nil {
   173  				return err
   174  			}
   175  			defer unlockFile(filename)
   176  		}
   177  	}
   178  
   179  	startingConfig, err := configAccess.GetStartingConfig()
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	// We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file.
   185  	// Special case the test for current context and preferences since those always write to the default file.
   186  	if reflect.DeepEqual(*startingConfig, newConfig) {
   187  		// nothing to do
   188  		return nil
   189  	}
   190  
   191  	if startingConfig.CurrentContext != newConfig.CurrentContext {
   192  		if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil {
   193  			return err
   194  		}
   195  	}
   196  
   197  	if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) {
   198  		if err := writePreferences(configAccess, newConfig.Preferences); err != nil {
   199  			return err
   200  		}
   201  	}
   202  
   203  	// Search every cluster, authInfo, and context.  First from new to old for differences, then from old to new for deletions
   204  	for key, cluster := range newConfig.Clusters {
   205  		startingCluster, exists := startingConfig.Clusters[key]
   206  		if !reflect.DeepEqual(cluster, startingCluster) || !exists {
   207  			destinationFile := cluster.LocationOfOrigin
   208  			if len(destinationFile) == 0 {
   209  				destinationFile = configAccess.GetDefaultFilename()
   210  			}
   211  
   212  			configToWrite, err := getConfigFromFile(destinationFile)
   213  			if err != nil {
   214  				return err
   215  			}
   216  			t := *cluster
   217  
   218  			configToWrite.Clusters[key] = &t
   219  			configToWrite.Clusters[key].LocationOfOrigin = destinationFile
   220  			if relativizePaths {
   221  				if err := RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil {
   222  					return err
   223  				}
   224  			}
   225  
   226  			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
   227  				return err
   228  			}
   229  		}
   230  	}
   231  
   232  	// seenConfigs stores a map of config source filenames to computed config objects
   233  	seenConfigs := map[string]*clientcmdapi.Config{}
   234  
   235  	for key, context := range newConfig.Contexts {
   236  		startingContext, exists := startingConfig.Contexts[key]
   237  		if !reflect.DeepEqual(context, startingContext) || !exists {
   238  			destinationFile := context.LocationOfOrigin
   239  			if len(destinationFile) == 0 {
   240  				destinationFile = configAccess.GetDefaultFilename()
   241  			}
   242  
   243  			// we only obtain a fresh config object from its source file
   244  			// if we have not seen it already - this prevents us from
   245  			// reading and writing to the same number of files repeatedly
   246  			// when multiple / all contexts share the same destination file.
   247  			configToWrite, seen := seenConfigs[destinationFile]
   248  			if !seen {
   249  				var err error
   250  				configToWrite, err = getConfigFromFile(destinationFile)
   251  				if err != nil {
   252  					return err
   253  				}
   254  				seenConfigs[destinationFile] = configToWrite
   255  			}
   256  
   257  			configToWrite.Contexts[key] = context
   258  		}
   259  	}
   260  
   261  	// actually persist config object changes
   262  	for destinationFile, configToWrite := range seenConfigs {
   263  		if err := WriteToFile(*configToWrite, destinationFile); err != nil {
   264  			return err
   265  		}
   266  	}
   267  
   268  	for key, authInfo := range newConfig.AuthInfos {
   269  		startingAuthInfo, exists := startingConfig.AuthInfos[key]
   270  		if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists {
   271  			destinationFile := authInfo.LocationOfOrigin
   272  			if len(destinationFile) == 0 {
   273  				destinationFile = configAccess.GetDefaultFilename()
   274  			}
   275  
   276  			configToWrite, err := getConfigFromFile(destinationFile)
   277  			if err != nil {
   278  				return err
   279  			}
   280  			t := *authInfo
   281  			configToWrite.AuthInfos[key] = &t
   282  			configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile
   283  			if relativizePaths {
   284  				if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil {
   285  					return err
   286  				}
   287  			}
   288  
   289  			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
   290  				return err
   291  			}
   292  		}
   293  	}
   294  
   295  	for key, cluster := range startingConfig.Clusters {
   296  		if _, exists := newConfig.Clusters[key]; !exists {
   297  			destinationFile := cluster.LocationOfOrigin
   298  			if len(destinationFile) == 0 {
   299  				destinationFile = configAccess.GetDefaultFilename()
   300  			}
   301  
   302  			configToWrite, err := getConfigFromFile(destinationFile)
   303  			if err != nil {
   304  				return err
   305  			}
   306  			delete(configToWrite.Clusters, key)
   307  
   308  			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
   309  				return err
   310  			}
   311  		}
   312  	}
   313  
   314  	for key, context := range startingConfig.Contexts {
   315  		if _, exists := newConfig.Contexts[key]; !exists {
   316  			destinationFile := context.LocationOfOrigin
   317  			if len(destinationFile) == 0 {
   318  				destinationFile = configAccess.GetDefaultFilename()
   319  			}
   320  
   321  			configToWrite, err := getConfigFromFile(destinationFile)
   322  			if err != nil {
   323  				return err
   324  			}
   325  			delete(configToWrite.Contexts, key)
   326  
   327  			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
   328  				return err
   329  			}
   330  		}
   331  	}
   332  
   333  	for key, authInfo := range startingConfig.AuthInfos {
   334  		if _, exists := newConfig.AuthInfos[key]; !exists {
   335  			destinationFile := authInfo.LocationOfOrigin
   336  			if len(destinationFile) == 0 {
   337  				destinationFile = configAccess.GetDefaultFilename()
   338  			}
   339  
   340  			configToWrite, err := getConfigFromFile(destinationFile)
   341  			if err != nil {
   342  				return err
   343  			}
   344  			delete(configToWrite.AuthInfos, key)
   345  
   346  			if err := WriteToFile(*configToWrite, destinationFile); err != nil {
   347  				return err
   348  			}
   349  		}
   350  	}
   351  
   352  	return nil
   353  }
   354  
   355  func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister {
   356  	return &persister{configAccess, user}
   357  }
   358  
   359  type persister struct {
   360  	configAccess ConfigAccess
   361  	user         string
   362  }
   363  
   364  func (p *persister) Persist(config map[string]string) error {
   365  	newConfig, err := p.configAccess.GetStartingConfig()
   366  	if err != nil {
   367  		return err
   368  	}
   369  	authInfo, ok := newConfig.AuthInfos[p.user]
   370  	if ok && authInfo.AuthProvider != nil {
   371  		authInfo.AuthProvider.Config = config
   372  		return ModifyConfig(p.configAccess, *newConfig, false)
   373  	}
   374  	return nil
   375  }
   376  
   377  // writeCurrentContext takes three possible paths.
   378  // If newCurrentContext is the same as the startingConfig's current context, then we exit.
   379  // If newCurrentContext has a value, then that value is written into the default destination file.
   380  // If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file
   381  func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error {
   382  	if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
   383  		return err
   384  	} else if startingConfig.CurrentContext == newCurrentContext {
   385  		return nil
   386  	}
   387  
   388  	if configAccess.IsExplicitFile() {
   389  		file := configAccess.GetExplicitFile()
   390  		currConfig, err := getConfigFromFile(file)
   391  		if err != nil {
   392  			return err
   393  		}
   394  		currConfig.CurrentContext = newCurrentContext
   395  		if err := WriteToFile(*currConfig, file); err != nil {
   396  			return err
   397  		}
   398  
   399  		return nil
   400  	}
   401  
   402  	if len(newCurrentContext) > 0 {
   403  		destinationFile := configAccess.GetDefaultFilename()
   404  		config, err := getConfigFromFile(destinationFile)
   405  		if err != nil {
   406  			return err
   407  		}
   408  		config.CurrentContext = newCurrentContext
   409  
   410  		if err := WriteToFile(*config, destinationFile); err != nil {
   411  			return err
   412  		}
   413  
   414  		return nil
   415  	}
   416  
   417  	// we're supposed to be clearing the current context.  We need to find the first spot in the chain that is setting it and clear it
   418  	for _, file := range configAccess.GetLoadingPrecedence() {
   419  		if _, err := os.Stat(file); err == nil {
   420  			currConfig, err := getConfigFromFile(file)
   421  			if err != nil {
   422  				return err
   423  			}
   424  
   425  			if len(currConfig.CurrentContext) > 0 {
   426  				currConfig.CurrentContext = newCurrentContext
   427  				if err := WriteToFile(*currConfig, file); err != nil {
   428  					return err
   429  				}
   430  
   431  				return nil
   432  			}
   433  		}
   434  	}
   435  
   436  	return errors.New("no config found to write context")
   437  }
   438  
   439  func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error {
   440  	if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
   441  		return err
   442  	} else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) {
   443  		return nil
   444  	}
   445  
   446  	if configAccess.IsExplicitFile() {
   447  		file := configAccess.GetExplicitFile()
   448  		currConfig, err := getConfigFromFile(file)
   449  		if err != nil {
   450  			return err
   451  		}
   452  		currConfig.Preferences = newPrefs
   453  		if err := WriteToFile(*currConfig, file); err != nil {
   454  			return err
   455  		}
   456  
   457  		return nil
   458  	}
   459  
   460  	for _, file := range configAccess.GetLoadingPrecedence() {
   461  		currConfig, err := getConfigFromFile(file)
   462  		if err != nil {
   463  			return err
   464  		}
   465  
   466  		if !reflect.DeepEqual(currConfig.Preferences, newPrefs) {
   467  			currConfig.Preferences = newPrefs
   468  			if err := WriteToFile(*currConfig, file); err != nil {
   469  				return err
   470  			}
   471  
   472  			return nil
   473  		}
   474  	}
   475  
   476  	return errors.New("no config found to write preferences")
   477  }
   478  
   479  // getConfigFromFile tries to read a kubeconfig file and if it can't, returns an error.  One exception, missing files result in empty configs, not an error.
   480  func getConfigFromFile(filename string) (*clientcmdapi.Config, error) {
   481  	config, err := LoadFromFile(filename)
   482  	if err != nil && !os.IsNotExist(err) {
   483  		return nil, err
   484  	}
   485  	if config == nil {
   486  		config = clientcmdapi.NewConfig()
   487  	}
   488  	return config, nil
   489  }
   490  
   491  // GetConfigFromFileOrDie tries to read a kubeconfig file and if it can't, it calls exit.  One exception, missing files result in empty configs, not an exit
   492  func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config {
   493  	config, err := getConfigFromFile(filename)
   494  	if err != nil {
   495  		klog.FatalDepth(1, err)
   496  	}
   497  
   498  	return config
   499  }