2019-01-12 04:58:27 +00: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 (
"errors"
"os"
"path"
"path/filepath"
"reflect"
"sort"
2020-08-10 17:43:49 +00:00
"k8s.io/klog/v2"
2019-01-12 04:58:27 +00:00
restclient "k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
// ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files
type ConfigAccess interface {
// GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config
GetLoadingPrecedence ( ) [ ] string
// GetStartingConfig returns the config that subcommands should being operating against. It may or may not be merged depending on loading rules
GetStartingConfig ( ) ( * clientcmdapi . Config , error )
// 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.
GetDefaultFilename ( ) string
// 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
IsExplicitFile ( ) bool
// 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
GetExplicitFile ( ) string
}
type PathOptions struct {
// GlobalFile is the full path to the file to load as the global (final) option
GlobalFile string
// EnvVar is the env var name that points to the list of kubeconfig files to load
EnvVar string
// ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file
ExplicitFileFlag string
// GlobalFileSubpath is an optional value used for displaying help
GlobalFileSubpath string
LoadingRules * ClientConfigLoadingRules
}
2020-08-10 17:43:49 +00:00
var (
// UseModifyConfigLock ensures that access to kubeconfig file using ModifyConfig method
// is being guarded by a lock file.
// This variable is intentionaly made public so other consumers of this library
// can modify its default behavior, but be caution when disabling it since
// this will make your code not threadsafe.
UseModifyConfigLock = true
)
2019-01-12 04:58:27 +00:00
func ( o * PathOptions ) GetEnvVarFiles ( ) [ ] string {
if len ( o . EnvVar ) == 0 {
return [ ] string { }
}
envVarValue := os . Getenv ( o . EnvVar )
if len ( envVarValue ) == 0 {
return [ ] string { }
}
fileList := filepath . SplitList ( envVarValue )
// prevent the same path load multiple times
return deduplicate ( fileList )
}
func ( o * PathOptions ) GetLoadingPrecedence ( ) [ ] string {
2020-12-01 01:06:26 +00:00
if o . IsExplicitFile ( ) {
return [ ] string { o . GetExplicitFile ( ) }
}
2019-01-12 04:58:27 +00:00
if envVarFiles := o . GetEnvVarFiles ( ) ; len ( envVarFiles ) > 0 {
return envVarFiles
}
return [ ] string { o . GlobalFile }
}
func ( o * PathOptions ) GetStartingConfig ( ) ( * clientcmdapi . Config , error ) {
// don't mutate the original
loadingRules := * o . LoadingRules
loadingRules . Precedence = o . GetLoadingPrecedence ( )
clientConfig := NewNonInteractiveDeferredLoadingClientConfig ( & loadingRules , & ConfigOverrides { } )
rawConfig , err := clientConfig . RawConfig ( )
if os . IsNotExist ( err ) {
return clientcmdapi . NewConfig ( ) , nil
}
if err != nil {
return nil , err
}
return & rawConfig , nil
}
func ( o * PathOptions ) GetDefaultFilename ( ) string {
if o . IsExplicitFile ( ) {
return o . GetExplicitFile ( )
}
if envVarFiles := o . GetEnvVarFiles ( ) ; len ( envVarFiles ) > 0 {
if len ( envVarFiles ) == 1 {
return envVarFiles [ 0 ]
}
// if any of the envvar files already exists, return it
for _ , envVarFile := range envVarFiles {
if _ , err := os . Stat ( envVarFile ) ; err == nil {
return envVarFile
}
}
// otherwise, return the last one in the list
return envVarFiles [ len ( envVarFiles ) - 1 ]
}
return o . GlobalFile
}
func ( o * PathOptions ) IsExplicitFile ( ) bool {
if len ( o . LoadingRules . ExplicitPath ) > 0 {
return true
}
return false
}
func ( o * PathOptions ) GetExplicitFile ( ) string {
return o . LoadingRules . ExplicitPath
}
func NewDefaultPathOptions ( ) * PathOptions {
ret := & PathOptions {
GlobalFile : RecommendedHomeFile ,
EnvVar : RecommendedConfigPathEnvVar ,
ExplicitFileFlag : RecommendedConfigPathFlag ,
GlobalFileSubpath : path . Join ( RecommendedHomeDir , RecommendedFileName ) ,
LoadingRules : NewDefaultClientConfigLoadingRules ( ) ,
}
ret . LoadingRules . DoNotResolvePaths = true
return ret
}
// ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or
// uses the default destination file to write the results into. This results in multiple file reads, but it's very easy to follow.
// Preferences and CurrentContext should always be set in the default destination file. Since we can't distinguish between empty and missing values
// (no nil strings), we're forced have separate handling for them. In the kubeconfig cases, newConfig should have at most one difference,
// 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
// modified element.
func ModifyConfig ( configAccess ConfigAccess , newConfig clientcmdapi . Config , relativizePaths bool ) error {
2020-08-10 17:43:49 +00:00
if UseModifyConfigLock {
possibleSources := configAccess . GetLoadingPrecedence ( )
// sort the possible kubeconfig files so we always "lock" in the same order
// to avoid deadlock (note: this can fail w/ symlinks, but... come on).
sort . Strings ( possibleSources )
for _ , filename := range possibleSources {
if err := lockFile ( filename ) ; err != nil {
return err
}
defer unlockFile ( filename )
2019-01-12 04:58:27 +00:00
}
}
startingConfig , err := configAccess . GetStartingConfig ( )
if err != nil {
return err
}
// We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file.
// Special case the test for current context and preferences since those always write to the default file.
if reflect . DeepEqual ( * startingConfig , newConfig ) {
// nothing to do
return nil
}
if startingConfig . CurrentContext != newConfig . CurrentContext {
if err := writeCurrentContext ( configAccess , newConfig . CurrentContext ) ; err != nil {
return err
}
}
if ! reflect . DeepEqual ( startingConfig . Preferences , newConfig . Preferences ) {
if err := writePreferences ( configAccess , newConfig . Preferences ) ; err != nil {
return err
}
}
// Search every cluster, authInfo, and context. First from new to old for differences, then from old to new for deletions
for key , cluster := range newConfig . Clusters {
startingCluster , exists := startingConfig . Clusters [ key ]
if ! reflect . DeepEqual ( cluster , startingCluster ) || ! exists {
destinationFile := cluster . LocationOfOrigin
if len ( destinationFile ) == 0 {
destinationFile = configAccess . GetDefaultFilename ( )
}
configToWrite , err := getConfigFromFile ( destinationFile )
if err != nil {
return err
}
t := * cluster
configToWrite . Clusters [ key ] = & t
configToWrite . Clusters [ key ] . LocationOfOrigin = destinationFile
if relativizePaths {
if err := RelativizeClusterLocalPaths ( configToWrite . Clusters [ key ] ) ; err != nil {
return err
}
}
if err := WriteToFile ( * configToWrite , destinationFile ) ; err != nil {
return err
}
}
}
// seenConfigs stores a map of config source filenames to computed config objects
seenConfigs := map [ string ] * clientcmdapi . Config { }
for key , context := range newConfig . Contexts {
startingContext , exists := startingConfig . Contexts [ key ]
if ! reflect . DeepEqual ( context , startingContext ) || ! exists {
destinationFile := context . LocationOfOrigin
if len ( destinationFile ) == 0 {
destinationFile = configAccess . GetDefaultFilename ( )
}
// we only obtain a fresh config object from its source file
// if we have not seen it already - this prevents us from
// reading and writing to the same number of files repeatedly
// when multiple / all contexts share the same destination file.
configToWrite , seen := seenConfigs [ destinationFile ]
if ! seen {
var err error
configToWrite , err = getConfigFromFile ( destinationFile )
if err != nil {
return err
}
seenConfigs [ destinationFile ] = configToWrite
}
configToWrite . Contexts [ key ] = context
}
}
// actually persist config object changes
for destinationFile , configToWrite := range seenConfigs {
if err := WriteToFile ( * configToWrite , destinationFile ) ; err != nil {
return err
}
}
for key , authInfo := range newConfig . AuthInfos {
startingAuthInfo , exists := startingConfig . AuthInfos [ key ]
if ! reflect . DeepEqual ( authInfo , startingAuthInfo ) || ! exists {
destinationFile := authInfo . LocationOfOrigin
if len ( destinationFile ) == 0 {
destinationFile = configAccess . GetDefaultFilename ( )
}
configToWrite , err := getConfigFromFile ( destinationFile )
if err != nil {
return err
}
t := * authInfo
configToWrite . AuthInfos [ key ] = & t
configToWrite . AuthInfos [ key ] . LocationOfOrigin = destinationFile
if relativizePaths {
if err := RelativizeAuthInfoLocalPaths ( configToWrite . AuthInfos [ key ] ) ; err != nil {
return err
}
}
if err := WriteToFile ( * configToWrite , destinationFile ) ; err != nil {
return err
}
}
}
for key , cluster := range startingConfig . Clusters {
if _ , exists := newConfig . Clusters [ key ] ; ! exists {
destinationFile := cluster . LocationOfOrigin
if len ( destinationFile ) == 0 {
destinationFile = configAccess . GetDefaultFilename ( )
}
configToWrite , err := getConfigFromFile ( destinationFile )
if err != nil {
return err
}
delete ( configToWrite . Clusters , key )
if err := WriteToFile ( * configToWrite , destinationFile ) ; err != nil {
return err
}
}
}
for key , context := range startingConfig . Contexts {
if _ , exists := newConfig . Contexts [ key ] ; ! exists {
destinationFile := context . LocationOfOrigin
if len ( destinationFile ) == 0 {
destinationFile = configAccess . GetDefaultFilename ( )
}
configToWrite , err := getConfigFromFile ( destinationFile )
if err != nil {
return err
}
delete ( configToWrite . Contexts , key )
if err := WriteToFile ( * configToWrite , destinationFile ) ; err != nil {
return err
}
}
}
for key , authInfo := range startingConfig . AuthInfos {
if _ , exists := newConfig . AuthInfos [ key ] ; ! exists {
destinationFile := authInfo . LocationOfOrigin
if len ( destinationFile ) == 0 {
destinationFile = configAccess . GetDefaultFilename ( )
}
configToWrite , err := getConfigFromFile ( destinationFile )
if err != nil {
return err
}
delete ( configToWrite . AuthInfos , key )
if err := WriteToFile ( * configToWrite , destinationFile ) ; err != nil {
return err
}
}
}
return nil
}
func PersisterForUser ( configAccess ConfigAccess , user string ) restclient . AuthProviderConfigPersister {
return & persister { configAccess , user }
}
type persister struct {
configAccess ConfigAccess
user string
}
func ( p * persister ) Persist ( config map [ string ] string ) error {
newConfig , err := p . configAccess . GetStartingConfig ( )
if err != nil {
return err
}
authInfo , ok := newConfig . AuthInfos [ p . user ]
if ok && authInfo . AuthProvider != nil {
authInfo . AuthProvider . Config = config
2021-03-18 22:40:29 +00:00
return ModifyConfig ( p . configAccess , * newConfig , false )
2019-01-12 04:58:27 +00:00
}
return nil
}
// writeCurrentContext takes three possible paths.
// If newCurrentContext is the same as the startingConfig's current context, then we exit.
// If newCurrentContext has a value, then that value is written into the default destination file.
// If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file
func writeCurrentContext ( configAccess ConfigAccess , newCurrentContext string ) error {
if startingConfig , err := configAccess . GetStartingConfig ( ) ; err != nil {
return err
} else if startingConfig . CurrentContext == newCurrentContext {
return nil
}
if configAccess . IsExplicitFile ( ) {
file := configAccess . GetExplicitFile ( )
currConfig , err := getConfigFromFile ( file )
if err != nil {
return err
}
currConfig . CurrentContext = newCurrentContext
if err := WriteToFile ( * currConfig , file ) ; err != nil {
return err
}
return nil
}
if len ( newCurrentContext ) > 0 {
destinationFile := configAccess . GetDefaultFilename ( )
config , err := getConfigFromFile ( destinationFile )
if err != nil {
return err
}
config . CurrentContext = newCurrentContext
if err := WriteToFile ( * config , destinationFile ) ; err != nil {
return err
}
return nil
}
// 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
for _ , file := range configAccess . GetLoadingPrecedence ( ) {
if _ , err := os . Stat ( file ) ; err == nil {
currConfig , err := getConfigFromFile ( file )
if err != nil {
return err
}
if len ( currConfig . CurrentContext ) > 0 {
currConfig . CurrentContext = newCurrentContext
if err := WriteToFile ( * currConfig , file ) ; err != nil {
return err
}
return nil
}
}
}
return errors . New ( "no config found to write context" )
}
func writePreferences ( configAccess ConfigAccess , newPrefs clientcmdapi . Preferences ) error {
if startingConfig , err := configAccess . GetStartingConfig ( ) ; err != nil {
return err
} else if reflect . DeepEqual ( startingConfig . Preferences , newPrefs ) {
return nil
}
if configAccess . IsExplicitFile ( ) {
file := configAccess . GetExplicitFile ( )
currConfig , err := getConfigFromFile ( file )
if err != nil {
return err
}
currConfig . Preferences = newPrefs
if err := WriteToFile ( * currConfig , file ) ; err != nil {
return err
}
return nil
}
for _ , file := range configAccess . GetLoadingPrecedence ( ) {
currConfig , err := getConfigFromFile ( file )
if err != nil {
return err
}
if ! reflect . DeepEqual ( currConfig . Preferences , newPrefs ) {
currConfig . Preferences = newPrefs
if err := WriteToFile ( * currConfig , file ) ; err != nil {
return err
}
return nil
}
}
return errors . New ( "no config found to write preferences" )
}
// 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.
func getConfigFromFile ( filename string ) ( * clientcmdapi . Config , error ) {
config , err := LoadFromFile ( filename )
if err != nil && ! os . IsNotExist ( err ) {
return nil , err
}
if config == nil {
config = clientcmdapi . NewConfig ( )
}
return config , nil
}
// 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
func GetConfigFromFileOrDie ( filename string ) * clientcmdapi . Config {
config , err := getConfigFromFile ( filename )
if err != nil {
klog . FatalDepth ( 1 , err )
}
return config
}