Skip to content

Commit

Permalink
Merge pull request #23066 from cjcullen/clientplugin
Browse files Browse the repository at this point in the history
Automatic merge from submit-queue

Client auth provider plugin framework

Allows client plugins to modify the underlying transport to, for example, add custom authorization headers.
  • Loading branch information
k8s-merge-robot committed Apr 22, 2016
2 parents 06160b6 + 03f9b5a commit f4beccf
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 12 deletions.
4 changes: 4 additions & 0 deletions pkg/client/restclient/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/crypto"
"k8s.io/kubernetes/pkg/version"
Expand Down Expand Up @@ -65,6 +66,9 @@ type Config struct {
// Impersonate is the username that this RESTClient will impersonate
Impersonate string

// Server requires plugin-specified authentication.
AuthProvider *clientcmdapi.AuthProviderConfig

// TLSClientConfig contains settings to enable transport layer security
TLSClientConfig

Expand Down
60 changes: 60 additions & 0 deletions pkg/client/restclient/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 restclient

import (
"fmt"
"net/http"
"sync"

"github.com/golang/glog"

clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)

type AuthProvider interface {
// WrapTransport allows the plugin to create a modified RoundTripper that
// attaches authorization headers (or other info) to requests.
WrapTransport(http.RoundTripper) http.RoundTripper
}

type Factory func() (AuthProvider, error)

// All registered auth provider plugins.
var pluginsLock sync.Mutex
var plugins = make(map[string]Factory)

func RegisterAuthProviderPlugin(name string, plugin Factory) error {
pluginsLock.Lock()
defer pluginsLock.Unlock()
if _, found := plugins[name]; found {
return fmt.Errorf("Auth Provider Plugin %q was registered twice", name)
}
glog.V(4).Infof("Registered Auth Provider Plugin %q", name)
plugins[name] = plugin
return nil
}

func GetAuthProvider(apc *clientcmdapi.AuthProviderConfig) (AuthProvider, error) {
pluginsLock.Lock()
defer pluginsLock.Unlock()
p, ok := plugins[apc.Name]
if !ok {
return nil, fmt.Errorf("No Auth Provider found for name %q", apc.Name)
}
return p()
}
39 changes: 33 additions & 6 deletions pkg/client/restclient/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,57 @@ import (
// TLSConfigFor returns a tls.Config that will provide the transport level security defined
// by the provided Config. Will return nil if no transport level security is requested.
func TLSConfigFor(config *Config) (*tls.Config, error) {
return transport.TLSConfigFor(config.transportConfig())
cfg, err := config.transportConfig()
if err != nil {
return nil, err
}
return transport.TLSConfigFor(cfg)
}

// TransportFor returns an http.RoundTripper that will provide the authentication
// or transport level security defined by the provided Config. Will return the
// default http.DefaultTransport if no special case behavior is needed.
func TransportFor(config *Config) (http.RoundTripper, error) {
return transport.New(config.transportConfig())
cfg, err := config.transportConfig()
if err != nil {
return nil, err
}
return transport.New(cfg)
}

// HTTPWrappersForConfig wraps a round tripper with any relevant layered behavior from the
// config. Exposed to allow more clients that need HTTP-like behavior but then must hijack
// the underlying connection (like WebSocket or HTTP2 clients). Pure HTTP clients should use
// the higher level TransportFor or RESTClientFor methods.
func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTripper, error) {
return transport.HTTPWrappersForConfig(config.transportConfig(), rt)
cfg, err := config.transportConfig()
if err != nil {
return nil, err
}
return transport.HTTPWrappersForConfig(cfg, rt)
}

// transportConfig converts a client config to an appropriate transport config.
func (c *Config) transportConfig() *transport.Config {
func (c *Config) transportConfig() (*transport.Config, error) {
wt := c.WrapTransport
if c.AuthProvider != nil {
provider, err := GetAuthProvider(c.AuthProvider)
if err != nil {
return nil, err
}
if wt != nil {
previousWT := wt
wt = func(rt http.RoundTripper) http.RoundTripper {
return provider.WrapTransport(previousWT(rt))
}
} else {
wt = provider.WrapTransport
}
}
return &transport.Config{
UserAgent: c.UserAgent,
Transport: c.Transport,
WrapTransport: c.WrapTransport,
WrapTransport: wt,
TLS: transport.TLSConfig{
CAFile: c.CAFile,
CAData: c.CAData,
Expand All @@ -63,5 +90,5 @@ func (c *Config) transportConfig() *transport.Config {
Password: c.Password,
BearerToken: c.BearerToken,
Impersonate: c.Impersonate,
}
}, nil
}
171 changes: 171 additions & 0 deletions pkg/client/restclient/transport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 restclient

import (
"fmt"
"net/http"
"testing"

clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)

func TestTransportConfigAuthPlugins(t *testing.T) {
if err := RegisterAuthProviderPlugin("pluginA", pluginAProvider); err != nil {
t.Errorf("Unexpected error: failed to register pluginA: %v", err)
}
if err := RegisterAuthProviderPlugin("pluginB", pluginBProvider); err != nil {
t.Errorf("Unexpected error: failed to register pluginB: %v", err)
}
if err := RegisterAuthProviderPlugin("pluginFail", pluginFailProvider); err != nil {
t.Errorf("Unexpected error: failed to register pluginFail: %v", err)
}
testCases := []struct {
useWrapTransport bool
plugin string
expectErr bool
expectPluginA bool
expectPluginB bool
}{
{false, "", false, false, false},
{false, "pluginA", false, true, false},
{false, "pluginB", false, false, true},
{false, "pluginFail", true, false, false},
{false, "pluginUnknown", true, false, false},
}
for i, tc := range testCases {
c := Config{}
if tc.useWrapTransport {
// Specify an existing WrapTransport in the config to make sure that
// plugins play nicely.
c.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
return &wrapTransport{rt}
}
}
if len(tc.plugin) != 0 {
c.AuthProvider = &clientcmdapi.AuthProviderConfig{Name: tc.plugin}
}
tConfig, err := c.transportConfig()
if err != nil {
// Unknown/bad plugins are expected to fail here.
if !tc.expectErr {
t.Errorf("%d. Did not expect errors loading Auth Plugin: %q. Got: %v", i, tc.plugin, err)
}
continue
}
var fullyWrappedTransport http.RoundTripper
fullyWrappedTransport = &emptyTransport{}
if tConfig.WrapTransport != nil {
fullyWrappedTransport = tConfig.WrapTransport(&emptyTransport{})
}
res, err := fullyWrappedTransport.RoundTrip(&http.Request{})
if err != nil {
t.Errorf("%d. Unexpected error in RoundTrip: %v", i, err)
continue
}
hasWrapTransport := res.Header.Get("wrapTransport") == "Y"
hasPluginA := res.Header.Get("pluginA") == "Y"
hasPluginB := res.Header.Get("pluginB") == "Y"
if hasWrapTransport != tc.useWrapTransport {
t.Errorf("%d. Expected Existing config.WrapTransport: %t; Got: %t", i, tc.useWrapTransport, hasWrapTransport)
}
if hasPluginA != tc.expectPluginA {
t.Errorf("%d. Expected Plugin A: %t; Got: %t", i, tc.expectPluginA, hasPluginA)
}
if hasPluginB != tc.expectPluginB {
t.Errorf("%d. Expected Plugin B: %t; Got: %t", i, tc.expectPluginB, hasPluginB)
}
}
}

// emptyTransport provides an empty http.Response with an initialized header
// to allow wrapping RoundTrippers to set header values.
type emptyTransport struct{}

func (*emptyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
res := &http.Response{
Header: make(map[string][]string),
}
return res, nil
}

// wrapTransport sets "wrapTransport" = "Y" on the response.
type wrapTransport struct {
rt http.RoundTripper
}

func (w *wrapTransport) RoundTrip(req *http.Request) (*http.Response, error) {
res, err := w.rt.RoundTrip(req)
if err != nil {
return nil, err
}
res.Header.Add("wrapTransport", "Y")
return res, nil
}

// wrapTransportA sets "pluginA" = "Y" on the response.
type wrapTransportA struct {
rt http.RoundTripper
}

func (w *wrapTransportA) RoundTrip(req *http.Request) (*http.Response, error) {
res, err := w.rt.RoundTrip(req)
if err != nil {
return nil, err
}
res.Header.Add("pluginA", "Y")
return res, nil
}

type pluginA struct{}

func (*pluginA) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return &wrapTransportA{rt}
}

func pluginAProvider() (AuthProvider, error) {
return &pluginA{}, nil
}

// wrapTransportB sets "pluginB" = "Y" on the response.
type wrapTransportB struct {
rt http.RoundTripper
}

func (w *wrapTransportB) RoundTrip(req *http.Request) (*http.Response, error) {
res, err := w.rt.RoundTrip(req)
if err != nil {
return nil, err
}
res.Header.Add("pluginB", "Y")
return res, nil
}

type pluginB struct{}

func (*pluginB) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return &wrapTransportB{rt}
}

