diff --git a/kubernetes/config/authentication.go b/kubernetes/config/authentication.go new file mode 100644 index 0000000..48904d6 --- /dev/null +++ b/kubernetes/config/authentication.go @@ -0,0 +1,77 @@ +/* +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. + + if l.loadGCPToken() || l.loadUserToken() || l.loadUserPassToken() { + 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/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..c98069e --- /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" + + "k8s.io/client/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..2744150 --- /dev/null +++ b/kubernetes/config/kube_config.go @@ -0,0 +1,277 @@ +/* +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" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + + "github.com/ghodss/yaml" + + "k8s.io/client/kubernetes/client" + "k8s.io/client/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 = 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 api.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 client.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 err + } + } + l.restConfig.clientCert, err = DataOrFile(l.user.ClientCertificateData, l.user.ClientCertificate) + if err != nil { + return err + } + l.restConfig.clientKey, err = DataOrFile(l.user.ClientKeyData, l.user.ClientKey) + if err != nil { + return err + } + } + + 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" && !l.restConfig.skipTLSVerify { + cert, err := tls.X509KeyPair(l.restConfig.clientCert, l.restConfig.clientKey) + if err != nil { + return nil, err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(l.restConfig.caCert) + transport.TLSClientConfig = &tls.Config{ + RootCAs: caCertPool, + Certificates: []tls.Certificate{cert}, + } + } + + 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..9a10af9 --- /dev/null +++ b/kubernetes/config/util.go @@ -0,0 +1,118 @@ +/* +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" + "time" + + "k8s.io/client/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 err != nil { + return nil, fmt.Errorf("failed to get data or file: %v", 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 *api.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) + } + } +}