Switch to github.com/golang/dep for vendoring

Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
This commit is contained in:
Mrunal Patel 2017-01-31 16:45:59 -08:00
parent d6ab91be27
commit 8e5b17cf13
15431 changed files with 3971413 additions and 8881 deletions

70
vendor/k8s.io/kubernetes/pkg/client/unversioned/BUILD generated vendored Normal file
View file

@ -0,0 +1,70 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"conditions.go",
"helper.go",
],
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/apis/apps:go_default_library",
"//pkg/apis/batch:go_default_library",
"//pkg/apis/extensions:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/apps/internalversion:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/batch/internalversion:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/extensions/internalversion:go_default_library",
"//pkg/client/restclient:go_default_library",
"//plugin/pkg/client/auth:go_default_library",
"//vendor:k8s.io/apimachinery/pkg/api/errors",
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
"//vendor:k8s.io/apimachinery/pkg/util/wait",
"//vendor:k8s.io/apimachinery/pkg/watch",
],
)
go_test(
name = "go_default_test",
srcs = ["helper_test.go"],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/api/testapi:go_default_library",
"//pkg/client/restclient:go_default_library",
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/client/unversioned/auth:all-srcs",
"//pkg/client/unversioned/clientcmd:all-srcs",
"//pkg/client/unversioned/portforward:all-srcs",
"//pkg/client/unversioned/remotecommand:all-srcs",
"//pkg/client/unversioned/testclient/simple:all-srcs",
],
tags = ["automanaged"],
)

40
vendor/k8s.io/kubernetes/pkg/client/unversioned/OWNERS generated vendored Executable file
View file

@ -0,0 +1,40 @@
reviewers:
- thockin
- lavalamp
- smarterclayton
- wojtek-t
- deads2k
- derekwaynecarr
- caesarxuchao
- vishh
- mikedanese
- liggitt
- nikhiljindal
- bprashanth
- ixdy
- gmarek
- erictune
- davidopp
- pmorie
- sttts
- kargakis
- saad-ali
- zmerlynn
- janetkuo
- ncdc
- timstclair
- mwielgus
- timothysc
- feiskyer
- jlowdermilk
- soltysh
- piosz
- dims
- errordeveloper
- madhusudancs
- hongchaodeng
- krousey
- jayunit100
- jszczepkowski
- jdef
- mml

View file

@ -0,0 +1,36 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["clientauth.go"],
tags = ["automanaged"],
deps = ["//pkg/client/restclient:go_default_library"],
)
go_test(
name = "go_default_xtest",
srcs = ["clientauth_test.go"],
tags = ["automanaged"],
deps = ["//pkg/client/unversioned/auth:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,125 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Package auth defines a file format for holding authentication
information needed by clients of Kubernetes. Typically,
a Kubernetes cluster will put auth info for the admin in a known
location when it is created, and will (soon) put it in a known
location within a Container's file tree for Containers that
need access to the Kubernetes API.
Having a defined format allows:
- clients to be implmented in multiple languages
- applications which link clients to be portable across
clusters with different authentication styles (e.g.
some may use SSL Client certs, others may not, etc)
- when the format changes, applications only
need to update this code.
The file format is json, marshalled from a struct authcfg.Info.
Clinet libraries in other languages should use the same format.
It is not intended to store general preferences, such as default
namespace, output options, etc. CLIs (such as kubectl) and UIs should
develop their own format and may wish to inline the authcfg.Info type.
The authcfg.Info is just a file format. It is distinct from
client.Config which holds options for creating a client.Client.
Helper functions are provided in this package to fill in a
client.Client from an authcfg.Info.
Example:
import (
"pkg/client"
"pkg/client/auth"
)
info, err := auth.LoadFromFile(filename)
if err != nil {
// handle error
}
clientConfig = client.Config{}
clientConfig.Host = "example.com:4901"
clientConfig = info.MergeWithConfig()
client := client.New(clientConfig)
client.Pods(ns).List()
*/
package auth
// TODO: need a way to rotate Tokens. Therefore, need a way for client object to be reset when the authcfg is updated.
import (
"encoding/json"
"io/ioutil"
"os"
"k8s.io/kubernetes/pkg/client/restclient"
)
// Info holds Kubernetes API authorization config. It is intended
// to be read/written from a file as a JSON object.
type Info struct {
User string
Password string
CAFile string
CertFile string
KeyFile string
BearerToken string
Insecure *bool
}
// LoadFromFile parses an Info object from a file path.
// If the file does not exist, then os.IsNotExist(err) == true
func LoadFromFile(path string) (*Info, error) {
var info Info
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &info)
if err != nil {
return nil, err
}
return &info, err
}
// MergeWithConfig returns a copy of a client.Config with values from the Info.
// The fields of client.Config with a corresponding field in the Info are set
// with the value from the Info.
func (info Info) MergeWithConfig(c restclient.Config) (restclient.Config, error) {
var config restclient.Config = c
config.Username = info.User
config.Password = info.Password
config.CAFile = info.CAFile
config.CertFile = info.CertFile
config.KeyFile = info.KeyFile
config.BearerToken = info.BearerToken
if info.Insecure != nil {
config.Insecure = *info.Insecure
}
return config, nil
}
func (info Info) Complete() bool {
return len(info.User) > 0 ||
len(info.CertFile) > 0 ||
len(info.BearerToken) > 0
}

View file

@ -0,0 +1,69 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package auth_test
import (
"io/ioutil"
"os"
"reflect"
"testing"
clientauth "k8s.io/kubernetes/pkg/client/unversioned/auth"
)
func TestLoadFromFile(t *testing.T) {
loadAuthInfoTests := []struct {
authData string
authInfo *clientauth.Info
expectErr bool
}{
{
`{"user": "user", "password": "pass"}`,
&clientauth.Info{User: "user", Password: "pass"},
false,
},
{
"", nil, true,
},
}
for _, loadAuthInfoTest := range loadAuthInfoTests {
tt := loadAuthInfoTest
aifile, err := ioutil.TempFile("", "testAuthInfo")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if tt.authData != "missing" {
defer os.Remove(aifile.Name())
defer aifile.Close()
_, err = aifile.WriteString(tt.authData)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
} else {
aifile.Close()
os.Remove(aifile.Name())
}
authInfo, err := clientauth.LoadFromFile(aifile.Name())
gotErr := err != nil
if gotErr != tt.expectErr {
t.Errorf("expected errorness: %v, actual errorness: %v", tt.expectErr, gotErr)
}
if !reflect.DeepEqual(authInfo, tt.authInfo) {
t.Errorf("Expected %v, got %v", tt.authInfo, authInfo)
}
}
}

View file

@ -0,0 +1,78 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"auth_loaders.go",
"client_config.go",
"config.go",
"doc.go",
"helpers.go",
"loader.go",
"merged_client_builder.go",
"overrides.go",
"validation.go",
],
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/client/unversioned/auth:go_default_library",
"//pkg/client/unversioned/clientcmd/api:go_default_library",
"//pkg/client/unversioned/clientcmd/api/latest:go_default_library",
"//pkg/util/homedir:go_default_library",
"//vendor:github.com/golang/glog",
"//vendor:github.com/howeyc/gopass",
"//vendor:github.com/imdario/mergo",
"//vendor:github.com/spf13/pflag",
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
"//vendor:k8s.io/apimachinery/pkg/util/errors",
"//vendor:k8s.io/apimachinery/pkg/util/validation",
],
)
go_test(
name = "go_default_test",
srcs = [
"client_config_test.go",
"loader_test.go",
"merged_client_builder_test.go",
"validation_test.go",
],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//pkg/client/restclient:go_default_library",
"//pkg/client/unversioned/clientcmd/api:go_default_library",
"//pkg/client/unversioned/clientcmd/api/latest:go_default_library",
"//vendor:github.com/ghodss/yaml",
"//vendor:github.com/imdario/mergo",
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/util/errors",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/client/unversioned/clientcmd/api:all-srcs",
],
tags = ["automanaged"],
)

View file

@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"helpers.go",
"register.go",
"types.go",
],
tags = ["automanaged"],
deps = [
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
],
)
go_test(
name = "go_default_test",
srcs = [
"helpers_test.go",
"types_test.go",
],
library = ":go_default_library",
tags = ["automanaged"],
deps = ["//vendor:github.com/ghodss/yaml"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/client/unversioned/clientcmd/api/latest:all-srcs",
"//pkg/client/unversioned/clientcmd/api/v1:all-srcs",
],
tags = ["automanaged"],
)

View file

