2 Copyright 2014 The Kubernetes Authors.
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
8 http://www.apache.org/licenses/LICENSE-2.0
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.
29 restclient "k8s.io/client-go/rest"
30 clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
33 // ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files
34 type ConfigAccess interface {
35 // GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config
36 GetLoadingPrecedence() []string
37 // GetStartingConfig returns the config that subcommands should being operating against. It may or may not be merged depending on loading rules
38 GetStartingConfig() (*clientcmdapi.Config, error)
39 // 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.
40 GetDefaultFilename() string
41 // 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
43 // 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
44 GetExplicitFile() string
47 type PathOptions struct {
48 // GlobalFile is the full path to the file to load as the global (final) option
50 // EnvVar is the env var name that points to the list of kubeconfig files to load
52 // ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file
53 ExplicitFileFlag string
55 // GlobalFileSubpath is an optional value used for displaying help
56 GlobalFileSubpath string
58 LoadingRules *ClientConfigLoadingRules
61 func (o *PathOptions) GetEnvVarFiles() []string {
62 if len(o.EnvVar) == 0 {
66 envVarValue := os.Getenv(o.EnvVar)
67 if len(envVarValue) == 0 {
71 fileList := filepath.SplitList(envVarValue)
72 // prevent the same path load multiple times
73 return deduplicate(fileList)
76 func (o *PathOptions) GetLoadingPrecedence() []string {
77 if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
81 return []string{o.GlobalFile}
84 func (o *PathOptions) GetStartingConfig() (*clientcmdapi.Config, error) {
85 // don't mutate the original
86 loadingRules := *o.LoadingRules
87 loadingRules.Precedence = o.GetLoadingPrecedence()
89 clientConfig := NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &ConfigOverrides{})
90 rawConfig, err := clientConfig.RawConfig()
91 if os.IsNotExist(err) {
92 return clientcmdapi.NewConfig(), nil
98 return &rawConfig, nil
101 func (o *PathOptions) GetDefaultFilename() string {
102 if o.IsExplicitFile() {
103 return o.GetExplicitFile()
106 if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
107 if len(envVarFiles) == 1 {
108 return envVarFiles[0]
111 // if any of the envvar files already exists, return it
112 for _, envVarFile := range envVarFiles {
113 if _, err := os.Stat(envVarFile); err == nil {
118 // otherwise, return the last one in the list
119 return envVarFiles[len(envVarFiles)-1]
125 func (o *PathOptions) IsExplicitFile() bool {
126 if len(o.LoadingRules.ExplicitPath) > 0 {
133 func (o *PathOptions) GetExplicitFile() string {
134 return o.LoadingRules.ExplicitPath
137 func NewDefaultPathOptions() *PathOptions {
139 GlobalFile: RecommendedHomeFile,
140 EnvVar: RecommendedConfigPathEnvVar,
141 ExplicitFileFlag: RecommendedConfigPathFlag,
143 GlobalFileSubpath: path.Join(RecommendedHomeDir, RecommendedFileName),
145 LoadingRules: NewDefaultClientConfigLoadingRules(),
147 ret.LoadingRules.DoNotResolvePaths = true
152 // ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or
153 // uses the default destination file to write the results into. This results in multiple file reads, but it's very easy to follow.
154 // Preferences and CurrentContext should always be set in the default destination file. Since we can't distinguish between empty and missing values
155 // (no nil strings), we're forced have separate handling for them. In the kubeconfig cases, newConfig should have at most one difference,
156 // 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
158 func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error {
159 possibleSources := configAccess.GetLoadingPrecedence()
160 // sort the possible kubeconfig files so we always "lock" in the same order
161 // to avoid deadlock (note: this can fail w/ symlinks, but... come on).
162 sort.Strings(possibleSources)
163 for _, filename := range possibleSources {
164 if err := lockFile(filename); err != nil {
167 defer unlockFile(filename)
170 startingConfig, err := configAccess.GetStartingConfig()
175 // We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file.
176 // Special case the test for current context and preferences since those always write to the default file.
177 if reflect.DeepEqual(*startingConfig, newConfig) {
182 if startingConfig.CurrentContext != newConfig.CurrentContext {
183 if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil {
188 if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) {
189 if err := writePreferences(configAccess, newConfig.Preferences); err != nil {
194 // Search every cluster, authInfo, and context. First from new to old for differences, then from old to new for deletions
195 for key, cluster := range newConfig.Clusters {
196 startingCluster, exists := startingConfig.Clusters[key]
197 if !reflect.DeepEqual(cluster, startingCluster) || !exists {
198 destinationFile := cluster.LocationOfOrigin
199 if len(destinationFile) == 0 {
200 destinationFile = configAccess.GetDefaultFilename()
203 configToWrite, err := getConfigFromFile(destinationFile)
209 configToWrite.Clusters[key] = &t
210 configToWrite.Clusters[key].LocationOfOrigin = destinationFile
212 if err := RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil {
217 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
223 // seenConfigs stores a map of config source filenames to computed config objects
224 seenConfigs := map[string]*clientcmdapi.Config{}
226 for key, context := range newConfig.Contexts {
227 startingContext, exists := startingConfig.Contexts[key]
228 if !reflect.DeepEqual(context, startingContext) || !exists {
229 destinationFile := context.LocationOfOrigin
230 if len(destinationFile) == 0 {
231 destinationFile = configAccess.GetDefaultFilename()
234 // we only obtain a fresh config object from its source file
235 // if we have not seen it already - this prevents us from
236 // reading and writing to the same number of files repeatedly
237 // when multiple / all contexts share the same destination file.
238 configToWrite, seen := seenConfigs[destinationFile]
241 configToWrite, err = getConfigFromFile(destinationFile)
245 seenConfigs[destinationFile] = configToWrite
248 configToWrite.Contexts[key] = context
252 // actually persist config object changes
253 for destinationFile, configToWrite := range seenConfigs {
254 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
259 for key, authInfo := range newConfig.AuthInfos {
260 startingAuthInfo, exists := startingConfig.AuthInfos[key]
261 if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists {
262 destinationFile := authInfo.LocationOfOrigin
263 if len(destinationFile) == 0 {
264 destinationFile = configAccess.GetDefaultFilename()
267 configToWrite, err := getConfigFromFile(destinationFile)
272 configToWrite.AuthInfos[key] = &t
273 configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile
275 if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil {
280 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
286 for key, cluster := range startingConfig.Clusters {
287 if _, exists := newConfig.Clusters[key]; !exists {
288 destinationFile := cluster.LocationOfOrigin
289 if len(destinationFile) == 0 {
290 destinationFile = configAccess.GetDefaultFilename()
293 configToWrite, err := getConfigFromFile(destinationFile)
297 delete(configToWrite.Clusters, key)
299 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
305 for key, context := range startingConfig.Contexts {
306 if _, exists := newConfig.Contexts[key]; !exists {
307 destinationFile := context.LocationOfOrigin
308 if len(destinationFile) == 0 {
309 destinationFile = configAccess.GetDefaultFilename()
312 configToWrite, err := getConfigFromFile(destinationFile)
316 delete(configToWrite.Contexts, key)
318 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
324 for key, authInfo := range startingConfig.AuthInfos {
325 if _, exists := newConfig.AuthInfos[key]; !exists {
326 destinationFile := authInfo.LocationOfOrigin
327 if len(destinationFile) == 0 {
328 destinationFile = configAccess.GetDefaultFilename()
331 configToWrite, err := getConfigFromFile(destinationFile)
335 delete(configToWrite.AuthInfos, key)
337 if err := WriteToFile(*configToWrite, destinationFile); err != nil {
346 func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister {
347 return &persister{configAccess, user}
350 type persister struct {
351 configAccess ConfigAccess
355 func (p *persister) Persist(config map[string]string) error {
356 newConfig, err := p.configAccess.GetStartingConfig()
360 authInfo, ok := newConfig.AuthInfos[p.user]
361 if ok && authInfo.AuthProvider != nil {
362 authInfo.AuthProvider.Config = config
363 ModifyConfig(p.configAccess, *newConfig, false)
368 // writeCurrentContext takes three possible paths.
369 // If newCurrentContext is the same as the startingConfig's current context, then we exit.
370 // If newCurrentContext has a value, then that value is written into the default destination file.
371 // If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file
372 func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error {
373 if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
375 } else if startingConfig.CurrentContext == newCurrentContext {
379 if configAccess.IsExplicitFile() {
380 file := configAccess.GetExplicitFile()
381 currConfig, err := getConfigFromFile(file)
385 currConfig.CurrentContext = newCurrentContext
386 if err := WriteToFile(*currConfig, file); err != nil {
393 if len(newCurrentContext) > 0 {
394 destinationFile := configAccess.GetDefaultFilename()
395 config, err := getConfigFromFile(destinationFile)
399 config.CurrentContext = newCurrentContext
401 if err := WriteToFile(*config, destinationFile); err != nil {
408 // 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
409 for _, file := range configAccess.GetLoadingPrecedence() {
410 if _, err := os.Stat(file); err == nil {
411 currConfig, err := getConfigFromFile(file)
416 if len(currConfig.CurrentContext) > 0 {
417 currConfig.CurrentContext = newCurrentContext
418 if err := WriteToFile(*currConfig, file); err != nil {
427 return errors.New("no config found to write context")
430 func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error {
431 if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
433 } else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) {
437 if configAccess.IsExplicitFile() {
438 file := configAccess.GetExplicitFile()
439 currConfig, err := getConfigFromFile(file)
443 currConfig.Preferences = newPrefs
444 if err := WriteToFile(*currConfig, file); err != nil {
451 for _, file := range configAccess.GetLoadingPrecedence() {
452 currConfig, err := getConfigFromFile(file)
457 if !reflect.DeepEqual(currConfig.Preferences, newPrefs) {
458 currConfig.Preferences = newPrefs
459 if err := WriteToFile(*currConfig, file); err != nil {
467 return errors.New("no config found to write preferences")
470 // 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.
471 func getConfigFromFile(filename string) (*clientcmdapi.Config, error) {
472 config, err := LoadFromFile(filename)
473 if err != nil && !os.IsNotExist(err) {
477 config = clientcmdapi.NewConfig()
482 // 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
483 func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config {
484 config, err := getConfigFromFile(filename)
486 klog.FatalDepth(1, err)