change kubeconfig loading order

This commit is contained in:
deads2k
2015-04-10 08:54:22 -04:00
parent b59266ad84
commit de4be5422c
50 changed files with 446 additions and 409 deletions

View File

@@ -18,10 +18,10 @@ package config
import (
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"reflect"
"strconv"
@@ -33,41 +33,54 @@ import (
)
type PathOptions struct {
Local bool
Global bool
UseEnvVar bool
LocalFile string
// GlobalFile is the full path to the file to load as the global (final) option
GlobalFile string
EnvVarFile string
EnvVar 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 *clientcmd.ClientConfigLoadingRules
}
// 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
}
func NewCmdConfig(pathOptions *PathOptions, out io.Writer) *cobra.Command {
if len(pathOptions.ExplicitFileFlag) == 0 {
pathOptions.ExplicitFileFlag = clientcmd.RecommendedConfigPathFlag
}
if len(pathOptions.EnvVar) > 0 {
pathOptions.EnvVarFile = os.Getenv(pathOptions.EnvVar)
}
cmd := &cobra.Command{
Use: "config SUBCOMMAND",
Short: "config modifies kubeconfig files",
Long: `config modifies kubeconfig files using subcommands like "kubectl config set current-context my-context"`,
Long: `config modifies kubeconfig files using subcommands like "kubectl config set current-context my-context"
The loading order follows these rules:
1. If the --` + pathOptions.ExplicitFileFlag + ` flag is set, then only that file is loaded. The flag may only be set once and no merging takes place.
2. If $` + pathOptions.EnvVar + ` environment variable is set, then it is used a list of paths (normal path delimitting rules for your system). These paths are merged together. When a value is modified, it is modified in the file that defines the stanza. When a value is created, it is created in the first file that exists. If no files in the chain exist, then it creates the last file in the list.
3. Otherwise, ` + path.Join("${HOME}", pathOptions.GlobalFileSubpath) + ` is used and no merging takes place.
`,
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}
// file paths are common to all sub commands
cmd.PersistentFlags().BoolVar(&pathOptions.Local, "local", pathOptions.Local, "use the kubeconfig in the current directory")
cmd.PersistentFlags().BoolVar(&pathOptions.Global, "global", pathOptions.Global, "use the kubeconfig from "+pathOptions.GlobalFile)
cmd.PersistentFlags().BoolVar(&pathOptions.UseEnvVar, "envvar", pathOptions.UseEnvVar, "use the kubeconfig from $"+pathOptions.EnvVar)
cmd.PersistentFlags().StringVar(&pathOptions.LoadingRules.ExplicitPath, pathOptions.ExplicitFileFlag, pathOptions.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
cmd.AddCommand(NewCmdConfigView(out, pathOptions))
@@ -83,13 +96,12 @@ func NewCmdConfig(pathOptions *PathOptions, out io.Writer) *cobra.Command {
func NewDefaultPathOptions() *PathOptions {
ret := &PathOptions{
LocalFile: ".kubeconfig",
GlobalFile: path.Join(os.Getenv("HOME"), "/.kube/.kubeconfig"),
EnvVar: clientcmd.RecommendedConfigPathEnvVar,
EnvVarFile: os.Getenv(clientcmd.RecommendedConfigPathEnvVar),
GlobalFile: clientcmd.RecommendedHomeFile,
EnvVar: clientcmd.RecommendedConfigPathEnvVar,
ExplicitFileFlag: clientcmd.RecommendedConfigPathFlag,
GlobalFileSubpath: clientcmd.RecommendedHomeFileName,
LoadingRules: clientcmd.NewDefaultClientConfigLoadingRules(),
}
ret.LoadingRules.DoNotResolvePaths = true
@@ -97,125 +109,78 @@ func NewDefaultPathOptions() *PathOptions {
return ret
}
func (o PathOptions) Validate() error {
if len(o.LoadingRules.ExplicitPath) > 0 {
if o.Global {
return errors.New("cannot specify both --" + o.ExplicitFileFlag + " and --global")
}
if o.Local {
return errors.New("cannot specify both --" + o.ExplicitFileFlag + " and --local")
}
if o.UseEnvVar {
return errors.New("cannot specify both --" + o.ExplicitFileFlag + " and --envvar")
}
func (o *PathOptions) GetEnvVarFiles() []string {
if len(o.EnvVar) == 0 {
return []string{}
}
if o.Global {
if o.Local {
return errors.New("cannot specify both --global and --local")
}
if o.UseEnvVar {
return errors.New("cannot specify both --global and --envvar")
}
envVarValue := os.Getenv(o.EnvVar)
if len(envVarValue) == 0 {
return []string{}
}
if o.Local {
if o.UseEnvVar {
return errors.New("cannot specify both --local and --envvar")
}
}
if o.UseEnvVar {
if len(o.EnvVarFile) == 0 {
return fmt.Errorf("environment variable %v does not have a value", o.EnvVar)
}
}
return nil
return filepath.SplitList(envVarValue)
}
func (o *PathOptions) getStartingConfig() (*clientcmdapi.Config, error) {
if err := o.Validate(); err != nil {
func (o *PathOptions) GetLoadingPrecedence() []string {
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 := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &clientcmd.ConfigOverrides{})
rawConfig, err := clientConfig.RawConfig()
if os.IsNotExist(err) {
return clientcmdapi.NewConfig(), nil
}
if err != nil {
return nil, err
}
config := clientcmdapi.NewConfig()
switch {
case o.Global:
config = getConfigFromFileOrDie(o.GlobalFile)
case o.UseEnvVar:
config = getConfigFromFileOrDie(o.EnvVarFile)
case o.Local:
config = getConfigFromFileOrDie(o.LocalFile)
// no specific flag was set, load according to the loading rules
default:
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(o.LoadingRules, &clientcmd.ConfigOverrides{})
rawConfig, err := clientConfig.RawConfig()
if err != nil {
return nil, err
}
config = &rawConfig
}
return config, nil
return &rawConfig, nil
}
// 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.
func (o *PathOptions) GetDefaultFilename() string {
if o.IsExplicitFile() {
return o.GetExplicitFile()
}
if len(o.EnvVarFile) > 0 {
return o.EnvVarFile
}
if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
if len(envVarFiles) == 1 {
return envVarFiles[0]
}
if _, err := os.Stat(o.LocalFile); err == nil {
return o.LocalFile
// 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 {
switch {
case len(o.LoadingRules.ExplicitPath) > 0 ||
o.Global ||
o.UseEnvVar ||
o.Local:
if len(o.LoadingRules.ExplicitPath) > 0 {
return true
}
return false
}
func (o *PathOptions) GetExplicitFile() string {
if !o.IsExplicitFile() {
return ""
}
switch {
case len(o.LoadingRules.ExplicitPath) > 0:
return o.LoadingRules.ExplicitPath
case o.Global:
return o.GlobalFile
case o.UseEnvVar:
return o.EnvVarFile
case o.Local:
return o.LocalFile
}
return ""
return o.LoadingRules.ExplicitPath
}
// ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or
@@ -223,8 +188,8 @@ func (o *PathOptions) GetExplicitFile() string {
// 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 all the currently known cases, newConfig should have, at most, one difference,
// that means that this code will only write into a single file.
func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
startingConfig, err := o.getStartingConfig()
func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config) error {
startingConfig, err := configAccess.GetStartingConfig()
if err != nil {
return err
}
@@ -237,12 +202,12 @@ func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
// nothing to do
case startingConfig.CurrentContext != newConfig.CurrentContext:
if err := o.writeCurrentContext(newConfig.CurrentContext); err != nil {
if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil {
return err
}
case !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences):
if err := o.writePreferences(newConfig.Preferences); err != nil {
if err := writePreferences(configAccess, newConfig.Preferences); err != nil {
return err
}
@@ -253,7 +218,7 @@ func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
if !reflect.DeepEqual(cluster, startingCluster) || !exists {
destinationFile := cluster.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = o.GetDefaultFilename()
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite := getConfigFromFileOrDie(destinationFile)
@@ -270,7 +235,7 @@ func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
if !reflect.DeepEqual(context, startingContext) || !exists {
destinationFile := context.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = o.GetDefaultFilename()
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite := getConfigFromFileOrDie(destinationFile)
@@ -287,7 +252,7 @@ func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists {
destinationFile := authInfo.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = o.GetDefaultFilename()
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite := getConfigFromFileOrDie(destinationFile)
@@ -303,7 +268,7 @@ func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
if _, exists := newConfig.Clusters[key]; !exists {
destinationFile := cluster.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = o.GetDefaultFilename()
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite := getConfigFromFileOrDie(destinationFile)
@@ -319,7 +284,7 @@ func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
if _, exists := newConfig.Contexts[key]; !exists {
destinationFile := context.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = o.GetDefaultFilename()
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite := getConfigFromFileOrDie(destinationFile)
@@ -335,7 +300,7 @@ func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
if _, exists := newConfig.AuthInfos[key]; !exists {
destinationFile := authInfo.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = o.GetDefaultFilename()
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite := getConfigFromFileOrDie(destinationFile)
@@ -356,15 +321,26 @@ func (o *PathOptions) ModifyConfig(newConfig clientcmdapi.Config) error {
// 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 (o *PathOptions) writeCurrentContext(newCurrentContext string) error {
if startingConfig, err := o.getStartingConfig(); err != nil {
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 := getConfigFromFileOrDie(file)
currConfig.CurrentContext = newCurrentContext
if err := clientcmd.WriteToFile(*currConfig, file); err != nil {
return err
}
return nil
}
if len(newCurrentContext) > 0 {
destinationFile := o.GetDefaultFilename()
destinationFile := configAccess.GetDefaultFilename()
config := getConfigFromFileOrDie(destinationFile)
config.CurrentContext = newCurrentContext
@@ -375,46 +351,34 @@ func (o *PathOptions) writeCurrentContext(newCurrentContext string) error {
return nil
}
if o.IsExplicitFile() {
file := o.GetExplicitFile()
currConfig := getConfigFromFileOrDie(file)
currConfig.CurrentContext = newCurrentContext
if err := clientcmd.WriteToFile(*currConfig, file); err != nil {
return err
}
// 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 := getConfigFromFileOrDie(file)
return nil
}
if len(currConfig.CurrentContext) > 0 {
currConfig.CurrentContext = newCurrentContext
if err := clientcmd.WriteToFile(*currConfig, file); err != nil {
return err
}
filesToCheck := make([]string, 0, len(o.LoadingRules.Precedence)+1)
filesToCheck = append(filesToCheck, o.LoadingRules.ExplicitPath)
filesToCheck = append(filesToCheck, o.LoadingRules.Precedence...)
for _, file := range filesToCheck {
currConfig := getConfigFromFileOrDie(file)
if len(currConfig.CurrentContext) > 0 {
currConfig.CurrentContext = newCurrentContext
if err := clientcmd.WriteToFile(*currConfig, file); err != nil {
return err
return nil
}
return nil
}
}
return nil
return errors.New("no config found to write context")
}
func (o *PathOptions) writePreferences(newPrefs clientcmdapi.Preferences) error {
if startingConfig, err := o.getStartingConfig(); err != nil {
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 o.IsExplicitFile() {
file := o.GetExplicitFile()
if configAccess.IsExplicitFile() {
file := configAccess.GetExplicitFile()
currConfig := getConfigFromFileOrDie(file)
currConfig.Preferences = newPrefs
if err := clientcmd.WriteToFile(*currConfig, file); err != nil {
@@ -424,11 +388,7 @@ func (o *PathOptions) writePreferences(newPrefs clientcmdapi.Preferences) error
return nil
}
filesToCheck := make([]string, 0, len(o.LoadingRules.Precedence)+1)
filesToCheck = append(filesToCheck, o.LoadingRules.ExplicitPath)
filesToCheck = append(filesToCheck, o.LoadingRules.Precedence...)
for _, file := range filesToCheck {
for _, file := range configAccess.GetLoadingPrecedence() {
currConfig := getConfigFromFileOrDie(file)
if !reflect.DeepEqual(currConfig.Preferences, newPrefs) {
@@ -441,7 +401,7 @@ func (o *PathOptions) writePreferences(newPrefs clientcmdapi.Preferences) error
}
}
return nil
return errors.New("no config found to write preferences")
}
// 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