a3bf13711c
I had to run `make generate`. Some API functions got additional parameters `Options` and `Context`. I used empty options and `context.TODO()` for now. Signed-off-by: leonnicolas <leonloechner@gmx.de>
505 lines
15 KiB
Go
505 lines
15 KiB
Go
/*
|
|
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"
|
|
|
|
"k8s.io/klog/v2"
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
)
|
|
|
|
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 {
|
|
if o.IsExplicitFile() {
|
|
return []string{o.GetExplicitFile()}
|
|
}
|
|
|
|
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 {
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
return ModifyConfig(p.configAccess, *newConfig, false)
|
|
}
|
|
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
|
|
}
|