Merge pull request #55739 from caesarxuchao/webhook-move-more-shared-code

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Reorganizing more webhook code

ref: kubernetes/features#492

Continue on https://github.com/kubernetes/kubernetes/pull/55132.

With this PR, all code shared between the mutating and validating webhook plugins is extracted into its own package.
pull/6/head
Kubernetes Submit Queue 2017-11-15 00:05:35 -08:00 committed by GitHub
commit c3e4084066
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 803 additions and 323 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,18 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package errors contains utilities for admission webhook specific errors
package errors // import "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package config
package errors
import "fmt"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 := &registrationv1alpha1.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")
}
}

View File

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

View File

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

View File

@ -0,0 +1,18 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package request creates admissionReview request based on admission attributes.
package request // import "k8s.io/apiserver/pkg/admission/plugin/webhook/request"

View File

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

View File

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

View File

@ -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 := &registrationv1alpha1.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")
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package 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"

View File

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

View File

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