...

Source file src/k8s.io/client-go/tools/clientcmd/config.go

Documentation: k8s.io/client-go/tools/clientcmd

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

View as plain text