config.go 15 KB

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