@ -0,0 +1,183 @@
/*
Copyright 2015 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
import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
)
func init() {
sDec, _ := base64.StdEncoding.DecodeString("REDACTED+")
redactedBytes = []byte(string(sDec))
}
// IsConfigEmpty returns true if the config is empty.
func IsConfigEmpty(config *Config) bool {
return len(config.AuthInfos) == 0 && len(config.Clusters) == 0 && len(config.Contexts) == 0 &&
len(config.CurrentContext) == 0 &&
len(config.Preferences.Extensions) == 0 && !config.Preferences.Colors &&
len(config.Extensions) == 0
}
// MinifyConfig read the current context and uses that to keep only the relevant pieces of config
// This is useful for making secrets based on kubeconfig files
func MinifyConfig(config *Config) error {
if len(config.CurrentContext) == 0 {
return errors.New("current-context must exist in order to minify")
}
currContext, exists := config.Contexts[config.CurrentContext]
if !exists {
return fmt.Errorf("cannot locate context %v", config.CurrentContext)
}
newContexts := map[string]*Context{}
newContexts[config.CurrentContext] = currContext
newClusters := map[string]*Cluster{}
if len(currContext.Cluster) > 0 {
if _, exists := config.Clusters[currContext.Cluster]; !exists {
return fmt.Errorf("cannot locate cluster %v", currContext.Cluster)
}
newClusters[currContext.Cluster] = config.Clusters[currContext.Cluster]
}
newAuthInfos := map[string]*AuthInfo{}
if len(currContext.AuthInfo) > 0 {
if _, exists := config.AuthInfos[currContext.AuthInfo]; !exists {
return fmt.Errorf("cannot locate user %v", currContext.AuthInfo)
}
newAuthInfos[currContext.AuthInfo] = config.AuthInfos[currContext.AuthInfo]
}
config.AuthInfos = newAuthInfos
config.Clusters = newClusters
config.Contexts = newContexts
return nil
}
var redactedBytes []byte
// Flatten redacts raw data entries from the config object for a human-readable view.
func ShortenConfig(config *Config) {
// trick json encoder into printing a human readable string in the raw data
// by base64 decoding what we want to print. Relies on implementation of
// http://golang.org/pkg/encoding/json/#Marshal using base64 to encode []byte
for key, authInfo := range config.AuthInfos {
if len(authInfo.ClientKeyData) > 0 {
authInfo.ClientKeyData = redactedBytes
}
if len(authInfo.ClientCertificateData) > 0 {
authInfo.ClientCertificateData = redactedBytes
}
config.AuthInfos[key] = authInfo
}
for key, cluster := range config.Clusters {
if len(cluster.CertificateAuthorityData) > 0 {
cluster.CertificateAuthorityData = redactedBytes
}
config.Clusters[key] = cluster
}
}
// Flatten changes the config object into a self contained config (useful for making secrets)
func FlattenConfig(config *Config) error {
for key, authInfo := range config.AuthInfos {
baseDir, err := MakeAbs(path.Dir(authInfo.LocationOfOrigin), "")
if err != nil {
return err
}
if err := FlattenContent(&authInfo.ClientCertificate, &authInfo.ClientCertificateData, baseDir); err != nil {
return err
}
if err := FlattenContent(&authInfo.ClientKey, &authInfo.ClientKeyData, baseDir); err != nil {
return err
}
config.AuthInfos[key] = authInfo
}
for key, cluster := range config.Clusters {
baseDir, err := MakeAbs(path.Dir(cluster.LocationOfOrigin), "")
if err != nil {
return err
}
if err := FlattenContent(&cluster.CertificateAuthority, &cluster.CertificateAuthorityData, baseDir); err != nil {
return err
}
config.Clusters[key] = cluster
}
return nil
}
func FlattenContent(path *string, contents *[]byte, baseDir string) error {
if len(*path) != 0 {
if len(*contents) > 0 {
return errors.New("cannot have values for both path and contents")
}
var err error
absPath := ResolvePath(*path, baseDir)
*contents, err = ioutil.ReadFile(absPath)
if err != nil {
return err
}
*path = ""
}
return nil
}
// ResolvePath returns the path as an absolute paths, relative to the given base directory
func ResolvePath(path string, base string) string {
// Don't resolve empty paths
if len(path) > 0 {
// Don't resolve absolute paths
if !filepath.IsAbs(path) {
return filepath.Join(base, path)
}
}
return path
}
func MakeAbs(path, base string) (string, error) {
if filepath.IsAbs(path) {
return path, nil
}
if len(base) == 0 {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
base = cwd
}
return filepath.Join(base, path), nil
}

View file

@ -0,0 +1,301 @@
/*
Copyright 2015 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
import (
"fmt"
"io/ioutil"
"os"
"reflect"
"testing"
"github.com/ghodss/yaml"
)
func newMergedConfig(certFile, certContent, keyFile, keyContent, caFile, caContent string, t *testing.T) Config {
if err := ioutil.WriteFile(certFile, []byte(certContent), 0644); err != nil {
t.Errorf("unexpected error: %v", err)
}
if err := ioutil.WriteFile(keyFile, []byte(keyContent), 0600); err != nil {
t.Errorf("unexpected error: %v", err)
}
if err := ioutil.WriteFile(caFile, []byte(caContent), 0644); err != nil {
t.Errorf("unexpected error: %v", err)
}
return Config{
AuthInfos: map[string]*AuthInfo{
"red-user": {Token: "red-token", ClientCertificateData: []byte(certContent), ClientKeyData: []byte(keyContent)},
"blue-user": {Token: "blue-token", ClientCertificate: certFile, ClientKey: keyFile}},
Clusters: map[string]*Cluster{
"cow-cluster": {Server: "http://cow.org:8080", CertificateAuthorityData: []byte(caContent)},
"chicken-cluster": {Server: "http://chicken.org:8080", CertificateAuthority: caFile}},
Contexts: map[string]*Context{
"federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster"},
"shaker-context": {AuthInfo: "blue-user", Cluster: "chicken-cluster"}},
CurrentContext: "federal-context",
}
}
func TestMinifySuccess(t *testing.T) {
certFile, _ := ioutil.TempFile("", "")
defer os.Remove(certFile.Name())
keyFile, _ := ioutil.TempFile("", "")
defer os.Remove(keyFile.Name())
caFile, _ := ioutil.TempFile("", "")
defer os.Remove(caFile.Name())
mutatingConfig := newMergedConfig(certFile.Name(), "cert", keyFile.Name(), "key", caFile.Name(), "ca", t)
if err := MinifyConfig(&mutatingConfig); err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(mutatingConfig.Contexts) > 1 {
t.Errorf("unexpected contexts: %v", mutatingConfig.Contexts)
}
if _, exists := mutatingConfig.Contexts["federal-context"]; !exists {
t.Errorf("missing context")
}
if len(mutatingConfig.Clusters) > 1 {
t.Errorf("unexpected clusters: %v", mutatingConfig.Clusters)
}
if _, exists := mutatingConfig.Clusters["cow-cluster"]; !exists {
t.Errorf("missing cluster")
}
if len(mutatingConfig.AuthInfos) > 1 {
t.Errorf("unexpected users: %v", mutatingConfig.AuthInfos)
}
if _, exists := mutatingConfig.AuthInfos["red-user"]; !exists {
t.Errorf("missing user")
}
}
func TestMinifyMissingContext(t *testing.T) {
certFile, _ := ioutil.TempFile("", "")
defer os.Remove(certFile.Name())
keyFile, _ := ioutil.TempFile("", "")
defer os.Remove(keyFile.Name())
caFile, _ := ioutil.TempFile("", "")
defer os.Remove(caFile.Name())
mutatingConfig := newMergedConfig(certFile.Name(), "cert", keyFile.Name(), "key", caFile.Name(), "ca", t)
mutatingConfig.CurrentContext = "missing"
errMsg := "cannot locate context missing"
if err := MinifyConfig(&mutatingConfig); err == nil || err.Error() != errMsg {
t.Errorf("expected %v, got %v", errMsg, err)
}
}
func TestMinifyMissingCluster(t *testing.T) {
certFile, _ := ioutil.TempFile("", "")
defer os.Remove(certFile.Name())
keyFile, _ := ioutil.TempFile("", "")
defer os.Remove(keyFile.Name())
caFile, _ := ioutil.TempFile("", "")
defer os.Remove(caFile.Name())
mutatingConfig := newMergedConfig(certFile.Name(), "cert", keyFile.Name(), "key", caFile.Name(), "ca", t)
delete(mutatingConfig.Clusters, mutatingConfig.Contexts[mutatingConfig.CurrentContext].Cluster)
errMsg := "cannot locate cluster cow-cluster"
if err := MinifyConfig(&mutatingConfig); err == nil || err.Error() != errMsg {
t.Errorf("expected %v, got %v", errMsg, err)
}
}
func TestMinifyMissingAuthInfo(t *testing.T) {
certFile, _ := ioutil.TempFile("", "")
defer os.Remove(certFile.Name())
keyFile, _ := ioutil.TempFile("", "")
defer os.Remove(keyFile.Name())
caFile, _ := ioutil.TempFile("", "")
defer os.Remove(caFile.Name())
mutatingConfig := newMergedConfig(certFile.Name(), "cert", keyFile.Name(), "key", caFile.Name(), "ca", t)
delete(mutatingConfig.AuthInfos, mutatingConfig.Contexts[mutatingConfig.CurrentContext].AuthInfo)
errMsg := "cannot locate user red-user"
if err := MinifyConfig(&mutatingConfig); err == nil || err.Error() != errMsg {
t.Errorf("expected %v, got %v", errMsg, err)
}
}
func TestFlattenSuccess(t *testing.T) {
certFile, _ := ioutil.TempFile("", "")
defer os.Remove(certFile.Name())
keyFile, _ := ioutil.TempFile("", "")
defer os.Remove(keyFile.Name())
caFile, _ := ioutil.TempFile("", "")
defer os.Remove(caFile.Name())
certData := "cert"
keyData := "key"
caData := "ca"
unchangingCluster := "cow-cluster"
unchangingAuthInfo := "red-user"
changingCluster := "chicken-cluster"
changingAuthInfo := "blue-user"
startingConfig := newMergedConfig(certFile.Name(), certData, keyFile.Name(), keyData, caFile.Name(), caData, t)
mutatingConfig := startingConfig
if err := FlattenConfig(&mutatingConfig); err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(mutatingConfig.Contexts) != 2 {
t.Errorf("unexpected contexts: %v", mutatingConfig.Contexts)
}
if !reflect.DeepEqual(startingConfig.Contexts, mutatingConfig.Contexts) {
t.Errorf("expected %v, got %v", startingConfig.Contexts, mutatingConfig.Contexts)
}
if len(mutatingConfig.Clusters) != 2 {
t.Errorf("unexpected clusters: %v", mutatingConfig.Clusters)
}
if !reflect.DeepEqual(startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster]) {
t.Errorf("expected %v, got %v", startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster])
}
if len(mutatingConfig.Clusters[changingCluster].CertificateAuthority) != 0 {
t.Errorf("unexpected caFile")
}
if string(mutatingConfig.Clusters[changingCluster].CertificateAuthorityData) != caData {
t.Errorf("expected %v, got %v", caData, string(mutatingConfig.Clusters[changingCluster].CertificateAuthorityData))
}
if len(mutatingConfig.AuthInfos) != 2 {
t.Errorf("unexpected users: %v", mutatingConfig.AuthInfos)
}
if !reflect.DeepEqual(startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) {
t.Errorf("expected %v, got %v", startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo])
}
if len(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificate) != 0 {
t.Errorf("unexpected caFile")
}
if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData) != certData {
t.Errorf("expected %v, got %v", certData, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData))
}
if len(mutatingConfig.AuthInfos[changingAuthInfo].ClientKey) != 0 {
t.Errorf("unexpected caFile")
}
if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData) != keyData {
t.Errorf("expected %v, got %v", keyData, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData))
}
}
func Example_minifyAndShorten() {
certFile, _ := ioutil.TempFile("", "")
defer os.Remove(certFile.Name())
keyFile, _ := ioutil.TempFile("", "")
defer os.Remove(keyFile.Name())
caFile, _ := ioutil.TempFile("", "")
defer os.Remove(caFile.Name())
certData := "cert"
keyData := "key"
caData := "ca"
config := newMergedConfig(certFile.Name(), certData, keyFile.Name(), keyData, caFile.Name(), caData, nil)
MinifyConfig(&config)
ShortenConfig(&config)
output, _ := yaml.Marshal(config)
fmt.Printf("%s", string(output))
// Output:
// clusters:
// cow-cluster:
// LocationOfOrigin: ""
// certificate-authority-data: REDACTED
// server: http://cow.org:8080
// contexts:
// federal-context:
// LocationOfOrigin: ""
// cluster: cow-cluster
// user: red-user
// current-context: federal-context
// preferences: {}
// users:
// red-user:
// LocationOfOrigin: ""
// client-certificate-data: REDACTED
// client-key-data: REDACTED
// token: red-token
}
func TestShortenSuccess(t *testing.T) {
certFile, _ := ioutil.TempFile("", "")
defer os.Remove(certFile.Name())
keyFile, _ := ioutil.TempFile("", "")
defer os.Remove(keyFile.Name())
caFile, _ := ioutil.TempFile("", "")
defer os.Remove(caFile.Name())
certData := "cert"
keyData := "key"
caData := "ca"
unchangingCluster := "chicken-cluster"
unchangingAuthInfo := "blue-user"
changingCluster := "cow-cluster"
changingAuthInfo := "red-user"
startingConfig := newMergedConfig(certFile.Name(), certData, keyFile.Name(), keyData, caFile.Name(), caData, t)
mutatingConfig := startingConfig
ShortenConfig(&mutatingConfig)
if len(mutatingConfig.Contexts) != 2 {
t.Errorf("unexpected contexts: %v", mutatingConfig.Contexts)
}
if !reflect.DeepEqual(startingConfig.Contexts, mutatingConfig.Contexts) {
t.Errorf("expected %v, got %v", startingConfig.Contexts, mutatingConfig.Contexts)
}
redacted := string(redactedBytes)
if len(mutatingConfig.Clusters) != 2 {
t.Errorf("unexpected clusters: %v", mutatingConfig.Clusters)
}
if !reflect.DeepEqual(startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster]) {
t.Errorf("expected %v, got %v", startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster])
}
if string(mutatingConfig.Clusters[changingCluster].CertificateAuthorityData) != redacted {
t.Errorf("expected %v, got %v", redacted, string(mutatingConfig.Clusters[changingCluster].CertificateAuthorityData))
}
if len(mutatingConfig.AuthInfos) != 2 {
t.Errorf("unexpected users: %v", mutatingConfig.AuthInfos)
}
if !reflect.DeepEqual(startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) {
t.Errorf("expected %v, got %v", startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo])
}
if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData) != redacted {
t.Errorf("expected %v, got %v", redacted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData))
}
if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData) != redacted {
t.Errorf("expected %v, got %v", redacted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData))
}
}

View file

@ -0,0 +1,35 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["latest.go"],
tags = ["automanaged"],
deps = [
"//pkg/client/unversioned/clientcmd/api:go_default_library",
"//pkg/client/unversioned/clientcmd/api/v1:go_default_library",
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer/json",
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer/versioning",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,66 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package latest
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api/v1"
)
// Version is the string that represents the current external default version.
const Version = "v1"
var ExternalVersion = schema.GroupVersion{Group: "", Version: "v1"}
// OldestVersion is the string that represents the oldest server version supported,
// for client code that wants to hardcode the lowest common denominator.
const OldestVersion = "v1"
// Versions is the list of versions that are recognized in code. The order provided
// may be assumed to be least feature rich to most feature rich, and clients may
// choose to prefer the latter items in the list over the former items when presented
// with a set of versions to choose.
var Versions = []string{"v1"}
var (
Codec runtime.Codec
Scheme *runtime.Scheme
)
func init() {
Scheme = runtime.NewScheme()
if err := api.AddToScheme(Scheme); err != nil {
// Programmer error, detect immediately
panic(err)
}
if err := v1.AddToScheme(Scheme); err != nil {
// Programmer error, detect immediately
panic(err)
}
yamlSerializer := json.NewYAMLSerializer(json.DefaultMetaFactory, Scheme, Scheme)
Codec = versioning.NewDefaultingCodecForScheme(
Scheme,
yamlSerializer,
yamlSerializer,
schema.GroupVersion{Version: Version},
runtime.InternalGroupVersioner,
)
}

View file

@ -0,0 +1,46 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// SchemeGroupVersion is group version used to register these objects
// TODO this should be in the "kubeconfig" group
var SchemeGroupVersion = schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Config{},
)
return nil
}
func (obj *Config) GetObjectKind() schema.ObjectKind { return obj }
func (obj *Config) SetGroupVersionKind(gvk schema.GroupVersionKind) {
obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind()
}
func (obj *Config) GroupVersionKind() schema.GroupVersionKind {
return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind)
}

View file

@ -0,0 +1,178 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"k8s.io/apimachinery/pkg/runtime"
)
// Where possible, json tags match the cli argument names.
// Top level config objects and all values required for proper functioning are not "omitempty". Any truly optional piece of config is allowed to be omitted.
// Config holds the information needed to build connect to remote kubernetes clusters as a given user
// IMPORTANT if you add fields to this struct, please update IsConfigEmpty()
type Config struct {
// Legacy field from pkg/api/types.go TypeMeta.
// TODO(jlowdermilk): remove this after eliminating downstream dependencies.
// +optional
Kind string `json:"kind,omitempty"`
// DEPRECATED: APIVersion is the preferred api version for communicating with the kubernetes cluster (v1, v2, etc).
// Because a cluster can run multiple API groups and potentially multiple versions of each, it no longer makes sense to specify
// a single value for the cluster version.
// This field isn't really needed anyway, so we are deprecating it without replacement.
// It will be ignored if it is present.
// +optional
APIVersion string `json:"apiVersion,omitempty"`
// Preferences holds general information to be use for cli interactions
Preferences Preferences `json:"preferences"`
// Clusters is a map of referencable names to cluster configs
Clusters map[string]*Cluster `json:"clusters"`
// AuthInfos is a map of referencable names to user configs
AuthInfos map[string]*AuthInfo `json:"users"`
// Contexts is a map of referencable names to context configs
Contexts map[string]*Context `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 map[string]runtime.Object `json:"extensions,omitempty"`
}
// IMPORTANT if you add fields to this struct, please update IsConfigEmpty()
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 map[string]runtime.Object `json:"extensions,omitempty"`
}
// Cluster contains information about how to communicate with a kubernetes cluster
type Cluster struct {
// LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized.
LocationOfOrigin string
// Server is the address of the kubernetes cluster (https://hostname:port).
Server string `json:"server"`
// APIVersion is the preferred api version for communicating with the kubernetes cluster (v1, v2, etc).
// +optional
APIVersion string `json:"api-version,omitempty"`
// InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
// +optional
InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"`
// CertificateAuthority is the path to a cert file for the certificate authority.
// +optional
CertificateAuthority string `json:"certificate-authority,omitempty"`
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
// +optional
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
// +optional
Extensions map[string]runtime.Object `json:"extensions,omitempty"`
}
// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
type AuthInfo struct {
// LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized.
LocationOfOrigin string
// 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 act-as.
// +optional
Impersonate string `json:"act-as,omitempty"`
// Username is the username for basic authentication to the kubernetes cluster.
// +optional
Username string `json:"username,omitempty"`
// Password is the password for basic authentication to the kubernetes cluster.
// +optional
Password string `json:"password,omitempty"`
// AuthProvider specifies a custom authentication plugin for the kubernetes cluster.
// +optional
AuthProvider *AuthProviderConfig `json:"auth-provider,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
// +optional
Extensions map[string]runtime.Object `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 {
// LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized.
LocationOfOrigin string
// 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 map[string]runtime.Object `json:"extensions,omitempty"`
}
// AuthProviderConfig holds the configuration for a specified auth provider.
type AuthProviderConfig struct {
Name string `json:"name"`
// +optional
Config map[string]string `json:"config,omitempty"`
}
// NewConfig is a convenience function that returns a new Config object with non-nil maps
func NewConfig() *Config {
return &Config{
Preferences: *NewPreferences(),
Clusters: make(map[string]*Cluster),
AuthInfos: make(map[string]*AuthInfo),
Contexts: make(map[string]*Context),
Extensions: make(map[string]runtime.Object),
}
}
// NewConfig is a convenience function that returns a new Config object with non-nil maps
func NewContext() *Context {
return &Context{Extensions: make(map[string]runtime.Object)}
}
// NewConfig is a convenience function that returns a new Config object with non-nil maps
func NewCluster() *Cluster {
return &Cluster{Extensions: make(map[string]runtime.Object)}
}
// NewConfig is a convenience function that returns a new Config object with non-nil maps
func NewAuthInfo() *AuthInfo {
return &AuthInfo{Extensions: make(map[string]runtime.Object)}
}
// NewConfig is a convenience function that returns a new Config object with non-nil maps
func NewPreferences() *Preferences {
return &Preferences{Extensions: make(map[string]runtime.Object)}
}

View file

@ -0,0 +1,135 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"fmt"
"github.com/ghodss/yaml"
)
func Example_emptyConfig() {
defaultConfig := NewConfig()
output, err := yaml.Marshal(defaultConfig)
if err != nil {
fmt.Printf("Unexpected error: %v", err)
}
fmt.Printf("%v", string(output))
// Output:
// clusters: {}
// contexts: {}
// current-context: ""
// preferences: {}
// users: {}
}
func Example_ofOptionsConfig() {
defaultConfig := NewConfig()
defaultConfig.Preferences.Colors = true
defaultConfig.Clusters["alfa"] = &Cluster{
Server: "https://alfa.org:8080",
InsecureSkipTLSVerify: true,
CertificateAuthority: "path/to/my/cert-ca-filename",
}
defaultConfig.Clusters["bravo"] = &Cluster{
Server: "https://bravo.org:8080",
InsecureSkipTLSVerify: false,
}
defaultConfig.AuthInfos["white-mage-via-cert"] = &AuthInfo{
ClientCertificate: "path/to/my/client-cert-filename",
ClientKey: "path/to/my/client-key-filename",
}
defaultConfig.AuthInfos["red-mage-via-token"] = &AuthInfo{
Token: "my-secret-token",
}
defaultConfig.AuthInfos["black-mage-via-auth-provider"] = &AuthInfo{
AuthProvider: &AuthProviderConfig{
Name: "gcp",
Config: map[string]string{
"foo": "bar",
"token": "s3cr3t-t0k3n",
},
},
}
defaultConfig.Contexts["bravo-as-black-mage"] = &Context{
Cluster: "bravo",
AuthInfo: "black-mage-via-auth-provider",
Namespace: "yankee",
}
defaultConfig.Contexts["alfa-as-black-mage"] = &Context{
Cluster: "alfa",
AuthInfo: "black-mage-via-auth-provider",
Namespace: "zulu",
}
defaultConfig.Contexts["alfa-as-white-mage"] = &Context{
Cluster: "alfa",
AuthInfo: "white-mage-via-cert",
}
defaultConfig.CurrentContext = "alfa-as-white-mage"
output, err := yaml.Marshal(defaultConfig)
if err != nil {
fmt.Printf("Unexpected error: %v", err)
}
fmt.Printf("%v", string(output))
// Output:
// clusters:
// alfa:
// LocationOfOrigin: ""
// certificate-authority: path/to/my/cert-ca-filename
// insecure-skip-tls-verify: true
// server: https://alfa.org:8080
// bravo:
// LocationOfOrigin: ""
// server: https://bravo.org:8080
// contexts:
// alfa-as-black-mage:
// LocationOfOrigin: ""
// cluster: alfa
// namespace: zulu
// user: black-mage-via-auth-provider
// alfa-as-white-mage:
// LocationOfOrigin: ""
// cluster: alfa
// user: white-mage-via-cert
// bravo-as-black-mage:
// LocationOfOrigin: ""
// cluster: bravo
// namespace: yankee
// user: black-mage-via-auth-provider
// current-context: alfa-as-white-mage
// preferences:
// colors: true
// users:
// black-mage-via-auth-provider:
// LocationOfOrigin: ""
// auth-provider:
// config:
// foo: bar
// token: s3cr3t-t0k3n
// name: gcp
// red-mage-via-token:
// LocationOfOrigin: ""
// token: my-secret-token
// white-mage-via-cert:
// LocationOfOrigin: ""
// client-certificate: path/to/my/client-cert-filename
// client-key: path/to/my/client-key-filename
}

View file

@ -0,0 +1,37 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"conversion.go",
"register.go",
"types.go",
],
tags = ["automanaged"],
deps = [
"//pkg/client/unversioned/clientcmd/api:go_default_library",
"//vendor:k8s.io/apimachinery/pkg/conversion",
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,227 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"sort"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
func addConversionFuncs(scheme *runtime.Scheme) error {
return scheme.AddConversionFuncs(
func(in *Cluster, out *api.Cluster, s conversion.Scope) error {
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
},
func(in *api.Cluster, out *Cluster, s conversion.Scope) error {
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
},
func(in *Preferences, out *api.Preferences, s conversion.Scope) error {
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
},
func(in *api.Preferences, out *Preferences, s conversion.Scope) error {
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
},
func(in *AuthInfo, out *api.AuthInfo, s conversion.Scope) error {
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
},
func(in *api.AuthInfo, out *AuthInfo, s conversion.Scope) error {
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
},
func(in *Context, out *api.Context, s conversion.Scope) error {
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
},
func(in *api.Context, out *Context, s conversion.Scope) error {
return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
},
func(in *Config, out *api.Config, s conversion.Scope) error {
out.CurrentContext = in.CurrentContext
if err := s.Convert(&in.Preferences, &out.Preferences, 0); err != nil {
return err
}
out.Clusters = make(map[string]*api.Cluster)
if err := s.Convert(&in.Clusters, &out.Clusters, 0); err != nil {
return err
}
out.AuthInfos = make(map[string]*api.AuthInfo)
if err := s.Convert(&in.AuthInfos, &out.AuthInfos, 0); err != nil {
return err
}
out.Contexts = make(map[string]*api.Context)
if err := s.Convert(&in.Contexts, &out.Contexts, 0); err != nil {
return err
}
out.Extensions = make(map[string]runtime.Object)
if err := s.Convert(&in.Extensions, &out.Extensions, 0); err != nil {
return err
}
return nil
},
func(in *api.Config, out *Config, s conversion.Scope) error {
out.CurrentContext = in.CurrentContext
if err := s.Convert(&in.Preferences, &out.Preferences, 0); err != nil {
return err
}
out.Clusters = make([]NamedCluster, 0, 0)
if err := s.Convert(&in.Clusters, &out.Clusters, 0); err != nil {
return err
}
out.AuthInfos = make([]NamedAuthInfo, 0, 0)
if err := s.Convert(&in.AuthInfos, &out.AuthInfos, 0); err != nil {
return err
}
out.Contexts = make([]NamedContext, 0, 0)
if err := s.Convert(&in.Contexts, &out.Contexts, 0); err != nil {
return err
}
out.Extensions = make([]NamedExtension, 0, 0)
if err := s.Convert(&in.Extensions, &out.Extensions, 0); err != nil {
return err
}
return nil
},
func(in *[]NamedCluster, out *map[string]*api.Cluster, s conversion.Scope) error {
for _, curr := range *in {
newCluster := api.NewCluster()
if err := s.Convert(&curr.Cluster, newCluster, 0); err != nil {
return err
}
(*out)[curr.Name] = newCluster
}
return nil
},
func(in *map[string]*api.Cluster, out *[]NamedCluster, s conversion.Scope) error {
allKeys := make([]string, 0, len(*in))
for key := range *in {
allKeys = append(allKeys, key)
}
sort.Strings(allKeys)
for _, key := range allKeys {
newCluster := (*in)[key]
oldCluster := &Cluster{}
if err := s.Convert(newCluster, oldCluster, 0); err != nil {
return err
}
namedCluster := NamedCluster{key, *oldCluster}
*out = append(*out, namedCluster)
}
return nil
},
func(in *[]NamedAuthInfo, out *map[string]*api.AuthInfo, s conversion.Scope) error {
for _, curr := range *in {
newAuthInfo := api.NewAuthInfo()
if err := s.Convert(&curr.AuthInfo, newAuthInfo, 0); err != nil {
return err
}
(*out)[curr.Name] = newAuthInfo
}
return nil
},
func(in *map[string]*api.AuthInfo, out *[]NamedAuthInfo, s conversion.Scope) error {
allKeys := make([]string, 0, len(*in))
for key := range *in {
allKeys = append(allKeys, key)
}
sort.Strings(allKeys)
for _, key := range allKeys {
newAuthInfo := (*in)[key]
oldAuthInfo := &AuthInfo{}
if err := s.Convert(newAuthInfo, oldAuthInfo, 0); err != nil {
return err
}
namedAuthInfo := NamedAuthInfo{key, *oldAuthInfo}
*out = append(*out, namedAuthInfo)
}
return nil
},
func(in *[]NamedContext, out *map[string]*api.Context, s conversion.Scope) error {
for _, curr := range *in {
newContext := api.NewContext()
if err := s.Convert(&curr.Context, newContext, 0); err != nil {
return err
}
(*out)[curr.Name] = newContext
}
return nil
},
func(in *map[string]*api.Context, out *[]NamedContext, s conversion.Scope) error {
allKeys := make([]string, 0, len(*in))
for key := range *in {
allKeys = append(allKeys, key)
}
sort.Strings(allKeys)
for _, key := range allKeys {
newContext := (*in)[key]
oldContext := &Context{}
if err := s.Convert(newContext, oldContext, 0); err != nil {
return err
}
namedContext := NamedContext{key, *oldContext}
*out = append(*out, namedContext)
}
return nil
},
func(in *[]NamedExtension, out *map[string]runtime.Object, s conversion.Scope) error {
for _, curr := range *in {
var newExtension runtime.Object
if err := s.Convert(&curr.Extension, &newExtension, 0); err != nil {
return err
}
(*out)[curr.Name] = newExtension
}
return nil
},
func(in *map[string]runtime.Object, out *[]NamedExtension, s conversion.Scope) error {
allKeys := make([]string, 0, len(*in))
for key := range *in {
allKeys = append(allKeys, key)
}
sort.Strings(allKeys)
for _, key := range allKeys {
newExtension := (*in)[key]
oldExtension := &runtime.RawExtension{}
if err := s.Convert(newExtension, oldExtension, 0); err != nil {
return err
}
namedExtension := NamedExtension{key, *oldExtension}
*out = append(*out, namedExtension)
}
return nil
},
)
}

View file

@ -0,0 +1,46 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// SchemeGroupVersion is group version used to register these objects
// TODO this should be in the "kubeconfig" group
var SchemeGroupVersion = schema.GroupVersion{Group: "", Version: "v1"}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes, addConversionFuncs)
AddToScheme = SchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Config{},
)
return nil
}
func (obj *Config) GetObjectKind() schema.ObjectKind { return obj }
func (obj *Config) SetGroupVersionKind(gvk schema.GroupVersionKind) {
obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind()
}
func (obj *Config) GroupVersionKind() schema.GroupVersionKind {
return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind)
}

View file

@ -0,0 +1,170 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"k8s.io/apimachinery/pkg/runtime"
)
// Where possible, json tags match the cli argument names.
// Top level config objects and all values required for proper functioning are not "omitempty". Any truly optional piece of config is allowed to be omitted.
// Config holds the information needed to build connect to remote kubernetes clusters as a given user
type Config struct {
// Legacy field from pkg/api/types.go TypeMeta.
// TODO(jlowdermilk): remove this after eliminating downstream dependencies.
// +optional
Kind string `json:"kind,omitempty"`
// DEPRECATED: APIVersion is the preferred api version for communicating with the kubernetes cluster (v1, v2, etc).
// Because a cluster can run multiple API groups and potentially multiple versions of each, it no longer makes sense to specify
// a single value for the cluster version.
// This field isn't really needed anyway, so we are deprecating it without replacement.
// It will be ignored if it is present.
// +optional
APIVersion string `json:"apiVersion,omitempty"`
// Preferences holds general information to be use for cli interactions
Preferences Preferences `json:"preferences"`
// Clusters is a map of referencable names to cluster configs
Clusters []NamedCluster `json:"clusters"`
// AuthInfos is a map of referencable names to user configs
AuthInfos []NamedAuthInfo `json:"users"`
// Contexts is a map of referencable names to context configs
Contexts []NamedContext `json:"contexts"`
// CurrentContext is the name of the context that you would like to use by default
CurrentContext string `json:"current-context"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
// +optional
Extensions []NamedExtension `json:"extensions,omitempty"`
}
type Preferences struct {
// +optional
Colors bool `json:"colors,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
// +optional
Extensions []NamedExtension `json:"extensions,omitempty"`
}
// Cluster contains information about how to communicate with a kubernetes cluster
type Cluster struct {
// Server is the address of the kubernetes cluster (https://hostname:port).
Server string `json:"server"`
// APIVersion is the preferred api version for communicating with the kubernetes cluster (v1, v2, etc).
// +optional
APIVersion string `json:"api-version,omitempty"`
// InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
// +optional
InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"`
// CertificateAuthority is the path to a cert file for the certificate authority.
// +optional
CertificateAuthority string `json:"certificate-authority,omitempty"`
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
// +optional
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
// +optional
Extensions []NamedExtension `json:"extensions,omitempty"`
}
// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
type AuthInfo struct {
// ClientCertificate is the path to a client cert file for TLS.
// +optional
ClientCertificate string `json:"client-certificate,omitempty"`
// ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate
// +optional
ClientCertificateData []byte `json:"client-certificate-data,omitempty"`
// ClientKey is the path to a client key file for TLS.
// +optional
ClientKey string `json:"client-key,omitempty"`
// ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey
// +optional
ClientKeyData []byte `json:"client-key-data,omitempty"`
// Token is the bearer token for authentication to the kubernetes cluster.
// +optional
Token string `json:"token,omitempty"`
// TokenFile is a pointer to a file that contains a bearer token (as described above). If both Token and TokenFile are present, Token takes precedence.
// +optional
TokenFile string `json:"tokenFile,omitempty"`
// Impersonate is the username to imperonate. The name matches the flag.
// +optional
Impersonate string `json:"as,omitempty"`
// Username is the username for basic authentication to the kubernetes cluster.
// +optional
Username string `json:"username,omitempty"`
// Password is the password for basic authentication to the kubernetes cluster.
// +optional
Password string `json:"password,omitempty"`
// AuthProvider specifies a custom authentication plugin for the kubernetes cluster.
// +optional
AuthProvider *AuthProviderConfig `json:"auth-provider,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
// +optional
Extensions []NamedExtension `json:"extensions,omitempty"`
}
// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with)
type Context struct {
// Cluster is the name of the cluster for this context
Cluster string `json:"cluster"`
// AuthInfo is the name of the authInfo for this context
AuthInfo string `json:"user"`
// Namespace is the default namespace to use on unspecified requests
// +optional
Namespace string `json:"namespace,omitempty"`
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
// +optional
Extensions []NamedExtension `json:"extensions,omitempty"`
}
// NamedCluster relates nicknames to cluster information
type NamedCluster struct {
// Name is the nickname for this Cluster
Name string `json:"name"`
// Cluster holds the cluster information
Cluster Cluster `json:"cluster"`
}
// NamedContext relates nicknames to context information
type NamedContext struct {
// Name is the nickname for this Context
Name string `json:"name"`
// Context holds the context information
Context Context `json:"context"`
}
// NamedAuthInfo relates nicknames to auth information
type NamedAuthInfo struct {
// Name is the nickname for this AuthInfo
Name string `json:"name"`
// AuthInfo holds the auth information
AuthInfo AuthInfo `json:"user"`
}
// NamedExtension relates nicknames to extension information
type NamedExtension struct {
// Name is the nickname for this Extension
Name string `json:"name"`
// Extension holds the extension information
Extension runtime.RawExtension `json:"extension"`
}
// AuthProviderConfig holds the configuration for a specified auth provider.
type AuthProviderConfig struct {
Name string `json:"name"`
Config map[string]string `json:"config"`
}

View file

@ -0,0 +1,106 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/howeyc/gopass"
clientauth "k8s.io/kubernetes/pkg/client/unversioned/auth"
)
// AuthLoaders are used to build clientauth.Info objects.
type AuthLoader interface {
// LoadAuth takes a path to a config file and can then do anything it needs in order to return a valid clientauth.Info
LoadAuth(path string) (*clientauth.Info, error)
}
// default implementation of an AuthLoader
type defaultAuthLoader struct{}
// LoadAuth for defaultAuthLoader simply delegates to clientauth.LoadFromFile
func (*defaultAuthLoader) LoadAuth(path string) (*clientauth.Info, error) {
return clientauth.LoadFromFile(path)
}
type PromptingAuthLoader struct {
reader io.Reader
}
// LoadAuth parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist.
func (a *PromptingAuthLoader) LoadAuth(path string) (*clientauth.Info, error) {
// Prompt for user/pass and write a file if none exists.
if _, err := os.Stat(path); os.IsNotExist(err) {
authPtr, err := a.Prompt()
auth := *authPtr
if err != nil {
return nil, err
}
data, err := json.Marshal(auth)
if err != nil {
return &auth, err
}
err = ioutil.WriteFile(path, data, 0600)
return &auth, err
}
authPtr, err := clientauth.LoadFromFile(path)
if err != nil {
return nil, err
}
return authPtr, nil
}
// Prompt pulls the user and password from a reader
func (a *PromptingAuthLoader) Prompt() (*clientauth.Info, error) {
var err error
auth := &clientauth.Info{}
auth.User, err = promptForString("Username", a.reader, true)
if err != nil {
return nil, err
}
auth.Password, err = promptForString("Password", nil, false)
if err != nil {
return nil, err
}
return auth, nil
}
func promptForString(field string, r io.Reader, show bool) (result string, err error) {
fmt.Printf("Please enter %s: ", field)
if show {
_, err = fmt.Fscan(r, &result)
} else {
var data []byte
data, err = gopass.GetPasswdMasked()
result = string(data)
}
return result, err
}
// NewPromptingAuthLoader is an AuthLoader that parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist.
func NewPromptingAuthLoader(reader io.Reader) *PromptingAuthLoader {
return &PromptingAuthLoader{reader}
}
// NewDefaultAuthLoader returns a default implementation of an AuthLoader that only reads from a config file
func NewDefaultAuthLoader() AuthLoader {
return &defaultAuthLoader{}
}

View file

@ -0,0 +1,537 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"strings"
"github.com/golang/glog"
"github.com/imdario/mergo"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/restclient"
clientauth "k8s.io/kubernetes/pkg/client/unversioned/auth"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
var (
// ClusterDefaults has the same behavior as the old EnvVar and DefaultCluster fields
// DEPRECATED will be replaced
ClusterDefaults = clientcmdapi.Cluster{Server: getDefaultServer()}
// DefaultClientConfig represents the legacy behavior of this package for defaulting
// DEPRECATED will be replace
DefaultClientConfig = DirectClientConfig{*clientcmdapi.NewConfig(), "", &ConfigOverrides{
ClusterDefaults: ClusterDefaults,
}, nil, NewDefaultClientConfigLoadingRules(), promptedCredentials{}}
)
// getDefaultServer returns a default setting for DefaultClientConfig
// DEPRECATED
func getDefaultServer() string {
if server := os.Getenv("KUBERNETES_MASTER"); len(server) > 0 {
return server
}
return "http://localhost:8080"
}
// ClientConfig is used to make it easy to get an api server client
type ClientConfig interface {
// RawConfig returns the merged result of all overrides
RawConfig() (clientcmdapi.Config, error)
// ClientConfig returns a complete client config
ClientConfig() (*restclient.Config, error)
// Namespace returns the namespace resulting from the merged
// result of all overrides and a boolean indicating if it was
// overridden
Namespace() (string, bool, error)
// ConfigAccess returns the rules for loading/persisting the config.
ConfigAccess() ConfigAccess
}
type PersistAuthProviderConfigForUser func(user string) restclient.AuthProviderConfigPersister
type promptedCredentials struct {
username string
password string
}
// DirectClientConfig is a ClientConfig interface that is backed by a clientcmdapi.Config, options overrides, and an optional fallbackReader for auth information
type DirectClientConfig struct {
config clientcmdapi.Config
contextName string
overrides *ConfigOverrides
fallbackReader io.Reader
configAccess ConfigAccess
// promptedCredentials store the credentials input by the user
promptedCredentials promptedCredentials
}
// NewDefaultClientConfig creates a DirectClientConfig using the config.CurrentContext as the context name
func NewDefaultClientConfig(config clientcmdapi.Config, overrides *ConfigOverrides) ClientConfig {
return &DirectClientConfig{config, config.CurrentContext, overrides, nil, NewDefaultClientConfigLoadingRules(), promptedCredentials{}}
}
// NewNonInteractiveClientConfig creates a DirectClientConfig using the passed context name and does not have a fallback reader for auth information
func NewNonInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, configAccess ConfigAccess) ClientConfig {
return &DirectClientConfig{config, contextName, overrides, nil, configAccess, promptedCredentials{}}
}
// NewInteractiveClientConfig creates a DirectClientConfig using the passed context name and a reader in case auth information is not provided via files or flags
func NewInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader, configAccess ConfigAccess) ClientConfig {
return &DirectClientConfig{config, contextName, overrides, fallbackReader, configAccess, promptedCredentials{}}
}
func (config *DirectClientConfig) RawConfig() (clientcmdapi.Config, error) {
return config.config, nil
}
// ClientConfig implements ClientConfig
func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
// check that getAuthInfo, getContext, and getCluster do not return an error.
// Do this before checking if the curent config is usable in the event that an
// AuthInfo, Context, or Cluster config with user-defined names are not found.
// This provides a user with the immediate cause for error if one is found
configAuthInfo, err := config.getAuthInfo()
if err != nil {
return nil, err
}
_, err = config.getContext()
if err != nil {
return nil, err
}
configClusterInfo, err := config.getCluster()
if err != nil {
return nil, err
}
if err := config.ConfirmUsable(); err != nil {
return nil, err
}
clientConfig := &restclient.Config{}
clientConfig.Host = configClusterInfo.Server
if len(config.overrides.Timeout) > 0 {
timeout, err := ParseTimeout(config.overrides.Timeout)
if err != nil {
return nil, err
}
clientConfig.Timeout = timeout
}
if u, err := url.ParseRequestURI(clientConfig.Host); err == nil && u.Opaque == "" && len(u.Path) > 1 {
u.RawQuery = ""
u.Fragment = ""
clientConfig.Host = u.String()
}
if len(configAuthInfo.Impersonate) > 0 {
clientConfig.Impersonate = restclient.ImpersonationConfig{UserName: configAuthInfo.Impersonate}
}
// only try to read the auth information if we are secure
if restclient.IsConfigTransportTLS(*clientConfig) {
var err error
// mergo is a first write wins for map value and a last writing wins for interface values
// NOTE: This behavior changed with https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a.
// Our mergo.Merge version is older than this change.
var persister restclient.AuthProviderConfigPersister
if config.configAccess != nil {
authInfoName, _ := config.getAuthInfoName()
persister = PersisterForUser(config.configAccess, authInfoName)
}
userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister)
if err != nil {
return nil, err
}
mergo.Merge(clientConfig, userAuthPartialConfig)
serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo)
if err != nil {
return nil, err
}
mergo.Merge(clientConfig, serverAuthPartialConfig)
}
return clientConfig, nil
}
// clientauth.Info object contain both user identification and server identification. We want different precedence orders for
// both, so we have to split the objects and merge them separately
// we want this order of precedence for the server identification
// 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files)
// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority)
// 3. load the ~/.kubernetes_auth file as a default
func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, configClusterInfo clientcmdapi.Cluster) (*restclient.Config, error) {
mergedConfig := &restclient.Config{}
// configClusterInfo holds the information identify the server provided by .kubeconfig
configClientConfig := &restclient.Config{}
configClientConfig.CAFile = configClusterInfo.CertificateAuthority
configClientConfig.CAData = configClusterInfo.CertificateAuthorityData
configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify
mergo.Merge(mergedConfig, configClientConfig)
return mergedConfig, nil
}
// clientauth.Info object contain both user identification and server identification. We want different precedence orders for
// both, so we have to split the objects and merge them separately
// we want this order of precedence for user identifcation
// 1. configAuthInfo minus auth-path (the final result of command line flags and merged .kubeconfig files)
// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority)
// 3. if there is not enough information to idenfity the user, load try the ~/.kubernetes_auth file
// 4. if there is not enough information to identify the user, prompt if possible
func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister) (*restclient.Config, error) {
mergedConfig := &restclient.Config{}
// blindly overwrite existing values based on precedence
if len(configAuthInfo.Token) > 0 {
mergedConfig.BearerToken = configAuthInfo.Token
} else if len(configAuthInfo.TokenFile) > 0 {
tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile)
if err != nil {
return nil, err
}
mergedConfig.BearerToken = string(tokenBytes)
}
if len(configAuthInfo.Impersonate) > 0 {
mergedConfig.Impersonate = restclient.ImpersonationConfig{UserName: configAuthInfo.Impersonate}
}
if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 {
mergedConfig.CertFile = configAuthInfo.ClientCertificate
mergedConfig.CertData = configAuthInfo.ClientCertificateData
mergedConfig.KeyFile = configAuthInfo.ClientKey
mergedConfig.KeyData = configAuthInfo.ClientKeyData
}
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
mergedConfig.Username = configAuthInfo.Username
mergedConfig.Password = configAuthInfo.Password
}
if configAuthInfo.AuthProvider != nil {
mergedConfig.AuthProvider = configAuthInfo.AuthProvider
mergedConfig.AuthConfigPersister = persistAuthConfig
}
// if there still isn't enough information to authenticate the user, try prompting
if !canIdentifyUser(*mergedConfig) && (fallbackReader != nil) {
if len(config.promptedCredentials.username) > 0 && len(config.promptedCredentials.password) > 0 {
mergedConfig.Username = config.promptedCredentials.username
mergedConfig.Password = config.promptedCredentials.password
return mergedConfig, nil
}
prompter := NewPromptingAuthLoader(fallbackReader)
promptedAuthInfo, err := prompter.Prompt()
if err != nil {
return nil, err
}
promptedConfig := makeUserIdentificationConfig(*promptedAuthInfo)
previouslyMergedConfig := mergedConfig
mergedConfig = &restclient.Config{}
mergo.Merge(mergedConfig, promptedConfig)
mergo.Merge(mergedConfig, previouslyMergedConfig)
config.promptedCredentials.username = mergedConfig.Username
config.promptedCredentials.password = mergedConfig.Password
}
return mergedConfig, nil
}
// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only user identification information
func makeUserIdentificationConfig(info clientauth.Info) *restclient.Config {
config := &restclient.Config{}
config.Username = info.User
config.Password = info.Password
config.CertFile = info.CertFile
config.KeyFile = info.KeyFile
config.BearerToken = info.BearerToken
return config
}
// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only server identification information
func makeServerIdentificationConfig(info clientauth.Info) restclient.Config {
config := restclient.Config{}
config.CAFile = info.CAFile
if info.Insecure != nil {
config.Insecure = *info.Insecure
}
return config
}
func canIdentifyUser(config restclient.Config) bool {
return len(config.Username) > 0 ||
(len(config.CertFile) > 0 || len(config.CertData) > 0) ||
len(config.BearerToken) > 0 ||
config.AuthProvider != nil
}
// Namespace implements ClientConfig
func (config *DirectClientConfig) Namespace() (string, bool, error) {
if err := config.ConfirmUsable(); err != nil {
return "", false, err
}
configContext, err := config.getContext()
if err != nil {
return "", false, err
}
if len(configContext.Namespace) == 0 {
return api.NamespaceDefault, false, nil
}
overridden := false
if config.overrides != nil && config.overrides.Context.Namespace != "" {
overridden = true
}
return configContext.Namespace, overridden, nil
}
// ConfigAccess implements ClientConfig
func (config *DirectClientConfig) ConfigAccess() ConfigAccess {
return config.configAccess
}
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
func (config *DirectClientConfig) ConfirmUsable() error {
validationErrors := make([]error, 0)
var contextName string
if len(config.contextName) != 0 {
contextName = config.contextName
} else {
contextName = config.config.CurrentContext
}
if len(contextName) > 0 {
_, exists := config.config.Contexts[contextName]
if !exists {
validationErrors = append(validationErrors, &errContextNotFound{contextName})
}
}
authInfoName, _ := config.getAuthInfoName()
authInfo, _ := config.getAuthInfo()
validationErrors = append(validationErrors, validateAuthInfo(authInfoName, authInfo)...)
clusterName, _ := config.getClusterName()
cluster, _ := config.getCluster()
validationErrors = append(validationErrors, validateClusterInfo(clusterName, cluster)...)
// when direct client config is specified, and our only error is that no server is defined, we should
// return a standard "no config" error
if len(validationErrors) == 1 && validationErrors[0] == ErrEmptyCluster {
return newErrConfigurationInvalid([]error{ErrEmptyConfig})
}
return newErrConfigurationInvalid(validationErrors)
}
// getContextName returns the default, or user-set context name, and a boolean that indicates
// whether the default context name has been overwritten by a user-set flag, or left as its default value
func (config *DirectClientConfig) getContextName() (string, bool) {
if len(config.overrides.CurrentContext) != 0 {
return config.overrides.CurrentContext, true
}
if len(config.contextName) != 0 {
return config.contextName, false
}
return config.config.CurrentContext, false
}
// getAuthInfoName returns a string containing the current authinfo name for the current context,
// and a boolean indicating whether the default authInfo name is overwritten by a user-set flag, or
// left as its default value
func (config *DirectClientConfig) getAuthInfoName() (string, bool) {
if len(config.overrides.Context.AuthInfo) != 0 {
return config.overrides.Context.AuthInfo, true
}
context, _ := config.getContext()
return context.AuthInfo, false
}
// getClusterName returns a string containing the default, or user-set cluster name, and a boolean
// indicating whether the default clusterName has been overwritten by a user-set flag, or left as
// its default value
func (config *DirectClientConfig) getClusterName() (string, bool) {
if len(config.overrides.Context.Cluster) != 0 {
return config.overrides.Context.Cluster, true
}
context, _ := config.getContext()
return context.Cluster, false
}
// getContext returns the clientcmdapi.Context, or an error if a required context is not found.
func (config *DirectClientConfig) getContext() (clientcmdapi.Context, error) {
contexts := config.config.Contexts
contextName, required := config.getContextName()
var mergedContext clientcmdapi.Context
if configContext, exists := contexts[contextName]; exists {
mergo.Merge(&mergedContext, configContext)
} else if required {
return clientcmdapi.Context{}, fmt.Errorf("context %q does not exist", contextName)
}
mergo.Merge(&mergedContext, config.overrides.Context)
return mergedContext, nil
}
// getAuthInfo returns the clientcmdapi.AuthInfo, or an error if a required auth info is not found.
func (config *DirectClientConfig) getAuthInfo() (clientcmdapi.AuthInfo, error) {
authInfos := config.config.AuthInfos
authInfoName, required := config.getAuthInfoName()
var mergedAuthInfo clientcmdapi.AuthInfo
if configAuthInfo, exists := authInfos[authInfoName]; exists {
mergo.Merge(&mergedAuthInfo, configAuthInfo)
} else if required {
return clientcmdapi.AuthInfo{}, fmt.Errorf("auth info %q does not exist", authInfoName)
}
mergo.Merge(&mergedAuthInfo, config.overrides.AuthInfo)
return mergedAuthInfo, nil
}
// getCluster returns the clientcmdapi.Cluster, or an error if a required cluster is not found.
func (config *DirectClientConfig) getCluster() (clientcmdapi.Cluster, error) {
clusterInfos := config.config.Clusters
clusterInfoName, required := config.getClusterName()
var mergedClusterInfo clientcmdapi.Cluster
mergo.Merge(&mergedClusterInfo, config.overrides.ClusterDefaults)
if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists {
mergo.Merge(&mergedClusterInfo, configClusterInfo)
} else if required {
return clientcmdapi.Cluster{}, fmt.Errorf("cluster %q does not exist", clusterInfoName)
}
mergo.Merge(&mergedClusterInfo, config.overrides.ClusterInfo)
// An override of --insecure-skip-tls-verify=true and no accompanying CA/CA data should clear already-set CA/CA data
// otherwise, a kubeconfig containing a CA reference would return an error that "CA and insecure-skip-tls-verify couldn't both be set"
caLen := len(config.overrides.ClusterInfo.CertificateAuthority)
caDataLen := len(config.overrides.ClusterInfo.CertificateAuthorityData)
if config.overrides.ClusterInfo.InsecureSkipTLSVerify && caLen == 0 && caDataLen == 0 {
mergedClusterInfo.CertificateAuthority = ""
mergedClusterInfo.CertificateAuthorityData = nil
}
return mergedClusterInfo, nil
}
// inClusterClientConfig makes a config that will work from within a kubernetes cluster container environment.
// Can take options overrides for flags explicitly provided to the command inside the cluster container.
type inClusterClientConfig struct {
overrides *ConfigOverrides
inClusterConfigProvider func() (*restclient.Config, error)
}
var _ ClientConfig = &inClusterClientConfig{}
func (config *inClusterClientConfig) RawConfig() (clientcmdapi.Config, error) {
return clientcmdapi.Config{}, fmt.Errorf("inCluster environment config doesn't support multiple clusters")
}
func (config *inClusterClientConfig) ClientConfig() (*restclient.Config, error) {
if config.inClusterConfigProvider == nil {
config.inClusterConfigProvider = restclient.InClusterConfig
}
icc, err := config.inClusterConfigProvider()
if err != nil {
return nil, err
}
// in-cluster configs only takes a host, token, or CA file
// if any of them were individually provided, ovewrite anything else
if config.overrides != nil {
if server := config.overrides.ClusterInfo.Server; len(server) > 0 {
icc.Host = server
}
if token := config.overrides.AuthInfo.Token; len(token) > 0 {
icc.BearerToken = token
}
if certificateAuthorityFile := config.overrides.ClusterInfo.CertificateAuthority; len(certificateAuthorityFile) > 0 {
icc.TLSClientConfig.CAFile = certificateAuthorityFile
}
}
return icc, err
}
func (config *inClusterClientConfig) Namespace() (string, bool, error) {
// This way assumes you've set the POD_NAMESPACE environment variable using the downward API.
// This check has to be done first for backwards compatibility with the way InClusterConfig was originally set up
if ns := os.Getenv("POD_NAMESPACE"); ns != "" {
return ns, true, nil
}
// Fall back to the namespace associated with the service account token, if available
if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
if ns := strings.TrimSpace(string(data)); len(ns) > 0 {
return ns, true, nil
}
}
return "default", false, nil
}
func (config *inClusterClientConfig) ConfigAccess() ConfigAccess {
return NewDefaultClientConfigLoadingRules()
}
// Possible returns true if loading an inside-kubernetes-cluster is possible.
func (config *inClusterClientConfig) Possible() bool {
fi, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token")
return os.Getenv("KUBERNETES_SERVICE_HOST") != "" &&
os.Getenv("KUBERNETES_SERVICE_PORT") != "" &&
err == nil && !fi.IsDir()
}
// BuildConfigFromFlags is a helper function that builds configs from a master
// url or a kubeconfig filepath. These are passed in as command line flags for cluster
// components. Warnings should reflect this usage. If neither masterUrl or kubeconfigPath
// are passed in we fallback to inClusterConfig. If inClusterConfig fails, we fallback
// to the default config.
func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) {
if kubeconfigPath == "" && masterUrl == "" {
glog.Warningf("Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.")
kubeconfig, err := restclient.InClusterConfig()
if err == nil {
return kubeconfig, nil
}
glog.Warning("error creating inClusterConfig, falling back to default config: ", err)
}
return NewNonInteractiveDeferredLoadingClientConfig(
&ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
&ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig()
}
// BuildConfigFromKubeconfigGetter is a helper function that builds configs from a master
// url and a kubeconfigGetter.
func BuildConfigFromKubeconfigGetter(masterUrl string, kubeconfigGetter KubeconfigGetter) (*restclient.Config, error) {
// TODO: We do not need a DeferredLoader here. Refactor code and see if we can use DirectClientConfig here.
cc := NewNonInteractiveDeferredLoadingClientConfig(
&ClientConfigGetter{kubeconfigGetter: kubeconfigGetter},
&ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}})
return cc.ClientConfig()
}

View file

@ -0,0 +1,506 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"io/ioutil"
"os"
"reflect"
"strings"
"testing"
"github.com/imdario/mergo"
"k8s.io/kubernetes/pkg/client/restclient"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
func TestOldMergoLib(t *testing.T) {
type T struct {
X string
}
dst := T{X: "one"}
src := T{X: "two"}
mergo.Merge(&dst, &src)
if dst.X != "two" {
// mergo.Merge changed in an incompatible way with
//
// https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a
//
// We have to stay with the old version which still does eager
// copying from src to dst in structs.
t.Errorf("mergo.Merge library found with incompatible, new behavior")
}
}
func createValidTestConfig() *clientcmdapi.Config {
const (
server = "https://anything.com:8080"
token = "the-token"
)
config := clientcmdapi.NewConfig()
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: server,
}
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
Token: token,
}
config.Contexts["clean"] = &clientcmdapi.Context{
Cluster: "clean",
AuthInfo: "clean",
}
config.CurrentContext = "clean"
return config
}
func createCAValidTestConfig() *clientcmdapi.Config {
config := createValidTestConfig()
config.Clusters["clean"].CertificateAuthorityData = []byte{0, 0}
return config
}
func TestInsecureOverridesCA(t *testing.T) {
config := createCAValidTestConfig()
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{
ClusterInfo: clientcmdapi.Cluster{
InsecureSkipTLSVerify: true,
},
}, nil)
actualCfg, err := clientBuilder.ClientConfig()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
matchBoolArg(true, actualCfg.Insecure, t)
matchStringArg("", actualCfg.TLSClientConfig.CAFile, t)
matchByteArg(nil, actualCfg.TLSClientConfig.CAData, t)
}
func TestMergeContext(t *testing.T) {
const namespace = "overriden-namespace"
config := createValidTestConfig()
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil)
_, overridden, err := clientBuilder.Namespace()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if overridden {
t.Error("Expected namespace to not be overridden")
}
clientBuilder = NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{
Context: clientcmdapi.Context{
Namespace: namespace,
},
}, nil)
actual, overridden, err := clientBuilder.Namespace()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !overridden {
t.Error("Expected namespace to be overridden")
}
matchStringArg(namespace, actual, t)
}
func TestCertificateData(t *testing.T) {
caData := []byte("ca-data")
certData := []byte("cert-data")
keyData := []byte("key-data")
config := clientcmdapi.NewConfig()
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "https://localhost:8443",
CertificateAuthorityData: caData,
}
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
ClientCertificateData: certData,
ClientKeyData: keyData,
}
config.Contexts["clean"] = &clientcmdapi.Context{
Cluster: "clean",
AuthInfo: "clean",
}
config.CurrentContext = "clean"
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil)
clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Make sure cert data gets into config (will override file paths)
matchByteArg(caData, clientConfig.TLSClientConfig.CAData, t)
matchByteArg(certData, clientConfig.TLSClientConfig.CertData, t)
matchByteArg(keyData, clientConfig.TLSClientConfig.KeyData, t)
}
func TestBasicAuthData(t *testing.T) {
username := "myuser"
password := "mypass"
config := clientcmdapi.NewConfig()
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "https://localhost:8443",
}
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
Username: username,
Password: password,
}
config.Contexts["clean"] = &clientcmdapi.Context{
Cluster: "clean",
AuthInfo: "clean",
}
config.CurrentContext = "clean"
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil)
clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Make sure basic auth data gets into config
matchStringArg(username, clientConfig.Username, t)
matchStringArg(password, clientConfig.Password, t)
}
func TestBasicTokenFile(t *testing.T) {
token := "exampletoken"
f, err := ioutil.TempFile("", "tokenfile")
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(token), 0644); err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
config := clientcmdapi.NewConfig()
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "https://localhost:8443",
}
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
TokenFile: f.Name(),
}
config.Contexts["clean"] = &clientcmdapi.Context{
Cluster: "clean",
AuthInfo: "clean",
}
config.CurrentContext = "clean"
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil)
clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
matchStringArg(token, clientConfig.BearerToken, t)
}
func TestPrecedenceTokenFile(t *testing.T) {
token := "exampletoken"
f, err := ioutil.TempFile("", "tokenfile")
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(token), 0644); err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
config := clientcmdapi.NewConfig()
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "https://localhost:8443",
}
expectedToken := "expected"
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
Token: expectedToken,
TokenFile: f.Name(),
}
config.Contexts["clean"] = &clientcmdapi.Context{
Cluster: "clean",
AuthInfo: "clean",
}
config.CurrentContext = "clean"
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil)
clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
matchStringArg(expectedToken, clientConfig.BearerToken, t)
}
func TestCreateClean(t *testing.T) {
config := createValidTestConfig()
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil)
clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
matchStringArg(config.Clusters["clean"].Server, clientConfig.Host, t)
matchStringArg("", clientConfig.APIPath, t)
matchBoolArg(config.Clusters["clean"].InsecureSkipTLSVerify, clientConfig.Insecure, t)
matchStringArg(config.AuthInfos["clean"].Token, clientConfig.BearerToken, t)
}
func TestCreateCleanWithPrefix(t *testing.T) {
tt := []struct {
server string
host string
}{
{"https://anything.com:8080/foo/bar", "https://anything.com:8080/foo/bar"},
{"http://anything.com:8080/foo/bar", "http://anything.com:8080/foo/bar"},
{"http://anything.com:8080/foo/bar/", "http://anything.com:8080/foo/bar/"},
{"http://anything.com:8080/", "http://anything.com:8080/"},
{"http://anything.com:8080//", "http://anything.com:8080//"},
{"anything.com:8080/foo/bar", "anything.com:8080/foo/bar"},
{"anything.com:8080", "anything.com:8080"},
{"anything.com", "anything.com"},
{"anything", "anything"},
}
tt = append(tt, struct{ server, host string }{"", "http://localhost:8080"})
for _, tc := range tt {
config := createValidTestConfig()
cleanConfig := config.Clusters["clean"]
cleanConfig.Server = tc.server
config.Clusters["clean"] = cleanConfig
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{
ClusterDefaults: clientcmdapi.Cluster{Server: "http://localhost:8080"},
}, nil)
clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
matchStringArg(tc.host, clientConfig.Host, t)
}
}
func TestCreateCleanDefault(t *testing.T) {
config := createValidTestConfig()
clientBuilder := NewDefaultClientConfig(*config, &ConfigOverrides{})
clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
matchStringArg(config.Clusters["clean"].Server, clientConfig.Host, t)
matchBoolArg(config.Clusters["clean"].InsecureSkipTLSVerify, clientConfig.Insecure, t)
matchStringArg(config.AuthInfos["clean"].Token, clientConfig.BearerToken, t)
}
func TestCreateCleanDefaultCluster(t *testing.T) {
config := createValidTestConfig()
clientBuilder := NewDefaultClientConfig(*config, &ConfigOverrides{
ClusterDefaults: clientcmdapi.Cluster{Server: "http://localhost:8080"},
})
clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
matchStringArg(config.Clusters["clean"].Server, clientConfig.Host, t)
matchBoolArg(config.Clusters["clean"].InsecureSkipTLSVerify, clientConfig.Insecure, t)
matchStringArg(config.AuthInfos["clean"].Token, clientConfig.BearerToken, t)
}
func TestCreateMissingContextNoDefault(t *testing.T) {
const expectedErrorContains = "Context was not found for specified context"
config := createValidTestConfig()
clientBuilder := NewNonInteractiveClientConfig(*config, "not-present", &ConfigOverrides{}, nil)
_, err := clientBuilder.ClientConfig()
if err == nil {
t.Fatalf("Unexpected error: %v", err)
}
}
func TestCreateMissingContext(t *testing.T) {
const expectedErrorContains = "context was not found for specified context: not-present"
config := createValidTestConfig()
clientBuilder := NewNonInteractiveClientConfig(*config, "not-present", &ConfigOverrides{
ClusterDefaults: clientcmdapi.Cluster{Server: "http://localhost:8080"},
}, nil)
_, err := clientBuilder.ClientConfig()
if err == nil {
t.Fatalf("Expected error: %v", expectedErrorContains)
}
if !strings.Contains(err.Error(), expectedErrorContains) {
t.Fatalf("Expected error: %v, but got %v", expectedErrorContains, err)
}
}
func TestInClusterClientConfigPrecedence(t *testing.T) {
tt := []struct {
overrides *ConfigOverrides
}{
{
overrides: &ConfigOverrides{
ClusterInfo: clientcmdapi.Cluster{
Server: "https://host-from-overrides.com",
},
},
},
{
overrides: &ConfigOverrides{
AuthInfo: clientcmdapi.AuthInfo{
Token: "https://host-from-overrides.com",
},
},
},
{
overrides: &ConfigOverrides{
ClusterInfo: clientcmdapi.Cluster{
CertificateAuthority: "/path/to/ca-from-overrides.crt",
},
},
},
{
overrides: &ConfigOverrides{
ClusterInfo: clientcmdapi.Cluster{
Server: "https://host-from-overrides.com",
},
AuthInfo: clientcmdapi.AuthInfo{
Token: "https://host-from-overrides.com",
},
},
},
{
overrides: &ConfigOverrides{
ClusterInfo: clientcmdapi.Cluster{
Server: "https://host-from-overrides.com",
CertificateAuthority: "/path/to/ca-from-overrides.crt",
},
},
},
{
overrides: &ConfigOverrides{
ClusterInfo: clientcmdapi.Cluster{
CertificateAuthority: "/path/to/ca-from-overrides.crt",
},
AuthInfo: clientcmdapi.AuthInfo{
Token: "https://host-from-overrides.com",
},
},
},
{
overrides: &ConfigOverrides{
ClusterInfo: clientcmdapi.Cluster{
Server: "https://host-from-overrides.com",
CertificateAuthority: "/path/to/ca-from-overrides.crt",
},
AuthInfo: clientcmdapi.AuthInfo{
Token: "https://host-from-overrides.com",
},
},
},
{
overrides: &ConfigOverrides{},
},
}
for _, tc := range tt {
expectedServer := "https://host-from-cluster.com"
expectedToken := "token-from-cluster"
expectedCAFile := "/path/to/ca-from-cluster.crt"
icc := &inClusterClientConfig{
inClusterConfigProvider: func() (*restclient.Config, error) {
return &restclient.Config{
Host: expectedServer,
BearerToken: expectedToken,
TLSClientConfig: restclient.TLSClientConfig{
CAFile: expectedCAFile,
},
}, nil
},
overrides: tc.overrides,
}
clientConfig, err := icc.ClientConfig()
if err != nil {
t.Fatalf("Unxpected error: %v", err)
}
if overridenServer := tc.overrides.ClusterInfo.Server; len(overridenServer) > 0 {
expectedServer = overridenServer
}
if overridenToken := tc.overrides.AuthInfo.Token; len(overridenToken) > 0 {
expectedToken = overridenToken
}
if overridenCAFile := tc.overrides.ClusterInfo.CertificateAuthority; len(overridenCAFile) > 0 {
expectedCAFile = overridenCAFile
}
if clientConfig.Host != expectedServer {
t.Errorf("Expected server %v, got %v", expectedServer, clientConfig.Host)
}
if clientConfig.BearerToken != expectedToken {
t.Errorf("Expected token %v, got %v", expectedToken, clientConfig.BearerToken)
}
if clientConfig.TLSClientConfig.CAFile != expectedCAFile {
t.Errorf("Expected Certificate Authority %v, got %v", expectedCAFile, clientConfig.TLSClientConfig.CAFile)
}
}
}
func matchBoolArg(expected, got bool, t *testing.T) {
if expected != got {
t.Errorf("Expected %v, got %v", expected, got)
}
}
func matchStringArg(expected, got string, t *testing.T) {
if expected != got {
t.Errorf("Expected %q, got %q", expected, got)
}
}
func matchByteArg(expected, got []byte, t *testing.T) {
if !reflect.DeepEqual(expected, got) {
t.Errorf("Expected %v, got %v", expected, got)
}
}

View file

@ -0,0 +1,472 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"errors"
"os"
"path"
"path/filepath"
"reflect"
"sort"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/client/restclient"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
// ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files
type ConfigAccess interface {
// GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config
GetLoadingPrecedence() []string
// GetStartingConfig returns the config that subcommands should being operating against. It may or may not be merged depending on loading rules
GetStartingConfig() (*clientcmdapi.Config, error)
// GetDefaultFilename returns the name of the file you should write into (create if necessary), if you're trying to create a new stanza as opposed to updating an existing one.
GetDefaultFilename() string
// IsExplicitFile indicates whether or not this command is interested in exactly one file. This implementation only ever does that via a flag, but implementations that handle local, global, and flags may have more
IsExplicitFile() bool
// GetExplicitFile returns the particular file this command is operating against. This implementation only ever has one, but implementations that handle local, global, and flags may have more
GetExplicitFile() string
}
type PathOptions struct {
// GlobalFile is the full path to the file to load as the global (final) option
GlobalFile string
// EnvVar is the env var name that points to the list of kubeconfig files to load
EnvVar string
// ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file
ExplicitFileFlag string
// GlobalFileSubpath is an optional value used for displaying help
GlobalFileSubpath string
LoadingRules *ClientConfigLoadingRules
}
func (o *PathOptions) GetEnvVarFiles() []string {
if len(o.EnvVar) == 0 {
return []string{}
}
envVarValue := os.Getenv(o.EnvVar)
if len(envVarValue) == 0 {
return []string{}
}
return filepath.SplitList(envVarValue)
}
func (o *PathOptions) GetLoadingPrecedence() []string {
if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
return envVarFiles
}
return []string{o.GlobalFile}
}
func (o *PathOptions) GetStartingConfig() (*clientcmdapi.Config, error) {
// don't mutate the original
loadingRules := *o.LoadingRules
loadingRules.Precedence = o.GetLoadingPrecedence()
clientConfig := NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &ConfigOverrides{})
rawConfig, err := clientConfig.RawConfig()
if os.IsNotExist(err) {
return clientcmdapi.NewConfig(), nil
}
if err != nil {
return nil, err
}
return &rawConfig, nil
}
func (o *PathOptions) GetDefaultFilename() string {
if o.IsExplicitFile() {
return o.GetExplicitFile()
}
if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 {
if len(envVarFiles) == 1 {
return envVarFiles[0]
}
// if any of the envvar files already exists, return it
for _, envVarFile := range envVarFiles {
if _, err := os.Stat(envVarFile); err == nil {
return envVarFile
}
}
// otherwise, return the last one in the list
return envVarFiles[len(envVarFiles)-1]
}
return o.GlobalFile
}
func (o *PathOptions) IsExplicitFile() bool {
if len(o.LoadingRules.ExplicitPath) > 0 {
return true
}
return false
}
func (o *PathOptions) GetExplicitFile() string {
return o.LoadingRules.ExplicitPath
}
func NewDefaultPathOptions() *PathOptions {
ret := &PathOptions{
GlobalFile: RecommendedHomeFile,
EnvVar: RecommendedConfigPathEnvVar,
ExplicitFileFlag: RecommendedConfigPathFlag,
GlobalFileSubpath: path.Join(RecommendedHomeDir, RecommendedFileName),
LoadingRules: NewDefaultClientConfigLoadingRules(),
}
ret.LoadingRules.DoNotResolvePaths = true
return ret
}
// ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or
// uses the default destination file to write the results into. This results in multiple file reads, but it's very easy to follow.
// Preferences and CurrentContext should always be set in the default destination file. Since we can't distinguish between empty and missing values
// (no nil strings), we're forced have separate handling for them. In the kubeconfig cases, newConfig should have at most one difference,
// that means that this code will only write into a single file. If you want to relativizePaths, you must provide a fully qualified path in any
// modified element.
func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error {
possibleSources := configAccess.GetLoadingPrecedence()
// sort the possible kubeconfig files so we always "lock" in the same order
// to avoid deadlock (note: this can fail w/ symlinks, but... come on).
sort.Strings(possibleSources)
for _, filename := range possibleSources {
if err := lockFile(filename); err != nil {
return err
}
defer unlockFile(filename)
}
startingConfig, err := configAccess.GetStartingConfig()
if err != nil {
return err
}
// We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file.
// Special case the test for current context and preferences since those always write to the default file.
if reflect.DeepEqual(*startingConfig, newConfig) {
// nothing to do
return nil
}
if startingConfig.CurrentContext != newConfig.CurrentContext {
if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil {
return err
}
}
if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) {
if err := writePreferences(configAccess, newConfig.Preferences); err != nil {
return err
}
}
// Search every cluster, authInfo, and context. First from new to old for differences, then from old to new for deletions
for key, cluster := range newConfig.Clusters {
startingCluster, exists := startingConfig.Clusters[key]
if !reflect.DeepEqual(cluster, startingCluster) || !exists {
destinationFile := cluster.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite, err := getConfigFromFile(destinationFile)
if err != nil {
return err
}
t := *cluster
configToWrite.Clusters[key] = &t
configToWrite.Clusters[key].LocationOfOrigin = destinationFile
if relativizePaths {
if err := RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil {
return err
}
}
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
return err
}
}
}
for key, context := range newConfig.Contexts {
startingContext, exists := startingConfig.Contexts[key]
if !reflect.DeepEqual(context, startingContext) || !exists {
destinationFile := context.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite, err := getConfigFromFile(destinationFile)
if err != nil {
return err
}
configToWrite.Contexts[key] = context
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
return err
}
}
}
for key, authInfo := range newConfig.AuthInfos {
startingAuthInfo, exists := startingConfig.AuthInfos[key]
if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists {
destinationFile := authInfo.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite, err := getConfigFromFile(destinationFile)
if err != nil {
return err
}
t := *authInfo
configToWrite.AuthInfos[key] = &t
configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile
if relativizePaths {
if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil {
return err
}
}
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
return err
}
}
}
for key, cluster := range startingConfig.Clusters {
if _, exists := newConfig.Clusters[key]; !exists {
destinationFile := cluster.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite, err := getConfigFromFile(destinationFile)
if err != nil {
return err
}
delete(configToWrite.Clusters, key)
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
return err
}
}
}
for key, context := range startingConfig.Contexts {
if _, exists := newConfig.Contexts[key]; !exists {
destinationFile := context.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite, err := getConfigFromFile(destinationFile)
if err != nil {
return err
}
delete(configToWrite.Contexts, key)
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
return err
}
}
}
for key, authInfo := range startingConfig.AuthInfos {
if _, exists := newConfig.AuthInfos[key]; !exists {
destinationFile := authInfo.LocationOfOrigin
if len(destinationFile) == 0 {
destinationFile = configAccess.GetDefaultFilename()
}
configToWrite, err := getConfigFromFile(destinationFile)
if err != nil {
return err
}
delete(configToWrite.AuthInfos, key)
if err := WriteToFile(*configToWrite, destinationFile); err != nil {
return err
}
}
}
return nil
}
func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister {
return &persister{configAccess, user}
}
type persister struct {
configAccess ConfigAccess
user string
}
func (p *persister) Persist(config map[string]string) error {
newConfig, err := p.configAccess.GetStartingConfig()
if err != nil {
return err
}
authInfo, ok := newConfig.AuthInfos[p.user]
if ok && authInfo.AuthProvider != nil {
authInfo.AuthProvider.Config = config
ModifyConfig(p.configAccess, *newConfig, false)
}
return nil
}
// writeCurrentContext takes three possible paths.
// If newCurrentContext is the same as the startingConfig's current context, then we exit.
// If newCurrentContext has a value, then that value is written into the default destination file.
// If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file
func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error {
if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
return err
} else if startingConfig.CurrentContext == newCurrentContext {
return nil
}
if configAccess.IsExplicitFile() {
file := configAccess.GetExplicitFile()
currConfig, err := getConfigFromFile(file)
if err != nil {
return err
}
currConfig.CurrentContext = newCurrentContext
if err := WriteToFile(*currConfig, file); err != nil {
return err
}
return nil
}
if len(newCurrentContext) > 0 {
destinationFile := configAccess.GetDefaultFilename()
config, err := getConfigFromFile(destinationFile)
if err != nil {
return err
}
config.CurrentContext = newCurrentContext
if err := WriteToFile(*config, destinationFile); err != nil {
return err
}
return nil
}
// we're supposed to be clearing the current context. We need to find the first spot in the chain that is setting it and clear it
for _, file := range configAccess.GetLoadingPrecedence() {
if _, err := os.Stat(file); err == nil {
currConfig, err := getConfigFromFile(file)
if err != nil {
return err
}
if len(currConfig.CurrentContext) > 0 {
currConfig.CurrentContext = newCurrentContext
if err := WriteToFile(*currConfig, file); err != nil {
return err
}
return nil
}
}
}
return errors.New("no config found to write context")
}
func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error {
if startingConfig, err := configAccess.GetStartingConfig(); err != nil {
return err
} else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) {
return nil
}
if configAccess.IsExplicitFile() {
file := configAccess.GetExplicitFile()
currConfig, err := getConfigFromFile(file)
if err != nil {
return err
}
currConfig.Preferences = newPrefs
if err := WriteToFile(*currConfig, file); err != nil {
return err
}
return nil
}
for _, file := range configAccess.GetLoadingPrecedence() {
currConfig, err := getConfigFromFile(file)
if err != nil {
return err
}
if !reflect.DeepEqual(currConfig.Preferences, newPrefs) {
currConfig.Preferences = newPrefs
if err := WriteToFile(*currConfig, file); err != nil {
return err
}
return nil
}
}
return errors.New("no config found to write preferences")
}
// getConfigFromFile tries to read a kubeconfig file and if it can't, returns an error. One exception, missing files result in empty configs, not an error.
func getConfigFromFile(filename string) (*clientcmdapi.Config, error) {
config, err := LoadFromFile(filename)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if config == nil {
config = clientcmdapi.NewConfig()
}
return config, nil
}
// GetConfigFromFileOrDie tries to read a kubeconfig file and if it can't, it calls exit. One exception, missing files result in empty configs, not an exit
func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config {
config, err := getConfigFromFile(filename)
if err != nil {
glog.FatalDepth(1, err)
}
return config
}

View file

@ -0,0 +1,37 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Package clientcmd provides one stop shopping for building a working client from a fixed config,
from a .kubeconfig file, from command line flags, or from any merged combination.
Sample usage from merged .kubeconfig files (local directory, home directory)
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// if you want to change the loading rules (which files in which order), you can do so here
configOverrides := &clientcmd.ConfigOverrides{}
// if you want to change override values or bind them to flags, there are methods to help you
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
// Do something
}
client, err := metav1.New(config)
// ...
*/
package clientcmd // import "k8s.io/kubernetes/pkg/client/unversioned/clientcmd"

View file

@ -0,0 +1,35 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"fmt"
"strconv"
"time"
)
// ParseTimeout returns a parsed duration from a string
// A duration string value must be a positive integer, optionally followed by a corresponding time unit (s|m|h).
func ParseTimeout(duration string) (time.Duration, error) {
if i, err := strconv.ParseInt(duration, 10, 64); err == nil && i >= 0 {
return (time.Duration(i) * time.Second), nil
}
if requestTimeout, err := time.ParseDuration(duration); err == nil {
return requestTimeout, nil
}
return 0, fmt.Errorf("Invalid timeout value. Timeout must be a single integer in seconds, or an integer followed by a corresponding time unit (e.g. 1s | 2m | 3h)")
}

View file

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

View file

@ -0,0 +1,578 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/ghodss/yaml"
"k8s.io/apimachinery/pkg/runtime"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
clientcmdlatest "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api/latest"
)
var (
testConfigAlfa = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"red-user": {Token: "red-token"}},
Clusters: map[string]*clientcmdapi.Cluster{
"cow-cluster": {Server: "http://cow.org:8080"}},
Contexts: map[string]*clientcmdapi.Context{
"federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"}},
}
testConfigBravo = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"black-user": {Token: "black-token"}},
Clusters: map[string]*clientcmdapi.Cluster{
"pig-cluster": {Server: "http://pig.org:8080"}},
Contexts: map[string]*clientcmdapi.Context{
"queen-anne-context": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"}},
}
testConfigCharlie = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"green-user": {Token: "green-token"}},
Clusters: map[string]*clientcmdapi.Cluster{
"horse-cluster": {Server: "http://horse.org:8080"}},
Contexts: map[string]*clientcmdapi.Context{
"shaker-context": {AuthInfo: "green-user", Cluster: "horse-cluster", Namespace: "chisel-ns"}},
}
testConfigDelta = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"blue-user": {Token: "blue-token"}},
Clusters: map[string]*clientcmdapi.Cluster{
"chicken-cluster": {Server: "http://chicken.org:8080"}},
Contexts: map[string]*clientcmdapi.Context{
"gothic-context": {AuthInfo: "blue-user", Cluster: "chicken-cluster", Namespace: "plane-ns"}},
}
testConfigConflictAlfa = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"red-user": {Token: "a-different-red-token"},
"yellow-user": {Token: "yellow-token"}},
Clusters: map[string]*clientcmdapi.Cluster{
"cow-cluster": {Server: "http://a-different-cow.org:8080", InsecureSkipTLSVerify: true},
"donkey-cluster": {Server: "http://donkey.org:8080", InsecureSkipTLSVerify: true}},
CurrentContext: "federal-context",
}
)
func TestNonExistentCommandLineFile(t *testing.T) {
loadingRules := ClientConfigLoadingRules{
ExplicitPath: "bogus_file",
}
_, err := loadingRules.Load()
if err == nil {
t.Fatalf("Expected error for missing command-line file, got none")
}
if !strings.Contains(err.Error(), "bogus_file") {
t.Fatalf("Expected error about 'bogus_file', got %s", err.Error())
}
}
func TestToleratingMissingFiles(t *testing.T) {
loadingRules := ClientConfigLoadingRules{
Precedence: []string{"bogus1", "bogus2", "bogus3"},
}
_, err := loadingRules.Load()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
func TestErrorReadingFile(t *testing.T) {
commandLineFile, _ := ioutil.TempFile("", "")
defer os.Remove(commandLineFile.Name())
if err := ioutil.WriteFile(commandLineFile.Name(), []byte("bogus value"), 0644); err != nil {
t.Fatalf("Error creating tempfile: %v", err)
}
loadingRules := ClientConfigLoadingRules{
ExplicitPath: commandLineFile.Name(),
}
_, err := loadingRules.Load()
if err == nil {
t.Fatalf("Expected error for unloadable file, got none")
}
if !strings.Contains(err.Error(), commandLineFile.Name()) {
t.Fatalf("Expected error about '%s', got %s", commandLineFile.Name(), err.Error())
}
}
func TestErrorReadingNonFile(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Couldn't create tmpdir")
}
defer os.Remove(tmpdir)
loadingRules := ClientConfigLoadingRules{
ExplicitPath: tmpdir,
}
_, err = loadingRules.Load()
if err == nil {
t.Fatalf("Expected error for non-file, got none")
}
if !strings.Contains(err.Error(), tmpdir) {
t.Fatalf("Expected error about '%s', got %s", tmpdir, err.Error())
}
}
func TestConflictingCurrentContext(t *testing.T) {
commandLineFile, _ := ioutil.TempFile("", "")
defer os.Remove(commandLineFile.Name())
envVarFile, _ := ioutil.TempFile("", "")
defer os.Remove(envVarFile.Name())
mockCommandLineConfig := clientcmdapi.Config{
CurrentContext: "any-context-value",
}
mockEnvVarConfig := clientcmdapi.Config{
CurrentContext: "a-different-context",
}
WriteToFile(mockCommandLineConfig, commandLineFile.Name())
WriteToFile(mockEnvVarConfig, envVarFile.Name())
loadingRules := ClientConfigLoadingRules{
ExplicitPath: commandLineFile.Name(),
Precedence: []string{envVarFile.Name()},
}
mergedConfig, err := loadingRules.Load()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if mergedConfig.CurrentContext != mockCommandLineConfig.CurrentContext {
t.Errorf("expected %v, got %v", mockCommandLineConfig.CurrentContext, mergedConfig.CurrentContext)
}
}
func TestLoadingEmptyMaps(t *testing.T) {
configFile, _ := ioutil.TempFile("", "")
defer os.Remove(configFile.Name())
mockConfig := clientcmdapi.Config{
CurrentContext: "any-context-value",
}
WriteToFile(mockConfig, configFile.Name())
config, err := LoadFromFile(configFile.Name())
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if config.Clusters == nil {
t.Error("expected config.Clusters to be non-nil")
}
if config.AuthInfos == nil {
t.Error("expected config.AuthInfos to be non-nil")
}
if config.Contexts == nil {
t.Error("expected config.Contexts to be non-nil")
}
}
func TestResolveRelativePaths(t *testing.T) {
pathResolutionConfig1 := clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"relative-user-1": {ClientCertificate: "relative/client/cert", ClientKey: "../relative/client/key"},
"absolute-user-1": {ClientCertificate: "/absolute/client/cert", ClientKey: "/absolute/client/key"},
},
Clusters: map[string]*clientcmdapi.Cluster{
"relative-server-1": {CertificateAuthority: "../relative/ca"},
"absolute-server-1": {CertificateAuthority: "/absolute/ca"},
},
}
pathResolutionConfig2 := clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"relative-user-2": {ClientCertificate: "relative/client/cert2", ClientKey: "../relative/client/key2"},
"absolute-user-2": {ClientCertificate: "/absolute/client/cert2", ClientKey: "/absolute/client/key2"},
},
Clusters: map[string]*clientcmdapi.Cluster{
"relative-server-2": {CertificateAuthority: "../relative/ca2"},
"absolute-server-2": {CertificateAuthority: "/absolute/ca2"},
},
}
configDir1, _ := ioutil.TempDir("", "")
configFile1 := path.Join(configDir1, ".kubeconfig")
configDir1, _ = filepath.Abs(configDir1)
defer os.Remove(configFile1)
configDir2, _ := ioutil.TempDir("", "")
configDir2, _ = ioutil.TempDir(configDir2, "")
configFile2 := path.Join(configDir2, ".kubeconfig")
configDir2, _ = filepath.Abs(configDir2)
defer os.Remove(configFile2)
WriteToFile(pathResolutionConfig1, configFile1)
WriteToFile(pathResolutionConfig2, configFile2)
loadingRules := ClientConfigLoadingRules{
Precedence: []string{configFile1, configFile2},
}
mergedConfig, err := loadingRules.Load()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
foundClusterCount := 0
for key, cluster := range mergedConfig.Clusters {
if key == "relative-server-1" {
foundClusterCount++
matchStringArg(path.Join(configDir1, pathResolutionConfig1.Clusters["relative-server-1"].CertificateAuthority), cluster.CertificateAuthority, t)
}
if key == "relative-server-2" {
foundClusterCount++
matchStringArg(path.Join(configDir2, pathResolutionConfig2.Clusters["relative-server-2"].CertificateAuthority), cluster.CertificateAuthority, t)
}
if key == "absolute-server-1" {
foundClusterCount++
matchStringArg(pathResolutionConfig1.Clusters["absolute-server-1"].CertificateAuthority, cluster.CertificateAuthority, t)
}
if key == "absolute-server-2" {
foundClusterCount++
matchStringArg(pathResolutionConfig2.Clusters["absolute-server-2"].CertificateAuthority, cluster.CertificateAuthority, t)
}
}
if foundClusterCount != 4 {
t.Errorf("Expected 4 clusters, found %v: %v", foundClusterCount, mergedConfig.Clusters)
}
foundAuthInfoCount := 0
for key, authInfo := range mergedConfig.AuthInfos {
if key == "relative-user-1" {
foundAuthInfoCount++
matchStringArg(path.Join(configDir1, pathResolutionConfig1.AuthInfos["relative-user-1"].ClientCertificate), authInfo.ClientCertificate, t)
matchStringArg(path.Join(configDir1, pathResolutionConfig1.AuthInfos["relative-user-1"].ClientKey), authInfo.ClientKey, t)
}
if key == "relative-user-2" {
foundAuthInfoCount++
matchStringArg(path.Join(configDir2, pathResolutionConfig2.AuthInfos["relative-user-2"].ClientCertificate), authInfo.ClientCertificate, t)
matchStringArg(path.Join(configDir2, pathResolutionConfig2.AuthInfos["relative-user-2"].ClientKey), authInfo.ClientKey, t)
}
if key == "absolute-user-1" {
foundAuthInfoCount++
matchStringArg(pathResolutionConfig1.AuthInfos["absolute-user-1"].ClientCertificate, authInfo.ClientCertificate, t)
matchStringArg(pathResolutionConfig1.AuthInfos["absolute-user-1"].ClientKey, authInfo.ClientKey, t)
}
if key == "absolute-user-2" {
foundAuthInfoCount++
matchStringArg(pathResolutionConfig2.AuthInfos["absolute-user-2"].ClientCertificate, authInfo.ClientCertificate, t)
matchStringArg(pathResolutionConfig2.AuthInfos["absolute-user-2"].ClientKey, authInfo.ClientKey, t)
}
}
if foundAuthInfoCount != 4 {
t.Errorf("Expected 4 users, found %v: %v", foundAuthInfoCount, mergedConfig.AuthInfos)
}
}
func TestMigratingFile(t *testing.T) {
sourceFile, _ := ioutil.TempFile("", "")
defer os.Remove(sourceFile.Name())
destinationFile, _ := ioutil.TempFile("", "")
// delete the file so that we'll write to it
os.Remove(destinationFile.Name())
WriteToFile(testConfigAlfa, sourceFile.Name())
loadingRules := ClientConfigLoadingRules{
MigrationRules: map[string]string{destinationFile.Name(): sourceFile.Name()},
}
if _, err := loadingRules.Load(); err != nil {
t.Errorf("unexpected error %v", err)
}
// the load should have recreated this file
defer os.Remove(destinationFile.Name())
sourceContent, err := ioutil.ReadFile(sourceFile.Name())
if err != nil {
t.Errorf("unexpected error %v", err)
}
destinationContent, err := ioutil.ReadFile(destinationFile.Name())
if err != nil {
t.Errorf("unexpected error %v", err)
}
if !reflect.DeepEqual(sourceContent, destinationContent) {
t.Errorf("source and destination do not match")
}
}
func TestMigratingFileLeaveExistingFileAlone(t *testing.T) {
sourceFile, _ := ioutil.TempFile("", "")
defer os.Remove(sourceFile.Name())
destinationFile, _ := ioutil.TempFile("", "")
defer os.Remove(destinationFile.Name())
WriteToFile(testConfigAlfa, sourceFile.Name())
loadingRules := ClientConfigLoadingRules{
MigrationRules: map[string]string{destinationFile.Name(): sourceFile.Name()},
}
if _, err := loadingRules.Load(); err != nil {
t.Errorf("unexpected error %v", err)
}
destinationContent, err := ioutil.ReadFile(destinationFile.Name())
if err != nil {
t.Errorf("unexpected error %v", err)
}
if len(destinationContent) > 0 {
t.Errorf("destination should not have been touched")
}
}
func TestMigratingFileSourceMissingSkip(t *testing.T) {
sourceFilename := "some-missing-file"
destinationFile, _ := ioutil.TempFile("", "")
// delete the file so that we'll write to it
os.Remove(destinationFile.Name())
loadingRules := ClientConfigLoadingRules{
MigrationRules: map[string]string{destinationFile.Name(): sourceFilename},
}
if _, err := loadingRules.Load(); err != nil {
t.Errorf("unexpected error %v", err)
}
if _, err := os.Stat(destinationFile.Name()); !os.IsNotExist(err) {
t.Errorf("destination should not exist")
}
}
func TestFileLocking(t *testing.T) {
f, _ := ioutil.TempFile("", "")
defer os.Remove(f.Name())
err := lockFile(f.Name())
if err != nil {
t.Errorf("unexpected error while locking file: %v", err)
}
defer unlockFile(f.Name())
err = lockFile(f.Name())
if err == nil {
t.Error("expected error while locking file.")
}
}
func Example_noMergingOnExplicitPaths() {
commandLineFile, _ := ioutil.TempFile("", "")
defer os.Remove(commandLineFile.Name())
envVarFile, _ := ioutil.TempFile("", "")
defer os.Remove(envVarFile.Name())
WriteToFile(testConfigAlfa, commandLineFile.Name())
WriteToFile(testConfigConflictAlfa, envVarFile.Name())
loadingRules := ClientConfigLoadingRules{
ExplicitPath: commandLineFile.Name(),
Precedence: []string{envVarFile.Name()},
}
mergedConfig, err := loadingRules.Load()
json, err := runtime.Encode(clientcmdlatest.Codec, mergedConfig)
if err != nil {
fmt.Printf("Unexpected error: %v", err)
}
output, err := yaml.JSONToYAML(json)
if err != nil {
fmt.Printf("Unexpected error: %v", err)
}
fmt.Printf("%v", string(output))
// Output:
// apiVersion: v1
// clusters:
// - cluster:
// server: http://cow.org:8080
// name: cow-cluster
// contexts:
// - context:
// cluster: cow-cluster
// namespace: hammer-ns
// user: red-user
// name: federal-context
// current-context: ""
// kind: Config
// preferences: {}
// users:
// - name: red-user
// user:
// token: red-token
}
func Example_mergingSomeWithConflict() {
commandLineFile, _ := ioutil.TempFile("", "")
defer os.Remove(commandLineFile.Name())
envVarFile, _ := ioutil.TempFile("", "")
defer os.Remove(envVarFile.Name())
WriteToFile(testConfigAlfa, commandLineFile.Name())
WriteToFile(testConfigConflictAlfa, envVarFile.Name())
loadingRules := ClientConfigLoadingRules{
Precedence: []string{commandLineFile.Name(), envVarFile.Name()},
}
mergedConfig, err := loadingRules.Load()
json, err := runtime.Encode(clientcmdlatest.Codec, mergedConfig)
if err != nil {
fmt.Printf("Unexpected error: %v", err)
}
output, err := yaml.JSONToYAML(json)
if err != nil {
fmt.Printf("Unexpected error: %v", err)
}
fmt.Printf("%v", string(output))
// Output:
// apiVersion: v1
// clusters:
// - cluster:
// server: http://cow.org:8080
// name: cow-cluster
// - cluster:
// insecure-skip-tls-verify: true
// server: http://donkey.org:8080
// name: donkey-cluster
// contexts:
// - context:
// cluster: cow-cluster
// namespace: hammer-ns
// user: red-user
// name: federal-context
// current-context: federal-context
// kind: Config
// preferences: {}
// users:
// - name: red-user
// user:
// token: red-token
// - name: yellow-user
// user:
// token: yellow-token
}
func Example_mergingEverythingNoConflicts() {
commandLineFile, _ := ioutil.TempFile("", "")
defer os.Remove(commandLineFile.Name())
envVarFile, _ := ioutil.TempFile("", "")
defer os.Remove(envVarFile.Name())
currentDirFile, _ := ioutil.TempFile("", "")
defer os.Remove(currentDirFile.Name())
homeDirFile, _ := ioutil.TempFile("", "")
defer os.Remove(homeDirFile.Name())
WriteToFile(testConfigAlfa, commandLineFile.Name())
WriteToFile(testConfigBravo, envVarFile.Name())
WriteToFile(testConfigCharlie, currentDirFile.Name())
WriteToFile(testConfigDelta, homeDirFile.Name())
loadingRules := ClientConfigLoadingRules{
Precedence: []string{commandLineFile.Name(), envVarFile.Name(), currentDirFile.Name(), homeDirFile.Name()},
}
mergedConfig, err := loadingRules.Load()
json, err := runtime.Encode(clientcmdlatest.Codec, mergedConfig)
if err != nil {
fmt.Printf("Unexpected error: %v", err)
}
output, err := yaml.JSONToYAML(json)
if err != nil {
fmt.Printf("Unexpected error: %v", err)
}
fmt.Printf("%v", string(output))
// Output:
// apiVersion: v1
// clusters:
// - cluster:
// server: http://chicken.org:8080
// name: chicken-cluster
// - cluster:
// server: http://cow.org:8080
// name: cow-cluster
// - cluster:
// server: http://horse.org:8080
// name: horse-cluster
// - cluster:
// server: http://pig.org:8080
// name: pig-cluster
// contexts:
// - context:
// cluster: cow-cluster
// namespace: hammer-ns
// user: red-user
// name: federal-context
// - context:
// cluster: chicken-cluster
// namespace: plane-ns
// user: blue-user
// name: gothic-context
// - context:
// cluster: pig-cluster
// namespace: saw-ns
// user: black-user
// name: queen-anne-context
// - context:
// cluster: horse-cluster
// namespace: chisel-ns
// user: green-user
// name: shaker-context
// current-context: ""
// kind: Config
// preferences: {}
// users:
// - name: black-user
// user:
// token: black-token
// - name: blue-user
// user:
// token: blue-token
// - name: green-user
// user:
// token: green-token
// - name: red-user
// user:
// token: red-token
}

