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