mirror of https://github.com/k3s-io/k3s
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
commit
c3e4084066
|
@ -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"
|
||||
|
|
|
@ -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"],
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"],
|
||||
)
|
|
@ -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"
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package config
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"],
|
||||
)
|
|
@ -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"
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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"],
|
||||
)
|
|
@ -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()
|
|
@ -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"
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"],
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue