config.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. /*
  2. Copyright 2014 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package clientcmd
  14. import (
  15. "errors"
  16. "os"
  17. "path/filepath"
  18. "reflect"
  19. "sort"
  20. "k8s.io/klog/v2"
  21. restclient "k8s.io/client-go/rest"
  22. clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
  23. )
  24. // ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files
  25. type ConfigAccess interface {
  26. // GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config
  27. GetLoadingPrecedence() []string
  28. // GetStartingConfig returns the config that subcommands should being operating against. It may or may not be merged depending on loading rules
  29. GetStartingConfig() (*clientcmdapi.Config, error)
  30. // 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.
  31. GetDefaultFilename() string
  32. // 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
  33. IsExplicitFile() bool
  34. // 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
  35. GetExplicitFile() string
  36. }
  37. type PathOptions struct {
  38. // GlobalFile is the full path to the file to load as the global (final) option
  39. GlobalFile string
  40. // EnvVar is the env var name that points to the list of kubeconfig files to load
  41. EnvVar string
  42. // ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file
  43. ExplicitFileFlag string
  44. // GlobalFileSubpath is an optional value used for displaying help
  45. GlobalFileSubpath string
  46. LoadingRules *ClientConfigLoadingRules
  47. }
  48. var (
  49. // UseModifyConfigLock ensures that access to kubeconfig file using ModifyConfig method
  50. // is being guarded by a lock file.
  51. // This variable is intentionaly made public so other consumers of this library
  52. // can modify its default behavior, but be caution when disabling it since
  53. // this will make your code not threadsafe.
  54. UseModifyConfigLock = true
  55. )
  56. func (o *PathOptions) GetEnvVarFiles() []string {
  57. if len(o.EnvVar) == 0 {
  58. return []string{}
  59. }
  60. envVarValue := os.Getenv(o.EnvVar)
  61. if len(envVarValue) == 0 {
  62. return []string{}
  63. }
  64. fileList := filepath.SplitList(envVarValue)
  65. // prevent the same path load multiple times
  66. return deduplicate(fileList)
  67. }
  68. func (o *PathOptions) GetLoadingPrecedence() []string {
  69. if o.IsExplicitFile() {
  70. return []string{o.GetExplicitFile()}
  71. }
  72. if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
  73. return envVarFiles
  74. }
  75. return []string{o.GlobalFile}
  76. }
  77. func (o *PathOptions) GetStartingConfig() (*clientcmdapi.Config, error) {
  78. // don't mutate the original
  79. loadingRules := *o.LoadingRules
  80. loadingRules.Precedence = o.GetLoadingPrecedence()
  81. clientConfig := NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &ConfigOverrides{})
  82. rawConfig, err := clientConfig.RawConfig()
  83. if os.IsNotExist(err) {
  84. return clientcmdapi.NewConfig(), nil
  85. }
  86. if err != nil {
  87. return nil, err
  88. }
  89. return &rawConfig, nil
  90. }
  91. func (o *PathOptions) GetDefaultFilename() string {
  92. if o.IsExplicitFile() {
  93. return o.GetExplicitFile()
  94. }
  95. if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
  96. if len(envVarFiles) == 1 {
  97. return envVarFiles[0]
  98. }
  99. // if any of the envvar files already exists, return it
  100. for _, envVarFile := range envVarFiles {
  101. if _, err := os.Stat(envVarFile); err == nil {
  102. return envVarFile
  103. }
  104. }
  105. // otherwise, return the last one in the list
  106. return envVarFiles[len(envVarFiles)-1]
  107. }
  108. return o.GlobalFile
  109. }
  110. func (o *PathOptions) IsExplicitFile() bool {
  111. return len(o.LoadingRules.ExplicitPath) > 0
  112. }
  113. func (o *PathOptions) GetExplicitFile() string {
  114. return o.LoadingRules.ExplicitPath
  115. }
  116. func NewDefaultPathOptions() *PathOptions {
  117. ret := &PathOptions{
  118. GlobalFile: RecommendedHomeFile,
  119. EnvVar: RecommendedConfigPathEnvVar,
  120. ExplicitFileFlag: RecommendedConfigPathFlag,
  121. GlobalFileSubpath: filepath.Join(RecommendedHomeDir, RecommendedFileName),
  122. LoadingRules: NewDefaultClientConfigLoadingRules(),
  123. }
  124. ret.LoadingRules.DoNotResolvePaths = true
  125. return ret
  126. }
  127. // ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or
  128. // uses the default destination file to write the results into. This results in multiple file reads, but it's very easy to follow.
  129. // Preferences and CurrentContext should always be set in the default destination file. Since we can't distinguish between empty and missing values
  130. // (no nil strings), we're forced have separate handling for them. In the kubeconfig cases, newConfig should have at most one difference,
  131. // 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
  132. // modified element.
  133. func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error {
  134. if UseModifyConfigLock {
  135. possibleSources := configAccess.GetLoadingPrecedence()
  136. // sort the possible kubeconfig files so we always "lock" in the same order
  137. // to avoid deadlock (note: this can fail w/ symlinks, but... come on).
  138. sort.Strings(possibleSources)
  139. for _, filename := range possibleSources {
  140. if err := lockFile(filename); err != nil {
  141. return err
  142. }
  143. defer unlockFile(filename)
  144. }
  145. }
  146. startingConfig, err := configAccess.GetStartingConfig()
  147. if err != nil {
  148. return err
  149. }
  150. // We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file.
  151. // Special case the test for current context and preferences since those always write to the default file.
  152. if reflect.DeepEqual(*startingConfig, newConfig) {
  153. // nothing to do
  154. return nil
  155. }
  156. if startingConfig.CurrentContext != newConfig.CurrentContext {
  157. if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil {
  158. return err
  159. }
  160. }
  161. if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) {
  162. if err := writePreferences(configAccess, newConfig.Preferences); err != nil {
  163. return err
  164. }
  165. }
  166. // Search every cluster, authInfo, and context. First from new to old for differences, then from old to new for deletions
  167. for key, cluster := range newConfig.Clusters {
  168. startingCluster, exists := startingConfig.Clusters[key]
  169. if !reflect.DeepEqual(cluster, startingCluster) || !exists {
  170. destinationFile := cluster.LocationOfOrigin
  171. if len(destinationFile) == 0 {
  172. destinationFile = configAccess.GetDefaultFilename()
  173. }
  174. configToWrite, err := getConfigFromFile(destinationFile)
  175. if err != nil {
  176. return err
  177. }
  178. t := *cluster
  179. configToWrite.Clusters[key] = &t
  180. configToWrite.Clusters[key].LocationOfOrigin = destinationFile
  181. if relativizePaths {
  182. if err := RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil {
  183. return err
  184. }
  185. }
  186. if err := WriteToFile(*configToWrite, destinationFile); err != nil {
  187. return err
  188. }
  189. }
  190. }
  191. // seenConfigs stores a map of config source filenames to computed config objects
  192. seenConfigs := map[string]*clientcmdapi.Config{}
  193. for key, context := range newConfig.Contexts {
  194. startingContext, exists := startingConfig.Contexts[key]
  195. if !reflect.DeepEqual(context, startingContext) || !exists {
  196. destinationFile := context.LocationOfOrigin
  197. if len(destinationFile) == 0 {
  198. destinationFile = configAccess.GetDefaultFilename()
  199. }
  200. // we only obtain a fresh config object from its source file
  201. // if we have not seen it already - this prevents us from
  202. // reading and writing to the same number of files repeatedly
  203. // when multiple / all contexts share the same destination file.
  204. configToWrite, seen := seenConfigs[destinationFile]
  205. if !seen {
  206. var err error
  207. configToWrite, err = getConfigFromFile(destinationFile)
  208. if err != nil {
  209. return err
  210. }
  211. seenConfigs[destinationFile] = configToWrite
  212. }
  213. configToWrite.Contexts[key] = context
  214. }
  215. }
  216. // actually persist config object changes
  217. for destinationFile, configToWrite := range seenConfigs {
  218. if err := WriteToFile(*configToWrite, destinationFile); err != nil {
  219. return err
  220. }
  221. }
  222. for key, authInfo := range newConfig.AuthInfos {
  223. startingAuthInfo, exists := startingConfig.AuthInfos[key]
  224. if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists {
  225. destinationFile := authInfo.LocationOfOrigin
  226. if len(destinationFile) == 0 {
  227. destinationFile = configAccess.GetDefaultFilename()
  228. }
  229. configToWrite, err := getConfigFromFile(destinationFile)
  230. if err != nil {
  231. return err
  232. }
  233. t := *authInfo
  234. configToWrite.AuthInfos[key] = &t
  235. configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile
  236. if relativizePaths {
  237. if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil {
  238. return err
  239. }
  240. }
  241. if err := WriteToFile(*configToWrite, destinationFile); err != nil {
  242. return err
  243. }
  244. }
  245. }
  246. for key, cluster := range startingConfig.Clusters {
  247. if _, exists := newConfig.Clusters[key]; !exists {
  248. destinationFile := cluster.LocationOfOrigin
  249. if len(destinationFile) == 0 {
  250. destinationFile = configAccess.GetDefaultFilename()
  251. }
  252. configToWrite, err := getConfigFromFile(destinationFile)
  253. if err != nil {
  254. return err
  255. }
  256. delete(configToWrite.Clusters, key)
  257. if err := WriteToFile(*configToWrite, destinationFile); err != nil {
  258. return err
  259. }
  260. }
  261. }
  262. for key, context := range startingConfig.Contexts {
  263. if _, exists := newConfig.Contexts[key]; !exists {
  264. destinationFile := context.LocationOfOrigin
  265. if len(destinationFile) == 0 {
  266. destinationFile = configAccess.GetDefaultFilename()
  267. }
  268. configToWrite, err := getConfigFromFile(destinationFile)
  269. if err != nil {
  270. return err
  271. }
  272. delete(configToWrite.Contexts, key)
  273. if err := WriteToFile(*configToWrite, destinationFile); err != nil {
  274. return err
  275. }
  276. }
  277. }
  278. for key, authInfo := range startingConfig.AuthInfos {
  279. if _, exists := newConfig.AuthInfos[key]; !exists {
  280. destinationFile := authInfo.LocationOfOrigin
  281. if len(destinationFile) == 0 {
  282. destinationFile = configAccess.GetDefaultFilename()
  283. }
  284. configToWrite, err := getConfigFromFile(destinationFile)
  285. if err != nil {
  286. return err
  287. }
  288. delete(configToWrite.AuthInfos, key)
  289. if err := WriteToFile(*configToWrite, destinationFile); err != nil {
  290. return err
  291. }
  292. }
  293. }
  294. return nil
  295. }
  296. func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister {
  297. return &persister{configAccess, user}
  298. }
  299. type persister struct {
  300. configAccess ConfigAccess
  301. user string
  302. }
  303. func (p *persister) Persist(config map[string]string) error {
  304. newConfig, err := p.configAccess.GetStartingConfig()
  305. if err != nil {
  306. return err
  307. }
  308. authInfo, ok := newConfig.AuthInfos[p.user]
  309. if ok && authInfo.AuthProvider != nil {
  310. authInfo.AuthProvider.Config = config
  311. return ModifyConfig(p.configAccess, *newConfig, false)
  312. }
  313. return nil
  314. }
  315. // writeCurrentContext takes three possible paths.
  316. // If newCurrentContext is the same as the startingConfig's current context, then we exit.
  317. // If newCurrentContext has a value, then that value is written into the default destination file.
  318. // If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file
  319. func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error {
  320. if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
  321. return err
  322. } else if startingConfig.CurrentContext == newCurrentContext {
  323. return nil
  324. }
  325. if configAccess.IsExplicitFile() {
  326. file := configAccess.GetExplicitFile()
  327. currConfig, err := getConfigFromFile(file)
  328. if err != nil {
  329. return err
  330. }
  331. currConfig.CurrentContext = newCurrentContext
  332. if err := WriteToFile(*currConfig, file); err != nil {
  333. return err
  334. }
  335. return nil
  336. }
  337. if len(newCurrentContext) > 0 {
  338. destinationFile := configAccess.GetDefaultFilename()
  339. config, err := getConfigFromFile(destinationFile)
  340. if err != nil {
  341. return err
  342. }
  343. config.CurrentContext = newCurrentContext
  344. if err := WriteToFile(*config, destinationFile); err != nil {
  345. return err
  346. }
  347. return nil
  348. }
  349. // 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
  350. for _, file := range configAccess.GetLoadingPrecedence() {
  351. if _, err := os.Stat(file); err == nil {
  352. currConfig, err := getConfigFromFile(file)
  353. if err != nil {
  354. return err
  355. }
  356. if len(currConfig.CurrentContext) > 0 {
  357. currConfig.CurrentContext = newCurrentContext
  358. if err := WriteToFile(*currConfig, file); err != nil {
  359. return err
  360. }
  361. return nil
  362. }
  363. }
  364. }
  365. return errors.New("no config found to write context")
  366. }
  367. func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error {
  368. if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
  369. return err
  370. } else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) {
  371. return nil
  372. }
  373. if configAccess.IsExplicitFile() {
  374. file := configAccess.GetExplicitFile()
  375. currConfig, err := getConfigFromFile(file)
  376. if err != nil {
  377. return err
  378. }
  379. currConfig.Preferences = newPrefs
  380. if err := WriteToFile(*currConfig, file); err != nil {
  381. return err
  382. }
  383. return nil
  384. }
  385. for _, file := range configAccess.GetLoadingPrecedence() {
  386. currConfig, err := getConfigFromFile(file)
  387. if err != nil {
  388. return err
  389. }
  390. if !reflect.DeepEqual(currConfig.Preferences, newPrefs) {
  391. currConfig.Preferences = newPrefs
  392. if err := WriteToFile(*currConfig, file); err != nil {
  393. return err
  394. }
  395. return nil
  396. }
  397. }
  398. return errors.New("no config found to write preferences")
  399. }
  400. // 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.
  401. func getConfigFromFile(filename string) (*clientcmdapi.Config, error) {
  402. config, err := LoadFromFile(filename)
  403. if err != nil && !os.IsNotExist(err) {
  404. return nil, err
  405. }
  406. if config == nil {
  407. config = clientcmdapi.NewConfig()
  408. }
  409. return config, nil
  410. }
  411. // 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
  412. func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config {
  413. config, err := getConfigFromFile(filename)
  414. if err != nil {
  415. klog.FatalDepth(1, err)
  416. }
  417. return config
  418. }