Adding the mutating webhook

pull/6/head
Chao Xu 2017-11-14 16:36:28 -08:00
parent 2aaab817de
commit ea123f82aa
22 changed files with 1819 additions and 271 deletions

View File

@ -316,7 +316,7 @@ if [[ -n "${GCE_GLBC_IMAGE:-}" ]]; then
fi
if [[ -z "${KUBE_ADMISSION_CONTROL:-}" ]]; then
ADMISSION_CONTROL="Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,PodPreset,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,Priority"
ADMISSION_CONTROL="MutatingAdmissionWebhook,Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,PodPreset,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,Priority"
if [[ "${ENABLE_POD_SECURITY_POLICY:-}" == "true" ]]; then
ADMISSION_CONTROL="${ADMISSION_CONTROL},PodSecurityPolicy"
fi

View File

@ -104,8 +104,8 @@ func TestAddFlags(t *testing.T) {
MinRequestTimeout: 1800,
},
Admission: &apiserveroptions.AdmissionOptions{
RecommendedPluginOrder: []string{"NamespaceLifecycle", "Initializers", "GenericAdmissionWebhook"},
DefaultOffPlugins: []string{"Initializers", "GenericAdmissionWebhook"},
RecommendedPluginOrder: []string{"MutatingAdmissionWebhook", "NamespaceLifecycle", "Initializers", "GenericAdmissionWebhook"},
DefaultOffPlugins: []string{"MutatingAdmissionWebhook", "Initializers", "GenericAdmissionWebhook"},
PluginNames: []string{"AlwaysDeny"},
ConfigFile: "/admission-control-config",
Plugins: s.Admission.Plugins,

View File

@ -419,7 +419,7 @@ function start_apiserver {
fi
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount${security_admission},DefaultStorageClass,DefaultTolerationSeconds,GenericAdmissionWebhook,ResourceQuota
ADMISSION_CONTROL=MutatingAdmissionWebhook,Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount${security_admission},DefaultStorageClass,DefaultTolerationSeconds,GenericAdmissionWebhook,ResourceQuota
# This is the default dir and filename where the apiserver will generate a self-signed cert
# which should be able to be used as the CA to verify itself

View File

@ -0,0 +1,70 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"admission.go",
"doc.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/admission/v1alpha1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/configuration:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"admission_test.go",
"certs_test.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
library = ":go_default_library",
deps = [
"//vendor/k8s.io/api/admission/v1alpha1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user: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"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,318 @@
/*
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 mutating delegates admission checks to dynamically configured
// mutating webhooks.
package mutating
import (
"context"
"fmt"
"io"
"time"
jsonpatch "github.com/evanphx/json-patch"
"github.com/golang/glog"
admissionv1alpha1 "k8s.io/api/admission/v1alpha1"
"k8s.io/api/admissionregistration/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/configuration"
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
"k8s.io/apiserver/pkg/admission/plugin/webhook/versioned"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
)
const (
// Name of admission plug-in
PluginName = "MutatingAdmissionWebhook"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
plugin, err := NewMutatingWebhook(configFile)
if err != nil {
return nil, err
}
return plugin, nil
})
}
// WebhookSource can list dynamic webhook plugins.
type WebhookSource interface {
Run(stopCh <-chan struct{})
Webhooks() (*v1alpha1.MutatingWebhookConfiguration, error)
}
// NewMutatingWebhook returns a generic admission webhook plugin.
func NewMutatingWebhook(configFile io.Reader) (*MutatingWebhook, error) {
kubeconfigFile, err := config.LoadConfig(configFile)
if err != nil {
return nil, err
}
cm, err := config.NewClientManager()
if err != nil {
return nil, err
}
authInfoResolver, err := config.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
if err != nil {
return nil, err
}
// Set defaults which may be overridden later.
cm.SetAuthenticationInfoResolver(authInfoResolver)
cm.SetServiceResolver(config.NewDefaultServiceResolver())
return &MutatingWebhook{
Handler: admission.NewHandler(
admission.Connect,
admission.Create,
admission.Delete,
admission.Update,
),
clientManager: cm,
}, nil
}
// MutatingWebhook is an implementation of admission.Interface.
type MutatingWebhook struct {
*admission.Handler
hookSource WebhookSource
namespaceMatcher namespace.Matcher
clientManager config.ClientManager
convertor versioned.Convertor
jsonSerializer runtime.Serializer
}
var (
_ = genericadmissioninit.WantsExternalKubeClientSet(&MutatingWebhook{})
)
// TODO find a better way wire this, but keep this pull small for now.
func (a *MutatingWebhook) SetAuthenticationInfoResolverWrapper(wrapper config.AuthenticationInfoResolverWrapper) {
a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper)
}
// SetServiceResolver sets a service resolver for the webhook admission plugin.
// Passing a nil resolver does not have an effect, instead a default one will be used.
func (a *MutatingWebhook) SetServiceResolver(sr config.ServiceResolver) {
a.clientManager.SetServiceResolver(sr)
}
// SetScheme sets a serializer(NegotiatedSerializer) which is derived from the scheme
func (a *MutatingWebhook) SetScheme(scheme *runtime.Scheme) {
if scheme != nil {
a.clientManager.SetNegotiatedSerializer(serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
Serializer: serializer.NewCodecFactory(scheme).LegacyCodec(admissionv1alpha1.SchemeGroupVersion),
}))
a.convertor.Scheme = scheme
a.jsonSerializer = json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
}
}
// WantsExternalKubeClientSet defines a function which sets external ClientSet for admission plugins that need it
func (a *MutatingWebhook) SetExternalKubeClientSet(client clientset.Interface) {
a.namespaceMatcher.Client = client
a.hookSource = configuration.NewMutatingWebhookConfigurationManager(client.AdmissionregistrationV1alpha1().MutatingWebhookConfigurations())
}
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
func (a *MutatingWebhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
namespaceInformer := f.Core().V1().Namespaces()
a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister()
a.SetReadyFunc(namespaceInformer.Informer().HasSynced)
}
// ValidateInitialization implements the InitializationValidator interface.
func (a *MutatingWebhook) ValidateInitialization() error {
if a.hookSource == nil {
return fmt.Errorf("MutatingWebhook admission plugin requires a Kubernetes client to be provided")
}
if a.jsonSerializer == nil {
return fmt.Errorf("MutatingWebhook admission plugin's jsonSerializer is not properly setup")
}
if err := a.namespaceMatcher.Validate(); err != nil {
return fmt.Errorf("MutatingWebhook.namespaceMatcher is not properly setup: %v", err)
}
if err := a.clientManager.Validate(); err != nil {
return fmt.Errorf("MutatingWebhook.clientManager is not properly setup: %v", err)
}
if err := a.convertor.Validate(); err != nil {
return fmt.Errorf("MutatingWebhook.convertor is not properly setup: %v", err)
}
go a.hookSource.Run(wait.NeverStop)
return nil
}
func (a *MutatingWebhook) loadConfiguration(attr admission.Attributes) (*v1alpha1.MutatingWebhookConfiguration, error) {
hookConfig, err := a.hookSource.Webhooks()
// if Webhook configuration is disabled, fail open
if err == configuration.ErrDisabled {
return &v1alpha1.MutatingWebhookConfiguration{}, nil
}
if err != nil {
e := apierrors.NewServerTimeout(attr.GetResource().GroupResource(), string(attr.GetOperation()), 1)
e.ErrStatus.Message = fmt.Sprintf("Unable to refresh the Webhook configuration: %v", err)
e.ErrStatus.Reason = "LoadingConfiguration"
e.ErrStatus.Details.Causes = append(e.ErrStatus.Details.Causes, metav1.StatusCause{
Type: "MutatingWebhookConfigurationFailure",
Message: "An error has occurred while refreshing the MutatingWebhook configuration, no resources can be created/updated/deleted/connected until a refresh succeeds.",
})
return nil, e
}
return hookConfig, nil
}
// Admit makes an admission decision based on the request attributes.
func (a *MutatingWebhook) Admit(attr admission.Attributes) error {
hookConfig, err := a.loadConfiguration(attr)
if err != nil {
return err
}
hooks := hookConfig.Webhooks
ctx := context.TODO()
var relevantHooks []*v1alpha1.Webhook
for i := range hooks {
call, err := a.shouldCallHook(&hooks[i], attr)
if err != nil {
return err
}
if call {
relevantHooks = append(relevantHooks, &hooks[i])
}
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
}
// convert the object to the external version before sending it to the webhook
versionedAttr := versioned.Attributes{
Attributes: attr,
}
if oldObj := attr.GetOldObject(); oldObj != nil {
out, err := a.convertor.ConvertToGVK(oldObj, attr.GetKind())
if err != nil {
return apierrors.NewInternalError(err)
}
versionedAttr.OldObject = out
}
if obj := attr.GetObject(); obj != nil {
out, err := a.convertor.ConvertToGVK(obj, attr.GetKind())
if err != nil {
return apierrors.NewInternalError(err)
}
versionedAttr.Object = out
}
for _, hook := range relevantHooks {
t := time.Now()
err := a.callAttrMutatingHook(ctx, hook, versionedAttr)
admission.Metrics.ObserveWebhook(time.Since(t), err != nil, hook, attr)
if err == nil {
continue
}
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1alpha1.Ignore
if callErr, ok := err.(*webhookerrors.ErrCallingWebhook); ok {
if ignoreClientCallFailures {
glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
continue
}
glog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
}
return apierrors.NewInternalError(err)
}
// convert attr.Object to the internal version
return a.convertor.Convert(versionedAttr.Object, attr.GetObject())
}
// TODO: factor into a common place along with the validating webhook version.
func (a *MutatingWebhook) shouldCallHook(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
var matches bool
for _, r := range h.Rules {
m := rules.Matcher{Rule: r, Attr: attr}
if m.Matches() {
matches = true
break
}
}
if !matches {
return false, nil
}
return a.namespaceMatcher.MatchNamespaceSelector(h, attr)
}
// note that callAttrMutatingHook updates attr
func (a *MutatingWebhook) callAttrMutatingHook(ctx context.Context, h *v1alpha1.Webhook, attr versioned.Attributes) error {
// Make the webhook request
request := request.CreateAdmissionReview(attr)
client, err := a.clientManager.HookClient(h)
if err != nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
response := &admissionv1alpha1.AdmissionReview{}
if err := client.Post().Context(ctx).Body(&request).Do().Into(response); err != nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
if !response.Response.Allowed {
return webhookerrors.ToStatusErr(h.Name, response.Response.Result)
}
patchJS := response.Response.Patch
if len(patchJS) == 0 {
return nil
}
patchObj, err := jsonpatch.DecodePatch(patchJS)
if err != nil {
return apierrors.NewInternalError(err)
}
objJS, err := runtime.Encode(a.jsonSerializer, attr.Object)
if err != nil {
return apierrors.NewInternalError(err)
}
patchedJS, err := patchObj.Apply(objJS)
if err != nil {
return apierrors.NewInternalError(err)
}
// TODO: if we have multiple mutating webhooks, we can remember the json
// instead of encoding and decoding for each one.
if _, _, err := a.jsonSerializer.Decode(patchedJS, nil, attr.Object); err != nil {
return apierrors.NewInternalError(err)
}
return nil
}

View File

@ -0,0 +1,649 @@
/*
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 mutating
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync/atomic"
"testing"
"k8s.io/api/admission/v1alpha1"
registrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/testdata"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/rest"
)
type fakeHookSource struct {
hooks []registrationv1alpha1.Webhook
err error
}
func (f *fakeHookSource) Webhooks() (*registrationv1alpha1.MutatingWebhookConfiguration, error) {
if f.err != nil {
return nil, f.err
}
for i, h := range f.hooks {
if h.NamespaceSelector == nil {
f.hooks[i].NamespaceSelector = &metav1.LabelSelector{}
}
}
return &registrationv1alpha1.MutatingWebhookConfiguration{Webhooks: f.hooks}, nil
}
func (f *fakeHookSource) Run(stopCh <-chan struct{}) {}
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
return &u, nil
}
type fakeNamespaceLister struct {
namespaces map[string]*corev1.Namespace
}
func (f fakeNamespaceLister) List(selector labels.Selector) (ret []*corev1.Namespace, err error) {
return nil, nil
}
func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) {
ns, ok := f.namespaces[name]
if ok {
return ns, nil
}
return nil, errors.NewNotFound(corev1.Resource("namespaces"), name)
}
// ccfgSVC returns a client config using the service reference mechanism.
func ccfgSVC(urlPath string) registrationv1alpha1.WebhookClientConfig {
return registrationv1alpha1.WebhookClientConfig{
Service: &registrationv1alpha1.ServiceReference{
Name: "webhook-test",
Namespace: "default",
Path: &urlPath,
},
CABundle: testdata.CACert,
}
}
type urlConfigGenerator struct {
baseURL *url.URL
}
// ccfgURL returns a client config using the URL mechanism.
func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1alpha1.WebhookClientConfig {
u2 := *c.baseURL
u2.Path = urlPath
urlString := u2.String()
return registrationv1alpha1.WebhookClientConfig{
URL: &urlString,
CABundle: testdata.CACert,
}
}
// TestAdmit tests that MutatingWebhook#Admit works as expected
func TestAdmit(t *testing.T) {
scheme := runtime.NewScheme()
v1alpha1.AddToScheme(scheme)
corev1.AddToScheme(scheme)
testServer := newTestServer(t)
testServer.StartTLS()
defer testServer.Close()
serverURL, err := url.ParseRequestURI(testServer.URL)
if err != nil {
t.Fatalf("this should never happen? %v", err)
}
wh, err := NewMutatingWebhook(nil)
if err != nil {
t.Fatal(err)
}
cm, err := config.NewClientManager()
if err != nil {
t.Fatalf("cannot create client manager: %v", err)
}
cm.SetAuthenticationInfoResolver(newFakeAuthenticationInfoResolver(new(int32)))
cm.SetServiceResolver(fakeServiceResolver{base: *serverURL})
wh.clientManager = cm
wh.SetScheme(scheme)
if err = wh.clientManager.Validate(); err != nil {
t.Fatal(err)
}
namespace := "webhook-test"
wh.namespaceMatcher.NamespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{
namespace: {
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"runlevel": "0",
},
},
},
},
}
// Set up a test object for the call
kind := corev1.SchemeGroupVersion.WithKind("Pod")
name := "my-pod"
object := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"pod.name": name,
},
Name: name,
Namespace: namespace,
},
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
}
oldObject := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
}
operation := admission.Update
resource := corev1.Resource("pods").WithVersion("v1")
subResource := ""
userInfo := user.DefaultInfo{
Name: "webhook-test",
UID: "webhook-test",
}
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
type test struct {
hookSource fakeHookSource
path string
expectAllow bool
errorContains string
}
matchEverythingRules := []registrationv1alpha1.RuleWithOperations{{
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll},
Rule: registrationv1alpha1.Rule{
APIGroups: []string{"*"},
APIVersions: []string{"*"},
Resources: []string{"*/*"},
},
}}
policyFail := registrationv1alpha1.Fail
policyIgnore := registrationv1alpha1.Ignore
table := map[string]test{
"no match": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "nomatch",
ClientConfig: ccfgSVC("disallow"),
Rules: []registrationv1alpha1.RuleWithOperations{{
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.Create},
}},
}},
},
expectAllow: true,
},
"match & allow": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "allow",
ClientConfig: ccfgSVC("allow"),
Rules: matchEverythingRules,
}},
},
expectAllow: true,
},
"match & disallow": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: ccfgSVC("disallow"),
Rules: matchEverythingRules,
}},
},
errorContains: "without explanation",
},
"match & disallow ii": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallowReason",
ClientConfig: ccfgSVC("disallowReason"),
Rules: matchEverythingRules,
}},
},
errorContains: "you shall not pass",
},
"match & disallow & but allowed because namespaceSelector exempt the namespace": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: ccfgSVC("disallow"),
Rules: newMatchEverythingRules(),
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{{
Key: "runlevel",
Values: []string{"1"},
Operator: metav1.LabelSelectorOpIn,
}},
},
}},
},
expectAllow: true,
},
"match & disallow & but allowed because namespaceSelector exempt the namespace ii": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: ccfgSVC("disallow"),
Rules: newMatchEverythingRules(),
NamespaceSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{{
Key: "runlevel",
Values: []string{"0"},
Operator: metav1.LabelSelectorOpNotIn,
}},
},
}},
},
expectAllow: true,
},
"match & fail (but allow because fail open)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}, {
Name: "internalErr B",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}, {
Name: "internalErr C",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
},
"match & fail (but disallow because fail closed on nil)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
}, {
Name: "internalErr B",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
}, {
Name: "internalErr C",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
}},
},
expectAllow: false,
},
"match & fail (but fail because fail closed)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}, {
Name: "internalErr B",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}, {
Name: "internalErr C",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}},
},
expectAllow: false,
},
"match & allow (url)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "allow",
ClientConfig: ccfgURL("allow"),
Rules: matchEverythingRules,
}},
},
expectAllow: true,
},
"match & disallow (url)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: ccfgURL("disallow"),
Rules: matchEverythingRules,
}},
},
errorContains: "without explanation",
},
// No need to test everything with the url case, since only the
// connection is different.
}
for name, tt := range table {
if !strings.Contains(name, "no match") {
continue
}
t.Run(name, func(t *testing.T) {
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("expected allowed=%v, but got err=%v", 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(" expected an error saying %q, but got %v", tt.errorContains, err)
}
}
if _, isStatusErr := err.(*apierrors.StatusError); err != nil && !isStatusErr {
t.Errorf("%s: expected a StatusError, got %T", name, err)
}
})
}
}
// TestAdmitCachedClient tests that MutatingWebhook#Admit should cache restClient
func TestAdmitCachedClient(t *testing.T) {
scheme := runtime.NewScheme()
v1alpha1.AddToScheme(scheme)
corev1.AddToScheme(scheme)
testServer := newTestServer(t)
testServer.StartTLS()
defer testServer.Close()
serverURL, err := url.ParseRequestURI(testServer.URL)
if err != nil {
t.Fatalf("this should never happen? %v", err)
}
wh, err := NewMutatingWebhook(nil)
if err != nil {
t.Fatal(err)
}
cm, err := config.NewClientManager()
if err != nil {
t.Fatalf("cannot create client manager: %v", err)
}
cm.SetServiceResolver(fakeServiceResolver{base: *serverURL})
wh.clientManager = cm
wh.SetScheme(scheme)
namespace := "webhook-test"
wh.namespaceMatcher.NamespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{
namespace: {
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"runlevel": "0",
},
},
},
},
}
// Set up a test object for the call
kind := corev1.SchemeGroupVersion.WithKind("Pod")
name := "my-pod"
object := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"pod.name": name,
},
Name: name,
Namespace: namespace,
},
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
}
oldObject := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
}
operation := admission.Update
resource := corev1.Resource("pods").WithVersion("v1")
subResource := ""
userInfo := user.DefaultInfo{
Name: "webhook-test",
UID: "webhook-test",
}
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
type test struct {
name string
hookSource fakeHookSource
expectAllow bool
expectCache bool
}
policyIgnore := registrationv1alpha1.Ignore
cases := []test{
{
name: "cache 1",
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache1",
ClientConfig: ccfgSVC("allow"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
expectCache: true,
},
{
name: "cache 2",
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache2",
ClientConfig: ccfgSVC("internalErr"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
expectCache: true,
},
{
name: "cache 3",
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache3",
ClientConfig: ccfgSVC("allow"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
expectCache: false,
},
{
name: "cache 4",
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache4",
ClientConfig: ccfgURL("allow"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
expectCache: true,
},
{
name: "cache 5",
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.Webhook{{
Name: "cache5",
ClientConfig: ccfgURL("allow"),
Rules: newMatchEverythingRules(),
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
expectCache: false,
},
}
for _, testcase := range cases {
t.Run(testcase.name, func(t *testing.T) {
wh.hookSource = &testcase.hookSource
authInfoResolverCount := new(int32)
r := newFakeAuthenticationInfoResolver(authInfoResolverCount)
wh.clientManager.SetAuthenticationInfoResolver(r)
if err = wh.clientManager.Validate(); err != nil {
t.Fatal(err)
}
err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, testcase.name, resource, subResource, operation, &userInfo))
if testcase.expectAllow != (err == nil) {
t.Errorf("expected allowed=%v, but got err=%v", testcase.expectAllow, err)
}
if testcase.expectCache && *authInfoResolverCount != 1 {
t.Errorf("expected cacheclient, but got none")
}
if !testcase.expectCache && *authInfoResolverCount != 0 {
t.Errorf("expected not cacheclient, but got cache")
}
})
}
}
func newTestServer(t *testing.T) *httptest.Server {
// Create the test webhook server
sCert, err := tls.X509KeyPair(testdata.ServerCert, testdata.ServerKey)
if err != nil {
t.Fatal(err)
}
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(testdata.CACert)
testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler))
testServer.TLS = &tls.Config{
Certificates: []tls.Certificate{sCert},
ClientCAs: rootCAs,
ClientAuth: tls.RequireAndVerifyClientCert,
}
return testServer
}
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{
Response: &v1alpha1.AdmissionResponse{
Allowed: false,
},
})
case "/disallowReason":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
Response: &v1alpha1.AdmissionResponse{
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{
Response: &v1alpha1.AdmissionResponse{
Allowed: true,
},
})
default:
http.NotFound(w, r)
}
}
func newFakeAuthenticationInfoResolver(count *int32) *fakeAuthenticationInfoResolver {
return &fakeAuthenticationInfoResolver{
restConfig: &rest.Config{
TLSClientConfig: rest.TLSClientConfig{
CAData: testdata.CACert,
CertData: testdata.ClientCert,
KeyData: testdata.ClientKey,
},
},
cachedCount: count,
}
}
type fakeAuthenticationInfoResolver struct {
restConfig *rest.Config
cachedCount *int32
}
func (c *fakeAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
atomic.AddInt32(c.cachedCount, 1)
return c.restConfig, nil
}
func newMatchEverythingRules() []registrationv1alpha1.RuleWithOperations {
return []registrationv1alpha1.RuleWithOperations{{
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll},
Rule: registrationv1alpha1.Rule{
APIGroups: []string{"*"},
APIVersions: []string{"*"},
Resources: []string{"*/*"},
},
}}
}

