From b56e290784ca3bd31af90010b60de129ef2a516f Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 13 Mar 2019 22:02:48 -0700 Subject: [PATCH] Re-activate this project... --- .gitmodules | 3 - .../main.go | 6 +- kubernetes/base | 1 - kubernetes/config | 1 - kubernetes/config/api/README.md | 27 + kubernetes/config/api/types.go | 204 ++++++++ .../config/api/zz_generated.deepcopy.go | 344 +++++++++++++ kubernetes/config/authentication.go | 78 +++ kubernetes/config/azure.go | 72 +++ kubernetes/config/gcp.go | 111 ++++ kubernetes/config/incluster_config.go | 74 +++ kubernetes/config/kube_config.go | 285 +++++++++++ kubernetes/config/kube_config_test.go | 484 ++++++++++++++++++ kubernetes/config/util.go | 122 +++++ kubernetes/config/util_test.go | 213 ++++++++ 15 files changed, 2016 insertions(+), 9 deletions(-) delete mode 160000 kubernetes/base delete mode 120000 kubernetes/config create mode 100644 kubernetes/config/api/README.md create mode 100644 kubernetes/config/api/types.go create mode 100644 kubernetes/config/api/zz_generated.deepcopy.go create mode 100644 kubernetes/config/authentication.go create mode 100644 kubernetes/config/azure.go create mode 100644 kubernetes/config/gcp.go create mode 100644 kubernetes/config/incluster_config.go create mode 100644 kubernetes/config/kube_config.go create mode 100644 kubernetes/config/kube_config_test.go create mode 100644 kubernetes/config/util.go create mode 100644 kubernetes/config/util_test.go diff --git a/.gitmodules b/.gitmodules index 6fd13f0..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "kubernetes/base"] - path = kubernetes/base - url = git@github.com:kubernetes-client/go-base.git diff --git a/examples/out-of-cluster-client-configuration/main.go b/examples/out-of-cluster-client-configuration/main.go index 35eb080..8d7be18 100644 --- a/examples/out-of-cluster-client-configuration/main.go +++ b/examples/out-of-cluster-client-configuration/main.go @@ -22,10 +22,8 @@ import ( "fmt" "time" - "k8s.io/client/kubernetes/client" - "k8s.io/client/kubernetes/config" - // Uncomment the following line to load the gcp plugin (only required to authenticate against GKE clusters). - // _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "github.com/kubernetes-client/go/kubernetes/client" + "github.com/kubernetes-client/go/kubernetes/config" ) func main() { diff --git a/kubernetes/base b/kubernetes/base deleted file mode 160000 index 8dcf05f..0000000 --- a/kubernetes/base +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8dcf05f9fda60f14bf054801fb5226175f6ff8ba diff --git a/kubernetes/config b/kubernetes/config deleted file mode 120000 index 70be64a..0000000 --- a/kubernetes/config +++ /dev/null @@ -1 +0,0 @@ -base/config/ \ No newline at end of file diff --git a/kubernetes/config/api/README.md b/kubernetes/config/api/README.md new file mode 100644 index 0000000..4472b6f --- /dev/null +++ b/kubernetes/config/api/README.md @@ -0,0 +1,27 @@ +# Kubernetes Config type Definition and Deepcopy Utility + +This directory contains type definition and deepcopy utility copied from +k8s.io/client-go that are used for parsing and persist kube config yaml +file. + +### types.go + +The Config type definition is copied from k8s.io/client-go/tools/clientcmd/api/v1/types.go +for parsing the kube config yaml. The "k8s.io/apimachinery/pkg/runtime" dependency has +been removed. An example of using this type definition to parse a kube config +yaml is: + +```go + // Init an empty api.Config as unmarshal layout template + c := api.Config{} + err = yaml.Unmarshal(kubeConfig, &c) + if err != nil { + return nil, err + } +``` + +### zz\_generated.deepcopy.go +The Config type deepcopy util file is copied from +k8s.io/client-go/tools/clientcmd/api/v1/zz\_generated.deepcopy.go +for deepcopy the kube config. The "k8s.io/apimachinery/pkg/runtime" dependency has +been removed. diff --git a/kubernetes/config/api/types.go b/kubernetes/config/api/types.go new file mode 100644 index 0000000..da916d3 --- /dev/null +++ b/kubernetes/config/api/types.go @@ -0,0 +1,204 @@ +/* +Copyright 2018 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 api + +// NOTE: This Config type definition is copied from k8s.io/client-go/tools/clientcmd/api/v1/types.go +// for parsing the kube config yaml. The "k8s.io/apimachinery/pkg/runtime" dependency has +// been removed. + +// 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 +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +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"` + // Legacy field from pkg/api/types.go TypeMeta. + // TODO(jlowdermilk): remove this after eliminating downstream dependencies. + // +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"` +} + +// Preferences holds general information to be use for cli interactions +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"` + // 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"` + // ImpersonateGroups is the groups to imperonate. + // +optional + ImpersonateGroups []string `json:"as-groups,omitempty"` + // ImpersonateUserExtra contains additional information for impersonated user. + // +optional + ImpersonateUserExtra map[string][]string `json:"as-user-extra,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"` + // Exec specifies a custom exec-based authentication plugin for the kubernetes cluster. + // +optional + Exec *ExecConfig `json:"exec,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 interface{} `json:"extension"` +} + +// AuthProviderConfig holds the configuration for a specified auth provider. +type AuthProviderConfig struct { + Name string `json:"name"` + Config map[string]string `json:"config"` +} + +// ExecConfig specifies a command to provide client credentials. The command is exec'd +// and outputs structured stdout holding credentials. +// +// See the client.authentiction.k8s.io API group for specifications of the exact input +// and output format +type ExecConfig struct { + // Command to execute. + Command string `json:"command"` + // Arguments to pass to the command when executing it. + // +optional + Args []string `json:"args"` + // Env defines additional environment variables to expose to the process. These + // are unioned with the host's environment, as well as variables client-go uses + // to pass argument to the plugin. + // +optional + Env []ExecEnvVar `json:"env"` + + // Preferred input version of the ExecInfo. The returned ExecCredentials MUST use + // the same encoding version as the input. + APIVersion string `json:"apiVersion,omitempty"` +} + +// ExecEnvVar is used for setting environment variables when executing an exec-based +// credential plugin. +type ExecEnvVar struct { + Name string `json:"name"` + Value string `json:"value"` +} diff --git a/kubernetes/config/api/zz_generated.deepcopy.go b/kubernetes/config/api/zz_generated.deepcopy.go new file mode 100644 index 0000000..77c5d02 --- /dev/null +++ b/kubernetes/config/api/zz_generated.deepcopy.go @@ -0,0 +1,344 @@ +// +build !ignore_autogenerated + +/* +Copyright 2018 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. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +// NOTE: This Config type deepcopy util file is copied from k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.deepcopy.go +// for deepcopy the kube config. The "k8s.io/apimachinery/pkg/runtime" dependency has +// been removed. + +package api + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthInfo) DeepCopyInto(out *AuthInfo) { + *out = *in + if in.ClientCertificateData != nil { + in, out := &in.ClientCertificateData, &out.ClientCertificateData + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.ClientKeyData != nil { + in, out := &in.ClientKeyData, &out.ClientKeyData + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.ImpersonateGroups != nil { + in, out := &in.ImpersonateGroups, &out.ImpersonateGroups + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ImpersonateUserExtra != nil { + in, out := &in.ImpersonateUserExtra, &out.ImpersonateUserExtra + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + if val == nil { + (*out)[key] = nil + } else { + (*out)[key] = make([]string, len(val)) + copy((*out)[key], val) + } + } + } + if in.AuthProvider != nil { + in, out := &in.AuthProvider, &out.AuthProvider + if *in == nil { + *out = nil + } else { + *out = new(AuthProviderConfig) + (*in).DeepCopyInto(*out) + } + } + if in.Exec != nil { + in, out := &in.Exec, &out.Exec + if *in == nil { + *out = nil + } else { + *out = new(ExecConfig) + (*in).DeepCopyInto(*out) + } + } + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]NamedExtension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthInfo. +func (in *AuthInfo) DeepCopy() *AuthInfo { + if in == nil { + return nil + } + out := new(AuthInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthProviderConfig) DeepCopyInto(out *AuthProviderConfig) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthProviderConfig. +func (in *AuthProviderConfig) DeepCopy() *AuthProviderConfig { + if in == nil { + return nil + } + out := new(AuthProviderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *out = *in + if in.CertificateAuthorityData != nil { + in, out := &in.CertificateAuthorityData, &out.CertificateAuthorityData + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]NamedExtension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Config) DeepCopyInto(out *Config) { + *out = *in + in.Preferences.DeepCopyInto(&out.Preferences) + if in.Clusters != nil { + in, out := &in.Clusters, &out.Clusters + *out = make([]NamedCluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AuthInfos != nil { + in, out := &in.AuthInfos, &out.AuthInfos + *out = make([]NamedAuthInfo, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Contexts != nil { + in, out := &in.Contexts, &out.Contexts + *out = make([]NamedContext, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]NamedExtension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config. +func (in *Config) DeepCopy() *Config { + if in == nil { + return nil + } + out := new(Config) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Context) DeepCopyInto(out *Context) { + *out = *in + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]NamedExtension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Context. +func (in *Context) DeepCopy() *Context { + if in == nil { + return nil + } + out := new(Context) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExecConfig) DeepCopyInto(out *ExecConfig) { + *out = *in + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]ExecEnvVar, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecConfig. +func (in *ExecConfig) DeepCopy() *ExecConfig { + if in == nil { + return nil + } + out := new(ExecConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExecEnvVar) DeepCopyInto(out *ExecEnvVar) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecEnvVar. +func (in *ExecEnvVar) DeepCopy() *ExecEnvVar { + if in == nil { + return nil + } + out := new(ExecEnvVar) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamedAuthInfo) DeepCopyInto(out *NamedAuthInfo) { + *out = *in + in.AuthInfo.DeepCopyInto(&out.AuthInfo) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamedAuthInfo. +func (in *NamedAuthInfo) DeepCopy() *NamedAuthInfo { + if in == nil { + return nil + } + out := new(NamedAuthInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamedCluster) DeepCopyInto(out *NamedCluster) { + *out = *in + in.Cluster.DeepCopyInto(&out.Cluster) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamedCluster. +func (in *NamedCluster) DeepCopy() *NamedCluster { + if in == nil { + return nil + } + out := new(NamedCluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamedContext) DeepCopyInto(out *NamedContext) { + *out = *in + in.Context.DeepCopyInto(&out.Context) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamedContext. +func (in *NamedContext) DeepCopy() *NamedContext { + if in == nil { + return nil + } + out := new(NamedContext) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamedExtension) DeepCopyInto(out *NamedExtension) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamedExtension. +func (in *NamedExtension) DeepCopy() *NamedExtension { + if in == nil { + return nil + } + out := new(NamedExtension) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Preferences) DeepCopyInto(out *Preferences) { + *out = *in + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]NamedExtension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Preferences. +func (in *Preferences) DeepCopy() *Preferences { + if in == nil { + return nil + } + out := new(Preferences) + in.DeepCopyInto(out) + return out +} diff --git a/kubernetes/config/authentication.go b/kubernetes/config/authentication.go new file mode 100644 index 0000000..76884a2 --- /dev/null +++ b/kubernetes/config/authentication.go @@ -0,0 +1,78 @@ +/* +Copyright 2018 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 config + +import ( + "io/ioutil" + "time" +) + +var ( + // time.Duration that prevents the client dropping valid credential + // due to time skew + expirySkewPreventionDelay = 5 * time.Minute +) + +// Read authentication from kube-config user section if exists. +// +// This function goes through various authentication methods in user +// section of kube-config and stops if it finds a valid authentication +// method. The order of authentication methods is: +// +// 1. GCP auth-provider +// 2. token_data +// 3. token field (point to a token file) +// 4. username/password +func (l *KubeConfigLoader) loadAuthentication() { + // The function walks though authentication methods. It doesn't fail on + // single method loading failure. It is each loading function's responsiblity + // to log meaningful failure message. Kubeconfig is allowed to have no user + // in current context, therefore it is allowed that no authentication is loaded. + + // TODO: This structure is ugly, there should be an 'authenticator' interface + if l.loadGCPToken() || l.loadUserToken() || l.loadUserPassToken() || l.loadAzureToken() { + return + } +} + +func (l *KubeConfigLoader) loadUserToken() bool { + if l.user.Token == "" && l.user.TokenFile == "" { + return false + } + // Token takes precedence than TokenFile + if l.user.Token != "" { + l.restConfig.token = "Bearer " + l.user.Token + return true + } + + // Read TokenFile + token, err := ioutil.ReadFile(l.user.TokenFile) + if err != nil { + // A user may not provide any TokenFile, so we don't log error here + return false + } + l.restConfig.token = "Bearer " + string(token) + return true +} + +func (l *KubeConfigLoader) loadUserPassToken() bool { + if l.user.Username != "" && l.user.Password != "" { + l.restConfig.token = basicAuthToken(l.user.Username, l.user.Password) + return true + } + return false +} diff --git a/kubernetes/config/azure.go b/kubernetes/config/azure.go new file mode 100644 index 0000000..9b44092 --- /dev/null +++ b/kubernetes/config/azure.go @@ -0,0 +1,72 @@ +package config + +import ( + "fmt" + "log" + "strconv" + "time" + + "github.com/Azure/go-autorest/autorest/adal" +) + +func (l *KubeConfigLoader) loadAzureToken() bool { + if l.user.AuthProvider == nil || l.user.AuthProvider.Name != "azure" { + return false + } + + // TODO refresh token if needed here. + if l.user.AuthProvider.Config != nil { + expires, exists := l.user.AuthProvider.Config["expires-on"] + if exists { + ts, err := strconv.ParseInt(expires, 10, 64) + if err != nil { + expiry := time.Unix(ts, 0) + if time.Now().After(expiry) { + if err := l.refreshAzureToken(); err != nil { + log.Printf("Error refreshing token: %v", err) + } + } + } + } + } + + // Use AAD access token + if l.user.AuthProvider.Config != nil { + token, exists := l.user.AuthProvider.Config["access-token"] + if exists { + l.restConfig.token = "Bearer " + token + return true + } + } + return false +} + +func (l *KubeConfigLoader) refreshAzureToken() error { + tenantID, exists := l.user.AuthProvider.Config["tenant-id"] + if !exists { + return fmt.Errorf("Missing tenant id in authProvider.config") + } + clientID, exists := l.user.AuthProvider.Config["client-id"] + if !exists { + return fmt.Errorf("Missing client id!") + } + + aadEndpoint := "https://login.microsoftonline.com" + config, err := adal.NewOAuthConfig(aadEndpoint, tenantID) + if err != nil { + return err + } + resource := aadEndpoint + "/" + tenantID + token := adal.Token{ + AccessToken: l.user.AuthProvider.Config["access-token"], + RefreshToken: l.user.AuthProvider.Config["refresh-token"], + ExpiresIn: l.user.AuthProvider.Config["expires-in"], + ExpiresOn: l.user.AuthProvider.Config["expires-in"], + } + sptToken, err := adal.NewServicePrincipalTokenFromManualToken(*config, clientID, resource, token) + if err := sptToken.Refresh(); err != nil { + return err + } + l.user.AuthProvider.Config["access-token"] = sptToken.OAuthToken() + return nil +} diff --git a/kubernetes/config/gcp.go b/kubernetes/config/gcp.go new file mode 100644 index 0000000..3652c5b --- /dev/null +++ b/kubernetes/config/gcp.go @@ -0,0 +1,111 @@ +/* +Copyright 2018 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 config + +import ( + "context" + "fmt" + + "github.com/golang/glog" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" +) + +const ( + gcpRFC3339Format = "2006-01-02 15:04:05" +) + +// GoogleCredentialLoader defines the interface for getting GCP token +type GoogleCredentialLoader interface { + GetGoogleCredentials() (*oauth2.Token, error) +} + +func (l *KubeConfigLoader) loadGCPToken() bool { + if l.user.AuthProvider == nil || l.user.AuthProvider.Name != "gcp" { + return false + } + + // Refresh GCP token if necessary + if l.user.AuthProvider.Config == nil { + if err := l.refreshGCPToken(); err != nil { + glog.Errorf("failed to refresh GCP token: %v", err) + return false + } + } + if _, ok := l.user.AuthProvider.Config["expiry"]; !ok { + if err := l.refreshGCPToken(); err != nil { + glog.Errorf("failed to refresh GCP token: %v", err) + return false + } + } + expired, err := isExpired(l.user.AuthProvider.Config["expiry"]) + if err != nil { + glog.Errorf("failed to determine if GCP token is expired: %v", err) + return false + } + + if expired { + if err := l.refreshGCPToken(); err != nil { + glog.Errorf("failed to refresh GCP token: %v", err) + return false + } + } + + // Use GCP access token + l.restConfig.token = "Bearer " + l.user.AuthProvider.Config["access-token"] + return true +} + +func (l *KubeConfigLoader) refreshGCPToken() error { + if l.user.AuthProvider.Config == nil { + l.user.AuthProvider.Config = map[string]string{} + } + + // Get *oauth2.Token through Google APIs + if l.gcLoader == nil { + l.gcLoader = DefaultGoogleCredentialLoader{} + } + credentials, err := l.gcLoader.GetGoogleCredentials() + if err != nil { + return err + } + + // Store credentials to Config + l.user.AuthProvider.Config["access-token"] = credentials.AccessToken + l.user.AuthProvider.Config["expiry"] = credentials.Expiry.Format(gcpRFC3339Format) + + setUserWithName(l.rawConfig.AuthInfos, l.currentContext.AuthInfo, &l.user) + // Persist kube config file + if !l.skipConfigPersist { + if err := l.persistConfig(); err != nil { + return err + } + } + return nil +} + +// DefaultGoogleCredentialLoader provides the default method for getting GCP token +type DefaultGoogleCredentialLoader struct{} + +// GetGoogleCredentials fetches GCP using default locations +func (l DefaultGoogleCredentialLoader) GetGoogleCredentials() (*oauth2.Token, error) { + credentials, err := google.FindDefaultCredentials(context.Background(), "https://www.googleapis.com/auth/cloud-platform") + if err != nil { + return nil, fmt.Errorf("failed to get Google credentials: %v", err) + } + return credentials.TokenSource.Token() +} diff --git a/kubernetes/config/incluster_config.go b/kubernetes/config/incluster_config.go new file mode 100644 index 0000000..dc92df6 --- /dev/null +++ b/kubernetes/config/incluster_config.go @@ -0,0 +1,74 @@ +/* +Copyright 2018 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 config + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + + "github.com/kubernetes-client/go/kubernetes/client" +) + +const ( + serviceHostEnvName = "KUBERNETES_SERVICE_HOST" + servicePortEnvName = "KUBERNETES_SERVICE_PORT" + serviceTokenFilename = "/var/run/secrets/kubernetes.io/serviceaccount/token" + serviceCertFilename = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" +) + +// InClusterConfig returns a config object which uses the service account +// kubernetes gives to pods. It's intended for clients that expect to be +// running inside a pod running on kubernetes. It will return an error if +// called from a process not running in a kubernetes environment. +func InClusterConfig() (*client.Configuration, error) { + host, port := os.Getenv(serviceHostEnvName), os.Getenv(servicePortEnvName) + if len(host) == 0 || len(port) == 0 { + return nil, fmt.Errorf("unable to load in-cluster configuration, %v and %v must be defined", serviceHostEnvName, servicePortEnvName) + } + + token, err := ioutil.ReadFile(serviceTokenFilename) + if err != nil { + return nil, err + } + caCert, err := ioutil.ReadFile(serviceCertFilename) + if err != nil { + return nil, err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + c := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: caCertPool, + }, + }, + } + + return &client.Configuration{ + BasePath: "https://" + net.JoinHostPort(host, port), + Host: net.JoinHostPort(host, port), + Scheme: "https", + DefaultHeader: map[string]string{"Authentication": "Bearer " + string(token)}, + UserAgent: defaultUserAgent, + HTTPClient: c, + }, nil +} diff --git a/kubernetes/config/kube_config.go b/kubernetes/config/kube_config.go new file mode 100644 index 0000000..ffa6e12 --- /dev/null +++ b/kubernetes/config/kube_config.go @@ -0,0 +1,285 @@ +/* +Copyright 2018 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 config + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path" + "strings" + + "github.com/ghodss/yaml" + "github.com/kubernetes-client/go/kubernetes/client" + "github.com/kubernetes-client/go/kubernetes/config/api" +) + +const ( + defaultUserAgent = "Swagger-Codegen/0.1.0a1/go" + kubeConfigEnvName = "KUBECONFIG" + kubeConfigDefaultFilename = ".kube/config" +) + +// KubeConfigLoader implements the util functions to load authentication and cluster +// info and hosts intermediate info values. +type KubeConfigLoader struct { + rawConfig api.Config + restConfig RestConfig + + // Skip config persistence, default to false + skipConfigPersist bool + configFilename string + + // Current cluster, user and context + cluster api.Cluster + user api.AuthInfo + currentContext api.Context + + // Set this interface to pass in custom Google credential loader instead of + // using the default loader + gcLoader GoogleCredentialLoader +} + +// RestConfig contains the information that a rest client needs to talk with a server +type RestConfig struct { + basePath string + host string + scheme string + + // authentication token + token string + + // TLS info + caCert []byte + clientCert []byte + clientKey []byte + + // skip TLS verification, default to false + skipTLSVerify bool +} + +// LoadKubeConfig loads authentication and cluster information from kube-config file +// and stores them in returned client.Configuration. +func LoadKubeConfig() (*client.Configuration, error) { + kubeConfigFilename := os.Getenv(kubeConfigEnvName) + // Fallback to default kubeconfig file location if no env variable set + if kubeConfigFilename == "" { + kubeConfigFilename = path.Join(os.Getenv("HOME"), kubeConfigDefaultFilename) + } + + loader, err := NewKubeConfigLoaderFromYAMLFile(kubeConfigFilename, false) + if err != nil { + return nil, err + } + + return loader.LoadAndSet() +} + +// NewKubeConfigLoaderFromYAMLFile creates a new KubeConfigLoader with a parsed +// config yaml file. +func NewKubeConfigLoaderFromYAMLFile(filename string, skipConfigPersist bool) (*KubeConfigLoader, error) { + kubeConfig, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + // Init an empty Config as unmarshal layout template + c := api.Config{} + if err := yaml.Unmarshal(kubeConfig, &c); err != nil { + return nil, err + } + + l := KubeConfigLoader{ + rawConfig: c, + skipConfigPersist: skipConfigPersist, + configFilename: filename, + } + + // Init loader with current cluster, user and context + if err := l.LoadActiveContext(); err != nil { + return nil, err + } + return &l, nil +} + +// LoadAndSet loads authentication and cluster information from kube-config file and +// stores them in returned Configuration. +func (l *KubeConfigLoader) LoadAndSet() (*client.Configuration, error) { + l.loadAuthentication() + + if err := l.loadClusterInfo(); err != nil { + return nil, err + } + return l.setConfig() +} + +// loadClusterInfo uses the current cluster, user and context info stored in loader and +// gets necessary TLS information +func (l *KubeConfigLoader) loadClusterInfo() error { + // The swagger-codegen go client doesn't work well with base path having trailing slash. + // This is a short term fix. + l.restConfig.basePath = strings.TrimRight(l.cluster.Server, "/") + + u, err := url.Parse(l.cluster.Server) + if err != nil { + return err + } + l.restConfig.host = u.Host + l.restConfig.scheme = u.Scheme + + if l.cluster.InsecureSkipTLSVerify { + l.restConfig.skipTLSVerify = true + } + + if l.restConfig.scheme == "https" { + if !l.restConfig.skipTLSVerify { + l.restConfig.caCert, err = DataOrFile(l.cluster.CertificateAuthorityData, l.cluster.CertificateAuthority) + if err != nil { + return fmt.Errorf("Error loading certificate-authority: %s", err.Error()) + } + } + l.restConfig.clientCert, err = DataOrFile(l.user.ClientCertificateData, l.user.ClientCertificate) + if err != nil { + return fmt.Errorf("Error loading client certificate: %s", err.Error()) + } + l.restConfig.clientKey, err = DataOrFile(l.user.ClientKeyData, l.user.ClientKey) + if err != nil { + return fmt.Errorf("Error loading client key: %s", err.Error()) + } + } + + return nil +} + +// setConfig converts authentication and TLS info into client Configuration +func (l *KubeConfigLoader) setConfig() (*client.Configuration, error) { + // Set TLS info + transport := http.Transport{} + if l.restConfig.scheme == "https" { + certs := []tls.Certificate{} + if len(l.restConfig.clientCert) > 0 || len(l.restConfig.clientKey) > 0 { + cert, err := tls.X509KeyPair(l.restConfig.clientCert, l.restConfig.clientKey) + if err != nil { + return nil, err + } + certs = append(certs, cert) + } + caCertPool := x509.NewCertPool() + if len(l.restConfig.caCert) > 0 { + caCertPool.AppendCertsFromPEM(l.restConfig.caCert) + } + transport.TLSClientConfig = &tls.Config{ + RootCAs: caCertPool, + Certificates: certs, + InsecureSkipVerify: l.restConfig.skipTLSVerify, + } + } + + c := &http.Client{ + Transport: &transport, + } + + header := make(map[string]string) + // Add authentication info to default header + if l.restConfig.token != "" { + header["Authorization"] = l.restConfig.token + // Handle Golang dropping headers on redirect + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + req.Header.Add("Authorization", l.restConfig.token) + return nil + } + } + + return &client.Configuration{ + BasePath: l.restConfig.basePath, + Host: l.restConfig.host, + Scheme: l.restConfig.scheme, + DefaultHeader: header, + UserAgent: defaultUserAgent, + HTTPClient: c, + }, nil +} + +// RestConfig returns the value of RestConfig in a KubeConfigLoader +func (l *KubeConfigLoader) RestConfig() RestConfig { + return l.restConfig +} + +// SetActiveContext sets the active context in rawConfig, performs necessary persistence, +// and reload active context. This function enables context switch +func (l *KubeConfigLoader) SetActiveContext(ctx string) error { + currentContext, err := getContextWithName(l.rawConfig.Contexts, ctx) + if err != nil { + return err + } + currentContext.DeepCopyInto(&l.currentContext) + l.rawConfig.CurrentContext = ctx + + // Persist kube config file + if !l.skipConfigPersist { + if err := l.persistConfig(); err != nil { + return err + } + } + + return l.LoadActiveContext() +} + +// LoadActiveContext parses the loader's rawConfig using current context and set loader's +// current cluster and user. +func (l *KubeConfigLoader) LoadActiveContext() error { + currentContext, err := getContextWithName(l.rawConfig.Contexts, l.rawConfig.CurrentContext) + if err != nil { + return err + } + currentContext.DeepCopyInto(&l.currentContext) + + cluster, err := getClusterWithName(l.rawConfig.Clusters, l.currentContext.Cluster) + if err != nil { + return err + } + cluster.DeepCopyInto(&l.cluster) + + user, err := getUserWithName(l.rawConfig.AuthInfos, l.currentContext.AuthInfo) + if err != nil { + return err + } + + // kube config may have no (current) user + if user != nil { + user.DeepCopyInto(&l.user) + } + return nil +} + +// persisConfig saves the stored rawConfig to the config file. This function is not exposed and +// should be called only when skipConfigPersist is false. +// TODO(roycaihw): enable custom persistConfig function +func (l *KubeConfigLoader) persistConfig() error { + if l.skipConfigPersist { + return nil + } + data, err := yaml.Marshal(l.rawConfig) + if err != nil { + return err + } + return ioutil.WriteFile(l.configFilename, data, 0644) +} diff --git a/kubernetes/config/kube_config_test.go b/kubernetes/config/kube_config_test.go new file mode 100644 index 0000000..3ea5dc9 --- /dev/null +++ b/kubernetes/config/kube_config_test.go @@ -0,0 +1,484 @@ +/* +Copyright 2018 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 config + +import ( + b64 "encoding/base64" + "fmt" + "io/ioutil" + "net/url" + "os" + "reflect" + "strings" + "testing" + "time" + + "golang.org/x/oauth2" + "k8s.io/client/kubernetes/config/api" +) + +const ( + testData = "test-data" + testAnotherData = "test-another-data" + + testServer = "http://test-server" + testUsername = "me" + testPassword = "pass" + + // token for me:pass + testBasicToken = "Basic bWU6cGFzcw==" + + testSSLServer = "https://test-server" + testCertAuth = "cert-auth" + testClientKey = "client-key" + testClientCert = "client-cert" + + bearerTokenFormat = "Bearer %s" + testTokenExpiry = "2000-01-01 12:00:00" // always in past +) + +var ( + // base64 encoded string, used as a test token + testDataBase64 = b64.StdEncoding.EncodeToString([]byte(testData)) + + // base64 encoded string, used as another test token + testAnotherDataBase64 = b64.StdEncoding.EncodeToString([]byte(testAnotherData)) + + testCertAuthBase64 = stringToBase64(testCertAuth) + + testClientKeyBase64 = stringToBase64(testClientKey) + + testClientCertBase64 = stringToBase64(testClientCert) + + // test time set to time.Now() + 2 * expirySkewPreventionDelay, which doesn't expire + testTokenNoExpiry = time.Now().Add(2 * expirySkewPreventionDelay).UTC().Format(gcpRFC3339Format) +) + +var testKubeConfig = api.Config{ + CurrentContext: "no_user", + Contexts: []api.NamedContext{ + { + Name: "no_user", + Context: api.Context{ + Cluster: "default", + }, + }, + { + Name: "non_existing_user", + Context: api.Context{ + Cluster: "default", + AuthInfo: "non_existing_user", + }, + }, + { + Name: "simple_token", + Context: api.Context{ + Cluster: "default", + AuthInfo: "simple_token", + }, + }, + { + Name: "gcp", + Context: api.Context{ + Cluster: "default", + AuthInfo: "gcp", + }, + }, + { + Name: "expired_gcp", + Context: api.Context{ + Cluster: "default", + AuthInfo: "expired_gcp", + }, + }, + { + Name: "user_pass", + Context: api.Context{ + Cluster: "default", + AuthInfo: "user_pass", + }, + }, + { + Name: "ssl", + Context: api.Context{ + Cluster: "ssl", + AuthInfo: "ssl", + }, + }, + { + Name: "ssl_no_verification", + Context: api.Context{ + Cluster: "ssl_no_verification", + AuthInfo: "ssl", + }, + }, + { + Name: "ssl_no_file", + Context: api.Context{ + Cluster: "ssl_no_file", + AuthInfo: "ssl_no_file", + }, + }, + }, + Clusters: []api.NamedCluster{ + { + Name: "default", + Cluster: api.Cluster{ + Server: testServer, + }, + }, + { + Name: "ssl", + Cluster: api.Cluster{ + Server: testSSLServer, + CertificateAuthorityData: testCertAuthBase64, + }, + }, + { + Name: "ssl_no_verification", + Cluster: api.Cluster{ + Server: testSSLServer, + InsecureSkipTLSVerify: true, + }, + }, + { + Name: "ssl_no_file", + Cluster: api.Cluster{ + Server: testSSLServer, + CertificateAuthority: "test-cert-no-file", + }, + }, + }, + AuthInfos: []api.NamedAuthInfo{ + { + Name: "simple_token", + AuthInfo: api.AuthInfo{ + Token: testDataBase64, + Username: testUsername, + Password: testPassword, + }, + }, + { + Name: "gcp", + AuthInfo: api.AuthInfo{ + AuthProvider: &api.AuthProviderConfig{ + Name: "gcp", + Config: map[string]string{ + "access-token": testDataBase64, + "expiry": testTokenNoExpiry, + }, + }, + Token: testDataBase64, + Username: testUsername, + Password: testPassword, + }, + }, + { + Name: "expired_gcp", + AuthInfo: api.AuthInfo{ + AuthProvider: &api.AuthProviderConfig{ + Name: "gcp", + Config: map[string]string{ + "access-token": testDataBase64, + "expiry": testTokenExpiry, + }, + }, + Token: testDataBase64, + Username: testUsername, + Password: testPassword, + }, + }, + { + Name: "user_pass", + AuthInfo: api.AuthInfo{ + Username: testUsername, + Password: testPassword, + }, + }, + { + Name: "ssl", + AuthInfo: api.AuthInfo{ + Token: testDataBase64, + ClientCertificateData: testClientCertBase64, + ClientKeyData: testClientKeyBase64, + }, + }, + { + Name: "ssl_no_file", + AuthInfo: api.AuthInfo{ + Token: testDataBase64, + ClientCertificate: "test-client-cert-no-file", + ClientKey: "test-client-key-no-file", + }, + }, + }, +} + +func TestLoadKubeConfig(t *testing.T) { + tcs := []struct { + ActiveContext string + + Server string + Token string + CACert []byte + Cert []byte + Key []byte + SkipTLSVerify bool + GCLoader GoogleCredentialLoader + }{ + { + ActiveContext: "no_user", + Server: testServer, + }, + { + ActiveContext: "non_existing_user", + Server: testServer, + }, + { + ActiveContext: "simple_token", + Server: testServer, + Token: fmt.Sprintf(bearerTokenFormat, testDataBase64), + }, + { + ActiveContext: "user_pass", + Server: testServer, + Token: testBasicToken, + }, + { + ActiveContext: "gcp", + Server: testServer, + Token: fmt.Sprintf(bearerTokenFormat, testDataBase64), + GCLoader: FakeGoogleCredentialLoaderNoRefresh{}, + }, + { + ActiveContext: "expired_gcp", + Server: testServer, + Token: fmt.Sprintf(bearerTokenFormat, testAnotherDataBase64), + GCLoader: FakeGoogleCredentialLoader{}, + }, + { + ActiveContext: "ssl", + Server: testSSLServer, + Token: fmt.Sprintf(bearerTokenFormat, testDataBase64), + CACert: testCertAuthBase64, + Cert: testClientCertBase64, + Key: testClientKeyBase64, + }, + { + ActiveContext: "ssl_no_verification", + Server: testSSLServer, + Token: fmt.Sprintf(bearerTokenFormat, testDataBase64), + Cert: testClientCertBase64, + Key: testClientKeyBase64, + SkipTLSVerify: true, + }, + } + + for _, tc := range tcs { + expected, err := FakeConfig(tc.Server, tc.Token, tc.CACert, tc.Cert, tc.Key, tc.SkipTLSVerify) + if err != nil { + t.Errorf("context %v, unexpected error setting up fake config: %v", tc.ActiveContext, err) + } + + actual := KubeConfigLoader{ + rawConfig: testKubeConfig, + skipConfigPersist: true, + gcLoader: tc.GCLoader, + } + if err := actual.SetActiveContext(tc.ActiveContext); err != nil { + t.Errorf("context %v, unexpected error setting config active context: %v", tc.ActiveContext, err) + } + + // We are only testing loading auth and TLS info in LoadAndSet; we are not testing setting + // the generate client's Configuration based on the restConfig, because we are using fake + // data as TLS cert, which would fail PEM validation + actual.loadAuthentication() + if err := actual.loadClusterInfo(); err != nil { + t.Errorf("context %v, unexpected error loading kube config: %v", tc.ActiveContext, err) + } + if !reflect.DeepEqual(expected, actual.RestConfig()) { + t.Errorf("context %v, config loaded mismatch: want %v, got %v", tc.ActiveContext, expected, actual.RestConfig()) + } + } +} + +func TestLoadKubeConfigSSLNoFile(t *testing.T) { + actual := KubeConfigLoader{ + rawConfig: testKubeConfig, + skipConfigPersist: true, + } + if err := actual.SetActiveContext("ssl_no_file"); err != nil { + t.Errorf("context %v, unexpected error setting config active context: %v", "ssl_no_file", err) + } + + // We are only testing loading auth and TLS info in LoadAndSet; we are not testing setting + // the generate client's Configuration based on the restConfig, because we are using fake + // data as TLS cert, which would fail PEM validation + actual.loadAuthentication() + if err := actual.loadClusterInfo(); err == nil || !strings.Contains(err.Error(), "failed to get data or file") { + t.Errorf("context %v, expecting failure to get file, got: %v", "ssl_no_file", err) + } +} + +func TestLoadKubeConfigSSLLocalFile(t *testing.T) { + tc := struct { + ActiveContext string + + Server string + Token string + CACert []byte + Cert []byte + Key []byte + SkipTLSVerify bool + }{ + + ActiveContext: "ssl_local_file", + Server: testSSLServer, + Token: fmt.Sprintf(bearerTokenFormat, testDataBase64), + CACert: testCertAuthBase64, + Cert: testClientCertBase64, + Key: testClientKeyBase64, + } + + expected, err := FakeConfig(tc.Server, tc.Token, tc.CACert, tc.Cert, tc.Key, tc.SkipTLSVerify) + if err != nil { + t.Errorf("context %v, unexpected error setting up fake config: %v", tc.ActiveContext, err) + } + + // Set up CA cert file + testCACertFile, err := ioutil.TempFile(os.TempDir(), "ca-cert") + if err != nil { + t.Errorf("error: failed to create temp ca-cert file") + } + defer os.Remove(testCACertFile.Name()) + if err := ioutil.WriteFile(testCACertFile.Name(), testCertAuthBase64, 0644); err != nil { + t.Errorf("context %v, unexpected error writing temp file %v: %v", tc.ActiveContext, testCACertFile.Name(), err) + } + + // Set up token file + testTokenFile, err := ioutil.TempFile(os.TempDir(), "token") + if err != nil { + t.Errorf("error: failed to create temp token file") + } + defer os.Remove(testTokenFile.Name()) + if err := ioutil.WriteFile(testTokenFile.Name(), []byte(testDataBase64), 0644); err != nil { + t.Errorf("context %v, unexpected error writing temp file %v: %v", tc.ActiveContext, testTokenFile.Name(), err) + } + + // Set up client cert file + testClientCertFile, err := ioutil.TempFile(os.TempDir(), "client-cert") + if err != nil { + t.Errorf("error: failed to create temp client-cert file") + } + defer os.Remove(testClientCertFile.Name()) + if err := ioutil.WriteFile(testClientCertFile.Name(), testClientCertBase64, 0644); err != nil { + t.Errorf("context %v, unexpected error writing temp file %v: %v", tc.ActiveContext, testClientCertFile.Name(), err) + } + + // Set up client key file + testClientKeyFile, err := ioutil.TempFile(os.TempDir(), "client-key") + if err != nil { + t.Errorf("error: failed to create temp client-key file") + } + defer os.Remove(testClientKeyFile.Name()) + if err := ioutil.WriteFile(testClientKeyFile.Name(), testClientKeyBase64, 0644); err != nil { + t.Errorf("context %v, unexpected error writing temp file %v: %v", tc.ActiveContext, testClientKeyFile.Name(), err) + } + + actual := KubeConfigLoader{ + rawConfig: api.Config{ + CurrentContext: "ssl_local_file", + Contexts: []api.NamedContext{ + { + Name: "ssl_local_file", + Context: api.Context{ + Cluster: "ssl_local_file", + AuthInfo: "ssl_local_file", + }, + }, + }, + Clusters: []api.NamedCluster{ + { + Name: "ssl_local_file", + Cluster: api.Cluster{ + Server: testSSLServer, + CertificateAuthority: testCACertFile.Name(), + }, + }, + }, + AuthInfos: []api.NamedAuthInfo{ + { + Name: "ssl_local_file", + AuthInfo: api.AuthInfo{ + TokenFile: testTokenFile.Name(), + ClientCertificate: testClientCertFile.Name(), + ClientKey: testClientKeyFile.Name(), + }, + }, + }, + }, + skipConfigPersist: true, + } + if err := actual.SetActiveContext(tc.ActiveContext); err != nil { + t.Errorf("context %v, unexpected error setting config active context: %v", tc.ActiveContext, err) + } + + // We are only testing loading auth and TLS info in LoadAndSet; we are not testing setting + // the generate client's Configuration based on the restConfig, because we are using fake + // data as TLS cert, which would fail PEM validation + actual.loadAuthentication() + if err := actual.loadClusterInfo(); err != nil { + t.Errorf("context %v, unexpected error loading kube config: %v", tc.ActiveContext, err) + } + if !reflect.DeepEqual(expected, actual.RestConfig()) { + t.Errorf("context %v, config loaded mismatch: want %v, got %v", tc.ActiveContext, expected, actual.RestConfig()) + } +} + +func FakeConfig(server, token string, caCert, clientCert, clientKey []byte, skipTLSVerify bool) (RestConfig, error) { + u, err := url.Parse(server) + if err != nil { + return RestConfig{}, err + } + + return RestConfig{ + basePath: strings.TrimRight(server, "/"), + host: u.Host, + scheme: u.Scheme, + token: token, + caCert: caCert, + clientCert: clientCert, + clientKey: clientKey, + skipTLSVerify: skipTLSVerify, + }, nil +} + +func stringToBase64(str string) []byte { + return []byte(b64.StdEncoding.EncodeToString([]byte(str))) +} + +type FakeGoogleCredentialLoader struct{} + +func (l FakeGoogleCredentialLoader) GetGoogleCredentials() (*oauth2.Token, error) { + return &oauth2.Token{AccessToken: testAnotherDataBase64, Expiry: time.Now().UTC()}, nil +} + +type FakeGoogleCredentialLoaderNoRefresh struct{} + +func (l FakeGoogleCredentialLoaderNoRefresh) GetGoogleCredentials() (*oauth2.Token, error) { + return nil, fmt.Errorf("should not be called") +} diff --git a/kubernetes/config/util.go b/kubernetes/config/util.go new file mode 100644 index 0000000..d76da38 --- /dev/null +++ b/kubernetes/config/util.go @@ -0,0 +1,122 @@ +/* +Copyright 2018 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 config + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "time" + + "github.com/kubernetes-client/go/kubernetes/config/api" +) + +// DataOrFile reads content of data, or file's content if data doesn't exist +// and represent it as []byte data. +func DataOrFile(data []byte, file string) ([]byte, error) { + if data != nil { + return data, nil + } + result, err := ioutil.ReadFile(file) + if os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get data or file (%s): %v", file, err) + } + return result, nil +} + +// isExpired returns true if the token expired in expirySkewPreventionDelay time (default is 5 minutes) +func isExpired(timestamp string) (bool, error) { + ts, err := time.Parse(gcpRFC3339Format, timestamp) + if err != nil { + return false, err + } + return ts.Before(time.Now().UTC().Add(expirySkewPreventionDelay)), nil +} + +func basicAuthToken(username, password string) string { + return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) +} + +func getContextWithName(contexts []api.NamedContext, name string) (*api.Context, error) { + var context *api.Context + for _, c := range contexts { + if c.Name == name { + if context != nil { + return nil, fmt.Errorf("error parsing kube config: duplicate contexts with name %v", name) + } + context = c.Context.DeepCopy() + } + } + if context == nil { + return nil, fmt.Errorf("error parsing kube config: couldn't find context with name %v", name) + } + return context, nil +} + +func getClusterWithName(clusters []api.NamedCluster, name string) (*api.Cluster, error) { + var cluster *api.Cluster + for _, c := range clusters { + if c.Name == name { + if cluster != nil { + return nil, fmt.Errorf("error parsing kube config: duplicate clusters with name %v", name) + } + cluster = c.Cluster.DeepCopy() + } + } + if cluster == nil { + return nil, fmt.Errorf("error parsing kube config: couldn't find cluster with name %v", name) + } + return cluster, nil +} + +func getUserWithName(users []api.NamedAuthInfo, name string) (*api.AuthInfo, error) { + var user *api.AuthInfo + for _, u := range users { + if u.Name == name { + if user != nil { + return nil, fmt.Errorf("error parsing kube config: duplicate users with name %v", name) + } + user = u.AuthInfo.DeepCopy() + } + } + // A context may have no user, or using non-existing user name. We simply return nil *AuthInfo in this case. + return user, nil +} + +func setUserWithName(users []api.NamedAuthInfo, name string, user *api.AuthInfo) error { + var userFound bool + var userTarget *api.AuthInfo + + for i, u := range users { + if u.Name == name { + if userFound { + return fmt.Errorf("error setting kube config: duplicate users with name %v", name) + } + userTarget = &users[i].AuthInfo + userFound = true + } + } + if !userFound { + return fmt.Errorf("error setting kube config: cannot find user with name: %v", name) + } + user.DeepCopyInto(userTarget) + return nil +} diff --git a/kubernetes/config/util_test.go b/kubernetes/config/util_test.go new file mode 100644 index 0000000..7c9f69b --- /dev/null +++ b/kubernetes/config/util_test.go @@ -0,0 +1,213 @@ +/* +Copyright 2018 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 config + +import ( + "fmt" + "reflect" + "testing" + + "k8s.io/client/kubernetes/config/api" +) + +func TestSetUserWithName(t *testing.T) { + tcs := []struct { + Origin []api.NamedAuthInfo + Name string + User *api.AuthInfo + Expected []api.NamedAuthInfo + }{ + { + Origin: []api.NamedAuthInfo{ + {"A", api.AuthInfo{}}, + {"B", api.AuthInfo{}}, + {"C", api.AuthInfo{}}, + }, + Name: "B", + User: &api.AuthInfo{Token: "test-token"}, + Expected: []api.NamedAuthInfo{ + {"A", api.AuthInfo{}}, + {"B", api.AuthInfo{Token: "test-token"}}, + {"C", api.AuthInfo{}}, + }, + }, + } + + for _, tc := range tcs { + if err := setUserWithName(tc.Origin, tc.Name, tc.User); err != nil { + t.Errorf("unexpected error setting user with name %v: %v", tc.Name, err) + } + + if !reflect.DeepEqual(tc.Origin, tc.Expected) { + t.Errorf("setUserWithName mismatch: want %v, got %v", tc.Expected, tc.Origin) + } + } +} + +func TestGetUserWithName(t *testing.T) { + users := []api.NamedAuthInfo{ + {"A", api.AuthInfo{}}, + {"B", api.AuthInfo{Token: "test-token"}}, + {"D", api.AuthInfo{}}, + {"D", api.AuthInfo{}}, + } + + tcs := []struct { + Name string + ExpectedUser *api.AuthInfo + ExpectedError error + }{ + { + Name: "A", + ExpectedUser: &api.AuthInfo{}, + ExpectedError: nil, + }, + { + Name: "B", + ExpectedUser: &api.AuthInfo{Token: "test-token"}, + ExpectedError: nil, + }, + { + Name: "C", + ExpectedUser: nil, + // A context may have no user, or using non-existing user name. + // We simply return nil *api.AuthInfo in this case. + ExpectedError: nil, + }, + { + Name: "D", + ExpectedUser: nil, + ExpectedError: fmt.Errorf("error parsing kube config: duplicate users with name D"), + }, + } + + for _, tc := range tcs { + user, err := getUserWithName(users, tc.Name) + + if !reflect.DeepEqual(tc.ExpectedUser, user) { + t.Errorf("getUserWithName mismatch: want %v, got %v", tc.ExpectedUser, user) + } + + if !reflect.DeepEqual(tc.ExpectedError, err) { + t.Errorf("getUserWithName error mismatch: want %v, got %v", tc.ExpectedError, err) + } + } +} + +func TestGetContextWithName(t *testing.T) { + contexts := []api.NamedContext{ + {"A", api.Context{}}, + {"B", api.Context{ + Cluster: "test-cluster", + AuthInfo: "test-user", + Namespace: "test-namespace", + }}, + {"D", api.Context{}}, + {"D", api.Context{}}, + } + + tcs := []struct { + Name string + ExpectedContext *api.Context + ExpectedError error + }{ + { + Name: "A", + ExpectedContext: &api.Context{}, + ExpectedError: nil, + }, + { + Name: "B", + ExpectedContext: &api.Context{ + Cluster: "test-cluster", + AuthInfo: "test-user", + Namespace: "test-namespace", + }, + ExpectedError: nil, + }, + { + Name: "C", + ExpectedContext: nil, + ExpectedError: fmt.Errorf("error parsing kube config: couldn't find context with name C"), + }, + { + Name: "D", + ExpectedContext: nil, + ExpectedError: fmt.Errorf("error parsing kube config: duplicate contexts with name D"), + }, + } + + for _, tc := range tcs { + context, err := getContextWithName(contexts, tc.Name) + + if !reflect.DeepEqual(tc.ExpectedContext, context) { + t.Errorf("getContextWithName mismatch: want %v, got %v", tc.ExpectedContext, context) + } + + if !reflect.DeepEqual(tc.ExpectedError, err) { + t.Errorf("getContextWithName error mismatch: want %v, got %v", tc.ExpectedError, err) + } + } +} + +func TestGetClusterWithName(t *testing.T) { + clusters := []api.NamedCluster{ + {"A", api.Cluster{}}, + {"B", api.Cluster{Server: "test-server"}}, + {"D", api.Cluster{}}, + {"D", api.Cluster{}}, + } + + tcs := []struct { + Name string + ExpectedCluster *api.Cluster + ExpectedError error + }{ + { + Name: "A", + ExpectedCluster: &api.Cluster{}, + ExpectedError: nil, + }, + { + Name: "B", + ExpectedCluster: &api.Cluster{Server: "test-server"}, + ExpectedError: nil, + }, + { + Name: "C", + ExpectedCluster: nil, + ExpectedError: fmt.Errorf("error parsing kube config: couldn't find cluster with name C"), + }, + { + Name: "D", + ExpectedCluster: nil, + ExpectedError: fmt.Errorf("error parsing kube config: duplicate clusters with name D"), + }, + } + + for _, tc := range tcs { + cluster, err := getClusterWithName(clusters, tc.Name) + + if !reflect.DeepEqual(tc.ExpectedCluster, cluster) { + t.Errorf("getClusterWithName mismatch: want %v, got %v", tc.ExpectedCluster, cluster) + } + + if !reflect.DeepEqual(tc.ExpectedError, err) { + t.Errorf("getClusterWithName error mismatch: want %v, got %v", tc.ExpectedError, err) + } + } +}