diff --git a/cmd/kube-apiserver/app/BUILD b/cmd/kube-apiserver/app/BUILD index d42de560cca..ed1ca4bbd8a 100644 --- a/cmd/kube-apiserver/app/BUILD +++ b/cmd/kube-apiserver/app/BUILD @@ -66,6 +66,7 @@ go_library( "//plugin/pkg/admission/securitycontext/scdeny:go_default_library", "//plugin/pkg/admission/serviceaccount:go_default_library", "//plugin/pkg/admission/storageclass/default:go_default_library", + "//plugin/pkg/admission/webhook:go_default_library", "//plugin/pkg/auth/authenticator/token/bootstrap:go_default_library", "//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/golang/glog:go_default_library", diff --git a/cmd/kube-apiserver/app/options/options.go b/cmd/kube-apiserver/app/options/options.go index 99ca76f92e8..edc844d5bb6 100644 --- a/cmd/kube-apiserver/app/options/options.go +++ b/cmd/kube-apiserver/app/options/options.go @@ -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 "+ "after we fix all the clients that send malformed updates.") - 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") - fs.StringVar(&s.ProxyClientKeyFile, "proxy-client-key-file", s.ProxyClientKeyFile, - "client certificate key used to prove the identity of the aggragator or kube-apiserver when it proxies requests to a user api-server") + fs.StringVar(&s.ProxyClientCertFile, "proxy-client-cert-file", s.ProxyClientCertFile, ""+ + "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. 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, "Turns on aggregator routing requests to endoints IP rather than cluster IP.") diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index f287a9a47d0..6818b28ddfa 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -47,6 +47,7 @@ import ( "k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny" "k8s.io/kubernetes/plugin/pkg/admission/serviceaccount" storagedefault "k8s.io/kubernetes/plugin/pkg/admission/storageclass/default" + "k8s.io/kubernetes/plugin/pkg/admission/webhook" ) // registerAllAdmissionPlugins registers all admission plugins @@ -73,4 +74,5 @@ func registerAllAdmissionPlugins(plugins *admission.Plugins) { scdeny.Register(plugins) serviceaccount.Register(plugins) storagedefault.Register(plugins) + webhook.Register(plugins) } diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index f827d4677ef..081abddc66f 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -425,6 +425,20 @@ func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client interna quotaRegistry := quotainstall.NewRegistry(nil, nil) 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 } diff --git a/hack/.linted_packages b/hack/.linted_packages index 47eec988f4f..59299ce5536 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -68,6 +68,7 @@ pkg/api/v1/node pkg/api/v1/service pkg/apis/abac/v0 pkg/apis/abac/v1beta1 +pkg/apis/admission/install pkg/apis/admissionregistration/install pkg/apis/apps/install pkg/apis/apps/v1beta1 diff --git a/hack/lib/init.sh b/hack/lib/init.sh index 41967e1c8ea..02aba9eeddd 100755 --- a/hack/lib/init.sh +++ b/hack/lib/init.sh @@ -53,6 +53,7 @@ KUBE_OUTPUT_HOSTBIN="${KUBE_OUTPUT_BINPATH}/$(kube::util::host_platform)" KUBE_AVAILABLE_GROUP_VERSIONS="${KUBE_AVAILABLE_GROUP_VERSIONS:-\ v1 \ admissionregistration.k8s.io/v1alpha1 \ +admission.k8s.io/v1alpha1 \ apps/v1beta1 \ authentication.k8s.io/v1 \ authentication.k8s.io/v1beta1 \ diff --git a/pkg/api/testapi/BUILD b/pkg/api/testapi/BUILD index 48167f98db2..fdaa4ac3733 100644 --- a/pkg/api/testapi/BUILD +++ b/pkg/api/testapi/BUILD @@ -17,6 +17,8 @@ go_library( "//federation/apis/federation/install:go_default_library", "//pkg/api:go_default_library", "//pkg/api/install:go_default_library", + "//pkg/apis/admission:go_default_library", + "//pkg/apis/admission/install:go_default_library", "//pkg/apis/admissionregistration:go_default_library", "//pkg/apis/admissionregistration/install:go_default_library", "//pkg/apis/apps:go_default_library", diff --git a/pkg/api/testapi/testapi.go b/pkg/api/testapi/testapi.go index d36efa95670..3a08f575bf9 100644 --- a/pkg/api/testapi/testapi.go +++ b/pkg/api/testapi/testapi.go @@ -36,6 +36,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer/recognizer" "k8s.io/kubernetes/federation/apis/federation" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/admission" "k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/authorization" @@ -52,6 +53,7 @@ import ( _ "k8s.io/kubernetes/federation/apis/federation/install" _ "k8s.io/kubernetes/pkg/api/install" + _ "k8s.io/kubernetes/pkg/apis/admission/install" _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" _ "k8s.io/kubernetes/pkg/apis/apps/install" _ "k8s.io/kubernetes/pkg/apis/authentication/install" @@ -84,6 +86,7 @@ var ( Settings TestGroup Storage TestGroup ImagePolicy TestGroup + Admission TestGroup Networking TestGroup serializer runtime.SerializerInfo @@ -292,6 +295,15 @@ func init() { externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } + if _, ok := Groups[admission.GroupName]; !ok { + externalGroupVersion := schema.GroupVersion{Group: admission.GroupName, Version: api.Registry.GroupOrDie(admission.GroupName).GroupVersion.Version} + Groups[admission.GroupName] = TestGroup{ + externalGroupVersion: externalGroupVersion, + internalGroupVersion: admission.SchemeGroupVersion, + internalTypes: api.Scheme.KnownTypes(admission.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), + } + } if _, ok := Groups[networking.GroupName]; !ok { externalGroupVersion := schema.GroupVersion{Group: networking.GroupName, Version: api.Registry.GroupOrDie(networking.GroupName).GroupVersion.Version} Groups[networking.GroupName] = TestGroup{ @@ -315,6 +327,7 @@ func init() { Storage = Groups[storage.GroupName] ImagePolicy = Groups[imagepolicy.GroupName] Authorization = Groups[authorization.GroupName] + Admission = Groups[admission.GroupName] Networking = Groups[networking.GroupName] } diff --git a/pkg/apis/admission/BUILD b/pkg/apis/admission/BUILD index 056cfe31de1..9c2b8d797e2 100644 --- a/pkg/apis/admission/BUILD +++ b/pkg/apis/admission/BUILD @@ -40,6 +40,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//pkg/apis/admission/install:all-srcs", "//pkg/apis/admission/v1alpha1:all-srcs", ], tags = ["automanaged"], diff --git a/pkg/apis/admission/install/BUILD b/pkg/apis/admission/install/BUILD new file mode 100644 index 00000000000..72644e603c7 --- /dev/null +++ b/pkg/apis/admission/install/BUILD @@ -0,0 +1,36 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = ["install.go"], + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/apis/admission:go_default_library", + "//pkg/apis/admission/v1alpha1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/apis/admission/install/install.go b/pkg/apis/admission/install/install.go new file mode 100644 index 00000000000..fca70e225e0 --- /dev/null +++ b/pkg/apis/admission/install/install.go @@ -0,0 +1,51 @@ +/* +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 install installs the experimental API group, making it available as +// an option to all of the API encoding/decoding machinery. +package install + +import ( + "k8s.io/apimachinery/pkg/apimachinery/announced" + "k8s.io/apimachinery/pkg/apimachinery/registered" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/admission" + "k8s.io/kubernetes/pkg/apis/admission/v1alpha1" +) + +func init() { + Install(api.GroupFactoryRegistry, api.Registry, api.Scheme) +} + +// Install registers the API group and adds types to a scheme +func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) { + if err := announced.NewGroupMetaFactory( + &announced.GroupMetaFactoryArgs{ + GroupName: admission.GroupName, + VersionPreferenceOrder: []string{v1alpha1.SchemeGroupVersion.Version}, + ImportPrefix: "k8s.io/kubernetes/pkg/apis/admission", + RootScopedKinds: sets.NewString("AdmissionReview"), + AddInternalObjectsToScheme: admission.AddToScheme, + }, + announced.VersionToSchemeFunc{ + v1alpha1.SchemeGroupVersion.Version: v1alpha1.AddToScheme, + }, + ).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil { + panic(err) + } +} diff --git a/pkg/apis/admission/v1alpha1/BUILD b/pkg/apis/admission/v1alpha1/BUILD index e35d110ae9a..867a5d3a3c9 100644 --- a/pkg/apis/admission/v1alpha1/BUILD +++ b/pkg/apis/admission/v1alpha1/BUILD @@ -16,6 +16,7 @@ go_library( "register.go", "types.generated.go", "types.go", + "types_swagger_doc_generated.go", "zz_generated.conversion.go", "zz_generated.deepcopy.go", "zz_generated.defaults.go", diff --git a/pkg/apis/admission/v1alpha1/types.generated.go b/pkg/apis/admission/v1alpha1/types.generated.go index 01fd30cd7e9..253ed58c49b 100644 --- a/pkg/apis/admission/v1alpha1/types.generated.go +++ b/pkg/apis/admission/v1alpha1/types.generated.go @@ -92,12 +92,13 @@ func (x *AdmissionReview) CodecEncodeSelf(e *codec1978.Encoder) { const yyr2 bool = false yyq2[0] = x.Kind != "" yyq2[1] = x.APIVersion != "" + yyq2[2] = true yyq2[3] = true var yynn2 int if yyr2 || yy2arr2 { r.EncodeArrayStart(4) } else { - yynn2 = 1 + yynn2 = 0 for _, b := range yyq2 { if b { yynn2++ @@ -158,14 +159,20 @@ func (x *AdmissionReview) CodecEncodeSelf(e *codec1978.Encoder) { } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayElem1234) - yy10 := &x.Spec - yy10.CodecEncodeSelf(e) + if yyq2[2] { + yy10 := &x.Spec + yy10.CodecEncodeSelf(e) + } else { + r.EncodeNil() + } } else { - z.EncSendContainerState(codecSelfer_containerMapKey1234) - r.EncodeString(codecSelferC_UTF81234, string("spec")) - z.EncSendContainerState(codecSelfer_containerMapValue1234) - yy12 := &x.Spec - yy12.CodecEncodeSelf(e) + if yyq2[2] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("spec")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + yy12 := &x.Spec + yy12.CodecEncodeSelf(e) + } } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayElem1234) diff --git a/pkg/apis/admission/v1alpha1/types.go b/pkg/apis/admission/v1alpha1/types.go index 453b7843632..64e156c22fa 100644 --- a/pkg/apis/admission/v1alpha1/types.go +++ b/pkg/apis/admission/v1alpha1/types.go @@ -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 // cost of deserializing it. // +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. // +optional Status AdmissionReviewStatus `json:"status,omitempty" protobuf:"bytes,2,opt,name=status"` diff --git a/pkg/apis/admission/v1alpha1/types_swagger_doc_generated.go b/pkg/apis/admission/v1alpha1/types_swagger_doc_generated.go new file mode 100644 index 00000000000..07da434c98d --- /dev/null +++ b/pkg/apis/admission/v1alpha1/types_swagger_doc_generated.go @@ -0,0 +1,67 @@ +/* +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 v1alpha1 + +// This file contains a collection of methods that can be used from go-restful to +// generate Swagger API documentation for its models. Please read this PR for more +// information on the implementation: https://github.com/emicklei/go-restful/pull/215 +// +// TODOs are ignored from the parser (e.g. TODO(andronat):... || TODO:...) if and only if +// they are on one line! For multiple line or blocks that you want to ignore use ---. +// Any context after a --- is ignored. +// +// Those methods can be generated by using hack/update-generated-swagger-docs.sh + +// AUTO-GENERATED FUNCTIONS START HERE +var map_AdmissionReview = map[string]string{ + "": "AdmissionReview describes an admission request.", + "spec": "Spec describes the attributes for the admission request. Since this admission controller is non-mutating the webhook should avoid setting this in its response to avoid the cost of deserializing it.", + "status": "Status is filled in by the webhook and indicates whether the admission request should be permitted.", +} + +func (AdmissionReview) SwaggerDoc() map[string]string { + return map_AdmissionReview +} + +var map_AdmissionReviewSpec = map[string]string{ + "": "AdmissionReviewSpec describes the admission.Attributes for the admission request.", + "kind": "Kind is the type of object being manipulated. For example: Pod", + "object": "Object is the object from the incoming request prior to default values being applied", + "oldObject": "OldObject is the existing object. Only populated for UPDATE requests.", + "operation": "Operation is the operation being performed", + "name": "Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and rely on the server to generate the name. If that is the case, this method will return the empty string.", + "namespace": "Namespace is the namespace associated with the request (if any).", + "resource": "Resource is the name of the resource being requested. This is not the kind. For example: pods", + "subResource": "SubResource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind. For instance, /pods has the resource \"pods\" and the kind \"Pod\", while /pods/foo/status has the resource \"pods\", the sub resource \"status\", and the kind \"Pod\" (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource \"pods\", subresource \"binding\", and kind \"Binding\".", + "userInfo": "UserInfo is information about the requesting user", +} + +func (AdmissionReviewSpec) SwaggerDoc() map[string]string { + return map_AdmissionReviewSpec +} + +var map_AdmissionReviewStatus = map[string]string{ + "": "AdmissionReviewStatus describes the status of the admission request.", + "allowed": "Allowed indicates whether or not the admission request was permitted.", + "status": "Result contains extra details into why an admission request was denied. This field IS NOT consulted in any way if \"Allowed\" is \"true\".", +} + +func (AdmissionReviewStatus) SwaggerDoc() map[string]string { + return map_AdmissionReviewStatus +} + +// AUTO-GENERATED FUNCTIONS END HERE diff --git a/pkg/kubeapiserver/admission/BUILD b/pkg/kubeapiserver/admission/BUILD index a55bf4ec166..8b796b26016 100644 --- a/pkg/kubeapiserver/admission/BUILD +++ b/pkg/kubeapiserver/admission/BUILD @@ -14,6 +14,7 @@ go_test( library = ":go_default_library", tags = ["automanaged"], deps = [ + "//pkg/apis/admissionregistration:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", "//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", ], @@ -24,6 +25,7 @@ go_library( srcs = ["initializer.go"], tags = ["automanaged"], deps = [ + "//pkg/apis/admissionregistration:go_default_library", "//pkg/client/clientset_generated/internalclientset:go_default_library", "//pkg/client/informers/informers_generated/internalversion:go_default_library", "//pkg/quota:go_default_library", diff --git a/pkg/kubeapiserver/admission/init_test.go b/pkg/kubeapiserver/admission/init_test.go index 27440f6c4a3..7176f14a72b 100644 --- a/pkg/kubeapiserver/admission/init_test.go +++ b/pkg/kubeapiserver/admission/init_test.go @@ -17,10 +17,12 @@ limitations under the License. package admission import ( + "net/url" "testing" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/kubernetes/pkg/apis/admissionregistration" ) // 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 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 // interface. type WantAuthorizerAdmission struct { + doNothingAdmission auth authorizer.Authorizer } func (self *WantAuthorizerAdmission) SetAuthorizer(a authorizer.Authorizer) { 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 _ WantsAuthorizer = &WantAuthorizerAdmission{} @@ -60,6 +66,7 @@ func TestWantsAuthorizer(t *testing.T) { } type WantsCloudConfigAdmissionPlugin struct { + doNothingAdmission cloudConfig []byte } @@ -67,10 +74,6 @@ func (self *WantsCloudConfigAdmissionPlugin) SetCloudConfig(cloudConfig []byte) 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) { cloudConfig := []byte("cloud-configuration") initializer := NewPluginInitializer(nil, nil, &TestAuthorizer{}, cloudConfig, nil, nil) @@ -81,3 +84,65 @@ func TestCloudConfigAdmissionPlugin(t *testing.T) { 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) + } +} diff --git a/pkg/kubeapiserver/admission/initializer.go b/pkg/kubeapiserver/admission/initializer.go index 540029045e9..be56e6efdd6 100644 --- a/pkg/kubeapiserver/admission/initializer.go +++ b/pkg/kubeapiserver/admission/initializer.go @@ -17,9 +17,12 @@ limitations under the License. package admission import ( + "net/url" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" "k8s.io/kubernetes/pkg/quota" @@ -61,25 +64,62 @@ type WantsQuotaRegistry interface { admission.Validator } -type pluginInitializer struct { - internalClient internalclientset.Interface - informers informers.SharedInformerFactory - authorizer authorizer.Authorizer - cloudConfig []byte - restMapper meta.RESTMapper - quotaRegistry quota.Registry +// WantsServiceResolver defines a fuction that accepts a ServiceResolver for +// admission plugins that need to make calls to services. +type WantsServiceResolver interface { + SetServiceResolver(ServiceResolver) } -var _ admission.PluginInitializer = pluginInitializer{} +// 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) +} + +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 +// 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, sharedInformers informers.SharedInformerFactory, authz authorizer.Authorizer, cloudConfig []byte, restMapper meta.RESTMapper, - quotaRegistry quota.Registry) admission.PluginInitializer { - return pluginInitializer{ + quotaRegistry quota.Registry) *PluginInitializer { + return &PluginInitializer{ internalClient: internalClient, informers: sharedInformers, authorizer: authz, @@ -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 // 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 { wants.SetInternalKubeClientSet(i.internalClient) } @@ -115,4 +176,25 @@ func (i pluginInitializer) Initialize(plugin admission.Interface) { if wants, ok := plugin.(WantsQuotaRegistry); ok { 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) + } } diff --git a/pkg/kubectl/cmd/apply_test.go b/pkg/kubectl/cmd/apply_test.go index 75a050371e3..838624f38ba 100644 --- a/pkg/kubectl/cmd/apply_test.go +++ b/pkg/kubectl/cmd/apply_test.go @@ -986,9 +986,8 @@ func TestRunApplySetLastApplied(t *testing.T) { if 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) { diff --git a/pkg/kubectl/cmd/create_deployment_test.go b/pkg/kubectl/cmd/create_deployment_test.go index 067f7e650f1..9dd6720b785 100644 --- a/pkg/kubectl/cmd/create_deployment_test.go +++ b/pkg/kubectl/cmd/create_deployment_test.go @@ -39,7 +39,7 @@ func TestCreateDeployment(t *testing.T) { Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, - Body: ioutil.NopCloser(&bytes.Buffer{}), + Body: ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))), }, nil }), } diff --git a/pkg/kubectl/cmd/run_test.go b/pkg/kubectl/cmd/run_test.go index 7f1eed8eee6..e922e70dbef 100644 --- a/pkg/kubectl/cmd/run_test.go +++ b/pkg/kubectl/cmd/run_test.go @@ -171,7 +171,7 @@ func TestRunArgsFollowDashRules(t *testing.T) { } else { return &http.Response{ StatusCode: http.StatusOK, - Body: ioutil.NopCloser(&bytes.Buffer{}), + Body: ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))), }, nil } }), diff --git a/pkg/master/BUILD b/pkg/master/BUILD index c8e62335dee..4b983f83ab9 100644 --- a/pkg/master/BUILD +++ b/pkg/master/BUILD @@ -25,6 +25,7 @@ go_library( "//pkg/api/endpoints:go_default_library", "//pkg/api/install:go_default_library", "//pkg/api/v1:go_default_library", + "//pkg/apis/admission/install:go_default_library", "//pkg/apis/admissionregistration/install:go_default_library", "//pkg/apis/admissionregistration/v1alpha1:go_default_library", "//pkg/apis/apps/install:go_default_library", diff --git a/pkg/master/import_known_versions.go b/pkg/master/import_known_versions.go index 076b9889372..5176dba5638 100644 --- a/pkg/master/import_known_versions.go +++ b/pkg/master/import_known_versions.go @@ -22,6 +22,7 @@ import ( "k8s.io/kubernetes/pkg/api" _ "k8s.io/kubernetes/pkg/api/install" + _ "k8s.io/kubernetes/pkg/apis/admission/install" _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" _ "k8s.io/kubernetes/pkg/apis/apps/install" _ "k8s.io/kubernetes/pkg/apis/authentication/install" diff --git a/pkg/master/import_known_versions_test.go b/pkg/master/import_known_versions_test.go index 49764685f6a..5047efaf42c 100644 --- a/pkg/master/import_known_versions_test.go +++ b/pkg/master/import_known_versions_test.go @@ -85,6 +85,7 @@ var typesAllowedTags = map[reflect.Type]bool{ reflect.TypeOf(metav1.DeleteOptions{}): true, reflect.TypeOf(metav1.GroupVersionKind{}): 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) { diff --git a/plugin/BUILD b/plugin/BUILD index 2af6b49bbce..56e40f49a77 100644 --- a/plugin/BUILD +++ b/plugin/BUILD @@ -36,6 +36,7 @@ filegroup( "//plugin/pkg/admission/securitycontext/scdeny:all-srcs", "//plugin/pkg/admission/serviceaccount:all-srcs", "//plugin/pkg/admission/storageclass/default:all-srcs", + "//plugin/pkg/admission/webhook:all-srcs", "//plugin/pkg/auth:all-srcs", "//plugin/pkg/scheduler:all-srcs", ], diff --git a/plugin/pkg/admission/webhook/BUILD b/plugin/pkg/admission/webhook/BUILD new file mode 100644 index 00000000000..a6466fffa00 --- /dev/null +++ b/plugin/pkg/admission/webhook/BUILD @@ -0,0 +1,68 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = [ + "admission_test.go", + "certs_test.go", + "rules_test.go", + ], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/apis/admission/install:go_default_library", + "//pkg/apis/admission/v1alpha1:go_default_library", + "//pkg/apis/admissionregistration:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + ], +) + +go_library( + name = "go_default_library", + srcs = [ + "admission.go", + "doc.go", + "rules.go", + ], + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/apis/admission/install:go_default_library", + "//pkg/apis/admission/v1alpha1:go_default_library", + "//pkg/apis/admissionregistration:go_default_library", + "//pkg/kubeapiserver/admission:go_default_library", + "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/errors: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/apimachinery/pkg/util/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", + "//vendor/k8s.io/client-go/rest:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/plugin/pkg/admission/webhook/admission.go b/plugin/pkg/admission/webhook/admission.go new file mode 100644 index 00000000000..7575d7aed80 --- /dev/null +++ b/plugin/pkg/admission/webhook/admission.go @@ -0,0 +1,222 @@ +/* +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 delegates admission checks to dynamically configured webhooks. +package webhook + +import ( + "context" + "fmt" + "io" + "sync" + "time" + + "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/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/admission" + "k8s.io/client-go/rest" + "k8s.io/kubernetes/pkg/api" + admissionv1alpha1 "k8s.io/kubernetes/pkg/apis/admission/v1alpha1" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + admissioninit "k8s.io/kubernetes/pkg/kubeapiserver/admission" + + // install the clientgo admission API for use with api registry + _ "k8s.io/kubernetes/pkg/apis/admission/install" +) + +var ( + groupVersions = []schema.GroupVersion{ + admissionv1alpha1.SchemeGroupVersion, + } +) + +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 +func Register(plugins *admission.Plugins) { + plugins.Register("GenericAdmissionWebhook", func(configFile io.Reader) (admission.Interface, error) { + plugin, err := NewGenericAdmissionWebhook() + if err != nil { + return nil, err + } + + return plugin, nil + }) +} + +// NewGenericAdmissionWebhook returns a generic admission webhook plugin. +func NewGenericAdmissionWebhook() (*GenericAdmissionWebhook, error) { + return &GenericAdmissionWebhook{ + Handler: admission.NewHandler( + admission.Connect, + admission.Create, + admission.Delete, + admission.Update, + ), + negotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{ + Serializer: api.Codecs.LegacyCodec(admissionv1alpha1.SchemeGroupVersion), + }), + }, nil +} + +// GenericAdmissionWebhook is an implementation of admission.Interface. +type GenericAdmissionWebhook struct { + *admission.Handler + hookSource admissioninit.WebhookSource + 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. +func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error { + hooks, err := a.hookSource.List() + if err != nil { + return fmt.Errorf("failed listing hooks: %v", err) + } + ctx := context.TODO() + + errCh := make(chan error, len(hooks)) + 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) + + var errs []error + for e := range errCh { + errs = append(errs, e) + } + if len(errs) == 0 { + 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] +} + +func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *admissionregistration.ExternalAdmissionHook, attr admission.Attributes) error { + matches := false + for _, r := range h.Rules { + m := RuleMatcher{Rule: r, Attr: attr} + if m.Matches() { + matches = true + break + } + } + if !matches { + return nil + } + + // Make the webhook request + request := admissionv1alpha1.NewAdmissionReview(attr) + client, err := a.hookClient(h) + if err != nil { + return &ErrCallingWebhook{WebhookName: h.Name, Reason: err} + } + if err := client.Post().Context(ctx).Body(&request).Do().Into(&request); err != nil { + return &ErrCallingWebhook{WebhookName: h.Name, Reason: err} + } + + if request.Status.Allowed { + return nil + } + + if request.Status.Result == nil { + return fmt.Errorf("admission webhook %q denied the request without explanation", h.Name) + } + + return &apierrors.StatusError{ + ErrStatus: *request.Status.Result, + } +} + +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 { + return nil, err + } + + // 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) +} diff --git a/plugin/pkg/admission/webhook/admission_test.go b/plugin/pkg/admission/webhook/admission_test.go new file mode 100644 index 00000000000..15f331f6510 --- /dev/null +++ b/plugin/pkg/admission/webhook/admission_test.go @@ -0,0 +1,263 @@ +/* +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 ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/admission/v1alpha1" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + + _ "k8s.io/kubernetes/pkg/apis/admission/install" +) + +type fakeHookSource struct { + hooks []admissionregistration.ExternalAdmissionHook + err error +} + +func (f *fakeHookSource) List() ([]admissionregistration.ExternalAdmissionHook, error) { + if f.err != nil { + return nil, f.err + } + return f.hooks, nil +} + +type fakeServiceResolver struct { + base url.URL +} + +func (f fakeServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) { + if namespace == "failResolve" { + return nil, fmt.Errorf("couldn't resolve service location") + } + u := f.base + u.Path = name + return &u, nil +} + +// TestAdmit tests that GenericAdmissionWebhook#Admit works as expected +func TestAdmit(t *testing.T) { + // Create the test webhook server + sCert, err := tls.X509KeyPair(serverCert, serverKey) + if err != nil { + t.Fatal(err) + } + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(caCert) + testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler)) + testServer.TLS = &tls.Config{ + Certificates: []tls.Certificate{sCert}, + ClientCAs: rootCAs, + ClientAuth: tls.RequireAndVerifyClientCert, + } + testServer.StartTLS() + defer testServer.Close() + serverURL, err := url.ParseRequestURI(testServer.URL) + if err != nil { + t.Fatalf("this should never happen? %v", err) + } + wh, err := NewGenericAdmissionWebhook() + if err != nil { + t.Fatal(err) + } + wh.serviceResolver = fakeServiceResolver{*serverURL} + wh.clientCert = clientCert + wh.clientKey = clientKey + + // Set up a test object for the call + kind := api.Kind("Pod").WithVersion("v1") + name := "my-pod" + namespace := "webhook-test" + object := api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "pod.name": name, + }, + Name: name, + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + } + oldObject := api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, + } + operation := admission.Update + resource := api.Resource("pods").WithVersion("v1") + subResource := "" + userInfo := user.DefaultInfo{ + Name: "webhook-test", + UID: "webhook-test", + } + + type test struct { + hookSource fakeHookSource + expectAllow bool + errorContains string + } + ccfg := func(result string) admissionregistration.AdmissionHookClientConfig { + return admissionregistration.AdmissionHookClientConfig{ + Service: admissionregistration.ServiceReference{ + Name: result, + }, + CABundle: caCert, + } + } + matchEverythingRules := []admissionregistration.RuleWithOperations{{ + Operations: []admissionregistration.OperationType{admissionregistration.OperationAll}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"*"}, + APIVersions: []string{"*"}, + Resources: []string{"*/*"}, + }, + }} + + table := map[string]test{ + "no match": { + hookSource: fakeHookSource{ + hooks: []admissionregistration.ExternalAdmissionHook{{ + Name: "nomatch", + ClientConfig: ccfg("disallow"), + Rules: []admissionregistration.RuleWithOperations{{ + Operations: []admissionregistration.OperationType{admissionregistration.Create}, + }}, + }}, + }, + expectAllow: true, + }, + "match & allow": { + hookSource: fakeHookSource{ + hooks: []admissionregistration.ExternalAdmissionHook{{ + Name: "allow", + ClientConfig: ccfg("allow"), + Rules: matchEverythingRules, + }}, + }, + expectAllow: true, + }, + "match & disallow": { + hookSource: fakeHookSource{ + hooks: []admissionregistration.ExternalAdmissionHook{{ + Name: "disallow", + ClientConfig: ccfg("disallow"), + Rules: matchEverythingRules, + }}, + }, + errorContains: "without explanation", + }, + "match & disallow ii": { + hookSource: fakeHookSource{ + hooks: []admissionregistration.ExternalAdmissionHook{{ + Name: "disallowReason", + ClientConfig: ccfg("disallowReason"), + Rules: matchEverythingRules, + }}, + }, + errorContains: "you shall not pass", + }, + "match & fail (but allow because fail open)": { + hookSource: fakeHookSource{ + hooks: []admissionregistration.ExternalAdmissionHook{{ + Name: "internalErr A", + ClientConfig: ccfg("internalErr"), + Rules: matchEverythingRules, + }, { + Name: "invalidReq B", + ClientConfig: ccfg("invalidReq"), + Rules: matchEverythingRules, + }, { + Name: "invalidResp C", + ClientConfig: ccfg("invalidResp"), + Rules: matchEverythingRules, + }}, + }, + expectAllow: true, + }, + } + + for name, tt := range table { + wh.hookSource = &tt.hookSource + + err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, name, resource, subResource, operation, &userInfo)) + if tt.expectAllow != (err == nil) { + t.Errorf("%q: expected allowed=%v, but got err=%v", name, tt.expectAllow, err) + } + // ErrWebhookRejected is not an error for our purposes + if tt.errorContains != "" { + if err == nil || !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("%q: expected an error saying %q, but got %v", name, tt.errorContains, err) + } + } + } +} + +func webhookHandler(w http.ResponseWriter, r *http.Request) { + fmt.Printf("got req: %v\n", r.URL.Path) + switch r.URL.Path { + case "/internalErr": + http.Error(w, "webhook internal server error", http.StatusInternalServerError) + return + case "/invalidReq": + w.WriteHeader(http.StatusSwitchingProtocols) + w.Write([]byte("webhook invalid request")) + return + case "/invalidResp": + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("webhook invalid response")) + case "/disallow": + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{ + Status: v1alpha1.AdmissionReviewStatus{ + Allowed: false, + }, + }) + case "/disallowReason": + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{ + Status: v1alpha1.AdmissionReviewStatus{ + Allowed: false, + Result: &metav1.Status{ + Message: "you shall not pass", + }, + }, + }) + case "/allow": + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{ + Status: v1alpha1.AdmissionReviewStatus{ + Allowed: true, + }, + }) + default: + http.NotFound(w, r) + } +} diff --git a/plugin/pkg/admission/webhook/certs_test.go b/plugin/pkg/admission/webhook/certs_test.go new file mode 100644 index 00000000000..db1e07be026 --- /dev/null +++ b/plugin/pkg/admission/webhook/certs_test.go @@ -0,0 +1,218 @@ +/* +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. +*/ + +// This file was generated using openssl by the gencerts.sh script +// and holds raw certificates for the webhook tests. + +package webhook + +var caKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5qdHpJR3sAg1afEWLlnxdbDV1LtAvCd9WWE/4g71+BBwvwLL +C/piPRi+m/7AYHJ9zoVwvwAUYBJM2ppQV2Gwsq4XA5dLvr8fXt4+YA/sLTAo0PAm +/QQmZziHU9v7OJ04ypjTTuA86D/EkkpEP7lRkN/NFpV3PhU0F5TTrkrzSpePBIR1 +aPSzh/6Um6TtFk9oiqat05leWcMzonizrgeQU9EW6bSYY5w+gq1X2eZb1s9eR98H +0xcZfoh3qHmJ1Iq9Cc2Nr+MUohm2m45ozT+1L+g46ZMJxyPB6xLr7uOhkoJsK30q +67AZKPo58tDTmEKSXfBotIvFI5N9P3sAWxcTiQIDAQABAoIBAAaJHOWT82RAh0ru +MuOzVr0v+o8hky8Bq3KZ59Z++AdEZ/1xldFMEfaLOfNvn4HcHKZ6b3xqAynJuvXC +w54GPZyChFJsug+4mKn2gCv2p4mMQMvS0jf/IxtvpZ4BsLek9NQAypQElJU8IVTH +1/E6Tg5d2RDXwV43+Zbld64Ln6MwZGwv8UFPEHylDMjwkex5u3tzVBD5NaegI0MD +AHAf3fiCsANmAeGWjTvUXQsOes6wjaHw6kbih5QrXM6iThHfU/YHXYmgfdfSSIFC +4puLaehp3/U8HcI97xN238B0khnkOVHzUmRJpmf17SWFSkOAZyUMiFfTSFSOedvu +lFO7v0UCgYEA/FczxADDZXq4SSTvrU61XzolgI//OKblsqe4RW8JGtk8pwjZdYOQ +v4UAYEcv5rUWA3wWohcjgNWzI9EzdhOCYpC9YFqbHJalmwhGgOjRCvDy581pcXz7 +xsfkm2loWm3g+PcrCIset2tGQw/5gqSkBW42E/U0ba25wCS6RlYWCFsCgYEA6f+Q +vENXPmBUBW8TsyjWj7MZKVzKuV2yyKT37Mf7RmrpSNpft0PLJDqGqRMfhe1J7Adm +Np1fv/18RngjGjV3fSnjbvQ51748gabwzGZKWKiWJPsRDqjEriiQcFInhaqVJs7F +D0TaWalBHWyjPHCQQx7rWqA8tr3Wpga0AhNUuOsCgYEAnbrQU7L6cEM+UBIzcswh +GO4apPrdWIcSSxMFXvlh4pNpkys36nmbj+tN6eB1c6s7oF//McBu48gwWrIYjbTy +KjQ4+7KHBF6yE28fytI8YK9t1jESuOqb4ovuPKqtnODT4Ct3jbaQM6xtVdv1ZZEO +KYrTaLQ72lbeJdmPSgnjacMCgYASip6kXE2ocqeVuqR7+MtvnYhr359sqsEE5xWC +HKKLhOMxU6Rr+CI7n6uV8B76VMAbxMZTo4q3wtU7HD/jzsLGFzCfVRjUQI241EqW +V7Cib9Fd4ssKN1NGXY58Z/YbwFWLOq0gtZr7qc6wDzCsFFtKBkQt7S6CaG5+v186 +HuACuwKBgEd0JaREFj6AG6boytQAx+Npj+wGG7K22O8v9YslJjaS5t2i8XrvIr0F +5ltR8Ijegp9a2pCgjshaEzUqMHhxX3EGvUxVM4R430EaQ933WRPmiLlVLyhthYt0 +9oPxMoN783J7UP/IkBc6AGi3a4uTn/h6Vi884wOLop4bmsK37uIt +-----END RSA PRIVATE KEY-----`) + +var caCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgIJAIf67NAEFfGmMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV +BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX +DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA0MTIwMAYDVQQDFClnZW5l +cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAOanR6SUd7AINWnxFi5Z8XWw1dS7QLwnfVlh +P+IO9fgQcL8Cywv6Yj0Yvpv+wGByfc6FcL8AFGASTNqaUFdhsLKuFwOXS76/H17e +PmAP7C0wKNDwJv0EJmc4h1Pb+zidOMqY007gPOg/xJJKRD+5UZDfzRaVdz4VNBeU +065K80qXjwSEdWj0s4f+lJuk7RZPaIqmrdOZXlnDM6J4s64HkFPRFum0mGOcPoKt +V9nmW9bPXkffB9MXGX6Id6h5idSKvQnNja/jFKIZtpuOaM0/tS/oOOmTCccjwesS +6+7joZKCbCt9KuuwGSj6OfLQ05hCkl3waLSLxSOTfT97AFsXE4kCAwEAAaOBljCB +kzAdBgNVHQ4EFgQU55hOE1Dsydy16+6wgOxwsPKZ8JEwZAYDVR0jBF0wW4AU55hO +E1Dsydy16+6wgOxwsPKZ8JGhOKQ2MDQxMjAwBgNVBAMUKWdlbmVyaWNfd2ViaG9v +a19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhggkAh/rs0AQV8aYwDAYDVR0TBAUw +AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAtEa7AMnFdc/GmVrtY0aOmA3h1WJ/rhhd +SnVOp7LmA+1jnA6FMMvVCOFflYLfBwLhIPjPj4arQadWHyd5Quok6GIqgzL4RkK7 +67hPMc8inNH8w+9K0kgsCls06Jy08NIt7QTtIckh8skvxQsfJ6An/ROiCNI5QiYj +oQOK3Tp8jGf/2wcsJLeO9y09ZPcOUbLkDe2YlnT+OqNMx9VXrPSvRwq2qtYphgYW +YWHPMEBnB/9XrVkEtnKeWPjtjarVd+rNnfVpCW0ImnZRFtKQQeI4rtf4Gr83Gdju +Z0gPepfIFptDMl5wKWyw4o2XVSZ69Ur8tQQynoNyX/FTIx6tQ//hzg== +-----END CERTIFICATE-----`) + +var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA2Mbqfl5nW+rYCSw/+G00ymtBElvTwzEI88sg3m5PXU0XMv5y +Tyjt+S3mhbzLY460ty6fXlEiveNNP0bzbo0k+tvUj7HbOG4q4Cs0JWiLvibchRXQ +LimqtndsVsT2xj56m7oTETWMYhjd8KFz1s6w7yK9dChe2pBCeeGjIbQs0def8Bf0 +5h7BbsmuXQb44nnpUdWgFI/Frqcz6jO6jySYV5U3ajqCjcWOBQmXRtMi1we48Hdj +2t4jW/evbRIo/tX88kluXFSVBmTitKA3nn23AzA+/qa6kTDwzQ8OntjC09JT7ykS +764WLRiBOo+wC3a6M3eZNMkW615+DNP7wAjYEwIDAQABAoIBAC9lZnXUvDKLqUpw +I1h0wBsV0jdqXmWJ/hQXsIsRgUa8CTt8CJAoOcfGcmWBPtL4q6h1iCC+CqOL5CLW +p3jfYVt73wC/+VdgNv2mVJNtRUiBBKwQdeDx+UJF4CkkjXQQywvrZinYFGaKW1Q2 +aLZpoKPYa6XPAdY1vmMZo2pGE5qZbGbIXKcl3kl5N2X0qyJKvTR6RA1Q8JiTmATh +H1U3S9K/MIGU+9OwP8zCDVKFdDI+xgJvDo05W8bt6XLfmE4bLRYHRfe0iE0uBNSC +zx5OGBNqkiHq/vMk5mLCYM0w/uzUkhB5uXqe5gSqa7DioLrHztqAe+sbtBvfL+Yd +hP3asWECgYEA9NQXa2B6AVXVf2GII67H8diPFrILKLEw4CUZjFmVbvrKIRB2ypqA +IsohpYl97s0nstetZt/75uEvkv3RPu5ad5LJaVAPHE3ESXKdAz6u4fSmi33qqPi6 +PvhHLYDDXvMgC7j2yCYyANVKNg2T+EJvpWMISlXM4h7CVm21CSDVZjECgYEA4qsl +zDA3sHoClfC4nAOVrRghYlHU2bT6HPLxjLtBkcUTfj6nO6nOLGC7EFVkqYw5mUWq +uSNntk1D1+MYVnZBeKqw6y21FFsclmzzXTtAJg0vuAg1jxnMHqiDbNqpUUO8ZWLG +iz2tdAWiBAKwZv0Psv44Dy++4v/BMd8FBb/iHYMCgYEAttKmRmW91b9t9Xg0fEjp +QBzyBQWhNZrTn520rUy8PSqDxBsSSgsDgncklwPMCYYjjfZmo3rBFdC0gPSOy4qb +/cycIMtK7VzZJeuzehfV6h+SOnolwFY0Zg9qv3z257FwDbDqf92d22dqymBrTaj2 +zC7eovvdSkGj53x3AsEE+hECgYAd7JJU3pi7h6AHw3vbvO1pqKHfpQYAp8/NOpWB +CsehQu9L32GcktJRMYQAqAVeDNEd1wCu6Gmsu46VVbnE0F/cWkx4/9PEGDMx+Lg4 +OrZBT8RY+1x2w+UatwyCtmtb+yFIET4866uWgZfeB6zaK9aCvuUPvDHrLfCHcPXs +yGRFmQKBgQDYvcjkUqKvnZwwpV6p6BHi6PuHWyjL+GLTk/IzsxOJykHzX4SV46Sd +9IxyE+OWZhkISBMSMe8wQC/S+QYs8hDbEE3WH1DXp33joyNgt2aWI0Splu7S1f6P +Pe08fYlLgz1xrbJuJAOgvkbskAUcmfmRwFeGaZSkkCtkzVxgN7LS3Q== +-----END RSA PRIVATE KEY-----`) + +var badCACert = []byte(`-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgIJAIabsJi6ILN7MA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV +BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX +DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA0MTIwMAYDVQQDFClnZW5l +cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANjG6n5eZ1vq2AksP/htNMprQRJb08MxCPPL +IN5uT11NFzL+ck8o7fkt5oW8y2OOtLcun15RIr3jTT9G826NJPrb1I+x2zhuKuAr +NCVoi74m3IUV0C4pqrZ3bFbE9sY+epu6ExE1jGIY3fChc9bOsO8ivXQoXtqQQnnh +oyG0LNHXn/AX9OYewW7Jrl0G+OJ56VHVoBSPxa6nM+ozuo8kmFeVN2o6go3FjgUJ +l0bTItcHuPB3Y9reI1v3r20SKP7V/PJJblxUlQZk4rSgN559twMwPv6mupEw8M0P +Dp7YwtPSU+8pEu+uFi0YgTqPsAt2ujN3mTTJFutefgzT+8AI2BMCAwEAAaOBljCB +kzAdBgNVHQ4EFgQU9SUu1vDxGeyGEgy7pzZjLm+WSH0wZAYDVR0jBF0wW4AU9SUu +1vDxGeyGEgy7pzZjLm+WSH2hOKQ2MDQxMjAwBgNVBAMUKWdlbmVyaWNfd2ViaG9v +a19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhggkAhpuwmLogs3swDAYDVR0TBAUw +AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAViib1ZcwLUi5YE279NRypsGLa8pfT/A8 +10u41L4xw+32mD2HojMmAlAF4jnC62exaXFsAYEsze4TF+0zqwDkHyGqViw/hKAv +SrGgPUX3C7wLyiMa3pjZfcQQy+80SKiJLeClxxjkdhO0mGNo1LdJThYU5IADHVtF +u2oOKLTjWBVzkMRkTXp5RReeEoUPvFgJhPKIVLggdXdJT8oQjgIVlx6IuzjU0AeM +tJ5AIWYrsqv9FlpfUXWjdiy8uF3iLWTOpd6pICnjzfj02wQouTEkxQ2iFinl7Das +iK+7d34q6Ww1/1nu4EBBDYB1VlWdhDLJVT4F+mF8wZFBu863Ba+U5Q== +-----END CERTIFICATE-----`) + +var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAquioChbTG+iyTg3+SwGww/7yN84jtj55v8Xld8TgkyavSZm7 +Q6WURevvXtJHMug/NAz6qoCFy/zOiozUrMB36GFOA8MtwaOddCMdHMruX7q2+CiF +amhFg58uPfVr9qit20JiyBhaPH97WmQzwsfRQt4E1mCbEHZYK6r1RlF3i00nDCxc +741wuYp4Uc5oYM0dYoVCv11DYdf62v1grLFO02a5qjAULS74KKNaJ4YDOmqnTtWt +I75ZkEJSC2TT90o8eFIRuImY17venqIbp30A+XtvMhITg55648Zdci+CGHDcqJA1 +C+y0rxXZkcuFFFrK0tiN7K/cPK1LDtS08gp61QIDAQABAoIBAFhGVxTu+Rc/N2lt +fNzNALobIoyEYpms50GQO5eDDuOyZXNEfh7QlScQV9DIF5JJtutxkL8kJvdXmm6h +ku+vcb+LErqKw0Vy9s6XnF/UyQ6U6BCBDXgKZ202eLHz41HBihrnzRHA0krRJato +efuvLXy2JBV+TFlSZvQXFxy801wVIxlI7Jh94YR7CT3HYmD5qjAtiTVkSS+wmx++ +cfh0rrMulkXDEUlRPm/fXqt0do+neH9eNYee4h0mbZ84f3ecz/ql3HfkfoK9ZBq+ +M85VWnEvRetRF0AQlYK3IUPQH2XHIEZkUab8s+EZoIsgVBY9xvl6VTEQWFtbzRWC +7ozg2PkCgYEA3X8Pv6a5wzWBwX17RkTXcPqS44uzJ/K0adv1QSrVvXrZJQ0FEGqr +gR74aQXiaVl06X/N50y/soQIHWpvqGjoEbkA+jS5y4GkGxAviFFAuRnW9DyRePxH +nQiYFzgxBj55iDsdvdqJ2e0vM1EWNcaVmghNI25D+cBc4Qh1Zz4qFSsCgYEAxYg9 +vYGepcTMJ0/dk4SVMaITi/8nAnZHnptILpSjgPNp8udsovQPkymwo+EDnjQqVPO4 +OIIICEopk6Zup0Rq2iW3zRGbRJtp+uJ7mfFS+nT6sAe0tPWbGejwrudDDcx0fkx4 +C+1//rJ95H0c9L4nd51azCJD0k2yKtIFyj91r/8CgYBwMkWa8exU+oyQo2xHSuXK +n9K6GnCUwrcqjDWuXfFI+qp1vyOajj3zuOlh4Y4viRXUlV2KVXEhDwpBREHtD77G +A22AUCbw8+lZoBhDt8zONk2RCAE0RK5N2CWaVWdX31uWa0OEgOelESUAnIlgkggD +r0LLuLYME6m4f51gv7d3YwKBgCFp8He0C3AjIB2uRt8DWHFy5zeRS7oA5BCSV915 +S0cu5ccvGpNeEZxlOvodwAzs6hRAvfLhHBa65NmTF7i3vBN2uea4iblLSNwln57k +0ZKIYzePtiO+QCRb4QrVF+SnpzUOHmh2HmapLt6Nw24rFGYJeih5y1sxxWe060HR +BkllAoGBAMkT1a3BhhEwoyyKiwC05+gzlKfAWz7t6J//6/yx+82lXDk4/J455qcw +ny2y6P6r964EUoqMrAU0bdTs3sKDtOLdNMIt5RfoDBsdQDt2ktbv0pvii3E8SQFi +JuJWSenrfFaI+AgwE9jDo1Hy6dhF6/hnV3+QoznwEPRAO6wmPyVA +-----END RSA PRIVATE KEY-----`) + +var serverCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDOzCCAiOgAwIBAgIJAN0PSMLOjTVAMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV +BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX +DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA4MTYwNAYDVQQDDC1nZW5l +cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19zZXJ2ZXIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq6KgKFtMb6LJODf5LAbDD/vI3ziO2 +Pnm/xeV3xOCTJq9JmbtDpZRF6+9e0kcy6D80DPqqgIXL/M6KjNSswHfoYU4Dwy3B +o510Ix0cyu5furb4KIVqaEWDny499Wv2qK3bQmLIGFo8f3taZDPCx9FC3gTWYJsQ +dlgrqvVGUXeLTScMLFzvjXC5inhRzmhgzR1ihUK/XUNh1/ra/WCssU7TZrmqMBQt +Lvgoo1onhgM6aqdO1a0jvlmQQlILZNP3Sjx4UhG4iZjXu96eohunfQD5e28yEhOD +nnrjxl1yL4IYcNyokDUL7LSvFdmRy4UUWsrS2I3sr9w8rUsO1LTyCnrVAgMBAAGj +SjBIMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC +BggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQCA +/IFclqY/dsadGY0e9U3xq0XliIjZIHaI6OLXmO8xBdLDVf2+d6Bt3dl9gQMtGQI3 +zj9vqJrM+znXR30yAERFefZItk8hTzAotk15HYExVJkIn5JQBaXRbeO2DUZFgAnu +6OU6KnuVC6i+7xDlbMl8wtRPmeZ6FS1wW4wnxLWZtKYAuLVDs0ISy6qbznGhCkWc +b0uPbxnMmZHQLVL+yF1LYWpX9+Xa9QnWXOSY7KHtcuYXZB/XV4Pt6aDncg76bFdl +MG3bocbJ9MsoS/LdlAiYzLNmKlFa243QPOo/zN170NhEZaF1lM80YBaLIE4rRtga +nrkNOnPHx1evvuleH0Yv +-----END CERTIFICATE-----`) + +var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA1Cf8Tch5IieN9zAI0b5FFJoMzPsN3RAYD5GAk3Xo9XNbKnvJ +38o2TKdxFu1/Q8hqG3668vWuL6TYDYVcYWtC6rwNpU/moAdJZyyEtmZIam1Va1VW +HDCpylsk4b8jbUxxm6OzF1XxZpBUJg9o/1OvL1XA0rmcDiddXN5VdL2adJZsS54t +gI57hcH0jkQocHTEv5gIl2tcTDStd4GeWxBmCotYdCQWcNk+96QMRxIai88aPPYw +QxpOQeaABrYrjPO1fJA0jimM9bhkjGXK6cnrRQrg3jkeF3vuPw9BFbE4VeENLY+y +v2OWBPYOD8ov0Tn2inge4QjE2DZyK95l46wQbwIDAQABAoIBAGZiAY1cALEt24H9 +yVPG+bluelz1jwQuvx3MPvtqvIivKcC/ynVYNYoaiCXjaTZB4orwRrH3RB8z8xvb +TvCofbugEwnDHG3/9jl3L3iCtdG+f6lznkGublH8WDklL6iQaocMoeHSFNRFNIbF +iwskzHcQcCSBdEEUWCb4GM9krMQz7pBR9BloyV40ZAGilMXI9F9FZ0YBWWee09gi +jid6sEzYQveZ5RQgEEDrE/i+jzXkU8sBKsSm1GuKH62+YrtelBqP83DwVIKthMOJ +79tw6i98v5JHV+1ikqC1Na/c7OxBBF3xrgwCN47ok0cHaIXh7SX+EIN6jcwKTmRH +VZQBz2ECgYEA6tTpJEKNAzEY3KOO578zY2hWhPUNqzUTm8yLxZedhDt2rwyWjd4J +HhS7CiNqFpMT/kxFyOupA8SFJPZDhJBXttnyzO9Sb61fLWgSP739K9OQfK1FGTA6 +khjc3vHtBeGrWm9+1jSxQtrwly7Rs+EmdvseDCN7yie/mgrBLS7p1l8CgYEA50fN +6BnbeAgCTK8GDBWSJPaYUlo/lebUDCn5QIp1LK93vPQWjJR8xRBwL1TbiVKGd054 +dRZVuJYMJx+2mbrt5ca9UArisZp5OZgR4xz29n9u69P5XiuG8Fq/JBJXp1GXONVx +JNOsUHOW/b3w2tNUWZcMQAH601BHOtO+EtaEX/ECgYBHygz4A8xeFG1YTjwKxt3r +3uLMRKoIE/LJp093eXEzEoam3v9LoXxCEO5ZHBh7jD0JecG/uaNyvmpBsXNUnFfk +U16xndwiveqh0/X4PJmgA05hfwrnt2HAdg9XrLfcG3Ap9nnc/EDQgmQYo7yB9Cux +JfW6mkJmu54Mdos1x+i+mwKBgHmewcGe71E0bPkkRLrQERUM89bCjJNoWfO3ktIE +vU9tSjr75GuyndYHKedJ6VRSKFHO2vs/bn5tsSBVxfEbYoSlOOJBhyo8AClwNV/H +2HqRUqQCySxjGUeFgOQYHS3ocuw5GZFzGjcIQctXObPo0391NcTnBZ5fpcVimZ5Q +XjYRAoGAN2O3HQjPyThoOUOn5MJEIx0L5bMvGNDzJO+oFngAntX/zv0zu/zsD9tc +kk4EbMLluiw2/XJvYjCStieaYxbSWioKwlThy39C+iUut+IbpP2eI46SOkhvPczt +4u1/sslqjs4ZSntR4Z9UOk3vY3oxRKbiXX2/vl9cqzB/cGYu01c= +-----END RSA PRIVATE KEY-----`) + +var clientCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDOzCCAiOgAwIBAgIJAN0PSMLOjTVBMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV +BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX +DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA4MTYwNAYDVQQDDC1nZW5l +cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jbGllbnQwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUJ/xNyHkiJ433MAjRvkUUmgzM+w3d +EBgPkYCTdej1c1sqe8nfyjZMp3EW7X9DyGobfrry9a4vpNgNhVxha0LqvA2lT+ag +B0lnLIS2ZkhqbVVrVVYcMKnKWyThvyNtTHGbo7MXVfFmkFQmD2j/U68vVcDSuZwO +J11c3lV0vZp0lmxLni2AjnuFwfSORChwdMS/mAiXa1xMNK13gZ5bEGYKi1h0JBZw +2T73pAxHEhqLzxo89jBDGk5B5oAGtiuM87V8kDSOKYz1uGSMZcrpyetFCuDeOR4X +e+4/D0EVsThV4Q0tj7K/Y5YE9g4Pyi/ROfaKeB7hCMTYNnIr3mXjrBBvAgMBAAGj +SjBIMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC +BggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQC/ +TBXk511JBKLosKVqrjluo8bzbgnREUrPcKclatiAOiIFKbMBy4nE4BlGZZW34t1u +sStB1dDHBHIuEkZxs93xwjqXN03yNNfve+FkRcb+guaZJEIBRlNocNxhd+lVDo8J +axRTdoOxyEOHGCjg+gyb0i9f/rqEqLnDwnYLZbH9Qbh/yv6OgISUTYOCzH35H0/6 +unY5JaBhRvmJHI0Z3KtmvMShbUyzoYD+oNLaS31fvoYIekcHsnOjZGBukaIx1bE1 +4SFjCUSPGDdzJdaYxQb0UXNI7oXKr6e6YeOrglIrVbboa0X3jtqGF1U7rop8ts3v +24SeXsvxqJht40itVvGK +-----END CERTIFICATE-----`) diff --git a/plugin/pkg/admission/webhook/doc.go b/plugin/pkg/admission/webhook/doc.go new file mode 100644 index 00000000000..e4efbbeb210 --- /dev/null +++ b/plugin/pkg/admission/webhook/doc.go @@ -0,0 +1,18 @@ +/* +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 "k8s.io/kubernetes/plugin/pkg/admission/webhook" diff --git a/plugin/pkg/admission/webhook/gencerts.sh b/plugin/pkg/admission/webhook/gencerts.sh new file mode 100755 index 00000000000..7de850a590a --- /dev/null +++ b/plugin/pkg/admission/webhook/gencerts.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +# 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. + +set -e + +# gencerts.sh generates the certificates for the generic webhook admission plugin tests. +# +# It is not expected to be run often (there is no go generate rule), and mainly +# exists for documentation purposes. + +CN_BASE="generic_webhook_admission_plugin_tests" + +cat > server.conf << EOF +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name +[req_distinguished_name] +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth, serverAuth +subjectAltName = @alt_names +[alt_names] +IP.1 = 127.0.0.1 +EOF + +cat > client.conf << EOF +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name +[req_distinguished_name] +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth, serverAuth +subjectAltName = @alt_names +[alt_names] +IP.1 = 127.0.0.1 +EOF + +# Create a certificate authority +openssl genrsa -out caKey.pem 2048 +openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=${CN_BASE}_ca" + +# Create a second certificate authority +openssl genrsa -out badCAKey.pem 2048 +openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=${CN_BASE}_ca" + +# Create a server certiticate +openssl genrsa -out serverKey.pem 2048 +openssl req -new -key serverKey.pem -out server.csr -subj "/CN=${CN_BASE}_server" -config server.conf +openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf + +# Create a client certiticate +openssl genrsa -out clientKey.pem 2048 +openssl req -new -key clientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf +openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf + +outfile=certs_test.go + +cat > $outfile << EOF +/* +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. +*/ + +EOF + +echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile +echo "// and holds raw certificates for the webhook tests." >> $outfile +echo "" >> $outfile +echo "package webhook" >> $outfile +for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do + data=$(cat ${file}.pem) + echo "" >> $outfile + echo "var $file = []byte(\`$data\`)" >> $outfile +done + +# Clean up after we're done. +rm *.pem +rm *.csr +rm *.srl +rm *.conf \ No newline at end of file diff --git a/plugin/pkg/admission/webhook/rules.go b/plugin/pkg/admission/webhook/rules.go new file mode 100644 index 00000000000..2ae2afee5a1 --- /dev/null +++ b/plugin/pkg/admission/webhook/rules.go @@ -0,0 +1,94 @@ +/* +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 ( + "strings" + + "k8s.io/apiserver/pkg/admission" + "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +type RuleMatcher struct { + Rule admissionregistration.RuleWithOperations + Attr admission.Attributes +} + +func (r *RuleMatcher) Matches() bool { + return r.operation() && + r.group() && + r.version() && + r.resource() +} + +func exactOrWildcard(items []string, requested string) bool { + for _, item := range items { + if item == "*" { + return true + } + if item == requested { + return true + } + } + + return false +} + +func (r *RuleMatcher) group() bool { + return exactOrWildcard(r.Rule.APIGroups, r.Attr.GetResource().Group) +} + +func (r *RuleMatcher) version() bool { + return exactOrWildcard(r.Rule.APIVersions, r.Attr.GetResource().Version) +} + +func (r *RuleMatcher) operation() bool { + attrOp := r.Attr.GetOperation() + for _, op := range r.Rule.Operations { + 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 +} + +func splitResource(resSub string) (res, sub string) { + parts := strings.SplitN(resSub, "/", 2) + if len(parts) == 2 { + return parts[0], parts[1] + } + return parts[0], "" +} + +func (r *RuleMatcher) resource() bool { + opRes, opSub := r.Attr.GetResource().Resource, r.Attr.GetSubresource() + for _, res := range r.Rule.Resources { + res, sub := splitResource(res) + resMatch := res == "*" || res == opRes + subMatch := sub == "*" || sub == opSub + if resMatch && subMatch { + return true + } + } + return false +} diff --git a/plugin/pkg/admission/webhook/rules_test.go b/plugin/pkg/admission/webhook/rules_test.go new file mode 100644 index 00000000000..dc6a79fbc3a --- /dev/null +++ b/plugin/pkg/admission/webhook/rules_test.go @@ -0,0 +1,300 @@ +/* +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 ( + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + adreg "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +type ruleTest struct { + rule adreg.RuleWithOperations + match []admission.Attributes + noMatch []admission.Attributes +} +type tests map[string]ruleTest + +func a(group, version, resource, subresource, name string, operation admission.Operation) admission.Attributes { + return admission.NewAttributesRecord( + nil, nil, + schema.GroupVersionKind{Group: group, Version: version, Kind: "k" + resource}, + "ns", name, + schema.GroupVersionResource{Group: group, Version: version, Resource: resource}, subresource, + operation, + nil, + ) +} + +func attrList(a ...admission.Attributes) []admission.Attributes { + return a +} + +func TestGroup(t *testing.T) { + table := tests{ + "wildcard": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + APIGroups: []string{"*"}, + }, + }, + match: attrList( + a("g", "v", "r", "", "name", admission.Create), + ), + }, + "exact": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + APIGroups: []string{"g1", "g2"}, + }, + }, + match: attrList( + a("g1", "v", "r", "", "name", admission.Create), + a("g2", "v2", "r3", "", "name", admission.Create), + ), + noMatch: attrList( + a("g3", "v", "r", "", "name", admission.Create), + a("g4", "v", "r", "", "name", admission.Create), + ), + }, + } + + for name, tt := range table { + for _, m := range tt.match { + r := RuleMatcher{tt.rule, m} + if !r.group() { + t.Errorf("%v: expected match %#v", name, m) + } + } + for _, m := range tt.noMatch { + r := RuleMatcher{tt.rule, m} + if r.group() { + t.Errorf("%v: expected no match %#v", name, m) + } + } + } +} + +func TestVersion(t *testing.T) { + table := tests{ + "wildcard": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + APIVersions: []string{"*"}, + }, + }, + match: attrList( + a("g", "v", "r", "", "name", admission.Create), + ), + }, + "exact": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + APIVersions: []string{"v1", "v2"}, + }, + }, + match: attrList( + a("g1", "v1", "r", "", "name", admission.Create), + a("g2", "v2", "r", "", "name", admission.Create), + ), + noMatch: attrList( + a("g1", "v3", "r", "", "name", admission.Create), + a("g2", "v4", "r", "", "name", admission.Create), + ), + }, + } + for name, tt := range table { + for _, m := range tt.match { + r := RuleMatcher{tt.rule, m} + if !r.version() { + t.Errorf("%v: expected match %#v", name, m) + } + } + for _, m := range tt.noMatch { + r := RuleMatcher{tt.rule, m} + if r.version() { + t.Errorf("%v: expected no match %#v", name, m) + } + } + } +} + +func TestOperation(t *testing.T) { + table := tests{ + "wildcard": { + rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.OperationAll}}, + match: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("g", "v", "r", "", "name", admission.Update), + a("g", "v", "r", "", "name", admission.Delete), + a("g", "v", "r", "", "name", admission.Connect), + ), + }, + "create": { + rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Create}}, + match: attrList( + a("g", "v", "r", "", "name", admission.Create), + ), + noMatch: attrList( + a("g", "v", "r", "", "name", admission.Update), + a("g", "v", "r", "", "name", admission.Delete), + a("g", "v", "r", "", "name", admission.Connect), + ), + }, + "update": { + rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Update}}, + match: attrList( + a("g", "v", "r", "", "name", admission.Update), + ), + noMatch: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("g", "v", "r", "", "name", admission.Delete), + a("g", "v", "r", "", "name", admission.Connect), + ), + }, + "delete": { + rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Delete}}, + match: attrList( + a("g", "v", "r", "", "name", admission.Delete), + ), + noMatch: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("g", "v", "r", "", "name", admission.Update), + a("g", "v", "r", "", "name", admission.Connect), + ), + }, + "connect": { + rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Connect}}, + match: attrList( + a("g", "v", "r", "", "name", admission.Connect), + ), + noMatch: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("g", "v", "r", "", "name", admission.Update), + a("g", "v", "r", "", "name", admission.Delete), + ), + }, + "multiple": { + rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Update, adreg.Delete}}, + match: attrList( + a("g", "v", "r", "", "name", admission.Update), + a("g", "v", "r", "", "name", admission.Delete), + ), + noMatch: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("g", "v", "r", "", "name", admission.Connect), + ), + }, + } + for name, tt := range table { + for _, m := range tt.match { + r := RuleMatcher{tt.rule, m} + if !r.operation() { + t.Errorf("%v: expected match %#v", name, m) + } + } + for _, m := range tt.noMatch { + r := RuleMatcher{tt.rule, m} + if r.operation() { + t.Errorf("%v: expected no match %#v", name, m) + } + } + } +} + +func TestResource(t *testing.T) { + table := tests{ + "no subresources": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + Resources: []string{"*"}, + }, + }, + match: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("2", "v", "r2", "", "name", admission.Create), + ), + noMatch: attrList( + a("g", "v", "r", "exec", "name", admission.Create), + a("2", "v", "r2", "proxy", "name", admission.Create), + ), + }, + "r & subresources": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + Resources: []string{"r/*"}, + }, + }, + match: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("g", "v", "r", "exec", "name", admission.Create), + ), + noMatch: attrList( + a("2", "v", "r2", "", "name", admission.Create), + a("2", "v", "r2", "proxy", "name", admission.Create), + ), + }, + "r & subresources or r2": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + Resources: []string{"r/*", "r2"}, + }, + }, + match: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("g", "v", "r", "exec", "name", admission.Create), + a("2", "v", "r2", "", "name", admission.Create), + ), + noMatch: attrList( + a("2", "v", "r2", "proxy", "name", admission.Create), + ), + }, + "proxy or exec": { + rule: adreg.RuleWithOperations{ + Rule: adreg.Rule{ + Resources: []string{"*/proxy", "*/exec"}, + }, + }, + match: attrList( + a("g", "v", "r", "exec", "name", admission.Create), + a("2", "v", "r2", "proxy", "name", admission.Create), + a("2", "v", "r3", "proxy", "name", admission.Create), + ), + noMatch: attrList( + a("g", "v", "r", "", "name", admission.Create), + a("2", "v", "r2", "", "name", admission.Create), + a("2", "v", "r4", "scale", "name", admission.Create), + ), + }, + } + for name, tt := range table { + for _, m := range tt.match { + r := RuleMatcher{tt.rule, m} + if !r.resource() { + t.Errorf("%v: expected match %#v", name, m) + } + } + for _, m := range tt.noMatch { + r := RuleMatcher{tt.rule, m} + if r.resource() { + t.Errorf("%v: expected no match %#v", name, m) + } + } + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/group_version.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/group_version.go index bd0e9612a64..bd4c6d9b586 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/group_version.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/group_version.go @@ -29,8 +29,8 @@ import ( // // +protobuf.options.(gogoproto.goproto_stringer)=false type GroupResource struct { - Group string `protobuf:"bytes,1,opt,name=group"` - Resource string `protobuf:"bytes,2,opt,name=resource"` + Group string `json:"group" protobuf:"bytes,1,opt,name=group"` + Resource string `json:"resource" protobuf:"bytes,2,opt,name=resource"` } func (gr *GroupResource) String() string { @@ -45,9 +45,9 @@ func (gr *GroupResource) String() string { // // +protobuf.options.(gogoproto.goproto_stringer)=false type GroupVersionResource struct { - Group string `protobuf:"bytes,1,opt,name=group"` - Version string `protobuf:"bytes,2,opt,name=version"` - Resource string `protobuf:"bytes,3,opt,name=resource"` + Group string `json:"group" protobuf:"bytes,1,opt,name=group"` + Version string `json:"version" protobuf:"bytes,2,opt,name=version"` + Resource string `json:"resource" protobuf:"bytes,3,opt,name=resource"` } func (gvr *GroupVersionResource) String() string { @@ -59,8 +59,8 @@ func (gvr *GroupVersionResource) String() string { // // +protobuf.options.(gogoproto.goproto_stringer)=false type GroupKind struct { - Group string `protobuf:"bytes,1,opt,name=group"` - Kind string `protobuf:"bytes,2,opt,name=kind"` + Group string `json:"group" protobuf:"bytes,1,opt,name=group"` + Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"` } func (gk *GroupKind) String() string { @@ -88,8 +88,8 @@ func (gvk GroupVersionKind) String() string { // // +protobuf.options.(gogoproto.goproto_stringer)=false type GroupVersion struct { - Group string `protobuf:"bytes,1,opt,name=group"` - Version string `protobuf:"bytes,2,opt,name=version"` + Group string `json:"group" protobuf:"bytes,1,opt,name=group"` + Version string `json:"version" protobuf:"bytes,2,opt,name=version"` } // Empty returns true if group and version are empty diff --git a/staging/src/k8s.io/client-go/rest/request.go b/staging/src/k8s.io/client-go/rest/request.go index b87ddaff51b..cfb4511bab8 100644 --- a/staging/src/k8s.io/client-go/rest/request.go +++ b/staging/src/k8s.io/client-go/rest/request.go @@ -1148,6 +1148,9 @@ func (r Result) Into(obj runtime.Object) error { if r.decoder == nil { 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) if err != nil || out == obj { diff --git a/staging/src/k8s.io/client-go/rest/request_test.go b/staging/src/k8s.io/client-go/rest/request_test.go index 15bf851d5a9..cedac794aed 100755 --- a/staging/src/k8s.io/client-go/rest/request_test.go +++ b/staging/src/k8s.io/client-go/rest/request_test.go @@ -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) { uri, _ := url.Parse("http://localhost") r := NewRequest(nil, "POST", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil) diff --git a/test/integration/etcd/etcd_storage_path_test.go b/test/integration/etcd/etcd_storage_path_test.go index 936ac26b153..896b2f69513 100644 --- a/test/integration/etcd/etcd_storage_path_test.go +++ b/test/integration/etcd/etcd_storage_path_test.go @@ -381,6 +381,10 @@ var ephemeralWhiteList = createEphemeralWhiteList( // k8s.io/kubernetes/pkg/apis/policy/v1beta1 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)