View file

@ -0,0 +1,154 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"io"
"sync"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/client/restclient"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
// DeferredLoadingClientConfig is a ClientConfig interface that is backed by a client config loader.
// It is used in cases where the loading rules may change after you've instantiated them and you want to be sure that
// the most recent rules are used. This is useful in cases where you bind flags to loading rule parameters before
// the parse happens and you want your calling code to be ignorant of how the values are being mutated to avoid
// passing extraneous information down a call stack
type DeferredLoadingClientConfig struct {
loader ClientConfigLoader
overrides *ConfigOverrides
fallbackReader io.Reader
clientConfig ClientConfig
loadingLock sync.Mutex
// provided for testing
icc InClusterConfig
}
// InClusterConfig abstracts details of whether the client is running in a cluster for testing.
type InClusterConfig interface {
ClientConfig
Possible() bool
}
// NewNonInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name
func NewNonInteractiveDeferredLoadingClientConfig(loader ClientConfigLoader, overrides *ConfigOverrides) ClientConfig {
return &DeferredLoadingClientConfig{loader: loader, overrides: overrides, icc: &inClusterClientConfig{overrides: overrides}}
}
// NewInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name and the fallback auth reader
func NewInteractiveDeferredLoadingClientConfig(loader ClientConfigLoader, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig {
return &DeferredLoadingClientConfig{loader: loader, overrides: overrides, icc: &inClusterClientConfig{overrides: overrides}, fallbackReader: fallbackReader}
}
func (config *DeferredLoadingClientConfig) createClientConfig() (ClientConfig, error) {
if config.clientConfig == nil {
config.loadingLock.Lock()
defer config.loadingLock.Unlock()
if config.clientConfig == nil {
mergedConfig, err := config.loader.Load()
if err != nil {
return nil, err
}
var mergedClientConfig ClientConfig
if config.fallbackReader != nil {
mergedClientConfig = NewInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.fallbackReader, config.loader)
} else {
mergedClientConfig = NewNonInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.loader)
}
config.clientConfig = mergedClientConfig
}
}
return config.clientConfig, nil
}
func (config *DeferredLoadingClientConfig) RawConfig() (clientcmdapi.Config, error) {
mergedConfig, err := config.createClientConfig()
if err != nil {
return clientcmdapi.Config{}, err
}
return mergedConfig.RawConfig()
}
// ClientConfig implements ClientConfig
func (config *DeferredLoadingClientConfig) ClientConfig() (*restclient.Config, error) {
mergedClientConfig, err := config.createClientConfig()
if err != nil {
return nil, err
}
// load the configuration and return on non-empty errors and if the
// content differs from the default config
mergedConfig, err := mergedClientConfig.ClientConfig()
switch {
case err != nil:
if !IsEmptyConfig(err) {
// return on any error except empty config
return nil, err
}
case mergedConfig != nil:
// the configuration is valid, but if this is equal to the defaults we should try
// in-cluster configuration
if !config.loader.IsDefaultConfig(mergedConfig) {
return mergedConfig, nil
}
}
// check for in-cluster configuration and use it
if config.icc.Possible() {
glog.V(4).Infof("Using in-cluster configuration")
return config.icc.ClientConfig()
}
// return the result of the merged client config
return mergedConfig, err
}
// Namespace implements KubeConfig
func (config *DeferredLoadingClientConfig) Namespace() (string, bool, error) {
mergedKubeConfig, err := config.createClientConfig()
if err != nil {
return "", false, err
}
ns, ok, err := mergedKubeConfig.Namespace()
// if we get an error and it is not empty config, or if the merged config defined an explicit namespace, or
// if in-cluster config is not possible, return immediately
if (err != nil && !IsEmptyConfig(err)) || ok || !config.icc.Possible() {
// return on any error except empty config
return ns, ok, err
}
glog.V(4).Infof("Using in-cluster namespace")
// allow the namespace from the service account token directory to be used.
return config.icc.Namespace()
}
// ConfigAccess implements ClientConfig
func (config *DeferredLoadingClientConfig) ConfigAccess() ConfigAccess {
return config.loader
}

