Commit d6e1140b authored by Daniel Smith's avatar Daniel Smith

Implement dynamic admission webhooks

Also fix a bug in rest client
parent f4403d26
...@@ -214,10 +214,18 @@ func (s *ServerRunOptions) AddFlags(fs *pflag.FlagSet) { ...@@ -214,10 +214,18 @@ func (s *ServerRunOptions) AddFlags(fs *pflag.FlagSet) {
"e.g., setting empty UID in update request to its existing value. This flag can be turned off "+ "e.g., setting empty UID in update request to its existing value. This flag can be turned off "+
"after we fix all the clients that send malformed updates.") "after we fix all the clients that send malformed updates.")
fs.StringVar(&s.ProxyClientCertFile, "proxy-client-cert-file", s.ProxyClientCertFile, fs.StringVar(&s.ProxyClientCertFile, "proxy-client-cert-file", s.ProxyClientCertFile, ""+
"client certificate used to prove the identity of the aggragator or kube-apiserver when it proxies requests to a user api-server") "Client certificate used to prove the identity of the aggregator or kube-apiserver "+
fs.StringVar(&s.ProxyClientKeyFile, "proxy-client-key-file", s.ProxyClientKeyFile, "when it must call out during a request. This includes proxying requests to a user "+
"client certificate key used to prove the identity of the aggragator or kube-apiserver when it proxies requests to a user api-server") "api-server and calling out to webhook admission plugins. It is expected that this "+
"cert includes a signature from the CA in the --requestheader-client-ca-file flag. "+
"That CA is published in the 'extension-apiserver-authentication' configmap in "+
"the kube-system namespace. Components recieving calls from kube-aggregator should "+
"use that CA to perform their half of the mutual TLS verification.")
fs.StringVar(&s.ProxyClientKeyFile, "proxy-client-key-file", s.ProxyClientKeyFile, ""+
"Private key for the client certificate used to prove the identity of the aggregator or kube-apiserver "+
"when it must call out during a request. This includes proxying requests to a user "+
"api-server and calling out to webhook admission plugins.")
fs.BoolVar(&s.EnableAggregatorRouting, "enable-aggregator-routing", s.EnableAggregatorRouting, fs.BoolVar(&s.EnableAggregatorRouting, "enable-aggregator-routing", s.EnableAggregatorRouting,
"Turns on aggregator routing requests to endoints IP rather than cluster IP.") "Turns on aggregator routing requests to endoints IP rather than cluster IP.")
......
...@@ -424,6 +424,20 @@ func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client interna ...@@ -424,6 +424,20 @@ func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client interna
quotaRegistry := quotainstall.NewRegistry(nil, nil) quotaRegistry := quotainstall.NewRegistry(nil, nil)
pluginInitializer := kubeapiserveradmission.NewPluginInitializer(client, sharedInformers, apiAuthorizer, cloudConfig, restMapper, quotaRegistry) pluginInitializer := kubeapiserveradmission.NewPluginInitializer(client, sharedInformers, apiAuthorizer, cloudConfig, restMapper, quotaRegistry)
// Read client cert/key for plugins that need to make calls out
if len(s.ProxyClientCertFile) > 0 && len(s.ProxyClientKeyFile) > 0 {
certBytes, err := ioutil.ReadFile(s.ProxyClientCertFile)
if err != nil {
return nil, err
}
keyBytes, err := ioutil.ReadFile(s.ProxyClientKeyFile)
if err != nil {
return nil, err
}
pluginInitializer = pluginInitializer.SetClientCert(certBytes, keyBytes)
}
return pluginInitializer, nil return pluginInitializer, nil
} }
......
...@@ -68,6 +68,7 @@ pkg/api/v1/node ...@@ -68,6 +68,7 @@ pkg/api/v1/node
pkg/api/v1/service pkg/api/v1/service
pkg/apis/abac/v0 pkg/apis/abac/v0
pkg/apis/abac/v1beta1 pkg/apis/abac/v1beta1
pkg/apis/admission/install
pkg/apis/admissionregistration/install pkg/apis/admissionregistration/install
pkg/apis/apps/install pkg/apis/apps/install
pkg/apis/apps/v1beta1 pkg/apis/apps/v1beta1
......
...@@ -30,7 +30,7 @@ type AdmissionReview struct { ...@@ -30,7 +30,7 @@ type AdmissionReview struct {
// Since this admission controller is non-mutating the webhook should avoid setting this in its response to avoid the // Since this admission controller is non-mutating the webhook should avoid setting this in its response to avoid the
// cost of deserializing it. // cost of deserializing it.
// +optional // +optional
Spec AdmissionReviewSpec `json:"spec" protobuf:"bytes,1,opt,name=spec"` Spec AdmissionReviewSpec `json:"spec,omitempty" protobuf:"bytes,1,opt,name=spec"`
// Status is filled in by the webhook and indicates whether the admission request should be permitted. // Status is filled in by the webhook and indicates whether the admission request should be permitted.
// +optional // +optional
Status AdmissionReviewStatus `json:"status,omitempty" protobuf:"bytes,2,opt,name=status"` Status AdmissionReviewStatus `json:"status,omitempty" protobuf:"bytes,2,opt,name=status"`
......
...@@ -17,10 +17,12 @@ limitations under the License. ...@@ -17,10 +17,12 @@ limitations under the License.
package admission package admission
import ( import (
"net/url"
"testing" "testing"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
) )
// TestAuthorizer is a testing struct for testing that fulfills the authorizer interface. // TestAuthorizer is a testing struct for testing that fulfills the authorizer interface.
...@@ -32,18 +34,22 @@ func (t *TestAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, re ...@@ -32,18 +34,22 @@ func (t *TestAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, re
var _ authorizer.Authorizer = &TestAuthorizer{} var _ authorizer.Authorizer = &TestAuthorizer{}
type doNothingAdmission struct{}
func (doNothingAdmission) Admit(a admission.Attributes) error { return nil }
func (doNothingAdmission) Handles(o admission.Operation) bool { return false }
func (doNothingAdmission) Validate() error { return nil }
// WantAuthorizerAdmission is a testing struct that fulfills the WantsAuthorizer // WantAuthorizerAdmission is a testing struct that fulfills the WantsAuthorizer
// interface. // interface.
type WantAuthorizerAdmission struct { type WantAuthorizerAdmission struct {
doNothingAdmission
auth authorizer.Authorizer auth authorizer.Authorizer
} }
func (self *WantAuthorizerAdmission) SetAuthorizer(a authorizer.Authorizer) { func (self *WantAuthorizerAdmission) SetAuthorizer(a authorizer.Authorizer) {
self.auth = a self.auth = a
} }
func (self *WantAuthorizerAdmission) Admit(a admission.Attributes) error { return nil }
func (self *WantAuthorizerAdmission) Handles(o admission.Operation) bool { return false }
func (self *WantAuthorizerAdmission) Validate() error { return nil }
var _ admission.Interface = &WantAuthorizerAdmission{} var _ admission.Interface = &WantAuthorizerAdmission{}
var _ WantsAuthorizer = &WantAuthorizerAdmission{} var _ WantsAuthorizer = &WantAuthorizerAdmission{}
...@@ -60,6 +66,7 @@ func TestWantsAuthorizer(t *testing.T) { ...@@ -60,6 +66,7 @@ func TestWantsAuthorizer(t *testing.T) {
} }
type WantsCloudConfigAdmissionPlugin struct { type WantsCloudConfigAdmissionPlugin struct {
doNothingAdmission
cloudConfig []byte cloudConfig []byte
} }
...@@ -67,10 +74,6 @@ func (self *WantsCloudConfigAdmissionPlugin) SetCloudConfig(cloudConfig []byte) ...@@ -67,10 +74,6 @@ func (self *WantsCloudConfigAdmissionPlugin) SetCloudConfig(cloudConfig []byte)
self.cloudConfig = cloudConfig self.cloudConfig = cloudConfig
} }
func (self *WantsCloudConfigAdmissionPlugin) Admit(a admission.Attributes) error { return nil }
func (self *WantsCloudConfigAdmissionPlugin) Handles(o admission.Operation) bool { return false }
func (self *WantsCloudConfigAdmissionPlugin) Validate() error { return nil }
func TestCloudConfigAdmissionPlugin(t *testing.T) { func TestCloudConfigAdmissionPlugin(t *testing.T) {
cloudConfig := []byte("cloud-configuration") cloudConfig := []byte("cloud-configuration")
initializer := NewPluginInitializer(nil, nil, &TestAuthorizer{}, cloudConfig, nil, nil) initializer := NewPluginInitializer(nil, nil, &TestAuthorizer{}, cloudConfig, nil, nil)
...@@ -81,3 +84,65 @@ func TestCloudConfigAdmissionPlugin(t *testing.T) { ...@@ -81,3 +84,65 @@ func TestCloudConfigAdmissionPlugin(t *testing.T) {
t.Errorf("Expected cloud config to be initialized but found nil") t.Errorf("Expected cloud config to be initialized but found nil")
} }
} }
type fakeServiceResolver struct{}
func (f *fakeServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
return nil, nil
}
type serviceWanter struct {
doNothingAdmission
got ServiceResolver
}
func (s *serviceWanter) SetServiceResolver(sr ServiceResolver) { s.got = sr }
func TestWantsServiceResolver(t *testing.T) {
sw := &serviceWanter{}
fsr := &fakeServiceResolver{}
i := &PluginInitializer{}
i.SetServiceResolver(fsr).Initialize(sw)
if got, ok := sw.got.(*fakeServiceResolver); !ok || got != fsr {
t.Errorf("plumbing fail - %v %v#", ok, got)
}
}
type clientCertWanter struct {
doNothingAdmission
gotCert, gotKey []byte
}
func (s *clientCertWanter) SetClientCert(cert, key []byte) { s.gotCert, s.gotKey = cert, key }
func TestWantsClientCert(t *testing.T) {
i := &PluginInitializer{}
ccw := &clientCertWanter{}
i.SetClientCert([]byte("cert"), []byte("key")).Initialize(ccw)
if string(ccw.gotCert) != "cert" || string(ccw.gotKey) != "key" {
t.Errorf("plumbing fail - %v %v", ccw.gotCert, ccw.gotKey)
}
}
type fakeHookSource struct{}
func (f *fakeHookSource) List() ([]admissionregistration.ExternalAdmissionHook, error) {
return nil, nil
}
type hookSourceWanter struct {
doNothingAdmission
got WebhookSource
}
func (s *hookSourceWanter) SetWebhookSource(w WebhookSource) { s.got = w }
func TestWantsWebhookSource(t *testing.T) {
hsw := &hookSourceWanter{}
fhs := &fakeHookSource{}
i := &PluginInitializer{}
i.SetWebhookSource(fhs).Initialize(hsw)
if got, ok := hsw.got.(*fakeHookSource); !ok || got != fhs {
t.Errorf("plumbing fail - %v %v#", ok, got)
}
}
...@@ -17,9 +17,12 @@ limitations under the License. ...@@ -17,9 +17,12 @@ limitations under the License.
package admission package admission
import ( import (
"net/url"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
...@@ -61,25 +64,62 @@ type WantsQuotaRegistry interface { ...@@ -61,25 +64,62 @@ type WantsQuotaRegistry interface {
admission.Validator admission.Validator
} }
type pluginInitializer struct { // WantsServiceResolver defines a fuction that accepts a ServiceResolver for
internalClient internalclientset.Interface // admission plugins that need to make calls to services.
informers informers.SharedInformerFactory type WantsServiceResolver interface {
authorizer authorizer.Authorizer SetServiceResolver(ServiceResolver)
cloudConfig []byte }
restMapper meta.RESTMapper
quotaRegistry quota.Registry // WantsClientCert defines a fuction that accepts a cert & key for admission
// plugins that need to make calls and prove their identity.
type WantsClientCert interface {
SetClientCert(cert, key []byte)
}
// WantsWebhookSource defines a function that accepts a webhook lister for the
// dynamic webhook plugin.
type WantsWebhookSource interface {
SetWebhookSource(WebhookSource)
}
// ServiceResolver knows how to convert a service reference into an actual
// location.
type ServiceResolver interface {
ResolveEndpoint(namespace, name string) (*url.URL, error)
}
// WebhookSource can list dynamic webhook plugins.
type WebhookSource interface {
List() ([]admissionregistration.ExternalAdmissionHook, error)
} }
var _ admission.PluginInitializer = pluginInitializer{} type PluginInitializer struct {
internalClient internalclientset.Interface
informers informers.SharedInformerFactory
authorizer authorizer.Authorizer
cloudConfig []byte
restMapper meta.RESTMapper
quotaRegistry quota.Registry
serviceResolver ServiceResolver
webhookSource WebhookSource
// for proving we are apiserver in call-outs
clientCert []byte
clientKey []byte
}
var _ admission.PluginInitializer = &PluginInitializer{}
// NewPluginInitializer constructs new instance of PluginInitializer // NewPluginInitializer constructs new instance of PluginInitializer
// TODO: switch these parameters to use the builder pattern or just make them
// all public, this construction method is pointless boilerplate.
func NewPluginInitializer(internalClient internalclientset.Interface, func NewPluginInitializer(internalClient internalclientset.Interface,
sharedInformers informers.SharedInformerFactory, sharedInformers informers.SharedInformerFactory,
authz authorizer.Authorizer, authz authorizer.Authorizer,
cloudConfig []byte, cloudConfig []byte,
restMapper meta.RESTMapper, restMapper meta.RESTMapper,
quotaRegistry quota.Registry) admission.PluginInitializer { quotaRegistry quota.Registry) *PluginInitializer {
return pluginInitializer{ return &PluginInitializer{
internalClient: internalClient, internalClient: internalClient,
informers: sharedInformers, informers: sharedInformers,
authorizer: authz, authorizer: authz,
...@@ -89,9 +129,30 @@ func NewPluginInitializer(internalClient internalclientset.Interface, ...@@ -89,9 +129,30 @@ func NewPluginInitializer(internalClient internalclientset.Interface,
} }
} }
// SetServiceResolver sets the service resolver which is needed by some plugins.
func (i *PluginInitializer) SetServiceResolver(s ServiceResolver) *PluginInitializer {
i.serviceResolver = s
return i
}
// SetClientCert sets the client cert & key (identity used for calling out to
// web hooks) which is needed by some plugins.
func (i *PluginInitializer) SetClientCert(cert, key []byte) *PluginInitializer {
i.clientCert = cert
i.clientKey = key
return i
}
// SetWebhookSource sets the webhook source-- admittedly this is probably
// specific to the external admission hook plugin.
func (i *PluginInitializer) SetWebhookSource(w WebhookSource) *PluginInitializer {
i.webhookSource = w
return i
}
// Initialize checks the initialization interfaces implemented by each plugin // Initialize checks the initialization interfaces implemented by each plugin
// and provide the appropriate initialization data // and provide the appropriate initialization data
func (i pluginInitializer) Initialize(plugin admission.Interface) { func (i *PluginInitializer) Initialize(plugin admission.Interface) {
if wants, ok := plugin.(WantsInternalKubeClientSet); ok { if wants, ok := plugin.(WantsInternalKubeClientSet); ok {
wants.SetInternalKubeClientSet(i.internalClient) wants.SetInternalKubeClientSet(i.internalClient)
} }
...@@ -115,4 +176,25 @@ func (i pluginInitializer) Initialize(plugin admission.Interface) { ...@@ -115,4 +176,25 @@ func (i pluginInitializer) Initialize(plugin admission.Interface) {
if wants, ok := plugin.(WantsQuotaRegistry); ok { if wants, ok := plugin.(WantsQuotaRegistry); ok {
wants.SetQuotaRegistry(i.quotaRegistry) wants.SetQuotaRegistry(i.quotaRegistry)
} }
if wants, ok := plugin.(WantsServiceResolver); ok {
if i.serviceResolver == nil {
panic("An admission plugin wants the service resolver, but it was not provided.")
}
wants.SetServiceResolver(i.serviceResolver)
}
if wants, ok := plugin.(WantsClientCert); ok {
if i.clientCert == nil || i.clientKey == nil {
panic("An admission plugin wants a client cert/key, but they were not provided.")
}
wants.SetClientCert(i.clientCert, i.clientKey)
}
if wants, ok := plugin.(WantsWebhookSource); ok {
if i.webhookSource == nil {
panic("An admission plugin wants a webhook source, but it was not provided.")
}
wants.SetWebhookSource(i.webhookSource)
}
} }
...@@ -987,9 +987,8 @@ func TestRunApplySetLastApplied(t *testing.T) { ...@@ -987,9 +987,8 @@ func TestRunApplySetLastApplied(t *testing.T) {
if buf.String() != test.expectedOut { if buf.String() != test.expectedOut {
t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut) t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut)
} }
} }
cmdutil.BehaviorOnFatal(func(str string, code int) {})
} }
func checkPatchString(t *testing.T, req *http.Request) { func checkPatchString(t *testing.T, req *http.Request) {
......
...@@ -39,7 +39,7 @@ func TestCreateDeployment(t *testing.T) { ...@@ -39,7 +39,7 @@ func TestCreateDeployment(t *testing.T) {
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
return &http.Response{ return &http.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
Body: ioutil.NopCloser(&bytes.Buffer{}), Body: ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))),
}, nil }, nil
}), }),
} }
......
...@@ -171,7 +171,7 @@ func TestRunArgsFollowDashRules(t *testing.T) { ...@@ -171,7 +171,7 @@ func TestRunArgsFollowDashRules(t *testing.T) {
} else { } else {
return &http.Response{ return &http.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
Body: ioutil.NopCloser(&bytes.Buffer{}), Body: ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))),
}, nil }, nil
} }
}), }),
......
...@@ -85,6 +85,7 @@ var typesAllowedTags = map[reflect.Type]bool{ ...@@ -85,6 +85,7 @@ var typesAllowedTags = map[reflect.Type]bool{
reflect.TypeOf(metav1.DeleteOptions{}): true, reflect.TypeOf(metav1.DeleteOptions{}): true,
reflect.TypeOf(metav1.GroupVersionKind{}): true, reflect.TypeOf(metav1.GroupVersionKind{}): true,
reflect.TypeOf(metav1.GroupVersionResource{}): true, reflect.TypeOf(metav1.GroupVersionResource{}): true,
reflect.TypeOf(metav1.Status{}): true,
} }
func ensureNoTags(t *testing.T, gvk schema.GroupVersionKind, tp reflect.Type, parents []reflect.Type) { func ensureNoTags(t *testing.T, gvk schema.GroupVersionKind, tp reflect.Type, parents []reflect.Type) {
......
...@@ -14,25 +14,31 @@ See the License for the specific language governing permissions and ...@@ -14,25 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// Package webhook checks a webhook for configured operation admission // Package webhook delegates admission checks to dynamically configured webhooks.
package webhook package webhook
import ( import (
"errors" "context"
"fmt" "fmt"
"io" "io"
"sync"
"time"
"github.com/golang/glog" "github.com/golang/glog"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
admissionv1alpha1 "k8s.io/kubernetes/pkg/apis/admission/v1alpha1" admissionv1alpha1 "k8s.io/kubernetes/pkg/apis/admission/v1alpha1"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
admissioninit "k8s.io/kubernetes/pkg/kubeapiserver/admission"
// install the clientgo admissiony API for use with api registry // install the clientgo admission API for use with api registry
_ "k8s.io/kubernetes/pkg/apis/admission/install" _ "k8s.io/kubernetes/pkg/apis/admission/install"
) )
...@@ -42,22 +48,22 @@ var ( ...@@ -42,22 +48,22 @@ var (
} }
) )
type ErrCallingWebhook struct {
WebhookName string
Reason error
}
func (e *ErrCallingWebhook) Error() string {
if e.Reason != nil {
return fmt.Sprintf("failed calling admission webhook %q: %v", e.WebhookName, e.Reason)
}
return fmt.Sprintf("failed calling admission webhook %q; no further details available", e.WebhookName)
}
// Register registers a plugin // Register registers a plugin
func Register(plugins *admission.Plugins) { func Register(plugins *admission.Plugins) {
plugins.Register("GenericAdmissionWebhook", func(configFile io.Reader) (admission.Interface, error) { plugins.Register("GenericAdmissionWebhook", func(configFile io.Reader) (admission.Interface, error) {
var gwhConfig struct { plugin, err := NewGenericAdmissionWebhook()
WebhookConfig GenericAdmissionWebhookConfig `json:"webhook"`
}
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
err := d.Decode(&gwhConfig)
if err != nil {
return nil, err
}
plugin, err := NewGenericAdmissionWebhook(&gwhConfig.WebhookConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -67,98 +73,150 @@ func Register(plugins *admission.Plugins) { ...@@ -67,98 +73,150 @@ func Register(plugins *admission.Plugins) {
} }
// NewGenericAdmissionWebhook returns a generic admission webhook plugin. // NewGenericAdmissionWebhook returns a generic admission webhook plugin.
func NewGenericAdmissionWebhook(config *GenericAdmissionWebhookConfig) (admission.Interface, error) { func NewGenericAdmissionWebhook() (*GenericAdmissionWebhook, error) {
err := normalizeConfig(config)
if err != nil {
return nil, err
}
gw, err := webhook.NewGenericWebhook(api.Registry, api.Codecs, config.KubeConfigFile, groupVersions, config.RetryBackoff)
if err != nil {
return nil, err
}
return &GenericAdmissionWebhook{ return &GenericAdmissionWebhook{
Handler: admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update), Handler: admission.NewHandler(
webhook: gw, admission.Connect,
rules: config.Rules, admission.Create,
admission.Delete,
admission.Update,
),
negotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
Serializer: api.Codecs.LegacyCodec(admissionv1alpha1.SchemeGroupVersion),
}),
}, nil }, nil
} }
// GenericAdmissionWebhook is an implementation of admission.Interface. // GenericAdmissionWebhook is an implementation of admission.Interface.
type GenericAdmissionWebhook struct { type GenericAdmissionWebhook struct {
*admission.Handler *admission.Handler
webhook *webhook.GenericWebhook hookSource admissioninit.WebhookSource
rules []Rule serviceResolver admissioninit.ServiceResolver
negotiatedSerializer runtime.NegotiatedSerializer
clientCert []byte
clientKey []byte
}
var (
_ = admissioninit.WantsServiceResolver(&GenericAdmissionWebhook{})
_ = admissioninit.WantsClientCert(&GenericAdmissionWebhook{})
_ = admissioninit.WantsWebhookSource(&GenericAdmissionWebhook{})
)
func (a *GenericAdmissionWebhook) SetServiceResolver(sr admissioninit.ServiceResolver) {
a.serviceResolver = sr
}
func (a *GenericAdmissionWebhook) SetClientCert(cert, key []byte) {
a.clientCert = cert
a.clientKey = key
}
func (a *GenericAdmissionWebhook) SetWebhookSource(ws admissioninit.WebhookSource) {
a.hookSource = ws
} }
// Admit makes an admission decision based on the request attributes. // Admit makes an admission decision based on the request attributes.
func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) (err error) { func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
var matched *Rule hooks, err := a.hookSource.List()
if err != nil {
// Process all declared rules to attempt to find a match return fmt.Errorf("failed listing hooks: %v", err)
for i, rule := range a.rules { }
if Matches(rule, attr) { ctx := context.TODO()
glog.V(2).Infof("rule at index %d matched request", i)
matched = &a.rules[i] errCh := make(chan error, len(hooks))
break wg := sync.WaitGroup{}
} wg.Add(len(hooks))
for i := range hooks {
go func(hook *admissionregistration.ExternalAdmissionHook) {
defer wg.Done()
if err := a.callHook(ctx, hook, attr); err == nil {
return
} else if callErr, ok := err.(*ErrCallingWebhook); ok {
glog.Warningf("Failed calling webhook %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
// Since we are failing open to begin with, we do not send an error down the channel
} else {
glog.Warningf("rejected by webhook %v %t: %v", hook.Name, err, err)
errCh <- err
}
}(&hooks[i])
} }
wg.Wait()
close(errCh)
if matched == nil { var errs []error
glog.V(2).Infof("rule explicitly allowed the request: no rule matched the admission request") for e := range errCh {
errs = append(errs, e)
}
if len(errs) == 0 {
return nil return nil
} }
if len(errs) > 1 {
for i := 1; i < len(errs); i++ {
// TODO: merge status errors; until then, just return the first one.
utilruntime.HandleError(errs[i])
}
}
return errs[0]
}
// The matched rule skips processing this request func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *admissionregistration.ExternalAdmissionHook, attr admission.Attributes) error {
if matched.Type == Skip { matches := false
glog.V(2).Infof("rule explicitly allowed the request") for _, r := range h.Rules {
m := RuleMatcher{Rule: r, Attr: attr}
if m.Matches() {
matches = true
break
}
}
if !matches {
return nil return nil
} }
// Make the webhook request // Make the webhook request
request := admissionv1alpha1.NewAdmissionReview(attr) request := admissionv1alpha1.NewAdmissionReview(attr)
response := a.webhook.RestClient.Post().Body(&request).Do() client, err := a.hookClient(h)
if err != nil {
// Handle webhook response return &ErrCallingWebhook{WebhookName: h.Name, Reason: err}
if err := response.Error(); err != nil {
return a.handleError(attr, matched.FailAction, err)
} }
if err := client.Post().Context(ctx).Body(&request).Do().Into(&request); err != nil {
var statusCode int return &ErrCallingWebhook{WebhookName: h.Name, Reason: err}
if response.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
return a.handleError(attr, matched.FailAction, fmt.Errorf("error contacting webhook: %d", statusCode))
} }
if err := response.Into(&request); err != nil { if request.Status.Allowed {
return a.handleError(attr, matched.FailAction, err) return nil
} }
if !request.Status.Allowed { if request.Status.Result == nil {
if request.Status.Result != nil && len(request.Status.Result.Reason) > 0 { return fmt.Errorf("admission webhook %q denied the request without explanation", h.Name)
return a.handleError(attr, Deny, fmt.Errorf("webhook backend denied the request: %s", request.Status.Result.Reason))
}
return a.handleError(attr, Deny, errors.New("webhook backend denied the request"))
} }
// The webhook admission controller DOES NOT allow mutation of the admission request so nothing else is required return &apierrors.StatusError{
ErrStatus: *request.Status.Result,
return nil }
} }
func (a *GenericAdmissionWebhook) handleError(attr admission.Attributes, allowIfErr FailAction, err error) error { func (a *GenericAdmissionWebhook) hookClient(h *admissionregistration.ExternalAdmissionHook) (*rest.RESTClient, error) {
u, err := a.serviceResolver.ResolveEndpoint(h.ClientConfig.Service.Namespace, h.ClientConfig.Service.Name)
if err != nil { if err != nil {
glog.V(2).Infof("error contacting webhook backend: %s", err) return nil, err
if allowIfErr != Allow {
glog.V(2).Infof("resource not allowed due to webhook backend failure: %s", err)
return admission.NewForbidden(attr, err)
}
glog.V(2).Infof("resource allowed in spite of webhook backend failure")
} }
return nil // TODO: cache these instead of constructing one each time
cfg := &rest.Config{
Host: u.Host,
APIPath: u.Path,
TLSClientConfig: rest.TLSClientConfig{
CAData: h.ClientConfig.CABundle,
CertData: a.clientCert,
KeyData: a.clientKey,
},
UserAgent: "kube-apiserver-admission",
Timeout: 30 * time.Second,
ContentConfig: rest.ContentConfig{
NegotiatedSerializer: a.negotiatedSerializer,
},
}
return rest.UnversionedRESTClientFor(cfg)
} }
// TODO: Allow configuring the serialization strategy
/*
Copyright 2017 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 webhook checks a webhook for configured operation admission
package webhook
import (
"fmt"
"strings"
"time"
"k8s.io/apiserver/pkg/admission"
)
const (
defaultFailAction = Deny
defaultRetryBackoff = time.Duration(500) * time.Millisecond
errInvalidFailAction = "webhook rule (%d) has an invalid fail action: %s should be either %v or %v"
errInvalidRuleOperation = "webhook rule (%d) has an invalid operation (%d): %s"
errInvalidRuleType = "webhook rule (%d) has an invalid type: %s should be either %v or %v"
errMissingKubeConfigFile = "kubeConfigFile is required"
errOneRuleRequired = "webhook requires at least one rule defined"
errRetryBackoffOutOfRange = "webhook retry backoff is invalid: %v should be between %v and %v"
minRetryBackoff = time.Duration(1) * time.Millisecond
maxRetryBackoff = time.Duration(5) * time.Minute
)
func normalizeConfig(config *GenericAdmissionWebhookConfig) error {
allowFailAction := string(Allow)
denyFailAction := string(Deny)
connectOperation := string(admission.Connect)
createOperation := string(admission.Create)
deleteOperation := string(admission.Delete)
updateOperation := string(admission.Update)
sendRuleType := string(Send)
skipRuleType := string(Skip)
// Validate the kubeConfigFile property is present
if config.KubeConfigFile == "" {
return fmt.Errorf(errMissingKubeConfigFile)
}
// Normalize and validate the retry backoff
if config.RetryBackoff == 0 {
config.RetryBackoff = defaultRetryBackoff
} else {
// Unmarshalling gives nanoseconds so convert to milliseconds
config.RetryBackoff *= time.Millisecond
}
if config.RetryBackoff < minRetryBackoff || config.RetryBackoff > maxRetryBackoff {
return fmt.Errorf(errRetryBackoffOutOfRange, config.RetryBackoff, minRetryBackoff, maxRetryBackoff)
}
// Validate that there is at least one rule
if len(config.Rules) == 0 {
return fmt.Errorf(errOneRuleRequired)
}
for i, rule := range config.Rules {
// Normalize and validate the fail action
failAction := strings.ToUpper(string(rule.FailAction))
switch failAction {
case "":
config.Rules[i].FailAction = defaultFailAction
case allowFailAction:
config.Rules[i].FailAction = Allow
case denyFailAction:
config.Rules[i].FailAction = Deny
default:
return fmt.Errorf(errInvalidFailAction, i, rule.FailAction, Allow, Deny)
}
// Normalize and validate the rule operation(s)
for j, operation := range rule.Operations {
operation := strings.ToUpper(string(operation))
switch operation {
case connectOperation:
config.Rules[i].Operations[j] = admission.Connect
case createOperation:
config.Rules[i].Operations[j] = admission.Create
case deleteOperation:
config.Rules[i].Operations[j] = admission.Delete
case updateOperation:
config.Rules[i].Operations[j] = admission.Update
default:
return fmt.Errorf(errInvalidRuleOperation, i, j, rule.Operations[j])
}
}
// Normalize and validate the rule type
ruleType := strings.ToUpper(string(rule.Type))
switch ruleType {
case sendRuleType:
config.Rules[i].Type = Send
case skipRuleType:
config.Rules[i].Type = Skip
default:
return fmt.Errorf(errInvalidRuleType, i, rule.Type, Send, Skip)
}
}
return nil
}
/*
Copyright 2017 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 webhook
import (
"fmt"
"reflect"
"testing"
"time"
"k8s.io/apiserver/pkg/admission"
)
func TestConfigNormalization(t *testing.T) {
defaultRules := []Rule{
Rule{
Type: Skip,
},
}
highRetryBackoff := (maxRetryBackoff / time.Millisecond) + (time.Duration(1) * time.Millisecond)
kubeConfigFile := "/tmp/kube/config"
lowRetryBackoff := time.Duration(-1)
normalizedValidRules := []Rule{
Rule{
APIGroups: []string{""},
FailAction: Allow,
Namespaces: []string{"my-ns"},
Operations: []admission.Operation{
admission.Connect,
admission.Create,
admission.Delete,
admission.Update,
},
Resources: []string{"pods"},
ResourceNames: []string{"my-name"},
Type: Send,
},
Rule{
FailAction: Deny,
Type: Skip,
},
}
rawValidRules := []Rule{
Rule{
APIGroups: []string{""},
FailAction: FailAction("AlLoW"),
Namespaces: []string{"my-ns"},
Operations: []admission.Operation{
admission.Operation("connect"),
admission.Operation("CREATE"),
admission.Operation("DeLeTe"),
admission.Operation("UPdaTE"),
},
Resources: []string{"pods"},
ResourceNames: []string{"my-name"},
Type: RuleType("SenD"),
},
Rule{
FailAction: Deny,
Type: Skip,
},
}
unknownFailAction := FailAction("Unknown")
unknownOperation := admission.Operation("Unknown")
unknownRuleType := RuleType("Allow")
tests := []struct {
test string
rawConfig GenericAdmissionWebhookConfig
normalizedConfig GenericAdmissionWebhookConfig
err error
}{
{
test: "kubeConfigFile was not provided (error)",
rawConfig: GenericAdmissionWebhookConfig{},
err: fmt.Errorf(errMissingKubeConfigFile),
},
{
test: "retryBackoff was not provided (use default)",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
Rules: defaultRules,
},
normalizedConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
RetryBackoff: defaultRetryBackoff,
Rules: defaultRules,
},
},
{
test: "retryBackoff was below minimum value (error)",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
RetryBackoff: lowRetryBackoff,
Rules: defaultRules,
},
err: fmt.Errorf(errRetryBackoffOutOfRange, lowRetryBackoff*time.Millisecond, minRetryBackoff, maxRetryBackoff),
},
{
test: "retryBackoff was above maximum value (error)",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
RetryBackoff: highRetryBackoff,
Rules: defaultRules,
},
err: fmt.Errorf(errRetryBackoffOutOfRange, highRetryBackoff*time.Millisecond, minRetryBackoff, maxRetryBackoff),
},
{
test: "rules should have at least one rule (error)",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
},
err: fmt.Errorf(errOneRuleRequired),
},
{
test: "fail action was not provided (use default)",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
Rules: []Rule{
Rule{
Type: Skip,
},
},
},
normalizedConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
RetryBackoff: defaultRetryBackoff,
Rules: []Rule{
Rule{
FailAction: defaultFailAction,
Type: Skip,
},
},
},
},
{
test: "rule has invalid fail action (error)",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
Rules: []Rule{
Rule{
FailAction: unknownFailAction,
},
},
},
err: fmt.Errorf(errInvalidFailAction, 0, unknownFailAction, Allow, Deny),
},
{
test: "rule has invalid operation (error)",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
Rules: []Rule{
Rule{
Operations: []admission.Operation{unknownOperation},
},
},
},
err: fmt.Errorf(errInvalidRuleOperation, 0, 0, unknownOperation),
},
{
test: "rule has invalid type (error)",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
Rules: []Rule{
Rule{
Type: unknownRuleType,
},
},
},
err: fmt.Errorf(errInvalidRuleType, 0, unknownRuleType, Send, Skip),
},
{
test: "valid configuration",
rawConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
Rules: rawValidRules,
},
normalizedConfig: GenericAdmissionWebhookConfig{
KubeConfigFile: kubeConfigFile,
RetryBackoff: defaultRetryBackoff,
Rules: normalizedValidRules,
},
err: nil,
},
}
for _, tt := range tests {
err := normalizeConfig(&tt.rawConfig)
if err == nil {
if tt.err != nil {
// Ensure that expected errors are produced
t.Errorf("%s: expected error but did not produce one", tt.test)
} else if !reflect.DeepEqual(tt.rawConfig, tt.normalizedConfig) {
// Ensure that valid configurations are structured properly
t.Errorf("%s: normalized config mismtach. got: %v expected: %v", tt.test, tt.rawConfig, tt.normalizedConfig)
}
} else {
if tt.err == nil {
// Ensure that unexpected errors are not produced
t.Errorf("%s: unexpected error: %v", tt.test, err)
} else if err != nil && tt.err != nil && err.Error() != tt.err.Error() {
// Ensure that expected errors are formated properly
t.Errorf("%s: error message mismatch. got: '%v' expected: '%v'", tt.test, err, tt.err)
}
}
}
}
...@@ -17,46 +17,31 @@ limitations under the License. ...@@ -17,46 +17,31 @@ limitations under the License.
// Package webhook checks a webhook for configured operation admission // Package webhook checks a webhook for configured operation admission
package webhook package webhook
import "k8s.io/apiserver/pkg/admission" import (
"strings"
func indexOf(items []string, requested string) int { "k8s.io/apiserver/pkg/admission"
for i, item := range items { "k8s.io/kubernetes/pkg/apis/admissionregistration"
if item == requested { )
return i
}
}
return -1
}
// APIGroupMatches returns if the admission.Attributes matches the rule's API groups
func APIGroupMatches(rule Rule, attr admission.Attributes) bool {
if len(rule.APIGroups) == 0 {
return true
}
return indexOf(rule.APIGroups, attr.GetResource().Group) > -1 type RuleMatcher struct {
Rule admissionregistration.RuleWithOperations
Attr admission.Attributes
} }
// Matches returns if the admission.Attributes matches the rule func (r *RuleMatcher) Matches() bool {
func Matches(rule Rule, attr admission.Attributes) bool { return r.operation() &&
return APIGroupMatches(rule, attr) && r.group() &&
NamespaceMatches(rule, attr) && r.version() &&
OperationMatches(rule, attr) && r.resource()
ResourceMatches(rule, attr) &&
ResourceNamesMatches(rule, attr)
} }
// OperationMatches returns if the admission.Attributes matches the rule's operation func exactOrWildcard(items []string, requested string) bool {
func OperationMatches(rule Rule, attr admission.Attributes) bool { for _, item := range items {
if len(rule.Operations) == 0 { if item == "*" {
return true return true
} }
if item == requested {
aOp := attr.GetOperation()
for _, rOp := range rule.Operations {
if aOp == rOp {
return true return true
} }
} }
...@@ -64,35 +49,46 @@ func OperationMatches(rule Rule, attr admission.Attributes) bool { ...@@ -64,35 +49,46 @@ func OperationMatches(rule Rule, attr admission.Attributes) bool {
return false return false
} }
// NamespaceMatches returns if the admission.Attributes matches the rule's namespaces func (r *RuleMatcher) group() bool {
func NamespaceMatches(rule Rule, attr admission.Attributes) bool { return exactOrWildcard(r.Rule.APIGroups, r.Attr.GetResource().Group)
if len(rule.Namespaces) == 0 { }
return true
}
return indexOf(rule.Namespaces, attr.GetNamespace()) > -1 func (r *RuleMatcher) version() bool {
return exactOrWildcard(r.Rule.APIVersions, r.Attr.GetResource().Version)
} }
// ResourceMatches returns if the admission.Attributes matches the rule's resource (and optional subresource) func (r *RuleMatcher) operation() bool {
func ResourceMatches(rule Rule, attr admission.Attributes) bool { attrOp := r.Attr.GetOperation()
if len(rule.Resources) == 0 { for _, op := range r.Rule.Operations {
return true if op == admissionregistration.OperationAll {
return true
}
// The constants are the same such that this is a valid cast (and this
// is tested).
if op == admissionregistration.OperationType(attrOp) {
return true
}
} }
return false
}
resource := attr.GetResource().Resource func splitResource(resSub string) (res, sub string) {
parts := strings.SplitN(resSub, "/", 2)
if len(attr.GetSubresource()) > 0 { if len(parts) == 2 {
resource = attr.GetResource().Resource + "/" + attr.GetSubresource() return parts[0], parts[1]
} }
return parts[0], ""
return indexOf(rule.Resources, resource) > -1
} }
// ResourceNamesMatches returns if the admission.Attributes matches the rule's resource names func (r *RuleMatcher) resource() bool {
func ResourceNamesMatches(rule Rule, attr admission.Attributes) bool { opRes, opSub := r.Attr.GetResource().Resource, r.Attr.GetSubresource()
if len(rule.ResourceNames) == 0 { for _, res := range r.Rule.Resources {
return true res, sub := splitResource(res)
resMatch := res == "*" || res == opRes
subMatch := sub == "*" || sub == opSub
if resMatch && subMatch {
return true
}
} }
return false
return indexOf(rule.ResourceNames, attr.GetName()) > -1
} }
/*
Copyright 2017 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 webhook checks a webhook for configured operation admission
package webhook
import (
"time"
"k8s.io/apiserver/pkg/admission"
)
// FailAction is the action to take whenever a webhook fails and there are no more retries available
type FailAction string
const (
// Allow is the fail action taken whenever the webhook call fails and there are no more retries
Allow FailAction = "ALLOW"
// Deny is the fail action taken whenever the webhook call fails and there are no more retries
Deny FailAction = "DENY"
)
// GenericAdmissionWebhookConfig holds configuration for an admission webhook
type GenericAdmissionWebhookConfig struct {
KubeConfigFile string `json:"kubeConfigFile"`
RetryBackoff time.Duration `json:"retryBackoff"`
Rules []Rule `json:"rules"`
}
// Rule is the type defining an admission rule in the admission controller configuration file
type Rule struct {
// APIGroups is a list of API groups that contain the resource this rule applies to.
APIGroups []string `json:"apiGroups"`
// FailAction is the action to take whenever the webhook fails and there are no more retries (Default: DENY)
FailAction FailAction `json:"failAction"`
// Namespaces is a list of namespaces this rule applies to.
Namespaces []string `json:"namespaces"`
// Operations is a list of admission operations this rule applies to.
Operations []admission.Operation `json:"operations"`
// Resources is a list of resources this rule applies to.
Resources []string `json:"resources"`
// ResourceNames is a list of resource names this rule applies to.
ResourceNames []string `json:"resourceNames"`
// Type is the admission rule type
Type RuleType `json:"type"`
}
// RuleType is the type of admission rule
type RuleType string
const (
// Send is the rule type for when a matching admission.Attributes should be sent to the webhook
Send RuleType = "SEND"
// Skip is the rule type for when a matching admission.Attributes should not be sent to the webhook
Skip RuleType = "SKIP"
)
...@@ -29,8 +29,8 @@ import ( ...@@ -29,8 +29,8 @@ import (
// //
// +protobuf.options.(gogoproto.goproto_stringer)=false // +protobuf.options.(gogoproto.goproto_stringer)=false
type GroupResource struct { type GroupResource struct {
Group string `protobuf:"bytes,1,opt,name=group"` Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
Resource string `protobuf:"bytes,2,opt,name=resource"` Resource string `json:"resource" protobuf:"bytes,2,opt,name=resource"`
} }
func (gr *GroupResource) String() string { func (gr *GroupResource) String() string {
...@@ -45,9 +45,9 @@ func (gr *GroupResource) String() string { ...@@ -45,9 +45,9 @@ func (gr *GroupResource) String() string {
// //
// +protobuf.options.(gogoproto.goproto_stringer)=false // +protobuf.options.(gogoproto.goproto_stringer)=false
type GroupVersionResource struct { type GroupVersionResource struct {
Group string `protobuf:"bytes,1,opt,name=group"` Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
Version string `protobuf:"bytes,2,opt,name=version"` Version string `json:"version" protobuf:"bytes,2,opt,name=version"`
Resource string `protobuf:"bytes,3,opt,name=resource"` Resource string `json:"resource" protobuf:"bytes,3,opt,name=resource"`
} }
func (gvr *GroupVersionResource) String() string { func (gvr *GroupVersionResource) String() string {
...@@ -59,8 +59,8 @@ func (gvr *GroupVersionResource) String() string { ...@@ -59,8 +59,8 @@ func (gvr *GroupVersionResource) String() string {
// //
// +protobuf.options.(gogoproto.goproto_stringer)=false // +protobuf.options.(gogoproto.goproto_stringer)=false
type GroupKind struct { type GroupKind struct {
Group string `protobuf:"bytes,1,opt,name=group"` Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
Kind string `protobuf:"bytes,2,opt,name=kind"` Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"`
} }
func (gk *GroupKind) String() string { func (gk *GroupKind) String() string {
...@@ -88,8 +88,8 @@ func (gvk GroupVersionKind) String() string { ...@@ -88,8 +88,8 @@ func (gvk GroupVersionKind) String() string {
// //
// +protobuf.options.(gogoproto.goproto_stringer)=false // +protobuf.options.(gogoproto.goproto_stringer)=false
type GroupVersion struct { type GroupVersion struct {
Group string `protobuf:"bytes,1,opt,name=group"` Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
Version string `protobuf:"bytes,2,opt,name=version"` Version string `json:"version" protobuf:"bytes,2,opt,name=version"`
} }
// Empty returns true if group and version are empty // Empty returns true if group and version are empty
......
...@@ -1148,6 +1148,9 @@ func (r Result) Into(obj runtime.Object) error { ...@@ -1148,6 +1148,9 @@ func (r Result) Into(obj runtime.Object) error {
if r.decoder == nil { if r.decoder == nil {
return fmt.Errorf("serializer for %s doesn't exist", r.contentType) return fmt.Errorf("serializer for %s doesn't exist", r.contentType)
} }
if len(r.body) == 0 {
return fmt.Errorf("0-length response")
}
out, _, err := r.decoder.Decode(r.body, nil, obj) out, _, err := r.decoder.Decode(r.body, nil, obj)
if err != nil || out == obj { if err != nil || out == obj {
......
...@@ -329,6 +329,16 @@ func TestResultIntoWithErrReturnsErr(t *testing.T) { ...@@ -329,6 +329,16 @@ func TestResultIntoWithErrReturnsErr(t *testing.T) {
} }
} }
func TestResultIntoWithNoBodyReturnsErr(t *testing.T) {
res := Result{
body: []byte{},
decoder: scheme.Codecs.LegacyCodec(v1.SchemeGroupVersion),
}
if err := res.Into(&v1.Pod{}); err == nil || !strings.Contains(err.Error(), "0-length") {
t.Errorf("should have complained about 0 length body")
}
}
func TestURLTemplate(t *testing.T) { func TestURLTemplate(t *testing.T) {
uri, _ := url.Parse("http://localhost") uri, _ := url.Parse("http://localhost")
r := NewRequest(nil, "POST", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil) r := NewRequest(nil, "POST", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil)
......
...@@ -381,6 +381,10 @@ var ephemeralWhiteList = createEphemeralWhiteList( ...@@ -381,6 +381,10 @@ var ephemeralWhiteList = createEphemeralWhiteList(
// k8s.io/kubernetes/pkg/apis/policy/v1beta1 // k8s.io/kubernetes/pkg/apis/policy/v1beta1
gvr("policy", "v1beta1", "evictions"), // not stored in etcd, deals with evicting kapiv1.Pod gvr("policy", "v1beta1", "evictions"), // not stored in etcd, deals with evicting kapiv1.Pod
// -- // --
// k8s.io/kubernetes/pkg/apis/admission/v1alpha1
gvr("admission.k8s.io", "v1alpha1", "admissionreviews"), // not stored in etcd, call out to webhooks.
// --
) )
// Only add kinds to this list when there is no mapping from GVK to GVR (and thus there is no way to create the object) // Only add kinds to this list when there is no mapping from GVK to GVR (and thus there is no way to create the object)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment