dep ensure -update k8s.io/client-go (#197)

This commit is contained in:
Kit Ewbank
2018-05-18 19:24:19 -04:00
committed by Robbie Zhang
parent 8068f3cac8
commit b4cb809968
774 changed files with 7107 additions and 15817 deletions

View File

@@ -1,30 +0,0 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["provider.go"],
importpath = "k8s.io/client-go/plugin/pkg/auth/authenticator/token/oidc/testing",
deps = [
"//vendor/github.com/coreos/go-oidc/jose:go_default_library",
"//vendor/github.com/coreos/go-oidc/key:go_default_library",
"//vendor/github.com/coreos/go-oidc/oidc: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

@@ -1,200 +0,0 @@
/*
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 testing
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"path/filepath"
"testing"
"time"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
"github.com/coreos/go-oidc/oidc"
)
// NewOIDCProvider provides a bare minimum OIDC IdP Server useful for testing.
func NewOIDCProvider(t *testing.T, issuerPath string) *OIDCProvider {
privKey, err := key.GeneratePrivateKey()
if err != nil {
t.Fatalf("Cannot create OIDC Provider: %v", err)
return nil
}
op := &OIDCProvider{
Mux: http.NewServeMux(),
PrivKey: privKey,
issuerPath: issuerPath,
}
op.Mux.HandleFunc(path.Join(issuerPath, "/.well-known/openid-configuration"), op.handleConfig)
op.Mux.HandleFunc(path.Join(issuerPath, "/keys"), op.handleKeys)
return op
}
type OIDCProvider struct {
Mux *http.ServeMux
PCFG oidc.ProviderConfig
PrivKey *key.PrivateKey
issuerPath string
}
func (op *OIDCProvider) ServeTLSWithKeyPair(cert, key string) (*httptest.Server, error) {
srv := httptest.NewUnstartedServer(op.Mux)
srv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
var err error
srv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("Cannot load cert/key pair: %v", err)
}
srv.StartTLS()
// The issuer's URL is extended by an optional path. This ensures that the plugin can
// handle issuers that use a non-root path for discovery (see kubernetes/kubernetes#29749).
srv.URL = srv.URL + op.issuerPath
u, err := url.Parse(srv.URL)
if err != nil {
return nil, err
}
pathFor := func(p string) *url.URL {
u2 := *u // Shallow copy.
u2.Path = path.Join(u2.Path, p)
return &u2
}
op.PCFG = oidc.ProviderConfig{
Issuer: u,
AuthEndpoint: pathFor("/auth"),
TokenEndpoint: pathFor("/token"),
KeysEndpoint: pathFor("/keys"),
ResponseTypesSupported: []string{"code"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValues: []string{"RS256"},
}
return srv, nil
}
func (op *OIDCProvider) handleConfig(w http.ResponseWriter, req *http.Request) {
b, err := json.Marshal(&op.PCFG)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}
func (op *OIDCProvider) handleKeys(w http.ResponseWriter, req *http.Request) {
keys := struct {
Keys []jose.JWK `json:"keys"`
}{
Keys: []jose.JWK{op.PrivKey.JWK()},
}
b, err := json.Marshal(keys)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(time.Hour.Seconds())))
w.Header().Set("Expires", time.Now().Add(time.Hour).Format(time.RFC1123))
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}
// generateSelfSignedCert generates a self-signed cert/key pairs and writes to the certPath/keyPath.
// This method is mostly identical to crypto.GenerateSelfSignedCert except for the 'IsCA' and 'KeyUsage'
// in the certificate template. (Maybe we can merge these two methods).
func GenerateSelfSignedCert(t *testing.T, host, certPath, keyPath string) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
if ip := net.ParseIP(host); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, host)
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
t.Fatal(err)
}
// Generate cert
certBuffer := bytes.Buffer{}
if err := pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
t.Fatal(err)
}
// Generate key
keyBuffer := bytes.Buffer{}
if err := pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
t.Fatal(err)
}
// Write cert
if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(certPath, certBuffer.Bytes(), os.FileMode(0644)); err != nil {
t.Fatal(err)
}
// Write key
if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(keyPath, keyBuffer.Bytes(), os.FileMode(0600)); err != nil {
t.Fatal(err)
}
}

View File

@@ -29,6 +29,7 @@ filegroup(
srcs = [
":package-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/azure:all-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec:all-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/gcp:all-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/oidc:all-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/openstack:all-srcs",

View File

@@ -9,8 +9,7 @@ load(
go_test(
name = "go_default_test",
srcs = ["azure_test.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/azure",
library = ":go_default_library",
embed = [":go_default_library"],
deps = ["//vendor/github.com/Azure/go-autorest/autorest/adal:go_default_library"],
)

View File

@@ -0,0 +1,44 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["exec.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/exec",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/golang.org/x/crypto/ssh/terminal:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library",
"//vendor/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["exec_test.go"],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
deps = [
"//vendor/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,280 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package exec
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"sync"
"time"
"github.com/golang/glog"
"golang.org/x/crypto/ssh/terminal"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/pkg/apis/clientauthentication"
"k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
"k8s.io/client-go/tools/clientcmd/api"
)
const execInfoEnv = "KUBERNETES_EXEC_INFO"
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
func init() {
v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
v1alpha1.AddToScheme(scheme)
clientauthentication.AddToScheme(scheme)
}
var (
// Since transports can be constantly re-initialized by programs like kubectl,
// keep a cache of initialized authenticators keyed by a hash of their config.
globalCache = newCache()
// The list of API versions we accept.
apiVersions = map[string]schema.GroupVersion{
v1alpha1.SchemeGroupVersion.String(): v1alpha1.SchemeGroupVersion,
}
)
func newCache() *cache {
return &cache{m: make(map[string]*Authenticator)}
}
func cacheKey(c *api.ExecConfig) string {
return fmt.Sprintf("%#v", c)
}
type cache struct {
mu sync.Mutex
m map[string]*Authenticator
}
func (c *cache) get(s string) (*Authenticator, bool) {
c.mu.Lock()
defer c.mu.Unlock()
a, ok := c.m[s]
return a, ok
}
// put inserts an authenticator into the cache. If an authenticator is already
// associated with the key, the first one is returned instead.
func (c *cache) put(s string, a *Authenticator) *Authenticator {
c.mu.Lock()
defer c.mu.Unlock()
existing, ok := c.m[s]
if ok {
return existing
}
c.m[s] = a
return a
}
// GetAuthenticator returns an exec-based plugin for providing client credentials.
func GetAuthenticator(config *api.ExecConfig) (*Authenticator, error) {
return newAuthenticator(globalCache, config)
}
func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error) {
key := cacheKey(config)
if a, ok := c.get(key); ok {
return a, nil
}
gv, ok := apiVersions[config.APIVersion]
if !ok {
return nil, fmt.Errorf("exec plugin: invalid apiVersion %q", config.APIVersion)
}
a := &Authenticator{
cmd: config.Command,
args: config.Args,
group: gv,
stdin: os.Stdin,
stderr: os.Stderr,
interactive: terminal.IsTerminal(int(os.Stdout.Fd())),
now: time.Now,
environ: os.Environ,
}
for _, env := range config.Env {
a.env = append(a.env, env.Name+"="+env.Value)
}
return c.put(key, a), nil
}
// Authenticator is a client credential provider that rotates credentials by executing a plugin.
// The plugin input and output are defined by the API group client.authentication.k8s.io.
type Authenticator struct {
// Set by the config
cmd string
args []string
group schema.GroupVersion
env []string
// Stubbable for testing
stdin io.Reader
stderr io.Writer
interactive bool
now func() time.Time
environ func() []string
// Cached results.
//
// The mutex also guards calling the plugin. Since the plugin could be
// interactive we want to make sure it's only called once.
mu sync.Mutex
cachedToken string
exp time.Time
}
// WrapTransport instruments an existing http.RoundTripper with credentials returned
// by the plugin.
func (a *Authenticator) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return &roundTripper{a, rt}
}
type roundTripper struct {
a *Authenticator
base http.RoundTripper
}
func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// If a user has already set credentials, use that. This makes commands like
// "kubectl get --token (token) pods" work.
if req.Header.Get("Authorization") != "" {
return r.base.RoundTrip(req)
}
token, err := r.a.token()
if err != nil {
return nil, fmt.Errorf("getting token: %v", err)
}
req.Header.Set("Authorization", "Bearer "+token)
res, err := r.base.RoundTrip(req)
if err != nil {
return nil, err
}
if res.StatusCode == http.StatusUnauthorized {
resp := &clientauthentication.Response{
Header: res.Header,
Code: int32(res.StatusCode),
}
if err := r.a.refresh(token, resp); err != nil {
glog.Errorf("refreshing token: %v", err)
}
}
return res, nil
}
func (a *Authenticator) tokenExpired() bool {
if a.exp.IsZero() {
return false
}
return a.now().After(a.exp)
}
func (a *Authenticator) token() (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.cachedToken != "" && !a.tokenExpired() {
return a.cachedToken, nil
}
return a.getToken(nil)
}
// refresh executes the plugin to force a rotation of the token.
func (a *Authenticator) refresh(token string, r *clientauthentication.Response) error {
a.mu.Lock()
defer a.mu.Unlock()
if token != a.cachedToken {
// Token already rotated.
return nil
}
_, err := a.getToken(r)
return err
}
// getToken executes the plugin and reads the credentials from stdout. It must be
// called while holding the Authenticator's mutex.
func (a *Authenticator) getToken(r *clientauthentication.Response) (string, error) {
cred := &clientauthentication.ExecCredential{
Spec: clientauthentication.ExecCredentialSpec{
Response: r,
Interactive: a.interactive,
},
}
data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
if err != nil {
return "", fmt.Errorf("encode ExecCredentials: %v", err)
}
env := append(a.environ(), a.env...)
env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data))
stdout := &bytes.Buffer{}
cmd := exec.Command(a.cmd, a.args...)
cmd.Env = env
cmd.Stderr = a.stderr
cmd.Stdout = stdout
if a.interactive {
cmd.Stdin = a.stdin
}
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("exec: %v", err)
}
_, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred)
if err != nil {
return "", fmt.Errorf("decode stdout: %v", err)
}
if gvk.Group != a.group.Group || gvk.Version != a.group.Version {
return "", fmt.Errorf("exec plugin is configured to use API version %s, plugin returned version %s",
a.group, schema.GroupVersion{Group: gvk.Group, Version: gvk.Version})
}
if cred.Status == nil {
return "", fmt.Errorf("exec plugin didn't return a status field")
}
if cred.Status.Token == "" {
return "", fmt.Errorf("exec plugin didn't return a token")
}
if cred.Status.ExpirationTimestamp != nil {
a.exp = cred.Status.ExpirationTimestamp.Time
} else {
a.exp = time.Time{}
}
a.cachedToken = cred.Status.Token
return a.cachedToken, nil
}

View File

@@ -0,0 +1,413 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package exec
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"k8s.io/client-go/pkg/apis/clientauthentication"
"k8s.io/client-go/tools/clientcmd/api"
)
func TestCacheKey(t *testing.T) {
c1 := &api.ExecConfig{
Command: "foo-bar",
Args: []string{"1", "2"},
Env: []api.ExecEnvVar{
{Name: "3", Value: "4"},
{Name: "5", Value: "6"},
{Name: "7", Value: "8"},
},
APIVersion: "client.authentication.k8s.io/v1alpha1",
}
c2 := &api.ExecConfig{
Command: "foo-bar",
Args: []string{"1", "2"},
Env: []api.ExecEnvVar{
{Name: "3", Value: "4"},
{Name: "5", Value: "6"},
{Name: "7", Value: "8"},
},
APIVersion: "client.authentication.k8s.io/v1alpha1",
}
c3 := &api.ExecConfig{
Command: "foo-bar",
Args: []string{"1", "2"},
Env: []api.ExecEnvVar{
{Name: "3", Value: "4"},
{Name: "5", Value: "6"},
},
APIVersion: "client.authentication.k8s.io/v1alpha1",
}
key1 := cacheKey(c1)
key2 := cacheKey(c2)
key3 := cacheKey(c3)
if key1 != key2 {
t.Error("key1 and key2 didn't match")
}
if key1 == key3 {
t.Error("key1 and key3 matched")
}
if key2 == key3 {
t.Error("key2 and key3 matched")
}
}
func compJSON(t *testing.T, got, want []byte) {
t.Helper()
gotJSON := &bytes.Buffer{}
wantJSON := &bytes.Buffer{}
if err := json.Indent(gotJSON, got, "", " "); err != nil {
t.Errorf("got invalid JSON: %v", err)
}
if err := json.Indent(wantJSON, want, "", " "); err != nil {
t.Errorf("want invalid JSON: %v", err)
}
g := strings.TrimSpace(gotJSON.String())
w := strings.TrimSpace(wantJSON.String())
if g != w {
t.Errorf("wanted %q, got %q", w, g)
}
}
func TestGetToken(t *testing.T) {
tests := []struct {
name string
config api.ExecConfig
output string
interactive bool
response *clientauthentication.Response
wantInput string
wantToken string
wantExpiry time.Time
wantErr bool
}{
{
name: "basic-request",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"token": "foo-bar"
}
}`,
wantToken: "foo-bar",
},
{
name: "interactive",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
interactive: true,
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {
"interactive": true
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"token": "foo-bar"
}
}`,
wantToken: "foo-bar",
},
{
name: "response",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
response: &clientauthentication.Response{
Header: map[string][]string{
"WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`},
},
Code: 401,
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {
"response": {
"header": {
"WWW-Authenticate": [
"Basic realm=\"Access to the staging site\", charset=\"UTF-8\""
]
},
"code": 401
}
}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"token": "foo-bar"
}
}`,
wantToken: "foo-bar",
},
{
name: "expiry",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"token": "foo-bar",
"expirationTimestamp": "2006-01-02T15:04:05Z"
}
}`,
wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC),
wantToken: "foo-bar",
},
{
name: "no-group-version",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {}
}`,
output: `{
"kind": "ExecCredential",
"status": {
"token": "foo-bar"
}
}`,
wantErr: true,
},
{
name: "no-status",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1"
}`,
wantErr: true,
},
{
name: "no-token",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
},
wantInput: `{
"kind":"ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"spec": {}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion":"client.authentication.k8s.io/v1alpha1",
"status": {}
}`,
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := test.config
c.Command = "./testdata/test-plugin.sh"
c.Env = append(c.Env, api.ExecEnvVar{
Name: "TEST_OUTPUT",
Value: test.output,
})
a, err := newAuthenticator(newCache(), &c)
if err != nil {
t.Fatal(err)
}
stderr := &bytes.Buffer{}
a.stderr = stderr
a.interactive = test.interactive
a.environ = func() []string { return nil }
token, err := a.getToken(test.response)
if err != nil {
if !test.wantErr {
t.Errorf("get token %v", err)
}
return
}
if test.wantErr {
t.Fatal("expected error getting token")
}
if token != test.wantToken {
t.Errorf("expected token %q got %q", test.wantToken, token)
}
if !a.exp.Equal(test.wantExpiry) {
t.Errorf("expected expiry %v got %v", test.wantExpiry, a.exp)
}
compJSON(t, stderr.Bytes(), []byte(test.wantInput))
})
}
}
func TestRoundTripper(t *testing.T) {
wantToken := ""
n := time.Now()
now := func() time.Time { return n }
env := []string{""}
environ := func() []string {
s := make([]string, len(env))
copy(s, env)
return s
}
setOutput := func(s string) {
env[0] = "TEST_OUTPUT=" + s
}
handler := func(w http.ResponseWriter, r *http.Request) {
gotToken := ""
parts := strings.Split(r.Header.Get("Authorization"), " ")
if len(parts) > 1 && strings.EqualFold(parts[0], "bearer") {
gotToken = parts[1]
}
if wantToken != gotToken {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
fmt.Fprintln(w, "ok")
}
server := httptest.NewServer(http.HandlerFunc(handler))
c := api.ExecConfig{
Command: "./testdata/test-plugin.sh",
APIVersion: "client.authentication.k8s.io/v1alpha1",
}
a, err := newAuthenticator(newCache(), &c)
if err != nil {
t.Fatal(err)
}
a.environ = environ
a.now = now
a.stderr = ioutil.Discard
client := http.Client{
Transport: a.WrapTransport(http.DefaultTransport),
}
get := func(t *testing.T, statusCode int) {
t.Helper()
resp, err := client.Get(server.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != statusCode {
t.Errorf("wanted status %d got %d", statusCode, resp.StatusCode)
}
}
setOutput(`{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"token": "token1"
}
}`)
wantToken = "token1"
get(t, http.StatusOK)
setOutput(`{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"token": "token2"
}
}`)
// Previous token should be cached
get(t, http.StatusOK)
wantToken = "token2"
// Token is still cached, hits unauthorized but causes token to rotate.
get(t, http.StatusUnauthorized)
// Follow up request uses the rotated token.
get(t, http.StatusOK)
setOutput(`{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"token": "token3",
"expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `"
}
}`)
wantToken = "token3"
// Token is still cached, hit's unauthorized but causes rotation to token with an expiry.
get(t, http.StatusUnauthorized)
get(t, http.StatusOK)
// Move time forward 2 hours, "token3" is now expired.
n = n.Add(time.Hour * 2)
setOutput(`{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"status": {
"token": "token4",
"expirationTimestamp": "` + now().Add(time.Hour).Format(time.RFC3339Nano) + `"
}
}`)
wantToken = "token4"
// Old token is expired, should refresh automatically without hitting a 401.
get(t, http.StatusOK)
}

View File

@@ -0,0 +1,18 @@
#!/bin/sh -e
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
>&2 echo "$KUBERNETES_EXEC_INFO"
echo "$TEST_OUTPUT"

View File

@@ -9,8 +9,7 @@ load(
go_test(
name = "go_default_test",
srcs = ["gcp_test.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/gcp",
library = ":go_default_library",
embed = [":go_default_library"],
deps = ["//vendor/golang.org/x/oauth2:go_default_library"],
)

View File

@@ -42,8 +42,18 @@ func init() {
}
}
// Stubbable for testing
var execCommand = exec.Command
var (
// Stubbable for testing
execCommand = exec.Command
// defaultScopes:
// - cloud-platform is the base scope to authenticate to GCP.
// - userinfo.email is used to authenticate to GKE APIs with gserviceaccount
// email instead of numeric uniqueID.
defaultScopes = []string{
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email"}
)
// gcpAuthProvider is an auth provider plugin that uses GCP credentials to provide
// tokens for kubectl to authenticate itself to the apiserver. A sample json config
@@ -55,6 +65,14 @@ var execCommand = exec.Command
// "name": "gcp",
//
// 'config': {
// # Authentication options
// # These options are used while getting a token.
//
// # comma-separated list of GCP API scopes. default value of this field
// # is "https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email".
// # to override the API scopes, specify this field explicitly.
// "scopes": "https://www.googleapis.com/auth/cloud-platform"
//
// # Caching options
//
// # Raw string data representing cached access token.
@@ -96,12 +114,32 @@ type gcpAuthProvider struct {
}
func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
var ts oauth2.TokenSource
var err error
if cmd, useCmd := gcpConfig["cmd-path"]; useCmd {
ts, err := tokenSource(isCmdTokenSource(gcpConfig), gcpConfig)
if err != nil {
return nil, err
}
cts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister, ts, gcpConfig)
if err != nil {
return nil, err
}
return &gcpAuthProvider{cts, persister}, nil
}
func isCmdTokenSource(gcpConfig map[string]string) bool {
_, ok := gcpConfig["cmd-path"]
return ok
}
func tokenSource(isCmd bool, gcpConfig map[string]string) (oauth2.TokenSource, error) {
// Command-based token source
if isCmd {
cmd := gcpConfig["cmd-path"]
if len(cmd) == 0 {
return nil, fmt.Errorf("missing access token cmd")
}
if gcpConfig["scopes"] != "" {
return nil, fmt.Errorf("scopes can only be used when kubectl is using a gcp service account key")
}
var args []string
if cmdArgs, ok := gcpConfig["cmd-args"]; ok {
args = strings.Fields(cmdArgs)
@@ -110,18 +148,29 @@ func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restcli
cmd = fields[0]
args = fields[1:]
}
ts = newCmdTokenSource(cmd, args, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"])
} else {
ts, err = google.DefaultTokenSource(context.Background(), "https://www.googleapis.com/auth/cloud-platform")
return newCmdTokenSource(cmd, args, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"]), nil
}
// Google Application Credentials-based token source
scopes := parseScopes(gcpConfig)
ts, err := google.DefaultTokenSource(context.Background(), scopes...)
if err != nil {
return nil, err
return nil, fmt.Errorf("cannot construct google default token source: %v", err)
}
cts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister, ts, gcpConfig)
if err != nil {
return nil, err
return ts, nil
}
// parseScopes constructs a list of scopes that should be included in token source
// from the config map.
func parseScopes(gcpConfig map[string]string) []string {
scopes, ok := gcpConfig["scopes"]
if !ok {
return defaultScopes
}
return &gcpAuthProvider{cts, persister}, nil
if scopes == "" {
return []string{}
}
return strings.Split(gcpConfig["scopes"], ",")
}
func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {

View File

@@ -18,6 +18,7 @@ package gcp
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
@@ -116,6 +117,114 @@ func TestHelperProcess(t *testing.T) {
os.Exit(0)
}
func Test_isCmdTokenSource(t *testing.T) {
c1 := map[string]string{"cmd-path": "foo"}
if v := isCmdTokenSource(c1); !v {
t.Fatalf("cmd-path present in config (%+v), but got %v", c1, v)
}
c2 := map[string]string{"cmd-args": "foo bar"}
if v := isCmdTokenSource(c2); v {
t.Fatalf("cmd-path not present in config (%+v), but got %v", c2, v)
}
}
func Test_tokenSource_cmd(t *testing.T) {
if _, err := tokenSource(true, map[string]string{}); err == nil {
t.Fatalf("expected error, cmd-args not present in config")
}
c := map[string]string{
"cmd-path": "foo",
"cmd-args": "bar"}
ts, err := tokenSource(true, c)
if err != nil {
t.Fatalf("failed to return cmd token source: %+v", err)
}
if ts == nil {
t.Fatal("returned nil token source")
}
if _, ok := ts.(*commandTokenSource); !ok {
t.Fatalf("returned token source type:(%T) expected:(*commandTokenSource)", ts)
}
}
func Test_tokenSource_cmdCannotBeUsedWithScopes(t *testing.T) {
c := map[string]string{
"cmd-path": "foo",
"scopes": "A,B"}
if _, err := tokenSource(true, c); err == nil {
t.Fatal("expected error when scopes is used with cmd-path")
}
}
func Test_tokenSource_applicationDefaultCredentials_fails(t *testing.T) {
// try to use empty ADC file
fakeTokenFile, err := ioutil.TempFile("", "adctoken")
if err != nil {
t.Fatalf("failed to create fake token file: +%v", err)
}
fakeTokenFile.Close()
defer os.Remove(fakeTokenFile.Name())
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", fakeTokenFile.Name())
defer os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS")
if _, err := tokenSource(false, map[string]string{}); err == nil {
t.Fatalf("expected error because specified ADC token file is not a JSON")
}
}
func Test_tokenSource_applicationDefaultCredentials(t *testing.T) {
fakeTokenFile, err := ioutil.TempFile("", "adctoken")
if err != nil {
t.Fatalf("failed to create fake token file: +%v", err)
}
fakeTokenFile.Close()
defer os.Remove(fakeTokenFile.Name())
if err := ioutil.WriteFile(fakeTokenFile.Name(), []byte(`{"type":"service_account"}`), 0600); err != nil {
t.Fatalf("failed to write to fake token file: %+v", err)
}
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", fakeTokenFile.Name())
defer os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS")
ts, err := tokenSource(false, map[string]string{})
if err != nil {
t.Fatalf("failed to get a token source: %+v", err)
}
if ts == nil {
t.Fatal("returned nil token source")
}
}
func Test_parseScopes(t *testing.T) {
cases := []struct {
in map[string]string
out []string
}{
{
map[string]string{},
[]string{
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email"},
},
{
map[string]string{"scopes": ""},
[]string{},
},
{
map[string]string{"scopes": "A,B,C"},
[]string{"A", "B", "C"},
},
}
for _, c := range cases {
got := parseScopes(c.in)
if !reflect.DeepEqual(got, c.out) {
t.Errorf("expected=%v, got=%v", c.out, got)
}
}
}
func errEquiv(got, want error) bool {
if got == want {
return true

View File

@@ -9,8 +9,7 @@ load(
go_test(
name = "go_default_test",
srcs = ["oidc_test.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/oidc",
library = ":go_default_library",
embed = [":go_default_library"],
)
go_library(

View File

@@ -9,8 +9,7 @@ load(
go_test(
name = "go_default_test",
srcs = ["openstack_test.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/openstack",
library = ":go_default_library",
embed = [":go_default_library"],
)
go_library(