vendor: k8s.io/client-go/tools/clientcmd/...@v3.0.0-beta.0
This commit is contained in:
parent
dc0713b248
commit
59c8c1d020
|
@ -0,0 +1,23 @@
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
|
"go_library",
|
||||||
|
"go_test",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = ["clientauth.go"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = ["//vendor/k8s.io/client-go/rest:go_default_library"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "go_default_xtest",
|
||||||
|
srcs = ["clientauth_test.go"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = ["//vendor/k8s.io/client-go/tools/auth:go_default_library"],
|
||||||
|
)
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
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 auth defines a file format for holding authentication
|
||||||
|
information needed by clients of Kubernetes. Typically,
|
||||||
|
a Kubernetes cluster will put auth info for the admin in a known
|
||||||
|
location when it is created, and will (soon) put it in a known
|
||||||
|
location within a Container's file tree for Containers that
|
||||||
|
need access to the Kubernetes API.
|
||||||
|
|
||||||
|
Having a defined format allows:
|
||||||
|
- clients to be implmented in multiple languages
|
||||||
|
- applications which link clients to be portable across
|
||||||
|
clusters with different authentication styles (e.g.
|
||||||
|
some may use SSL Client certs, others may not, etc)
|
||||||
|
- when the format changes, applications only
|
||||||
|
need to update this code.
|
||||||
|
|
||||||
|
The file format is json, marshalled from a struct authcfg.Info.
|
||||||
|
|
||||||
|
Clinet libraries in other languages should use the same format.
|
||||||
|
|
||||||
|
It is not intended to store general preferences, such as default
|
||||||
|
namespace, output options, etc. CLIs (such as kubectl) and UIs should
|
||||||
|
develop their own format and may wish to inline the authcfg.Info type.
|
||||||
|
|
||||||
|
The authcfg.Info is just a file format. It is distinct from
|
||||||
|
client.Config which holds options for creating a client.Client.
|
||||||
|
Helper functions are provided in this package to fill in a
|
||||||
|
client.Client from an authcfg.Info.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
import (
|
||||||
|
"pkg/client"
|
||||||
|
"pkg/client/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
info, err := auth.LoadFromFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
clientConfig = client.Config{}
|
||||||
|
clientConfig.Host = "example.com:4901"
|
||||||
|
clientConfig = info.MergeWithConfig()
|
||||||
|
client := client.New(clientConfig)
|
||||||
|
client.Pods(ns).List()
|
||||||
|
*/
|
||||||
|
package auth
|
||||||
|
|
||||||
|
// TODO: need a way to rotate Tokens. Therefore, need a way for client object to be reset when the authcfg is updated.
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info holds Kubernetes API authorization config. It is intended
|
||||||
|
// to be read/written from a file as a JSON object.
|
||||||
|
type Info struct {
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
CAFile string
|
||||||
|
CertFile string
|
||||||
|
KeyFile string
|
||||||
|
BearerToken string
|
||||||
|
Insecure *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromFile parses an Info object from a file path.
|
||||||
|
// If the file does not exist, then os.IsNotExist(err) == true
|
||||||
|
func LoadFromFile(path string) (*Info, error) {
|
||||||
|
var info Info
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeWithConfig returns a copy of a client.Config with values from the Info.
|
||||||
|
// The fields of client.Config with a corresponding field in the Info are set
|
||||||
|
// with the value from the Info.
|
||||||
|
func (info Info) MergeWithConfig(c restclient.Config) (restclient.Config, error) {
|
||||||
|
var config restclient.Config = c
|
||||||
|
config.Username = info.User
|
||||||
|
config.Password = info.Password
|
||||||
|
config.CAFile = info.CAFile
|
||||||
|
config.CertFile = info.CertFile
|
||||||
|
config.KeyFile = info.KeyFile
|
||||||
|
config.BearerToken = info.BearerToken
|
||||||
|
if info.Insecure != nil {
|
||||||
|
config.Insecure = *info.Insecure
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info Info) Complete() bool {
|
||||||
|
return len(info.User) > 0 ||
|
||||||
|
len(info.CertFile) > 0 ||
|
||||||
|
len(info.BearerToken) > 0
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
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 latest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
|
||||||
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
"k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version is the string that represents the current external default version.
|
||||||
|
const Version = "v1"
|
||||||
|
|
||||||
|
var ExternalVersion = schema.GroupVersion{Group: "", Version: "v1"}
|
||||||
|
|
||||||
|
// OldestVersion is the string that represents the oldest server version supported,
|
||||||
|
// for client code that wants to hardcode the lowest common denominator.
|
||||||
|
const OldestVersion = "v1"
|
||||||
|
|
||||||
|
// Versions is the list of versions that are recognized in code. The order provided
|
||||||
|
// may be assumed to be least feature rich to most feature rich, and clients may
|
||||||
|
// choose to prefer the latter items in the list over the former items when presented
|
||||||
|
// with a set of versions to choose.
|
||||||
|
var Versions = []string{"v1"}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Codec runtime.Codec
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Scheme = runtime.NewScheme()
|
||||||
|
if err := api.AddToScheme(Scheme); err != nil {
|
||||||
|
// Programmer error, detect immediately
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := v1.AddToScheme(Scheme); err != nil {
|
||||||
|
// Programmer error, detect immediately
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
yamlSerializer := json.NewYAMLSerializer(json.DefaultMetaFactory, Scheme, Scheme)
|
||||||
|
Codec = versioning.NewDefaultingCodecForScheme(
|
||||||
|
Scheme,
|
||||||
|
yamlSerializer,
|
||||||
|
yamlSerializer,
|
||||||
|
schema.GroupVersion{Version: Version},
|
||||||
|
runtime.InternalGroupVersioner,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
/*
|
||||||
|
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 v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/conversion"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addConversionFuncs(scheme *runtime.Scheme) error {
|
||||||
|
return scheme.AddConversionFuncs(
|
||||||
|
func(in *Cluster, out *api.Cluster, s conversion.Scope) error {
|
||||||
|
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
|
||||||
|
},
|
||||||
|
func(in *api.Cluster, out *Cluster, s conversion.Scope) error {
|
||||||
|
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
|
||||||
|
},
|
||||||
|
func(in *Preferences, out *api.Preferences, s conversion.Scope) error {
|
||||||
|
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
|
||||||
|
},
|
||||||
|
func(in *api.Preferences, out *Preferences, s conversion.Scope) error {
|
||||||
|
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
|
||||||
|
},
|
||||||
|
func(in *AuthInfo, out *api.AuthInfo, s conversion.Scope) error {
|
||||||
|
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
|
||||||
|
},
|
||||||
|
func(in *api.AuthInfo, out *AuthInfo, s conversion.Scope) error {
|
||||||
|
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
|
||||||
|
},
|
||||||
|
func(in *Context, out *api.Context, s conversion.Scope) error {
|
||||||
|
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
|
||||||
|
},
|
||||||
|
func(in *api.Context, out *Context, s conversion.Scope) error {
|
||||||
|
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
|
||||||
|
},
|
||||||
|
|
||||||
|
func(in *Config, out *api.Config, s conversion.Scope) error {
|
||||||
|
out.CurrentContext = in.CurrentContext
|
||||||
|
if err := s.Convert(&in.Preferences, &out.Preferences, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Clusters = make(map[string]*api.Cluster)
|
||||||
|
if err := s.Convert(&in.Clusters, &out.Clusters, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.AuthInfos = make(map[string]*api.AuthInfo)
|
||||||
|
if err := s.Convert(&in.AuthInfos, &out.AuthInfos, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.Contexts = make(map[string]*api.Context)
|
||||||
|
if err := s.Convert(&in.Contexts, &out.Contexts, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.Extensions = make(map[string]runtime.Object)
|
||||||
|
if err := s.Convert(&in.Extensions, &out.Extensions, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *api.Config, out *Config, s conversion.Scope) error {
|
||||||
|
out.CurrentContext = in.CurrentContext
|
||||||
|
if err := s.Convert(&in.Preferences, &out.Preferences, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Clusters = make([]NamedCluster, 0, 0)
|
||||||
|
if err := s.Convert(&in.Clusters, &out.Clusters, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.AuthInfos = make([]NamedAuthInfo, 0, 0)
|
||||||
|
if err := s.Convert(&in.AuthInfos, &out.AuthInfos, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.Contexts = make([]NamedContext, 0, 0)
|
||||||
|
if err := s.Convert(&in.Contexts, &out.Contexts, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.Extensions = make([]NamedExtension, 0, 0)
|
||||||
|
if err := s.Convert(&in.Extensions, &out.Extensions, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *[]NamedCluster, out *map[string]*api.Cluster, s conversion.Scope) error {
|
||||||
|
for _, curr := range *in {
|
||||||
|
newCluster := api.NewCluster()
|
||||||
|
if err := s.Convert(&curr.Cluster, newCluster, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*out)[curr.Name] = newCluster
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *map[string]*api.Cluster, out *[]NamedCluster, s conversion.Scope) error {
|
||||||
|
allKeys := make([]string, 0, len(*in))
|
||||||
|
for key := range *in {
|
||||||
|
allKeys = append(allKeys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(allKeys)
|
||||||
|
|
||||||
|
for _, key := range allKeys {
|
||||||
|
newCluster := (*in)[key]
|
||||||
|
oldCluster := &Cluster{}
|
||||||
|
if err := s.Convert(newCluster, oldCluster, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
namedCluster := NamedCluster{key, *oldCluster}
|
||||||
|
*out = append(*out, namedCluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *[]NamedAuthInfo, out *map[string]*api.AuthInfo, s conversion.Scope) error {
|
||||||
|
for _, curr := range *in {
|
||||||
|
newAuthInfo := api.NewAuthInfo()
|
||||||
|
if err := s.Convert(&curr.AuthInfo, newAuthInfo, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*out)[curr.Name] = newAuthInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *map[string]*api.AuthInfo, out *[]NamedAuthInfo, s conversion.Scope) error {
|
||||||
|
allKeys := make([]string, 0, len(*in))
|
||||||
|
for key := range *in {
|
||||||
|
allKeys = append(allKeys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(allKeys)
|
||||||
|
|
||||||
|
for _, key := range allKeys {
|
||||||
|
newAuthInfo := (*in)[key]
|
||||||
|
oldAuthInfo := &AuthInfo{}
|
||||||
|
if err := s.Convert(newAuthInfo, oldAuthInfo, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
namedAuthInfo := NamedAuthInfo{key, *oldAuthInfo}
|
||||||
|
*out = append(*out, namedAuthInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *[]NamedContext, out *map[string]*api.Context, s conversion.Scope) error {
|
||||||
|
for _, curr := range *in {
|
||||||
|
newContext := api.NewContext()
|
||||||
|
if err := s.Convert(&curr.Context, newContext, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*out)[curr.Name] = newContext
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *map[string]*api.Context, out *[]NamedContext, s conversion.Scope) error {
|
||||||
|
allKeys := make([]string, 0, len(*in))
|
||||||
|
for key := range *in {
|
||||||
|
allKeys = append(allKeys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(allKeys)
|
||||||
|
|
||||||
|
for _, key := range allKeys {
|
||||||
|
newContext := (*in)[key]
|
||||||
|
oldContext := &Context{}
|
||||||
|
if err := s.Convert(newContext, oldContext, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
namedContext := NamedContext{key, *oldContext}
|
||||||
|
*out = append(*out, namedContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *[]NamedExtension, out *map[string]runtime.Object, s conversion.Scope) error {
|
||||||
|
for _, curr := range *in {
|
||||||
|
var newExtension runtime.Object
|
||||||
|
if err := s.Convert(&curr.Extension, &newExtension, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*out)[curr.Name] = newExtension
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(in *map[string]runtime.Object, out *[]NamedExtension, s conversion.Scope) error {
|
||||||
|
allKeys := make([]string, 0, len(*in))
|
||||||
|
for key := range *in {
|
||||||
|
allKeys = append(allKeys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(allKeys)
|
||||||
|
|
||||||
|
for _, key := range allKeys {
|
||||||
|
newExtension := (*in)[key]
|
||||||
|
oldExtension := &runtime.RawExtension{}
|
||||||
|
if err := s.Convert(newExtension, oldExtension, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
namedExtension := NamedExtension{key, *oldExtension}
|
||||||
|
*out = append(*out, namedExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
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 v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SchemeGroupVersion is group version used to register these objects
|
||||||
|
// TODO this should be in the "kubeconfig" group
|
||||||
|
var SchemeGroupVersion = schema.GroupVersion{Group: "", Version: "v1"}
|
||||||
|
|
||||||
|
var (
|
||||||
|
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes, addConversionFuncs)
|
||||||
|
AddToScheme = SchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
|
|
||||||
|
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||||
|
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||||
|
&Config{},
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *Config) GetObjectKind() schema.ObjectKind { return obj }
|
||||||
|
func (obj *Config) SetGroupVersionKind(gvk schema.GroupVersionKind) {
|
||||||
|
obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind()
|
||||||
|
}
|
||||||
|
func (obj *Config) GroupVersionKind() schema.GroupVersionKind {
|
||||||
|
return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind)
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
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 v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Where possible, json tags match the cli argument names.
|
||||||
|
// Top level config objects and all values required for proper functioning are not "omitempty". Any truly optional piece of config is allowed to be omitted.
|
||||||
|
|
||||||
|
// Config holds the information needed to build connect to remote kubernetes clusters as a given user
|
||||||
|
type Config struct {
|
||||||
|
// Legacy field from pkg/api/types.go TypeMeta.
|
||||||
|
// TODO(jlowdermilk): remove this after eliminating downstream dependencies.
|
||||||
|
// +optional
|
||||||
|
Kind string `json:"kind,omitempty"`
|
||||||
|
// DEPRECATED: APIVersion is the preferred api version for communicating with the kubernetes cluster (v1, v2, etc).
|
||||||
|
// Because a cluster can run multiple API groups and potentially multiple versions of each, it no longer makes sense to specify
|
||||||
|
// a single value for the cluster version.
|
||||||
|
// This field isn't really needed anyway, so we are deprecating it without replacement.
|
||||||
|
// It will be ignored if it is present.
|
||||||
|
// +optional
|
||||||
|
APIVersion string `json:"apiVersion,omitempty"`
|
||||||
|
// Preferences holds general information to be use for cli interactions
|
||||||
|
Preferences Preferences `json:"preferences"`
|
||||||
|
// Clusters is a map of referencable names to cluster configs
|
||||||
|
Clusters []NamedCluster `json:"clusters"`
|
||||||
|
// AuthInfos is a map of referencable names to user configs
|
||||||
|
AuthInfos []NamedAuthInfo `json:"users"`
|
||||||
|
// Contexts is a map of referencable names to context configs
|
||||||
|
Contexts []NamedContext `json:"contexts"`
|
||||||
|
// CurrentContext is the name of the context that you would like to use by default
|
||||||
|
CurrentContext string `json:"current-context"`
|
||||||
|
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
||||||
|
// +optional
|
||||||
|
Extensions []NamedExtension `json:"extensions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Preferences struct {
|
||||||
|
// +optional
|
||||||
|
Colors bool `json:"colors,omitempty"`
|
||||||
|
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
||||||
|
// +optional
|
||||||
|
Extensions []NamedExtension `json:"extensions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cluster contains information about how to communicate with a kubernetes cluster
|
||||||
|
type Cluster struct {
|
||||||
|
// Server is the address of the kubernetes cluster (https://hostname:port).
|
||||||
|
Server string `json:"server"`
|
||||||
|
// APIVersion is the preferred api version for communicating with the kubernetes cluster (v1, v2, etc).
|
||||||
|
// +optional
|
||||||
|
APIVersion string `json:"api-version,omitempty"`
|
||||||
|
// InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
|
||||||
|
// +optional
|
||||||
|
InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"`
|
||||||
|
// CertificateAuthority is the path to a cert file for the certificate authority.
|
||||||
|
// +optional
|
||||||
|
CertificateAuthority string `json:"certificate-authority,omitempty"`
|
||||||
|
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
|
||||||
|
// +optional
|
||||||
|
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
|
||||||
|
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
||||||
|
// +optional
|
||||||
|
Extensions []NamedExtension `json:"extensions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
|
||||||
|
type AuthInfo struct {
|
||||||
|
// ClientCertificate is the path to a client cert file for TLS.
|
||||||
|
// +optional
|
||||||
|
ClientCertificate string `json:"client-certificate,omitempty"`
|
||||||
|
// ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate
|
||||||
|
// +optional
|
||||||
|
ClientCertificateData []byte `json:"client-certificate-data,omitempty"`
|
||||||
|
// ClientKey is the path to a client key file for TLS.
|
||||||
|
// +optional
|
||||||
|
ClientKey string `json:"client-key,omitempty"`
|
||||||
|
// ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey
|
||||||
|
// +optional
|
||||||
|
ClientKeyData []byte `json:"client-key-data,omitempty"`
|
||||||
|
// Token is the bearer token for authentication to the kubernetes cluster.
|
||||||
|
// +optional
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
// TokenFile is a pointer to a file that contains a bearer token (as described above). If both Token and TokenFile are present, Token takes precedence.
|
||||||
|
// +optional
|
||||||
|
TokenFile string `json:"tokenFile,omitempty"`
|
||||||
|
// Impersonate is the username to imperonate. The name matches the flag.
|
||||||
|
// +optional
|
||||||
|
Impersonate string `json:"as,omitempty"`
|
||||||
|
// Username is the username for basic authentication to the kubernetes cluster.
|
||||||
|
// +optional
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
// Password is the password for basic authentication to the kubernetes cluster.
|
||||||
|
// +optional
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
// AuthProvider specifies a custom authentication plugin for the kubernetes cluster.
|
||||||
|
// +optional
|
||||||
|
AuthProvider *AuthProviderConfig `json:"auth-provider,omitempty"`
|
||||||
|
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
||||||
|
// +optional
|
||||||
|
Extensions []NamedExtension `json:"extensions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with)
|
||||||
|
type Context struct {
|
||||||
|
// Cluster is the name of the cluster for this context
|
||||||
|
Cluster string `json:"cluster"`
|
||||||
|
// AuthInfo is the name of the authInfo for this context
|
||||||
|
AuthInfo string `json:"user"`
|
||||||
|
// Namespace is the default namespace to use on unspecified requests
|
||||||
|
// +optional
|
||||||
|
Namespace string `json:"namespace,omitempty"`
|
||||||
|
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
||||||
|
// +optional
|
||||||
|
Extensions []NamedExtension `json:"extensions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamedCluster relates nicknames to cluster information
|
||||||
|
type NamedCluster struct {
|
||||||
|
// Name is the nickname for this Cluster
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Cluster holds the cluster information
|
||||||
|
Cluster Cluster `json:"cluster"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamedContext relates nicknames to context information
|
||||||
|
type NamedContext struct {
|
||||||
|
// Name is the nickname for this Context
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Context holds the context information
|
||||||
|
Context Context `json:"context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamedAuthInfo relates nicknames to auth information
|
||||||
|
type NamedAuthInfo struct {
|
||||||
|
// Name is the nickname for this AuthInfo
|
||||||
|
Name string `json:"name"`
|
||||||
|
// AuthInfo holds the auth information
|
||||||
|
AuthInfo AuthInfo `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamedExtension relates nicknames to extension information
|
||||||
|
type NamedExtension struct {
|
||||||
|
// Name is the nickname for this Extension
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Extension holds the extension information
|
||||||
|
Extension runtime.RawExtension `json:"extension"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthProviderConfig holds the configuration for a specified auth provider.
|
||||||
|
type AuthProviderConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Config map[string]string `json:"config"`
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/howeyc/gopass"
|
||||||
|
clientauth "k8s.io/client-go/tools/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthLoaders are used to build clientauth.Info objects.
|
||||||
|
type AuthLoader interface {
|
||||||
|
// LoadAuth takes a path to a config file and can then do anything it needs in order to return a valid clientauth.Info
|
||||||
|
LoadAuth(path string) (*clientauth.Info, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default implementation of an AuthLoader
|
||||||
|
type defaultAuthLoader struct{}
|
||||||
|
|
||||||
|
// LoadAuth for defaultAuthLoader simply delegates to clientauth.LoadFromFile
|
||||||
|
func (*defaultAuthLoader) LoadAuth(path string) (*clientauth.Info, error) {
|
||||||
|
return clientauth.LoadFromFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PromptingAuthLoader struct {
|
||||||
|
reader io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAuth parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist.
|
||||||
|
func (a *PromptingAuthLoader) LoadAuth(path string) (*clientauth.Info, error) {
|
||||||
|
// Prompt for user/pass and write a file if none exists.
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
authPtr, err := a.Prompt()
|
||||||
|
auth := *authPtr
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(auth)
|
||||||
|
if err != nil {
|
||||||
|
return &auth, err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(path, data, 0600)
|
||||||
|
return &auth, err
|
||||||
|
}
|
||||||
|
authPtr, err := clientauth.LoadFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return authPtr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt pulls the user and password from a reader
|
||||||
|
func (a *PromptingAuthLoader) Prompt() (*clientauth.Info, error) {
|
||||||
|
var err error
|
||||||
|
auth := &clientauth.Info{}
|
||||||
|
auth.User, err = promptForString("Username", a.reader, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
auth.Password, err = promptForString("Password", nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptForString(field string, r io.Reader, show bool) (result string, err error) {
|
||||||
|
fmt.Printf("Please enter %s: ", field)
|
||||||
|
if show {
|
||||||
|
_, err = fmt.Fscan(r, &result)
|
||||||
|
} else {
|
||||||
|
var data []byte
|
||||||
|
data, err = gopass.GetPasswdMasked()
|
||||||
|
result = string(data)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPromptingAuthLoader is an AuthLoader that parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist.
|
||||||
|
func NewPromptingAuthLoader(reader io.Reader) *PromptingAuthLoader {
|
||||||
|
return &PromptingAuthLoader{reader}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultAuthLoader returns a default implementation of an AuthLoader that only reads from a config file
|
||||||
|
func NewDefaultAuthLoader() AuthLoader {
|
||||||
|
return &defaultAuthLoader{}
|
||||||
|
}
|
|
@ -0,0 +1,537 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
"github.com/imdario/mergo"
|
||||||
|
|
||||||
|
"k8s.io/client-go/pkg/api"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
clientauth "k8s.io/client-go/tools/auth"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ClusterDefaults has the same behavior as the old EnvVar and DefaultCluster fields
|
||||||
|
// DEPRECATED will be replaced
|
||||||
|
ClusterDefaults = clientcmdapi.Cluster{Server: getDefaultServer()}
|
||||||
|
// DefaultClientConfig represents the legacy behavior of this package for defaulting
|
||||||
|
// DEPRECATED will be replace
|
||||||
|
DefaultClientConfig = DirectClientConfig{*clientcmdapi.NewConfig(), "", &ConfigOverrides{
|
||||||
|
ClusterDefaults: ClusterDefaults,
|
||||||
|
}, nil, NewDefaultClientConfigLoadingRules(), promptedCredentials{}}
|
||||||
|
)
|
||||||
|
|
||||||
|
// getDefaultServer returns a default setting for DefaultClientConfig
|
||||||
|
// DEPRECATED
|
||||||
|
func getDefaultServer() string {
|
||||||
|
if server := os.Getenv("KUBERNETES_MASTER"); len(server) > 0 {
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
return "http://localhost:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConfig is used to make it easy to get an api server client
|
||||||
|
type ClientConfig interface {
|
||||||
|
// RawConfig returns the merged result of all overrides
|
||||||
|
RawConfig() (clientcmdapi.Config, error)
|
||||||
|
// ClientConfig returns a complete client config
|
||||||
|
ClientConfig() (*restclient.Config, error)
|
||||||
|
// Namespace returns the namespace resulting from the merged
|
||||||
|
// result of all overrides and a boolean indicating if it was
|
||||||
|
// overridden
|
||||||
|
Namespace() (string, bool, error)
|
||||||
|
// ConfigAccess returns the rules for loading/persisting the config.
|
||||||
|
ConfigAccess() ConfigAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersistAuthProviderConfigForUser func(user string) restclient.AuthProviderConfigPersister
|
||||||
|
|
||||||
|
type promptedCredentials struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectClientConfig is a ClientConfig interface that is backed by a clientcmdapi.Config, options overrides, and an optional fallbackReader for auth information
|
||||||
|
type DirectClientConfig struct {
|
||||||
|
config clientcmdapi.Config
|
||||||
|
contextName string
|
||||||
|
overrides *ConfigOverrides
|
||||||
|
fallbackReader io.Reader
|
||||||
|
configAccess ConfigAccess
|
||||||
|
// promptedCredentials store the credentials input by the user
|
||||||
|
promptedCredentials promptedCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultClientConfig creates a DirectClientConfig using the config.CurrentContext as the context name
|
||||||
|
func NewDefaultClientConfig(config clientcmdapi.Config, overrides *ConfigOverrides) ClientConfig {
|
||||||
|
return &DirectClientConfig{config, config.CurrentContext, overrides, nil, NewDefaultClientConfigLoadingRules(), promptedCredentials{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNonInteractiveClientConfig creates a DirectClientConfig using the passed context name and does not have a fallback reader for auth information
|
||||||
|
func NewNonInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, configAccess ConfigAccess) ClientConfig {
|
||||||
|
return &DirectClientConfig{config, contextName, overrides, nil, configAccess, promptedCredentials{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInteractiveClientConfig creates a DirectClientConfig using the passed context name and a reader in case auth information is not provided via files or flags
|
||||||
|
func NewInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader, configAccess ConfigAccess) ClientConfig {
|
||||||
|
return &DirectClientConfig{config, contextName, overrides, fallbackReader, configAccess, promptedCredentials{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *DirectClientConfig) RawConfig() (clientcmdapi.Config, error) {
|
||||||
|
return config.config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConfig implements ClientConfig
|
||||||
|
func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
|
||||||
|
// check that getAuthInfo, getContext, and getCluster do not return an error.
|
||||||
|
// Do this before checking if the curent config is usable in the event that an
|
||||||
|
// AuthInfo, Context, or Cluster config with user-defined names are not found.
|
||||||
|
// This provides a user with the immediate cause for error if one is found
|
||||||
|
configAuthInfo, err := config.getAuthInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = config.getContext()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configClusterInfo, err := config.getCluster()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.ConfirmUsable(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientConfig := &restclient.Config{}
|
||||||
|
clientConfig.Host = configClusterInfo.Server
|
||||||
|
|
||||||
|
if len(config.overrides.Timeout) > 0 {
|
||||||
|
timeout, err := ParseTimeout(config.overrides.Timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientConfig.Timeout = timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, err := url.ParseRequestURI(clientConfig.Host); err == nil && u.Opaque == "" && len(u.Path) > 1 {
|
||||||
|
u.RawQuery = ""
|
||||||
|
u.Fragment = ""
|
||||||
|
clientConfig.Host = u.String()
|
||||||
|
}
|
||||||
|
if len(configAuthInfo.Impersonate) > 0 {
|
||||||
|
clientConfig.Impersonate = restclient.ImpersonationConfig{UserName: configAuthInfo.Impersonate}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only try to read the auth information if we are secure
|
||||||
|
if restclient.IsConfigTransportTLS(*clientConfig) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// mergo is a first write wins for map value and a last writing wins for interface values
|
||||||
|
// NOTE: This behavior changed with https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a.
|
||||||
|
// Our mergo.Merge version is older than this change.
|
||||||
|
var persister restclient.AuthProviderConfigPersister
|
||||||
|
if config.configAccess != nil {
|
||||||
|
authInfoName, _ := config.getAuthInfoName()
|
||||||
|
persister = PersisterForUser(config.configAccess, authInfoName)
|
||||||
|
}
|
||||||
|
userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mergo.Merge(clientConfig, userAuthPartialConfig)
|
||||||
|
|
||||||
|
serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mergo.Merge(clientConfig, serverAuthPartialConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientauth.Info object contain both user identification and server identification. We want different precedence orders for
|
||||||
|
// both, so we have to split the objects and merge them separately
|
||||||
|
// we want this order of precedence for the server identification
|
||||||
|
// 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files)
|
||||||
|
// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority)
|
||||||
|
// 3. load the ~/.kubernetes_auth file as a default
|
||||||
|
func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, configClusterInfo clientcmdapi.Cluster) (*restclient.Config, error) {
|
||||||
|
mergedConfig := &restclient.Config{}
|
||||||
|
|
||||||
|
// configClusterInfo holds the information identify the server provided by .kubeconfig
|
||||||
|
configClientConfig := &restclient.Config{}
|
||||||
|
configClientConfig.CAFile = configClusterInfo.CertificateAuthority
|
||||||
|
configClientConfig.CAData = configClusterInfo.CertificateAuthorityData
|
||||||
|
configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify
|
||||||
|
mergo.Merge(mergedConfig, configClientConfig)
|
||||||
|
|
||||||
|
return mergedConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientauth.Info object contain both user identification and server identification. We want different precedence orders for
|
||||||
|
// both, so we have to split the objects and merge them separately
|
||||||
|
// we want this order of precedence for user identifcation
|
||||||
|
// 1. configAuthInfo minus auth-path (the final result of command line flags and merged .kubeconfig files)
|
||||||
|
// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority)
|
||||||
|
// 3. if there is not enough information to idenfity the user, load try the ~/.kubernetes_auth file
|
||||||
|
// 4. if there is not enough information to identify the user, prompt if possible
|
||||||
|
func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister) (*restclient.Config, error) {
|
||||||
|
mergedConfig := &restclient.Config{}
|
||||||
|
|
||||||
|
// blindly overwrite existing values based on precedence
|
||||||
|
if len(configAuthInfo.Token) > 0 {
|
||||||
|
mergedConfig.BearerToken = configAuthInfo.Token
|
||||||
|
} else if len(configAuthInfo.TokenFile) > 0 {
|
||||||
|
tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mergedConfig.BearerToken = string(tokenBytes)
|
||||||
|
}
|
||||||
|
if len(configAuthInfo.Impersonate) > 0 {
|
||||||
|
mergedConfig.Impersonate = restclient.ImpersonationConfig{UserName: configAuthInfo.Impersonate}
|
||||||
|
}
|
||||||
|
if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 {
|
||||||
|
mergedConfig.CertFile = configAuthInfo.ClientCertificate
|
||||||
|
mergedConfig.CertData = configAuthInfo.ClientCertificateData
|
||||||
|
mergedConfig.KeyFile = configAuthInfo.ClientKey
|
||||||
|
mergedConfig.KeyData = configAuthInfo.ClientKeyData
|
||||||
|
}
|
||||||
|
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
|
||||||
|
mergedConfig.Username = configAuthInfo.Username
|
||||||
|
mergedConfig.Password = configAuthInfo.Password
|
||||||
|
}
|
||||||
|
if configAuthInfo.AuthProvider != nil {
|
||||||
|
mergedConfig.AuthProvider = configAuthInfo.AuthProvider
|
||||||
|
mergedConfig.AuthConfigPersister = persistAuthConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there still isn't enough information to authenticate the user, try prompting
|
||||||
|
if !canIdentifyUser(*mergedConfig) && (fallbackReader != nil) {
|
||||||
|
if len(config.promptedCredentials.username) > 0 && len(config.promptedCredentials.password) > 0 {
|
||||||
|
mergedConfig.Username = config.promptedCredentials.username
|
||||||
|
mergedConfig.Password = config.promptedCredentials.password
|
||||||
|
return mergedConfig, nil
|
||||||
|
}
|
||||||
|
prompter := NewPromptingAuthLoader(fallbackReader)
|
||||||
|
promptedAuthInfo, err := prompter.Prompt()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
promptedConfig := makeUserIdentificationConfig(*promptedAuthInfo)
|
||||||
|
previouslyMergedConfig := mergedConfig
|
||||||
|
mergedConfig = &restclient.Config{}
|
||||||
|
mergo.Merge(mergedConfig, promptedConfig)
|
||||||
|
mergo.Merge(mergedConfig, previouslyMergedConfig)
|
||||||
|
config.promptedCredentials.username = mergedConfig.Username
|
||||||
|
config.promptedCredentials.password = mergedConfig.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only user identification information
|
||||||
|
func makeUserIdentificationConfig(info clientauth.Info) *restclient.Config {
|
||||||
|
config := &restclient.Config{}
|
||||||
|
config.Username = info.User
|
||||||
|
config.Password = info.Password
|
||||||
|
config.CertFile = info.CertFile
|
||||||
|
config.KeyFile = info.KeyFile
|
||||||
|
config.BearerToken = info.BearerToken
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only server identification information
|
||||||
|
func makeServerIdentificationConfig(info clientauth.Info) restclient.Config {
|
||||||
|
config := restclient.Config{}
|
||||||
|
config.CAFile = info.CAFile
|
||||||
|
if info.Insecure != nil {
|
||||||
|
config.Insecure = *info.Insecure
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func canIdentifyUser(config restclient.Config) bool {
|
||||||
|
return len(config.Username) > 0 ||
|
||||||
|
(len(config.CertFile) > 0 || len(config.CertData) > 0) ||
|
||||||
|
len(config.BearerToken) > 0 ||
|
||||||
|
config.AuthProvider != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespace implements ClientConfig
|
||||||
|
func (config *DirectClientConfig) Namespace() (string, bool, error) {
|
||||||
|
if err := config.ConfirmUsable(); err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configContext, err := config.getContext()
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(configContext.Namespace) == 0 {
|
||||||
|
return api.NamespaceDefault, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
overridden := false
|
||||||
|
if config.overrides != nil && config.overrides.Context.Namespace != "" {
|
||||||
|
overridden = true
|
||||||
|
}
|
||||||
|
return configContext.Namespace, overridden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigAccess implements ClientConfig
|
||||||
|
func (config *DirectClientConfig) ConfigAccess() ConfigAccess {
|
||||||
|
return config.configAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
|
||||||
|
// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
|
||||||
|
func (config *DirectClientConfig) ConfirmUsable() error {
|
||||||
|
validationErrors := make([]error, 0)
|
||||||
|
|
||||||
|
var contextName string
|
||||||
|
if len(config.contextName) != 0 {
|
||||||
|
contextName = config.contextName
|
||||||
|
} else {
|
||||||
|
contextName = config.config.CurrentContext
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(contextName) > 0 {
|
||||||
|
_, exists := config.config.Contexts[contextName]
|
||||||
|
if !exists {
|
||||||
|
validationErrors = append(validationErrors, &errContextNotFound{contextName})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authInfoName, _ := config.getAuthInfoName()
|
||||||
|
authInfo, _ := config.getAuthInfo()
|
||||||
|
validationErrors = append(validationErrors, validateAuthInfo(authInfoName, authInfo)...)
|
||||||
|
clusterName, _ := config.getClusterName()
|
||||||
|
cluster, _ := config.getCluster()
|
||||||
|
validationErrors = append(validationErrors, validateClusterInfo(clusterName, cluster)...)
|
||||||
|
// when direct client config is specified, and our only error is that no server is defined, we should
|
||||||
|
// return a standard "no config" error
|
||||||
|
if len(validationErrors) == 1 && validationErrors[0] == ErrEmptyCluster {
|
||||||
|
return newErrConfigurationInvalid([]error{ErrEmptyConfig})
|
||||||
|
}
|
||||||
|
return newErrConfigurationInvalid(validationErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContextName returns the default, or user-set context name, and a boolean that indicates
|
||||||
|
// whether the default context name has been overwritten by a user-set flag, or left as its default value
|
||||||
|
func (config *DirectClientConfig) getContextName() (string, bool) {
|
||||||
|
if len(config.overrides.CurrentContext) != 0 {
|
||||||
|
return config.overrides.CurrentContext, true
|
||||||
|
}
|
||||||
|
if len(config.contextName) != 0 {
|
||||||
|
return config.contextName, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.config.CurrentContext, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAuthInfoName returns a string containing the current authinfo name for the current context,
|
||||||
|
// and a boolean indicating whether the default authInfo name is overwritten by a user-set flag, or
|
||||||
|
// left as its default value
|
||||||
|
func (config *DirectClientConfig) getAuthInfoName() (string, bool) {
|
||||||
|
if len(config.overrides.Context.AuthInfo) != 0 {
|
||||||
|
return config.overrides.Context.AuthInfo, true
|
||||||
|
}
|
||||||
|
context, _ := config.getContext()
|
||||||
|
return context.AuthInfo, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClusterName returns a string containing the default, or user-set cluster name, and a boolean
|
||||||
|
// indicating whether the default clusterName has been overwritten by a user-set flag, or left as
|
||||||
|
// its default value
|
||||||
|
func (config *DirectClientConfig) getClusterName() (string, bool) {
|
||||||
|
if len(config.overrides.Context.Cluster) != 0 {
|
||||||
|
return config.overrides.Context.Cluster, true
|
||||||
|
}
|
||||||
|
context, _ := config.getContext()
|
||||||
|
return context.Cluster, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContext returns the clientcmdapi.Context, or an error if a required context is not found.
|
||||||
|
func (config *DirectClientConfig) getContext() (clientcmdapi.Context, error) {
|
||||||
|
contexts := config.config.Contexts
|
||||||
|
contextName, required := config.getContextName()
|
||||||
|
|
||||||
|
mergedContext := clientcmdapi.NewContext()
|
||||||
|
if configContext, exists := contexts[contextName]; exists {
|
||||||
|
mergo.Merge(mergedContext, configContext)
|
||||||
|
} else if required {
|
||||||
|
return clientcmdapi.Context{}, fmt.Errorf("context %q does not exist", contextName)
|
||||||
|
}
|
||||||
|
mergo.Merge(mergedContext, config.overrides.Context)
|
||||||
|
|
||||||
|
return *mergedContext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAuthInfo returns the clientcmdapi.AuthInfo, or an error if a required auth info is not found.
|
||||||
|
func (config *DirectClientConfig) getAuthInfo() (clientcmdapi.AuthInfo, error) {
|
||||||
|
authInfos := config.config.AuthInfos
|
||||||
|
authInfoName, required := config.getAuthInfoName()
|
||||||
|
|
||||||
|
mergedAuthInfo := clientcmdapi.NewAuthInfo()
|
||||||
|
if configAuthInfo, exists := authInfos[authInfoName]; exists {
|
||||||
|
mergo.Merge(mergedAuthInfo, configAuthInfo)
|
||||||
|
} else if required {
|
||||||
|
return clientcmdapi.AuthInfo{}, fmt.Errorf("auth info %q does not exist", authInfoName)
|
||||||
|
}
|
||||||
|
mergo.Merge(mergedAuthInfo, config.overrides.AuthInfo)
|
||||||
|
|
||||||
|
return *mergedAuthInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCluster returns the clientcmdapi.Cluster, or an error if a required cluster is not found.
|
||||||
|
func (config *DirectClientConfig) getCluster() (clientcmdapi.Cluster, error) {
|
||||||
|
clusterInfos := config.config.Clusters
|
||||||
|
clusterInfoName, required := config.getClusterName()
|
||||||
|
|
||||||
|
mergedClusterInfo := clientcmdapi.NewCluster()
|
||||||
|
mergo.Merge(mergedClusterInfo, config.overrides.ClusterDefaults)
|
||||||
|
if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists {
|
||||||
|
mergo.Merge(mergedClusterInfo, configClusterInfo)
|
||||||
|
} else if required {
|
||||||
|
return clientcmdapi.Cluster{}, fmt.Errorf("cluster %q does not exist", clusterInfoName)
|
||||||
|
}
|
||||||
|
mergo.Merge(mergedClusterInfo, config.overrides.ClusterInfo)
|
||||||
|
// An override of --insecure-skip-tls-verify=true and no accompanying CA/CA data should clear already-set CA/CA data
|
||||||
|
// otherwise, a kubeconfig containing a CA reference would return an error that "CA and insecure-skip-tls-verify couldn't both be set"
|
||||||
|
caLen := len(config.overrides.ClusterInfo.CertificateAuthority)
|
||||||
|
caDataLen := len(config.overrides.ClusterInfo.CertificateAuthorityData)
|
||||||
|
if config.overrides.ClusterInfo.InsecureSkipTLSVerify && caLen == 0 && caDataLen == 0 {
|
||||||
|
mergedClusterInfo.CertificateAuthority = ""
|
||||||
|
mergedClusterInfo.CertificateAuthorityData = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return *mergedClusterInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// inClusterClientConfig makes a config that will work from within a kubernetes cluster container environment.
|
||||||
|
// Can take options overrides for flags explicitly provided to the command inside the cluster container.
|
||||||
|
type inClusterClientConfig struct {
|
||||||
|
overrides *ConfigOverrides
|
||||||
|
inClusterConfigProvider func() (*restclient.Config, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ClientConfig = &inClusterClientConfig{}
|
||||||
|
|
||||||
|
func (config *inClusterClientConfig) RawConfig() (clientcmdapi.Config, error) {
|
||||||
|
return clientcmdapi.Config{}, fmt.Errorf("inCluster environment config doesn't support multiple clusters")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *inClusterClientConfig) ClientConfig() (*restclient.Config, error) {
|
||||||
|
if config.inClusterConfigProvider == nil {
|
||||||
|
config.inClusterConfigProvider = restclient.InClusterConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
icc, err := config.inClusterConfigProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// in-cluster configs only takes a host, token, or CA file
|
||||||
|
// if any of them were individually provided, ovewrite anything else
|
||||||
|
if config.overrides != nil {
|
||||||
|
if server := config.overrides.ClusterInfo.Server; len(server) > 0 {
|
||||||
|
icc.Host = server
|
||||||
|
}
|
||||||
|
if token := config.overrides.AuthInfo.Token; len(token) > 0 {
|
||||||
|
icc.BearerToken = token
|
||||||
|
}
|
||||||
|
if certificateAuthorityFile := config.overrides.ClusterInfo.CertificateAuthority; len(certificateAuthorityFile) > 0 {
|
||||||
|
icc.TLSClientConfig.CAFile = certificateAuthorityFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return icc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *inClusterClientConfig) Namespace() (string, bool, error) {
|
||||||
|
// This way assumes you've set the POD_NAMESPACE environment variable using the downward API.
|
||||||
|
// This check has to be done first for backwards compatibility with the way InClusterConfig was originally set up
|
||||||
|
if ns := os.Getenv("POD_NAMESPACE"); ns != "" {
|
||||||
|
return ns, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to the namespace associated with the service account token, if available
|
||||||
|
if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
|
||||||
|
if ns := strings.TrimSpace(string(data)); len(ns) > 0 {
|
||||||
|
return ns, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *inClusterClientConfig) ConfigAccess() ConfigAccess {
|
||||||
|
return NewDefaultClientConfigLoadingRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possible returns true if loading an inside-kubernetes-cluster is possible.
|
||||||
|
func (config *inClusterClientConfig) Possible() bool {
|
||||||
|
fi, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||||
|
return os.Getenv("KUBERNETES_SERVICE_HOST") != "" &&
|
||||||
|
os.Getenv("KUBERNETES_SERVICE_PORT") != "" &&
|
||||||
|
err == nil && !fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildConfigFromFlags is a helper function that builds configs from a master
|
||||||
|
// url or a kubeconfig filepath. These are passed in as command line flags for cluster
|
||||||
|
// components. Warnings should reflect this usage. If neither masterUrl or kubeconfigPath
|
||||||
|
// are passed in we fallback to inClusterConfig. If inClusterConfig fails, we fallback
|
||||||
|
// to the default config.
|
||||||
|
func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) {
|
||||||
|
if kubeconfigPath == "" && masterUrl == "" {
|
||||||
|
glog.Warningf("Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.")
|
||||||
|
kubeconfig, err := restclient.InClusterConfig()
|
||||||
|
if err == nil {
|
||||||
|
return kubeconfig, nil
|
||||||
|
}
|
||||||
|
glog.Warning("error creating inClusterConfig, falling back to default config: ", err)
|
||||||
|
}
|
||||||
|
return NewNonInteractiveDeferredLoadingClientConfig(
|
||||||
|
&ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
|
||||||
|
&ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildConfigFromKubeconfigGetter is a helper function that builds configs from a master
|
||||||
|
// url and a kubeconfigGetter.
|
||||||
|
func BuildConfigFromKubeconfigGetter(masterUrl string, kubeconfigGetter KubeconfigGetter) (*restclient.Config, error) {
|
||||||
|
// TODO: We do not need a DeferredLoader here. Refactor code and see if we can use DirectClientConfig here.
|
||||||
|
cc := NewNonInteractiveDeferredLoadingClientConfig(
|
||||||
|
&ClientConfigGetter{kubeconfigGetter: kubeconfigGetter},
|
||||||
|
&ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}})
|
||||||
|
return cc.ClientConfig()
|
||||||
|
}
|
|
@ -0,0 +1,472 @@
|
||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files
|
||||||
|
type ConfigAccess interface {
|
||||||
|
// GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config
|
||||||
|
GetLoadingPrecedence() []string
|
||||||
|
// GetStartingConfig returns the config that subcommands should being operating against. It may or may not be merged depending on loading rules
|
||||||
|
GetStartingConfig() (*clientcmdapi.Config, error)
|
||||||
|
// 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.
|
||||||
|
GetDefaultFilename() string
|
||||||
|
// 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
|
||||||
|
IsExplicitFile() bool
|
||||||
|
// 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
|
||||||
|
GetExplicitFile() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PathOptions struct {
|
||||||
|
// GlobalFile is the full path to the file to load as the global (final) option
|
||||||
|
GlobalFile string
|
||||||
|
// EnvVar is the env var name that points to the list of kubeconfig files to load
|
||||||
|
EnvVar string
|
||||||
|
// ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file
|
||||||
|
ExplicitFileFlag string
|
||||||
|
|
||||||
|
// GlobalFileSubpath is an optional value used for displaying help
|
||||||
|
GlobalFileSubpath string
|
||||||
|
|
||||||
|
LoadingRules *ClientConfigLoadingRules
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *PathOptions) GetEnvVarFiles() []string {
|
||||||
|
if len(o.EnvVar) == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
envVarValue := os.Getenv(o.EnvVar)
|
||||||
|
if len(envVarValue) == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.SplitList(envVarValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *PathOptions) GetLoadingPrecedence() []string {
|
||||||
|
if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
|
||||||
|
return envVarFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{o.GlobalFile}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *PathOptions) GetStartingConfig() (*clientcmdapi.Config, error) {
|
||||||
|
// don't mutate the original
|
||||||
|
loadingRules := *o.LoadingRules
|
||||||
|
loadingRules.Precedence = o.GetLoadingPrecedence()
|
||||||
|
|
||||||
|
clientConfig := NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &ConfigOverrides{})
|
||||||
|
rawConfig, err := clientConfig.RawConfig()
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return clientcmdapi.NewConfig(), nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &rawConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *PathOptions) GetDefaultFilename() string {
|
||||||
|
if o.IsExplicitFile() {
|
||||||
|
return o.GetExplicitFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
|
||||||
|
if len(envVarFiles) == 1 {
|
||||||
|
return envVarFiles[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// if any of the envvar files already exists, return it
|
||||||
|
for _, envVarFile := range envVarFiles {
|
||||||
|
if _, err := os.Stat(envVarFile); err == nil {
|
||||||
|
return envVarFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, return the last one in the list
|
||||||
|
return envVarFiles[len(envVarFiles)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.GlobalFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *PathOptions) IsExplicitFile() bool {
|
||||||
|
if len(o.LoadingRules.ExplicitPath) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *PathOptions) GetExplicitFile() string {
|
||||||
|
return o.LoadingRules.ExplicitPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultPathOptions() *PathOptions {
|
||||||
|
ret := &PathOptions{
|
||||||
|
GlobalFile: RecommendedHomeFile,
|
||||||
|
EnvVar: RecommendedConfigPathEnvVar,
|
||||||
|
ExplicitFileFlag: RecommendedConfigPathFlag,
|
||||||
|
|
||||||
|
GlobalFileSubpath: path.Join(RecommendedHomeDir, RecommendedFileName),
|
||||||
|
|
||||||
|
LoadingRules: NewDefaultClientConfigLoadingRules(),
|
||||||
|
}
|
||||||
|
ret.LoadingRules.DoNotResolvePaths = true
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or
|
||||||
|
// uses the default destination file to write the results into. This results in multiple file reads, but it's very easy to follow.
|
||||||
|
// Preferences and CurrentContext should always be set in the default destination file. Since we can't distinguish between empty and missing values
|
||||||
|
// (no nil strings), we're forced have separate handling for them. In the kubeconfig cases, newConfig should have at most one difference,
|
||||||
|
// 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
|
||||||
|
// modified element.
|
||||||
|
func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error {
|
||||||
|
possibleSources := configAccess.GetLoadingPrecedence()
|
||||||
|
// sort the possible kubeconfig files so we always "lock" in the same order
|
||||||
|
// to avoid deadlock (note: this can fail w/ symlinks, but... come on).
|
||||||
|
sort.Strings(possibleSources)
|
||||||
|
for _, filename := range possibleSources {
|
||||||
|
if err := lockFile(filename); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer unlockFile(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
startingConfig, err := configAccess.GetStartingConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file.
|
||||||
|
// Special case the test for current context and preferences since those always write to the default file.
|
||||||
|
if reflect.DeepEqual(*startingConfig, newConfig) {
|
||||||
|
// nothing to do
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if startingConfig.CurrentContext != newConfig.CurrentContext {
|
||||||
|
if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) {
|
||||||
|
if err := writePreferences(configAccess, newConfig.Preferences); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search every cluster, authInfo, and context. First from new to old for differences, then from old to new for deletions
|
||||||
|
for key, cluster := range newConfig.Clusters {
|
||||||
|
startingCluster, exists := startingConfig.Clusters[key]
|
||||||
|
if !reflect.DeepEqual(cluster, startingCluster) || !exists {
|
||||||
|
destinationFile := cluster.LocationOfOrigin
|
||||||
|
if len(destinationFile) == 0 {
|
||||||
|
destinationFile = configAccess.GetDefaultFilename()
|
||||||
|
}
|
||||||
|
|
||||||
|
configToWrite, err := getConfigFromFile(destinationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t := *cluster
|
||||||
|
|
||||||
|
configToWrite.Clusters[key] = &t
|
||||||
|
configToWrite.Clusters[key].LocationOfOrigin = destinationFile
|
||||||
|
if relativizePaths {
|
||||||
|
if err := RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, context := range newConfig.Contexts {
|
||||||
|
startingContext, exists := startingConfig.Contexts[key]
|
||||||
|
if !reflect.DeepEqual(context, startingContext) || !exists {
|
||||||
|
destinationFile := context.LocationOfOrigin
|
||||||
|
if len(destinationFile) == 0 {
|
||||||
|
destinationFile = configAccess.GetDefaultFilename()
|
||||||
|
}
|
||||||
|
|
||||||
|
configToWrite, err := getConfigFromFile(destinationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configToWrite.Contexts[key] = context
|
||||||
|
|
||||||
|
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, authInfo := range newConfig.AuthInfos {
|
||||||
|
startingAuthInfo, exists := startingConfig.AuthInfos[key]
|
||||||
|
if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists {
|
||||||
|
destinationFile := authInfo.LocationOfOrigin
|
||||||
|
if len(destinationFile) == 0 {
|
||||||
|
destinationFile = configAccess.GetDefaultFilename()
|
||||||
|
}
|
||||||
|
|
||||||
|
configToWrite, err := getConfigFromFile(destinationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t := *authInfo
|
||||||
|
configToWrite.AuthInfos[key] = &t
|
||||||
|
configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile
|
||||||
|
if relativizePaths {
|
||||||
|
if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, cluster := range startingConfig.Clusters {
|
||||||
|
if _, exists := newConfig.Clusters[key]; !exists {
|
||||||
|
destinationFile := cluster.LocationOfOrigin
|
||||||
|
if len(destinationFile) == 0 {
|
||||||
|
destinationFile = configAccess.GetDefaultFilename()
|
||||||
|
}
|
||||||
|
|
||||||
|
configToWrite, err := getConfigFromFile(destinationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(configToWrite.Clusters, key)
|
||||||
|
|
||||||
|
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, context := range startingConfig.Contexts {
|
||||||
|
if _, exists := newConfig.Contexts[key]; !exists {
|
||||||
|
destinationFile := context.LocationOfOrigin
|
||||||
|
if len(destinationFile) == 0 {
|
||||||
|
destinationFile = configAccess.GetDefaultFilename()
|
||||||
|
}
|
||||||
|
|
||||||
|
configToWrite, err := getConfigFromFile(destinationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(configToWrite.Contexts, key)
|
||||||
|
|
||||||
|
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, authInfo := range startingConfig.AuthInfos {
|
||||||
|
if _, exists := newConfig.AuthInfos[key]; !exists {
|
||||||
|
destinationFile := authInfo.LocationOfOrigin
|
||||||
|
if len(destinationFile) == 0 {
|
||||||
|
destinationFile = configAccess.GetDefaultFilename()
|
||||||
|
}
|
||||||
|
|
||||||
|
configToWrite, err := getConfigFromFile(destinationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(configToWrite.AuthInfos, key)
|
||||||
|
|
||||||
|
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister {
|
||||||
|
return &persister{configAccess, user}
|
||||||
|
}
|
||||||
|
|
||||||
|
type persister struct {
|
||||||
|
configAccess ConfigAccess
|
||||||
|
user string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *persister) Persist(config map[string]string) error {
|
||||||
|
newConfig, err := p.configAccess.GetStartingConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authInfo, ok := newConfig.AuthInfos[p.user]
|
||||||
|
if ok && authInfo.AuthProvider != nil {
|
||||||
|
authInfo.AuthProvider.Config = config
|
||||||
|
ModifyConfig(p.configAccess, *newConfig, false)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCurrentContext takes three possible paths.
|
||||||
|
// If newCurrentContext is the same as the startingConfig's current context, then we exit.
|
||||||
|
// If newCurrentContext has a value, then that value is written into the default destination file.
|
||||||
|
// If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file
|
||||||
|
func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error {
|
||||||
|
if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if startingConfig.CurrentContext == newCurrentContext {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if configAccess.IsExplicitFile() {
|
||||||
|
file := configAccess.GetExplicitFile()
|
||||||
|
currConfig, err := getConfigFromFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currConfig.CurrentContext = newCurrentContext
|
||||||
|
if err := WriteToFile(*currConfig, file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newCurrentContext) > 0 {
|
||||||
|
destinationFile := configAccess.GetDefaultFilename()
|
||||||
|
config, err := getConfigFromFile(destinationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.CurrentContext = newCurrentContext
|
||||||
|
|
||||||
|
if err := WriteToFile(*config, destinationFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
for _, file := range configAccess.GetLoadingPrecedence() {
|
||||||
|
if _, err := os.Stat(file); err == nil {
|
||||||
|
currConfig, err := getConfigFromFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(currConfig.CurrentContext) > 0 {
|
||||||
|
currConfig.CurrentContext = newCurrentContext
|
||||||
|
if err := WriteToFile(*currConfig, file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("no config found to write context")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error {
|
||||||
|
if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if configAccess.IsExplicitFile() {
|
||||||
|
file := configAccess.GetExplicitFile()
|
||||||
|
currConfig, err := getConfigFromFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currConfig.Preferences = newPrefs
|
||||||
|
if err := WriteToFile(*currConfig, file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range configAccess.GetLoadingPrecedence() {
|
||||||
|
currConfig, err := getConfigFromFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(currConfig.Preferences, newPrefs) {
|
||||||
|
currConfig.Preferences = newPrefs
|
||||||
|
if err := WriteToFile(*currConfig, file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("no config found to write preferences")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func getConfigFromFile(filename string) (*clientcmdapi.Config, error) {
|
||||||
|
config, err := LoadFromFile(filename)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if config == nil {
|
||||||
|
config = clientcmdapi.NewConfig()
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config {
|
||||||
|
config, err := getConfigFromFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
glog.FatalDepth(1, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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 provides one stop shopping for building a working client from a fixed config,
|
||||||
|
from a .kubeconfig file, from command line flags, or from any merged combination.
|
||||||
|
|
||||||
|
Sample usage from merged .kubeconfig files (local directory, home directory)
|
||||||
|
|
||||||
|
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||||
|
// if you want to change the loading rules (which files in which order), you can do so here
|
||||||
|
|
||||||
|
configOverrides := &clientcmd.ConfigOverrides{}
|
||||||
|
// if you want to change override values or bind them to flags, there are methods to help you
|
||||||
|
|
||||||
|
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
|
||||||
|
config, err := kubeConfig.ClientConfig()
|
||||||
|
if err != nil {
|
||||||
|
// Do something
|
||||||
|
}
|
||||||
|
client, err := metav1.New(config)
|
||||||
|
// ...
|
||||||
|
*/
|
||||||
|
package clientcmd // import "k8s.io/client-go/tools/clientcmd"
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 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"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseTimeout returns a parsed duration from a string
|
||||||
|
// A duration string value must be a positive integer, optionally followed by a corresponding time unit (s|m|h).
|
||||||
|
func ParseTimeout(duration string) (time.Duration, error) {
|
||||||
|
if i, err := strconv.ParseInt(duration, 10, 64); err == nil && i >= 0 {
|
||||||
|
return (time.Duration(i) * time.Second), nil
|
||||||
|
}
|
||||||
|
if requestTimeout, err := time.ParseDuration(duration); err == nil {
|
||||||
|
return requestTimeout, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("Invalid timeout value. Timeout must be a single integer in seconds, or an integer followed by a corresponding time unit (e.g. 1s | 2m | 3h)")
|
||||||
|
}
|
|
@ -0,0 +1,609 @@
|
||||||
|
/*
|
||||||
|
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/golang/glog"
|
||||||
|
"github.com/imdario/mergo"
|
||||||
|
|
||||||
|
"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 RecommendedHomeFile = path.Join(homedir.HomeDir(), RecommendedHomeDir, RecommendedFileName)
|
||||||
|
var RecommendedSchemaFile = path.Join(homedir.HomeDir(), RecommendedHomeDir, 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 thie 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{}
|
||||||
|
|
||||||
|
envVarFiles := os.Getenv(RecommendedConfigPathEnvVar)
|
||||||
|
if len(envVarFiles) != 0 {
|
||||||
|
chain = append(chain, filepath.SplitList(envVarFiles)...)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
chain = append(chain, RecommendedHomeFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ClientConfigLoadingRules{
|
||||||
|
Precedence: chain,
|
||||||
|
MigrationRules: currentMigrationRules(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{}
|
||||||
|
|
||||||
|
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
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
errlist = append(errlist, fmt.Errorf("Error loading config file \"%s\": %v", filename, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeconfigs = append(kubeconfigs, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// first merge all of our maps
|
||||||
|
mapConfig := clientcmdapi.NewConfig()
|
||||||
|
|
||||||
|
for _, kubeconfig := range kubeconfigs {
|
||||||
|
mergo.Merge(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.Merge(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.Merge(config, mapConfig)
|
||||||
|
mergo.Merge(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
|
||||||
|
}
|
||||||
|
glog.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 seperate 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 {
|
||||||
|
return []*string{&authInfo.ClientCertificate, &authInfo.ClientKey, &authInfo.TokenFile}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeferredLoadingClientConfig is a ClientConfig interface that is backed by a client config loader.
|
||||||
|
// It is used in cases where the loading rules may change after you've instantiated them and you want to be sure that
|
||||||
|
// the most recent rules are used. This is useful in cases where you bind flags to loading rule parameters before
|
||||||
|
// the parse happens and you want your calling code to be ignorant of how the values are being mutated to avoid
|
||||||
|
// passing extraneous information down a call stack
|
||||||
|
type DeferredLoadingClientConfig struct {
|
||||||
|
loader ClientConfigLoader
|
||||||
|
overrides *ConfigOverrides
|
||||||
|
fallbackReader io.Reader
|
||||||
|
|
||||||
|
clientConfig ClientConfig
|
||||||
|
loadingLock sync.Mutex
|
||||||
|
|
||||||
|
// provided for testing
|
||||||
|
icc InClusterConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// InClusterConfig abstracts details of whether the client is running in a cluster for testing.
|
||||||
|
type InClusterConfig interface {
|
||||||
|
ClientConfig
|
||||||
|
Possible() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNonInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name
|
||||||
|
func NewNonInteractiveDeferredLoadingClientConfig(loader ClientConfigLoader, overrides *ConfigOverrides) ClientConfig {
|
||||||
|
return &DeferredLoadingClientConfig{loader: loader, overrides: overrides, icc: &inClusterClientConfig{overrides: overrides}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name and the fallback auth reader
|
||||||
|
func NewInteractiveDeferredLoadingClientConfig(loader ClientConfigLoader, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig {
|
||||||
|
return &DeferredLoadingClientConfig{loader: loader, overrides: overrides, icc: &inClusterClientConfig{overrides: overrides}, fallbackReader: fallbackReader}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *DeferredLoadingClientConfig) createClientConfig() (ClientConfig, error) {
|
||||||
|
if config.clientConfig == nil {
|
||||||
|
config.loadingLock.Lock()
|
||||||
|
defer config.loadingLock.Unlock()
|
||||||
|
|
||||||
|
if config.clientConfig == nil {
|
||||||
|
mergedConfig, err := config.loader.Load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var mergedClientConfig ClientConfig
|
||||||
|
if config.fallbackReader != nil {
|
||||||
|
mergedClientConfig = NewInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.fallbackReader, config.loader)
|
||||||
|
} else {
|
||||||
|
mergedClientConfig = NewNonInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.clientConfig = mergedClientConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.clientConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *DeferredLoadingClientConfig) RawConfig() (clientcmdapi.Config, error) {
|
||||||
|
mergedConfig, err := config.createClientConfig()
|
||||||
|
if err != nil {
|
||||||
|
return clientcmdapi.Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedConfig.RawConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConfig implements ClientConfig
|
||||||
|
func (config *DeferredLoadingClientConfig) ClientConfig() (*restclient.Config, error) {
|
||||||
|
mergedClientConfig, err := config.createClientConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the configuration and return on non-empty errors and if the
|
||||||
|
// content differs from the default config
|
||||||
|
mergedConfig, err := mergedClientConfig.ClientConfig()
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
if !IsEmptyConfig(err) {
|
||||||
|
// return on any error except empty config
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case mergedConfig != nil:
|
||||||
|
// the configuration is valid, but if this is equal to the defaults we should try
|
||||||
|
// in-cluster configuration
|
||||||
|
if !config.loader.IsDefaultConfig(mergedConfig) {
|
||||||
|
return mergedConfig, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for in-cluster configuration and use it
|
||||||
|
if config.icc.Possible() {
|
||||||
|
glog.V(4).Infof("Using in-cluster configuration")
|
||||||
|
return config.icc.ClientConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the result of the merged client config
|
||||||
|
return mergedConfig, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespace implements KubeConfig
|
||||||
|
func (config *DeferredLoadingClientConfig) Namespace() (string, bool, error) {
|
||||||
|
mergedKubeConfig, err := config.createClientConfig()
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, ok, err := mergedKubeConfig.Namespace()
|
||||||
|
// if we get an error and it is not empty config, or if the merged config defined an explicit namespace, or
|
||||||
|
// if in-cluster config is not possible, return immediately
|
||||||
|
if (err != nil && !IsEmptyConfig(err)) || ok || !config.icc.Possible() {
|
||||||
|
// return on any error except empty config
|
||||||
|
return ns, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.V(4).Infof("Using in-cluster namespace")
|
||||||
|
|
||||||
|
// allow the namespace from the service account token directory to be used.
|
||||||
|
return config.icc.Namespace()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigAccess implements ClientConfig
|
||||||
|
func (config *DeferredLoadingClientConfig) ConfigAccess() ConfigAccess {
|
||||||
|
return config.loader
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigOverrides holds values that should override whatever information is pulled from the actual Config object. You can't
|
||||||
|
// simply use an actual Config object, because Configs hold maps, but overrides are restricted to "at most one"
|
||||||
|
type ConfigOverrides struct {
|
||||||
|
AuthInfo clientcmdapi.AuthInfo
|
||||||
|
// ClusterDefaults are applied before the configured cluster info is loaded.
|
||||||
|
ClusterDefaults clientcmdapi.Cluster
|
||||||
|
ClusterInfo clientcmdapi.Cluster
|
||||||
|
Context clientcmdapi.Context
|
||||||
|
CurrentContext string
|
||||||
|
Timeout string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigOverrideFlags holds the flag names to be used for binding command line flags. Notice that this structure tightly
|
||||||
|
// corresponds to ConfigOverrides
|
||||||
|
type ConfigOverrideFlags struct {
|
||||||
|
AuthOverrideFlags AuthOverrideFlags
|
||||||
|
ClusterOverrideFlags ClusterOverrideFlags
|
||||||
|
ContextOverrideFlags ContextOverrideFlags
|
||||||
|
CurrentContext FlagInfo
|
||||||
|
Timeout FlagInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthOverrideFlags holds the flag names to be used for binding command line flags for AuthInfo objects
|
||||||
|
type AuthOverrideFlags struct {
|
||||||
|
ClientCertificate FlagInfo
|
||||||
|
ClientKey FlagInfo
|
||||||
|
Token FlagInfo
|
||||||
|
Impersonate FlagInfo
|
||||||
|
Username FlagInfo
|
||||||
|
Password FlagInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextOverrideFlags holds the flag names to be used for binding command line flags for Cluster objects
|
||||||
|
type ContextOverrideFlags struct {
|
||||||
|
ClusterName FlagInfo
|
||||||
|
AuthInfoName FlagInfo
|
||||||
|
Namespace FlagInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClusterOverride holds the flag names to be used for binding command line flags for Cluster objects
|
||||||
|
type ClusterOverrideFlags struct {
|
||||||
|
APIServer FlagInfo
|
||||||
|
APIVersion FlagInfo
|
||||||
|
CertificateAuthority FlagInfo
|
||||||
|
InsecureSkipTLSVerify FlagInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagInfo contains information about how to register a flag. This struct is useful if you want to provide a way for an extender to
|
||||||
|
// get back a set of recommended flag names, descriptions, and defaults, but allow for customization by an extender. This makes for
|
||||||
|
// coherent extension, without full prescription
|
||||||
|
type FlagInfo struct {
|
||||||
|
// LongName is the long string for a flag. If this is empty, then the flag will not be bound
|
||||||
|
LongName string
|
||||||
|
// ShortName is the single character for a flag. If this is empty, then there will be no short flag
|
||||||
|
ShortName string
|
||||||
|
// Default is the default value for the flag
|
||||||
|
Default string
|
||||||
|
// Description is the description for the flag
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSecretAnnotation add secret flag to Annotation.
|
||||||
|
func (f FlagInfo) AddSecretAnnotation(flags *pflag.FlagSet) FlagInfo {
|
||||||
|
flags.SetAnnotation(f.LongName, "classified", []string{"true"})
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindStringFlag binds the flag based on the provided info. If LongName == "", nothing is registered
|
||||||
|
func (f FlagInfo) BindStringFlag(flags *pflag.FlagSet, target *string) FlagInfo {
|
||||||
|
// you can't register a flag without a long name
|
||||||
|
if len(f.LongName) > 0 {
|
||||||
|
flags.StringVarP(target, f.LongName, f.ShortName, f.Default, f.Description)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindBoolFlag binds the flag based on the provided info. If LongName == "", nothing is registered
|
||||||
|
func (f FlagInfo) BindBoolFlag(flags *pflag.FlagSet, target *bool) FlagInfo {
|
||||||
|
// you can't register a flag without a long name
|
||||||
|
if len(f.LongName) > 0 {
|
||||||
|
// try to parse Default as a bool. If it fails, assume false
|
||||||
|
boolVal, err := strconv.ParseBool(f.Default)
|
||||||
|
if err != nil {
|
||||||
|
boolVal = false
|
||||||
|
}
|
||||||
|
|
||||||
|
flags.BoolVarP(target, f.LongName, f.ShortName, boolVal, f.Description)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagClusterName = "cluster"
|
||||||
|
FlagAuthInfoName = "user"
|
||||||
|
FlagContext = "context"
|
||||||
|
FlagNamespace = "namespace"
|
||||||
|
FlagAPIServer = "server"
|
||||||
|
FlagAPIVersion = "api-version"
|
||||||
|
FlagInsecure = "insecure-skip-tls-verify"
|
||||||
|
FlagCertFile = "client-certificate"
|
||||||
|
FlagKeyFile = "client-key"
|
||||||
|
FlagCAFile = "certificate-authority"
|
||||||
|
FlagEmbedCerts = "embed-certs"
|
||||||
|
FlagBearerToken = "token"
|
||||||
|
FlagImpersonate = "as"
|
||||||
|
FlagUsername = "username"
|
||||||
|
FlagPassword = "password"
|
||||||
|
FlagTimeout = "request-timeout"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecommendedConfigOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
|
||||||
|
func RecommendedConfigOverrideFlags(prefix string) ConfigOverrideFlags {
|
||||||
|
return ConfigOverrideFlags{
|
||||||
|
AuthOverrideFlags: RecommendedAuthOverrideFlags(prefix),
|
||||||
|
ClusterOverrideFlags: RecommendedClusterOverrideFlags(prefix),
|
||||||
|
ContextOverrideFlags: RecommendedContextOverrideFlags(prefix),
|
||||||
|
|
||||||
|
CurrentContext: FlagInfo{prefix + FlagContext, "", "", "The name of the kubeconfig context to use"},
|
||||||
|
Timeout: FlagInfo{prefix + FlagTimeout, "", "0", "The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendedAuthOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
|
||||||
|
func RecommendedAuthOverrideFlags(prefix string) AuthOverrideFlags {
|
||||||
|
return AuthOverrideFlags{
|
||||||
|
ClientCertificate: FlagInfo{prefix + FlagCertFile, "", "", "Path to a client certificate file for TLS"},
|
||||||
|
ClientKey: FlagInfo{prefix + FlagKeyFile, "", "", "Path to a client key file for TLS"},
|
||||||
|
Token: FlagInfo{prefix + FlagBearerToken, "", "", "Bearer token for authentication to the API server"},
|
||||||
|
Impersonate: FlagInfo{prefix + FlagImpersonate, "", "", "Username to impersonate for the operation"},
|
||||||
|
Username: FlagInfo{prefix + FlagUsername, "", "", "Username for basic authentication to the API server"},
|
||||||
|
Password: FlagInfo{prefix + FlagPassword, "", "", "Password for basic authentication to the API server"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendedClusterOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
|
||||||
|
func RecommendedClusterOverrideFlags(prefix string) ClusterOverrideFlags {
|
||||||
|
return ClusterOverrideFlags{
|
||||||
|
APIServer: FlagInfo{prefix + FlagAPIServer, "", "", "The address and port of the Kubernetes API server"},
|
||||||
|
APIVersion: FlagInfo{prefix + FlagAPIVersion, "", "", "DEPRECATED: The API version to use when talking to the server"},
|
||||||
|
CertificateAuthority: FlagInfo{prefix + FlagCAFile, "", "", "Path to a cert. file for the certificate authority"},
|
||||||
|
InsecureSkipTLSVerify: FlagInfo{prefix + FlagInsecure, "", "false", "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendedContextOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
|
||||||
|
func RecommendedContextOverrideFlags(prefix string) ContextOverrideFlags {
|
||||||
|
return ContextOverrideFlags{
|
||||||
|
ClusterName: FlagInfo{prefix + FlagClusterName, "", "", "The name of the kubeconfig cluster to use"},
|
||||||
|
AuthInfoName: FlagInfo{prefix + FlagAuthInfoName, "", "", "The name of the kubeconfig user to use"},
|
||||||
|
Namespace: FlagInfo{prefix + FlagNamespace, "n", "", "If present, the namespace scope for this CLI request"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindOverrideFlags is a convenience method to bind the specified flags to their associated variables
|
||||||
|
func BindOverrideFlags(overrides *ConfigOverrides, flags *pflag.FlagSet, flagNames ConfigOverrideFlags) {
|
||||||
|
BindAuthInfoFlags(&overrides.AuthInfo, flags, flagNames.AuthOverrideFlags)
|
||||||
|
BindClusterFlags(&overrides.ClusterInfo, flags, flagNames.ClusterOverrideFlags)
|
||||||
|
BindContextFlags(&overrides.Context, flags, flagNames.ContextOverrideFlags)
|
||||||
|
flagNames.CurrentContext.BindStringFlag(flags, &overrides.CurrentContext)
|
||||||
|
flagNames.Timeout.BindStringFlag(flags, &overrides.Timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindAuthInfoFlags is a convenience method to bind the specified flags to their associated variables
|
||||||
|
func BindAuthInfoFlags(authInfo *clientcmdapi.AuthInfo, flags *pflag.FlagSet, flagNames AuthOverrideFlags) {
|
||||||
|
flagNames.ClientCertificate.BindStringFlag(flags, &authInfo.ClientCertificate).AddSecretAnnotation(flags)
|
||||||
|
flagNames.ClientKey.BindStringFlag(flags, &authInfo.ClientKey).AddSecretAnnotation(flags)
|
||||||
|
flagNames.Token.BindStringFlag(flags, &authInfo.Token).AddSecretAnnotation(flags)
|
||||||
|
flagNames.Impersonate.BindStringFlag(flags, &authInfo.Impersonate).AddSecretAnnotation(flags)
|
||||||
|
flagNames.Username.BindStringFlag(flags, &authInfo.Username).AddSecretAnnotation(flags)
|
||||||
|
flagNames.Password.BindStringFlag(flags, &authInfo.Password).AddSecretAnnotation(flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindClusterFlags is a convenience method to bind the specified flags to their associated variables
|
||||||
|
func BindClusterFlags(clusterInfo *clientcmdapi.Cluster, flags *pflag.FlagSet, flagNames ClusterOverrideFlags) {
|
||||||
|
flagNames.APIServer.BindStringFlag(flags, &clusterInfo.Server)
|
||||||
|
// TODO: remove --api-version flag in 1.3.
|
||||||
|
flagNames.APIVersion.BindStringFlag(flags, &clusterInfo.APIVersion)
|
||||||
|
flags.MarkDeprecated(FlagAPIVersion, "flag is no longer respected and will be deleted in the next release")
|
||||||
|
flagNames.CertificateAuthority.BindStringFlag(flags, &clusterInfo.CertificateAuthority)
|
||||||
|
flagNames.InsecureSkipTLSVerify.BindBoolFlag(flags, &clusterInfo.InsecureSkipTLSVerify)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindFlags is a convenience method to bind the specified flags to their associated variables
|
||||||
|
func BindContextFlags(contextInfo *clientcmdapi.Context, flags *pflag.FlagSet, flagNames ContextOverrideFlags) {
|
||||||
|
flagNames.ClusterName.BindStringFlag(flags, &contextInfo.Cluster)
|
||||||
|
flagNames.AuthInfoName.BindStringFlag(flags, &contextInfo.AuthInfo)
|
||||||
|
flagNames.Namespace.BindStringFlag(flags, &contextInfo.Namespace)
|
||||||
|
}
|
|
@ -0,0 +1,270 @@
|
||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoContext = errors.New("no context chosen")
|
||||||
|
ErrEmptyConfig = errors.New("no configuration has been provided")
|
||||||
|
// message is for consistency with old behavior
|
||||||
|
ErrEmptyCluster = errors.New("cluster has no server defined")
|
||||||
|
)
|
||||||
|
|
||||||
|
type errContextNotFound struct {
|
||||||
|
ContextName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errContextNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("context was not found for specified context: %v", e.ContextName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsContextNotFound returns a boolean indicating whether the error is known to
|
||||||
|
// report that a context was not found
|
||||||
|
func IsContextNotFound(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := err.(*errContextNotFound); ok || err == ErrNoContext {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.Contains(err.Error(), "context was not found for specified context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmptyConfig returns true if the provided error indicates the provided configuration
|
||||||
|
// is empty.
|
||||||
|
func IsEmptyConfig(err error) bool {
|
||||||
|
switch t := err.(type) {
|
||||||
|
case errConfigurationInvalid:
|
||||||
|
return len(t) == 1 && t[0] == ErrEmptyConfig
|
||||||
|
}
|
||||||
|
return err == ErrEmptyConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// errConfigurationInvalid is a set of errors indicating the configuration is invalid.
|
||||||
|
type errConfigurationInvalid []error
|
||||||
|
|
||||||
|
// errConfigurationInvalid implements error and Aggregate
|
||||||
|
var _ error = errConfigurationInvalid{}
|
||||||
|
var _ utilerrors.Aggregate = errConfigurationInvalid{}
|
||||||
|
|
||||||
|
func newErrConfigurationInvalid(errs []error) error {
|
||||||
|
switch len(errs) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errConfigurationInvalid(errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface
|
||||||
|
func (e errConfigurationInvalid) Error() string {
|
||||||
|
return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors implements the AggregateError interface
|
||||||
|
func (e errConfigurationInvalid) Errors() []error {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConfigurationInvalid returns true if the provided error indicates the configuration is invalid.
|
||||||
|
func IsConfigurationInvalid(err error) bool {
|
||||||
|
switch err.(type) {
|
||||||
|
case *errContextNotFound, errConfigurationInvalid:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return IsContextNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks for errors in the Config. It does not return early so that it can find as many errors as possible.
|
||||||
|
func Validate(config clientcmdapi.Config) error {
|
||||||
|
validationErrors := make([]error, 0)
|
||||||
|
|
||||||
|
if clientcmdapi.IsConfigEmpty(&config) {
|
||||||
|
return newErrConfigurationInvalid([]error{ErrEmptyConfig})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.CurrentContext) != 0 {
|
||||||
|
if _, exists := config.Contexts[config.CurrentContext]; !exists {
|
||||||
|
validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for contextName, context := range config.Contexts {
|
||||||
|
validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for authInfoName, authInfo := range config.AuthInfos {
|
||||||
|
validationErrors = append(validationErrors, validateAuthInfo(authInfoName, *authInfo)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for clusterName, clusterInfo := range config.Clusters {
|
||||||
|
validationErrors = append(validationErrors, validateClusterInfo(clusterName, *clusterInfo)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newErrConfigurationInvalid(validationErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
|
||||||
|
// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
|
||||||
|
func ConfirmUsable(config clientcmdapi.Config, passedContextName string) error {
|
||||||
|
validationErrors := make([]error, 0)
|
||||||
|
|
||||||
|
if clientcmdapi.IsConfigEmpty(&config) {
|
||||||
|
return newErrConfigurationInvalid([]error{ErrEmptyConfig})
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextName string
|
||||||
|
if len(passedContextName) != 0 {
|
||||||
|
contextName = passedContextName
|
||||||
|
} else {
|
||||||
|
contextName = config.CurrentContext
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(contextName) == 0 {
|
||||||
|
return ErrNoContext
|
||||||
|
}
|
||||||
|
|
||||||
|
context, exists := config.Contexts[contextName]
|
||||||
|
if !exists {
|
||||||
|
validationErrors = append(validationErrors, &errContextNotFound{contextName})
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
|
||||||
|
validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, *config.AuthInfos[context.AuthInfo])...)
|
||||||
|
validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, *config.Clusters[context.Cluster])...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newErrConfigurationInvalid(validationErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateClusterInfo looks for conflicts and errors in the cluster info
|
||||||
|
func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) []error {
|
||||||
|
validationErrors := make([]error, 0)
|
||||||
|
|
||||||
|
if reflect.DeepEqual(clientcmdapi.Cluster{}, clusterInfo) {
|
||||||
|
return []error{ErrEmptyCluster}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clusterInfo.Server) == 0 {
|
||||||
|
if len(clusterName) == 0 {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("default cluster has no server defined"))
|
||||||
|
} else {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Make sure CA data and CA file aren't both specified
|
||||||
|
if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName))
|
||||||
|
}
|
||||||
|
if len(clusterInfo.CertificateAuthority) != 0 {
|
||||||
|
clientCertCA, err := os.Open(clusterInfo.CertificateAuthority)
|
||||||
|
defer clientCertCA.Close()
|
||||||
|
if err != nil {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAuthInfo looks for conflicts and errors in the auth info
|
||||||
|
func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error {
|
||||||
|
validationErrors := make([]error, 0)
|
||||||
|
|
||||||
|
usingAuthPath := false
|
||||||
|
methods := make([]string, 0, 3)
|
||||||
|
if len(authInfo.Token) != 0 {
|
||||||
|
methods = append(methods, "token")
|
||||||
|
}
|
||||||
|
if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 {
|
||||||
|
methods = append(methods, "basicAuth")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 {
|
||||||
|
// Make sure cert data and file aren't both specified
|
||||||
|
if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName))
|
||||||
|
}
|
||||||
|
// Make sure key data and file aren't both specified
|
||||||
|
if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName))
|
||||||
|
}
|
||||||
|
// Make sure a key is specified
|
||||||
|
if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(authInfo.ClientCertificate) != 0 {
|
||||||
|
clientCertFile, err := os.Open(authInfo.ClientCertificate)
|
||||||
|
defer clientCertFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(authInfo.ClientKey) != 0 {
|
||||||
|
clientKeyFile, err := os.Open(authInfo.ClientKey)
|
||||||
|
defer clientKeyFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
|
||||||
|
if (len(methods) > 1) && (!usingAuthPath) {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods))
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateContext looks for errors in the context. It is not transitive, so errors in the reference authInfo or cluster configs are not included in this return
|
||||||
|
func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error {
|
||||||
|
validationErrors := make([]error, 0)
|
||||||
|
|
||||||
|
if len(context.AuthInfo) == 0 {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName))
|
||||||
|
} else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(context.Cluster) == 0 {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName))
|
||||||
|
} else if _, exists := config.Clusters[context.Cluster]; !exists {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(context.Namespace) != 0 {
|
||||||
|
if len(validation.IsDNS1123Label(context.Namespace)) != 0 {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS_LABEL rules", context.Namespace, contextName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationErrors
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
|
"go_library",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = ["homedir.go"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
)
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 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 homedir
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HomeDir returns the home directory for the current user
|
||||||
|
func HomeDir() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
|
||||||
|
// First prefer the HOME environmental variable
|
||||||
|
if home := os.Getenv("HOME"); len(home) > 0 {
|
||||||
|
if _, err := os.Stat(home); err == nil {
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if homeDrive, homePath := os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"); len(homeDrive) > 0 && len(homePath) > 0 {
|
||||||
|
homeDir := homeDrive + homePath
|
||||||
|
if _, err := os.Stat(homeDir); err == nil {
|
||||||
|
return homeDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userProfile := os.Getenv("USERPROFILE"); len(userProfile) > 0 {
|
||||||
|
if _, err := os.Stat(userProfile); err == nil {
|
||||||
|
return userProfile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.Getenv("HOME")
|
||||||
|
}
|
|
@ -4299,6 +4299,20 @@
|
||||||
"version": "v3.0.0-beta.0",
|
"version": "v3.0.0-beta.0",
|
||||||
"versionExact": "v3.0.0-beta.0"
|
"versionExact": "v3.0.0-beta.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "1ckiORH3dJD8v3R8tLEZ/5wttAk=",
|
||||||
|
"path": "k8s.io/client-go/tools/auth",
|
||||||
|
"revision": "6b1566ccaa7c069f392461bef8a5fb17087f4729",
|
||||||
|
"revisionTime": "2017-05-22T17:54:13Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "y8RjCuBi8XrHvvVsguI7E97Fh6M=",
|
||||||
|
"path": "k8s.io/client-go/tools/clientcmd",
|
||||||
|
"revision": "3627aeb7d4f6ade38f995d2c923e459146493c7e",
|
||||||
|
"revisionTime": "2017-03-31T20:34:26Z",
|
||||||
|
"version": "v3.0.0-beta.0",
|
||||||
|
"versionExact": "v3.0.0-beta.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "1jUQmUDroNs5XYhsQna4kNTzMik=",
|
"checksumSHA1": "1jUQmUDroNs5XYhsQna4kNTzMik=",
|
||||||
"path": "k8s.io/client-go/tools/clientcmd/api",
|
"path": "k8s.io/client-go/tools/clientcmd/api",
|
||||||
|
@ -4307,6 +4321,22 @@
|
||||||
"version": "v3.0.0-beta.0",
|
"version": "v3.0.0-beta.0",
|
||||||
"versionExact": "v3.0.0-beta.0"
|
"versionExact": "v3.0.0-beta.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "6cVFBhDYvRiAwBf0QYbmUoLi5lQ=",
|
||||||
|
"path": "k8s.io/client-go/tools/clientcmd/api/latest",
|
||||||
|
"revision": "3627aeb7d4f6ade38f995d2c923e459146493c7e",
|
||||||
|
"revisionTime": "2017-03-31T20:34:26Z",
|
||||||
|
"version": "v3.0.0-beta.0",
|
||||||
|
"versionExact": "v3.0.0-beta.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "DG6X6yy03Om6CFqIZm08+geRGkI=",
|
||||||
|
"path": "k8s.io/client-go/tools/clientcmd/api/v1",
|
||||||
|
"revision": "3627aeb7d4f6ade38f995d2c923e459146493c7e",
|
||||||
|
"revisionTime": "2017-03-31T20:34:26Z",
|
||||||
|
"version": "v3.0.0-beta.0",
|
||||||
|
"versionExact": "v3.0.0-beta.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "rRC9GCWXfyIXKHBCeKpw7exVybE=",
|
"checksumSHA1": "rRC9GCWXfyIXKHBCeKpw7exVybE=",
|
||||||
"path": "k8s.io/client-go/tools/metrics",
|
"path": "k8s.io/client-go/tools/metrics",
|
||||||
|
@ -4347,6 +4377,12 @@
|
||||||
"version": "v3.0.0-beta.0",
|
"version": "v3.0.0-beta.0",
|
||||||
"versionExact": "v3.0.0-beta.0"
|
"versionExact": "v3.0.0-beta.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "jP91eRmXcfaEo/NU7Lk1coovunQ=",
|
||||||
|
"path": "k8s.io/client-go/util/homedir",
|
||||||
|
"revision": "6b1566ccaa7c069f392461bef8a5fb17087f4729",
|
||||||
|
"revisionTime": "2017-05-22T17:54:13Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "W6bJiNAwgNwNJC+y+TPtacgUGlY=",
|
"checksumSHA1": "W6bJiNAwgNwNJC+y+TPtacgUGlY=",
|
||||||
"path": "k8s.io/client-go/util/integer",
|
"path": "k8s.io/client-go/util/integer",
|
||||||
|
|
Loading…
Reference in New Issue