diff --git a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json index 3e921fea2d..d2c48a7a13 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json @@ -882,6 +882,18 @@ "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/config", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/errors", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/request", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/rules", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" @@ -890,6 +902,10 @@ "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/validating", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/apis/apiserver", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/BUILD index 291df5b2ff..0567a2ac49 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/BUILD @@ -79,8 +79,12 @@ filegroup( "//staging/src/k8s.io/apiserver/pkg/admission/plugin/initialization:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned:all-srcs", ], tags = ["automanaged"], ) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/BUILD index 0f9ff984c4..1d348eef28 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/BUILD @@ -5,7 +5,6 @@ go_library( srcs = [ "authentication.go", "client.go", - "errors.go", "kubeconfig.go", "serviceresolver.go", ], @@ -17,6 +16,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/client.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/client.go index 8203bb7dac..3af3a2f2b8 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/client.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/client.go @@ -27,6 +27,7 @@ import ( "k8s.io/api/admissionregistration/v1alpha1" "k8s.io/apimachinery/pkg/runtime" utilerrors "k8s.io/apimachinery/pkg/util/errors" + webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors" "k8s.io/client-go/rest" ) @@ -153,12 +154,12 @@ func (cm *ClientManager) HookClient(h *v1alpha1.Webhook) (*rest.RESTClient, erro } if h.ClientConfig.URL == nil { - return nil, &ErrCallingWebhook{WebhookName: h.Name, Reason: ErrNeedServiceOrURL} + return nil, &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: ErrNeedServiceOrURL} } u, err := url.Parse(*h.ClientConfig.URL) if err != nil { - return nil, &ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Unparsable URL: %v", err)} + return nil, &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Unparsable URL: %v", err)} } restConfig, err := cm.authInfoResolver.ClientConfigFor(u.Host) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/BUILD new file mode 100644 index 0000000000..2880072384 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/BUILD @@ -0,0 +1,38 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "errors.go", + "statuserror.go", + ], + importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/errors", + visibility = ["//visibility:public"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["statuserror_test.go"], + importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/errors", + library = ":go_default_library", + deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1: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/errors/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/doc.go new file mode 100644 index 0000000000..6e86a1b5f2 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package errors contains utilities for admission webhook specific errors +package errors // import "k8s.io/apiserver/pkg/admission/plugin/webhook/errors" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/errors.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/errors.go similarity index 98% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/errors.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/errors.go index fa259758fd..2396152286 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/errors.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/errors.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package config +package errors import "fmt" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/statuserror.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/statuserror.go new file mode 100644 index 0000000000..f37dec0177 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/statuserror.go @@ -0,0 +1,47 @@ +/* +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 errors + +import ( + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ToStatusErr returns a StatusError with information about the webhook plugin +func ToStatusErr(webhookName string, result *metav1.Status) *apierrors.StatusError { + deniedBy := fmt.Sprintf("admission webhook %q denied the request", webhookName) + const noExp = "without explanation" + + if result == nil { + result = &metav1.Status{Status: metav1.StatusFailure} + } + + switch { + case len(result.Message) > 0: + result.Message = fmt.Sprintf("%s: %s", deniedBy, result.Message) + case len(result.Reason) > 0: + result.Message = fmt.Sprintf("%s: %s", deniedBy, result.Reason) + default: + result.Message = fmt.Sprintf("%s %s", deniedBy, noExp) + } + + return &apierrors.StatusError{ + ErrStatus: *result, + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/statuserror_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/statuserror_test.go new file mode 100644 index 0000000000..98b780b451 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors/statuserror_test.go @@ -0,0 +1,73 @@ +/* +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 errors + +import ( + "fmt" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestToStatusErr(t *testing.T) { + hookName := "foo" + deniedBy := fmt.Sprintf("admission webhook %q denied the request", hookName) + tests := []struct { + name string + result *metav1.Status + expectedError string + }{ + { + "nil result", + nil, + deniedBy + " without explanation", + }, + { + "only message", + &metav1.Status{ + Message: "you shall not pass", + }, + deniedBy + ": you shall not pass", + }, + { + "only reason", + &metav1.Status{ + Reason: metav1.StatusReasonForbidden, + }, + deniedBy + ": Forbidden", + }, + { + "message and reason", + &metav1.Status{ + Message: "you shall not pass", + Reason: metav1.StatusReasonForbidden, + }, + deniedBy + ": you shall not pass", + }, + { + "no message, no reason", + &metav1.Status{}, + deniedBy + " without explanation", + }, + } + for _, test := range tests { + err := ToStatusErr(hookName, test.result) + if err == nil || err.Error() != test.expectedError { + t.Errorf("%s: expected an error saying %q, but got %v", test.name, test.expectedError, err) + } + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/BUILD new file mode 100644 index 0000000000..fabc75c15e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/BUILD @@ -0,0 +1,52 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "matcher.go", + ], + importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace", + visibility = ["//visibility:public"], + deps = [ + "//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/meta: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/util/errors:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", + "//vendor/k8s.io/client-go/kubernetes:go_default_library", + "//vendor/k8s.io/client-go/listers/core/v1:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["matcher_test.go"], + importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace", + library = ":go_default_library", + deps = [ + "//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/schema:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission: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/namespace/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/doc.go new file mode 100644 index 0000000000..d1a2853383 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/doc.go @@ -0,0 +1,20 @@ +/* +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 namespace defines the utilities that are used by the webhook +// plugin to decide if a webhook should be applied to an object based on its +// namespace. +package namespace // import "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher.go new file mode 100644 index 0000000000..48042c5a73 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher.go @@ -0,0 +1,117 @@ +/* +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 namespace + +import ( + "fmt" + + "k8s.io/api/admissionregistration/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/admission" + clientset "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" +) + +// Matcher decides if a request is exempted by the NamespaceSelector of a +// webhook configuration. +type Matcher struct { + NamespaceLister corelisters.NamespaceLister + Client clientset.Interface +} + +// Validate checks if the Matcher has a NamespaceLister and Client. +func (m *Matcher) Validate() error { + var errs []error + if m.NamespaceLister == nil { + errs = append(errs, fmt.Errorf("the namespace matcher requires a namespaceLister")) + } + if m.Client == nil { + errs = append(errs, fmt.Errorf("the namespace matcher requires a namespaceLister")) + } + return utilerrors.NewAggregate(errs) +} + +// GetNamespaceLabels gets the labels of the namespace related to the attr. +func (m *Matcher) GetNamespaceLabels(attr admission.Attributes) (map[string]string, error) { + // If the request itself is creating or updating a namespace, then get the + // labels from attr.Object, because namespaceLister doesn't have the latest + // namespace yet. + // + // However, if the request is deleting a namespace, then get the label from + // the namespace in the namespaceLister, because a delete request is not + // going to change the object, and attr.Object will be a DeleteOptions + // rather than a namespace object. + if attr.GetResource().Resource == "namespaces" && + len(attr.GetSubresource()) == 0 && + (attr.GetOperation() == admission.Create || attr.GetOperation() == admission.Update) { + accessor, err := meta.Accessor(attr.GetObject()) + if err != nil { + return nil, err + } + return accessor.GetLabels(), nil + } + + namespaceName := attr.GetNamespace() + namespace, err := m.NamespaceLister.Get(namespaceName) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + if apierrors.IsNotFound(err) { + // in case of latency in our caches, make a call direct to storage to verify that it truly exists or not + namespace, err = m.Client.CoreV1().Namespaces().Get(namespaceName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + } + return namespace.Labels, nil +} + +// MatchNamespaceSelector decideds whether the request matches the +// namespaceSelctor of the webhook. Only when they match, the webhook is called. +func (m *Matcher) MatchNamespaceSelector(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) { + namespaceName := attr.GetNamespace() + if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" { + // If the request is about a cluster scoped resource, and it is not a + // namespace, it is exempted from all webhooks for now. + // TODO: figure out a way selective exempt cluster scoped resources. + // Also update the comment in types.go + return false, nil + } + namespaceLabels, err := m.GetNamespaceLabels(attr) + // this means the namespace is not found, for backwards compatibility, + // return a 404 + if apierrors.IsNotFound(err) { + status, ok := err.(apierrors.APIStatus) + if !ok { + return false, apierrors.NewInternalError(err) + } + return false, &apierrors.StatusError{status.Status()} + } + if err != nil { + return false, apierrors.NewInternalError(err) + } + // TODO: adding an LRU cache to cache the translation + selector, err := metav1.LabelSelectorAsSelector(h.NamespaceSelector) + if err != nil { + return false, apierrors.NewInternalError(err) + } + return selector.Matches(labels.Set(namespaceLabels)), nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher_test.go new file mode 100644 index 0000000000..8b9889e974 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace/matcher_test.go @@ -0,0 +1,129 @@ +/* +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 namespace + +import ( + "reflect" + "testing" + + registrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" +) + +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) +} + +func TestGetNamespaceLabels(t *testing.T) { + namespace1Labels := map[string]string{ + "runlevel": "1", + } + namespace1 := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "1", + Labels: namespace1Labels, + }, + } + namespace2Labels := map[string]string{ + "runlevel": "2", + } + namespace2 := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "2", + Labels: namespace2Labels, + }, + } + namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{ + "1": &namespace1, + }, + } + + tests := []struct { + name string + attr admission.Attributes + expectedLabels map[string]string + }{ + { + name: "request is for creating namespace, the labels should be from the object itself", + attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, "", namespace2.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Create, nil), + expectedLabels: namespace2Labels, + }, + { + name: "request is for updating namespace, the labels should be from the new object", + attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, namespace2.Name, namespace2.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Update, nil), + expectedLabels: namespace2Labels, + }, + { + name: "request is for deleting namespace, the labels should be from the cache", + attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, namespace1.Name, namespace1.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Delete, nil), + expectedLabels: namespace1Labels, + }, + { + name: "request is for namespace/finalizer", + attr: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, namespace1.Name, "mock-name", schema.GroupVersionResource{Resource: "namespaces"}, "finalizers", admission.Create, nil), + expectedLabels: namespace1Labels, + }, + { + name: "request is for pod", + attr: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, namespace1.Name, "mock-name", schema.GroupVersionResource{Resource: "pods"}, "", admission.Create, nil), + expectedLabels: namespace1Labels, + }, + } + matcher := Matcher{ + NamespaceLister: namespaceLister, + } + for _, tt := range tests { + actualLabels, err := matcher.GetNamespaceLabels(tt.attr) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(actualLabels, tt.expectedLabels) { + t.Errorf("expected labels to be %#v, got %#v", tt.expectedLabels, actualLabels) + } + } +} + +func TestExemptClusterScopedResource(t *testing.T) { + hook := ®istrationv1alpha1.Webhook{ + NamespaceSelector: &metav1.LabelSelector{}, + } + attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, nil) + matcher := Matcher{} + matches, err := matcher.MatchNamespaceSelector(hook, attr) + if err != nil { + t.Fatal(err) + } + if matches { + t.Errorf("cluster scoped resources (but not a namespace) should be exempted from all webhooks") + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/BUILD new file mode 100644 index 0000000000..f838848de5 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/BUILD @@ -0,0 +1,32 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "admissionreview.go", + "doc.go", + ], + importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/request", + visibility = ["//visibility:public"], + deps = [ + "//vendor/k8s.io/api/admission/v1alpha1:go_default_library", + "//vendor/k8s.io/api/authentication/v1: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/apiserver/pkg/admission: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/validating/admissionreview.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go similarity index 87% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admissionreview.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go index cdde9bc04f..e34436ff68 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/admissionreview.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package webhook delegates admission checks to dynamically configured webhooks. -package validating +package request import ( admissionv1alpha1 "k8s.io/api/admission/v1alpha1" @@ -25,9 +24,8 @@ import ( "k8s.io/apiserver/pkg/admission" ) -// TODO: move this function to a common package -// createAdmissionReview creates an AdmissionReview for the provided admission.Attributes -func createAdmissionReview(attr admission.Attributes) admissionv1alpha1.AdmissionReview { +// CreateAdmissionReview creates an AdmissionReview for the provided admission.Attributes +func CreateAdmissionReview(attr admission.Attributes) admissionv1alpha1.AdmissionReview { gvk := attr.GetKind() gvr := attr.GetResource() aUserInfo := attr.GetUserInfo() diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/doc.go new file mode 100644 index 0000000000..fbacf33717 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package request creates admissionReview request based on admission attributes. +package request // import "k8s.io/apiserver/pkg/admission/plugin/webhook/request" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/BUILD index 63b1402f24..161bd30eee 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/BUILD @@ -4,7 +4,6 @@ go_library( name = "go_default_library", srcs = [ "admission.go", - "admissionreview.go", "doc.go", ], importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating", @@ -13,13 +12,9 @@ go_library( "//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/api/authentication/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", - "//vendor/k8s.io/apimachinery/pkg/api/meta: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/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", @@ -27,10 +22,13 @@ go_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", - "//vendor/k8s.io/client-go/listers/core/v1:go_default_library", ], ) @@ -39,7 +37,6 @@ go_test( srcs = [ "admission_test.go", "certs_test.go", - "conversion_test.go", ], importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating", library = ":go_default_library", @@ -49,15 +46,10 @@ go_test( "//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/apis/meta/v1/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", - "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", "//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library", - "//vendor/k8s.io/apiserver/pkg/apis/example:go_default_library", - "//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", - "//vendor/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library", "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library", ], 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 98df6433a5..254781cd2b 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 @@ -30,11 +30,8 @@ import ( admissionv1alpha1 "k8s.io/api/admission/v1alpha1" "k8s.io/api/admissionregistration/v1alpha1" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" @@ -42,10 +39,13 @@ import ( "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" - corelisters "k8s.io/client-go/listers/core/v1" ) const ( @@ -104,12 +104,10 @@ func NewGenericAdmissionWebhook(configFile io.Reader) (*GenericAdmissionWebhook, // GenericAdmissionWebhook is an implementation of admission.Interface. type GenericAdmissionWebhook struct { *admission.Handler - hookSource WebhookSource - namespaceLister corelisters.NamespaceLister - client clientset.Interface - convertor runtime.ObjectConvertor - creator runtime.ObjectCreater - clientManager config.ClientManager + hookSource WebhookSource + namespaceMatcher namespace.Matcher + clientManager config.ClientManager + convertor versioned.Convertor } var ( @@ -133,21 +131,20 @@ func (a *GenericAdmissionWebhook) SetScheme(scheme *runtime.Scheme) { a.clientManager.SetNegotiatedSerializer(serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{ Serializer: serializer.NewCodecFactory(scheme).LegacyCodec(admissionv1alpha1.SchemeGroupVersion), })) - a.convertor = scheme - a.creator = scheme + a.convertor.Scheme = scheme } } // WantsExternalKubeClientSet defines a function which sets external ClientSet for admission plugins that need it func (a *GenericAdmissionWebhook) SetExternalKubeClientSet(client clientset.Interface) { - a.client = client + a.namespaceMatcher.Client = client a.hookSource = configuration.NewValidatingWebhookConfigurationManager(client.AdmissionregistrationV1alpha1().ValidatingWebhookConfigurations()) } // SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface. func (a *GenericAdmissionWebhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { namespaceInformer := f.Core().V1().Namespaces() - a.namespaceLister = namespaceInformer.Lister() + a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister() a.SetReadyFunc(namespaceInformer.Informer().HasSynced) } @@ -156,12 +153,15 @@ func (a *GenericAdmissionWebhook) ValidateInitialization() error { if a.hookSource == nil { return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a Kubernetes client to be provided") } - if a.namespaceLister == nil { - return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a namespaceLister") + if err := a.namespaceMatcher.Validate(); err != nil { + return fmt.Errorf("the 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) } + if err := a.convertor.Validate(); err != nil { + return fmt.Errorf("the GenericAdmissionWebhook.convertor is not properly setup: %v", err) + } go a.hookSource.Run(wait.NeverStop) return nil } @@ -185,39 +185,6 @@ func (a *GenericAdmissionWebhook) loadConfiguration(attr admission.Attributes) ( return hookConfig, nil } -// TODO: move this object to a common package -type versionedAttributes struct { - admission.Attributes - oldObject runtime.Object - object runtime.Object -} - -func (v versionedAttributes) GetObject() runtime.Object { - return v.object -} - -func (v versionedAttributes) GetOldObject() runtime.Object { - return v.oldObject -} - -// TODO: move this method to a common package -func (a *GenericAdmissionWebhook) convertToGVK(obj runtime.Object, gvk schema.GroupVersionKind) (runtime.Object, error) { - // Unlike other resources, custom resources do not have internal version, so - // if obj is a custom resource, it should not need conversion. - if obj.GetObjectKind().GroupVersionKind() == gvk { - return obj, nil - } - out, err := a.creator.New(gvk) - if err != nil { - return nil, err - } - err = a.convertor.Convert(obj, out, nil) - if err != nil { - return nil, err - } - return out, nil -} - // Admit makes an admission decision based on the request attributes. func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error { hookConfig, err := a.loadConfiguration(attr) @@ -244,22 +211,22 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error { } // convert the object to the external version before sending it to the webhook - versionedAttr := versionedAttributes{ + versionedAttr := versioned.Attributes{ Attributes: attr, } if oldObj := attr.GetOldObject(); oldObj != nil { - out, err := a.convertToGVK(oldObj, attr.GetKind()) + out, err := a.convertor.ConvertToGVK(oldObj, attr.GetKind()) if err != nil { return apierrors.NewInternalError(err) } - versionedAttr.oldObject = out + versionedAttr.OldObject = out } if obj := attr.GetObject(); obj != nil { - out, err := a.convertToGVK(obj, attr.GetKind()) + out, err := a.convertor.ConvertToGVK(obj, attr.GetKind()) if err != nil { return apierrors.NewInternalError(err) } - versionedAttr.object = out + versionedAttr.Object = out } wg := sync.WaitGroup{} @@ -277,7 +244,7 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error { } ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1alpha1.Ignore - if callErr, ok := err.(*config.ErrCallingWebhook); ok { + if callErr, ok := err.(*webhookerrors.ErrCallingWebhook); ok { if ignoreClientCallFailures { glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr) utilruntime.HandleError(callErr) @@ -313,75 +280,6 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error { return errs[0] } -// TODO: move this method to a common package -func (a *GenericAdmissionWebhook) getNamespaceLabels(attr admission.Attributes) (map[string]string, error) { - // If the request itself is creating or updating a namespace, then get the - // labels from attr.Object, because namespaceLister doesn't have the latest - // namespace yet. - // - // However, if the request is deleting a namespace, then get the label from - // the namespace in the namespaceLister, because a delete request is not - // going to change the object, and attr.Object will be a DeleteOptions - // rather than a namespace object. - if attr.GetResource().Resource == "namespaces" && - len(attr.GetSubresource()) == 0 && - (attr.GetOperation() == admission.Create || attr.GetOperation() == admission.Update) { - accessor, err := meta.Accessor(attr.GetObject()) - if err != nil { - return nil, err - } - return accessor.GetLabels(), nil - } - - namespaceName := attr.GetNamespace() - namespace, err := a.namespaceLister.Get(namespaceName) - if err != nil && !apierrors.IsNotFound(err) { - return nil, err - } - if apierrors.IsNotFound(err) { - // in case of latency in our caches, make a call direct to storage to verify that it truly exists or not - namespace, err = a.client.Core().Namespaces().Get(namespaceName, metav1.GetOptions{}) - if err != nil { - return nil, err - } - } - return namespace.Labels, nil -} - -// TODO: move this method to a common package -// whether the request is exempted by the webhook because of the -// namespaceSelector of the webhook. -func (a *GenericAdmissionWebhook) exemptedByNamespaceSelector(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) { - namespaceName := attr.GetNamespace() - if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" { - // If the request is about a cluster scoped resource, and it is not a - // namespace, it is exempted from all webhooks for now. - // TODO: figure out a way selective exempt cluster scoped resources. - // Also update the comment in types.go - return true, nil - } - namespaceLabels, err := a.getNamespaceLabels(attr) - // this means the namespace is not found, for backwards compatibility, - // return a 404 - if apierrors.IsNotFound(err) { - status, ok := err.(apierrors.APIStatus) - if !ok { - return false, apierrors.NewInternalError(err) - } - return false, &apierrors.StatusError{status.Status()} - } - if err != nil { - return false, apierrors.NewInternalError(err) - } - // TODO: adding an LRU cache to cache the translation - selector, err := metav1.LabelSelectorAsSelector(h.NamespaceSelector) - if err != nil { - return false, apierrors.NewInternalError(err) - } - return !selector.Matches(labels.Set(namespaceLabels)), nil -} - -// TODO: move this method to a common package func (a *GenericAdmissionWebhook) shouldCallHook(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) { var matches bool for _, r := range h.Rules { @@ -395,52 +293,24 @@ func (a *GenericAdmissionWebhook) shouldCallHook(h *v1alpha1.Webhook, attr admis return false, nil } - excluded, err := a.exemptedByNamespaceSelector(h, attr) - if err != nil { - return false, err - } - return !excluded, nil + return a.namespaceMatcher.MatchNamespaceSelector(h, attr) } func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *v1alpha1.Webhook, attr admission.Attributes) error { // Make the webhook request - request := createAdmissionReview(attr) + request := request.CreateAdmissionReview(attr) client, err := a.clientManager.HookClient(h) if err != nil { - return &config.ErrCallingWebhook{WebhookName: h.Name, Reason: err} + 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 &config.ErrCallingWebhook{WebhookName: h.Name, Reason: err} + return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: err} } if response.Status.Allowed { return nil } - return toStatusErr(h.Name, response.Status.Result) -} - -// TODO: move this function to a common package -// toStatusErr returns a StatusError with information about the webhook controller -func toStatusErr(name string, result *metav1.Status) *apierrors.StatusError { - deniedBy := fmt.Sprintf("admission webhook %q denied the request", name) - const noExp = "without explanation" - - if result == nil { - result = &metav1.Status{Status: metav1.StatusFailure} - } - - switch { - case len(result.Message) > 0: - result.Message = fmt.Sprintf("%s: %s", deniedBy, result.Message) - case len(result.Reason) > 0: - result.Message = fmt.Sprintf("%s: %s", deniedBy, result.Reason) - default: - result.Message = fmt.Sprintf("%s %s", deniedBy, noExp) - } - - return &apierrors.StatusError{ - ErrStatus: *result, - } + return webhookerrors.ToStatusErr(h.Name, response.Status.Result) } 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 e773a9b648..4ad25f3e94 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 @@ -24,7 +24,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "reflect" "strings" "sync/atomic" "testing" @@ -37,7 +36,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/plugin/webhook/config" "k8s.io/apiserver/pkg/authentication/user" @@ -146,7 +144,7 @@ func TestAdmit(t *testing.T) { t.Fatal(err) } namespace := "webhook-test" - wh.namespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{ + wh.namespaceMatcher.NamespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{ namespace: { ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ @@ -414,7 +412,7 @@ func TestAdmitCachedClient(t *testing.T) { wh.clientManager = cm wh.SetScheme(scheme) namespace := "webhook-test" - wh.namespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{ + wh.namespaceMatcher.NamespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{ namespace: { ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ @@ -638,55 +636,6 @@ func (c *fakeAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.C return c.restConfig, nil } -func TestToStatusErr(t *testing.T) { - hookName := "foo" - deniedBy := fmt.Sprintf("admission webhook %q denied the request", hookName) - tests := []struct { - name string - result *metav1.Status - expectedError string - }{ - { - "nil result", - nil, - deniedBy + " without explanation", - }, - { - "only message", - &metav1.Status{ - Message: "you shall not pass", - }, - deniedBy + ": you shall not pass", - }, - { - "only reason", - &metav1.Status{ - Reason: metav1.StatusReasonForbidden, - }, - deniedBy + ": Forbidden", - }, - { - "message and reason", - &metav1.Status{ - Message: "you shall not pass", - Reason: metav1.StatusReasonForbidden, - }, - deniedBy + ": you shall not pass", - }, - { - "no message, no reason", - &metav1.Status{}, - deniedBy + " without explanation", - }, - } - for _, test := range tests { - err := toStatusErr(hookName, test.result) - if err == nil || err.Error() != test.expectedError { - t.Errorf("%s: expected an error saying %q, but got %v", test.name, test.expectedError, err) - } - } -} - func newMatchEverythingRules() []registrationv1alpha1.RuleWithOperations { return []registrationv1alpha1.RuleWithOperations{{ Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll}, @@ -697,89 +646,3 @@ func newMatchEverythingRules() []registrationv1alpha1.RuleWithOperations { }, }} } - -func TestGetNamespaceLabels(t *testing.T) { - namespace1Labels := map[string]string{ - "runlevel": "1", - } - namespace1 := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "1", - Labels: namespace1Labels, - }, - } - namespace2Labels := map[string]string{ - "runlevel": "2", - } - namespace2 := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "2", - Labels: namespace2Labels, - }, - } - namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{ - "1": &namespace1, - }, - } - - tests := []struct { - name string - attr admission.Attributes - expectedLabels map[string]string - }{ - { - name: "request is for creating namespace, the labels should be from the object itself", - attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, "", namespace2.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Create, nil), - expectedLabels: namespace2Labels, - }, - { - name: "request is for updating namespace, the labels should be from the new object", - attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, namespace2.Name, namespace2.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Update, nil), - expectedLabels: namespace2Labels, - }, - { - name: "request is for deleting namespace, the labels should be from the cache", - attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, namespace1.Name, namespace1.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Delete, nil), - expectedLabels: namespace1Labels, - }, - { - name: "request is for namespace/finalizer", - attr: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, namespace1.Name, "mock-name", schema.GroupVersionResource{Resource: "namespaces"}, "finalizers", admission.Create, nil), - expectedLabels: namespace1Labels, - }, - { - name: "request is for pod", - attr: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, namespace1.Name, "mock-name", schema.GroupVersionResource{Resource: "pods"}, "", admission.Create, nil), - expectedLabels: namespace1Labels, - }, - } - wh, err := NewGenericAdmissionWebhook(nil) - if err != nil { - t.Fatal(err) - } - wh.namespaceLister = namespaceLister - for _, tt := range tests { - actualLabels, err := wh.getNamespaceLabels(tt.attr) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(actualLabels, tt.expectedLabels) { - t.Errorf("expected labels to be %#v, got %#v", tt.expectedLabels, actualLabels) - } - } -} - -func TestExemptClusterScopedResource(t *testing.T) { - hook := ®istrationv1alpha1.Webhook{ - NamespaceSelector: &metav1.LabelSelector{}, - } - attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, nil) - g := GenericAdmissionWebhook{} - exempted, err := g.exemptedByNamespaceSelector(hook, attr) - if err != nil { - t.Fatal(err) - } - if !exempted { - t.Errorf("cluster scoped resources (but not a namespace) should be exempted from all webhooks") - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/BUILD b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/BUILD new file mode 100644 index 0000000000..4bd5efbf38 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/BUILD @@ -0,0 +1,47 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "attributes.go", + "conversion.go", + "doc.go", + ], + importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned", + visibility = ["//visibility:public"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apiserver/pkg/admission:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["conversion_test.go"], + importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned", + library = ":go_default_library", + deps = [ + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/example:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/apis/example2/v1: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/versioned/attributes.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/attributes.go new file mode 100644 index 0000000000..58f8ae6aa3 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/attributes.go @@ -0,0 +1,42 @@ +/* +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 versioned + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/admission" +) + +// Attributes is a wrapper around the original admission attributes. It allows +// override the internal objects with the versioned ones. +type Attributes struct { + admission.Attributes + OldObject runtime.Object + Object runtime.Object +} + +// GetObject overrides the original GetObjects() and it returns the versioned +// object. +func (v Attributes) GetObject() runtime.Object { + return v.Object +} + +// GetOldObject overrides the original GetOldObjects() and it returns the +// versioned oldObject. +func (v Attributes) GetOldObject() runtime.Object { + return v.OldObject +} 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 new file mode 100644 index 0000000000..8a9fa1d0fd --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion.go @@ -0,0 +1,55 @@ +/* +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 versioned + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Convertor converts objects to the desired version. +type Convertor struct { + Scheme *runtime.Scheme +} + +// 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 + // if obj is a custom resource, it should not need conversion. + if obj.GetObjectKind().GroupVersionKind() == gvk { + return obj, nil + } + out, err := c.Scheme.New(gvk) + if err != nil { + return nil, err + } + err = c.Scheme.Convert(obj, out, nil) + if err != nil { + return nil, err + } + return out, nil +} + +// Validate checks if the conversion has a scheme. +func (c *Convertor) Validate() error { + if c.Scheme == nil { + return fmt.Errorf("the Convertor requires a scheme") + } + return nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/conversion_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion_test.go similarity index 95% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/conversion_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion_test.go index e0be84f8b9..c80db3d0fd 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/conversion_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/conversion_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validating +package versioned import ( "reflect" @@ -39,10 +39,7 @@ func initiateScheme() *runtime.Scheme { func TestConvertToGVK(t *testing.T) { scheme := initiateScheme() - w := GenericAdmissionWebhook{ - convertor: scheme, - creator: scheme, - } + c := Convertor{Scheme: scheme} table := map[string]struct { obj runtime.Object gvk schema.GroupVersionKind @@ -123,7 +120,7 @@ func TestConvertToGVK(t *testing.T) { for name, test := range table { t.Run(name, func(t *testing.T) { - actual, err := w.convertToGVK(test.obj, test.gvk) + actual, err := c.ConvertToGVK(test.obj, test.gvk) if err != nil { t.Error(err) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/doc.go new file mode 100644 index 0000000000..d557a9fec8 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned/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 versioned provides tools for making sure the objects sent to a +// webhook are in a version the webhook understands. +package versioned // import "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned" diff --git a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json index ca7a1ac1f4..e5194fa298 100644 --- a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json @@ -850,6 +850,18 @@ "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/config", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/errors", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/request", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/rules", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" @@ -858,6 +870,10 @@ "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/validating", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/apis/apiserver", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json index c8d95b56d0..602eca6311 100644 --- a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json @@ -846,6 +846,18 @@ "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/config", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/errors", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/request", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/rules", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" @@ -854,6 +866,10 @@ "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/validating", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/apis/apiserver", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"