...

Source file src/k8s.io/client-go/tools/clientcmd/loader.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  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"reflect"
    24  	goruntime "runtime"
    25  	"strings"
    26  
    27  	"github.com/imdario/mergo"
    28  	"k8s.io/klog/v2"
    29  
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    33  	restclient "k8s.io/client-go/rest"
    34  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    35  	clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest"
    36  	"k8s.io/client-go/util/homedir"
    37  )
    38  
    39  const (
    40  	RecommendedConfigPathFlag   = "kubeconfig"
    41  	RecommendedConfigPathEnvVar = "KUBECONFIG"
    42  	RecommendedHomeDir          = ".kube"
    43  	RecommendedFileName         = "config"
    44  	RecommendedSchemaName       = "schema"
    45  )
    46  
    47  var (
    48  	RecommendedConfigDir  = filepath.Join(homedir.HomeDir(), RecommendedHomeDir)
    49  	RecommendedHomeFile   = filepath.Join(RecommendedConfigDir, RecommendedFileName)
    50  	RecommendedSchemaFile = filepath.Join(RecommendedConfigDir, RecommendedSchemaName)
    51  )
    52  
    53  // currentMigrationRules returns a map that holds the history of recommended home directories used in previous versions.
    54  // Any future changes to RecommendedHomeFile and related are expected to add a migration rule here, in order to make
    55  // sure existing config files are migrated to their new locations properly.
    56  func currentMigrationRules() map[string]string {
    57  	var oldRecommendedHomeFileName string
    58  	if goruntime.GOOS == "windows" {
    59  		oldRecommendedHomeFileName = RecommendedFileName
    60  	} else {
    61  		oldRecommendedHomeFileName = ".kubeconfig"
    62  	}
    63  	return map[string]string{
    64  		RecommendedHomeFile: filepath.Join(os.Getenv("HOME"), RecommendedHomeDir, oldRecommendedHomeFileName),
    65  	}
    66  }
    67  
    68  type ClientConfigLoader interface {
    69  	ConfigAccess
    70  	// IsDefaultConfig returns true if the returned config matches the defaults.
    71  	IsDefaultConfig(*restclient.Config) bool
    72  	// Load returns the latest config
    73  	Load() (*clientcmdapi.Config, error)
    74  }
    75  
    76  type KubeconfigGetter func() (*clientcmdapi.Config, error)
    77  
    78  type ClientConfigGetter struct {
    79  	kubeconfigGetter KubeconfigGetter
    80  }
    81  
    82  // ClientConfigGetter implements the ClientConfigLoader interface.
    83  var _ ClientConfigLoader = &ClientConfigGetter{}
    84  
    85  func (g *ClientConfigGetter) Load() (*clientcmdapi.Config, error) {
    86  	return g.kubeconfigGetter()
    87  }
    88  
    89  func (g *ClientConfigGetter) GetLoadingPrecedence() []string {
    90  	return nil
    91  }
    92  func (g *ClientConfigGetter) GetStartingConfig() (*clientcmdapi.Config, error) {
    93  	return g.kubeconfigGetter()
    94  }
    95  func (g *ClientConfigGetter) GetDefaultFilename() string {
    96  	return ""
    97  }
    98  func (g *ClientConfigGetter) IsExplicitFile() bool {
    99  	return false
   100  }
   101  func (g *ClientConfigGetter) GetExplicitFile() string {
   102  	return ""
   103  }
   104  func (g *ClientConfigGetter) IsDefaultConfig(config *restclient.Config) bool {
   105  	return false
   106  }
   107  
   108  // ClientConfigLoadingRules is an ExplicitPath and string slice of specific locations that are used for merging together a Config
   109  // Callers can put the chain together however they want, but we'd recommend:
   110  // EnvVarPathFiles if set (a list of files if set) OR the HomeDirectoryPath
   111  // ExplicitPath is special, because if a user specifically requests a certain file be used and error is reported if this file is not present
   112  type ClientConfigLoadingRules struct {
   113  	ExplicitPath string
   114  	Precedence   []string
   115  
   116  	// MigrationRules is a map of destination files to source files.  If a destination file is not present, then the source file is checked.
   117  	// If the source file is present, then it is copied to the destination file BEFORE any further loading happens.
   118  	MigrationRules map[string]string
   119  
   120  	// DoNotResolvePaths indicates whether or not to resolve paths with respect to the originating files.  This is phrased as a negative so
   121  	// that a default object that doesn't set this will usually get the behavior it wants.
   122  	DoNotResolvePaths bool
   123  
   124  	// DefaultClientConfig is an optional field indicating what rules to use to calculate a default configuration.
   125  	// This should match the overrides passed in to ClientConfig loader.
   126  	DefaultClientConfig ClientConfig
   127  
   128  	// WarnIfAllMissing indicates whether the configuration files pointed by KUBECONFIG environment variable are present or not.
   129  	// In case of missing files, it warns the user about the missing files.
   130  	WarnIfAllMissing bool
   131  
   132  	// Warner is the warning log callback to use in case of missing files.
   133  	Warner WarningHandler
   134  }
   135  
   136  // WarningHandler allows to set the logging function to use
   137  type WarningHandler func(error)
   138  
   139  func (handler WarningHandler) Warn(err error) {
   140  	if handler == nil {
   141  		klog.V(1).Info(err)
   142  	} else {
   143  		handler(err)
   144  	}
   145  }
   146  
   147  type MissingConfigError struct {
   148  	Missing []string
   149  }
   150  
   151  func (c MissingConfigError) Error() string {
   152  	return fmt.Sprintf("Config not found: %s", strings.Join(c.Missing, ", "))
   153  }
   154  
   155  // ClientConfigLoadingRules implements the ClientConfigLoader interface.
   156  var _ ClientConfigLoader = &ClientConfigLoadingRules{}
   157  
   158  // NewDefaultClientConfigLoadingRules returns a ClientConfigLoadingRules object with default fields filled in.  You are not required to
   159  // use this constructor
   160  func NewDefaultClientConfigLoadingRules() *ClientConfigLoadingRules {
   161  	chain := []string{}
   162  	warnIfAllMissing := false
   163  
   164  	envVarFiles := os.Getenv(RecommendedConfigPathEnvVar)
   165  	if len(envVarFiles) != 0 {
   166  		fileList := filepath.SplitList(envVarFiles)
   167  		// prevent the same path load multiple times
   168  		chain = append(chain, deduplicate(fileList)...)
   169  		warnIfAllMissing = true
   170  
   171  	} else {
   172  		chain = append(chain, RecommendedHomeFile)
   173  	}
   174  
   175  	return &ClientConfigLoadingRules{
   176  		Precedence:       chain,
   177  		MigrationRules:   currentMigrationRules(),
   178  		WarnIfAllMissing: warnIfAllMissing,
   179  	}
   180  }
   181  
   182  // Load starts by running the MigrationRules and then
   183  // takes the loading rules and returns a Config object based on following rules.
   184  //
   185  //	if the ExplicitPath, return the unmerged explicit file
   186  //	Otherwise, return a merged config based on the Precedence slice
   187  //
   188  // A missing ExplicitPath file produces an error. Empty filenames or other missing files are ignored.
   189  // Read errors or files with non-deserializable content produce errors.
   190  // The first file to set a particular map key wins and map key's value is never changed.
   191  // BUT, if you set a struct value that is NOT contained inside of map, the value WILL be changed.
   192  // This results in some odd looking logic to merge in one direction, merge in the other, and then merge the two.
   193  // It also means that if two files specify a "red-user", only values from the first file's red-user are used.  Even
   194  // non-conflicting entries from the second file's "red-user" are discarded.
   195  // Relative paths inside of the .kubeconfig files are resolved against the .kubeconfig file's parent folder
   196  // and only absolute file paths are returned.
   197  func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) {
   198  	if err := rules.Migrate(); err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	errlist := []error{}
   203  	missingList := []string{}
   204  
   205  	kubeConfigFiles := []string{}
   206  
   207  	// Make sure a file we were explicitly told to use exists
   208  	if len(rules.ExplicitPath) > 0 {
   209  		if _, err := os.Stat(rules.ExplicitPath); os.IsNotExist(err) {
   210  			return nil, err
   211  		}
   212  		kubeConfigFiles = append(kubeConfigFiles, rules.ExplicitPath)
   213  
   214  	} else {
   215  		kubeConfigFiles = append(kubeConfigFiles, rules.Precedence...)
   216  	}
   217  
   218  	kubeconfigs := []*clientcmdapi.Config{}
   219  	// read and cache the config files so that we only look at them once
   220  	for _, filename := range kubeConfigFiles {
   221  		if len(filename) == 0 {
   222  			// no work to do
   223  			continue
   224  		}
   225  
   226  		config, err := LoadFromFile(filename)
   227  
   228  		if os.IsNotExist(err) {
   229  			// skip missing files
   230  			// Add to the missing list to produce a warning
   231  			missingList = append(missingList, filename)
   232  			continue
   233  		}
   234  
   235  		if err != nil {
   236  			errlist = append(errlist, fmt.Errorf("error loading config file \"%s\": %v", filename, err))
   237  			continue
   238  		}
   239  
   240  		kubeconfigs = append(kubeconfigs, config)
   241  	}
   242  
   243  	if rules.WarnIfAllMissing && len(missingList) > 0 && len(kubeconfigs) == 0 {
   244  		rules.Warner.Warn(MissingConfigError{Missing: missingList})
   245  	}
   246  
   247  	// first merge all of our maps
   248  	mapConfig := clientcmdapi.NewConfig()
   249  
   250  	for _, kubeconfig := range kubeconfigs {
   251  		mergo.Merge(mapConfig, kubeconfig, mergo.WithOverride)
   252  	}
   253  
   254  	// merge all of the struct values in the reverse order so that priority is given correctly
   255  	// errors are not added to the list the second time
   256  	nonMapConfig := clientcmdapi.NewConfig()
   257  	for i := len(kubeconfigs) - 1; i >= 0; i-- {
   258  		kubeconfig := kubeconfigs[i]
   259  		mergo.Merge(nonMapConfig, kubeconfig, mergo.WithOverride)
   260  	}
   261  
   262  	// since values are overwritten, but maps values are not, we can merge the non-map config on top of the map config and
   263  	// get the values we expect.
   264  	config := clientcmdapi.NewConfig()
   265  	mergo.Merge(config, mapConfig, mergo.WithOverride)
   266  	mergo.Merge(config, nonMapConfig, mergo.WithOverride)
   267  
   268  	if rules.ResolvePaths() {
   269  		if err := ResolveLocalPaths(config); err != nil {
   270  			errlist = append(errlist, err)
   271  		}
   272  	}
   273  	return config, utilerrors.NewAggregate(errlist)
   274  }
   275  
   276  // Migrate uses the MigrationRules map.  If a destination file is not present, then the source file is checked.
   277  // If the source file is present, then it is copied to the destination file BEFORE any further loading happens.
   278  func (rules *ClientConfigLoadingRules) Migrate() error {
   279  	if rules.MigrationRules == nil {
   280  		return nil
   281  	}
   282  
   283  	for destination, source := range rules.MigrationRules {
   284  		if _, err := os.Stat(destination); err == nil {
   285  			// if the destination already exists, do nothing
   286  			continue
   287  		} else if os.IsPermission(err) {
   288  			// if we can't access the file, skip it
   289  			continue
   290  		} else if !os.IsNotExist(err) {
   291  			// if we had an error other than non-existence, fail
   292  			return err
   293  		}
   294  
   295  		if sourceInfo, err := os.Stat(source); err != nil {
   296  			if os.IsNotExist(err) || os.IsPermission(err) {
   297  				// if the source file doesn't exist or we can't access it, there's no work to do.
   298  				continue
   299  			}
   300  
   301  			// if we had an error other than non-existence, fail
   302  			return err
   303  		} else if sourceInfo.IsDir() {
   304  			return fmt.Errorf("cannot migrate %v to %v because it is a directory", source, destination)
   305  		}
   306  
   307  		data, err := os.ReadFile(source)
   308  		if err != nil {
   309  			return err
   310  		}
   311  		// destination is created with mode 0666 before umask
   312  		err = os.WriteFile(destination, data, 0666)
   313  		if err != nil {
   314  			return err
   315  		}
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  // GetLoadingPrecedence implements ConfigAccess
   322  func (rules *ClientConfigLoadingRules) GetLoadingPrecedence() []string {
   323  	if len(rules.ExplicitPath) > 0 {
   324  		return []string{rules.ExplicitPath}
   325  	}
   326  
   327  	return rules.Precedence
   328  }
   329  
   330  // GetStartingConfig implements ConfigAccess
   331  func (rules *ClientConfigLoadingRules) GetStartingConfig() (*clientcmdapi.Config, error) {
   332  	clientConfig := NewNonInteractiveDeferredLoadingClientConfig(rules, &ConfigOverrides{})
   333  	rawConfig, err := clientConfig.RawConfig()
   334  	if os.IsNotExist(err) {
   335  		return clientcmdapi.NewConfig(), nil
   336  	}
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  
   341  	return &rawConfig, nil
   342  }
   343  
   344  // GetDefaultFilename implements ConfigAccess
   345  func (rules *ClientConfigLoadingRules) GetDefaultFilename() string {
   346  	// Explicit file if we have one.
   347  	if rules.IsExplicitFile() {
   348  		return rules.GetExplicitFile()
   349  	}
   350  	// Otherwise, first existing file from precedence.
   351  	for _, filename := range rules.GetLoadingPrecedence() {
   352  		if _, err := os.Stat(filename); err == nil {
   353  			return filename
   354  		}
   355  	}
   356  	// If none exists, use the first from precedence.
   357  	if len(rules.Precedence) > 0 {
   358  		return rules.Precedence[0]
   359  	}
   360  	return ""
   361  }
   362  
   363  // IsExplicitFile implements ConfigAccess
   364  func (rules *ClientConfigLoadingRules) IsExplicitFile() bool {
   365  	return len(rules.ExplicitPath) > 0
   366  }
   367  
   368  // GetExplicitFile implements ConfigAccess
   369  func (rules *ClientConfigLoadingRules) GetExplicitFile() string {
   370  	return rules.ExplicitPath
   371  }
   372  
   373  // IsDefaultConfig returns true if the provided configuration matches the default
   374  func (rules *ClientConfigLoadingRules) IsDefaultConfig(config *restclient.Config) bool {
   375  	if rules.DefaultClientConfig == nil {
   376  		return false
   377  	}
   378  	defaultConfig, err := rules.DefaultClientConfig.ClientConfig()
   379  	if err != nil {
   380  		return false
   381  	}
   382  	return reflect.DeepEqual(config, defaultConfig)
   383  }
   384  
   385  // LoadFromFile takes a filename and deserializes the contents into Config object
   386  func LoadFromFile(filename string) (*clientcmdapi.Config, error) {
   387  	kubeconfigBytes, err := os.ReadFile(filename)
   388  	if err != nil {
   389  		return nil, err
   390  	}
   391  	config, err := Load(kubeconfigBytes)
   392  	if err != nil {
   393  		return nil, err
   394  	}
   395  	klog.V(6).Infoln("Config loaded from file: ", filename)
   396  
   397  	// set LocationOfOrigin on every Cluster, User, and Context
   398  	for key, obj := range config.AuthInfos {
   399  		obj.LocationOfOrigin = filename
   400  		config.AuthInfos[key] = obj
   401  	}
   402  	for key, obj := range config.Clusters {
   403  		obj.LocationOfOrigin = filename
   404  		config.Clusters[key] = obj
   405  	}
   406  	for key, obj := range config.Contexts {
   407  		obj.LocationOfOrigin = filename
   408  		config.Contexts[key] = obj
   409  	}
   410  
   411  	if config.AuthInfos == nil {
   412  		config.AuthInfos = map[string]*clientcmdapi.AuthInfo{}
   413  	}
   414  	if config.Clusters == nil {
   415  		config.Clusters = map[string]*clientcmdapi.Cluster{}
   416  	}
   417  	if config.Contexts == nil {
   418  		config.Contexts = map[string]*clientcmdapi.Context{}
   419  	}
   420  
   421  	return config, nil
   422  }
   423  
   424  // Load takes a byte slice and deserializes the contents into Config object.
   425  // Encapsulates deserialization without assuming the source is a file.
   426  func Load(data []byte) (*clientcmdapi.Config, error) {
   427  	config := clientcmdapi.NewConfig()
   428  	// if there's no data in a file, return the default object instead of failing (DecodeInto reject empty input)
   429  	if len(data) == 0 {
   430  		return config, nil
   431  	}
   432  	decoded, _, err := clientcmdlatest.Codec.Decode(data, &schema.GroupVersionKind{Version: clientcmdlatest.Version, Kind: "Config"}, config)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	return decoded.(*clientcmdapi.Config), nil
   437  }
   438  
   439  // WriteToFile serializes the config to yaml and writes it out to a file.  If not present, it creates the file with the mode 0600.  If it is present
   440  // it stomps the contents
   441  func WriteToFile(config clientcmdapi.Config, filename string) error {
   442  	content, err := Write(config)
   443  	if err != nil {
   444  		return err
   445  	}
   446  	dir := filepath.Dir(filename)
   447  	if _, err := os.Stat(dir); os.IsNotExist(err) {
   448  		if err = os.MkdirAll(dir, 0755); err != nil {
   449  			return err
   450  		}
   451  	}
   452  
   453  	if err := os.WriteFile(filename, content, 0600); err != nil {
   454  		return err
   455  	}
   456  	return nil
   457  }
   458  
   459  func lockFile(filename string) error {
   460  	// TODO: find a way to do this with actual file locks. Will
   461  	// probably need separate solution for windows and Linux.
   462  
   463  	// Make sure the dir exists before we try to create a lock file.
   464  	dir := filepath.Dir(filename)
   465  	if _, err := os.Stat(dir); os.IsNotExist(err) {
   466  		if err = os.MkdirAll(dir, 0755); err != nil {
   467  			return err
   468  		}
   469  	}
   470  	f, err := os.OpenFile(lockName(filename), os.O_CREATE|os.O_EXCL, 0)
   471  	if err != nil {
   472  		return err
   473  	}
   474  	f.Close()
   475  	return nil
   476  }
   477  
   478  func unlockFile(filename string) error {
   479  	return os.Remove(lockName(filename))
   480  }
   481  
   482  func lockName(filename string) string {
   483  	return filename + ".lock"
   484  }
   485  
   486  // Write serializes the config to yaml.
   487  // Encapsulates serialization without assuming the destination is a file.
   488  func Write(config clientcmdapi.Config) ([]byte, error) {
   489  	return runtime.Encode(clientcmdlatest.Codec, &config)
   490  }
   491  
   492  func (rules ClientConfigLoadingRules) ResolvePaths() bool {
   493  	return !rules.DoNotResolvePaths
   494  }
   495  
   496  // ResolveLocalPaths resolves all relative paths in the config object with respect to the stanza's LocationOfOrigin
   497  // this cannot be done directly inside of LoadFromFile because doing so there would make it impossible to load a file without
   498  // modification of its contents.
   499  func ResolveLocalPaths(config *clientcmdapi.Config) error {
   500  	for _, cluster := range config.Clusters {
   501  		if len(cluster.LocationOfOrigin) == 0 {
   502  			continue
   503  		}
   504  		base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin))
   505  		if err != nil {
   506  			return fmt.Errorf("could not determine the absolute path of config file %s: %v", cluster.LocationOfOrigin, err)
   507  		}
   508  
   509  		if err := ResolvePaths(GetClusterFileReferences(cluster), base); err != nil {
   510  			return err
   511  		}
   512  	}
   513  	for _, authInfo := range config.AuthInfos {
   514  		if len(authInfo.LocationOfOrigin) == 0 {
   515  			continue
   516  		}
   517  		base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin))
   518  		if err != nil {
   519  			return fmt.Errorf("could not determine the absolute path of config file %s: %v", authInfo.LocationOfOrigin, err)
   520  		}
   521  
   522  		if err := ResolvePaths(GetAuthInfoFileReferences(authInfo), base); err != nil {
   523  			return err
   524  		}
   525  	}
   526  
   527  	return nil
   528  }
   529  
   530  // RelativizeClusterLocalPaths first absolutizes the paths by calling ResolveLocalPaths.  This assumes that any NEW path is already
   531  // absolute, but any existing path will be resolved relative to LocationOfOrigin
   532  func RelativizeClusterLocalPaths(cluster *clientcmdapi.Cluster) error {
   533  	if len(cluster.LocationOfOrigin) == 0 {
   534  		return fmt.Errorf("no location of origin for %s", cluster.Server)
   535  	}
   536  	base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin))
   537  	if err != nil {
   538  		return fmt.Errorf("could not determine the absolute path of config file %s: %v", cluster.LocationOfOrigin, err)
   539  	}
   540  
   541  	if err := ResolvePaths(GetClusterFileReferences(cluster), base); err != nil {
   542  		return err
   543  	}
   544  	if err := RelativizePathWithNoBacksteps(GetClusterFileReferences(cluster), base); err != nil {
   545  		return err
   546  	}
   547  
   548  	return nil
   549  }
   550  
   551  // RelativizeAuthInfoLocalPaths first absolutizes the paths by calling ResolveLocalPaths.  This assumes that any NEW path is already
   552  // absolute, but any existing path will be resolved relative to LocationOfOrigin
   553  func RelativizeAuthInfoLocalPaths(authInfo *clientcmdapi.AuthInfo) error {
   554  	if len(authInfo.LocationOfOrigin) == 0 {
   555  		return fmt.Errorf("no location of origin for %v", authInfo)
   556  	}
   557  	base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin))
   558  	if err != nil {
   559  		return fmt.Errorf("could not determine the absolute path of config file %s: %v", authInfo.LocationOfOrigin, err)
   560  	}
   561  
   562  	if err := ResolvePaths(GetAuthInfoFileReferences(authInfo), base); err != nil {
   563  		return err
   564  	}
   565  	if err := RelativizePathWithNoBacksteps(GetAuthInfoFileReferences(authInfo), base); err != nil {
   566  		return err
   567  	}
   568  
   569  	return nil
   570  }
   571  
   572  func RelativizeConfigPaths(config *clientcmdapi.Config, base string) error {
   573  	return RelativizePathWithNoBacksteps(GetConfigFileReferences(config), base)
   574  }
   575  
   576  func ResolveConfigPaths(config *clientcmdapi.Config, base string) error {
   577  	return ResolvePaths(GetConfigFileReferences(config), base)
   578  }
   579  
   580  func GetConfigFileReferences(config *clientcmdapi.Config) []*string {
   581  	refs := []*string{}
   582  
   583  	for _, cluster := range config.Clusters {
   584  		refs = append(refs, GetClusterFileReferences(cluster)...)
   585  	}
   586  	for _, authInfo := range config.AuthInfos {
   587  		refs = append(refs, GetAuthInfoFileReferences(authInfo)...)
   588  	}
   589  
   590  	return refs
   591  }
   592  
   593  func GetClusterFileReferences(cluster *clientcmdapi.Cluster) []*string {
   594  	return []*string{&cluster.CertificateAuthority}
   595  }
   596  
   597  func GetAuthInfoFileReferences(authInfo *clientcmdapi.AuthInfo) []*string {
   598  	s := []*string{&authInfo.ClientCertificate, &authInfo.ClientKey, &authInfo.TokenFile}
   599  	// Only resolve exec command if it isn't PATH based.
   600  	if authInfo.Exec != nil && strings.ContainsRune(authInfo.Exec.Command, filepath.Separator) {
   601  		s = append(s, &authInfo.Exec.Command)
   602  	}
   603  	return s
   604  }
   605  
   606  // ResolvePaths updates the given refs to be absolute paths, relative to the given base directory
   607  func ResolvePaths(refs []*string, base string) error {
   608  	for _, ref := range refs {
   609  		// Don't resolve empty paths
   610  		if len(*ref) > 0 {
   611  			// Don't resolve absolute paths
   612  			if !filepath.IsAbs(*ref) {
   613  				*ref = filepath.Join(base, *ref)
   614  			}
   615  		}
   616  	}
   617  	return nil
   618  }
   619  
   620  // RelativizePathWithNoBacksteps updates the given refs to be relative paths, relative to the given base directory as long as they do not require backsteps.
   621  // Any path requiring a backstep is left as-is as long it is absolute.  Any non-absolute path that can't be relativized produces an error
   622  func RelativizePathWithNoBacksteps(refs []*string, base string) error {
   623  	for _, ref := range refs {
   624  		// Don't relativize empty paths
   625  		if len(*ref) > 0 {
   626  			rel, err := MakeRelative(*ref, base)
   627  			if err != nil {
   628  				return err
   629  			}
   630  
   631  			// if we have a backstep, don't mess with the path
   632  			if strings.HasPrefix(rel, "../") {
   633  				if filepath.IsAbs(*ref) {
   634  					continue
   635  				}
   636  
   637  				return fmt.Errorf("%v requires backsteps and is not absolute", *ref)
   638  			}
   639  
   640  			*ref = rel
   641  		}
   642  	}
   643  	return nil
   644  }
   645  
   646  func MakeRelative(path, base string) (string, error) {
   647  	if len(path) > 0 {
   648  		rel, err := filepath.Rel(base, path)
   649  		if err != nil {
   650  			return path, err
   651  		}
   652  		return rel, nil
   653  	}
   654  	return path, nil
   655  }
   656  
   657  // deduplicate removes any duplicated values and returns a new slice, keeping the order unchanged
   658  func deduplicate(s []string) []string {
   659  	encountered := map[string]bool{}
   660  	ret := make([]string, 0)
   661  	for i := range s {
   662  		if encountered[s[i]] {
   663  			continue
   664  		}
   665  		encountered[s[i]] = true
   666  		ret = append(ret, s[i])
   667  	}
   668  	return ret
   669  }
   670  

View as plain text