View file

@ -0,0 +1,328 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"fmt"
"testing"
"k8s.io/kubernetes/pkg/client/restclient"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
type testLoader struct {
ClientConfigLoader
called bool
config *clientcmdapi.Config
err error
}
func (l *testLoader) Load() (*clientcmdapi.Config, error) {
l.called = true
return l.config, l.err
}
type testClientConfig struct {
config *restclient.Config
namespace string
namespaceSpecified bool
err error
}
func (c *testClientConfig) RawConfig() (clientcmdapi.Config, error) {
return clientcmdapi.Config{}, fmt.Errorf("unexpected call")
}
func (c *testClientConfig) ClientConfig() (*restclient.Config, error) {
return c.config, c.err
}
func (c *testClientConfig) Namespace() (string, bool, error) {
return c.namespace, c.namespaceSpecified, c.err
}
func (c *testClientConfig) ConfigAccess() ConfigAccess {
return nil
}
type testICC struct {
testClientConfig
possible bool
called bool
}
func (icc *testICC) Possible() bool {
icc.called = true
return icc.possible
}
func TestInClusterConfig(t *testing.T) {
default1 := &DirectClientConfig{
config: *createValidTestConfig(),
contextName: "clean",
overrides: &ConfigOverrides{},
}
invalidDefaultConfig := clientcmdapi.NewConfig()
invalidDefaultConfig.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "http://localhost:8080",
}
invalidDefaultConfig.Contexts["other"] = &clientcmdapi.Context{
Cluster: "clean",
}
invalidDefaultConfig.CurrentContext = "clean"
defaultInvalid := &DirectClientConfig{
config: *invalidDefaultConfig,
overrides: &ConfigOverrides{},
}
if _, err := defaultInvalid.ClientConfig(); err == nil || !IsConfigurationInvalid(err) {
t.Fatal(err)
}
config1, err := default1.ClientConfig()
if err != nil {
t.Fatal(err)
}
config2 := &restclient.Config{Host: "config2"}
err1 := fmt.Errorf("unique error")
testCases := map[string]struct {
clientConfig *testClientConfig
icc *testICC
defaultConfig *DirectClientConfig
checkedICC bool
result *restclient.Config
err error
}{
"in-cluster checked on other error": {
clientConfig: &testClientConfig{err: ErrEmptyConfig},
icc: &testICC{},
checkedICC: true,
result: nil,
err: ErrEmptyConfig,
},
"in-cluster not checked on non-empty error": {
clientConfig: &testClientConfig{err: ErrEmptyCluster},
icc: &testICC{},
checkedICC: false,
result: nil,
err: ErrEmptyCluster,
},
"in-cluster checked when config is default": {
defaultConfig: default1,
clientConfig: &testClientConfig{config: config1},
icc: &testICC{},
checkedICC: true,
result: config1,
err: nil,
},
"in-cluster not checked when default config is invalid": {
defaultConfig: defaultInvalid,
clientConfig: &testClientConfig{config: config1},
icc: &testICC{},
checkedICC: false,
result: config1,
err: nil,
},
"in-cluster not checked when config is not equal to default": {
defaultConfig: default1,
clientConfig: &testClientConfig{config: config2},
icc: &testICC{},
checkedICC: false,
result: config2,
err: nil,
},
"in-cluster checked when config is not equal to default and error is empty": {
clientConfig: &testClientConfig{config: config2, err: ErrEmptyConfig},
icc: &testICC{},
checkedICC: true,
result: config2,
err: ErrEmptyConfig,
},
"in-cluster error returned when config is empty": {
clientConfig: &testClientConfig{err: ErrEmptyConfig},
icc: &testICC{
possible: true,
testClientConfig: testClientConfig{
err: err1,
},
},
checkedICC: true,
result: nil,
err: err1,
},
"in-cluster config returned when config is empty": {
clientConfig: &testClientConfig{err: ErrEmptyConfig},
icc: &testICC{
possible: true,
testClientConfig: testClientConfig{
config: config2,
},
},
checkedICC: true,
result: config2,
err: nil,
},
"in-cluster not checked when standard default is invalid": {
defaultConfig: &DefaultClientConfig,
clientConfig: &testClientConfig{config: config2},
icc: &testICC{},
checkedICC: false,
result: config2,
err: nil,
},
}
for name, test := range testCases {
c := &DeferredLoadingClientConfig{icc: test.icc}
c.loader = &ClientConfigLoadingRules{DefaultClientConfig: test.defaultConfig}
c.clientConfig = test.clientConfig
cfg, err := c.ClientConfig()
if test.icc.called != test.checkedICC {
t.Errorf("%s: unexpected in-cluster-config call %t", name, test.icc.called)
}
if err != test.err || cfg != test.result {
t.Errorf("%s: unexpected result: %v %#v", name, err, cfg)
}
}
}
func TestInClusterConfigNamespace(t *testing.T) {
err1 := fmt.Errorf("unique error")
testCases := map[string]struct {
clientConfig *testClientConfig
icc *testICC
checkedICC bool
result string
ok bool
err error
}{
"in-cluster checked on empty error": {
clientConfig: &testClientConfig{err: ErrEmptyConfig},
icc: &testICC{},
checkedICC: true,
err: ErrEmptyConfig,
},
"in-cluster not checked on non-empty error": {
clientConfig: &testClientConfig{err: ErrEmptyCluster},
icc: &testICC{},
err: ErrEmptyCluster,
},
"in-cluster checked when config is default": {
clientConfig: &testClientConfig{},
icc: &testICC{},
checkedICC: true,
},
"in-cluster not checked when config is not equal to default": {
clientConfig: &testClientConfig{namespace: "test", namespaceSpecified: true},
icc: &testICC{},
result: "test",
ok: true,
},
"in-cluster checked when namespace is not specified, but is defaulted": {
clientConfig: &testClientConfig{namespace: "test", namespaceSpecified: false},
icc: &testICC{},
checkedICC: true,
result: "test",
ok: false,
},
"in-cluster error returned when config is empty": {
clientConfig: &testClientConfig{err: ErrEmptyConfig},
icc: &testICC{
possible: true,
testClientConfig: testClientConfig{
err: err1,
},
},
checkedICC: true,
err: err1,
},
"in-cluster config returned when config is empty": {
clientConfig: &testClientConfig{err: ErrEmptyConfig},
icc: &testICC{
possible: true,
testClientConfig: testClientConfig{
namespace: "test",
namespaceSpecified: true,
},
},
checkedICC: true,
result: "test",
ok: true,
},
"in-cluster config returned when config is empty and namespace is defaulted but not explicitly set": {
clientConfig: &testClientConfig{err: ErrEmptyConfig},
icc: &testICC{
possible: true,
testClientConfig: testClientConfig{
namespace: "test",
namespaceSpecified: false,
},
},
checkedICC: true,
result: "test",
ok: false,
},
}
for name, test := range testCases {
c := &DeferredLoadingClientConfig{icc: test.icc}
c.clientConfig = test.clientConfig
ns, ok, err := c.Namespace()
if test.icc.called != test.checkedICC {
t.Errorf("%s: unexpected in-cluster-config call %t", name, test.icc.called)
}
if err != test.err || ns != test.result || ok != test.ok {
t.Errorf("%s: unexpected result: %v %s %t", name, err, ns, ok)
}
}
}

View file

@ -0,0 +1,206 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"strconv"
"github.com/spf13/pflag"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
// ConfigOverrides holds values that should override whatever information is pulled from the actual Config object. You can't
// simply use an actual Config object, because Configs hold maps, but overrides are restricted to "at most one"
type ConfigOverrides struct {
AuthInfo clientcmdapi.AuthInfo
// ClusterDefaults are applied before the configured cluster info is loaded.
ClusterDefaults clientcmdapi.Cluster
ClusterInfo clientcmdapi.Cluster
Context clientcmdapi.Context
CurrentContext string
Timeout string
}
// ConfigOverrideFlags holds the flag names to be used for binding command line flags. Notice that this structure tightly
// corresponds to ConfigOverrides
type ConfigOverrideFlags struct {
AuthOverrideFlags AuthOverrideFlags
ClusterOverrideFlags ClusterOverrideFlags
ContextOverrideFlags ContextOverrideFlags
CurrentContext FlagInfo
Timeout FlagInfo
}
// AuthOverrideFlags holds the flag names to be used for binding command line flags for AuthInfo objects
type AuthOverrideFlags struct {
ClientCertificate FlagInfo
ClientKey FlagInfo
Token FlagInfo
Impersonate FlagInfo
Username FlagInfo
Password FlagInfo
}
// ContextOverrideFlags holds the flag names to be used for binding command line flags for Cluster objects
type ContextOverrideFlags struct {
ClusterName FlagInfo
AuthInfoName FlagInfo
Namespace FlagInfo
}
// ClusterOverride holds the flag names to be used for binding command line flags for Cluster objects
type ClusterOverrideFlags struct {
APIServer FlagInfo
APIVersion FlagInfo
CertificateAuthority FlagInfo
InsecureSkipTLSVerify FlagInfo
}
// FlagInfo contains information about how to register a flag. This struct is useful if you want to provide a way for an extender to
// get back a set of recommended flag names, descriptions, and defaults, but allow for customization by an extender. This makes for
// coherent extension, without full prescription
type FlagInfo struct {
// LongName is the long string for a flag. If this is empty, then the flag will not be bound
LongName string
// ShortName is the single character for a flag. If this is empty, then there will be no short flag
ShortName string
// Default is the default value for the flag
Default string
// Description is the description for the flag
Description string
}
// BindStringFlag binds the flag based on the provided info. If LongName == "", nothing is registered
func (f FlagInfo) BindStringFlag(flags *pflag.FlagSet, target *string) {
// you can't register a flag without a long name
if len(f.LongName) > 0 {
flags.StringVarP(target, f.LongName, f.ShortName, f.Default, f.Description)
}
}
// BindBoolFlag binds the flag based on the provided info. If LongName == "", nothing is registered
func (f FlagInfo) BindBoolFlag(flags *pflag.FlagSet, target *bool) {
// you can't register a flag without a long name
if len(f.LongName) > 0 {
// try to parse Default as a bool. If it fails, assume false
boolVal, err := strconv.ParseBool(f.Default)
if err != nil {
boolVal = false
}
flags.BoolVarP(target, f.LongName, f.ShortName, boolVal, f.Description)
}
}
const (
FlagClusterName = "cluster"
FlagAuthInfoName = "user"
FlagContext = "context"
FlagNamespace = "namespace"
FlagAPIServer = "server"
FlagAPIVersion = "api-version"
FlagInsecure = "insecure-skip-tls-verify"
FlagCertFile = "client-certificate"
FlagKeyFile = "client-key"
FlagCAFile = "certificate-authority"
FlagEmbedCerts = "embed-certs"
FlagBearerToken = "token"
FlagImpersonate = "as"
FlagUsername = "username"
FlagPassword = "password"
FlagTimeout = "request-timeout"
)
// RecommendedConfigOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
func RecommendedConfigOverrideFlags(prefix string) ConfigOverrideFlags {
return ConfigOverrideFlags{
AuthOverrideFlags: RecommendedAuthOverrideFlags(prefix),
ClusterOverrideFlags: RecommendedClusterOverrideFlags(prefix),
ContextOverrideFlags: RecommendedContextOverrideFlags(prefix),
CurrentContext: FlagInfo{prefix + FlagContext, "", "", "The name of the kubeconfig context to use"},
Timeout: FlagInfo{prefix + FlagTimeout, "", "0", "The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests."},
}
}
// RecommendedAuthOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
func RecommendedAuthOverrideFlags(prefix string) AuthOverrideFlags {
return AuthOverrideFlags{
ClientCertificate: FlagInfo{prefix + FlagCertFile, "", "", "Path to a client certificate file for TLS"},
ClientKey: FlagInfo{prefix + FlagKeyFile, "", "", "Path to a client key file for TLS"},
Token: FlagInfo{prefix + FlagBearerToken, "", "", "Bearer token for authentication to the API server"},
Impersonate: FlagInfo{prefix + FlagImpersonate, "", "", "Username to impersonate for the operation"},
Username: FlagInfo{prefix + FlagUsername, "", "", "Username for basic authentication to the API server"},
Password: FlagInfo{prefix + FlagPassword, "", "", "Password for basic authentication to the API server"},
}
}
// RecommendedClusterOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
func RecommendedClusterOverrideFlags(prefix string) ClusterOverrideFlags {
return ClusterOverrideFlags{
APIServer: FlagInfo{prefix + FlagAPIServer, "", "", "The address and port of the Kubernetes API server"},
APIVersion: FlagInfo{prefix + FlagAPIVersion, "", "", "DEPRECATED: The API version to use when talking to the server"},
CertificateAuthority: FlagInfo{prefix + FlagCAFile, "", "", "Path to a cert. file for the certificate authority"},
InsecureSkipTLSVerify: FlagInfo{prefix + FlagInsecure, "", "false", "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"},
}
}
// RecommendedContextOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
func RecommendedContextOverrideFlags(prefix string) ContextOverrideFlags {
return ContextOverrideFlags{
ClusterName: FlagInfo{prefix + FlagClusterName, "", "", "The name of the kubeconfig cluster to use"},
AuthInfoName: FlagInfo{prefix + FlagAuthInfoName, "", "", "The name of the kubeconfig user to use"},
Namespace: FlagInfo{prefix + FlagNamespace, "n", "", "If present, the namespace scope for this CLI request"},
}
}
// BindOverrideFlags is a convenience method to bind the specified flags to their associated variables
func BindOverrideFlags(overrides *ConfigOverrides, flags *pflag.FlagSet, flagNames ConfigOverrideFlags) {
BindAuthInfoFlags(&overrides.AuthInfo, flags, flagNames.AuthOverrideFlags)
BindClusterFlags(&overrides.ClusterInfo, flags, flagNames.ClusterOverrideFlags)
BindContextFlags(&overrides.Context, flags, flagNames.ContextOverrideFlags)
flagNames.CurrentContext.BindStringFlag(flags, &overrides.CurrentContext)
flagNames.Timeout.BindStringFlag(flags, &overrides.Timeout)
}
// BindAuthInfoFlags is a convenience method to bind the specified flags to their associated variables
func BindAuthInfoFlags(authInfo *clientcmdapi.AuthInfo, flags *pflag.FlagSet, flagNames AuthOverrideFlags) {
flagNames.ClientCertificate.BindStringFlag(flags, &authInfo.ClientCertificate)
flagNames.ClientKey.BindStringFlag(flags, &authInfo.ClientKey)
flagNames.Token.BindStringFlag(flags, &authInfo.Token)
flagNames.Impersonate.BindStringFlag(flags, &authInfo.Impersonate)
flagNames.Username.BindStringFlag(flags, &authInfo.Username)
flagNames.Password.BindStringFlag(flags, &authInfo.Password)
}
// BindClusterFlags is a convenience method to bind the specified flags to their associated variables
func BindClusterFlags(clusterInfo *clientcmdapi.Cluster, flags *pflag.FlagSet, flagNames ClusterOverrideFlags) {
flagNames.APIServer.BindStringFlag(flags, &clusterInfo.Server)
// TODO: remove --api-version flag in 1.3.
flagNames.APIVersion.BindStringFlag(flags, &clusterInfo.APIVersion)
flags.MarkDeprecated(FlagAPIVersion, "flag is no longer respected and will be deleted in the next release")
flagNames.CertificateAuthority.BindStringFlag(flags, &clusterInfo.CertificateAuthority)
flagNames.InsecureSkipTLSVerify.BindBoolFlag(flags, &clusterInfo.InsecureSkipTLSVerify)
}
// BindFlags is a convenience method to bind the specified flags to their associated variables
func BindContextFlags(contextInfo *clientcmdapi.Context, flags *pflag.FlagSet, flagNames ContextOverrideFlags) {
flagNames.ClusterName.BindStringFlag(flags, &contextInfo.Cluster)
flagNames.AuthInfoName.BindStringFlag(flags, &contextInfo.AuthInfo)
flagNames.Namespace.BindStringFlag(flags, &contextInfo.Namespace)
}

View file

@ -0,0 +1,270 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"errors"
"fmt"
"os"
"reflect"
"strings"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/validation"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
var (
ErrNoContext = errors.New("no context chosen")
ErrEmptyConfig = errors.New("no configuration has been provided")
// message is for consistency with old behavior
ErrEmptyCluster = errors.New("cluster has no server defined")
)
type errContextNotFound struct {
ContextName string
}
func (e *errContextNotFound) Error() string {
return fmt.Sprintf("context was not found for specified context: %v", e.ContextName)
}
// IsContextNotFound returns a boolean indicating whether the error is known to
// report that a context was not found
func IsContextNotFound(err error) bool {
if err == nil {
return false
}
if _, ok := err.(*errContextNotFound); ok || err == ErrNoContext {
return true
}
return strings.Contains(err.Error(), "context was not found for specified context")
}
// IsEmptyConfig returns true if the provided error indicates the provided configuration
// is empty.
func IsEmptyConfig(err error) bool {
switch t := err.(type) {
case errConfigurationInvalid:
return len(t) == 1 && t[0] == ErrEmptyConfig
}
return err == ErrEmptyConfig
}
// errConfigurationInvalid is a set of errors indicating the configuration is invalid.
type errConfigurationInvalid []error
// errConfigurationInvalid implements error and Aggregate
var _ error = errConfigurationInvalid{}
var _ utilerrors.Aggregate = errConfigurationInvalid{}
func newErrConfigurationInvalid(errs []error) error {
switch len(errs) {
case 0:
return nil
default:
return errConfigurationInvalid(errs)
}
}
// Error implements the error interface
func (e errConfigurationInvalid) Error() string {
return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error())
}
// Errors implements the AggregateError interface
func (e errConfigurationInvalid) Errors() []error {
return e
}
// IsConfigurationInvalid returns true if the provided error indicates the configuration is invalid.
func IsConfigurationInvalid(err error) bool {
switch err.(type) {
case *errContextNotFound, errConfigurationInvalid:
return true
}
return IsContextNotFound(err)
}
// Validate checks for errors in the Config. It does not return early so that it can find as many errors as possible.
func Validate(config clientcmdapi.Config) error {
validationErrors := make([]error, 0)
if clientcmdapi.IsConfigEmpty(&config) {
return newErrConfigurationInvalid([]error{ErrEmptyConfig})
}
if len(config.CurrentContext) != 0 {
if _, exists := config.Contexts[config.CurrentContext]; !exists {
validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext})
}
}
for contextName, context := range config.Contexts {
validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
}
for authInfoName, authInfo := range config.AuthInfos {
validationErrors = append(validationErrors, validateAuthInfo(authInfoName, *authInfo)...)
}
for clusterName, clusterInfo := range config.Clusters {
validationErrors = append(validationErrors, validateClusterInfo(clusterName, *clusterInfo)...)
}
return newErrConfigurationInvalid(validationErrors)
}
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
func ConfirmUsable(config clientcmdapi.Config, passedContextName string) error {
validationErrors := make([]error, 0)
if clientcmdapi.IsConfigEmpty(&config) {
return newErrConfigurationInvalid([]error{ErrEmptyConfig})
}
var contextName string
if len(passedContextName) != 0 {
contextName = passedContextName
} else {
contextName = config.CurrentContext
}
if len(contextName) == 0 {
return ErrNoContext
}
context, exists := config.Contexts[contextName]
if !exists {
validationErrors = append(validationErrors, &errContextNotFound{contextName})
}
if exists {
validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, *config.AuthInfos[context.AuthInfo])...)
validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, *config.Clusters[context.Cluster])...)
}
return newErrConfigurationInvalid(validationErrors)
}
// validateClusterInfo looks for conflicts and errors in the cluster info
func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) []error {
validationErrors := make([]error, 0)
if reflect.DeepEqual(clientcmdapi.Cluster{}, clusterInfo) {
return []error{ErrEmptyCluster}
}
if len(clusterInfo.Server) == 0 {
if len(clusterName) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("default cluster has no server defined"))
} else {
validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName))
}
}
// Make sure CA data and CA file aren't both specified
if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName))
}
if len(clusterInfo.CertificateAuthority) != 0 {
clientCertCA, err := os.Open(clusterInfo.CertificateAuthority)
defer clientCertCA.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err))
}
}
return validationErrors
}
// validateAuthInfo looks for conflicts and errors in the auth info
func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error {
validationErrors := make([]error, 0)
usingAuthPath := false
methods := make([]string, 0, 3)
if len(authInfo.Token) != 0 {
methods = append(methods, "token")
}
if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 {
methods = append(methods, "basicAuth")
}
if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 {
// Make sure cert data and file aren't both specified
if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 {
validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName))
}
// Make sure key data and file aren't both specified
if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 {
validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName))
}
// Make sure a key is specified
if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName))
}
if len(authInfo.ClientCertificate) != 0 {
clientCertFile, err := os.Open(authInfo.ClientCertificate)
defer clientCertFile.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
}
}
if len(authInfo.ClientKey) != 0 {
clientKeyFile, err := os.Open(authInfo.ClientKey)
defer clientKeyFile.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
}
}
}
// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
if (len(methods) > 1) && (!usingAuthPath) {
validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods))
}
return validationErrors
}
// validateContext looks for errors in the context. It is not transitive, so errors in the reference authInfo or cluster configs are not included in this return
func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error {
validationErrors := make([]error, 0)
if len(context.AuthInfo) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName))
} else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName))
}
if len(context.Cluster) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName))
} else if _, exists := config.Clusters[context.Cluster]; !exists {
validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName))
}
if len(context.Namespace) != 0 {
if len(validation.IsDNS1123Label(context.Namespace)) != 0 {
validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS_LABEL rules", context.Namespace, contextName))
}
}
return validationErrors
}

View file

