2017-11-20 09:30:52 -05:00
/ *
Copyright 2014 The Kubernetes Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package clientcmd
import (
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
goruntime "runtime"
"strings"
"github.com/imdario/mergo"
2019-03-25 08:03:02 -04:00
"k8s.io/klog"
2017-11-20 09:30:52 -05:00
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
restclient "k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest"
"k8s.io/client-go/util/homedir"
)
const (
RecommendedConfigPathFlag = "kubeconfig"
RecommendedConfigPathEnvVar = "KUBECONFIG"
RecommendedHomeDir = ".kube"
RecommendedFileName = "config"
RecommendedSchemaName = "schema"
)
var (
RecommendedConfigDir = path . Join ( homedir . HomeDir ( ) , RecommendedHomeDir )
RecommendedHomeFile = path . Join ( RecommendedConfigDir , RecommendedFileName )
RecommendedSchemaFile = path . Join ( RecommendedConfigDir , RecommendedSchemaName )
)
// currentMigrationRules returns a map that holds the history of recommended home directories used in previous versions.
// Any future changes to RecommendedHomeFile and related are expected to add a migration rule here, in order to make
// sure existing config files are migrated to their new locations properly.
func currentMigrationRules ( ) map [ string ] string {
oldRecommendedHomeFile := path . Join ( os . Getenv ( "HOME" ) , "/.kube/.kubeconfig" )
oldRecommendedWindowsHomeFile := path . Join ( os . Getenv ( "HOME" ) , RecommendedHomeDir , RecommendedFileName )
migrationRules := map [ string ] string { }
migrationRules [ RecommendedHomeFile ] = oldRecommendedHomeFile
if goruntime . GOOS == "windows" {
migrationRules [ RecommendedHomeFile ] = oldRecommendedWindowsHomeFile
}
return migrationRules
}
type ClientConfigLoader interface {
ConfigAccess
// IsDefaultConfig returns true if the returned config matches the defaults.
IsDefaultConfig ( * restclient . Config ) bool
// Load returns the latest config
Load ( ) ( * clientcmdapi . Config , error )
}
type KubeconfigGetter func ( ) ( * clientcmdapi . Config , error )
type ClientConfigGetter struct {
kubeconfigGetter KubeconfigGetter
}
// ClientConfigGetter implements the ClientConfigLoader interface.
var _ ClientConfigLoader = & ClientConfigGetter { }
func ( g * ClientConfigGetter ) Load ( ) ( * clientcmdapi . Config , error ) {
return g . kubeconfigGetter ( )
}
func ( g * ClientConfigGetter ) GetLoadingPrecedence ( ) [ ] string {
return nil
}
func ( g * ClientConfigGetter ) GetStartingConfig ( ) ( * clientcmdapi . Config , error ) {
return g . kubeconfigGetter ( )
}
func ( g * ClientConfigGetter ) GetDefaultFilename ( ) string {
return ""
}
func ( g * ClientConfigGetter ) IsExplicitFile ( ) bool {
return false
}
func ( g * ClientConfigGetter ) GetExplicitFile ( ) string {
return ""
}
func ( g * ClientConfigGetter ) IsDefaultConfig ( config * restclient . Config ) bool {
return false
}
// ClientConfigLoadingRules is an ExplicitPath and string slice of specific locations that are used for merging together a Config
// Callers can put the chain together however they want, but we'd recommend:
// EnvVarPathFiles if set (a list of files if set) OR the HomeDirectoryPath
2018-07-25 04:17:02 -04:00
// ExplicitPath is special, because if a user specifically requests a certain file be used and error is reported if this file is not present
2017-11-20 09:30:52 -05:00
type ClientConfigLoadingRules struct {
ExplicitPath string
Precedence [ ] string
// MigrationRules is a map of destination files to source files. If a destination file is not present, then the source file is checked.
// If the source file is present, then it is copied to the destination file BEFORE any further loading happens.
MigrationRules map [ string ] string
// DoNotResolvePaths indicates whether or not to resolve paths with respect to the originating files. This is phrased as a negative so
// that a default object that doesn't set this will usually get the behavior it wants.
DoNotResolvePaths bool
// DefaultClientConfig is an optional field indicating what rules to use to calculate a default configuration.
// This should match the overrides passed in to ClientConfig loader.
DefaultClientConfig ClientConfig
2019-10-15 10:01:09 -04:00
// WarnIfAllMissing indicates whether the configuration files pointed by KUBECONFIG environment variable are present or not.
// In case of missing files, it warns the user about the missing files.
WarnIfAllMissing bool
2017-11-20 09:30:52 -05:00
}
// ClientConfigLoadingRules implements the ClientConfigLoader interface.
var _ ClientConfigLoader = & ClientConfigLoadingRules { }
// NewDefaultClientConfigLoadingRules returns a ClientConfigLoadingRules object with default fields filled in. You are not required to
// use this constructor
func NewDefaultClientConfigLoadingRules ( ) * ClientConfigLoadingRules {
chain := [ ] string { }
2019-10-15 10:01:09 -04:00
warnIfAllMissing := false
2017-11-20 09:30:52 -05:00
envVarFiles := os . Getenv ( RecommendedConfigPathEnvVar )
if len ( envVarFiles ) != 0 {
2018-07-25 04:17:02 -04:00
fileList := filepath . SplitList ( envVarFiles )
// prevent the same path load multiple times
chain = append ( chain , deduplicate ( fileList ) ... )
2019-10-15 10:01:09 -04:00
warnIfAllMissing = true
2017-11-20 09:30:52 -05:00
} else {
chain = append ( chain , RecommendedHomeFile )
}
return & ClientConfigLoadingRules {
2019-10-15 10:01:09 -04:00
Precedence : chain ,
MigrationRules : currentMigrationRules ( ) ,
WarnIfAllMissing : warnIfAllMissing ,
2017-11-20 09:30:52 -05:00
}
}
// Load starts by running the MigrationRules and then
// takes the loading rules and returns a Config object based on following rules.
// if the ExplicitPath, return the unmerged explicit file
// Otherwise, return a merged config based on the Precedence slice
// A missing ExplicitPath file produces an error. Empty filenames or other missing files are ignored.
// Read errors or files with non-deserializable content produce errors.
// The first file to set a particular map key wins and map key's value is never changed.
// BUT, if you set a struct value that is NOT contained inside of map, the value WILL be changed.
// This results in some odd looking logic to merge in one direction, merge in the other, and then merge the two.
// It also means that if two files specify a "red-user", only values from the first file's red-user are used. Even
// non-conflicting entries from the second file's "red-user" are discarded.
// Relative paths inside of the .kubeconfig files are resolved against the .kubeconfig file's parent folder
// and only absolute file paths are returned.
func ( rules * ClientConfigLoadingRules ) Load ( ) ( * clientcmdapi . Config , error ) {
if err := rules . Migrate ( ) ; err != nil {
return nil , err
}
errlist := [ ] error { }
2019-10-15 10:01:09 -04:00
missingList := [ ] string { }
2017-11-20 09:30:52 -05:00
kubeConfigFiles := [ ] string { }
// Make sure a file we were explicitly told to use exists
if len ( rules . ExplicitPath ) > 0 {
if _ , err := os . Stat ( rules . ExplicitPath ) ; os . IsNotExist ( err ) {
return nil , err
}
kubeConfigFiles = append ( kubeConfigFiles , rules . ExplicitPath )
} else {
kubeConfigFiles = append ( kubeConfigFiles , rules . Precedence ... )
}
kubeconfigs := [ ] * clientcmdapi . Config { }
// read and cache the config files so that we only look at them once
for _ , filename := range kubeConfigFiles {
if len ( filename ) == 0 {
// no work to do
continue
}
config , err := LoadFromFile ( filename )
2019-10-15 10:01:09 -04:00
2017-11-20 09:30:52 -05:00
if os . IsNotExist ( err ) {
// skip missing files
2019-10-15 10:01:09 -04:00
// Add to the missing list to produce a warning
missingList = append ( missingList , filename )
2017-11-20 09:30:52 -05:00
continue
}
2019-10-15 10:01:09 -04:00
2017-11-20 09:30:52 -05:00
if err != nil {
2019-10-15 10:01:09 -04:00
errlist = append ( errlist , fmt . Errorf ( "error loading config file \"%s\": %v" , filename , err ) )
2017-11-20 09:30:52 -05:00
continue
}
kubeconfigs = append ( kubeconfigs , config )
}
2019-10-15 10:01:09 -04:00
if rules . WarnIfAllMissing && len ( missingList ) > 0 && len ( kubeconfigs ) == 0 {
klog . Warningf ( "Config not found: %s" , strings . Join ( missingList , ", " ) )
}
2017-11-20 09:30:52 -05:00
// first merge all of our maps
mapConfig := clientcmdapi . NewConfig ( )
for _ , kubeconfig := range kubeconfigs {
2019-03-25 08:03:02 -04:00
mergo . MergeWithOverwrite ( mapConfig , kubeconfig )
2017-11-20 09:30:52 -05:00
}
// merge all of the struct values in the reverse order so that priority is given correctly
// errors are not added to the list the second time
nonMapConfig := clientcmdapi . NewConfig ( )
for i := len ( kubeconfigs ) - 1 ; i >= 0 ; i -- {
kubeconfig := kubeconfigs [ i ]
2019-03-25 08:03:02 -04:00
mergo . MergeWithOverwrite ( nonMapConfig , kubeconfig )
2017-11-20 09:30:52 -05:00
}
// since values are overwritten, but maps values are not, we can merge the non-map config on top of the map config and
// get the values we expect.
config := clientcmdapi . NewConfig ( )
2019-03-25 08:03:02 -04:00
mergo . MergeWithOverwrite ( config , mapConfig )
mergo . MergeWithOverwrite ( config , nonMapConfig )
2017-11-20 09:30:52 -05:00
if rules . ResolvePaths ( ) {
if err := ResolveLocalPaths ( config ) ; err != nil {
errlist = append ( errlist , err )
}
}
return config , utilerrors . NewAggregate ( errlist )
}
// Migrate uses the MigrationRules map. If a destination file is not present, then the source file is checked.
// If the source file is present, then it is copied to the destination file BEFORE any further loading happens.
func ( rules * ClientConfigLoadingRules ) Migrate ( ) error {
if rules . MigrationRules == nil {
return nil
}
for destination , source := range rules . MigrationRules {
if _ , err := os . Stat ( destination ) ; err == nil {
// if the destination already exists, do nothing
continue
} else if os . IsPermission ( err ) {
// if we can't access the file, skip it
continue
} else if ! os . IsNotExist ( err ) {
// if we had an error other than non-existence, fail
return err
}
if sourceInfo , err := os . Stat ( source ) ; err != nil {
if os . IsNotExist ( err ) || os . IsPermission ( err ) {
// if the source file doesn't exist or we can't access it, there's no work to do.
continue
}
// if we had an error other than non-existence, fail
return err
} else if sourceInfo . IsDir ( ) {
return fmt . Errorf ( "cannot migrate %v to %v because it is a directory" , source , destination )
}
in , err := os . Open ( source )
if err != nil {
return err
}
defer in . Close ( )
out , err := os . Create ( destination )
if err != nil {
return err
}
defer out . Close ( )
if _ , err = io . Copy ( out , in ) ; err != nil {
return err
}
}
return nil
}
// GetLoadingPrecedence implements ConfigAccess
func ( rules * ClientConfigLoadingRules ) GetLoadingPrecedence ( ) [ ] string {
return rules . Precedence
}
// GetStartingConfig implements ConfigAccess
func ( rules * ClientConfigLoadingRules ) GetStartingConfig ( ) ( * clientcmdapi . Config , error ) {
clientConfig := NewNonInteractiveDeferredLoadingClientConfig ( rules , & ConfigOverrides { } )
rawConfig , err := clientConfig . RawConfig ( )
if os . IsNotExist ( err ) {
return clientcmdapi . NewConfig ( ) , nil
}
if err != nil {
return nil , err
}
return & rawConfig , nil
}
// GetDefaultFilename implements ConfigAccess
func ( rules * ClientConfigLoadingRules ) GetDefaultFilename ( ) string {
// Explicit file if we have one.
if rules . IsExplicitFile ( ) {
return rules . GetExplicitFile ( )
}
// Otherwise, first existing file from precedence.
for _ , filename := range rules . GetLoadingPrecedence ( ) {
if _ , err := os . Stat ( filename ) ; err == nil {
return filename
}
}
// If none exists, use the first from precedence.
if len ( rules . Precedence ) > 0 {
return rules . Precedence [ 0 ]
}
return ""
}
// IsExplicitFile implements ConfigAccess
func ( rules * ClientConfigLoadingRules ) IsExplicitFile ( ) bool {
return len ( rules . ExplicitPath ) > 0
}
// GetExplicitFile implements ConfigAccess
func ( rules * ClientConfigLoadingRules ) GetExplicitFile ( ) string {
return rules . ExplicitPath
}
// IsDefaultConfig returns true if the provided configuration matches the default
func ( rules * ClientConfigLoadingRules ) IsDefaultConfig ( config * restclient . Config ) bool {
if rules . DefaultClientConfig == nil {
return false
}
defaultConfig , err := rules . DefaultClientConfig . ClientConfig ( )
if err != nil {
return false
}
return reflect . DeepEqual ( config , defaultConfig )
}
// LoadFromFile takes a filename and deserializes the contents into Config object
func LoadFromFile ( filename string ) ( * clientcmdapi . Config , error ) {
kubeconfigBytes , err := ioutil . ReadFile ( filename )
if err != nil {
return nil , err
}
config , err := Load ( kubeconfigBytes )
if err != nil {
return nil , err
}
2019-10-15 10:01:09 -04:00
klog . V ( 6 ) . Infoln ( "Config loaded from file: " , filename )
2017-11-20 09:30:52 -05:00
// set LocationOfOrigin on every Cluster, User, and Context
for key , obj := range config . AuthInfos {
obj . LocationOfOrigin = filename
config . AuthInfos [ key ] = obj
}
for key , obj := range config . Clusters {
obj . LocationOfOrigin = filename
config . Clusters [ key ] = obj
}
for key , obj := range config . Contexts {
obj . LocationOfOrigin = filename
config . Contexts [ key ] = obj
}
if config . AuthInfos == nil {
config . AuthInfos = map [ string ] * clientcmdapi . AuthInfo { }
}
if config . Clusters == nil {
config . Clusters = map [ string ] * clientcmdapi . Cluster { }
}
if config . Contexts == nil {
config . Contexts = map [ string ] * clientcmdapi . Context { }
}
return config , nil
}
// Load takes a byte slice and deserializes the contents into Config object.
// Encapsulates deserialization without assuming the source is a file.
func Load ( data [ ] byte ) ( * clientcmdapi . Config , error ) {
config := clientcmdapi . NewConfig ( )
// if there's no data in a file, return the default object instead of failing (DecodeInto reject empty input)
if len ( data ) == 0 {
return config , nil
}
decoded , _ , err := clientcmdlatest . Codec . Decode ( data , & schema . GroupVersionKind { Version : clientcmdlatest . Version , Kind : "Config" } , config )
if err != nil {
return nil , err
}
return decoded . ( * clientcmdapi . Config ) , nil
}
// 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
// it stomps the contents
func WriteToFile ( config clientcmdapi . Config , filename string ) error {
content , err := Write ( config )
if err != nil {
return err
}
dir := filepath . Dir ( filename )
if _ , err := os . Stat ( dir ) ; os . IsNotExist ( err ) {
if err = os . MkdirAll ( dir , 0755 ) ; err != nil {
return err
}
}
if err := ioutil . WriteFile ( filename , content , 0600 ) ; err != nil {
return err
}
return nil
}
func lockFile ( filename string ) error {
// TODO: find a way to do this with actual file locks. Will
2018-07-25 04:17:02 -04:00
// probably need separate solution for windows and Linux.
2017-11-20 09:30:52 -05:00
// Make sure the dir exists before we try to create a lock file.
dir := filepath . Dir ( filename )
if _ , err := os . Stat ( dir ) ; os . IsNotExist ( err ) {
if err = os . MkdirAll ( dir , 0755 ) ; err != nil {
return err
}
}
f , err := os . OpenFile ( lockName ( filename ) , os . O_CREATE | os . O_EXCL , 0 )
if err != nil {
return err
}
f . Close ( )
return nil
}
func unlockFile ( filename string ) error {
return os . Remove ( lockName ( filename ) )
}
func lockName ( filename string ) string {
return filename + ".lock"
}
// Write serializes the config to yaml.
// Encapsulates serialization without assuming the destination is a file.
func Write ( config clientcmdapi . Config ) ( [ ] byte , error ) {
return runtime . Encode ( clientcmdlatest . Codec , & config )
}
func ( rules ClientConfigLoadingRules ) ResolvePaths ( ) bool {
return ! rules . DoNotResolvePaths
}
// ResolveLocalPaths resolves all relative paths in the config object with respect to the stanza's LocationOfOrigin
// this cannot be done directly inside of LoadFromFile because doing so there would make it impossible to load a file without
// modification of its contents.
func ResolveLocalPaths ( config * clientcmdapi . Config ) error {
for _ , cluster := range config . Clusters {
if len ( cluster . LocationOfOrigin ) == 0 {
continue
}
base , err := filepath . Abs ( filepath . Dir ( cluster . LocationOfOrigin ) )
if err != nil {
2019-10-15 10:01:09 -04:00
return fmt . Errorf ( "could not determine the absolute path of config file %s: %v" , cluster . LocationOfOrigin , err )
2017-11-20 09:30:52 -05:00
}
if err := ResolvePaths ( GetClusterFileReferences ( cluster ) , base ) ; err != nil {
return err
}
}
for _ , authInfo := range config . AuthInfos {
if len ( authInfo . LocationOfOrigin ) == 0 {
continue
}
base , err := filepath . Abs ( filepath . Dir ( authInfo . LocationOfOrigin ) )
if err != nil {
2019-10-15 10:01:09 -04:00
return fmt . Errorf ( "could not determine the absolute path of config file %s: %v" , authInfo . LocationOfOrigin , err )
2017-11-20 09:30:52 -05:00
}
if err := ResolvePaths ( GetAuthInfoFileReferences ( authInfo ) , base ) ; err != nil {
return err
}
}
return nil
}
// RelativizeClusterLocalPaths first absolutizes the paths by calling ResolveLocalPaths. This assumes that any NEW path is already
// absolute, but any existing path will be resolved relative to LocationOfOrigin
func RelativizeClusterLocalPaths ( cluster * clientcmdapi . Cluster ) error {
if len ( cluster . LocationOfOrigin ) == 0 {
return fmt . Errorf ( "no location of origin for %s" , cluster . Server )
}
base , err := filepath . Abs ( filepath . Dir ( cluster . LocationOfOrigin ) )
if err != nil {
return fmt . Errorf ( "could not determine the absolute path of config file %s: %v" , cluster . LocationOfOrigin , err )
}
if err := ResolvePaths ( GetClusterFileReferences ( cluster ) , base ) ; err != nil {
return err
}
if err := RelativizePathWithNoBacksteps ( GetClusterFileReferences ( cluster ) , base ) ; err != nil {
return err
}
return nil
}
// RelativizeAuthInfoLocalPaths first absolutizes the paths by calling ResolveLocalPaths. This assumes that any NEW path is already
// absolute, but any existing path will be resolved relative to LocationOfOrigin
func RelativizeAuthInfoLocalPaths ( authInfo * clientcmdapi . AuthInfo ) error {
if len ( authInfo . LocationOfOrigin ) == 0 {
return fmt . Errorf ( "no location of origin for %v" , authInfo )
}
base , err := filepath . Abs ( filepath . Dir ( authInfo . LocationOfOrigin ) )
if err != nil {
return fmt . Errorf ( "could not determine the absolute path of config file %s: %v" , authInfo . LocationOfOrigin , err )
}
if err := ResolvePaths ( GetAuthInfoFileReferences ( authInfo ) , base ) ; err != nil {
return err
}
if err := RelativizePathWithNoBacksteps ( GetAuthInfoFileReferences ( authInfo ) , base ) ; err != nil {
return err
}
return nil
}
func RelativizeConfigPaths ( config * clientcmdapi . Config , base string ) error {
return RelativizePathWithNoBacksteps ( GetConfigFileReferences ( config ) , base )
}
func ResolveConfigPaths ( config * clientcmdapi . Config , base string ) error {
return ResolvePaths ( GetConfigFileReferences ( config ) , base )
}
func GetConfigFileReferences ( config * clientcmdapi . Config ) [ ] * string {
refs := [ ] * string { }
for _ , cluster := range config . Clusters {
refs = append ( refs , GetClusterFileReferences ( cluster ) ... )
}
for _ , authInfo := range config . AuthInfos {
refs = append ( refs , GetAuthInfoFileReferences ( authInfo ) ... )
}
return refs
}
func GetClusterFileReferences ( cluster * clientcmdapi . Cluster ) [ ] * string {
return [ ] * string { & cluster . CertificateAuthority }
}
func GetAuthInfoFileReferences ( authInfo * clientcmdapi . AuthInfo ) [ ] * string {
2018-07-25 04:17:02 -04:00
s := [ ] * string { & authInfo . ClientCertificate , & authInfo . ClientKey , & authInfo . TokenFile }
// Only resolve exec command if it isn't PATH based.
if authInfo . Exec != nil && strings . ContainsRune ( authInfo . Exec . Command , filepath . Separator ) {
s = append ( s , & authInfo . Exec . Command )
}
return s
2017-11-20 09:30:52 -05:00
}
// ResolvePaths updates the given refs to be absolute paths, relative to the given base directory
func ResolvePaths ( refs [ ] * string , base string ) error {
for _ , ref := range refs {
// Don't resolve empty paths
if len ( * ref ) > 0 {
// Don't resolve absolute paths
if ! filepath . IsAbs ( * ref ) {
* ref = filepath . Join ( base , * ref )
}
}
}
return nil
}
// RelativizePathWithNoBacksteps updates the given refs to be relative paths, relative to the given base directory as long as they do not require backsteps.
// 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
func RelativizePathWithNoBacksteps ( refs [ ] * string , base string ) error {
for _ , ref := range refs {
// Don't relativize empty paths
if len ( * ref ) > 0 {
rel , err := MakeRelative ( * ref , base )
if err != nil {
return err
}
// if we have a backstep, don't mess with the path
if strings . HasPrefix ( rel , "../" ) {
if filepath . IsAbs ( * ref ) {
continue
}
return fmt . Errorf ( "%v requires backsteps and is not absolute" , * ref )
}
* ref = rel
}
}
return nil
}
func MakeRelative ( path , base string ) ( string , error ) {
if len ( path ) > 0 {
rel , err := filepath . Rel ( base , path )
if err != nil {
return path , err
}
return rel , nil
}
return path , nil
}
2018-07-25 04:17:02 -04:00
// deduplicate removes any duplicated values and returns a new slice, keeping the order unchanged
func deduplicate ( s [ ] string ) [ ] string {
encountered := map [ string ] bool { }
ret := make ( [ ] string , 0 )
for i := range s {
if encountered [ s [ i ] ] {
continue
}
encountered [ s [ i ] ] = true
ret = append ( ret , s [ i ] )
}
return ret
}