Merge pull request #46388 from lavalamp/whitlockjc-generic-webhook-admission

Automatic merge from submit-queue (batch tested with PRs 46239, 46627, 46346, 46388, 46524)

Dynamic webhook admission control plugin

Unit tests pass.

Needs plumbing:
* [ ] service resolver (depends on @wfender PR)
* [x] client cert (depends on ????)
* [ ] hook source (depends on @caesarxuchao PR)

Also at least one thing will need to be renamed after Chao's PR merges.

```release-note
Allow remote admission controllers to be dynamically added and removed by administrators.  External admission controllers make an HTTP POST containing details of the requested action which the service can approve or reject.
```
pull/6/head
Kubernetes Submit Queue 2017-06-02 23:37:42 -07:00 committed by GitHub
commit e837c3bbc2
37 changed files with 1708 additions and 44 deletions

View File

@ -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",

View File

@ -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.")

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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 \

View File

@ -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",

View File

@ -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]
}

View File

@ -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"],

View File

@ -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"],
)

View File

@ -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)
}
}

View File

@ -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",

View File

@ -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)

View File

@ -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"`

View File

@ -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

View File

@ -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",

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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
}),
}

View File

@ -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
}
}),

View File

@ -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",

View File

@ -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"

View File

@ -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) {

View File

@ -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",
],

View File

@ -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"],
)

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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-----`)

View File

@ -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"

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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)