| /* |
| 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 |
| } |