@ -0,0 +1,432 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"io/ioutil"
"os"
"strings"
"testing"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
func TestConfirmUsableBadInfoButOkConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
config.Clusters["missing ca"] = &clientcmdapi.Cluster{
Server: "anything",
CertificateAuthority: "missing",
}
config.AuthInfos["error"] = &clientcmdapi.AuthInfo{
Username: "anything",
Token: "here",
}
config.Contexts["dirty"] = &clientcmdapi.Context{
Cluster: "missing ca",
AuthInfo: "error",
}
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "anything",
}
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
Token: "here",
}
config.Contexts["clean"] = &clientcmdapi.Context{
Cluster: "clean",
AuthInfo: "clean",
}
badValidation := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read certificate-authority"},
}
okTest := configValidationTest{
config: config,
}
okTest.testConfirmUsable("clean", t)
badValidation.testConfig(t)
}
func TestConfirmUsableBadInfoConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
config.Clusters["missing ca"] = &clientcmdapi.Cluster{
Server: "anything",
CertificateAuthority: "missing",
}
config.AuthInfos["error"] = &clientcmdapi.AuthInfo{
Username: "anything",
Token: "here",
}
config.Contexts["first"] = &clientcmdapi.Context{
Cluster: "missing ca",
AuthInfo: "error",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read certificate-authority"},
}
test.testConfirmUsable("first", t)
}
func TestConfirmUsableEmptyConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"invalid configuration: no configuration has been provided"},
}
test.testConfirmUsable("", t)
}
func TestConfirmUsableMissingConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"invalid configuration: no configuration has been provided"},
}
test.testConfirmUsable("not-here", t)
}
func TestValidateEmptyConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"invalid configuration: no configuration has been provided"},
}
test.testConfig(t)
}
func TestValidateMissingCurrentContextConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
config.CurrentContext = "anything"
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"context was not found for specified "},
}
test.testConfig(t)
}
func TestIsContextNotFound(t *testing.T) {
config := clientcmdapi.NewConfig()
config.CurrentContext = "anything"
err := Validate(*config)
if !IsContextNotFound(err) {
t.Errorf("Expected context not found, but got %v", err)
}
if !IsConfigurationInvalid(err) {
t.Errorf("Expected configuration invalid, but got %v", err)
}
}
func TestIsEmptyConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
err := Validate(*config)
if !IsEmptyConfig(err) {
t.Errorf("Expected context not found, but got %v", err)
}
if !IsConfigurationInvalid(err) {
t.Errorf("Expected configuration invalid, but got %v", err)
}
}
func TestIsConfigurationInvalid(t *testing.T) {
if newErrConfigurationInvalid([]error{}) != nil {
t.Errorf("unexpected error")
}
if newErrConfigurationInvalid([]error{ErrNoContext}) == ErrNoContext {
t.Errorf("unexpected error")
}
if newErrConfigurationInvalid([]error{ErrNoContext, ErrNoContext}) == nil {
t.Errorf("unexpected error")
}
if !IsConfigurationInvalid(newErrConfigurationInvalid([]error{ErrNoContext, ErrNoContext})) {
t.Errorf("unexpected error")
}
}
func TestValidateMissingReferencesConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
config.CurrentContext = "anything"
config.Contexts["anything"] = &clientcmdapi.Context{Cluster: "missing", AuthInfo: "missing"}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"user \"missing\" was not found for context \"anything\"", "cluster \"missing\" was not found for context \"anything\""},
}
test.testContext("anything", t)
test.testConfig(t)
}
func TestValidateEmptyContext(t *testing.T) {
config := clientcmdapi.NewConfig()
config.CurrentContext = "anything"
config.Contexts["anything"] = &clientcmdapi.Context{}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"user was not specified for context \"anything\"", "cluster was not specified for context \"anything\""},
}
test.testContext("anything", t)
test.testConfig(t)
}
func TestValidateEmptyClusterInfo(t *testing.T) {
config := clientcmdapi.NewConfig()
config.Clusters["empty"] = &clientcmdapi.Cluster{}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"cluster has no server defined"},
}
test.testCluster("empty", t)
test.testConfig(t)
}
func TestValidateMissingCAFileClusterInfo(t *testing.T) {
config := clientcmdapi.NewConfig()
config.Clusters["missing ca"] = &clientcmdapi.Cluster{
Server: "anything",
CertificateAuthority: "missing",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read certificate-authority"},
}
test.testCluster("missing ca", t)
test.testConfig(t)
}
func TestValidateCleanClusterInfo(t *testing.T) {
config := clientcmdapi.NewConfig()
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "anything",
}
test := configValidationTest{
config: config,
}
test.testCluster("clean", t)
test.testConfig(t)
}
func TestValidateCleanWithCAClusterInfo(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "")
defer os.Remove(tempFile.Name())
config := clientcmdapi.NewConfig()
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "anything",
CertificateAuthority: tempFile.Name(),
}
test := configValidationTest{
config: config,
}
test.testCluster("clean", t)
test.testConfig(t)
}
func TestValidateEmptyAuthInfo(t *testing.T) {
config := clientcmdapi.NewConfig()
config.AuthInfos["error"] = &clientcmdapi.AuthInfo{}
test := configValidationTest{
config: config,
}
test.testAuthInfo("error", t)
test.testConfig(t)
}
func TestValidateCertFilesNotFoundAuthInfo(t *testing.T) {
config := clientcmdapi.NewConfig()
config.AuthInfos["error"] = &clientcmdapi.AuthInfo{
ClientCertificate: "missing",
ClientKey: "missing",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read client-cert", "unable to read client-key"},
}
test.testAuthInfo("error", t)
test.testConfig(t)
}
func TestValidateCertDataOverridesFiles(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "")
defer os.Remove(tempFile.Name())
config := clientcmdapi.NewConfig()
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
ClientCertificate: tempFile.Name(),
ClientCertificateData: []byte("certdata"),
ClientKey: tempFile.Name(),
ClientKeyData: []byte("keydata"),
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"client-cert-data and client-cert are both specified", "client-key-data and client-key are both specified"},
}
test.testAuthInfo("clean", t)
test.testConfig(t)
}
func TestValidateCleanCertFilesAuthInfo(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "")
defer os.Remove(tempFile.Name())
config := clientcmdapi.NewConfig()
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
ClientCertificate: tempFile.Name(),
ClientKey: tempFile.Name(),
}
test := configValidationTest{
config: config,
}
test.testAuthInfo("clean", t)
test.testConfig(t)
}
func TestValidateCleanTokenAuthInfo(t *testing.T) {
config := clientcmdapi.NewConfig()
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
Token: "any-value",
}
test := configValidationTest{
config: config,
}
test.testAuthInfo("clean", t)
test.testConfig(t)
}
func TestValidateMultipleMethodsAuthInfo(t *testing.T) {
config := clientcmdapi.NewConfig()
config.AuthInfos["error"] = &clientcmdapi.AuthInfo{
Token: "token",
Username: "username",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"more than one authentication method", "token", "basicAuth"},
}
test.testAuthInfo("error", t)
test.testConfig(t)
}
type configValidationTest struct {
config *clientcmdapi.Config
expectedErrorSubstring []string
}
func (c configValidationTest) testContext(contextName string, t *testing.T) {
errs := validateContext(contextName, *c.config.Contexts[contextName], *c.config)
if len(c.expectedErrorSubstring) != 0 {
if len(errs) == 0 {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
}
for _, curr := range c.expectedErrorSubstring {
if len(errs) != 0 && !strings.Contains(utilerrors.NewAggregate(errs).Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, utilerrors.NewAggregate(errs))
}
}
} else {
if len(errs) != 0 {
t.Errorf("Unexpected error: %v", utilerrors.NewAggregate(errs))
}
}
}
func (c configValidationTest) testConfirmUsable(contextName string, t *testing.T) {
err := ConfirmUsable(*c.config, contextName)
if len(c.expectedErrorSubstring) != 0 {
if err == nil {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
} else {
for _, curr := range c.expectedErrorSubstring {
if err != nil && !strings.Contains(err.Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
}
}
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
}
func (c configValidationTest) testConfig(t *testing.T) {
err := Validate(*c.config)
if len(c.expectedErrorSubstring) != 0 {
if err == nil {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
} else {
for _, curr := range c.expectedErrorSubstring {
if err != nil && !strings.Contains(err.Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
}
}
if !IsConfigurationInvalid(err) {
t.Errorf("all errors should be configuration invalid: %v", err)
}
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
}
func (c configValidationTest) testCluster(clusterName string, t *testing.T) {
errs := validateClusterInfo(clusterName, *c.config.Clusters[clusterName])
if len(c.expectedErrorSubstring) != 0 {
if len(errs) == 0 {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
}
for _, curr := range c.expectedErrorSubstring {
if len(errs) != 0 && !strings.Contains(utilerrors.NewAggregate(errs).Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, utilerrors.NewAggregate(errs))
}
}
} else {
if len(errs) != 0 {
t.Errorf("Unexpected error: %v", utilerrors.NewAggregate(errs))
}
}
}
func (c configValidationTest) testAuthInfo(authInfoName string, t *testing.T) {
errs := validateAuthInfo(authInfoName, *c.config.AuthInfos[authInfoName])
if len(c.expectedErrorSubstring) != 0 {
if len(errs) == 0 {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
}
for _, curr := range c.expectedErrorSubstring {
if len(errs) != 0 && !strings.Contains(utilerrors.NewAggregate(errs).Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, utilerrors.NewAggregate(errs))
}
}
} else {
if len(errs) != 0 {
t.Errorf("Unexpected error: %v", utilerrors.NewAggregate(errs))
}
}
}

View file

@ -0,0 +1,273 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package unversioned
import (
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/apps"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/apis/extensions"
appsclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/apps/internalversion"
batchclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/batch/internalversion"
coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
extensionsclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/extensions/internalversion"
)
// ControllerHasDesiredReplicas returns a condition that will be true if and only if
// the desired replica count for a controller's ReplicaSelector equals the Replicas count.
func ControllerHasDesiredReplicas(rcClient coreclient.ReplicationControllersGetter, controller *api.ReplicationController) wait.ConditionFunc {
// If we're given a controller where the status lags the spec, it either means that the controller is stale,
// or that the rc manager hasn't noticed the update yet. Polling status.Replicas is not safe in the latter case.
desiredGeneration := controller.Generation
return func() (bool, error) {
ctrl, err := rcClient.ReplicationControllers(controller.Namespace).Get(controller.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
// There's a chance a concurrent update modifies the Spec.Replicas causing this check to pass,
// or, after this check has passed, a modification causes the rc manager to create more pods.
// This will not be an issue once we've implemented graceful delete for rcs, but till then
// concurrent stop operations on the same rc might have unintended side effects.
return ctrl.Status.ObservedGeneration >= desiredGeneration && ctrl.Status.Replicas == ctrl.Spec.Replicas, nil
}
}
// ReplicaSetHasDesiredReplicas returns a condition that will be true if and only if
// the desired replica count for a ReplicaSet's ReplicaSelector equals the Replicas count.
func ReplicaSetHasDesiredReplicas(rsClient extensionsclient.ReplicaSetsGetter, replicaSet *extensions.ReplicaSet) wait.ConditionFunc {
// If we're given a ReplicaSet where the status lags the spec, it either means that the
// ReplicaSet is stale, or that the ReplicaSet manager hasn't noticed the update yet.
// Polling status.Replicas is not safe in the latter case.
desiredGeneration := replicaSet.Generation
return func() (bool, error) {
rs, err := rsClient.ReplicaSets(replicaSet.Namespace).Get(replicaSet.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
// There's a chance a concurrent update modifies the Spec.Replicas causing this check to
// pass, or, after this check has passed, a modification causes the ReplicaSet manager to
// create more pods. This will not be an issue once we've implemented graceful delete for
// ReplicaSets, but till then concurrent stop operations on the same ReplicaSet might have
// unintended side effects.
return rs.Status.ObservedGeneration >= desiredGeneration && rs.Status.Replicas == rs.Spec.Replicas, nil
}
}
// StatefulSetHasDesiredPets returns a conditon that checks the number of petset replicas
func StatefulSetHasDesiredPets(psClient appsclient.StatefulSetsGetter, petset *apps.StatefulSet) wait.ConditionFunc {
// TODO: Differentiate between 0 pets and a really quick scale down using generation.
return func() (bool, error) {
ps, err := psClient.StatefulSets(petset.Namespace).Get(petset.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
return ps.Status.Replicas == ps.Spec.Replicas, nil
}
}
// JobHasDesiredParallelism returns a condition that will be true if the desired parallelism count
// for a job equals the current active counts or is less by an appropriate successful/unsuccessful count.
func JobHasDesiredParallelism(jobClient batchclient.JobsGetter, job *batch.Job) wait.ConditionFunc {
return func() (bool, error) {
job, err := jobClient.Jobs(job.Namespace).Get(job.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
// desired parallelism can be either the exact number, in which case return immediately
if job.Status.Active == *job.Spec.Parallelism {
return true, nil
}
if job.Spec.Completions == nil {
// A job without specified completions needs to wait for Active to reach Parallelism.
return false, nil
}
// otherwise count successful
progress := *job.Spec.Completions - job.Status.Active - job.Status.Succeeded
return progress == 0, nil
}
}
// DeploymentHasDesiredReplicas returns a condition that will be true if and only if
// the desired replica count for a deployment equals its updated replicas count.
// (non-terminated pods that have the desired template spec).
func DeploymentHasDesiredReplicas(dClient extensionsclient.DeploymentsGetter, deployment *extensions.Deployment) wait.ConditionFunc {
// If we're given a deployment where the status lags the spec, it either
// means that the deployment is stale, or that the deployment manager hasn't
// noticed the update yet. Polling status.Replicas is not safe in the latter
// case.
desiredGeneration := deployment.Generation
return func() (bool, error) {
deployment, err := dClient.Deployments(deployment.Namespace).Get(deployment.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
return deployment.Status.ObservedGeneration >= desiredGeneration &&
deployment.Status.UpdatedReplicas == deployment.Spec.Replicas, nil
}
}
// ErrPodCompleted is returned by PodRunning or PodContainerRunning to indicate that
// the pod has already reached completed state.
var ErrPodCompleted = fmt.Errorf("pod ran to completion")
// ErrContainerTerminated is returned by PodContainerRunning in the intermediate
// state where the pod indicates it's still running, but its container is already terminated
var ErrContainerTerminated = fmt.Errorf("container terminated")
// PodRunning returns true if the pod is running, false if the pod has not yet reached running state,
// returns ErrPodCompleted if the pod has run to completion, or an error in any other case.
func PodRunning(event watch.Event) (bool, error) {
switch event.Type {
case watch.Deleted:
return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "")
}
switch t := event.Object.(type) {
case *api.Pod:
switch t.Status.Phase {
case api.PodRunning:
return true, nil
case api.PodFailed, api.PodSucceeded:
return false, ErrPodCompleted
}
}
return false, nil
}
// PodCompleted returns true if the pod has run to completion, false if the pod has not yet
// reached running state, or an error in any other case.
func PodCompleted(event watch.Event) (bool, error) {
switch event.Type {
case watch.Deleted:
return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "")
}
switch t := event.Object.(type) {
case *api.Pod:
switch t.Status.Phase {
case api.PodFailed, api.PodSucceeded:
return true, nil
}
}
return false, nil
}
// PodRunningAndReady returns true if the pod is running and ready, false if the pod has not
// yet reached those states, returns ErrPodCompleted if the pod has run to completion, or
// an error in any other case.
func PodRunningAndReady(event watch.Event) (bool, error) {
switch event.Type {
case watch.Deleted:
return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "")
}
switch t := event.Object.(type) {
case *api.Pod:
switch t.Status.Phase {
case api.PodFailed, api.PodSucceeded:
return false, ErrPodCompleted
case api.PodRunning:
return api.IsPodReady(t), nil
}
}
return false, nil
}
// PodNotPending returns true if the pod has left the pending state, false if it has not,
// or an error in any other case (such as if the pod was deleted).
func PodNotPending(event watch.Event) (bool, error) {
switch event.Type {
case watch.Deleted:
return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "")
}
switch t := event.Object.(type) {
case *api.Pod:
switch t.Status.Phase {
case api.PodPending:
return false, nil
default:
return true, nil
}
}
return false, nil
}
// PodContainerRunning returns false until the named container has ContainerStatus running (at least once),
// and will return an error if the pod is deleted, runs to completion, or the container pod is not available.
func PodContainerRunning(containerName string) watch.ConditionFunc {
return func(event watch.Event) (bool, error) {
switch event.Type {
case watch.Deleted:
return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "")
}
switch t := event.Object.(type) {
case *api.Pod:
switch t.Status.Phase {
case api.PodRunning, api.PodPending:
case api.PodFailed, api.PodSucceeded:
return false, ErrPodCompleted
default:
return false, nil
}
for _, s := range t.Status.ContainerStatuses {
if s.Name != containerName {
continue
}
if s.State.Terminated != nil {
return false, ErrContainerTerminated
}
return s.State.Running != nil, nil
}
for _, s := range t.Status.InitContainerStatuses {
if s.Name != containerName {
continue
}
if s.State.Terminated != nil {
return false, ErrContainerTerminated
}
return s.State.Running != nil, nil
}
return false, nil
}
return false, nil
}
}
// ServiceAccountHasSecrets returns true if the service account has at least one secret,
// false if it does not, or an error.
func ServiceAccountHasSecrets(event watch.Event) (bool, error) {
switch event.Type {
case watch.Deleted:
return false, errors.NewNotFound(schema.GroupResource{Resource: "serviceaccounts"}, "")
}
switch t := event.Object.(type) {
case *api.ServiceAccount:
return len(t.Secrets) > 0, nil
}
return false, nil
}

View file

@ -0,0 +1,69 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package unversioned
import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/restclient"
// Import solely to initialize client auth plugins.
_ "k8s.io/kubernetes/plugin/pkg/client/auth"
)
const (
legacyAPIPath = "/api"
defaultAPIPath = "/apis"
)
// SetKubernetesDefaults sets default values on the provided client config for accessing the
// Kubernetes API or returns an error if any of the defaults are impossible or invalid.
// TODO: this method needs to be split into one that sets defaults per group, expected to be fix in PR "Refactoring clientcache.go and helper.go #14592"
func SetKubernetesDefaults(config *restclient.Config) error {
if config.APIPath == "" {
config.APIPath = legacyAPIPath
}
if config.GroupVersion == nil || config.GroupVersion.Group != api.GroupName {
g, err := api.Registry.Group(api.GroupName)
if err != nil {
return err
}
copyGroupVersion := g.GroupVersion
config.GroupVersion = &copyGroupVersion
}
if config.NegotiatedSerializer == nil {
config.NegotiatedSerializer = api.Codecs
}
return restclient.SetKubernetesDefaults(config)
}
func setGroupDefaults(groupName string, config *restclient.Config) error {
config.APIPath = defaultAPIPath
if config.UserAgent == "" {
config.UserAgent = restclient.DefaultKubernetesUserAgent()
}
if config.GroupVersion == nil || config.GroupVersion.Group != groupName {
g, err := api.Registry.Group(groupName)
if err != nil {
return err
}
copyGroupVersion := g.GroupVersion
config.GroupVersion = &copyGroupVersion
}
if config.NegotiatedSerializer == nil {
config.NegotiatedSerializer = api.Codecs
}
return nil
}

View file

@ -0,0 +1,182 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package unversioned
import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/client/restclient"
)
func TestSetKubernetesDefaults(t *testing.T) {
testCases := []struct {
Config restclient.Config
After restclient.Config
Err bool
}{
{
restclient.Config{},
restclient.Config{
APIPath: "/api",
ContentConfig: restclient.ContentConfig{
GroupVersion: &api.Registry.GroupOrDie(api.GroupName).GroupVersion,
NegotiatedSerializer: testapi.Default.NegotiatedSerializer(),
},
},
false,
},
// Add this test back when we fixed config and SetKubernetesDefaults
// {
// restclient.Config{
// GroupVersion: &schema.GroupVersion{Group: "not.a.group", Version: "not_an_api"},
// },
// restclient.Config{},
// true,
// },
}
for _, testCase := range testCases {
val := &testCase.Config
err := SetKubernetesDefaults(val)
val.UserAgent = ""
switch {
case err == nil && testCase.Err:
t.Errorf("expected error but was nil")
continue
case err != nil && !testCase.Err:
t.Errorf("unexpected error %v", err)
continue
case err != nil:
continue
}
if !reflect.DeepEqual(*val, testCase.After) {
t.Errorf("unexpected result object: %#v", val)
}
}
}
func TestHelperGetServerAPIVersions(t *testing.T) {
expect := []string{"v1", "v2", "v3"}
APIVersions := metav1.APIVersions{Versions: expect}
expect = append(expect, "group1/v1", "group1/v2", "group2/v1", "group2/v2")
APIGroupList := metav1.APIGroupList{
Groups: []metav1.APIGroup{
{
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "group1/v1",
},
{
GroupVersion: "group1/v2",
},
},
},
{
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "group2/v1",
},
{
GroupVersion: "group2/v2",
},
},
},
},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
var output []byte
var err error
switch req.URL.Path {
case "/api":
output, err = json.Marshal(APIVersions)
case "/apis":
output, err = json.Marshal(APIGroupList)
}
if err != nil {
t.Errorf("unexpected encoding error: %v", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(output)
}))
defer server.Close()
got, err := restclient.ServerAPIVersions(&restclient.Config{Host: server.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "invalid version", Version: "one"}, NegotiatedSerializer: testapi.Default.NegotiatedSerializer()}})
if err != nil {
t.Fatalf("unexpected encoding error: %v", err)
}
if e, a := expect, got; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
func TestSetsCodec(t *testing.T) {
testCases := map[string]struct {
Err bool
Prefix string
NegotiatedSerializer runtime.NegotiatedSerializer
}{
api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version: {
Err: false,
Prefix: "/api/" + api.Registry.GroupOrDie(api.GroupName).GroupVersion.Version,
NegotiatedSerializer: testapi.Default.NegotiatedSerializer(),
},
// Add this test back when we fixed config and SetKubernetesDefaults
// "invalidVersion": {true, "", nil},
}
for version, expected := range testCases {
conf := &restclient.Config{
Host: "127.0.0.1",
ContentConfig: restclient.ContentConfig{
GroupVersion: &schema.GroupVersion{Version: version},
},
}
var versionedPath string
err := SetKubernetesDefaults(conf)
if err == nil {
_, versionedPath, err = restclient.DefaultServerURL(conf.Host, conf.APIPath, *conf.GroupVersion, false)
}
switch {
case err == nil && expected.Err:
t.Errorf("expected error but was nil")
continue
case err != nil && !expected.Err:
t.Errorf("unexpected error %v", err)
continue
case err != nil:
continue
}
if e, a := expected.Prefix, versionedPath; e != a {
t.Errorf("expected %#v, got %#v", e, a)
}
if e, a := expected.NegotiatedSerializer, conf.NegotiatedSerializer; !reflect.DeepEqual(e, a) {
t.Errorf("expected %#v, got %#v", e, a)
}
}
}

View file

@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"portforward.go",
],
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/kubelet/server/portforward:go_default_library",
"//pkg/util/httpstream:go_default_library",
"//vendor:k8s.io/apimachinery/pkg/util/runtime",
],
)
go_test(
name = "go_default_test",
srcs = ["portforward_test.go"],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//pkg/client/restclient:go_default_library",
"//pkg/client/unversioned/remotecommand:go_default_library",
"//pkg/kubelet/server/portforward:go_default_library",
"//pkg/util/httpstream:go_default_library",
"//vendor:k8s.io/apimachinery/pkg/types",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,19 @@
/*
Copyright 2015 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 portforward adds support for SSH-like port forwarding from the client's
// local host to remote containers.
package portforward // import "k8s.io/kubernetes/pkg/client/unversioned/portforward"

View file

@ -0,0 +1,340 @@
/*
Copyright 2015 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 portforward
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"strconv"
"strings"
"sync"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/kubelet/server/portforward"
"k8s.io/kubernetes/pkg/util/httpstream"
)
// PortForwarder knows how to listen for local connections and forward them to
// a remote pod via an upgraded HTTP request.
type PortForwarder struct {
ports []ForwardedPort
stopChan <-chan struct{}
dialer httpstream.Dialer
streamConn httpstream.Connection
listeners []io.Closer
Ready chan struct{}
requestIDLock sync.Mutex
requestID int
out io.Writer
errOut io.Writer
}
// ForwardedPort contains a Local:Remote port pairing.
type ForwardedPort struct {
Local uint16
Remote uint16
}
/*
valid port specifications:
5000
- forwards from localhost:5000 to pod:5000
8888:5000
- forwards from localhost:8888 to pod:5000
0:5000
:5000
- selects a random available local port,
forwards from localhost:<random port> to pod:5000
*/
func parsePorts(ports []string) ([]ForwardedPort, error) {
var forwards []ForwardedPort
for _, portString := range ports {
parts := strings.Split(portString, ":")
var localString, remoteString string
if len(parts) == 1 {
localString = parts[0]
remoteString = parts[0]
} else if len(parts) == 2 {
localString = parts[0]
if localString == "" {
// support :5000
localString = "0"
}
remoteString = parts[1]
} else {
return nil, fmt.Errorf("Invalid port format '%s'", portString)
}
localPort, err := strconv.ParseUint(localString, 10, 16)
if err != nil {
return nil, fmt.Errorf("Error parsing local port '%s': %s", localString, err)
}
remotePort, err := strconv.ParseUint(remoteString, 10, 16)
if err != nil {
return nil, fmt.Errorf("Error parsing remote port '%s': %s", remoteString, err)
}
if remotePort == 0 {
return nil, fmt.Errorf("Remote port must be > 0")
}
forwards = append(forwards, ForwardedPort{uint16(localPort), uint16(remotePort)})
}
return forwards, nil
}
// New creates a new PortForwarder.
func New(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (*PortForwarder, error) {
if len(ports) == 0 {
return nil, errors.New("You must specify at least 1 port")
}
parsedPorts, err := parsePorts(ports)
if err != nil {
return nil, err
}
return &PortForwarder{
dialer: dialer,
ports: parsedPorts,
stopChan: stopChan,
Ready: readyChan,
out: out,
errOut: errOut,
}, nil
}
// ForwardPorts formats and executes a port forwarding request. The connection will remain
// open until stopChan is closed.
func (pf *PortForwarder) ForwardPorts() error {
defer pf.Close()
var err error
pf.streamConn, _, err = pf.dialer.Dial(portforward.PortForwardProtocolV1Name)
if err != nil {
return fmt.Errorf("error upgrading connection: %s", err)
}
defer pf.streamConn.Close()
return pf.forward()
}
// forward dials the remote host specific in req, upgrades the request, starts
// listeners for each port specified in ports, and forwards local connections
// to the remote host via streams.
func (pf *PortForwarder) forward() error {
var err error
listenSuccess := false
for _, port := range pf.ports {
err = pf.listenOnPort(&port)
switch {
case err == nil:
listenSuccess = true
default:
if pf.errOut != nil {
fmt.Fprintf(pf.errOut, "Unable to listen on port %d: %v\n", port.Local, err)
}
}
}
if !listenSuccess {
return fmt.Errorf("Unable to listen on any of the requested ports: %v", pf.ports)
}
if pf.Ready != nil {
close(pf.Ready)
}
// wait for interrupt or conn closure
select {
case <-pf.stopChan:
case <-pf.streamConn.CloseChan():
runtime.HandleError(errors.New("lost connection to pod"))
}
return nil
}
// listenOnPort delegates tcp4 and tcp6 listener creation and waits for connections on both of these addresses.
// If both listener creation fail, an error is raised.
func (pf *PortForwarder) listenOnPort(port *ForwardedPort) error {
errTcp4 := pf.listenOnPortAndAddress(port, "tcp4", "127.0.0.1")
errTcp6 := pf.listenOnPortAndAddress(port, "tcp6", "[::1]")
if errTcp4 != nil && errTcp6 != nil {
return fmt.Errorf("All listeners failed to create with the following errors: %s, %s", errTcp4, errTcp6)
}
return nil
}
// listenOnPortAndAddress delegates listener creation and waits for new connections
// in the background f
func (pf *PortForwarder) listenOnPortAndAddress(port *ForwardedPort, protocol string, address string) error {
listener, err := pf.getListener(protocol, address, port)
if err != nil {
return err
}
pf.listeners = append(pf.listeners, listener)
go pf.waitForConnection(listener, *port)
return nil
}
// getListener creates a listener on the interface targeted by the given hostname on the given port with
// the given protocol. protocol is in net.Listen style which basically admits values like tcp, tcp4, tcp6
func (pf *PortForwarder) getListener(protocol string, hostname string, port *ForwardedPort) (net.Listener, error) {
listener, err := net.Listen(protocol, fmt.Sprintf("%s:%d", hostname, port.Local))
if err != nil {
runtime.HandleError(fmt.Errorf("Unable to create listener: Error %s", err))
return nil, err
}
listenerAddress := listener.Addr().String()
host, localPort, _ := net.SplitHostPort(listenerAddress)
localPortUInt, err := strconv.ParseUint(localPort, 10, 16)
if err != nil {
return nil, fmt.Errorf("Error parsing local port: %s from %s (%s)", err, listenerAddress, host)
}
port.Local = uint16(localPortUInt)
if pf.out != nil {
fmt.Fprintf(pf.out, "Forwarding from %s:%d -> %d\n", hostname, localPortUInt, port.Remote)
}
return listener, nil
}
// waitForConnection waits for new connections to listener and handles them in
// the background.
func (pf *PortForwarder) waitForConnection(listener net.Listener, port ForwardedPort) {
for {
conn, err := listener.Accept()
if err != nil {
// TODO consider using something like https://github.com/hydrogen18/stoppableListener?
if !strings.Contains(strings.ToLower(err.Error()), "use of closed network connection") {
runtime.HandleError(fmt.Errorf("Error accepting connection on port %d: %v", port.Local, err))
}
return
}
go pf.handleConnection(conn, port)
}
}
func (pf *PortForwarder) nextRequestID() int {
pf.requestIDLock.Lock()
defer pf.requestIDLock.Unlock()
id := pf.requestID
pf.requestID++
return id
}
// handleConnection copies data between the local connection and the stream to
// the remote server.
func (pf *PortForwarder) handleConnection(conn net.Conn, port ForwardedPort) {
defer conn.Close()
if pf.out != nil {
fmt.Fprintf(pf.out, "Handling connection for %d\n", port.Local)
}
requestID := pf.nextRequestID()
// create error stream
headers := http.Header{}
headers.Set(api.StreamType, api.StreamTypeError)
headers.Set(api.PortHeader, fmt.Sprintf("%d", port.Remote))
headers.Set(api.PortForwardRequestIDHeader, strconv.Itoa(requestID))
errorStream, err := pf.streamConn.CreateStream(headers)
if err != nil {
runtime.HandleError(fmt.Errorf("error creating error stream for port %d -> %d: %v", port.Local, port.Remote, err))
return
}
// we're not writing to this stream
errorStream.Close()
errorChan := make(chan error)
go func() {
message, err := ioutil.ReadAll(errorStream)
switch {
case err != nil:
errorChan <- fmt.Errorf("error reading from error stream for port %d -> %d: %v", port.Local, port.Remote, err)
case len(message) > 0:
errorChan <- fmt.Errorf("an error occurred forwarding %d -> %d: %v", port.Local, port.Remote, string(message))
}
close(errorChan)
}()
// create data stream
headers.Set(api.StreamType, api.StreamTypeData)
dataStream, err := pf.streamConn.CreateStream(headers)
if err != nil {
runtime.HandleError(fmt.Errorf("error creating forwarding stream for port %d -> %d: %v", port.Local, port.Remote, err))
return
}
localError := make(chan struct{})
remoteDone := make(chan struct{})
go func() {
// Copy from the remote side to the local port.
if _, err := io.Copy(conn, dataStream); err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
runtime.HandleError(fmt.Errorf("error copying from remote stream to local connection: %v", err))
}
// inform the select below that the remote copy is done
close(remoteDone)
}()
go func() {
// inform server we're not sending any more data after copy unblocks
defer dataStream.Close()
// Copy from the local port to the remote side.
if _, err := io.Copy(dataStream, conn); err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
runtime.HandleError(fmt.Errorf("error copying from local connection to remote stream: %v", err))
// break out of the select below without waiting for the other copy to finish
close(localError)
}
}()
// wait for either a local->remote error or for copying from remote->local to finish
select {
case <-remoteDone:
case <-localError:
}
// always expect something on errorChan (it may be nil)
err = <-errorChan
if err != nil {
runtime.HandleError(err)
}
}
func (pf *PortForwarder) Close() {
// stop all listeners
for _, l := range pf.listeners {
if err := l.Close(); err != nil {
runtime.HandleError(fmt.Errorf("error closing listener: %v", err))
}
}
}