func pluginBProvider() (AuthProvider, error) {
return &pluginB{}, nil
}

// pluginFailProvider simulates a registered AuthPlugin that fails to load.
func pluginFailProvider() (AuthProvider, error) {
return nil, fmt.Errorf("Failed to load AuthProvider")
}
7 changes: 7 additions & 0 deletions pkg/client/unversioned/clientcmd/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ type AuthInfo struct {
Username string `json:"username,omitempty"`
// Password is the password for basic authentication to the kubernetes cluster.
Password string `json:"password,omitempty"`
// AuthProvider specifies a custom authentication plugin for the kubernetes cluster.
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
Extensions map[string]runtime.Object `json:"extensions,omitempty"`
}
Expand All @@ -112,6 +114,11 @@ type Context struct {
Extensions map[string]runtime.Object `json:"extensions,omitempty"`
}

// AuthProviderConfig holds the configuration for a specified auth provider.
type AuthProviderConfig struct {
Name string `json:"name"`
}

// NewConfig is a convenience function that returns a new Config object with non-nil maps
func NewConfig() *Config {
return &Config{
Expand Down
15 changes: 11 additions & 4 deletions pkg/client/unversioned/clientcmd/api/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,17 @@ func Example_ofOptionsConfig() {
defaultConfig.AuthInfos["red-mage-via-token"] = &AuthInfo{
Token: "my-secret-token",
}
defaultConfig.AuthInfos["black-mage-via-auth-provider"] = &AuthInfo{
AuthProvider: &AuthProviderConfig{Name: "gcp"},
}
defaultConfig.Contexts["bravo-as-black-mage"] = &Context{
Cluster: "bravo",
AuthInfo: "black-mage-via-file",
AuthInfo: "black-mage-via-auth-provider",
Namespace: "yankee",
}
defaultConfig.Contexts["alfa-as-black-mage"] = &Context{
Cluster: "alfa",
AuthInfo: "black-mage-via-file",
AuthInfo: "black-mage-via-auth-provider",
Namespace: "zulu",
}
defaultConfig.Contexts["alfa-as-white-mage"] = &Context{
Expand Down Expand Up @@ -95,7 +98,7 @@ func Example_ofOptionsConfig() {
// LocationOfOrigin: ""
// cluster: alfa
// namespace: zulu
// user: black-mage-via-file
// user: black-mage-via-auth-provider
// alfa-as-white-mage:
// LocationOfOrigin: ""
// cluster: alfa
Expand All @@ -104,11 +107,15 @@ func Example_ofOptionsConfig() {
// LocationOfOrigin: ""
// cluster: bravo
// namespace: yankee
// user: black-mage-via-file
// user: black-mage-via-auth-provider
// current-context: alfa-as-white-mage
// preferences:
// colors: true
// users:
// black-mage-via-auth-provider:
// LocationOfOrigin: ""
// auth-provider:
// name: gcp
// red-mage-via-token:
// LocationOfOrigin: ""
// token: my-secret-token
Expand Down
Loading

1 comment on commit f4beccf

@k8s-teamcity-mesosphere

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TeamCity OSS :: Kubernetes Mesos :: 4 - Smoke Tests Build 22267 outcome was FAILURE
Summary: Tests failed: 1, passed: 0 Build time: 00:20:52

Failed tests

null: Kubernetes e2e suite.BeforeSuite: <no details avaliable>

Please sign in to comment.