123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649 |
- /*
- 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 (
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "path"
- "path/filepath"
- "reflect"
- goruntime "runtime"
- "strings"
- "github.com/imdario/mergo"
- "k8s.io/klog/v2"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- utilerrors "k8s.io/apimachinery/pkg/util/errors"
- restclient "k8s.io/client-go/rest"
- clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
- clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest"
- "k8s.io/client-go/util/homedir"
- )
- const (
- RecommendedConfigPathFlag = "kubeconfig"
- RecommendedConfigPathEnvVar = "KUBECONFIG"
- RecommendedHomeDir = ".kube"
- RecommendedFileName = "config"
- RecommendedSchemaName = "schema"
- )
- var (
- RecommendedConfigDir = path.Join(homedir.HomeDir(), RecommendedHomeDir)
- RecommendedHomeFile = path.Join(RecommendedConfigDir, RecommendedFileName)
- RecommendedSchemaFile = path.Join(RecommendedConfigDir, RecommendedSchemaName)
- )
- // currentMigrationRules returns a map that holds the history of recommended home directories used in previous versions.
- // Any future changes to RecommendedHomeFile and related are expected to add a migration rule here, in order to make
- // sure existing config files are migrated to their new locations properly.
- func currentMigrationRules() map[string]string {
- oldRecommendedHomeFile := path.Join(os.Getenv("HOME"), "/.kube/.kubeconfig")
- oldRecommendedWindowsHomeFile := path.Join(os.Getenv("HOME"), RecommendedHomeDir, RecommendedFileName)
- migrationRules := map[string]string{}
- migrationRules[RecommendedHomeFile] = oldRecommendedHomeFile
- if goruntime.GOOS == "windows" {
- migrationRules[RecommendedHomeFile] = oldRecommendedWindowsHomeFile
- }
- return migrationRules
- }
- type ClientConfigLoader interface {
- ConfigAccess
- // IsDefaultConfig returns true if the returned config matches the defaults.
- IsDefaultConfig(*restclient.Config) bool
- // Load returns the latest config
- Load() (*clientcmdapi.Config, error)
- }
- type KubeconfigGetter func() (*clientcmdapi.Config, error)
- type ClientConfigGetter struct {
- kubeconfigGetter KubeconfigGetter
- }
- // ClientConfigGetter implements the ClientConfigLoader interface.
- var _ ClientConfigLoader = &ClientConfigGetter{}
- func (g *ClientConfigGetter) Load() (*clientcmdapi.Config, error) {
- return g.kubeconfigGetter()
- }
- func (g *ClientConfigGetter) GetLoadingPrecedence() []string {
- return nil
- }
- func (g *ClientConfigGetter) GetStartingConfig() (*clientcmdapi.Config, error) {
- return g.kubeconfigGetter()
- }
- func (g *ClientConfigGetter) GetDefaultFilename() string {
- return ""
- }
- func (g *ClientConfigGetter) IsExplicitFile() bool {
- return false
- }
- func (g *ClientConfigGetter) GetExplicitFile() string {
- return ""
- }
- func (g *ClientConfigGetter) IsDefaultConfig(config *restclient.Config) bool {
- return false
- }
- // ClientConfigLoadingRules is an ExplicitPath and string slice of specific locations that are used for merging together a Config
- // Callers can put the chain together however they want, but we'd recommend:
- // EnvVarPathFiles if set (a list of files if set) OR the HomeDirectoryPath
- // ExplicitPath is special, because if a user specifically requests a certain file be used and error is reported if this file is not present
- type ClientConfigLoadingRules struct {
- ExplicitPath string
- Precedence []string
- // MigrationRules is a map of destination files to source files. If a destination file is not present, then the source file is checked.
- // If the source file is present, then it is copied to the destination file BEFORE any further loading happens.
- MigrationRules map[string]string
- // DoNotResolvePaths indicates whether or not to resolve paths with respect to the originating files. This is phrased as a negative so
- // that a default object that doesn't set this will usually get the behavior it wants.
- DoNotResolvePaths bool
- // DefaultClientConfig is an optional field indicating what rules to use to calculate a default configuration.
- // This should match the overrides passed in to ClientConfig loader.
- DefaultClientConfig ClientConfig
- // WarnIfAllMissing indicates whether the configuration files pointed by KUBECONFIG environment variable are present or not.
- // In case of missing files, it warns the user about the missing files.
- WarnIfAllMissing bool
- }
- // ClientConfigLoadingRules implements the ClientConfigLoader interface.
- var _ ClientConfigLoader = &ClientConfigLoadingRules{}
- // NewDefaultClientConfigLoadingRules returns a ClientConfigLoadingRules object with default fields filled in. You are not required to
- // use this constructor
- func NewDefaultClientConfigLoadingRules() *ClientConfigLoadingRules {
- chain := []string{}
- warnIfAllMissing := false
- envVarFiles := os.Getenv(RecommendedConfigPathEnvVar)
- if len(envVarFiles) != 0 {
- fileList := filepath.SplitList(envVarFiles)
- // prevent the same path load multiple times
- chain = append(chain, deduplicate(fileList)...)
- warnIfAllMissing = true
- } else {
- chain = append(chain, RecommendedHomeFile)
- }
- return &ClientConfigLoadingRules{
- Precedence: chain,
- MigrationRules: currentMigrationRules(),
- WarnIfAllMissing: warnIfAllMissing,
- }
- }
- // Load starts by running the MigrationRules and then
- // takes the loading rules and returns a Config object based on following rules.
- // if the ExplicitPath, return the unmerged explicit file
- // Otherwise, return a merged config based on the Precedence slice
- // A missing ExplicitPath file produces an error. Empty filenames or other missing files are ignored.
- // Read errors or files with non-deserializable content produce errors.
- // The first file to set a particular map key wins and map key's value is never changed.
- // BUT, if you set a struct value that is NOT contained inside of map, the value WILL be changed.
- // This results in some odd looking logic to merge in one direction, merge in the other, and then merge the two.
- // It also means that if two files specify a "red-user", only values from the first file's red-user are used. Even
- // non-conflicting entries from the second file's "red-user" are discarded.
- // Relative paths inside of the .kubeconfig files are resolved against the .kubeconfig file's parent folder
- // and only absolute file paths are returned.
- func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) {
- if err := rules.Migrate(); err != nil {
- return nil, err
- }
- errlist := []error{}
- missingList := []string{}
- kubeConfigFiles := []string{}
- // Make sure a file we were explicitly told to use exists
- if len(rules.ExplicitPath) > 0 {
- if _, err := os.Stat(rules.ExplicitPath); os.IsNotExist(err) {
- return nil, err
- }
- kubeConfigFiles = append(kubeConfigFiles, rules.ExplicitPath)
- } else {
- kubeConfigFiles = append(kubeConfigFiles, rules.Precedence...)
- }
- kubeconfigs := []*clientcmdapi.Config{}
- // read and cache the config files so that we only look at them once
- for _, filename := range kubeConfigFiles {
- if len(filename) == 0 {
- // no work to do
- continue
- }
- config, err := LoadFromFile(filename)
- if os.IsNotExist(err) {
- // skip missing files
- // Add to the missing list to produce a warning
- missingList = append(missingList, filename)
- continue
- }
- if err != nil {
- errlist = append(errlist, fmt.Errorf("error loading config file \"%s\": %v", filename, err))
- continue
- }
- kubeconfigs = append(kubeconfigs, config)
- }
- if rules.WarnIfAllMissing && len(missingList) > 0 && len(kubeconfigs) == 0 {
- klog.Warningf("Config not found: %s", strings.Join(missingList, ", "))
- }
- // first merge all of our maps
- mapConfig := clientcmdapi.NewConfig()
- for _, kubeconfig := range kubeconfigs {
- mergo.MergeWithOverwrite(mapConfig, kubeconfig)
- }
- // merge all of the struct values in the reverse order so that priority is given correctly
- // errors are not added to the list the second time
- nonMapConfig := clientcmdapi.NewConfig()
- for i := len(kubeconfigs) - 1; i >= 0; i-- {
- kubeconfig := kubeconfigs[i]
- mergo.MergeWithOverwrite(nonMapConfig, kubeconfig)
- }
- // since values are overwritten, but maps values are not, we can merge the non-map config on top of the map config and
- // get the values we expect.
- config := clientcmdapi.NewConfig()
- mergo.MergeWithOverwrite(config, mapConfig)
- mergo.MergeWithOverwrite(config, nonMapConfig)
- if rules.ResolvePaths() {
- if err := ResolveLocalPaths(config); err != nil {
- errlist = append(errlist, err)
- }
- }
- return config, utilerrors.NewAggregate(errlist)
- }
- // Migrate uses the MigrationRules map. If a destination file is not present, then the source file is checked.
- // If the source file is present, then it is copied to the destination file BEFORE any further loading happens.
- func (rules *ClientConfigLoadingRules) Migrate() error {
- if rules.MigrationRules == nil {
- return nil
- }
- for destination, source := range rules.MigrationRules {
- if _, err := os.Stat(destination); err == nil {
- // if the destination already exists, do nothing
- continue
- } else if os.IsPermission(err) {
- // if we can't access the file, skip it
- continue
- } else if !os.IsNotExist(err) {
- // if we had an error other than non-existence, fail
- return err
- }
- if sourceInfo, err := os.Stat(source); err != nil {
- if os.IsNotExist(err) || os.IsPermission(err) {
- // if the source file doesn't exist or we can't access it, there's no work to do.
- continue
- }
- // if we had an error other than non-existence, fail
- return err
- } else if sourceInfo.IsDir() {
- return fmt.Errorf("cannot migrate %v to %v because it is a directory", source, destination)
- }
- in, err := os.Open(source)
- if err != nil {
- return err
- }
- defer in.Close()
- out, err := os.Create(destination)
- if err != nil {
- return err
- }
- defer out.Close()
- if _, err = io.Copy(out, in); err != nil {
- return err
- }
- }
- return nil
- }
- // GetLoadingPrecedence implements ConfigAccess
- func (rules *ClientConfigLoadingRules) GetLoadingPrecedence() []string {
- return rules.Precedence
- }
- // GetStartingConfig implements ConfigAccess
- func (rules *ClientConfigLoadingRules) GetStartingConfig() (*clientcmdapi.Config, error) {
- clientConfig := NewNonInteractiveDeferredLoadingClientConfig(rules, &ConfigOverrides{})
- rawConfig, err := clientConfig.RawConfig()
- if os.IsNotExist(err) {
- return clientcmdapi.NewConfig(), nil
- }
- if err != nil {
- return nil, err
- }
- return &rawConfig, nil
- }
- // GetDefaultFilename implements ConfigAccess
- func (rules *ClientConfigLoadingRules) GetDefaultFilename() string {
- // Explicit file if we have one.
- if rules.IsExplicitFile() {
- return rules.GetExplicitFile()
- }
- // Otherwise, first existing file from precedence.
- for _, filename := range rules.GetLoadingPrecedence() {
- if _, err := os.Stat(filename); err == nil {
- return filename
- }
- }
- // If none exists, use the first from precedence.
- if len(rules.Precedence) > 0 {
- return rules.Precedence[0]
- }
- return ""
- }
- // IsExplicitFile implements ConfigAccess
- func (rules *ClientConfigLoadingRules) IsExplicitFile() bool {
- return len(rules.ExplicitPath) > 0
- }
- // GetExplicitFile implements ConfigAccess
- func (rules *ClientConfigLoadingRules) GetExplicitFile() string {
- return rules.ExplicitPath
- }
- // IsDefaultConfig returns true if the provided configuration matches the default
- func (rules *ClientConfigLoadingRules) IsDefaultConfig(config *restclient.Config) bool {
- if rules.DefaultClientConfig == nil {
- return false
- }
- defaultConfig, err := rules.DefaultClientConfig.ClientConfig()
- if err != nil {
- return false
- }
- return reflect.DeepEqual(config, defaultConfig)
- }
- // LoadFromFile takes a filename and deserializes the contents into Config object
- func LoadFromFile(filename string) (*clientcmdapi.Config, error) {
- kubeconfigBytes, err := ioutil.ReadFile(filename)
- if err != nil {
- return nil, err
- }
- config, err := Load(kubeconfigBytes)
- if err != nil {
- return nil, err
- }
- klog.V(6).Infoln("Config loaded from file: ", filename)
- // set LocationOfOrigin on every Cluster, User, and Context
- for key, obj := range config.AuthInfos {
- obj.LocationOfOrigin = filename
- config.AuthInfos[key] = obj
- }
- for key, obj := range config.Clusters {
- obj.LocationOfOrigin = filename
- config.Clusters[key] = obj
- }
- for key, obj := range config.Contexts {
- obj.LocationOfOrigin = filename
- config.Contexts[key] = obj
- }
- if config.AuthInfos == nil {
- config.AuthInfos = map[string]*clientcmdapi.AuthInfo{}
- }
- if config.Clusters == nil {
- config.Clusters = map[string]*clientcmdapi.Cluster{}
- }
- if config.Contexts == nil {
- config.Contexts = map[string]*clientcmdapi.Context{}
- }
- return config, nil
- }
- // Load takes a byte slice and deserializes the contents into Config object.
- // Encapsulates deserialization without assuming the source is a file.
- func Load(data []byte) (*clientcmdapi.Config, error) {
- config := clientcmdapi.NewConfig()
- // if there's no data in a file, return the default object instead of failing (DecodeInto reject empty input)
- if len(data) == 0 {
- return config, nil
- }
- decoded, _, err := clientcmdlatest.Codec.Decode(data, &schema.GroupVersionKind{Version: clientcmdlatest.Version, Kind: "Config"}, config)
- if err != nil {
- return nil, err
- }
- return decoded.(*clientcmdapi.Config), nil
- }
- // WriteToFile serializes the config to yaml and writes it out to a file. If not present, it creates the file with the mode 0600. If it is present
- // it stomps the contents
- func WriteToFile(config clientcmdapi.Config, filename string) error {
- content, err := Write(config)
- if err != nil {
- return err
- }
- dir := filepath.Dir(filename)
- if _, err := os.Stat(dir); os.IsNotExist(err) {
- if err = os.MkdirAll(dir, 0755); err != nil {
- return err
- }
- }
- if err := ioutil.WriteFile(filename, content, 0600); err != nil {
- return err
- }
- return nil
- }
- func lockFile(filename string) error {
- // TODO: find a way to do this with actual file locks. Will
- // probably need separate solution for windows and Linux.
- // Make sure the dir exists before we try to create a lock file.
- dir := filepath.Dir(filename)
- if _, err := os.Stat(dir); os.IsNotExist(err) {
- if err = os.MkdirAll(dir, 0755); err != nil {
- return err
- }
- }
- f, err := os.OpenFile(lockName(filename), os.O_CREATE|os.O_EXCL, 0)
- if err != nil {
- return err
- }
- f.Close()
- return nil
- }
- func unlockFile(filename string) error {
- return os.Remove(lockName(filename))
- }
- func lockName(filename string) string {
- return filename + ".lock"
- }
- // Write serializes the config to yaml.
- // Encapsulates serialization without assuming the destination is a file.
- func Write(config clientcmdapi.Config) ([]byte, error) {
- return runtime.Encode(clientcmdlatest.Codec, &config)
- }
- func (rules ClientConfigLoadingRules) ResolvePaths() bool {
- return !rules.DoNotResolvePaths
- }
- // ResolveLocalPaths resolves all relative paths in the config object with respect to the stanza's LocationOfOrigin
- // this cannot be done directly inside of LoadFromFile because doing so there would make it impossible to load a file without
- // modification of its contents.
- func ResolveLocalPaths(config *clientcmdapi.Config) error {
- for _, cluster := range config.Clusters {
- if len(cluster.LocationOfOrigin) == 0 {
- continue
- }
- base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin))
- if err != nil {
- return fmt.Errorf("could not determine the absolute path of config file %s: %v", cluster.LocationOfOrigin, err)
- }
- if err := ResolvePaths(GetClusterFileReferences(cluster), base); err != nil {
- return err
- }
- }
- for _, authInfo := range config.AuthInfos {
- if len(authInfo.LocationOfOrigin) == 0 {
- continue
- }
- base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin))
- if err != nil {
- return fmt.Errorf("could not determine the absolute path of config file %s: %v", authInfo.LocationOfOrigin, err)
- }
- if err := ResolvePaths(GetAuthInfoFileReferences(authInfo), base); err != nil {
- return err
- }
- }
- return nil
- }
- // RelativizeClusterLocalPaths first absolutizes the paths by calling ResolveLocalPaths. This assumes that any NEW path is already
- // absolute, but any existing path will be resolved relative to LocationOfOrigin
- func RelativizeClusterLocalPaths(cluster *clientcmdapi.Cluster) error {
- if len(cluster.LocationOfOrigin) == 0 {
- return fmt.Errorf("no location of origin for %s", cluster.Server)
- }
- base, err := filepath.Abs(filepath.Dir(cluster.LocationOfOrigin))
- if err != nil {
- return fmt.Errorf("could not determine the absolute path of config file %s: %v", cluster.LocationOfOrigin, err)
- }
- if err := ResolvePaths(GetClusterFileReferences(cluster), base); err != nil {
- return err
- }
- if err := RelativizePathWithNoBacksteps(GetClusterFileReferences(cluster), base); err != nil {
- return err
- }
- return nil
- }
- // RelativizeAuthInfoLocalPaths first absolutizes the paths by calling ResolveLocalPaths. This assumes that any NEW path is already
- // absolute, but any existing path will be resolved relative to LocationOfOrigin
- func RelativizeAuthInfoLocalPaths(authInfo *clientcmdapi.AuthInfo) error {
- if len(authInfo.LocationOfOrigin) == 0 {
- return fmt.Errorf("no location of origin for %v", authInfo)
- }
- base, err := filepath.Abs(filepath.Dir(authInfo.LocationOfOrigin))
- if err != nil {
- return fmt.Errorf("could not determine the absolute path of config file %s: %v", authInfo.LocationOfOrigin, err)
- }
- if err := ResolvePaths(GetAuthInfoFileReferences(authInfo), base); err != nil {
- return err
- }
- if err := RelativizePathWithNoBacksteps(GetAuthInfoFileReferences(authInfo), base); err != nil {
- return err
- }
- return nil
- }
- func RelativizeConfigPaths(config *clientcmdapi.Config, base string) error {
- return RelativizePathWithNoBacksteps(GetConfigFileReferences(config), base)
- }
- func ResolveConfigPaths(config *clientcmdapi.Config, base string) error {
- return ResolvePaths(GetConfigFileReferences(config), base)
- }
- func GetConfigFileReferences(config *clientcmdapi.Config) []*string {
- refs := []*string{}
- for _, cluster := range config.Clusters {
- refs = append(refs, GetClusterFileReferences(cluster)...)
- }
- for _, authInfo := range config.AuthInfos {
- refs = append(refs, GetAuthInfoFileReferences(authInfo)...)
- }
- return refs
- }
- func GetClusterFileReferences(cluster *clientcmdapi.Cluster) []*string {
- return []*string{&cluster.CertificateAuthority}
- }
- func GetAuthInfoFileReferences(authInfo *clientcmdapi.AuthInfo) []*string {
- s := []*string{&authInfo.ClientCertificate, &authInfo.ClientKey, &authInfo.TokenFile}
- // Only resolve exec command if it isn't PATH based.
- if authInfo.Exec != nil && strings.ContainsRune(authInfo.Exec.Command, filepath.Separator) {
- s = append(s, &authInfo.Exec.Command)
- }
- return s
- }
- // ResolvePaths updates the given refs to be absolute paths, relative to the given base directory
- func ResolvePaths(refs []*string, base string) error {
- for _, ref := range refs {
- // Don't resolve empty paths
- if len(*ref) > 0 {
- // Don't resolve absolute paths
- if !filepath.IsAbs(*ref) {
- *ref = filepath.Join(base, *ref)
- }
- }
- }
- return nil
- }
- // RelativizePathWithNoBacksteps updates the given refs to be relative paths, relative to the given base directory as long as they do not require backsteps.
- // Any path requiring a backstep is left as-is as long it is absolute. Any non-absolute path that can't be relativized produces an error
- func RelativizePathWithNoBacksteps(refs []*string, base string) error {
- for _, ref := range refs {
- // Don't relativize empty paths
- if len(*ref) > 0 {
- rel, err := MakeRelative(*ref, base)
- if err != nil {
- return err
- }
- // if we have a backstep, don't mess with the path
- if strings.HasPrefix(rel, "../") {
- if filepath.IsAbs(*ref) {
- continue
- }
- return fmt.Errorf("%v requires backsteps and is not absolute", *ref)
- }
- *ref = rel
- }
- }
- return nil
- }
- func MakeRelative(path, base string) (string, error) {
- if len(path) > 0 {
- rel, err := filepath.Rel(base, path)
- if err != nil {
- return path, err
- }
- return rel, nil
- }
- return path, nil
- }
- // deduplicate removes any duplicated values and returns a new slice, keeping the order unchanged
- func deduplicate(s []string) []string {
- encountered := map[string]bool{}
- ret := make([]string, 0)
- for i := range s {
- if encountered[s[i]] {
- continue
- }
- encountered[s[i]] = true
- ret = append(ret, s[i])
- }
- return ret
- }
|