View file

@ -0,0 +1,398 @@
/*
Copyright 2015 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 portforward
import (
"bytes"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
"k8s.io/kubernetes/pkg/kubelet/server/portforward"
"k8s.io/kubernetes/pkg/util/httpstream"
)
type fakeDialer struct {
dialed bool
conn httpstream.Connection
err error
negotiatedProtocol string
}
func (d *fakeDialer) Dial(protocols ...string) (httpstream.Connection, string, error) {
d.dialed = true
return d.conn, d.negotiatedProtocol, d.err
}
func TestParsePortsAndNew(t *testing.T) {
tests := []struct {
input []string
expected []ForwardedPort
expectParseError bool
expectNewError bool
}{
{input: []string{}, expectNewError: true},
{input: []string{"a"}, expectParseError: true, expectNewError: true},
{input: []string{":a"}, expectParseError: true, expectNewError: true},
{input: []string{"-1"}, expectParseError: true, expectNewError: true},
{input: []string{"65536"}, expectParseError: true, expectNewError: true},
{input: []string{"0"}, expectParseError: true, expectNewError: true},
{input: []string{"0:0"}, expectParseError: true, expectNewError: true},
{input: []string{"a:5000"}, expectParseError: true, expectNewError: true},
{input: []string{"5000:a"}, expectParseError: true, expectNewError: true},
{
input: []string{"5000", "5000:5000", "8888:5000", "5000:8888", ":5000", "0:5000"},
expected: []ForwardedPort{
{5000, 5000},
{5000, 5000},
{8888, 5000},
{5000, 8888},
{0, 5000},
{0, 5000},
},
},
}
for i, test := range tests {
parsed, err := parsePorts(test.input)
haveError := err != nil
if e, a := test.expectParseError, haveError; e != a {
t.Fatalf("%d: parsePorts: error expected=%t, got %t: %s", i, e, a, err)
}
dialer := &fakeDialer{}
expectedStopChan := make(chan struct{})
readyChan := make(chan struct{})
pf, err := New(dialer, test.input, expectedStopChan, readyChan, os.Stdout, os.Stderr)
haveError = err != nil
if e, a := test.expectNewError, haveError; e != a {
t.Fatalf("%d: New: error expected=%t, got %t: %s", i, e, a, err)
}
if test.expectParseError || test.expectNewError {
continue
}
for pi, expectedPort := range test.expected {
if e, a := expectedPort.Local, parsed[pi].Local; e != a {
t.Fatalf("%d: local expected: %d, got: %d", i, e, a)
}
if e, a := expectedPort.Remote, parsed[pi].Remote; e != a {
t.Fatalf("%d: remote expected: %d, got: %d", i, e, a)
}
}
if dialer.dialed {
t.Fatalf("%d: expected not dialed", i)
}
if e, a := test.expected, pf.ports; !reflect.DeepEqual(e, a) {
t.Fatalf("%d: ports: expected %#v, got %#v", i, e, a)
}
if e, a := expectedStopChan, pf.stopChan; e != a {
t.Fatalf("%d: stopChan: expected %#v, got %#v", i, e, a)
}
if pf.Ready == nil {
t.Fatalf("%d: Ready should be non-nil", i)
}
}
}
type GetListenerTestCase struct {
Hostname string
Protocol string
ShouldRaiseError bool
ExpectedListenerAddress string
}
func TestGetListener(t *testing.T) {
var pf PortForwarder
testCases := []GetListenerTestCase{
{
Hostname: "localhost",
Protocol: "tcp4",
ShouldRaiseError: false,
ExpectedListenerAddress: "127.0.0.1",
},
{
Hostname: "127.0.0.1",
Protocol: "tcp4",
ShouldRaiseError: false,
ExpectedListenerAddress: "127.0.0.1",
},
{
Hostname: "[::1]",
Protocol: "tcp6",
ShouldRaiseError: false,
ExpectedListenerAddress: "::1",
},
{
Hostname: "[::1]",
Protocol: "tcp4",
ShouldRaiseError: true,
},
{
Hostname: "127.0.0.1",
Protocol: "tcp6",
ShouldRaiseError: true,
},
{
// IPv6 address must be put into brackets. This test reveals this.
Hostname: "::1",
Protocol: "tcp6",
ShouldRaiseError: true,
},
}
for i, testCase := range testCases {
expectedListenerPort := "12345"
listener, err := pf.getListener(testCase.Protocol, testCase.Hostname, &ForwardedPort{12345, 12345})
if err != nil && strings.Contains(err.Error(), "cannot assign requested address") {
t.Logf("Can't test #%d: %v", i, err)
continue
}
errorRaised := err != nil
if testCase.ShouldRaiseError != errorRaised {
t.Errorf("Test case #%d failed: Data %v an error has been raised(%t) where it should not (or reciprocally): %v", i, testCase, testCase.ShouldRaiseError, err)
continue
}
if errorRaised {
continue
}
if listener == nil {
t.Errorf("Test case #%d did not raise an error but failed in initializing listener", i)
continue
}
host, port, _ := net.SplitHostPort(listener.Addr().String())
t.Logf("Asked a %s forward for: %s:%v, got listener %s:%s, expected: %s", testCase.Protocol, testCase.Hostname, 12345, host, port, expectedListenerPort)
if host != testCase.ExpectedListenerAddress {
t.Errorf("Test case #%d failed: Listener does not listen on exepected address: asked %v got %v", i, testCase.ExpectedListenerAddress, host)
}
if port != expectedListenerPort {
t.Errorf("Test case #%d failed: Listener does not listen on exepected port: asked %v got %v", i, expectedListenerPort, port)
}
listener.Close()
}
}
// fakePortForwarder simulates port forwarding for testing. It implements
// portforward.PortForwarder.
type fakePortForwarder struct {
lock sync.Mutex
// stores data expected from the stream per port
expected map[uint16]string
// stores data received from the stream per port
received map[uint16]string
// data to be sent to the stream per port
send map[uint16]string
}
var _ portforward.PortForwarder = &fakePortForwarder{}
func (pf *fakePortForwarder) PortForward(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error {
defer stream.Close()
// read from the client
received := make([]byte, len(pf.expected[port]))
n, err := stream.Read(received)
if err != nil {
return fmt.Errorf("error reading from client for port %d: %v", port, err)
}
if n != len(pf.expected[port]) {
return fmt.Errorf("unexpected length read from client for port %d: got %d, expected %d. data=%q", port, n, len(pf.expected[port]), string(received))
}
// store the received content
pf.lock.Lock()
pf.received[port] = string(received)
pf.lock.Unlock()
// send the hardcoded data to the client
io.Copy(stream, strings.NewReader(pf.send[port]))
return nil
}
// fakePortForwardServer creates an HTTP server that can handle port forwarding
// requests.
func fakePortForwardServer(t *testing.T, testName string, serverSends, expectedFromClient map[uint16]string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
pf := &fakePortForwarder{
expected: expectedFromClient,
received: make(map[uint16]string),
send: serverSends,
}
portforward.ServePortForward(w, req, pf, "pod", "uid", 0, 10*time.Second)
for port, expected := range expectedFromClient {
actual, ok := pf.received[port]
if !ok {
t.Errorf("%s: server didn't receive any data for port %d", testName, port)
continue
}
if expected != actual {
t.Errorf("%s: server expected to receive %q, got %q for port %d", testName, expected, actual, port)
}
}
for port, actual := range pf.received {
if _, ok := expectedFromClient[port]; !ok {
t.Errorf("%s: server unexpectedly received %q for port %d", testName, actual, port)
}
}
})
}
func TestForwardPorts(t *testing.T) {
tests := map[string]struct {
ports []string
clientSends map[uint16]string
serverSends map[uint16]string
}{
"forward 1 port with no data either direction": {
ports: []string{"5000"},
},
"forward 2 ports with bidirectional data": {
ports: []string{"5001", "6000"},
clientSends: map[uint16]string{
5001: "abcd",
6000: "ghij",
},
serverSends: map[uint16]string{
5001: "1234",
6000: "5678",
},
},
}
for testName, test := range tests {
server := httptest.NewServer(fakePortForwardServer(t, testName, test.serverSends, test.clientSends))
url, _ := url.Parse(server.URL)
exec, err := remotecommand.NewExecutor(&restclient.Config{}, "POST", url)
if err != nil {
t.Fatal(err)
}
stopChan := make(chan struct{}, 1)
readyChan := make(chan struct{})
pf, err := New(exec, test.ports, stopChan, readyChan, os.Stdout, os.Stderr)
if err != nil {
t.Fatalf("%s: unexpected error calling New: %v", testName, err)
}
doneChan := make(chan error)
go func() {
doneChan <- pf.ForwardPorts()
}()
<-pf.Ready
for port, data := range test.clientSends {
clientConn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil {
t.Errorf("%s: error dialing %d: %s", testName, port, err)
server.Close()
continue
}
defer clientConn.Close()
n, err := clientConn.Write([]byte(data))
if err != nil && err != io.EOF {
t.Errorf("%s: Error sending data '%s': %s", testName, data, err)
server.Close()
continue
}
if n == 0 {
t.Errorf("%s: unexpected write of 0 bytes", testName)
server.Close()
continue
}
b := make([]byte, 4)
n, err = clientConn.Read(b)
if err != nil && err != io.EOF {
t.Errorf("%s: Error reading data: %s", testName, err)
server.Close()
continue
}
if !bytes.Equal([]byte(test.serverSends[port]), b) {
t.Errorf("%s: expected to read '%s', got '%s'", testName, test.serverSends[port], b)
server.Close()
continue
}
}
// tell r.ForwardPorts to stop
close(stopChan)
// wait for r.ForwardPorts to actually return
err = <-doneChan
if err != nil {
t.Errorf("%s: unexpected error: %s", testName, err)
}
server.Close()
}
}
func TestForwardPortsReturnsErrorWhenAllBindsFailed(t *testing.T) {
server := httptest.NewServer(fakePortForwardServer(t, "allBindsFailed", nil, nil))
defer server.Close()
url, _ := url.Parse(server.URL)
exec, err := remotecommand.NewExecutor(&restclient.Config{}, "POST", url)
if err != nil {
t.Fatal(err)
}
stopChan1 := make(chan struct{}, 1)
defer close(stopChan1)
readyChan1 := make(chan struct{})
pf1, err := New(exec, []string{"5555"}, stopChan1, readyChan1, os.Stdout, os.Stderr)
if err != nil {
t.Fatalf("error creating pf1: %v", err)
}
go pf1.ForwardPorts()
<-pf1.Ready
stopChan2 := make(chan struct{}, 1)
readyChan2 := make(chan struct{})
pf2, err := New(exec, []string{"5555"}, stopChan2, readyChan2, os.Stdout, os.Stderr)
if err != nil {
t.Fatalf("error creating pf2: %v", err)
}
if err := pf2.ForwardPorts(); err == nil {
t.Fatal("expected non-nil error for pf2.ForwardPorts")
}
}

View file

@ -0,0 +1,72 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"errorstream.go",
"remotecommand.go",
"v1.go",
"v2.go",
"v3.go",
"v4.go",
],
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/client/transport:go_default_library",
"//pkg/kubelet/server/remotecommand:go_default_library",
"//pkg/util/exec:go_default_library",
"//pkg/util/httpstream:go_default_library",
"//pkg/util/httpstream/spdy:go_default_library",
"//pkg/util/term:go_default_library",
"//vendor:github.com/golang/glog",
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
"//vendor:k8s.io/apimachinery/pkg/util/runtime",
],
)
go_test(
name = "go_default_test",
srcs = [
"remotecommand_test.go",
"v2_test.go",
"v4_test.go",
],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/api/testapi:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/kubelet/server/remotecommand:go_default_library",
"//pkg/util/httpstream:go_default_library",
"//pkg/util/term:go_default_library",
"//vendor:github.com/stretchr/testify/require",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
"//vendor:k8s.io/apimachinery/pkg/types",
"//vendor:k8s.io/apimachinery/pkg/util/wait",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,20 @@
/*
Copyright 2015 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 remotecommand adds support for executing commands in containers,
// with support for separate stdin, stdout, and stderr streams, as well as
// TTY.
package remotecommand // import "k8s.io/kubernetes/pkg/client/unversioned/remotecommand"

View file

@ -0,0 +1,55 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
import (
"fmt"
"io"
"io/ioutil"
"k8s.io/apimachinery/pkg/util/runtime"
)
// errorStreamDecoder interprets the data on the error channel and creates a go error object from it.
type errorStreamDecoder interface {
decode(message []byte) error
}
// watchErrorStream watches the errorStream for remote command error data,
// decodes it with the given errorStreamDecoder, sends the decoded error (or nil if the remote
// command exited successfully) to the returned error channel, and closes it.
// This function returns immediately.
func watchErrorStream(errorStream io.Reader, d errorStreamDecoder) chan error {
errorChan := make(chan error)
go func() {
defer runtime.HandleCrash()
message, err := ioutil.ReadAll(errorStream)
switch {
case err != nil && err != io.EOF:
errorChan <- fmt.Errorf("error reading from error stream: %s", err)
case len(message) > 0:
errorChan <- d.decode(message)
default:
errorChan <- nil
}
close(errorChan)
}()
return errorChan
}

View file

@ -0,0 +1,179 @@
/*
Copyright 2015 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 remotecommand
import (
"fmt"
"io"
"net/http"
"net/url"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/transport"
"k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
"k8s.io/kubernetes/pkg/util/httpstream"
"k8s.io/kubernetes/pkg/util/httpstream/spdy"
"k8s.io/kubernetes/pkg/util/term"
)
// StreamOptions holds information pertaining to the current streaming session: supported stream
// protocols, input/output streams, if the client is requesting a TTY, and a terminal size queue to
// support terminal resizing.
type StreamOptions struct {
SupportedProtocols []string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Tty bool
TerminalSizeQueue term.TerminalSizeQueue
}
// Executor is an interface for transporting shell-style streams.
type Executor interface {
// Stream initiates the transport of the standard shell streams. It will transport any
// non-nil stream to a remote system, and return an error if a problem occurs. If tty
// is set, the stderr stream is not used (raw TTY manages stdout and stderr over the
// stdout stream).
Stream(options StreamOptions) error
}
// StreamExecutor supports the ability to dial an httpstream connection and the ability to
// run a command line stream protocol over that dialer.
type StreamExecutor interface {
Executor
httpstream.Dialer
}
// streamExecutor handles transporting standard shell streams over an httpstream connection.
type streamExecutor struct {
upgrader httpstream.UpgradeRoundTripper
transport http.RoundTripper
method string
url *url.URL
}
// NewExecutor connects to the provided server and upgrades the connection to
// multiplexed bidirectional streams. The current implementation uses SPDY,
// but this could be replaced with HTTP/2 once it's available, or something else.
// TODO: the common code between this and portforward could be abstracted.
func NewExecutor(config *restclient.Config, method string, url *url.URL) (StreamExecutor, error) {
tlsConfig, err := restclient.TLSConfigFor(config)
if err != nil {
return nil, err
}
upgradeRoundTripper := spdy.NewRoundTripper(tlsConfig)
wrapper, err := restclient.HTTPWrappersForConfig(config, upgradeRoundTripper)
if err != nil {
return nil, err
}
return &streamExecutor{
upgrader: upgradeRoundTripper,
transport: wrapper,
method: method,
url: url,
}, nil
}
// NewStreamExecutor upgrades the request so that it supports multiplexed bidirectional
// streams. This method takes a stream upgrader and an optional function that is invoked
// to wrap the round tripper. This method may be used by clients that are lower level than
// Kubernetes clients or need to provide their own upgrade round tripper.
func NewStreamExecutor(upgrader httpstream.UpgradeRoundTripper, fn func(http.RoundTripper) http.RoundTripper, method string, url *url.URL) (StreamExecutor, error) {
rt := http.RoundTripper(upgrader)
if fn != nil {
rt = fn(rt)
}
return &streamExecutor{
upgrader: upgrader,
transport: rt,
method: method,
url: url,
}, nil
}
// Dial opens a connection to a remote server and attempts to negotiate a SPDY
// connection. Upon success, it returns the connection and the protocol
// selected by the server.
func (e *streamExecutor) Dial(protocols ...string) (httpstream.Connection, string, error) {
rt := transport.DebugWrappers(e.transport)
// TODO the client probably shouldn't be created here, as it doesn't allow
// flexibility to allow callers to configure it.
client := &http.Client{Transport: rt}
req, err := http.NewRequest(e.method, e.url.String(), nil)
if err != nil {
return nil, "", fmt.Errorf("error creating request: %v", err)
}
for i := range protocols {
req.Header.Add(httpstream.HeaderProtocolVersion, protocols[i])
}
resp, err := client.Do(req)
if err != nil {
return nil, "", fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()
conn, err := e.upgrader.NewConnection(resp)
if err != nil {
return nil, "", err
}
return conn, resp.Header.Get(httpstream.HeaderProtocolVersion), nil
}
type streamCreator interface {
CreateStream(headers http.Header) (httpstream.Stream, error)
}
type streamProtocolHandler interface {
stream(conn streamCreator) error
}
// Stream opens a protocol streamer to the server and streams until a client closes
// the connection or the server disconnects.
func (e *streamExecutor) Stream(options StreamOptions) error {
conn, protocol, err := e.Dial(options.SupportedProtocols...)
if err != nil {
return err
}
defer conn.Close()
var streamer streamProtocolHandler
switch protocol {
case remotecommand.StreamProtocolV4Name:
streamer = newStreamProtocolV4(options)
case remotecommand.StreamProtocolV3Name:
streamer = newStreamProtocolV3(options)
case remotecommand.StreamProtocolV2Name:
streamer = newStreamProtocolV2(options)
case "":
glog.V(4).Infof("The server did not negotiate a streaming protocol version. Falling back to %s", remotecommand.StreamProtocolV1Name)
fallthrough
case remotecommand.StreamProtocolV1Name:
streamer = newStreamProtocolV1(options)
}
return streamer.stream(conn)
}

View file

@ -0,0 +1,369 @@
/*
Copyright 2015 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 remotecommand
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
"k8s.io/kubernetes/pkg/util/httpstream"
"k8s.io/kubernetes/pkg/util/term"
)
type fakeExecutor struct {
t *testing.T
testName string
errorData string
stdoutData string
stderrData string
expectStdin bool
stdinReceived bytes.Buffer
tty bool
messageCount int
command []string
exec bool
}
func (ex *fakeExecutor) ExecInContainer(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan term.Size, timeout time.Duration) error {
return ex.run(name, uid, container, cmd, in, out, err, tty)
}
func (ex *fakeExecutor) AttachContainer(name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan term.Size) error {
return ex.run(name, uid, container, nil, in, out, err, tty)
}
func (ex *fakeExecutor) run(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error {
ex.command = cmd
ex.tty = tty
if e, a := "pod", name; e != a {
ex.t.Errorf("%s: pod: expected %q, got %q", ex.testName, e, a)
}
if e, a := "uid", uid; e != string(a) {
ex.t.Errorf("%s: uid: expected %q, got %q", ex.testName, e, a)
}
if ex.exec {
if e, a := "ls /", strings.Join(ex.command, " "); e != a {
ex.t.Errorf("%s: command: expected %q, got %q", ex.testName, e, a)
}
} else {
if len(ex.command) > 0 {
ex.t.Errorf("%s: command: expected nothing, got %v", ex.testName, ex.command)
}
}
if len(ex.errorData) > 0 {
return errors.New(ex.errorData)
}
if len(ex.stdoutData) > 0 {
for i := 0; i < ex.messageCount; i++ {
fmt.Fprint(out, ex.stdoutData)
}
}
if len(ex.stderrData) > 0 {
for i := 0; i < ex.messageCount; i++ {
fmt.Fprint(err, ex.stderrData)
}
}
if ex.expectStdin {
io.Copy(&ex.stdinReceived, in)
}
return nil
}
func fakeServer(t *testing.T, testName string, exec bool, stdinData, stdoutData, stderrData, errorData string, tty bool, messageCount int, serverProtocols []string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
executor := &fakeExecutor{
t: t,
testName: testName,
errorData: errorData,
stdoutData: stdoutData,
stderrData: stderrData,
expectStdin: len(stdinData) > 0,
tty: tty,
messageCount: messageCount,
exec: exec,
}
opts, err := remotecommand.NewOptions(req)
require.NoError(t, err)
if exec {
cmd := req.URL.Query()[api.ExecCommandParamm]
remotecommand.ServeExec(w, req, executor, "pod", "uid", "container", cmd, opts, 0, 10*time.Second, serverProtocols)
} else {
remotecommand.ServeAttach(w, req, executor, "pod", "uid", "container", opts, 0, 10*time.Second, serverProtocols)
}
if e, a := strings.Repeat(stdinData, messageCount), executor.stdinReceived.String(); e != a {
t.Errorf("%s: stdin: expected %q, got %q", testName, e, a)
}
})
}
func TestStream(t *testing.T) {
testCases := []struct {
TestName string
Stdin string
Stdout string
Stderr string
Error string
Tty bool
MessageCount int
ClientProtocols []string
ServerProtocols []string
}{
{
TestName: "error",
Error: "bail",
Stdout: "a",
ClientProtocols: []string{remotecommand.StreamProtocolV2Name},
ServerProtocols: []string{remotecommand.StreamProtocolV2Name},
},
{
TestName: "in/out/err",
Stdin: "a",
Stdout: "b",
Stderr: "c",
MessageCount: 100,
ClientProtocols: []string{remotecommand.StreamProtocolV2Name},
ServerProtocols: []string{remotecommand.StreamProtocolV2Name},
},
{
TestName: "in/out/tty",
Stdin: "a",
Stdout: "b",
Tty: true,
MessageCount: 100,
ClientProtocols: []string{remotecommand.StreamProtocolV2Name},
ServerProtocols: []string{remotecommand.StreamProtocolV2Name},
},
{
// 1.0 kubectl, 1.0 kubelet
TestName: "unversioned client, unversioned server",
Stdout: "b",
Stderr: "c",
MessageCount: 1,
ClientProtocols: []string{},
ServerProtocols: []string{},
},
{
// 1.0 kubectl, 1.1+ kubelet
TestName: "unversioned client, versioned server",
Stdout: "b",
Stderr: "c",
MessageCount: 1,
ClientProtocols: []string{},
ServerProtocols: []string{remotecommand.StreamProtocolV2Name, remotecommand.StreamProtocolV1Name},
},
{
// 1.1+ kubectl, 1.0 kubelet
TestName: "versioned client, unversioned server",
Stdout: "b",
Stderr: "c",
MessageCount: 1,
ClientProtocols: []string{remotecommand.StreamProtocolV2Name, remotecommand.StreamProtocolV1Name},
ServerProtocols: []string{},
},
}
for _, testCase := range testCases {
for _, exec := range []bool{true, false} {
var name string
if exec {
name = testCase.TestName + " (exec)"
} else {
name = testCase.TestName + " (attach)"
}
var (
streamIn io.Reader
streamOut, streamErr io.Writer
)
localOut := &bytes.Buffer{}
localErr := &bytes.Buffer{}
server := httptest.NewServer(fakeServer(t, name, exec, testCase.Stdin, testCase.Stdout, testCase.Stderr, testCase.Error, testCase.Tty, testCase.MessageCount, testCase.ServerProtocols))
url, _ := url.ParseRequestURI(server.URL)
config := restclient.ContentConfig{
GroupVersion: &schema.GroupVersion{Group: "x"},
NegotiatedSerializer: testapi.Default.NegotiatedSerializer(),
}
c, err := restclient.NewRESTClient(url, "", config, -1, -1, nil, nil)
if err != nil {
t.Fatalf("failed to create a client: %v", err)
}
req := c.Post().Resource("testing")
if exec {
req.Param("command", "ls")
req.Param("command", "/")
}
if len(testCase.Stdin) > 0 {
req.Param(api.ExecStdinParam, "1")
streamIn = strings.NewReader(strings.Repeat(testCase.Stdin, testCase.MessageCount))
}
if len(testCase.Stdout) > 0 {
req.Param(api.ExecStdoutParam, "1")
streamOut = localOut
}
if testCase.Tty {
req.Param(api.ExecTTYParam, "1")
} else if len(testCase.Stderr) > 0 {
req.Param(api.ExecStderrParam, "1")
streamErr = localErr
}
conf := &restclient.Config{
Host: server.URL,
}
e, err := NewExecutor(conf, "POST", req.URL())
if err != nil {
t.Errorf("%s: unexpected error: %v", name, err)
continue
}
err = e.Stream(StreamOptions{
SupportedProtocols: testCase.ClientProtocols,
Stdin: streamIn,
Stdout: streamOut,
Stderr: streamErr,
Tty: testCase.Tty,
})
hasErr := err != nil
if len(testCase.Error) > 0 {
if !hasErr {
t.Errorf("%s: expected an error", name)
} else {
if e, a := testCase.Error, err.Error(); !strings.Contains(a, e) {
t.Errorf("%s: expected error stream read %q, got %q", name, e, a)
}
}
server.Close()
continue
}
if hasErr {
t.Errorf("%s: unexpected error: %v", name, err)
server.Close()
continue
}
if len(testCase.Stdout) > 0 {
if e, a := strings.Repeat(testCase.Stdout, testCase.MessageCount), localOut; e != a.String() {
t.Errorf("%s: expected stdout data %q, got %q", name, e, a)
}
}
if testCase.Stderr != "" {
if e, a := strings.Repeat(testCase.Stderr, testCase.MessageCount), localErr; e != a.String() {
t.Errorf("%s: expected stderr data %q, got %q", name, e, a)
}
}
server.Close()
}
}
}
type fakeUpgrader struct {
req *http.Request
resp *http.Response
conn httpstream.Connection
err, connErr error
checkResponse bool
t *testing.T
}
func (u *fakeUpgrader) RoundTrip(req *http.Request) (*http.Response, error) {
u.req = req
return u.resp, u.err
}
func (u *fakeUpgrader) NewConnection(resp *http.Response) (httpstream.Connection, error) {
if u.checkResponse && u.resp != resp {
u.t.Errorf("response objects passed did not match: %#v", resp)
}
return u.conn, u.connErr
}
type fakeConnection struct {
httpstream.Connection
}
// Dial is the common functionality between any stream based upgrader, regardless of protocol.
// This method ensures that someone can use a generic stream executor without being dependent
// on the core Kube client config behavior.
func TestDial(t *testing.T) {
upgrader := &fakeUpgrader{
t: t,
checkResponse: true,
conn: &fakeConnection{},
resp: &http.Response{
StatusCode: http.StatusSwitchingProtocols,
Body: ioutil.NopCloser(&bytes.Buffer{}),
},
}
var called bool
testFn := func(rt http.RoundTripper) http.RoundTripper {
if rt != upgrader {
t.Fatalf("unexpected round tripper: %#v", rt)
}
called = true
return rt
}
exec, err := NewStreamExecutor(upgrader, testFn, "POST", &url.URL{Host: "something.com", Scheme: "https"})
if err != nil {
t.Fatal(err)
}
conn, protocol, err := exec.Dial("protocol1")
if err != nil {
t.Fatal(err)
}
if conn != upgrader.conn {
t.Errorf("unexpected connection: %#v", conn)
}
if !called {
t.Errorf("wrapper not called")
}
_ = protocol
}

View file

@ -0,0 +1,160 @@
/*
Copyright 2015 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 remotecommand
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/util/httpstream"
)
// streamProtocolV1 implements the first version of the streaming exec & attach
// protocol. This version has some bugs, such as not being able to detect when
// non-interactive stdin data has ended. See http://issues.k8s.io/13394 and
// http://issues.k8s.io/13395 for more details.
type streamProtocolV1 struct {
StreamOptions
errorStream httpstream.Stream
remoteStdin httpstream.Stream
remoteStdout httpstream.Stream
remoteStderr httpstream.Stream
}
var _ streamProtocolHandler = &streamProtocolV1{}
func newStreamProtocolV1(options StreamOptions) streamProtocolHandler {
return &streamProtocolV1{
StreamOptions: options,
}
}
func (p *streamProtocolV1) stream(conn streamCreator) error {
doneChan := make(chan struct{}, 2)
errorChan := make(chan error)
cp := func(s string, dst io.Writer, src io.Reader) {
glog.V(6).Infof("Copying %s", s)
defer glog.V(6).Infof("Done copying %s", s)
if _, err := io.Copy(dst, src); err != nil && err != io.EOF {
glog.Errorf("Error copying %s: %v", s, err)
}
if s == api.StreamTypeStdout || s == api.StreamTypeStderr {
doneChan <- struct{}{}
}
}
// set up all the streams first
var err error
headers := http.Header{}
headers.Set(api.StreamType, api.StreamTypeError)
p.errorStream, err = conn.CreateStream(headers)
if err != nil {
return err
}
defer p.errorStream.Reset()
// Create all the streams first, then start the copy goroutines. The server doesn't start its copy
// goroutines until it's received all of the streams. If the client creates the stdin stream and
// immediately begins copying stdin data to the server, it's possible to overwhelm and wedge the
// spdy frame handler in the server so that it is full of unprocessed frames. The frames aren't
// getting processed because the server hasn't started its copying, and it won't do that until it
// gets all the streams. By creating all the streams first, we ensure that the server is ready to
// process data before the client starts sending any. See https://issues.k8s.io/16373 for more info.
if p.Stdin != nil {
headers.Set(api.StreamType, api.StreamTypeStdin)
p.remoteStdin, err = conn.CreateStream(headers)
if err != nil {
return err
}
defer p.remoteStdin.Reset()
}
if p.Stdout != nil {
headers.Set(api.StreamType, api.StreamTypeStdout)
p.remoteStdout, err = conn.CreateStream(headers)
if err != nil {
return err
}
defer p.remoteStdout.Reset()
}
if p.Stderr != nil && !p.Tty {
headers.Set(api.StreamType, api.StreamTypeStderr)
p.remoteStderr, err = conn.CreateStream(headers)
if err != nil {
return err
}
defer p.remoteStderr.Reset()
}
// now that all the streams have been created, proceed with reading & copying
// always read from errorStream
go func() {
message, err := ioutil.ReadAll(p.errorStream)
if err != nil && err != io.EOF {
errorChan <- fmt.Errorf("Error reading from error stream: %s", err)
return
}
if len(message) > 0 {
errorChan <- fmt.Errorf("Error executing remote command: %s", message)
return
}
}()
if p.Stdin != nil {
// TODO this goroutine will never exit cleanly (the io.Copy never unblocks)
// because stdin is not closed until the process exits. If we try to call
// stdin.Close(), it returns no error but doesn't unblock the copy. It will
// exit when the process exits, instead.
go cp(api.StreamTypeStdin, p.remoteStdin, p.Stdin)
}
waitCount := 0
completedStreams := 0
if p.Stdout != nil {
waitCount++
go cp(api.StreamTypeStdout, p.Stdout, p.remoteStdout)
}
if p.Stderr != nil && !p.Tty {
waitCount++
go cp(api.StreamTypeStderr, p.Stderr, p.remoteStderr)
}
Loop:
for {
select {
case <-doneChan:
completedStreams++
if completedStreams == waitCount {
break Loop
}
case err := <-errorChan:
return err
}
}
return nil
}

View file

@ -0,0 +1,195 @@
/*
Copyright 2015 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 remotecommand
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"sync"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/kubernetes/pkg/api"
)
// streamProtocolV2 implements version 2 of the streaming protocol for attach
// and exec. The original streaming protocol was metav1. As a result, this
// version is referred to as version 2, even though it is the first actual
// numbered version.
type streamProtocolV2 struct {
StreamOptions
errorStream io.Reader
remoteStdin io.ReadWriteCloser
remoteStdout io.Reader
remoteStderr io.Reader
}
var _ streamProtocolHandler = &streamProtocolV2{}
func newStreamProtocolV2(options StreamOptions) streamProtocolHandler {
return &streamProtocolV2{
StreamOptions: options,
}
}
func (p *streamProtocolV2) createStreams(conn streamCreator) error {
var err error
headers := http.Header{}
// set up error stream
headers.Set(api.StreamType, api.StreamTypeError)
p.errorStream, err = conn.CreateStream(headers)
if err != nil {
return err
}
// set up stdin stream
if p.Stdin != nil {
headers.Set(api.StreamType, api.StreamTypeStdin)
p.remoteStdin, err = conn.CreateStream(headers)
if err != nil {
return err
}
}
// set up stdout stream
if p.Stdout != nil {
headers.Set(api.StreamType, api.StreamTypeStdout)
p.remoteStdout, err = conn.CreateStream(headers)
if err != nil {
return err
}
}
// set up stderr stream
if p.Stderr != nil && !p.Tty {
headers.Set(api.StreamType, api.StreamTypeStderr)
p.remoteStderr, err = conn.CreateStream(headers)
if err != nil {
return err
}
}
return nil
}
func (p *streamProtocolV2) copyStdin() {
if p.Stdin != nil {
var once sync.Once
// copy from client's stdin to container's stdin
go func() {
defer runtime.HandleCrash()
// if p.stdin is noninteractive, p.g. `echo abc | kubectl exec -i <pod> -- cat`, make sure
// we close remoteStdin as soon as the copy from p.stdin to remoteStdin finishes. Otherwise
// the executed command will remain running.
defer once.Do(func() { p.remoteStdin.Close() })
if _, err := io.Copy(p.remoteStdin, p.Stdin); err != nil {
runtime.HandleError(err)
}
}()
// read from remoteStdin until the stream is closed. this is essential to
// be able to exit interactive sessions cleanly and not leak goroutines or
// hang the client's terminal.
//
// TODO we aren't using go-dockerclient any more; revisit this to determine if it's still
// required by engine-api.
//
// go-dockerclient's current hijack implementation
// (https://github.com/fsouza/go-dockerclient/blob/89f3d56d93788dfe85f864a44f85d9738fca0670/client.go#L564)
// waits for all three streams (stdin/stdout/stderr) to finish copying
// before returning. When hijack finishes copying stdout/stderr, it calls
// Close() on its side of remoteStdin, which allows this copy to complete.
// When that happens, we must Close() on our side of remoteStdin, to
// allow the copy in hijack to complete, and hijack to return.
go func() {
defer runtime.HandleCrash()
defer once.Do(func() { p.remoteStdin.Close() })
// this "copy" doesn't actually read anything - it's just here to wait for
// the server to close remoteStdin.
if _, err := io.Copy(ioutil.Discard, p.remoteStdin); err != nil {
runtime.HandleError(err)
}
}()
}
}
func (p *streamProtocolV2) copyStdout(wg *sync.WaitGroup) {
if p.Stdout == nil {
return
}
wg.Add(1)
go func() {
defer runtime.HandleCrash()
defer wg.Done()
if _, err := io.Copy(p.Stdout, p.remoteStdout); err != nil {
runtime.HandleError(err)
}
}()
}
func (p *streamProtocolV2) copyStderr(wg *sync.WaitGroup) {
if p.Stderr == nil || p.Tty {
return
}
wg.Add(1)
go func() {
defer runtime.HandleCrash()
defer wg.Done()
if _, err := io.Copy(p.Stderr, p.remoteStderr); err != nil {
runtime.HandleError(err)
}
}()
}
func (p *streamProtocolV2) stream(conn streamCreator) error {
if err := p.createStreams(conn); err != nil {
return err
}
// now that all the streams have been created, proceed with reading & copying
errorChan := watchErrorStream(p.errorStream, &errorDecoderV2{})
p.copyStdin()
var wg sync.WaitGroup
p.copyStdout(&wg)
p.copyStderr(&wg)
// we're waiting for stdout/stderr to finish copying
wg.Wait()
// waits for errorStream to finish reading with an error or nil
return <-errorChan
}
// errorDecoderV2 interprets the error channel data as plain text.
type errorDecoderV2 struct{}
func (d *errorDecoderV2) decode(message []byte) error {
return fmt.Errorf("error executing remote command: %s", message)
}

View file

@ -0,0 +1,228 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
import (
"errors"
"io"
"net/http"
"strings"
"testing"
"time"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/util/httpstream"
)
type fakeReader struct {
err error
}
func (r *fakeReader) Read([]byte) (int, error) { return 0, r.err }
type fakeWriter struct{}
func (*fakeWriter) Write([]byte) (int, error) { return 0, nil }
type fakeStreamCreator struct {
created map[string]bool
errors map[string]error
}
var _ streamCreator = &fakeStreamCreator{}
func (f *fakeStreamCreator) CreateStream(headers http.Header) (httpstream.Stream, error) {
streamType := headers.Get(api.StreamType)
f.created[streamType] = true
return nil, f.errors[streamType]
}
func TestV2CreateStreams(t *testing.T) {
tests := []struct {
name string
stdin bool
stdinError error
stdout bool
stdoutError error
stderr bool
stderrError error
errorError error
tty bool
expectError bool
}{
{
name: "stdin error",
stdin: true,
stdinError: errors.New("stdin error"),
expectError: true,
},
{
name: "stdout error",
stdout: true,
stdoutError: errors.New("stdout error"),
expectError: true,
},
{
name: "stderr error",
stderr: true,
stderrError: errors.New("stderr error"),
expectError: true,
},
{
name: "error stream error",
stdin: true,
stdout: true,
stderr: true,
errorError: errors.New("error stream error"),
expectError: true,
},
{
name: "no errors",
stdin: true,
stdout: true,
stderr: true,
expectError: false,
},
{
name: "no errors, stderr & tty set, don't expect stderr",
stdin: true,
stdout: true,
stderr: true,
tty: true,
expectError: false,
},
}
for _, test := range tests {
conn := &fakeStreamCreator{
created: make(map[string]bool),
errors: map[string]error{
api.StreamTypeStdin: test.stdinError,
api.StreamTypeStdout: test.stdoutError,
api.StreamTypeStderr: test.stderrError,
api.StreamTypeError: test.errorError,
},
}
opts := StreamOptions{Tty: test.tty}
if test.stdin {
opts.Stdin = &fakeReader{}
}
if test.stdout {
opts.Stdout = &fakeWriter{}
}
if test.stderr {
opts.Stderr = &fakeWriter{}
}
h := newStreamProtocolV2(opts).(*streamProtocolV2)
err := h.createStreams(conn)
if test.expectError {
if err == nil {
t.Errorf("%s: expected error", test.name)
continue
}
if e, a := test.stdinError, err; test.stdinError != nil && e != a {
t.Errorf("%s: expected %v, got %v", test.name, e, a)
}
if e, a := test.stdoutError, err; test.stdoutError != nil && e != a {
t.Errorf("%s: expected %v, got %v", test.name, e, a)
}
if e, a := test.stderrError, err; test.stderrError != nil && e != a {
t.Errorf("%s: expected %v, got %v", test.name, e, a)
}
if e, a := test.errorError, err; test.errorError != nil && e != a {
t.Errorf("%s: expected %v, got %v", test.name, e, a)
}
continue
}
if !test.expectError && err != nil {
t.Errorf("%s: unexpected error: %v", test.name, err)
continue
}
if test.stdin && !conn.created[api.StreamTypeStdin] {
t.Errorf("%s: expected stdin stream", test.name)
}
if test.stdout && !conn.created[api.StreamTypeStdout] {
t.Errorf("%s: expected stdout stream", test.name)
}
if test.stderr {
if test.tty && conn.created[api.StreamTypeStderr] {
t.Errorf("%s: unexpected stderr stream because tty is set", test.name)
} else if !test.tty && !conn.created[api.StreamTypeStderr] {
t.Errorf("%s: expected stderr stream", test.name)
}
}
if !conn.created[api.StreamTypeError] {
t.Errorf("%s: expected error stream", test.name)
}
}
}
func TestV2ErrorStreamReading(t *testing.T) {
tests := []struct {
name string
stream io.Reader
expectedError error
}{
{
name: "error reading from stream",
stream: &fakeReader{errors.New("foo")},
expectedError: errors.New("error reading from error stream: foo"),
},
{
name: "stream returns an error",
stream: strings.NewReader("some error"),
expectedError: errors.New("error executing remote command: some error"),
},
}
for _, test := range tests {
h := newStreamProtocolV2(StreamOptions{}).(*streamProtocolV2)
h.errorStream = test.stream
ch := watchErrorStream(h.errorStream, &errorDecoderV2{})
if ch == nil {
t.Fatalf("%s: unexpected nil channel", test.name)
}
var err error
select {
case err = <-ch:
case <-time.After(wait.ForeverTestTimeout):
t.Fatalf("%s: timed out", test.name)
}
if test.expectedError != nil {
if err == nil {
t.Errorf("%s: expected an error", test.name)
} else if e, a := test.expectedError, err; e.Error() != a.Error() {
t.Errorf("%s: expected %q, got %q", test.name, e, a)
}
continue
}
if test.expectedError == nil && err != nil {
t.Errorf("%s: unexpected error: %v", test.name, err)
continue
}
}
}

View file

@ -0,0 +1,112 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
import (
"encoding/json"
"io"
"net/http"
"sync"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/kubernetes/pkg/api"
)
// streamProtocolV3 implements version 3 of the streaming protocol for attach
// and exec. This version adds support for resizing the container's terminal.
type streamProtocolV3 struct {
*streamProtocolV2
resizeStream io.Writer
}
var _ streamProtocolHandler = &streamProtocolV3{}
func newStreamProtocolV3(options StreamOptions) streamProtocolHandler {
return &streamProtocolV3{
streamProtocolV2: newStreamProtocolV2(options).(*streamProtocolV2),
}
}
func (p *streamProtocolV3) createStreams(conn streamCreator) error {
// set up the streams from v2
if err := p.streamProtocolV2.createStreams(conn); err != nil {
return err
}
// set up resize stream
if p.Tty {
headers := http.Header{}
headers.Set(api.StreamType, api.StreamTypeResize)
var err error
p.resizeStream, err = conn.CreateStream(headers)
if err != nil {
return err
}
}
return nil
}
func (p *streamProtocolV3) handleResizes() {
if p.resizeStream == nil || p.TerminalSizeQueue == nil {
return
}
go func() {
defer runtime.HandleCrash()
encoder := json.NewEncoder(p.resizeStream)
for {
size := p.TerminalSizeQueue.Next()
if size == nil {
return
}
if err := encoder.Encode(&size); err != nil {
runtime.HandleError(err)
}
}
}()
}
func (p *streamProtocolV3) stream(conn streamCreator) error {
if err := p.createStreams(conn); err != nil {
return err
}
// now that all the streams have been created, proceed with reading & copying
errorChan := watchErrorStream(p.errorStream, &errorDecoderV3{})
p.handleResizes()
p.copyStdin()
var wg sync.WaitGroup
p.copyStdout(&wg)
p.copyStderr(&wg)
// we're waiting for stdout/stderr to finish copying
wg.Wait()
// waits for errorStream to finish reading with an error or nil
return <-errorChan
}
type errorDecoderV3 struct {
errorDecoderV2
}

View file

@ -0,0 +1,119 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"sync"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
"k8s.io/kubernetes/pkg/util/exec"
)
// streamProtocolV4 implements version 4 of the streaming protocol for attach
// and exec. This version adds support for exit codes on the error stream through
// the use of metav1.Status instead of plain text messages.
type streamProtocolV4 struct {
*streamProtocolV3
}
var _ streamProtocolHandler = &streamProtocolV4{}
func newStreamProtocolV4(options StreamOptions) streamProtocolHandler {
return &streamProtocolV4{
streamProtocolV3: newStreamProtocolV3(options).(*streamProtocolV3),
}
}
func (p *streamProtocolV4) createStreams(conn streamCreator) error {
return p.streamProtocolV3.createStreams(conn)
}
func (p *streamProtocolV4) handleResizes() {
p.streamProtocolV3.handleResizes()
}
func (p *streamProtocolV4) stream(conn streamCreator) error {
if err := p.createStreams(conn); err != nil {
return err
}
// now that all the streams have been created, proceed with reading & copying
errorChan := watchErrorStream(p.errorStream, &errorDecoderV4{})
p.handleResizes()
p.copyStdin()
var wg sync.WaitGroup
p.copyStdout(&wg)
p.copyStderr(&wg)
// we're waiting for stdout/stderr to finish copying
wg.Wait()
// waits for errorStream to finish reading with an error or nil
return <-errorChan
}
// errorDecoderV4 interprets the json-marshaled metav1.Status on the error channel
// and creates an exec.ExitError from it.
type errorDecoderV4 struct{}
func (d *errorDecoderV4) decode(message []byte) error {
status := metav1.Status{}
err := json.Unmarshal(message, &status)
if err != nil {
return fmt.Errorf("error stream protocol error: %v in %q", err, string(message))
}
switch status.Status {
case metav1.StatusSuccess:
return nil
case metav1.StatusFailure:
if status.Reason == remotecommand.NonZeroExitCodeReason {
if status.Details == nil {
return errors.New("error stream protocol error: details must be set")
}
for i := range status.Details.Causes {
c := &status.Details.Causes[i]
if c.Type != remotecommand.ExitCodeCauseType {
continue
}
rc, err := strconv.ParseUint(c.Message, 10, 8)
if err != nil {
return fmt.Errorf("error stream protocol error: invalid exit code value %q", c.Message)
}
return exec.CodeExitError{
Err: fmt.Errorf("command terminated with exit code %d", rc),
Code: int(rc),
}
}
return fmt.Errorf("error stream protocol error: no %s cause given", remotecommand.ExitCodeCauseType)
}
default:
return errors.New("error stream protocol error: unknown error")
}
return fmt.Errorf(status.Message)
}

View file

@ -0,0 +1,71 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package remotecommand
import (
"fmt"
"testing"
)
func TestV4ErrorDecoder(t *testing.T) {
dec := errorDecoderV4{}
type Test struct {
message string
err string
}
for _, test := range []Test{
{
message: "{}",
err: "error stream protocol error: unknown error",
},
{
message: "{",
err: "error stream protocol error: unexpected end of JSON input in \"{\"",
},
{
message: `{"status": "Success" }`,
err: "",
},
{
message: `{"status": "Failure", "message": "foobar" }`,
err: "foobar",
},
{
message: `{"status": "Failure", "message": "foobar", "reason": "NonZeroExitCode", "details": {"causes": [{"reason": "foo"}] } }`,
err: "error stream protocol error: no ExitCode cause given",
},
{
message: `{"status": "Failure", "message": "foobar", "reason": "NonZeroExitCode", "details": {"causes": [{"reason": "ExitCode"}] } }`,
err: "error stream protocol error: invalid exit code value \"\"",
},
{
message: `{"status": "Failure", "message": "foobar", "reason": "NonZeroExitCode", "details": {"causes": [{"reason": "ExitCode", "message": "42"}] } }`,
err: "command terminated with exit code 42",
},
} {
err := dec.decode([]byte(test.message))
want := test.err
if want == "" {
want = "<nil>"
}
if got := fmt.Sprintf("%v", err); got != want {
t.Errorf("wrong error for message %q: want=%q, got=%q", test.message, want, got)
}
}
}

View file

@ -0,0 +1,38 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["simple_testclient.go"],
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/api/testapi:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/fields:go_default_library",
"//pkg/util/testing:go_default_library",
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
"//vendor:k8s.io/apimachinery/pkg/labels",
"//vendor:k8s.io/apimachinery/pkg/runtime",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,222 @@
/*
Copyright 2015 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 simple
import (
"net/http/httptest"
"net/url"
"path"
"reflect"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/testapi"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/fields"
utiltesting "k8s.io/kubernetes/pkg/util/testing"
)
const NameRequiredError = "resource name may not be empty"
type Request struct {
Method string
Path string
Header string
Query url.Values
Body runtime.Object
RawBody *string
}
type Response struct {
StatusCode int
Body runtime.Object
RawBody *string
}
type Client struct {
Clientset *clientset.Clientset
Request Request
Response Response
Error bool
Created bool
server *httptest.Server
handler *utiltesting.FakeHandler
// For query args, an optional function to validate the contents
// useful when the contents can change but still be correct.
// Maps from query arg key to validator.
// If no validator is present, string equality is used.
QueryValidator map[string]func(string, string) bool
// If your object could exist in multiple groups, set this to
// correspond to the URL you're testing it with.
ResourceGroup string
}
func (c *Client) Setup(t *testing.T) *Client {
c.handler = &utiltesting.FakeHandler{
StatusCode: c.Response.StatusCode,
}
if responseBody := c.body(t, c.Response.Body, c.Response.RawBody); responseBody != nil {
c.handler.ResponseBody = *responseBody
}
c.server = httptest.NewServer(c.handler)
if c.Clientset == nil {
c.Clientset = clientset.NewForConfigOrDie(&restclient.Config{Host: c.server.URL})
}
c.QueryValidator = map[string]func(string, string) bool{}
return c
}
func (c *Client) Close() {
if c.server != nil {
c.server.Close()
}
}
func (c *Client) ServerURL() string {
return c.server.URL
}
func (c *Client) Validate(t *testing.T, received runtime.Object, err error) {
c.ValidateCommon(t, err)
if c.Response.Body != nil && !api.Semantic.DeepDerivative(c.Response.Body, received) {
t.Errorf("bad response for request %#v: \nexpected %#v\ngot %#v\n", c.Request, c.Response.Body, received)
}
}
func (c *Client) ValidateRaw(t *testing.T, received []byte, err error) {
c.ValidateCommon(t, err)
if c.Response.Body != nil && !reflect.DeepEqual(c.Response.Body, received) {
t.Errorf("bad response for request %#v: expected %#v, got %#v", c.Request, c.Response.Body, received)
}
}
func (c *Client) ValidateCommon(t *testing.T, err error) {
if c.Error {
if err == nil {
t.Errorf("error expected for %#v, got none", c.Request)
}
return
}
if err != nil {
t.Errorf("no error expected for %#v, got: %v", c.Request, err)
}
if c.handler.RequestReceived == nil {
t.Errorf("handler had an empty request, %#v", c)
return
}
requestBody := c.body(t, c.Request.Body, c.Request.RawBody)
actualQuery := c.handler.RequestReceived.URL.Query()
t.Logf("got query: %v", actualQuery)
t.Logf("path: %v", c.Request.Path)
// We check the query manually, so blank it out so that FakeHandler.ValidateRequest
// won't check it.
c.handler.RequestReceived.URL.RawQuery = ""
c.handler.ValidateRequest(t, path.Join(c.Request.Path), c.Request.Method, requestBody)
for key, values := range c.Request.Query {
validator, ok := c.QueryValidator[key]
if !ok {
switch key {
case metav1.LabelSelectorQueryParam(api.Registry.GroupOrDie(api.GroupName).GroupVersion.String()):
validator = ValidateLabels
case metav1.FieldSelectorQueryParam(api.Registry.GroupOrDie(api.GroupName).GroupVersion.String()):
validator = validateFields
default:
validator = func(a, b string) bool { return a == b }
}
}
observed := actualQuery.Get(key)
wanted := strings.Join(values, "")
if !validator(wanted, observed) {
t.Errorf("Unexpected query arg for key: %s. Expected %s, Received %s", key, wanted, observed)
}
}
if c.Request.Header != "" {
if c.handler.RequestReceived.Header.Get(c.Request.Header) == "" {
t.Errorf("header %q not found in request %#v", c.Request.Header, c.handler.RequestReceived)
}
}
if expected, received := requestBody, c.handler.RequestBody; expected != nil && *expected != received {
t.Errorf("bad body for request %#v: expected %s, got %s", c.Request, *expected, received)
}
}
// buildQueryValues is a convenience function for knowing if a namespace should be in a query param or not
func BuildQueryValues(query url.Values) url.Values {
v := url.Values{}
if query != nil {
for key, values := range query {
for _, value := range values {
v.Add(key, value)
}
}
}
return v
}
func ValidateLabels(a, b string) bool {
sA, eA := labels.Parse(a)
if eA != nil {
return false
}
sB, eB := labels.Parse(b)
if eB != nil {
return false
}
return sA.String() == sB.String()
}
func validateFields(a, b string) bool {
sA, _ := fields.ParseSelector(a)
sB, _ := fields.ParseSelector(b)
return sA.String() == sB.String()
}
func (c *Client) body(t *testing.T, obj runtime.Object, raw *string) *string {
if obj != nil {
fqKinds, _, err := api.Scheme.ObjectKinds(obj)
if err != nil {
t.Errorf("unexpected encoding error: %v", err)
}
groupName := fqKinds[0].GroupVersion().Group
if c.ResourceGroup != "" {
groupName = c.ResourceGroup
}
var bs []byte
g, found := testapi.Groups[groupName]
if !found {
t.Errorf("Group %s is not registered in testapi", groupName)
}
bs, err = runtime.Encode(g.Codec(), obj)
if err != nil {
t.Errorf("unexpected encoding error: %v", err)
}
body := string(bs)
return &body
}
return raw
}