View File

@ -0,0 +1,19 @@
/*
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 mutating makes calls to mutating webhooks during the admission
// process.
package mutating // import "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"

View File

@ -0,0 +1,216 @@
/*
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 testdata
var CAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEArqnW4K+UsmPzbSB7JYhN0HNsJNItjw/87SJxIjGqUttC+2ts
+Y+BddcQgO0EfzP68QJ6+H7itmzdqWPTfoZJiuy+twXxgFwMH2WCKB6I5CgnVHcD
acmdP7vOgr1GcyqIV16NWEnt6hxRNfVnerduLWWfVr0/wGY3ajw06FHEx7oL1Jfx
1lo4UPVdp39Pt5rOww66FP8etRaEhy2AHEtfHS4L4GxcJL/0n3w8UIfMqYjTKW9l
zuKXUurz9HEJ3N+JMItolf+3ohMp/xciiSBvHzFbfu/Zv2nXqWGcxqOmnM1L8ZLw
Q4c3ZRQ/n9tIacEZyy69VASAQcIjMdwIHcWZLwIDAQABAoIBADybeJGMu6dPIYfr
bm/upTnA43S/bcmnvZc3jVRVMYoAxXRiqXTLhBu03egu1pGhIuGAf9U8ikTM7/m4
RwovZNONJPxzVoK47gfy/EAZoFyzRjp79bY+nI8iBx28ufZ6esb+a0OIm8LRwqhb
mGWvws6D5c9+aeHEVlRJwf4faY34ASEbw19QhOLfPCGp0wOy4MX3aIMaCfZ+iHYc
GAVaf44rWmTYKHEkLMABky9jGIXXJXROY/ggKWC1zXdPVChO40ECd1b2XGry6Ta/
j+quFXDgI7b/ju/7jLnDCQjuC5G6E7X3n8KLZtzJrReiwpyeFo86GWc5E99umOyB
tPjwNikCgYEA1mbNAVg3mBOFcEAN0j5QKEy1hv16nVlGRwBye/0nG3mUHCs+UzC2
fQyDfPcfXZERyDb4QJCJ4Y6VSVoo3Jn1okFPbz7y2eKdjo1N65T9HrK7G+QZXinH
/72LktfAWphdz2JKuSnrlJS8YupSx7pS+lpz+W+rcDpdYVJXoD6L6SMCgYEA0I1E
4h3MH5O46GJvMoGW4PH5FIi5nbWj69Y4/nRJCACNlLPJ+k3lcf0OTYcy9pllv5Ya
EV5n0qHAH7ACudKoB6YqvDsrZxfv8tlmWLBTFp5QQpBdlMWjgGSbJLbkxvt3rUfF
x/eQebvzSqp69R0/XqJ9fxWXvdtZoZYXNJxVPoUCgYAgA7W077FNegzA2C+4Jync
+qdYgt0eRchircRqk0CVr6/YDPT/gxSc05OGw3fhhtn65YpoSaztC1drXpUfa7Xs
BoiP+fxVYKtaL+tktBifztx1q7fGAcMlgu4mfSTx4jKP1wOFZqcQxqzisE6wGDhv
vbX3lx8oYO60q5D+EpjdtQKBgDM/A3YsrEP2ILG5vmlCvrh3vST2k+XVBHqnIUol
eOymdiPcKf1/tqnT7PfQCQ3fk8kIMU+jSw/O/07KCWFwCioXAtlOENQ8ZZHfKe8R
JNmh/UbeAqDUD+E014qmBoF+uWGzCT6h7rZ7IMVwLtacYT33366it67Hf7bdEsay
w5+hAoGABSgjlf9WsC7WzY6sZwZG25aBMGFw6vr3jawLiuNk3Hr+pGV/H7PEzSh+
vBpvC0Vkp5Dg32asmME40LbYpMu2BV4E1wK17i+DZVUMezNO0mABykWecyPYdmxL
bJtLu4yaP84W433T5E6G7Im+x+KjXI7TRzpQZFQnVadmmpuurUY=
-----END RSA PRIVATE KEY-----`)
var CACert = []byte(`-----BEGIN CERTIFICATE-----
MIIDPTCCAiWgAwIBAgIJALTyMgMR6YygMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjA0MTIwMAYDVQQDDClnZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAK6p1uCvlLJj820geyWITdBzbCTSLY8P/O0i
cSIxqlLbQvtrbPmPgXXXEIDtBH8z+vECevh+4rZs3alj036GSYrsvrcF8YBcDB9l
gigeiOQoJ1R3A2nJnT+7zoK9RnMqiFdejVhJ7eocUTX1Z3q3bi1ln1a9P8BmN2o8
NOhRxMe6C9SX8dZaOFD1Xad/T7eazsMOuhT/HrUWhIctgBxLXx0uC+BsXCS/9J98
PFCHzKmI0ylvZc7il1Lq8/RxCdzfiTCLaJX/t6ITKf8XIokgbx8xW37v2b9p16lh
nMajppzNS/GS8EOHN2UUP5/bSGnBGcsuvVQEgEHCIzHcCB3FmS8CAwEAAaNQME4w
HQYDVR0OBBYEFBsZKbynr9Iix+ud0FxQvMVIPZqOMB8GA1UdIwQYMBaAFBsZKbyn
r9Iix+ud0FxQvMVIPZqOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
AJZXfcHyAsq5qr1UqhutrNlsW2u7kkAc+Ql5wZEdXIyjKKC+kOllWqKo5IPtmMIi
R5VCm1g3iFCUV6FdXNtfF7tWZqaHV58nkJYlDc2yZxQzaWQeu8U92w+Qr5H1ansL
FOGS6A4+rMj2EDEt+lCmsz+l5UD7nrhnyGMzeumASQ6cXPV1uB2LTc4IzsOYFKs9
nt9SDH7tF+0bQwZ1YUrfMYJpNp6ETjpPKJVhq8/FGqwT+9egFbgjAhrEpPccFkXo
D7NLhM1JHUiqNQStDq9mDKLHKmp++UScrNc66b6egN1sIPBHyLu8ApzcF2YHuEYC
RGyWZ0sJtjzWjK7IU9RdmLI=
-----END CERTIFICATE-----`)
var BadCAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEArgYoZAUDj1/dQ2G+dMib+b1m39ACK6wBt4zDjl8x5iUZZjDy
O1l/Hj6n3NUHphFsYZ5070ds8GwXANvYGFi3F6GFiRW/R5lcChmhxhY5JJtWquDp
amonNEjiAuiFf6u6CKeBvm8CgcH/Cbzc4XP1oJZiSaGOpmqGiHlLeL25y6ZR3X7D
jOPalSi9WHQN0m9DS7EMvyalySAwzJKlcOpSeBPdMTp1Ay1HShJiiNj8sdCRLpq7
rwHypKS6mVNItsAnx+jc40d162dmFQg8FBm3M6d5QTrJBAfuvoLrlNCtssl11hOi
XDYJshTBVI9HAR/lzQiF+coZOHJwYuYXp7xYOwIDAQABAoIBAQChzSvkwxyqQ9Gw
AsNYReVv8IAj/HzoKgd2p7RzPWNhvoC9GSk/sViVwF/G3XM9HtoMcY37pAdQCs/g
hoeHK4UgvZcw/D1azuZapbZaPPNoa93LB08/F+/XlyQ83ACz0fEodsYVT5WfG8aL
QUSFgpGQfAJqv4GojUcEwPJBEvYat8I028fzYMlLJ9m45pQFzFsGKU36vs6esPkL
MKbVO1qI6CEVDtnLkIo2bE5vpo8w5C22HseFO+E+1VNaHIRK5TBBGGWVJVfUZ3bq
7LWngkaN9gspCauKkTozj2bl611lFRI7wbA32WV1eIYgNT+8jTESo/oHpsnpRSI7
4UMp5GExAoGBAN7wkIS4zWGF2hijsorcPLBx3YOsBxW26Qhl/ck6a3lVGZHrDijc
u8hDhOWNDxSSqUwQiL7UAVcE7npw6XpZ37Obc//t+Hm/gGUOIGlB/Pl3g4h7pF7T
s2pXIMKvF0dfQGpmqgCytUz7Oho4LLbkywky7jykMc8IlVuTZEdKbdZHAoGBAMfU
nR+79gT8yIBruEX3VI71Vbce0Pn+3+G+PO12uUN6XqMb9YA+f5aS6AwP5EupERwn
YvkMkCNSYYkV+GU5b+N9Pn7xt33dnEqhrGPUrOLoIAl6qJ7jc78GZy67SCuIKrZZ
AN5qFQlRCENv28C+0Ne6rMX+8/JL1Mxo+0J/6QRtAoGAbNhs5q/Hbm7IfbEmkY9X
fhoJuai6yMpF2hjZoG6KXHHFCy4E+sRSVkNI1j5Zd4TnbUDBUtH1WYQJ3vPTui24
/1rNds27u81YpX4RKvLRzQahzHf5V2bquOeTEhokNm915rz7EV4vEEe0JWr5wc3Q
p0wbbrYHr3oUWeKLWhcnqy8CgYEAh9XiHMFDIe7HSGxw7baLl0Xzxy++dEGp5CTR
+8VZeCIFlLCbuFpDlpI0BIcE891wEQhBAfRlQm1seagimoRpp2Tqh5Y92eQ7qout
yIq4HuIVbPwhBSit9Gsg1qZeD6FXD27+5TGNLTEVAepWofXTtuFhMpH1N34OoAi4
y2Jxfh0CgYB4IrPUeBAZKiC6Lo6nwxo0rrsiHLJwXJojLwsUt0zG5vh3mI25KAB1
a3ARRbiRKU/IX5I9ToclJ3h0a1nVr1kzV/E5f+5FgQ9swkTNEbM8gBsc5X9ZayjD
Hfv6+p7TH3bReXDKtpOUgso0dIy2anN6Ppu1wODtrFnUOJ9wkO4OSg==
-----END RSA PRIVATE KEY-----`)
var BadCACert = []byte(`-----BEGIN CERTIFICATE-----
MIIDPTCCAiWgAwIBAgIJAIaoBDrksTyaMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjA0MTIwMAYDVQQDDClnZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAK4GKGQFA49f3UNhvnTIm/m9Zt/QAiusAbeM
w45fMeYlGWYw8jtZfx4+p9zVB6YRbGGedO9HbPBsFwDb2BhYtxehhYkVv0eZXAoZ
ocYWOSSbVqrg6WpqJzRI4gLohX+rugingb5vAoHB/wm83OFz9aCWYkmhjqZqhoh5
S3i9ucumUd1+w4zj2pUovVh0DdJvQ0uxDL8mpckgMMySpXDqUngT3TE6dQMtR0oS
YojY/LHQkS6au68B8qSkuplTSLbAJ8fo3ONHdetnZhUIPBQZtzOneUE6yQQH7r6C
65TQrbLJddYTolw2CbIUwVSPRwEf5c0IhfnKGThycGLmF6e8WDsCAwEAAaNQME4w
HQYDVR0OBBYEFFFthspVCOb5fSkQ2BFCykech3RVMB8GA1UdIwQYMBaAFFFthspV
COb5fSkQ2BFCykech3RVMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
AEgGbcx1qhdi4lFNC0YRHJxjn3JPW6tr4qgDiusqMj9TF9/RohKOvLblq2kSB0x3
pyDMkVv2rd5U4qtKruEQ1OgY3cB7hy6mt/ZhldF540Lli8j9N63LMRXwIu068j2W
WSiWV416LOZEcuid7mZjAsbG4xvaDg/yW1RBpA3XnwMSmr7Y+T6XkjzgT3WWiwOf
4ANc3ecsl53x/beb9YF+TjqmjmtGSgUW78UTAsGFFKmjJ/cStQUaMCEvS9Gun7hH
eLarZIVV5Ia/FziGHoi7Q44C66pXD437xmkR1ueExoKwXbBt4c5GeH1rJjUVnlyk
pMokZBC57nXx8krZVEu1SRA=
-----END CERTIFICATE-----`)
var ServerKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA13f50PPWuR/InxLIoJjHdNSG+jVUd25CY7ZL2J023X2BAY+1
M6jkLR6C2nSFZnn58ubiB74/d1g/Fg1Twd419iR615A013f+qOoyFx3LFHxU1S6e
v22fgJ6ntK/+4QD5MwNgOwD8k1jN2WxHqNWn16IF4Tidbv8M9A35YHAdtYDYaOJC
kzjVztzRw1y6bKRakpMXxHylQyWmAKDJ2GSbRTbGtjr7Ji54WBfG43k94tO5X8K4
VGbz/uxrKe1IFMHNOlrjR438dbOXusksx9EIqDA9a42J3qjr5NKSqzCIbgBFl6qu
45V3A7cdRI/sJ2G1aqlWIXh2fAQiaFQAEBrPfwIDAQABAoIBAAZbxgWCjJ2d8H+x
QDZtC8XI18redAWqPU9P++ECkrHqmDoBkalanJEwS1BDDATAKL4gTh9IX/sXoZT3
A7e+5PzEitN9r/GD2wIFF0FTYcDTAnXgEFM52vEivXQ5lV3yd2gn+1kCaHG4typp
ZZv34iIc5+uDjjHOWQWCvA86f8XxX5EfYH+GkjfixTtN2xhWWlfi9vzYeESS4Jbt
tqfH0iEaZ1Bm/qvb8vFgKiuSTOoSpaf+ojAdtPtXDjf1bBtQQG+RSQkP59O/taLM
FCVuRrU8EtdB0+9anwmAP+O2UqjL5izA578lQtdIh13jHtGEgOcnfGNUphK11y9r
Mg5V28ECgYEA9fwI6Xy1Rb9b9irp4bU5Ec99QXa4x2bxld5cDdNOZWJQu9OnaIbg
kw/1SyUkZZCGMmibM/BiWGKWoDf8E+rn/ujGOtd70sR9U0A94XMPqEv7iHxhpZmD
rZuSz4/snYbOWCZQYXFoD/nqOwE7Atnz7yh+Jti0qxBQ9bmkb9o0QW8CgYEA4D3d
okzodg5QQ1y9L0J6jIC6YysoDedveYZMd4Un9bKlZEJev4OwiT4xXmSGBYq/7dzo
OJOvN6qgPfibr27mSB8NkAk6jL/VdJf3thWxNYmjF4E3paLJ24X31aSipN1Ta6K3
KKQUQRvixVoI1q+8WHAubBDEqvFnNYRHD+AjKvECgYBkekjhpvEcxme4DBtw+OeQ
4OJXJTmhKemwwB12AERboWc88d3GEqIVMEWQJmHRotFOMfCDrMNfOxYv5+5t7FxL
gaXHT1Hi7CQNJ4afWrKgmjjqrXPtguGIvq2fXzjVt8T9uNjIlNxe+kS1SXFjXsgH
ftDY6VgTMB0B4ozKq6UAvQKBgQDER8K5buJHe+3rmMCMHn+Qfpkndr4ftYXQ9Kn4
MFiy6sV0hdfTgRzEdOjXu9vH/BRVy3iFFVhYvIR42iTEIal2VaAUhM94Je5cmSyd
eE1eFHTqfRPNazmPaqttmSc4cfa0D4CNFVoZR6RupIl6Cect7jvkIaVUD+wMXxWo
osOFsQKBgDLwVhZWoQ13RV/jfQxS3veBUnHJwQJ7gKlL1XZ16mpfEOOVnJF7Es8j
TIIXXYhgSy/XshUbsgXQ+YGliye/rXSCTXHBXvWShOqxEMgeMYMRkcm8ZLp/DH7C
kC2pemkLPUJqgSh1PASGcJbDJIvFGUfP69tUCYpHpk3nHzexuAg3
-----END RSA PRIVATE KEY-----`)
var ServerCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIJANWw74P5KJk2MA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjAjMSEwHwYDVQQDExh3ZWJo
b29rLXRlc3QuZGVmYXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDXd/nQ89a5H8ifEsigmMd01Ib6NVR3bkJjtkvYnTbdfYEBj7UzqOQtHoLa
dIVmefny5uIHvj93WD8WDVPB3jX2JHrXkDTXd/6o6jIXHcsUfFTVLp6/bZ+Anqe0
r/7hAPkzA2A7APyTWM3ZbEeo1afXogXhOJ1u/wz0DflgcB21gNho4kKTONXO3NHD
XLpspFqSkxfEfKVDJaYAoMnYZJtFNsa2OvsmLnhYF8bjeT3i07lfwrhUZvP+7Gsp
7UgUwc06WuNHjfx1s5e6ySzH0QioMD1rjYneqOvk0pKrMIhuAEWXqq7jlXcDtx1E
j+wnYbVqqVYheHZ8BCJoVAAQGs9/AgMBAAGjZDBiMAkGA1UdEwQCMAAwCwYDVR0P
BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATApBgNVHREEIjAg
hwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVsdC5zdmMwDQYJKoZIhvcNAQELBQAD
ggEBAD/GKSPNyQuAOw/jsYZesb+RMedbkzs18sSwlxAJQMUrrXwlVdHrA8q5WhE6
ABLqU1b8lQ8AWun07R8k5tqTmNvCARrAPRUqls/ryER+3Y9YEcxEaTc3jKNZFLbc
T6YtcnkdhxsiO136wtiuatpYL91RgCmuSpR8+7jEHhuFU01iaASu7ypFrUzrKHTF
bKwiLRQi1cMzVcLErq5CDEKiKhUkoDucyARFszrGt9vNIl/YCcBOkcNvM3c05Hn3
M++C29JwS3Hwbubg6WO3wjFjoEhpCwU6qRYUz3MRp4tHO4kxKXx+oQnUiFnR7vW0
YkNtGc1RUDHwecCTFpJtPb7Yu/E=
-----END CERTIFICATE-----`)
var ClientKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAo3q//YITCuCWOSLPAdXSGUU+KvydADr63dy5CDTW5wBovnh9
9Lb0r7iZUkSklk0nMbqVILMGt87MuuW2sdge2paWMlhMlh1R5gWuKSQUahF6pHrD
O9fOeUEHxpK0hI1l/gGBKP5DjoGNALu9m2AEkUG02BXZJ0AVUpbtgXDtf6AbdSlt
ZrlCiETkMuzuYZ8xHDS/AhnR6d8TMQ0hh1dj2UpR0jrMmMg5Im3+D7r35NuiCSoy
LCYSecBLCQ4TZnylLHMLzhgOhReqCdwaKeunliDPascgGvtcabEak7grPzyD2nUV
Zt1yysTXnsV87OF+b1tuwXFFo6W3KGqTtwik5QIDAQABAoIBAQChJ5dts7VL6ruM
FXlViM/1Y2H2hFHM8VduMHEi2tvimm+nHCamf1jUhLh39fz9wY7aoeDyfCkqNy1x
LJQd2zwHJZ1ogcz1ym96vqzCF7QcH6Dz1aTyMDp1I5sjsGlNpgoeDKOjoos8Rw+V
4nz2VwAJpWk9/sOzwqOCaBA3ovgs7zdpPcFhMMle0v79TOUBQ/aa86X0xtRDxwuH
hT8Z2t5hPjSLAjLO9cT0i5bYVVVnvq1ZTGXETXEwi7mMI2HPLELdcTSwesxTTRpt
ACIJOuwHPK5KxxC2HFHTyS0THaCDCR9Hqk8lwKEa+CBmjeCc2MwXuYsVGKsm0FaZ
viS+fGBBAoGBANcglIBOb8WA+BR+F7Bi7jU3nVtalU4DAHKbhSYg8EAxMIuWMq14
UK+h2Qz2RrT1ezegVAm/NEqLX28vhYm1yz2RHDCUqAughKvhwNJ2mkLLREsJNATw
AMXDS2KhDPIsbJMKY66Gci17+q2FhyXiW10dxpTReVqnOiT1qeUilPkRAoGBAMKK
HG5EGaiF3brr8swsmPaqq33aXqo1k40/pd97xuauPIC/HBLeu+g6w3z8QecOKEYk
+Z5R/o/rsjCIpG4uF19lyAKZ9IgGpHX0rbEfWEyl5WARDOegXHGVfj1DNGhZEtO+
kSq1i5LteQSfRXvarbhbV7bKgvJYtLK5960XaM6VAoGBALyIPfzQQN5LL57t/p7D
pNWYvtwf37d1o//M0fzfYw4uzceXQySJy9SQN+NHNiJC/NB8PwonupEV4fZUJGjS
nKKBOL5OmZNPAtaLy2vnKzwcXeaQ0zj8iQDILZnrYKggTKr0sPVzuD6qZ7+IxS9r
V/ycKrujdQIAilF3xoQcMYixAoGAfx2NvENFXMez/brFGMKfZLZafk7dAm0lr+sB
8MjJS9xX7mxx5Kajs/gJ2rZePaMTj9oDPX8oTlRdR7dRcikt3oj8Ky78CJIGjojF
ofHwWY0hFyes/gDbxuA+77rlGLXzRmbEJlsgC26eX/XOikJ2tvsAkpE7BS4PTKWV
gAXG1w0CgYEAq4rhFKVi37qKI8kVHO5O3lvRfKOmiMs2j2X+3T2QSTGcGRZD31EO
ImRWsYCAaX97313xYhjTT4jJzNU4fdnJ5hFte+Nt7BK1h/ze4+0XGJBK7wnDqaqg
kL0SB6nxr/Gqnhwx+wEaLkfhiy7Gx0E0IoSGEELsW/MMgvzzAo1/jaM=
-----END RSA PRIVATE KEY-----`)
var ClientCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDVTCCAj2gAwIBAgIJANWw74P5KJk3MA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjA4MTYwNAYDVQQDFC1nZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jbGllbnQwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCjer/9ghMK4JY5Is8B1dIZRT4q/J0A
Ovrd3LkINNbnAGi+eH30tvSvuJlSRKSWTScxupUgswa3zsy65bax2B7alpYyWEyW
HVHmBa4pJBRqEXqkesM71855QQfGkrSEjWX+AYEo/kOOgY0Au72bYASRQbTYFdkn
QBVSlu2BcO1/oBt1KW1muUKIROQy7O5hnzEcNL8CGdHp3xMxDSGHV2PZSlHSOsyY
yDkibf4Puvfk26IJKjIsJhJ5wEsJDhNmfKUscwvOGA6FF6oJ3Bop66eWIM9qxyAa
+1xpsRqTuCs/PIPadRVm3XLKxNeexXzs4X5vW27BcUWjpbcoapO3CKTlAgMBAAGj
ZDBiMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC
BggrBgEFBQcDATApBgNVHREEIjAghwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVs
dC5zdmMwDQYJKoZIhvcNAQELBQADggEBACDF/OlwaoxLu4h4bvyNJnuQdsw3O2Zz
xEADJOkeqM389hYmTlfyPFFhHocFW79ObUxa+73haBXTI6wFP0wSr2jaSQ86j85/
V99S8WP/D4jmVqXXTe43o3WvvKFUHfJ7BO4OEHED0orRe11IcSkP8emSHHehqXxg
V0P3s1cZao7pPplRSZjcOC5dimEfKnx7ibBh22a8wjq2vPbGxTDf56nkeq4/fbc5
MaAAeVpyFlN6ueREaz7ixy0r3yLMhC9xr4E6p8VvWsYBkQHWyukiUzbwVUwpK+Rw
Hy80c9+1z7X9/eKr9N/fzwbfrGjb3rbi7o1UHEEwiLaq1a+Df6dP92o=
-----END CERTIFICATE-----`)

View File

@ -0,0 +1,19 @@
/*
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 testdata contains generated key pairs used by the unit tests of
// mutating and validating webhooks. They are for testing only.
package testdata // import "k8s.io/apiserver/pkg/admission/plugin/webhook/testdata"

View File

@ -54,24 +54,24 @@ DNS.1 = webhook-test.default.svc
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"
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"
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=webhook-test.default.svc" -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
openssl genrsa -out ServerKey.pem 2048
openssl req -new -key ServerKey.pem -out server.csr -subj "/CN=webhook-test.default.svc" -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
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
outfile=certs.go
cat > $outfile << EOF
/*
@ -95,8 +95,8 @@ 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 validating" >> $outfile
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
echo "package testdata" >> $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

View File

@ -151,16 +151,16 @@ func (a *GenericAdmissionWebhook) SetExternalKubeInformerFactory(f informers.Sha
// ValidateInitialization implements the InitializationValidator interface.
func (a *GenericAdmissionWebhook) ValidateInitialization() error {
if a.hookSource == nil {
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a Kubernetes client to be provided")
return fmt.Errorf("GenericAdmissionWebhook admission plugin requires a Kubernetes client to be provided")
}
if err := a.namespaceMatcher.Validate(); err != nil {
return fmt.Errorf("the GenericAdmissionWebhook.namespaceMatcher is not properly setup: %v", err)
return fmt.Errorf("GenericAdmissionWebhook.namespaceMatcher is not properly setup: %v", err)
}
if err := a.clientManager.Validate(); err != nil {
return fmt.Errorf("the GenericAdmissionWebhook.clientManager is not properly setup: %v", err)
return fmt.Errorf("GenericAdmissionWebhook.clientManager is not properly setup: %v", err)
}
if err := a.convertor.Validate(); err != nil {
return fmt.Errorf("the GenericAdmissionWebhook.convertor is not properly setup: %v", err)
return fmt.Errorf("GenericAdmissionWebhook.convertor is not properly setup: %v", err)
}
go a.hookSource.Run(wait.NeverStop)
return nil
@ -248,7 +248,6 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
if ignoreClientCallFailures {
glog.Warningf("Failed calling webhook, failing open %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
return
}
@ -280,6 +279,7 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
return errs[0]
}
// TODO: factor into a common place along with the validating webhook version.
func (a *GenericAdmissionWebhook) shouldCallHook(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
var matches bool
for _, r := range h.Rules {

View File

@ -38,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/testdata"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/rest"
)
@ -96,7 +97,7 @@ func ccfgSVC(urlPath string) registrationv1alpha1.WebhookClientConfig {
Namespace: "default",
Path: &urlPath,
},
CABundle: caCert,
CABundle: testdata.CACert,
}
}
@ -111,7 +112,7 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1alpha1.Webhook
urlString := u2.String()
return registrationv1alpha1.WebhookClientConfig{
URL: &urlString,
CABundle: caCert,
CABundle: testdata.CACert,
}
}
@ -578,12 +579,12 @@ func TestAdmitCachedClient(t *testing.T) {
func newTestServer(t *testing.T) *httptest.Server {
// Create the test webhook server
sCert, err := tls.X509KeyPair(serverCert, serverKey)
sCert, err := tls.X509KeyPair(testdata.ServerCert, testdata.ServerKey)
if err != nil {
t.Fatal(err)
}
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(caCert)
rootCAs.AppendCertsFromPEM(testdata.CACert)
testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler))
testServer.TLS = &tls.Config{
Certificates: []tls.Certificate{sCert},
@ -642,9 +643,9 @@ func newFakeAuthenticationInfoResolver(count *int32) *fakeAuthenticationInfoReso
return &fakeAuthenticationInfoResolver{
restConfig: &rest.Config{
TLSClientConfig: rest.TLSClientConfig{
CAData: caCert,
CertData: clientCert,
KeyData: clientKey,
CAData: testdata.CACert,
CertData: testdata.ClientCert,
KeyData: testdata.ClientKey,
},
},
cachedCount: count,

View File

@ -1,216 +0,0 @@
/*
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 validating
var caKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAt8E1XykA4860Tj7mypnsSU+hW0taUEvz26a5rgFSrwgKe1g+
zOXc0XoAdnWivWKwWXTW+P1mmjMApEf8ndfPy+juKIrPKP6ccF31iPvOGfNRm/g/
ulZAJnAjBn0zkZ9ARhpdDxKwDpwIKSrTna5GB/gX0VbHQ/M23u0RjVUZuNM9cayW
HReRx7xlOQD+uREQ/wh1zkgQime+rji5U3jxB9YD3zfTeGBJdrq9ptTdkPEIQUKf
DM8SnM8fleEPkBq3XrhfmAEfkHBGpn4Hc82tk/oEZ+LMyMaR/GmzdJXU2/d4zixC
Dgqdg1nB76uXJ4aITX2BuR+ttmAI0b6Si4UvHwIDAQABAoIBAA/Msg0blnL/++ra
Z7e14mYvTZ1u7jYHQdF6FW8LuBNKqrQOU2AEx6bPSajl1ndYO/eFH1LLXv4VMpHt
ip/7xWcwAQJFZSiOM99JhOohVIhQroytnLUl42AqtihBraRwv/MHI0c/gRnQercn
coiVSno2771VK88A44/pbF/tmEeW5Nq7bwHrtjdt3MDKjv2LQaPToDzBivSwz/F8
3dMBpCUKT3tKC6QiDAFi4WaVOqXZrDfm/HJ1L8LWYjrcGTwwzMGpDfQEMxhq+2AR
Ya89jKF1I2+3kgXrZER7eHktUEQ0bGckSmAN9yo01rdm7E5gmmPuTK13riSFWrJn
/Dg21PECgYEA3vM0OLGevaUP4geBv1nPLpANC551Wf8wu2QG+Ts1/LFQHtFTOTst
JjFy/XT90ki1wni8P/pcIbMEXDJJezR4giWfiwMzr/E4arkJc5rJwV3kcmGrVihS
9BIJVlWq8kPmklTctfoqjDMa7tkYZoYStg+1Xljvw/HJFqZ6VoWyxFcCgYEA0v6S
Fx960kQyqPYyQMaZpce9rAsgGBJ4uMU6dXVfYxDy0CEKZ1lV1xwUg9eWNFj7E46A
RJDl9fR2KztTbgBobEOCVlO9QftY8RiIzibq8R4P5XyEV+TCkPk+eYffDZfOueGK
uCzBcAcl12SkAy/KMeS0+/+KYfetGyh23GH/bnkCgYEAnqMefVilQvu4GXSN9cHJ
kbAeGC5gAfF6k1vROnXPLEZeZA890HMy5QI6d+5OzNm/uuh9ymgyNihS6ec+MdRc
Cv8KTrewh3h0VDvlZcS12kkcy+aDK4L1w4Ux76R1RnzaCzUm9rVSoP+cImeG3Sx5
E+KJguB1ek8Ibn12fyoS0XECgYEAvKyAHsU7o0Lwuj5NebceNiyC45GfRWdfJHrZ
Z6dpgMDrIEorb3dnV0/42Fy0KGNZQYewE6Auwt2zvbzzQe6Dcix8JI4FMzd7tTxn
OVF7zdlABcpu3dnmUpVO1IY3Y4RYi8evsDn1UCRUJmQMdf0KJcuKO72rFSfRV/O7
Nh87tqECgYAN21eEb68fg7z7WqnUhL24SilIFNx7qmyhFZ5ZbIuNBrCOW4lxj7z4
1A4WyB3Wj2CeTeFrFamq+bgI5gl4DNzcml8bFxij3WDbwzq6C2FTJ1U+Ax/Z9Y60
qdc1fMRy+0Wkglk26VZ+xA4gW0eetIDXf/IocFYZ56ti36lWfXMKFA==
-----END RSA PRIVATE KEY-----`)
var caCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDPTCCAiWgAwIBAgIJALl4JUWeGrsQMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTAxNzAxMDcyM1oYDzIyOTEwODAyMDEwNzIzWjA0MTIwMAYDVQQDDClnZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBALfBNV8pAOPOtE4+5sqZ7ElPoVtLWlBL89um
ua4BUq8ICntYPszl3NF6AHZ1or1isFl01vj9ZpozAKRH/J3Xz8vo7iiKzyj+nHBd
9Yj7zhnzUZv4P7pWQCZwIwZ9M5GfQEYaXQ8SsA6cCCkq052uRgf4F9FWx0PzNt7t
EY1VGbjTPXGslh0Xkce8ZTkA/rkREP8Idc5IEIpnvq44uVN48QfWA98303hgSXa6
vabU3ZDxCEFCnwzPEpzPH5XhD5Aat164X5gBH5BwRqZ+B3PNrZP6BGfizMjGkfxp
s3SV1Nv3eM4sQg4KnYNZwe+rlyeGiE19gbkfrbZgCNG+kouFLx8CAwEAAaNQME4w
HQYDVR0OBBYEFJ+UXeXeN9DfxuCA65LuhRaXI5bnMB8GA1UdIwQYMBaAFJ+UXeXe
N9DfxuCA65LuhRaXI5bnMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
AJlfuK7LcFxXCjB6rnRYvjIFF3JewaPGx273YDpV2DkvCJXvez+mDC6ZfD1U7nEw
P+0A+NJctU2Vv+bbhh1vXlmBUoA9zKyIPld0pXDt8PxK8L9QRdLdzN96MtUjE0Cr
jBDxy3eIke+ElQQyU3MSx1uohZao40WOTDvR1fBswrFGhFNtELgwT3zIJ7tMO4ws
1f6LRVB4xsD6cPNUmFyJW6UqecJ3ZSeErF0r8uN4Ta0zUoJ03CflwsgujoVNpIPR
/VVElRE6Cd2C1i3qLBMOZ+aQrxGw4teNXilXbKtwJzpDYU+bypPgim+vxAwD9XnQ
J/rCqIaSYOI/3WkZmQ+E6aE=
-----END CERTIFICATE-----`)
var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEAsLjPsYMRv5T3+OoPbE+mKqUkBrn6ZQZU28UulLaS8UsaX/Dx
o/N2P6YGSlBeEnO0IV7fHqfdlgTRqqhlhX9L+suj963skZUmwoHB54lsHBYnXEBr
UlkVn5Wv+BEp5FX9iC9EV+wyWxuddGqElNUSMvuC71lRAE1edKbFZYIgqpGson6u
kcn5cpBoyFjFyxKhuKh5WTCQGYFZBtIhcggyduUw5+JzbPUlrlOcCiW8aQwL0f+s
9vA+4lDcngyOXDgAOip/Cr3lYiajhv41TyMNR9aSCH3gwI06QabQpXeHmYKjdMDO
epo9czAOJ2ZI+o2sKRZGUvyfACg6VJpU2jiWdwIDAQABAoIBAQCClDxbFNcDcaZ8
1S4KQRwt/JIPKlJ7XV9MeHl/xxvykSTu6VETbOzeAOY6+QFZrwbVdY110GGp3Ouz
pvRE3ReeO+RvOaNIuyXFqS1G0UMBydjRkIP8d/jDT06UBNKodmV8wDhGoy9eJJyG
jcJjWsE0zKUmCCATEhgOJ8BJzgonHXwSkgjhBophYpDNGi4W1Ke8a80V0S26UOsV
b72mBqPYDAcGNR9dFpJGYRRdZ77SjcAAOFOqnU6KXUFHvitNYSkUG22wHa4+yuO6
fTDUitdXhxCcFCWEFyX6Chm3lmf1NgSI4bni46K6/BC9mlNjZ+7QrTJTTp1/WyHj
fV2TnwRhAoGBAN2kTatss76FZwKFamPSvX45ulo0/U9LB1Eb9A7/M+enfAJGPPuk
LRdy3JjDiEKxgsBlA7zy0Gc9wUjBGmPBOiJmCTfmMykoqV1qVH5YQcuVUaKE589a
iUAUn6PNcNsnu2Z8BIDU3Fvyq2KJammSz/zqNtkH7I6SDur6Sb9SRJORAoGBAMwd
5OkOOGvZKxnJz8s5hwDicN2Fk5zxWpA74mgUt44sOiG3F8/7y/ntT/O3DGKz/yDb
2Ju6Yf3ojqYnirAPlWLxIm0Y3K9gNQffJ5cpe/kCzbhCLAnLk6YXu8ZVoMMq/0LF
f0bh+UAktqlFi5Tl+0LGAGd9wWxAe7/DHmolBfWHAoGBAJxLy/Wx3wLgQebWPFMO
flAv10jbizHKX+uDgdS9hFW8lsdnzoNJn/6kIgmcAU++q8yOr1ckB3B2bQGoIrrr
vNobCC8iJzvED8LvQ4whIqy0rG+lt25SkuzcXkL9kbMJzq4TkH1lHcu9UbxX2PF/
9SmN5IWhf+B+AQUU4MKI+hDxAoGBAIRXrZ/d9H8Yo3VpAC2H8xyDtSIsBXVwl4OF
EFrjc8/epSJPEEVtwOcfEwO133Xvtq+bW2o9AmQacMMSSD23HOi159hMkmmzOy8L
ZSQBZbwiMTgSz3LaZ7T9FmaWBlIEgtTMMKXIxk7sfvJpgQLdyneU4ZY4VzzU4meH
HyU7NA3pAoGBAM53r4pKMsdH9c4w/aRiRZeRge+MdPqdBo4Kje50uXVUaplJr1fJ
9Bm2P1oSfr+Zh6pNyDQE2OhptOxskd/+XDC4i6+MZ1iR1JHFq4oTF5OUYc3Je23G
FjD/vUn+ha2d750IPhsDztb8XyGfQdn7oo0ikhg1Ayjix6LE0GAd+Ve2
-----END RSA PRIVATE KEY-----`)
var badCACert = []byte(`-----BEGIN CERTIFICATE-----
MIIDPTCCAiWgAwIBAgIJANumDUaVJHIhMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTAxNzAxMDcyNFoYDzIyOTEwODAyMDEwNzI0WjA0MTIwMAYDVQQDDClnZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBALC4z7GDEb+U9/jqD2xPpiqlJAa5+mUGVNvF
LpS2kvFLGl/w8aPzdj+mBkpQXhJztCFe3x6n3ZYE0aqoZYV/S/rLo/et7JGVJsKB
weeJbBwWJ1xAa1JZFZ+Vr/gRKeRV/YgvRFfsMlsbnXRqhJTVEjL7gu9ZUQBNXnSm
xWWCIKqRrKJ+rpHJ+XKQaMhYxcsSobioeVkwkBmBWQbSIXIIMnblMOfic2z1Ja5T
nAolvGkMC9H/rPbwPuJQ3J4Mjlw4ADoqfwq95WImo4b+NU8jDUfWkgh94MCNOkGm
0KV3h5mCo3TAznqaPXMwDidmSPqNrCkWRlL8nwAoOlSaVNo4lncCAwEAAaNQME4w
HQYDVR0OBBYEFAYhiaN5L1bHf3VQO9bAwLTCFOyZMB8GA1UdIwQYMBaAFAYhiaN5
L1bHf3VQO9bAwLTCFOyZMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
AAb6qDKiWDuYbFI3f/AGkHaiWnVrdU/2oQ1P03N/CD0DEGzRTmEQrl5l0pHDUJ5g
XTszW/5Bgjgzx8HLG3VeMQlZpCeGrUBsWWIlGFsdfAKQ8xkB1JYNCtb920WBCOpZ
FewLzQwbRyeYju+VP7lq+IF3htOTbeXRax61c0qu2o402NbKCNMlwAWMWl92dji5
zJG0U0g+U+GC1QVyRlpf6hsXONgNOWTuZJgZDmN0exZOH7rF9syr3lfDwX0I/no7
zIJHt7Tx9oanqZbj/Oe8FC4jYNJiZryQ4MaWBk7Op+aR5bUgk5SwA9pLuKLm3Zyl
KkC1zS2x3RD1WxxiJjXcJ3M=
-----END CERTIFICATE-----`)
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA4Slves29h8gstITXD8cuHT0sZzP9x7Ip5pcSrQgjIpk9V7zB
ulRsFfTaPVMAzq5nlX5qbM6OezidzdC0oVQi7DCRd+TqQi2zNDJNeWAjDMkRa6ir
vq9XNuQJlMxi/YdFzjWl8LPMyq+WoW+0gtM86jPOEk69iw0nFqmUTt/4WXLOIDH9
ffIiZ6h+x6IJGFqcn4heNE9SK++bMmjijmhIF3i0dKquApVm0+E/NYDDHLzeQ42r
BhgPWmuLqGBvVavTHweXRJ9qdZ5LVzMJj1KNNLA4T/vsk17jADJGPikrvZWZ0MJE
SPKpDAJ9uxDpIGNg8EBdUoStfrQMMsDEnNE1/QIDAQABAoIBAQC/ROumbk+qoKkZ
UB9BD/pkbCrkII5crURa1crPojH2miY5+ea32i9XF4Csx23QJOdpXtIZS/5NPnMO
+1P2F/rymO954cP+I8QveuvFR51+pu9dfRMoENsNjfl1pYoRxG/QFFK6foJhS3ex
+6pj1/3PFeLgOnNZQ/sIjVWnCyt1DzFUaELE1jsDWtsQ9j2ed1ceqjrEVG0BbB8O
LeXUp0lDWgRmF9LexFVd361ew6DM4Zr3l9W3Dubd/6gIPNSRCB7T1usErFKPi8Dq
ML2lnO98+gKZHp6DKUISWEwt0By3QRLV09fBTes8fr+gylZfhwX2Q2EjiZ/v0Nlb
YGLAZwMhAoGBAPigApUlA8L0AGgVoSuBRthdFVzqLKTIKYimiZirRnXljTs3HvS4
Z/RCkXRVP6aQ8tORVUyct0drBUgddu+YPS/hfk8ipYraGonAKWfxL2+sFcxHg9iy
O6OxR7jOlI7bG8Ue78lJjqfzpXyi2/ikL2udHLzMeCh3czzix4wKXl2LAoGBAOfX
QKCudMGOHb2LCkC1jP2VLUAg09Y7q7sX5avNXhZsLNhVgu1i8FNTTXXCHwyBLvxY
AX7AnYaUP5K1YOyHVXnfqp2uuctR8tm7TUh9mw3RGnE+D9yw39RjlF4ZjZMtBC0+
5/A2upRqY5H2pepxi/bPCAC58UgHbvjTS3LD6zuXAoGAQUj7BKDgmPurc6liVeMv
cDcZGfnf2TE6PsjETtOCwAiUCl2SAl695VTpjuunuBxNtyJtjJ2GPvmqPGKITafj
QURr/2mwoIJe/5b3CHU7qI4+dxK8W1WJ9ZTiqXONbOm6JAvYmTl4fT+sT8sQCf1K
+m4aErV6Q94B45YFIg/C8bsCgYEAx2pkAZHthasrM6UL3ZsLufb9pCJYc/aBgX1N
pRgRrPHBJRdwdaXbl6CYiQi/Ui8v7gf4yUD+fgq4IAX5Z5oE0L6tb9Ihp5xGajfs
gsTfgOPyfaNnW2mcLYC11rbeCtD2vcBVGk7I7+4O1Tc1gVHHlTSA6rcFrfIO5uJA
DGguxuMCgYB9y4JY44YLVpxbHOmQcp5XzN2uDESkBDmzXhn6Kx4wyyMI1/J2a44p
68AL0TaVWK1vvzV8X4f+92ufvUCXuuItIVDvkSdKMl6kL54djCA25tGuxNHxMk7e
/l4fshoaRwF3ybwHbREMOy8pQHrsek7m21sC/q/DIDN/IPdqo7aaVw==
-----END RSA PRIVATE KEY-----`)
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIJAM2Hyhl1N+5sMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTAxNzAxMDcyNFoYDzIyOTEwODAyMDEwNzI0WjAjMSEwHwYDVQQDDBh3ZWJo
b29rLXRlc3QuZGVmYXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDhKW96zb2HyCy0hNcPxy4dPSxnM/3HsinmlxKtCCMimT1XvMG6VGwV9No9
UwDOrmeVfmpszo57OJ3N0LShVCLsMJF35OpCLbM0Mk15YCMMyRFrqKu+r1c25AmU
zGL9h0XONaXws8zKr5ahb7SC0zzqM84STr2LDScWqZRO3/hZcs4gMf198iJnqH7H
ogkYWpyfiF40T1Ir75syaOKOaEgXeLR0qq4ClWbT4T81gMMcvN5DjasGGA9aa4uo
YG9Vq9MfB5dEn2p1nktXMwmPUo00sDhP++yTXuMAMkY+KSu9lZnQwkRI8qkMAn27
EOkgY2DwQF1ShK1+tAwywMSc0TX9AgMBAAGjZDBiMAkGA1UdEwQCMAAwCwYDVR0P
BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATApBgNVHREEIjAg
hwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVsdC5zdmMwDQYJKoZIhvcNAQELBQAD
ggEBAFwJ5UtHJsSx7mrJ7X51XDVg8ocNdWYLuehWfpM2Tlrv9kklONF7VehS+9kg
MjSiuXtIhtEEN7GIy08sl6rhANtwXxWhj5b+qPNSNiHGNRvmHkCJuO2PGG7TpSpH
CfOgX+HH9CnX/piC7Uqr+vmS+SmhSjIyw1bUtP9cDmFvNQB9/0qvcXZP/oX90jsa
qF6fQvKP/OtRcW/kyWmhzqeIMufru82Hbrf/WJuQXCvpgtY43cOlHVEr8X2PbF9F
t4eliujfewSu1cyXNcT5KcriCvZyXU/d8UHm+z9rnMJdC4bfvwOLeG7VmEG1Vp/X
sRiMsjRcun8Jvbl7BbH86nu1xus=
-----END CERTIFICATE-----`)
var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA05j8mb6tyOYOxfdmra9nlBu+VefZ46d+Q018EEi8LLpXFovP
+4D6mdZiG2K1Or9Kx7wpiG4gyrqn8aENTEbREtw7+GWpsrtWcOnmtCoMrcqV97S+
TurL5DY1Rh9XOBxa/QninOnP4O1dASYmi0uZBZW/Neidwt70/sb3VGxj8Ex8rzD/
LWsiVj+ijkK2A18+S2m01HNuNramJiJ8Ns5VxHj5TspRIBik1NEfEDFEi42ddZ0n
9q4u3eZmFC1QqMLB/371+oFrieC+gmbg2FCMUw4LIiUMWURhUP8kXNEFCDWvTLbM
40wU6lejJw+JF0SBs3AIcI4GJeSfxEuT2AFCBwIDAQABAoIBAC3Rb8ke1+SrpEFL
vAkZ9TTF+SYC6VR5XUbXjWi9RznsM5VnOub728fZ+y5w5ktNRrUPUnL/XcxoNJuG
wylkIDuUQswbv247UJFspI8Yl9w+BNE5awgNoY7OCiUf/jPhN/aY4GAX5PKQk1X/
W9NH0F+8OEZFE3wx6R0OGlpGijFrCbEoqfMcReOVkXt9jjHAxHxI6erMlQvQ6Jop
KZ+OndRt8ilNtcjZLxAK8d3odgXN7OGezi74/VnG8b6NVmXsZMRkgS5xcP/42h2e
GDGGm4Gia8x0lcosgr+LdZ4FMEITj2p84PGNoeoh7PtTMUQs2qnfPPbpQpab6w9/
7j7jRXECgYEA7/R8ByzNFUfcBOW0F8mmQ4bAmJcjuVEBay/XFf8gxWrefxsIEBu/
n3GPI10bxGdEpSQGln1P9VCyZPauHPQC6DNB2OQjbmAujuigj2uM3GbXmYt20z2s
iUHhZQstznEO+BLqOMhk2SICnwsecPn0jP2MvWS7tY+szwvdLROHZj8CgYEA4b8V
iGU8/3mXGYRsT/tuuWjnalstCYzKosK92K8PS6LnAjv8t/8CMrL5gHAGXdf6fEAh
qDndlB1VkQ/ymiqR35el/ErVRt1/2pbwTLSQzAGJawY/osnClMnShO8gdQnsJnmi
zx909lWVxrKkN38szLQfK2bq4C8z6Bw5+6IxAjkCgYBnEUas9kto1qLk352Jki3+
V0UmxdSsZuULG1NxuVJkOdE0G3JNKP4YCHkJIZcpt4m+vUivH0hXAMB/qY2EFjOh
dVLVTLkDUgDtlXJR6Epq6Sm2ZDc36QfRNSERe8nDIMDjQYylsz3OHlOt6OK8eEDY
xpfLShdulzYNAPWRxQ+llQKBgQCmWEvhqdf82PgCkZXOihPZA/giYvUY6GoY7S8/
kB/ROETJXLKoUnyoJ0G65tGKLTAiho9Giv0/uy3mKr4149CB1hk1g18NTQJ9bGO9
4gAgk7FS79PMfKepQ96gniRomdstrsvNm/xv2Dj5pYFkc43reX7OWJQShjXVf5cq
WSWL4QKBgGVoe8yldF7dijRgB0NYJ7LV+xTQxLpSzK+3b5LyrvSTMwGut0O6QsbK
S060B37PdwBxMD05yTy8Jcr0abl9+tiLGAdok5Ufnm2e7sNsxf8fLhsyPuReutB8
zg1721jMeo9rwnduEi4/U1PIjqYUBIH/jjc9RHSvhk+MVUKXS95R
-----END RSA PRIVATE KEY-----`)
var clientCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDVTCCAj2gAwIBAgIJAM2Hyhl1N+5tMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTAxNzAxMDcyNFoYDzIyOTEwODAyMDEwNzI0WjA4MTYwNAYDVQQDDC1nZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jbGllbnQwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTmPyZvq3I5g7F92atr2eUG75V59nj
p35DTXwQSLwsulcWi8/7gPqZ1mIbYrU6v0rHvCmIbiDKuqfxoQ1MRtES3Dv4Zamy
u1Zw6ea0KgytypX3tL5O6svkNjVGH1c4HFr9CeKc6c/g7V0BJiaLS5kFlb816J3C
3vT+xvdUbGPwTHyvMP8tayJWP6KOQrYDXz5LabTUc242tqYmInw2zlXEePlOylEg
GKTU0R8QMUSLjZ11nSf2ri7d5mYULVCowsH/fvX6gWuJ4L6CZuDYUIxTDgsiJQxZ
RGFQ/yRc0QUINa9MtszjTBTqV6MnD4kXRIGzcAhwjgYl5J/ES5PYAUIHAgMBAAGj
ZDBiMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC
BggrBgEFBQcDATApBgNVHREEIjAghwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVs
dC5zdmMwDQYJKoZIhvcNAQELBQADggEBAAgu8K/+UA6V7+AiOPP0Hs3jGTsVjnPB
3XRCSWof5LL93iSRu1rI5LYmjS4N80lV0JkaJNvsoAKxETS3MW4rgv6t3kFOyLMw
mTfIli3iSBMz4WF55px1yhgF85wghEv2+YRF9aSUqAyz4DmlTGlFCEUx+ntkysUD
F97k/jB56EJVqMpSoY5O81vxr21Jpzlryd/UoMVwhYuO3tN0FP+PjoRiQhCGdQTz
2H+TQytZ6Xx6B8BE/joh3WBnQ4705jFhFaDSP8DSH45r48dzbxNLJVNeqLQQ0PhI
clrHwa1WiAnv+4Ydc5CiXGjLjU0sIvETjVtQGPv/gAykeVJo/4nXM8c=
-----END CERTIFICATE-----`)

View File

@ -14,5 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package validating checks a non-mutating webhook for configured operation admission
// Package validating makes calls to validating (i.e., non-mutating) webhooks
// during the admission process.
package validating // import "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"

View File

@ -28,6 +28,18 @@ type Convertor struct {
Scheme *runtime.Scheme
}
// Convert converts the in object to the out object and returns an error if the
// conversion fails.
func (c Convertor) Convert(in runtime.Object, out runtime.Object) error {
// For custom resources, because ConvertToGVK reuses the passed in object as
// the output. c.Scheme.Convert resets the objects to empty if in == out, so
// we skip the conversion if that's the case.
if in == out {
return nil
}
return c.Scheme.Convert(in, out, nil)
}
// ConvertToGVK converts object to the desired gvk.
func (c Convertor) ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind) (runtime.Object, error) {
// Unlike other resources, custom resources do not have internal version, so

View File

@ -130,3 +130,90 @@ func TestConvertToGVK(t *testing.T) {
})
}
}
func TestConvert(t *testing.T) {
scheme := initiateScheme()
c := Convertor{Scheme: scheme}
sampleCRD := unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.k8s.io/v1",
"kind": "Flunder",
"data": map[string]interface{}{
"Key": "Value",
},
},
}
table := map[string]struct {
in runtime.Object
out runtime.Object
expectedObj runtime.Object
}{
"convert example/v1#Pod to example#Pod": {
in: &examplev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Labels: map[string]string{
"key": "value",
},
},
Spec: examplev1.PodSpec{
RestartPolicy: examplev1.RestartPolicy("never"),
},
},
out: &example.Pod{},
expectedObj: &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example.PodSpec{
RestartPolicy: example.RestartPolicy("never"),
},
},
},
"convert example2/v1#replicaset to example#replicaset": {
in: &example2v1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example2v1.ReplicaSetSpec{
Replicas: func() *int32 { var i int32; i = 1; return &i }(),
},
},
out: &example.ReplicaSet{},
expectedObj: &example.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example.ReplicaSetSpec{
Replicas: 1,
},
},
},
"no conversion if the object is the same": {
in: &sampleCRD,
out: &sampleCRD,
expectedObj: &sampleCRD,
},
}
for name, test := range table {
t.Run(name, func(t *testing.T) {
err := c.Convert(test.in, test.out)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(test.out, test.expectedObj) {
t.Errorf("\nexpected:\n%#v\ngot:\n %#v\n", test.expectedObj, test.out)
}
})
}
}

View File

@ -26,6 +26,7 @@ import (
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/initialization"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
"k8s.io/apiserver/pkg/server"
"k8s.io/client-go/informers"
@ -56,8 +57,8 @@ func NewAdmissionOptions() *AdmissionOptions {
options := &AdmissionOptions{
Plugins: &admission.Plugins{},
PluginNames: []string{},
RecommendedPluginOrder: []string{lifecycle.PluginName, initialization.PluginName, validatingwebhook.PluginName},
DefaultOffPlugins: []string{initialization.PluginName, validatingwebhook.PluginName},
RecommendedPluginOrder: []string{mutatingwebhook.PluginName, lifecycle.PluginName, initialization.PluginName, validatingwebhook.PluginName},
DefaultOffPlugins: []string{mutatingwebhook.PluginName, initialization.PluginName, validatingwebhook.PluginName},
}
server.RegisterAllAdmissionPlugins(options.Plugins)
return options

View File

@ -21,6 +21,7 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/initialization"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
)
@ -29,4 +30,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
lifecycle.Register(plugins)
initialization.Register(plugins)
validatingwebhook.Register(plugins)
mutatingwebhook.Register(plugins)
}

View File

@ -18,6 +18,7 @@ package apimachinery
import (
"fmt"
"reflect"
"strings"
"time"
@ -45,26 +46,28 @@ import (
)
const (
secretName = "sample-webhook-secret"
deploymentName = "sample-webhook-deployment"
serviceName = "e2e-test-webhook"
roleBindingName = "webhook-auth-reader"
webhookConfigName = "e2e-test-webhook-config"
skipNamespaceLabelKey = "skip-webhook-admission"
skipNamespaceLabelValue = "yes"
skippedNamespaceName = "exempted-namesapce"
disallowedPodName = "disallowed-pod"
disallowedConfigMapName = "disallowed-configmap"
allowedConfigMapName = "allowed-configmap"
crdName = "e2e-test-webhook-crd"
crdKind = "E2e-test-webhook-crd"
crdWebhookConfigName = "e2e-test-webhook-config-crd"
crdAPIGroup = "webhook-crd-test.k8s.io"
crdAPIVersion = "v1"
webhookFailClosedConfigName = "e2e-test-webhook-fail-closed"
failNamespaceLabelKey = "fail-closed-webhook"
failNamespaceLabelValue = "yes"
failNamespaceName = "fail-closed-namesapce"
secretName = "sample-webhook-secret"
deploymentName = "sample-webhook-deployment"
serviceName = "e2e-test-webhook"
roleBindingName = "webhook-auth-reader"
webhookConfigName = "e2e-test-webhook-config"
mutatingWebhookConfigName = "e2e-test-mutating-webhook-config"
skipNamespaceLabelKey = "skip-webhook-admission"
skipNamespaceLabelValue = "yes"
skippedNamespaceName = "exempted-namesapce"
disallowedPodName = "disallowed-pod"
disallowedConfigMapName = "disallowed-configmap"
allowedConfigMapName = "allowed-configmap"
crdName = "e2e-test-webhook-crd"
crdKind = "E2e-test-webhook-crd"
crdWebhookConfigName = "e2e-test-webhook-config-crd"
crdMutatingWebhookConfigName = "e2e-test-mutating-webhook-config-crd"
crdAPIGroup = "webhook-crd-test.k8s.io"
crdAPIVersion = "v1"
webhookFailClosedConfigName = "e2e-test-webhook-fail-closed"
failNamespaceLabelKey = "fail-closed-webhook"
failNamespaceLabelValue = "yes"
failNamespaceName = "fail-closed-namesapce"
)
var serverWebhookVersion = utilversion.MustParseSemantic("v1.8.0")
@ -96,7 +99,7 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
// Note that in 1.9 we will have backwards incompatible change to
// admission webhooks, so the image will be updated to 1.9 sometime in
// the development 1.9 cycle.
deployWebhookAndService(f, "gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v5", context)
deployWebhookAndService(f, "gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v6", context)
})
AfterEach(func() {
cleanWebhookTest(client, namespaceName)
@ -121,6 +124,26 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
err := f.ClientSet.AdmissionregistrationV1alpha1().ValidatingWebhookConfigurations().Delete(webhookFailClosedConfigName, nil)
Expect(err).NotTo(HaveOccurred(), "failed deleting fail closed webhook, this may cause subsequent e2e tests to fail")
})
It("Should mutate configmap", func() {
registerMutatingWebhookForConfigMap(f, context)
testMutatingConfigMapWebhook(f)
})
It("Should mutate crd", func() {
crdCleanup, dynamicClient := createCRD(f)
defer crdCleanup()
registerMutatingWebhookForCRD(f, context)
testMutatingCRDWebhook(f, dynamicClient)
})
// TODO: add more e2e tests for mutating webhooks
// 1. mutating webhook that mutates pod
// 2. mutating webhook that sends empty patch
// 2.1 and sets status.allowed=true
// 2.2 and sets status.allowed=false
// 3. mutating webhook that sends patch, but also sets status.allowed=false
// 4. mtuating webhook that fail-open v.s. fail-closed
})
func createAuthReaderRoleBinding(f *framework.Framework, namespace string) {
@ -340,6 +363,78 @@ func registerWebhook(f *framework.Framework, context *certContext) {
time.Sleep(10 * time.Second)
}
func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certContext) {
client := f.ClientSet
By("Registering the mutating configmap webhook via the AdmissionRegistration API")
namespace := f.Namespace.Name
_, err := client.AdmissionregistrationV1alpha1().MutatingWebhookConfigurations().Create(&v1alpha1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: mutatingWebhookConfigName,
},
Webhooks: []v1alpha1.Webhook{
{
Name: "adding-configmap-data-stage-1.k8s.io",
Rules: []v1alpha1.RuleWithOperations{{
Operations: []v1alpha1.OperationType{v1alpha1.Create},
Rule: v1alpha1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"configmaps"},
},
}},
ClientConfig: v1alpha1.WebhookClientConfig{
Service: &v1alpha1.ServiceReference{
Namespace: namespace,
Name: serviceName,
Path: strPtr("/mutating-configmaps"),
},
CABundle: context.signingCert,
},
},
{
Name: "adding-configmap-data-stage-2.k8s.io",
Rules: []v1alpha1.RuleWithOperations{{
Operations: []v1alpha1.OperationType{v1alpha1.Create},
Rule: v1alpha1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"configmaps"},
},
}},
ClientConfig: v1alpha1.WebhookClientConfig{
Service: &v1alpha1.ServiceReference{
Namespace: namespace,
Name: serviceName,
Path: strPtr("/mutating-configmaps"),
},
CABundle: context.signingCert,
},
},
},
})
framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", mutatingWebhookConfigName, namespace)
// The webhook configuration is honored in 1s.
time.Sleep(10 * time.Second)
}
func testMutatingConfigMapWebhook(f *framework.Framework) {
By("create a configmap that should be updated by the webhook")
client := f.ClientSet
configMap := toBeMutatedConfigMap(f)
mutatedConfigMap, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configMap)
Expect(err).To(BeNil())
expectedConfigMapData := map[string]string{
"mutation-start": "yes",
"mutation-stage-1": "yes",
"mutation-stage-2": "yes",
}
if !reflect.DeepEqual(expectedConfigMapData, mutatedConfigMap.Data) {
framework.Failf("\nexpected %#v\n, got %#v\n", expectedConfigMapData, mutatedConfigMap.Data)
}
}
func testWebhook(f *framework.Framework) {
By("create a pod that should be denied by the webhook")
client := f.ClientSet
@ -542,6 +637,17 @@ func nonCompliantConfigMap(f *framework.Framework) *v1.ConfigMap {
}
}
func toBeMutatedConfigMap(f *framework.Framework) *v1.ConfigMap {
return &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "to-be-mutated",
},
Data: map[string]string{
"mutation-start": "yes",
},
}
}
func nonCompliantConfigMapPatch() string {
return fmt.Sprint(`{"data":{"webhook-e2e-test":"webhook-disallow"}}`)
}
@ -571,6 +677,7 @@ func updateConfigMap(c clientset.Interface, ns, name string, update updateConfig
func cleanWebhookTest(client clientset.Interface, namespaceName string) {
_ = client.AdmissionregistrationV1alpha1().ValidatingWebhookConfigurations().Delete(webhookConfigName, nil)
_ = client.AdmissionregistrationV1alpha1().ValidatingWebhookConfigurations().Delete(crdWebhookConfigName, nil)
_ = client.AdmissionregistrationV1alpha1().MutatingWebhookConfigurations().Delete(mutatingWebhookConfigName, nil)
_ = client.CoreV1().Services(namespaceName).Delete(serviceName, nil)
_ = client.ExtensionsV1beta1().Deployments(namespaceName).Delete(deploymentName, nil)
_ = client.CoreV1().Secrets(namespaceName).Delete(secretName, nil)
@ -667,6 +774,62 @@ func registerWebhookForCRD(f *framework.Framework, context *certContext) {
time.Sleep(10 * time.Second)
}
func registerMutatingWebhookForCRD(f *framework.Framework, context *certContext) {
client := f.ClientSet
By("Registering the mutating webhook for crd via the AdmissionRegistration API")
namespace := f.Namespace.Name
_, err := client.AdmissionregistrationV1alpha1().MutatingWebhookConfigurations().Create(&v1alpha1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: crdMutatingWebhookConfigName,
},
Webhooks: []v1alpha1.Webhook{
{
Name: "mutate-crd-data-stage-1.k8s.io",
Rules: []v1alpha1.RuleWithOperations{{
Operations: []v1alpha1.OperationType{v1alpha1.Create},
Rule: v1alpha1.Rule{
APIGroups: []string{crdAPIGroup},
APIVersions: []string{crdAPIVersion},
Resources: []string{crdName + "s"},
},
}},
ClientConfig: v1alpha1.WebhookClientConfig{
Service: &v1alpha1.ServiceReference{
Namespace: namespace,
Name: serviceName,
Path: strPtr("/mutating-crd"),
},
CABundle: context.signingCert,
},
},
{
Name: "mutate-crd-data-stage-2.k8s.io",
Rules: []v1alpha1.RuleWithOperations{{
Operations: []v1alpha1.OperationType{v1alpha1.Create},
Rule: v1alpha1.Rule{
APIGroups: []string{crdAPIGroup},
APIVersions: []string{crdAPIVersion},
Resources: []string{crdName + "s"},
},
}},
ClientConfig: v1alpha1.WebhookClientConfig{
Service: &v1alpha1.ServiceReference{
Namespace: namespace,
Name: serviceName,
Path: strPtr("/mutating-crd"),
},
CABundle: context.signingCert,
},
},
},
})
framework.ExpectNoError(err, "registering crd webhook config %s with namespace %s", webhookConfigName, namespace)
// The webhook configuration is honored in 1s.
time.Sleep(10 * time.Second)
}
func testCRDWebhook(f *framework.Framework, crdClient dynamic.ResourceInterface) {
By("Creating a custom resource that should be denied by the webhook")
crd := newCRDForAdmissionWebhookTest()
@ -690,3 +853,31 @@ func testCRDWebhook(f *framework.Framework, crdClient dynamic.ResourceInterface)
framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
}
}
func testMutatingCRDWebhook(f *framework.Framework, crdClient dynamic.ResourceInterface) {
By("Creating a custom resource that should be mutated by the webhook")
crd := newCRDForAdmissionWebhookTest()
cr := &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": crd.Spec.Names.Kind,
"apiVersion": crd.Spec.Group + "/" + crd.Spec.Version,
"metadata": map[string]interface{}{
"name": "cr-instance-1",
"namespace": f.Namespace.Name,
},
"data": map[string]interface{}{
"mutation-start": "yes",
},
},
}
mutatedCR, err := crdClient.Create(cr)
Expect(err).To(BeNil())
expectedCRData := map[string]interface{}{
"mutation-start": "yes",
"mutation-stage-1": "yes",
"mutation-stage-2": "yes",
}
if !reflect.DeepEqual(expectedCRData, mutatedCR.Object["data"]) {
framework.Failf("\nexpected %#v\n, got %#v\n", expectedCRData, mutatedCR.Object["data"])
}
}

View File

@ -14,7 +14,7 @@
build:
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webhook .
docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v5 .
docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v6 .
rm -rf webhook
push:
gcloud docker -- push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v5
gcloud docker -- push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v6

View File

@ -28,6 +28,18 @@ import (
"k8s.io/api/admission/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
// TODO: try this library to see if it generates correct json patch
// https://github.com/mattbaird/jsonpatch
)
const (
patch1 string = `[
{ "op": "add", "path": "/data/mutation-stage-1", "value": "yes" }
]`
patch2 string = `[
{ "op": "add", "path": "/data/mutation-stage-2", "value": "yes" }
]`
)
// Config contains the server (the webhook) cert and key.
@ -120,6 +132,64 @@ func admitConfigMaps(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
return &reviewResponse
}
func mutateConfigmaps(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
glog.V(2).Info("mutating configmaps")
configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}
if ar.Request.Resource != configMapResource {
glog.Errorf("expect resource to be %s", configMapResource)
return nil
}
raw := ar.Request.Object.Raw
configmap := corev1.ConfigMap{}
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(raw, nil, &configmap); err != nil {
glog.Error(err)
return toAdmissionResponse(err)
}
reviewResponse := v1alpha1.AdmissionResponse{}
reviewResponse.Allowed = true
if configmap.Data["mutation-start"] == "yes" {
reviewResponse.Patch = []byte(patch1)
}
if configmap.Data["mutation-stage-1"] == "yes" {
reviewResponse.Patch = []byte(patch2)
}
pt := v1alpha1.PatchTypeJSONPatch
reviewResponse.PatchType = &pt
return &reviewResponse
}
func mutateCRD(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
glog.V(2).Info("mutating crd")
cr := struct {
metav1.ObjectMeta
Data map[string]string
}{}
raw := ar.Request.Object.Raw
err := json.Unmarshal(raw, &cr)
if err != nil {
glog.Error(err)
return toAdmissionResponse(err)
}
reviewResponse := v1alpha1.AdmissionResponse{}
reviewResponse.Allowed = true
if cr.Data["mutation-start"] == "yes" {
reviewResponse.Patch = []byte(patch1)
}
if cr.Data["mutation-stage-1"] == "yes" {
reviewResponse.Patch = []byte(patch2)
}
pt := v1alpha1.PatchTypeJSONPatch
reviewResponse.PatchType = &pt
return &reviewResponse
}
func admitCRD(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
glog.V(2).Info("admitting crd")
cr := struct {
@ -179,6 +249,9 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
response.Response = reviewResponse
response.Response.UID = ar.Request.UID
}
// reset the Object and OldObject, they are not needed in a response.
ar.Request.Object = runtime.RawExtension{}
ar.Request.OldObject = runtime.RawExtension{}
resp, err := json.Marshal(response)
if err != nil {
@ -197,10 +270,18 @@ func serveConfigmaps(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitConfigMaps)
}
func serveMutateConfigmaps(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutateConfigmaps)
}
func serveCRD(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitCRD)
}
func serveMutateCRD(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutateCRD)
}
func main() {
var config Config
config.addFlags()
@ -208,7 +289,9 @@ func main() {
http.HandleFunc("/pods", servePods)
http.HandleFunc("/configmaps", serveConfigmaps)
http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps)
http.HandleFunc("/crd", serveCRD)
http.HandleFunc("/mutating-crd", serveMutateCRD)
clientset := getClient()
server := &http.Server{
Addr: ":443",

View File

@ -0,0 +1,95 @@
/*
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 main
import (
"encoding/json"
"reflect"
"testing"
jsonpatch "github.com/evanphx/json-patch"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestJSONPatchForConfigMap(t *testing.T) {
cm := corev1.ConfigMap{
Data: map[string]string{
"mutation-start": "yes",
},
}
cmJS, err := json.Marshal(cm)
if err != nil {
t.Fatal(err)
}
patchObj, err := jsonpatch.DecodePatch([]byte(patch1))
if err != nil {
t.Fatal(err)
}
patchedJS, err := patchObj.Apply(cmJS)
patchedObj := corev1.ConfigMap{}
err = json.Unmarshal(patchedJS, &patchedObj)
if err != nil {
t.Fatal(err)
}
expected := corev1.ConfigMap{
Data: map[string]string{
"mutation-start": "yes",
"mutation-stage-1": "yes",
},
}
if !reflect.DeepEqual(patchedObj, expected) {
t.Errorf("\nexpected %#v\n, got %#v", expected, patchedObj)
}
}
func TestJSONPatchForUnstructured(t *testing.T) {
cr := &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "Something",
"apiVersion": "somegroup/v1",
"data": map[string]interface{}{
"mutation-start": "yes",
},
},
}
crJS, err := json.Marshal(cr)
if err != nil {
t.Fatal(err)
}
patchObj, err := jsonpatch.DecodePatch([]byte(patch1))
if err != nil {
t.Fatal(err)
}
patchedJS, err := patchObj.Apply(crJS)
patchedObj := unstructured.Unstructured{}
err = json.Unmarshal(patchedJS, &patchedObj)
if err != nil {
t.Fatal(err)
}
expectedData := map[string]interface{}{
"mutation-start": "yes",
"mutation-stage-1": "yes",
}
if !reflect.DeepEqual(patchedObj.Object["data"], expectedData) {
t.Errorf("\nexpected %#v\n, got %#v", expectedData, patchedObj.Object["data"])
}
}