diff --git a/cluster/gce/config-test.sh b/cluster/gce/config-test.sh index 46995d7096..676b4afa80 100755 --- a/cluster/gce/config-test.sh +++ b/cluster/gce/config-test.sh @@ -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 diff --git a/cmd/kube-apiserver/app/options/options_test.go b/cmd/kube-apiserver/app/options/options_test.go index 949f3c1339..b76f6e59dc 100644 --- a/cmd/kube-apiserver/app/options/options_test.go +++ b/cmd/kube-apiserver/app/options/options_test.go @@ -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, diff --git a/hack/local-up-cluster.sh b/hack/local-up-cluster.sh index 6c602226eb..c660b94d56 100755 --- a/hack/local-up-cluster.sh +++ b/hack/local-up-cluster.sh @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/BUILD new file mode 100644 index 0000000000..161bd30eee --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/BUILD @@ -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"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission.go new file mode 100644 index 0000000000..64527b4963 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission_test.go new file mode 100644 index 0000000000..8f3ec6b1e1 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission_test.go @@ -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 ®istrationv1alpha1.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: ®istrationv1alpha1.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{"*/*"}, + }, + }} +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/doc.go new file mode 100644 index 0000000000..d804aca1cf --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/doc.go @@ -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" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/certs.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/certs.go new file mode 100644 index 0000000000..8e5d2d13f3 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/certs.go @@ -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-----`) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/doc.go new file mode 100644 index 0000000000..0f11ec834e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/doc.go @@ -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" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/gencerts.sh b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/gencerts.sh similarity index 75% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/gencerts.sh rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/gencerts.sh index f03124933d..1df97774aa 100755 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/gencerts.sh +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testdata/gencerts.sh @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission.go index ded6b0261b..a06e224bf7 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission.go @@ -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 { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission_test.go index 38981f3ce9..da7b510534 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admission_test.go @@ -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, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/certs_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/certs_test.go deleted file mode 100644 index 57605ffc81..0000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/certs_test.go +++ /dev/null @@ -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-----`) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/doc.go index 0241c8e537..ede53c668f 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/doc.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/doc.go @@ -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" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion.go index 8a9fa1d0fd..a1ba712fc7 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion.go @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion_test.go index c80db3d0fd..1429c71e10 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion_test.go @@ -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) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/admission.go b/staging/src/k8s.io/apiserver/pkg/server/options/admission.go index c5ed7f9b8a..b6abf70c46 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/admission.go @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/server/plugins.go b/staging/src/k8s.io/apiserver/pkg/server/plugins.go index 54cd539619..84813aafe6 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/plugins.go +++ b/staging/src/k8s.io/apiserver/pkg/server/plugins.go @@ -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) } diff --git a/test/e2e/apimachinery/webhook.go b/test/e2e/apimachinery/webhook.go index 3abdb52e57..6c349ad463 100644 --- a/test/e2e/apimachinery/webhook.go +++ b/test/e2e/apimachinery/webhook.go @@ -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"]) + } +} diff --git a/test/images/webhook/Makefile b/test/images/webhook/Makefile index 5e6b8a5224..75f04cd374 100644 --- a/test/images/webhook/Makefile +++ b/test/images/webhook/Makefile @@ -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 diff --git a/test/images/webhook/main.go b/test/images/webhook/main.go index 109f2c33d6..eecbb26d8f 100644 --- a/test/images/webhook/main.go +++ b/test/images/webhook/main.go @@ -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", diff --git a/test/images/webhook/patch_test.go b/test/images/webhook/patch_test.go new file mode 100644 index 0000000000..9b11e10e2b --- /dev/null +++ b/test/images/webhook/patch_test.go @@ -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"]) + } +}