mirror of https://github.com/k3s-io/k3s
Merge pull request #46388 from lavalamp/whitlockjc-generic-webhook-admission
Automatic merge from submit-queue (batch tested with PRs 46239, 46627, 46346, 46388, 46524) Dynamic webhook admission control plugin Unit tests pass. Needs plumbing: * [ ] service resolver (depends on @wfender PR) * [x] client cert (depends on ????) * [ ] hook source (depends on @caesarxuchao PR) Also at least one thing will need to be renamed after Chao's PR merges. ```release-note Allow remote admission controllers to be dynamically added and removed by administrators. External admission controllers make an HTTP POST containing details of the requested action which the service can approve or reject. ```pull/6/head
commit
e837c3bbc2
|
@ -66,6 +66,7 @@ go_library(
|
|||
"//plugin/pkg/admission/securitycontext/scdeny:go_default_library",
|
||||
"//plugin/pkg/admission/serviceaccount:go_default_library",
|
||||
"//plugin/pkg/admission/storageclass/default:go_default_library",
|
||||
"//plugin/pkg/admission/webhook:go_default_library",
|
||||
"//plugin/pkg/auth/authenticator/token/bootstrap:go_default_library",
|
||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
|
|
|
@ -214,10 +214,18 @@ func (s *ServerRunOptions) AddFlags(fs *pflag.FlagSet) {
|
|||
"e.g., setting empty UID in update request to its existing value. This flag can be turned off "+
|
||||
"after we fix all the clients that send malformed updates.")
|
||||
|
||||
fs.StringVar(&s.ProxyClientCertFile, "proxy-client-cert-file", s.ProxyClientCertFile,
|
||||
"client certificate used to prove the identity of the aggragator or kube-apiserver when it proxies requests to a user api-server")
|
||||
fs.StringVar(&s.ProxyClientKeyFile, "proxy-client-key-file", s.ProxyClientKeyFile,
|
||||
"client certificate key used to prove the identity of the aggragator or kube-apiserver when it proxies requests to a user api-server")
|
||||
fs.StringVar(&s.ProxyClientCertFile, "proxy-client-cert-file", s.ProxyClientCertFile, ""+
|
||||
"Client certificate used to prove the identity of the aggregator or kube-apiserver "+
|
||||
"when it must call out during a request. This includes proxying requests to a user "+
|
||||
"api-server and calling out to webhook admission plugins. It is expected that this "+
|
||||
"cert includes a signature from the CA in the --requestheader-client-ca-file flag. "+
|
||||
"That CA is published in the 'extension-apiserver-authentication' configmap in "+
|
||||
"the kube-system namespace. Components recieving calls from kube-aggregator should "+
|
||||
"use that CA to perform their half of the mutual TLS verification.")
|
||||
fs.StringVar(&s.ProxyClientKeyFile, "proxy-client-key-file", s.ProxyClientKeyFile, ""+
|
||||
"Private key for the client certificate used to prove the identity of the aggregator or kube-apiserver "+
|
||||
"when it must call out during a request. This includes proxying requests to a user "+
|
||||
"api-server and calling out to webhook admission plugins.")
|
||||
|
||||
fs.BoolVar(&s.EnableAggregatorRouting, "enable-aggregator-routing", s.EnableAggregatorRouting,
|
||||
"Turns on aggregator routing requests to endoints IP rather than cluster IP.")
|
||||
|
|
|
@ -47,6 +47,7 @@ import (
|
|||
"k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
|
||||
storagedefault "k8s.io/kubernetes/plugin/pkg/admission/storageclass/default"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/webhook"
|
||||
)
|
||||
|
||||
// registerAllAdmissionPlugins registers all admission plugins
|
||||
|
@ -73,4 +74,5 @@ func registerAllAdmissionPlugins(plugins *admission.Plugins) {
|
|||
scdeny.Register(plugins)
|
||||
serviceaccount.Register(plugins)
|
||||
storagedefault.Register(plugins)
|
||||
webhook.Register(plugins)
|
||||
}
|
||||
|
|
|
@ -425,6 +425,20 @@ func BuildAdmissionPluginInitializer(s *options.ServerRunOptions, client interna
|
|||
quotaRegistry := quotainstall.NewRegistry(nil, nil)
|
||||
|
||||
pluginInitializer := kubeapiserveradmission.NewPluginInitializer(client, sharedInformers, apiAuthorizer, cloudConfig, restMapper, quotaRegistry)
|
||||
|
||||
// Read client cert/key for plugins that need to make calls out
|
||||
if len(s.ProxyClientCertFile) > 0 && len(s.ProxyClientKeyFile) > 0 {
|
||||
certBytes, err := ioutil.ReadFile(s.ProxyClientCertFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyBytes, err := ioutil.ReadFile(s.ProxyClientKeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginInitializer = pluginInitializer.SetClientCert(certBytes, keyBytes)
|
||||
}
|
||||
|
||||
return pluginInitializer, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ pkg/api/v1/node
|
|||
pkg/api/v1/service
|
||||
pkg/apis/abac/v0
|
||||
pkg/apis/abac/v1beta1
|
||||
pkg/apis/admission/install
|
||||
pkg/apis/admissionregistration/install
|
||||
pkg/apis/apps/install
|
||||
pkg/apis/apps/v1beta1
|
||||
|
|
|
@ -53,6 +53,7 @@ KUBE_OUTPUT_HOSTBIN="${KUBE_OUTPUT_BINPATH}/$(kube::util::host_platform)"
|
|||
KUBE_AVAILABLE_GROUP_VERSIONS="${KUBE_AVAILABLE_GROUP_VERSIONS:-\
|
||||
v1 \
|
||||
admissionregistration.k8s.io/v1alpha1 \
|
||||
admission.k8s.io/v1alpha1 \
|
||||
apps/v1beta1 \
|
||||
authentication.k8s.io/v1 \
|
||||
authentication.k8s.io/v1beta1 \
|
||||
|
|
|
@ -17,6 +17,8 @@ go_library(
|
|||
"//federation/apis/federation/install:go_default_library",
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/api/install:go_default_library",
|
||||
"//pkg/apis/admission:go_default_library",
|
||||
"//pkg/apis/admission/install:go_default_library",
|
||||
"//pkg/apis/admissionregistration:go_default_library",
|
||||
"//pkg/apis/admissionregistration/install:go_default_library",
|
||||
"//pkg/apis/apps:go_default_library",
|
||||
|
|
|
@ -36,6 +36,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime/serializer/recognizer"
|
||||
"k8s.io/kubernetes/federation/apis/federation"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/admission"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/apis/apps"
|
||||
"k8s.io/kubernetes/pkg/apis/authorization"
|
||||
|
@ -52,6 +53,7 @@ import (
|
|||
|
||||
_ "k8s.io/kubernetes/federation/apis/federation/install"
|
||||
_ "k8s.io/kubernetes/pkg/api/install"
|
||||
_ "k8s.io/kubernetes/pkg/apis/admission/install"
|
||||
_ "k8s.io/kubernetes/pkg/apis/admissionregistration/install"
|
||||
_ "k8s.io/kubernetes/pkg/apis/apps/install"
|
||||
_ "k8s.io/kubernetes/pkg/apis/authentication/install"
|
||||
|
@ -84,6 +86,7 @@ var (
|
|||
Settings TestGroup
|
||||
Storage TestGroup
|
||||
ImagePolicy TestGroup
|
||||
Admission TestGroup
|
||||
Networking TestGroup
|
||||
|
||||
serializer runtime.SerializerInfo
|
||||
|
@ -292,6 +295,15 @@ func init() {
|
|||
externalTypes: api.Scheme.KnownTypes(externalGroupVersion),
|
||||
}
|
||||
}
|
||||
if _, ok := Groups[admission.GroupName]; !ok {
|
||||
externalGroupVersion := schema.GroupVersion{Group: admission.GroupName, Version: api.Registry.GroupOrDie(admission.GroupName).GroupVersion.Version}
|
||||
Groups[admission.GroupName] = TestGroup{
|
||||
externalGroupVersion: externalGroupVersion,
|
||||
internalGroupVersion: admission.SchemeGroupVersion,
|
||||
internalTypes: api.Scheme.KnownTypes(admission.SchemeGroupVersion),
|
||||
externalTypes: api.Scheme.KnownTypes(externalGroupVersion),
|
||||
}
|
||||
}
|
||||
if _, ok := Groups[networking.GroupName]; !ok {
|
||||
externalGroupVersion := schema.GroupVersion{Group: networking.GroupName, Version: api.Registry.GroupOrDie(networking.GroupName).GroupVersion.Version}
|
||||
Groups[networking.GroupName] = TestGroup{
|
||||
|
@ -315,6 +327,7 @@ func init() {
|
|||
Storage = Groups[storage.GroupName]
|
||||
ImagePolicy = Groups[imagepolicy.GroupName]
|
||||
Authorization = Groups[authorization.GroupName]
|
||||
Admission = Groups[admission.GroupName]
|
||||
Networking = Groups[networking.GroupName]
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ filegroup(
|
|||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//pkg/apis/admission/install:all-srcs",
|
||||
"//pkg/apis/admission/v1alpha1:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["install.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/apis/admission:go_default_library",
|
||||
"//pkg/apis/admission/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
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 install installs the experimental API group, making it available as
|
||||
// an option to all of the API encoding/decoding machinery.
|
||||
package install
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
||||
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/admission"
|
||||
"k8s.io/kubernetes/pkg/apis/admission/v1alpha1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Install(api.GroupFactoryRegistry, api.Registry, api.Scheme)
|
||||
}
|
||||
|
||||
// Install registers the API group and adds types to a scheme
|
||||
func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) {
|
||||
if err := announced.NewGroupMetaFactory(
|
||||
&announced.GroupMetaFactoryArgs{
|
||||
GroupName: admission.GroupName,
|
||||
VersionPreferenceOrder: []string{v1alpha1.SchemeGroupVersion.Version},
|
||||
ImportPrefix: "k8s.io/kubernetes/pkg/apis/admission",
|
||||
RootScopedKinds: sets.NewString("AdmissionReview"),
|
||||
AddInternalObjectsToScheme: admission.AddToScheme,
|
||||
},
|
||||
announced.VersionToSchemeFunc{
|
||||
v1alpha1.SchemeGroupVersion.Version: v1alpha1.AddToScheme,
|
||||
},
|
||||
).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ go_library(
|
|||
"register.go",
|
||||
"types.generated.go",
|
||||
"types.go",
|
||||
"types_swagger_doc_generated.go",
|
||||
"zz_generated.conversion.go",
|
||||
"zz_generated.deepcopy.go",
|
||||
"zz_generated.defaults.go",
|
||||
|
|
|
@ -92,12 +92,13 @@ func (x *AdmissionReview) CodecEncodeSelf(e *codec1978.Encoder) {
|
|||
const yyr2 bool = false
|
||||
yyq2[0] = x.Kind != ""
|
||||
yyq2[1] = x.APIVersion != ""
|
||||
yyq2[2] = true
|
||||
yyq2[3] = true
|
||||
var yynn2 int
|
||||
if yyr2 || yy2arr2 {
|
||||
r.EncodeArrayStart(4)
|
||||
} else {
|
||||
yynn2 = 1
|
||||
yynn2 = 0
|
||||
for _, b := range yyq2 {
|
||||
if b {
|
||||
yynn2++
|
||||
|
@ -158,14 +159,20 @@ func (x *AdmissionReview) CodecEncodeSelf(e *codec1978.Encoder) {
|
|||
}
|
||||
if yyr2 || yy2arr2 {
|
||||
z.EncSendContainerState(codecSelfer_containerArrayElem1234)
|
||||
yy10 := &x.Spec
|
||||
yy10.CodecEncodeSelf(e)
|
||||
if yyq2[2] {
|
||||
yy10 := &x.Spec
|
||||
yy10.CodecEncodeSelf(e)
|
||||
} else {
|
||||
r.EncodeNil()
|
||||
}
|
||||
} else {
|
||||
z.EncSendContainerState(codecSelfer_containerMapKey1234)
|
||||
r.EncodeString(codecSelferC_UTF81234, string("spec"))
|
||||
z.EncSendContainerState(codecSelfer_containerMapValue1234)
|
||||
yy12 := &x.Spec
|
||||
yy12.CodecEncodeSelf(e)
|
||||
if yyq2[2] {
|
||||
z.EncSendContainerState(codecSelfer_containerMapKey1234)
|
||||
r.EncodeString(codecSelferC_UTF81234, string("spec"))
|
||||
z.EncSendContainerState(codecSelfer_containerMapValue1234)
|
||||
yy12 := &x.Spec
|
||||
yy12.CodecEncodeSelf(e)
|
||||
}
|
||||
}
|
||||
if yyr2 || yy2arr2 {
|
||||
z.EncSendContainerState(codecSelfer_containerArrayElem1234)
|
||||
|
|
|
@ -30,7 +30,7 @@ type AdmissionReview struct {
|
|||
// Since this admission controller is non-mutating the webhook should avoid setting this in its response to avoid the
|
||||
// cost of deserializing it.
|
||||
// +optional
|
||||
Spec AdmissionReviewSpec `json:"spec" protobuf:"bytes,1,opt,name=spec"`
|
||||
Spec AdmissionReviewSpec `json:"spec,omitempty" protobuf:"bytes,1,opt,name=spec"`
|
||||
// Status is filled in by the webhook and indicates whether the admission request should be permitted.
|
||||
// +optional
|
||||
Status AdmissionReviewStatus `json:"status,omitempty" protobuf:"bytes,2,opt,name=status"`
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2016 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 v1alpha1
|
||||
|
||||
// This file contains a collection of methods that can be used from go-restful to
|
||||
// generate Swagger API documentation for its models. Please read this PR for more
|
||||
// information on the implementation: https://github.com/emicklei/go-restful/pull/215
|
||||
//
|
||||
// TODOs are ignored from the parser (e.g. TODO(andronat):... || TODO:...) if and only if
|
||||
// they are on one line! For multiple line or blocks that you want to ignore use ---.
|
||||
// Any context after a --- is ignored.
|
||||
//
|
||||
// Those methods can be generated by using hack/update-generated-swagger-docs.sh
|
||||
|
||||
// AUTO-GENERATED FUNCTIONS START HERE
|
||||
var map_AdmissionReview = map[string]string{
|
||||
"": "AdmissionReview describes an admission request.",
|
||||
"spec": "Spec describes the attributes for the admission request. Since this admission controller is non-mutating the webhook should avoid setting this in its response to avoid the cost of deserializing it.",
|
||||
"status": "Status is filled in by the webhook and indicates whether the admission request should be permitted.",
|
||||
}
|
||||
|
||||
func (AdmissionReview) SwaggerDoc() map[string]string {
|
||||
return map_AdmissionReview
|
||||
}
|
||||
|
||||
var map_AdmissionReviewSpec = map[string]string{
|
||||
"": "AdmissionReviewSpec describes the admission.Attributes for the admission request.",
|
||||
"kind": "Kind is the type of object being manipulated. For example: Pod",
|
||||
"object": "Object is the object from the incoming request prior to default values being applied",
|
||||
"oldObject": "OldObject is the existing object. Only populated for UPDATE requests.",
|
||||
"operation": "Operation is the operation being performed",
|
||||
"name": "Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and rely on the server to generate the name. If that is the case, this method will return the empty string.",
|
||||
"namespace": "Namespace is the namespace associated with the request (if any).",
|
||||
"resource": "Resource is the name of the resource being requested. This is not the kind. For example: pods",
|
||||
"subResource": "SubResource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind. For instance, /pods has the resource \"pods\" and the kind \"Pod\", while /pods/foo/status has the resource \"pods\", the sub resource \"status\", and the kind \"Pod\" (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource \"pods\", subresource \"binding\", and kind \"Binding\".",
|
||||
"userInfo": "UserInfo is information about the requesting user",
|
||||
}
|
||||
|
||||
func (AdmissionReviewSpec) SwaggerDoc() map[string]string {
|
||||
return map_AdmissionReviewSpec
|
||||
}
|
||||
|
||||
var map_AdmissionReviewStatus = map[string]string{
|
||||
"": "AdmissionReviewStatus describes the status of the admission request.",
|
||||
"allowed": "Allowed indicates whether or not the admission request was permitted.",
|
||||
"status": "Result contains extra details into why an admission request was denied. This field IS NOT consulted in any way if \"Allowed\" is \"true\".",
|
||||
}
|
||||
|
||||
func (AdmissionReviewStatus) SwaggerDoc() map[string]string {
|
||||
return map_AdmissionReviewStatus
|
||||
}
|
||||
|
||||
// AUTO-GENERATED FUNCTIONS END HERE
|
|
@ -14,6 +14,7 @@ go_test(
|
|||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/apis/admissionregistration:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
|
@ -24,6 +25,7 @@ go_library(
|
|||
srcs = ["initializer.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/apis/admissionregistration:go_default_library",
|
||||
"//pkg/client/clientset_generated/internalclientset:go_default_library",
|
||||
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
|
||||
"//pkg/quota:go_default_library",
|
||||
|
|
|
@ -17,10 +17,12 @@ limitations under the License.
|
|||
package admission
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
// TestAuthorizer is a testing struct for testing that fulfills the authorizer interface.
|
||||
|
@ -32,18 +34,22 @@ func (t *TestAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, re
|
|||
|
||||
var _ authorizer.Authorizer = &TestAuthorizer{}
|
||||
|
||||
type doNothingAdmission struct{}
|
||||
|
||||
func (doNothingAdmission) Admit(a admission.Attributes) error { return nil }
|
||||
func (doNothingAdmission) Handles(o admission.Operation) bool { return false }
|
||||
func (doNothingAdmission) Validate() error { return nil }
|
||||
|
||||
// WantAuthorizerAdmission is a testing struct that fulfills the WantsAuthorizer
|
||||
// interface.
|
||||
type WantAuthorizerAdmission struct {
|
||||
doNothingAdmission
|
||||
auth authorizer.Authorizer
|
||||
}
|
||||
|
||||
func (self *WantAuthorizerAdmission) SetAuthorizer(a authorizer.Authorizer) {
|
||||
self.auth = a
|
||||
}
|
||||
func (self *WantAuthorizerAdmission) Admit(a admission.Attributes) error { return nil }
|
||||
func (self *WantAuthorizerAdmission) Handles(o admission.Operation) bool { return false }
|
||||
func (self *WantAuthorizerAdmission) Validate() error { return nil }
|
||||
|
||||
var _ admission.Interface = &WantAuthorizerAdmission{}
|
||||
var _ WantsAuthorizer = &WantAuthorizerAdmission{}
|
||||
|
@ -60,6 +66,7 @@ func TestWantsAuthorizer(t *testing.T) {
|
|||
}
|
||||
|
||||
type WantsCloudConfigAdmissionPlugin struct {
|
||||
doNothingAdmission
|
||||
cloudConfig []byte
|
||||
}
|
||||
|
||||
|
@ -67,10 +74,6 @@ func (self *WantsCloudConfigAdmissionPlugin) SetCloudConfig(cloudConfig []byte)
|
|||
self.cloudConfig = cloudConfig
|
||||
}
|
||||
|
||||
func (self *WantsCloudConfigAdmissionPlugin) Admit(a admission.Attributes) error { return nil }
|
||||
func (self *WantsCloudConfigAdmissionPlugin) Handles(o admission.Operation) bool { return false }
|
||||
func (self *WantsCloudConfigAdmissionPlugin) Validate() error { return nil }
|
||||
|
||||
func TestCloudConfigAdmissionPlugin(t *testing.T) {
|
||||
cloudConfig := []byte("cloud-configuration")
|
||||
initializer := NewPluginInitializer(nil, nil, &TestAuthorizer{}, cloudConfig, nil, nil)
|
||||
|
@ -81,3 +84,65 @@ func TestCloudConfigAdmissionPlugin(t *testing.T) {
|
|||
t.Errorf("Expected cloud config to be initialized but found nil")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeServiceResolver struct{}
|
||||
|
||||
func (f *fakeServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type serviceWanter struct {
|
||||
doNothingAdmission
|
||||
got ServiceResolver
|
||||
}
|
||||
|
||||
func (s *serviceWanter) SetServiceResolver(sr ServiceResolver) { s.got = sr }
|
||||
|
||||
func TestWantsServiceResolver(t *testing.T) {
|
||||
sw := &serviceWanter{}
|
||||
fsr := &fakeServiceResolver{}
|
||||
i := &PluginInitializer{}
|
||||
i.SetServiceResolver(fsr).Initialize(sw)
|
||||
if got, ok := sw.got.(*fakeServiceResolver); !ok || got != fsr {
|
||||
t.Errorf("plumbing fail - %v %v#", ok, got)
|
||||
}
|
||||
}
|
||||
|
||||
type clientCertWanter struct {
|
||||
doNothingAdmission
|
||||
gotCert, gotKey []byte
|
||||
}
|
||||
|
||||
func (s *clientCertWanter) SetClientCert(cert, key []byte) { s.gotCert, s.gotKey = cert, key }
|
||||
|
||||
func TestWantsClientCert(t *testing.T) {
|
||||
i := &PluginInitializer{}
|
||||
ccw := &clientCertWanter{}
|
||||
i.SetClientCert([]byte("cert"), []byte("key")).Initialize(ccw)
|
||||
if string(ccw.gotCert) != "cert" || string(ccw.gotKey) != "key" {
|
||||
t.Errorf("plumbing fail - %v %v", ccw.gotCert, ccw.gotKey)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeHookSource struct{}
|
||||
|
||||
func (f *fakeHookSource) List() ([]admissionregistration.ExternalAdmissionHook, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type hookSourceWanter struct {
|
||||
doNothingAdmission
|
||||
got WebhookSource
|
||||
}
|
||||
|
||||
func (s *hookSourceWanter) SetWebhookSource(w WebhookSource) { s.got = w }
|
||||
|
||||
func TestWantsWebhookSource(t *testing.T) {
|
||||
hsw := &hookSourceWanter{}
|
||||
fhs := &fakeHookSource{}
|
||||
i := &PluginInitializer{}
|
||||
i.SetWebhookSource(fhs).Initialize(hsw)
|
||||
if got, ok := hsw.got.(*fakeHookSource); !ok || got != fhs {
|
||||
t.Errorf("plumbing fail - %v %v#", ok, got)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,12 @@ limitations under the License.
|
|||
package admission
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
|
||||
"k8s.io/kubernetes/pkg/quota"
|
||||
|
@ -61,25 +64,62 @@ type WantsQuotaRegistry interface {
|
|||
admission.Validator
|
||||
}
|
||||
|
||||
type pluginInitializer struct {
|
||||
internalClient internalclientset.Interface
|
||||
informers informers.SharedInformerFactory
|
||||
authorizer authorizer.Authorizer
|
||||
cloudConfig []byte
|
||||
restMapper meta.RESTMapper
|
||||
quotaRegistry quota.Registry
|
||||
// WantsServiceResolver defines a fuction that accepts a ServiceResolver for
|
||||
// admission plugins that need to make calls to services.
|
||||
type WantsServiceResolver interface {
|
||||
SetServiceResolver(ServiceResolver)
|
||||
}
|
||||
|
||||
var _ admission.PluginInitializer = pluginInitializer{}
|
||||
// WantsClientCert defines a fuction that accepts a cert & key for admission
|
||||
// plugins that need to make calls and prove their identity.
|
||||
type WantsClientCert interface {
|
||||
SetClientCert(cert, key []byte)
|
||||
}
|
||||
|
||||
// WantsWebhookSource defines a function that accepts a webhook lister for the
|
||||
// dynamic webhook plugin.
|
||||
type WantsWebhookSource interface {
|
||||
SetWebhookSource(WebhookSource)
|
||||
}
|
||||
|
||||
// ServiceResolver knows how to convert a service reference into an actual
|
||||
// location.
|
||||
type ServiceResolver interface {
|
||||
ResolveEndpoint(namespace, name string) (*url.URL, error)
|
||||
}
|
||||
|
||||
// WebhookSource can list dynamic webhook plugins.
|
||||
type WebhookSource interface {
|
||||
List() ([]admissionregistration.ExternalAdmissionHook, error)
|
||||
}
|
||||
|
||||
type PluginInitializer struct {
|
||||
internalClient internalclientset.Interface
|
||||
informers informers.SharedInformerFactory
|
||||
authorizer authorizer.Authorizer
|
||||
cloudConfig []byte
|
||||
restMapper meta.RESTMapper
|
||||
quotaRegistry quota.Registry
|
||||
serviceResolver ServiceResolver
|
||||
webhookSource WebhookSource
|
||||
|
||||
// for proving we are apiserver in call-outs
|
||||
clientCert []byte
|
||||
clientKey []byte
|
||||
}
|
||||
|
||||
var _ admission.PluginInitializer = &PluginInitializer{}
|
||||
|
||||
// NewPluginInitializer constructs new instance of PluginInitializer
|
||||
// TODO: switch these parameters to use the builder pattern or just make them
|
||||
// all public, this construction method is pointless boilerplate.
|
||||
func NewPluginInitializer(internalClient internalclientset.Interface,
|
||||
sharedInformers informers.SharedInformerFactory,
|
||||
authz authorizer.Authorizer,
|
||||
cloudConfig []byte,
|
||||
restMapper meta.RESTMapper,
|
||||
quotaRegistry quota.Registry) admission.PluginInitializer {
|
||||
return pluginInitializer{
|
||||
quotaRegistry quota.Registry) *PluginInitializer {
|
||||
return &PluginInitializer{
|
||||
internalClient: internalClient,
|
||||
informers: sharedInformers,
|
||||
authorizer: authz,
|
||||
|
@ -89,9 +129,30 @@ func NewPluginInitializer(internalClient internalclientset.Interface,
|
|||
}
|
||||
}
|
||||
|
||||
// SetServiceResolver sets the service resolver which is needed by some plugins.
|
||||
func (i *PluginInitializer) SetServiceResolver(s ServiceResolver) *PluginInitializer {
|
||||
i.serviceResolver = s
|
||||
return i
|
||||
}
|
||||
|
||||
// SetClientCert sets the client cert & key (identity used for calling out to
|
||||
// web hooks) which is needed by some plugins.
|
||||
func (i *PluginInitializer) SetClientCert(cert, key []byte) *PluginInitializer {
|
||||
i.clientCert = cert
|
||||
i.clientKey = key
|
||||
return i
|
||||
}
|
||||
|
||||
// SetWebhookSource sets the webhook source-- admittedly this is probably
|
||||
// specific to the external admission hook plugin.
|
||||
func (i *PluginInitializer) SetWebhookSource(w WebhookSource) *PluginInitializer {
|
||||
i.webhookSource = w
|
||||
return i
|
||||
}
|
||||
|
||||
// Initialize checks the initialization interfaces implemented by each plugin
|
||||
// and provide the appropriate initialization data
|
||||
func (i pluginInitializer) Initialize(plugin admission.Interface) {
|
||||
func (i *PluginInitializer) Initialize(plugin admission.Interface) {
|
||||
if wants, ok := plugin.(WantsInternalKubeClientSet); ok {
|
||||
wants.SetInternalKubeClientSet(i.internalClient)
|
||||
}
|
||||
|
@ -115,4 +176,25 @@ func (i pluginInitializer) Initialize(plugin admission.Interface) {
|
|||
if wants, ok := plugin.(WantsQuotaRegistry); ok {
|
||||
wants.SetQuotaRegistry(i.quotaRegistry)
|
||||
}
|
||||
|
||||
if wants, ok := plugin.(WantsServiceResolver); ok {
|
||||
if i.serviceResolver == nil {
|
||||
panic("An admission plugin wants the service resolver, but it was not provided.")
|
||||
}
|
||||
wants.SetServiceResolver(i.serviceResolver)
|
||||
}
|
||||
|
||||
if wants, ok := plugin.(WantsClientCert); ok {
|
||||
if i.clientCert == nil || i.clientKey == nil {
|
||||
panic("An admission plugin wants a client cert/key, but they were not provided.")
|
||||
}
|
||||
wants.SetClientCert(i.clientCert, i.clientKey)
|
||||
}
|
||||
|
||||
if wants, ok := plugin.(WantsWebhookSource); ok {
|
||||
if i.webhookSource == nil {
|
||||
panic("An admission plugin wants a webhook source, but it was not provided.")
|
||||
}
|
||||
wants.SetWebhookSource(i.webhookSource)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -986,9 +986,8 @@ func TestRunApplySetLastApplied(t *testing.T) {
|
|||
if buf.String() != test.expectedOut {
|
||||
t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {})
|
||||
}
|
||||
|
||||
func checkPatchString(t *testing.T, req *http.Request) {
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestCreateDeployment(t *testing.T) {
|
|||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(&bytes.Buffer{}),
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -171,7 +171,7 @@ func TestRunArgsFollowDashRules(t *testing.T) {
|
|||
} else {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(&bytes.Buffer{}),
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer([]byte("{}"))),
|
||||
}, nil
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -25,6 +25,7 @@ go_library(
|
|||
"//pkg/api/endpoints:go_default_library",
|
||||
"//pkg/api/install:go_default_library",
|
||||
"//pkg/api/v1:go_default_library",
|
||||
"//pkg/apis/admission/install:go_default_library",
|
||||
"//pkg/apis/admissionregistration/install:go_default_library",
|
||||
"//pkg/apis/admissionregistration/v1alpha1:go_default_library",
|
||||
"//pkg/apis/apps/install:go_default_library",
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
_ "k8s.io/kubernetes/pkg/api/install"
|
||||
_ "k8s.io/kubernetes/pkg/apis/admission/install"
|
||||
_ "k8s.io/kubernetes/pkg/apis/admissionregistration/install"
|
||||
_ "k8s.io/kubernetes/pkg/apis/apps/install"
|
||||
_ "k8s.io/kubernetes/pkg/apis/authentication/install"
|
||||
|
|
|
@ -85,6 +85,7 @@ var typesAllowedTags = map[reflect.Type]bool{
|
|||
reflect.TypeOf(metav1.DeleteOptions{}): true,
|
||||
reflect.TypeOf(metav1.GroupVersionKind{}): true,
|
||||
reflect.TypeOf(metav1.GroupVersionResource{}): true,
|
||||
reflect.TypeOf(metav1.Status{}): true,
|
||||
}
|
||||
|
||||
func ensureNoTags(t *testing.T, gvk schema.GroupVersionKind, tp reflect.Type, parents []reflect.Type) {
|
||||
|
|
|
@ -36,6 +36,7 @@ filegroup(
|
|||
"//plugin/pkg/admission/securitycontext/scdeny:all-srcs",
|
||||
"//plugin/pkg/admission/serviceaccount:all-srcs",
|
||||
"//plugin/pkg/admission/storageclass/default:all-srcs",
|
||||
"//plugin/pkg/admission/webhook:all-srcs",
|
||||
"//plugin/pkg/auth:all-srcs",
|
||||
"//plugin/pkg/scheduler:all-srcs",
|
||||
],
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"admission_test.go",
|
||||
"certs_test.go",
|
||||
"rules_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/apis/admission/install:go_default_library",
|
||||
"//pkg/apis/admission/v1alpha1:go_default_library",
|
||||
"//pkg/apis/admissionregistration:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1: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/authentication/user:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"admission.go",
|
||||
"doc.go",
|
||||
"rules.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/apis/admission/install:go_default_library",
|
||||
"//pkg/apis/admission/v1alpha1:go_default_library",
|
||||
"//pkg/apis/admissionregistration:go_default_library",
|
||||
"//pkg/kubeapiserver/admission:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors: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/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/client-go/rest:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
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 webhook delegates admission checks to dynamically configured webhooks.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"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/apiserver/pkg/admission"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
admissionv1alpha1 "k8s.io/kubernetes/pkg/apis/admission/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
admissioninit "k8s.io/kubernetes/pkg/kubeapiserver/admission"
|
||||
|
||||
// install the clientgo admission API for use with api registry
|
||||
_ "k8s.io/kubernetes/pkg/apis/admission/install"
|
||||
)
|
||||
|
||||
var (
|
||||
groupVersions = []schema.GroupVersion{
|
||||
admissionv1alpha1.SchemeGroupVersion,
|
||||
}
|
||||
)
|
||||
|
||||
type ErrCallingWebhook struct {
|
||||
WebhookName string
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e *ErrCallingWebhook) Error() string {
|
||||
if e.Reason != nil {
|
||||
return fmt.Sprintf("failed calling admission webhook %q: %v", e.WebhookName, e.Reason)
|
||||
}
|
||||
return fmt.Sprintf("failed calling admission webhook %q; no further details available", e.WebhookName)
|
||||
}
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register("GenericAdmissionWebhook", func(configFile io.Reader) (admission.Interface, error) {
|
||||
plugin, err := NewGenericAdmissionWebhook()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
// NewGenericAdmissionWebhook returns a generic admission webhook plugin.
|
||||
func NewGenericAdmissionWebhook() (*GenericAdmissionWebhook, error) {
|
||||
return &GenericAdmissionWebhook{
|
||||
Handler: admission.NewHandler(
|
||||
admission.Connect,
|
||||
admission.Create,
|
||||
admission.Delete,
|
||||
admission.Update,
|
||||
),
|
||||
negotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
|
||||
Serializer: api.Codecs.LegacyCodec(admissionv1alpha1.SchemeGroupVersion),
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenericAdmissionWebhook is an implementation of admission.Interface.
|
||||
type GenericAdmissionWebhook struct {
|
||||
*admission.Handler
|
||||
hookSource admissioninit.WebhookSource
|
||||
serviceResolver admissioninit.ServiceResolver
|
||||
negotiatedSerializer runtime.NegotiatedSerializer
|
||||
clientCert []byte
|
||||
clientKey []byte
|
||||
}
|
||||
|
||||
var (
|
||||
_ = admissioninit.WantsServiceResolver(&GenericAdmissionWebhook{})
|
||||
_ = admissioninit.WantsClientCert(&GenericAdmissionWebhook{})
|
||||
_ = admissioninit.WantsWebhookSource(&GenericAdmissionWebhook{})
|
||||
)
|
||||
|
||||
func (a *GenericAdmissionWebhook) SetServiceResolver(sr admissioninit.ServiceResolver) {
|
||||
a.serviceResolver = sr
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) SetClientCert(cert, key []byte) {
|
||||
a.clientCert = cert
|
||||
a.clientKey = key
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) SetWebhookSource(ws admissioninit.WebhookSource) {
|
||||
a.hookSource = ws
|
||||
}
|
||||
|
||||
// Admit makes an admission decision based on the request attributes.
|
||||
func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
|
||||
hooks, err := a.hookSource.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed listing hooks: %v", err)
|
||||
}
|
||||
ctx := context.TODO()
|
||||
|
||||
errCh := make(chan error, len(hooks))
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(hooks))
|
||||
for i := range hooks {
|
||||
go func(hook *admissionregistration.ExternalAdmissionHook) {
|
||||
defer wg.Done()
|
||||
if err := a.callHook(ctx, hook, attr); err == nil {
|
||||
return
|
||||
} else if callErr, ok := err.(*ErrCallingWebhook); ok {
|
||||
glog.Warningf("Failed calling webhook %v: %v", hook.Name, callErr)
|
||||
utilruntime.HandleError(callErr)
|
||||
// Since we are failing open to begin with, we do not send an error down the channel
|
||||
} else {
|
||||
glog.Warningf("rejected by webhook %v %t: %v", hook.Name, err, err)
|
||||
errCh <- err
|
||||
}
|
||||
}(&hooks[i])
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
|
||||
var errs []error
|
||||
for e := range errCh {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
for i := 1; i < len(errs); i++ {
|
||||
// TODO: merge status errors; until then, just return the first one.
|
||||
utilruntime.HandleError(errs[i])
|
||||
}
|
||||
}
|
||||
return errs[0]
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *admissionregistration.ExternalAdmissionHook, attr admission.Attributes) error {
|
||||
matches := false
|
||||
for _, r := range h.Rules {
|
||||
m := RuleMatcher{Rule: r, Attr: attr}
|
||||
if m.Matches() {
|
||||
matches = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matches {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the webhook request
|
||||
request := admissionv1alpha1.NewAdmissionReview(attr)
|
||||
client, err := a.hookClient(h)
|
||||
if err != nil {
|
||||
return &ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||
}
|
||||
if err := client.Post().Context(ctx).Body(&request).Do().Into(&request); err != nil {
|
||||
return &ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||
}
|
||||
|
||||
if request.Status.Allowed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if request.Status.Result == nil {
|
||||
return fmt.Errorf("admission webhook %q denied the request without explanation", h.Name)
|
||||
}
|
||||
|
||||
return &apierrors.StatusError{
|
||||
ErrStatus: *request.Status.Result,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) hookClient(h *admissionregistration.ExternalAdmissionHook) (*rest.RESTClient, error) {
|
||||
u, err := a.serviceResolver.ResolveEndpoint(h.ClientConfig.Service.Namespace, h.ClientConfig.Service.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: cache these instead of constructing one each time
|
||||
cfg := &rest.Config{
|
||||
Host: u.Host,
|
||||
APIPath: u.Path,
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
CAData: h.ClientConfig.CABundle,
|
||||
CertData: a.clientCert,
|
||||
KeyData: a.clientKey,
|
||||
},
|
||||
UserAgent: "kube-apiserver-admission",
|
||||
Timeout: 30 * time.Second,
|
||||
ContentConfig: rest.ContentConfig{
|
||||
NegotiatedSerializer: a.negotiatedSerializer,
|
||||
},
|
||||
}
|
||||
return rest.UnversionedRESTClientFor(cfg)
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
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 webhook
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/admission/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
|
||||
_ "k8s.io/kubernetes/pkg/apis/admission/install"
|
||||
)
|
||||
|
||||
type fakeHookSource struct {
|
||||
hooks []admissionregistration.ExternalAdmissionHook
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeHookSource) List() ([]admissionregistration.ExternalAdmissionHook, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
return f.hooks, nil
|
||||
}
|
||||
|
||||
type fakeServiceResolver struct {
|
||||
base url.URL
|
||||
}
|
||||
|
||||
func (f fakeServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
|
||||
if namespace == "failResolve" {
|
||||
return nil, fmt.Errorf("couldn't resolve service location")
|
||||
}
|
||||
u := f.base
|
||||
u.Path = name
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// TestAdmit tests that GenericAdmissionWebhook#Admit works as expected
|
||||
func TestAdmit(t *testing.T) {
|
||||
// Create the test webhook server
|
||||
sCert, err := tls.X509KeyPair(serverCert, serverKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AppendCertsFromPEM(caCert)
|
||||
testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler))
|
||||
testServer.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{sCert},
|
||||
ClientCAs: rootCAs,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
testServer.StartTLS()
|
||||
defer testServer.Close()
|
||||
serverURL, err := url.ParseRequestURI(testServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("this should never happen? %v", err)
|
||||
}
|
||||
wh, err := NewGenericAdmissionWebhook()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wh.serviceResolver = fakeServiceResolver{*serverURL}
|
||||
wh.clientCert = clientCert
|
||||
wh.clientKey = clientKey
|
||||
|
||||
// Set up a test object for the call
|
||||
kind := api.Kind("Pod").WithVersion("v1")
|
||||
name := "my-pod"
|
||||
namespace := "webhook-test"
|
||||
object := api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
},
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := api.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
UID: "webhook-test",
|
||||
}
|
||||
|
||||
type test struct {
|
||||
hookSource fakeHookSource
|
||||
expectAllow bool
|
||||
errorContains string
|
||||
}
|
||||
ccfg := func(result string) admissionregistration.AdmissionHookClientConfig {
|
||||
return admissionregistration.AdmissionHookClientConfig{
|
||||
Service: admissionregistration.ServiceReference{
|
||||
Name: result,
|
||||
},
|
||||
CABundle: caCert,
|
||||
}
|
||||
}
|
||||
matchEverythingRules := []admissionregistration.RuleWithOperations{{
|
||||
Operations: []admissionregistration.OperationType{admissionregistration.OperationAll},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"*"},
|
||||
APIVersions: []string{"*"},
|
||||
Resources: []string{"*/*"},
|
||||
},
|
||||
}}
|
||||
|
||||
table := map[string]test{
|
||||
"no match": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "nomatch",
|
||||
ClientConfig: ccfg("disallow"),
|
||||
Rules: []admissionregistration.RuleWithOperations{{
|
||||
Operations: []admissionregistration.OperationType{admissionregistration.Create},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & allow": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "allow",
|
||||
ClientConfig: ccfg("allow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & disallow": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfg("disallow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "without explanation",
|
||||
},
|
||||
"match & disallow ii": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "disallowReason",
|
||||
ClientConfig: ccfg("disallowReason"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "you shall not pass",
|
||||
},
|
||||
"match & fail (but allow because fail open)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfg("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
}, {
|
||||
Name: "invalidReq B",
|
||||
ClientConfig: ccfg("invalidReq"),
|
||||
Rules: matchEverythingRules,
|
||||
}, {
|
||||
Name: "invalidResp C",
|
||||
ClientConfig: ccfg("invalidResp"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range table {
|
||||
wh.hookSource = &tt.hookSource
|
||||
|
||||
err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, name, resource, subResource, operation, &userInfo))
|
||||
if tt.expectAllow != (err == nil) {
|
||||
t.Errorf("%q: expected allowed=%v, but got err=%v", name, tt.expectAllow, err)
|
||||
}
|
||||
// ErrWebhookRejected is not an error for our purposes
|
||||
if tt.errorContains != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tt.errorContains) {
|
||||
t.Errorf("%q: expected an error saying %q, but got %v", name, tt.errorContains, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("got req: %v\n", r.URL.Path)
|
||||
switch r.URL.Path {
|
||||
case "/internalErr":
|
||||
http.Error(w, "webhook internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
case "/invalidReq":
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
w.Write([]byte("webhook invalid request"))
|
||||
return
|
||||
case "/invalidResp":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("webhook invalid response"))
|
||||
case "/disallow":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
|
||||
Status: v1alpha1.AdmissionReviewStatus{
|
||||
Allowed: false,
|
||||
},
|
||||
})
|
||||
case "/disallowReason":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
|
||||
Status: v1alpha1.AdmissionReviewStatus{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Message: "you shall not pass",
|
||||
},
|
||||
},
|
||||
})
|
||||
case "/allow":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
|
||||
Status: v1alpha1.AdmissionReviewStatus{
|
||||
Allowed: true,
|
||||
},
|
||||
})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// This file was generated using openssl by the gencerts.sh script
|
||||
// and holds raw certificates for the webhook tests.
|
||||
|
||||
package webhook
|
||||
|
||||
var caKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA5qdHpJR3sAg1afEWLlnxdbDV1LtAvCd9WWE/4g71+BBwvwLL
|
||||
C/piPRi+m/7AYHJ9zoVwvwAUYBJM2ppQV2Gwsq4XA5dLvr8fXt4+YA/sLTAo0PAm
|
||||
/QQmZziHU9v7OJ04ypjTTuA86D/EkkpEP7lRkN/NFpV3PhU0F5TTrkrzSpePBIR1
|
||||
aPSzh/6Um6TtFk9oiqat05leWcMzonizrgeQU9EW6bSYY5w+gq1X2eZb1s9eR98H
|
||||
0xcZfoh3qHmJ1Iq9Cc2Nr+MUohm2m45ozT+1L+g46ZMJxyPB6xLr7uOhkoJsK30q
|
||||
67AZKPo58tDTmEKSXfBotIvFI5N9P3sAWxcTiQIDAQABAoIBAAaJHOWT82RAh0ru
|
||||
MuOzVr0v+o8hky8Bq3KZ59Z++AdEZ/1xldFMEfaLOfNvn4HcHKZ6b3xqAynJuvXC
|
||||
w54GPZyChFJsug+4mKn2gCv2p4mMQMvS0jf/IxtvpZ4BsLek9NQAypQElJU8IVTH
|
||||
1/E6Tg5d2RDXwV43+Zbld64Ln6MwZGwv8UFPEHylDMjwkex5u3tzVBD5NaegI0MD
|
||||
AHAf3fiCsANmAeGWjTvUXQsOes6wjaHw6kbih5QrXM6iThHfU/YHXYmgfdfSSIFC
|
||||
4puLaehp3/U8HcI97xN238B0khnkOVHzUmRJpmf17SWFSkOAZyUMiFfTSFSOedvu
|
||||
lFO7v0UCgYEA/FczxADDZXq4SSTvrU61XzolgI//OKblsqe4RW8JGtk8pwjZdYOQ
|
||||
v4UAYEcv5rUWA3wWohcjgNWzI9EzdhOCYpC9YFqbHJalmwhGgOjRCvDy581pcXz7
|
||||
xsfkm2loWm3g+PcrCIset2tGQw/5gqSkBW42E/U0ba25wCS6RlYWCFsCgYEA6f+Q
|
||||
vENXPmBUBW8TsyjWj7MZKVzKuV2yyKT37Mf7RmrpSNpft0PLJDqGqRMfhe1J7Adm
|
||||
Np1fv/18RngjGjV3fSnjbvQ51748gabwzGZKWKiWJPsRDqjEriiQcFInhaqVJs7F
|
||||
D0TaWalBHWyjPHCQQx7rWqA8tr3Wpga0AhNUuOsCgYEAnbrQU7L6cEM+UBIzcswh
|
||||
GO4apPrdWIcSSxMFXvlh4pNpkys36nmbj+tN6eB1c6s7oF//McBu48gwWrIYjbTy
|
||||
KjQ4+7KHBF6yE28fytI8YK9t1jESuOqb4ovuPKqtnODT4Ct3jbaQM6xtVdv1ZZEO
|
||||
KYrTaLQ72lbeJdmPSgnjacMCgYASip6kXE2ocqeVuqR7+MtvnYhr359sqsEE5xWC
|
||||
HKKLhOMxU6Rr+CI7n6uV8B76VMAbxMZTo4q3wtU7HD/jzsLGFzCfVRjUQI241EqW
|
||||
V7Cib9Fd4ssKN1NGXY58Z/YbwFWLOq0gtZr7qc6wDzCsFFtKBkQt7S6CaG5+v186
|
||||
HuACuwKBgEd0JaREFj6AG6boytQAx+Npj+wGG7K22O8v9YslJjaS5t2i8XrvIr0F
|
||||
5ltR8Ijegp9a2pCgjshaEzUqMHhxX3EGvUxVM4R430EaQ933WRPmiLlVLyhthYt0
|
||||
9oPxMoN783J7UP/IkBc6AGi3a4uTn/h6Vi884wOLop4bmsK37uIt
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var caCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDhDCCAmygAwIBAgIJAIf67NAEFfGmMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV
|
||||
BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||
DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA0MTIwMAYDVQQDFClnZW5l
|
||||
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAOanR6SUd7AINWnxFi5Z8XWw1dS7QLwnfVlh
|
||||
P+IO9fgQcL8Cywv6Yj0Yvpv+wGByfc6FcL8AFGASTNqaUFdhsLKuFwOXS76/H17e
|
||||
PmAP7C0wKNDwJv0EJmc4h1Pb+zidOMqY007gPOg/xJJKRD+5UZDfzRaVdz4VNBeU
|
||||
065K80qXjwSEdWj0s4f+lJuk7RZPaIqmrdOZXlnDM6J4s64HkFPRFum0mGOcPoKt
|
||||
V9nmW9bPXkffB9MXGX6Id6h5idSKvQnNja/jFKIZtpuOaM0/tS/oOOmTCccjwesS
|
||||
6+7joZKCbCt9KuuwGSj6OfLQ05hCkl3waLSLxSOTfT97AFsXE4kCAwEAAaOBljCB
|
||||
kzAdBgNVHQ4EFgQU55hOE1Dsydy16+6wgOxwsPKZ8JEwZAYDVR0jBF0wW4AU55hO
|
||||
E1Dsydy16+6wgOxwsPKZ8JGhOKQ2MDQxMjAwBgNVBAMUKWdlbmVyaWNfd2ViaG9v
|
||||
a19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhggkAh/rs0AQV8aYwDAYDVR0TBAUw
|
||||
AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAtEa7AMnFdc/GmVrtY0aOmA3h1WJ/rhhd
|
||||
SnVOp7LmA+1jnA6FMMvVCOFflYLfBwLhIPjPj4arQadWHyd5Quok6GIqgzL4RkK7
|
||||
67hPMc8inNH8w+9K0kgsCls06Jy08NIt7QTtIckh8skvxQsfJ6An/ROiCNI5QiYj
|
||||
oQOK3Tp8jGf/2wcsJLeO9y09ZPcOUbLkDe2YlnT+OqNMx9VXrPSvRwq2qtYphgYW
|
||||
YWHPMEBnB/9XrVkEtnKeWPjtjarVd+rNnfVpCW0ImnZRFtKQQeI4rtf4Gr83Gdju
|
||||
Z0gPepfIFptDMl5wKWyw4o2XVSZ69Ur8tQQynoNyX/FTIx6tQ//hzg==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA2Mbqfl5nW+rYCSw/+G00ymtBElvTwzEI88sg3m5PXU0XMv5y
|
||||
Tyjt+S3mhbzLY460ty6fXlEiveNNP0bzbo0k+tvUj7HbOG4q4Cs0JWiLvibchRXQ
|
||||
LimqtndsVsT2xj56m7oTETWMYhjd8KFz1s6w7yK9dChe2pBCeeGjIbQs0def8Bf0
|
||||
5h7BbsmuXQb44nnpUdWgFI/Frqcz6jO6jySYV5U3ajqCjcWOBQmXRtMi1we48Hdj
|
||||
2t4jW/evbRIo/tX88kluXFSVBmTitKA3nn23AzA+/qa6kTDwzQ8OntjC09JT7ykS
|
||||
764WLRiBOo+wC3a6M3eZNMkW615+DNP7wAjYEwIDAQABAoIBAC9lZnXUvDKLqUpw
|
||||
I1h0wBsV0jdqXmWJ/hQXsIsRgUa8CTt8CJAoOcfGcmWBPtL4q6h1iCC+CqOL5CLW
|
||||
p3jfYVt73wC/+VdgNv2mVJNtRUiBBKwQdeDx+UJF4CkkjXQQywvrZinYFGaKW1Q2
|
||||
aLZpoKPYa6XPAdY1vmMZo2pGE5qZbGbIXKcl3kl5N2X0qyJKvTR6RA1Q8JiTmATh
|
||||
H1U3S9K/MIGU+9OwP8zCDVKFdDI+xgJvDo05W8bt6XLfmE4bLRYHRfe0iE0uBNSC
|
||||
zx5OGBNqkiHq/vMk5mLCYM0w/uzUkhB5uXqe5gSqa7DioLrHztqAe+sbtBvfL+Yd
|
||||
hP3asWECgYEA9NQXa2B6AVXVf2GII67H8diPFrILKLEw4CUZjFmVbvrKIRB2ypqA
|
||||
IsohpYl97s0nstetZt/75uEvkv3RPu5ad5LJaVAPHE3ESXKdAz6u4fSmi33qqPi6
|
||||
PvhHLYDDXvMgC7j2yCYyANVKNg2T+EJvpWMISlXM4h7CVm21CSDVZjECgYEA4qsl
|
||||
zDA3sHoClfC4nAOVrRghYlHU2bT6HPLxjLtBkcUTfj6nO6nOLGC7EFVkqYw5mUWq
|
||||
uSNntk1D1+MYVnZBeKqw6y21FFsclmzzXTtAJg0vuAg1jxnMHqiDbNqpUUO8ZWLG
|
||||
iz2tdAWiBAKwZv0Psv44Dy++4v/BMd8FBb/iHYMCgYEAttKmRmW91b9t9Xg0fEjp
|
||||
QBzyBQWhNZrTn520rUy8PSqDxBsSSgsDgncklwPMCYYjjfZmo3rBFdC0gPSOy4qb
|
||||
/cycIMtK7VzZJeuzehfV6h+SOnolwFY0Zg9qv3z257FwDbDqf92d22dqymBrTaj2
|
||||
zC7eovvdSkGj53x3AsEE+hECgYAd7JJU3pi7h6AHw3vbvO1pqKHfpQYAp8/NOpWB
|
||||
CsehQu9L32GcktJRMYQAqAVeDNEd1wCu6Gmsu46VVbnE0F/cWkx4/9PEGDMx+Lg4
|
||||
OrZBT8RY+1x2w+UatwyCtmtb+yFIET4866uWgZfeB6zaK9aCvuUPvDHrLfCHcPXs
|
||||
yGRFmQKBgQDYvcjkUqKvnZwwpV6p6BHi6PuHWyjL+GLTk/IzsxOJykHzX4SV46Sd
|
||||
9IxyE+OWZhkISBMSMe8wQC/S+QYs8hDbEE3WH1DXp33joyNgt2aWI0Splu7S1f6P
|
||||
Pe08fYlLgz1xrbJuJAOgvkbskAUcmfmRwFeGaZSkkCtkzVxgN7LS3Q==
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var badCACert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDhDCCAmygAwIBAgIJAIabsJi6ILN7MA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV
|
||||
BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||
DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA0MTIwMAYDVQQDFClnZW5l
|
||||
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBANjG6n5eZ1vq2AksP/htNMprQRJb08MxCPPL
|
||||
IN5uT11NFzL+ck8o7fkt5oW8y2OOtLcun15RIr3jTT9G826NJPrb1I+x2zhuKuAr
|
||||
NCVoi74m3IUV0C4pqrZ3bFbE9sY+epu6ExE1jGIY3fChc9bOsO8ivXQoXtqQQnnh
|
||||
oyG0LNHXn/AX9OYewW7Jrl0G+OJ56VHVoBSPxa6nM+ozuo8kmFeVN2o6go3FjgUJ
|
||||
l0bTItcHuPB3Y9reI1v3r20SKP7V/PJJblxUlQZk4rSgN559twMwPv6mupEw8M0P
|
||||
Dp7YwtPSU+8pEu+uFi0YgTqPsAt2ujN3mTTJFutefgzT+8AI2BMCAwEAAaOBljCB
|
||||
kzAdBgNVHQ4EFgQU9SUu1vDxGeyGEgy7pzZjLm+WSH0wZAYDVR0jBF0wW4AU9SUu
|
||||
1vDxGeyGEgy7pzZjLm+WSH2hOKQ2MDQxMjAwBgNVBAMUKWdlbmVyaWNfd2ViaG9v
|
||||
a19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhggkAhpuwmLogs3swDAYDVR0TBAUw
|
||||
AwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAViib1ZcwLUi5YE279NRypsGLa8pfT/A8
|
||||
10u41L4xw+32mD2HojMmAlAF4jnC62exaXFsAYEsze4TF+0zqwDkHyGqViw/hKAv
|
||||
SrGgPUX3C7wLyiMa3pjZfcQQy+80SKiJLeClxxjkdhO0mGNo1LdJThYU5IADHVtF
|
||||
u2oOKLTjWBVzkMRkTXp5RReeEoUPvFgJhPKIVLggdXdJT8oQjgIVlx6IuzjU0AeM
|
||||
tJ5AIWYrsqv9FlpfUXWjdiy8uF3iLWTOpd6pICnjzfj02wQouTEkxQ2iFinl7Das
|
||||
iK+7d34q6Ww1/1nu4EBBDYB1VlWdhDLJVT4F+mF8wZFBu863Ba+U5Q==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAquioChbTG+iyTg3+SwGww/7yN84jtj55v8Xld8TgkyavSZm7
|
||||
Q6WURevvXtJHMug/NAz6qoCFy/zOiozUrMB36GFOA8MtwaOddCMdHMruX7q2+CiF
|
||||
amhFg58uPfVr9qit20JiyBhaPH97WmQzwsfRQt4E1mCbEHZYK6r1RlF3i00nDCxc
|
||||
741wuYp4Uc5oYM0dYoVCv11DYdf62v1grLFO02a5qjAULS74KKNaJ4YDOmqnTtWt
|
||||
I75ZkEJSC2TT90o8eFIRuImY17venqIbp30A+XtvMhITg55648Zdci+CGHDcqJA1
|
||||
C+y0rxXZkcuFFFrK0tiN7K/cPK1LDtS08gp61QIDAQABAoIBAFhGVxTu+Rc/N2lt
|
||||
fNzNALobIoyEYpms50GQO5eDDuOyZXNEfh7QlScQV9DIF5JJtutxkL8kJvdXmm6h
|
||||
ku+vcb+LErqKw0Vy9s6XnF/UyQ6U6BCBDXgKZ202eLHz41HBihrnzRHA0krRJato
|
||||
efuvLXy2JBV+TFlSZvQXFxy801wVIxlI7Jh94YR7CT3HYmD5qjAtiTVkSS+wmx++
|
||||
cfh0rrMulkXDEUlRPm/fXqt0do+neH9eNYee4h0mbZ84f3ecz/ql3HfkfoK9ZBq+
|
||||
M85VWnEvRetRF0AQlYK3IUPQH2XHIEZkUab8s+EZoIsgVBY9xvl6VTEQWFtbzRWC
|
||||
7ozg2PkCgYEA3X8Pv6a5wzWBwX17RkTXcPqS44uzJ/K0adv1QSrVvXrZJQ0FEGqr
|
||||
gR74aQXiaVl06X/N50y/soQIHWpvqGjoEbkA+jS5y4GkGxAviFFAuRnW9DyRePxH
|
||||
nQiYFzgxBj55iDsdvdqJ2e0vM1EWNcaVmghNI25D+cBc4Qh1Zz4qFSsCgYEAxYg9
|
||||
vYGepcTMJ0/dk4SVMaITi/8nAnZHnptILpSjgPNp8udsovQPkymwo+EDnjQqVPO4
|
||||
OIIICEopk6Zup0Rq2iW3zRGbRJtp+uJ7mfFS+nT6sAe0tPWbGejwrudDDcx0fkx4
|
||||
C+1//rJ95H0c9L4nd51azCJD0k2yKtIFyj91r/8CgYBwMkWa8exU+oyQo2xHSuXK
|
||||
n9K6GnCUwrcqjDWuXfFI+qp1vyOajj3zuOlh4Y4viRXUlV2KVXEhDwpBREHtD77G
|
||||
A22AUCbw8+lZoBhDt8zONk2RCAE0RK5N2CWaVWdX31uWa0OEgOelESUAnIlgkggD
|
||||
r0LLuLYME6m4f51gv7d3YwKBgCFp8He0C3AjIB2uRt8DWHFy5zeRS7oA5BCSV915
|
||||
S0cu5ccvGpNeEZxlOvodwAzs6hRAvfLhHBa65NmTF7i3vBN2uea4iblLSNwln57k
|
||||
0ZKIYzePtiO+QCRb4QrVF+SnpzUOHmh2HmapLt6Nw24rFGYJeih5y1sxxWe060HR
|
||||
BkllAoGBAMkT1a3BhhEwoyyKiwC05+gzlKfAWz7t6J//6/yx+82lXDk4/J455qcw
|
||||
ny2y6P6r964EUoqMrAU0bdTs3sKDtOLdNMIt5RfoDBsdQDt2ktbv0pvii3E8SQFi
|
||||
JuJWSenrfFaI+AgwE9jDo1Hy6dhF6/hnV3+QoznwEPRAO6wmPyVA
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDOzCCAiOgAwIBAgIJAN0PSMLOjTVAMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV
|
||||
BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||
DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA4MTYwNAYDVQQDDC1nZW5l
|
||||
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19zZXJ2ZXIwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq6KgKFtMb6LJODf5LAbDD/vI3ziO2
|
||||
Pnm/xeV3xOCTJq9JmbtDpZRF6+9e0kcy6D80DPqqgIXL/M6KjNSswHfoYU4Dwy3B
|
||||
o510Ix0cyu5furb4KIVqaEWDny499Wv2qK3bQmLIGFo8f3taZDPCx9FC3gTWYJsQ
|
||||
dlgrqvVGUXeLTScMLFzvjXC5inhRzmhgzR1ihUK/XUNh1/ra/WCssU7TZrmqMBQt
|
||||
Lvgoo1onhgM6aqdO1a0jvlmQQlILZNP3Sjx4UhG4iZjXu96eohunfQD5e28yEhOD
|
||||
nnrjxl1yL4IYcNyokDUL7LSvFdmRy4UUWsrS2I3sr9w8rUsO1LTyCnrVAgMBAAGj
|
||||
SjBIMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC
|
||||
BggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQCA
|
||||
/IFclqY/dsadGY0e9U3xq0XliIjZIHaI6OLXmO8xBdLDVf2+d6Bt3dl9gQMtGQI3
|
||||
zj9vqJrM+znXR30yAERFefZItk8hTzAotk15HYExVJkIn5JQBaXRbeO2DUZFgAnu
|
||||
6OU6KnuVC6i+7xDlbMl8wtRPmeZ6FS1wW4wnxLWZtKYAuLVDs0ISy6qbznGhCkWc
|
||||
b0uPbxnMmZHQLVL+yF1LYWpX9+Xa9QnWXOSY7KHtcuYXZB/XV4Pt6aDncg76bFdl
|
||||
MG3bocbJ9MsoS/LdlAiYzLNmKlFa243QPOo/zN170NhEZaF1lM80YBaLIE4rRtga
|
||||
nrkNOnPHx1evvuleH0Yv
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA1Cf8Tch5IieN9zAI0b5FFJoMzPsN3RAYD5GAk3Xo9XNbKnvJ
|
||||
38o2TKdxFu1/Q8hqG3668vWuL6TYDYVcYWtC6rwNpU/moAdJZyyEtmZIam1Va1VW
|
||||
HDCpylsk4b8jbUxxm6OzF1XxZpBUJg9o/1OvL1XA0rmcDiddXN5VdL2adJZsS54t
|
||||
gI57hcH0jkQocHTEv5gIl2tcTDStd4GeWxBmCotYdCQWcNk+96QMRxIai88aPPYw
|
||||
QxpOQeaABrYrjPO1fJA0jimM9bhkjGXK6cnrRQrg3jkeF3vuPw9BFbE4VeENLY+y
|
||||
v2OWBPYOD8ov0Tn2inge4QjE2DZyK95l46wQbwIDAQABAoIBAGZiAY1cALEt24H9
|
||||
yVPG+bluelz1jwQuvx3MPvtqvIivKcC/ynVYNYoaiCXjaTZB4orwRrH3RB8z8xvb
|
||||
TvCofbugEwnDHG3/9jl3L3iCtdG+f6lznkGublH8WDklL6iQaocMoeHSFNRFNIbF
|
||||
iwskzHcQcCSBdEEUWCb4GM9krMQz7pBR9BloyV40ZAGilMXI9F9FZ0YBWWee09gi
|
||||
jid6sEzYQveZ5RQgEEDrE/i+jzXkU8sBKsSm1GuKH62+YrtelBqP83DwVIKthMOJ
|
||||
79tw6i98v5JHV+1ikqC1Na/c7OxBBF3xrgwCN47ok0cHaIXh7SX+EIN6jcwKTmRH
|
||||
VZQBz2ECgYEA6tTpJEKNAzEY3KOO578zY2hWhPUNqzUTm8yLxZedhDt2rwyWjd4J
|
||||
HhS7CiNqFpMT/kxFyOupA8SFJPZDhJBXttnyzO9Sb61fLWgSP739K9OQfK1FGTA6
|
||||
khjc3vHtBeGrWm9+1jSxQtrwly7Rs+EmdvseDCN7yie/mgrBLS7p1l8CgYEA50fN
|
||||
6BnbeAgCTK8GDBWSJPaYUlo/lebUDCn5QIp1LK93vPQWjJR8xRBwL1TbiVKGd054
|
||||
dRZVuJYMJx+2mbrt5ca9UArisZp5OZgR4xz29n9u69P5XiuG8Fq/JBJXp1GXONVx
|
||||
JNOsUHOW/b3w2tNUWZcMQAH601BHOtO+EtaEX/ECgYBHygz4A8xeFG1YTjwKxt3r
|
||||
3uLMRKoIE/LJp093eXEzEoam3v9LoXxCEO5ZHBh7jD0JecG/uaNyvmpBsXNUnFfk
|
||||
U16xndwiveqh0/X4PJmgA05hfwrnt2HAdg9XrLfcG3Ap9nnc/EDQgmQYo7yB9Cux
|
||||
JfW6mkJmu54Mdos1x+i+mwKBgHmewcGe71E0bPkkRLrQERUM89bCjJNoWfO3ktIE
|
||||
vU9tSjr75GuyndYHKedJ6VRSKFHO2vs/bn5tsSBVxfEbYoSlOOJBhyo8AClwNV/H
|
||||
2HqRUqQCySxjGUeFgOQYHS3ocuw5GZFzGjcIQctXObPo0391NcTnBZ5fpcVimZ5Q
|
||||
XjYRAoGAN2O3HQjPyThoOUOn5MJEIx0L5bMvGNDzJO+oFngAntX/zv0zu/zsD9tc
|
||||
kk4EbMLluiw2/XJvYjCStieaYxbSWioKwlThy39C+iUut+IbpP2eI46SOkhvPczt
|
||||
4u1/sslqjs4ZSntR4Z9UOk3vY3oxRKbiXX2/vl9cqzB/cGYu01c=
|
||||
-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
var clientCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDOzCCAiOgAwIBAgIJAN0PSMLOjTVBMA0GCSqGSIb3DQEBBQUAMDQxMjAwBgNV
|
||||
BAMUKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||
DTE3MDUwNDIyMzMyOFoYDzIyOTEwMjE3MjIzMzI4WjA4MTYwNAYDVQQDDC1nZW5l
|
||||
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jbGllbnQwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUJ/xNyHkiJ433MAjRvkUUmgzM+w3d
|
||||
EBgPkYCTdej1c1sqe8nfyjZMp3EW7X9DyGobfrry9a4vpNgNhVxha0LqvA2lT+ag
|
||||
B0lnLIS2ZkhqbVVrVVYcMKnKWyThvyNtTHGbo7MXVfFmkFQmD2j/U68vVcDSuZwO
|
||||
J11c3lV0vZp0lmxLni2AjnuFwfSORChwdMS/mAiXa1xMNK13gZ5bEGYKi1h0JBZw
|
||||
2T73pAxHEhqLzxo89jBDGk5B5oAGtiuM87V8kDSOKYz1uGSMZcrpyetFCuDeOR4X
|
||||
e+4/D0EVsThV4Q0tj7K/Y5YE9g4Pyi/ROfaKeB7hCMTYNnIr3mXjrBBvAgMBAAGj
|
||||
SjBIMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC
|
||||
BggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQC/
|
||||
TBXk511JBKLosKVqrjluo8bzbgnREUrPcKclatiAOiIFKbMBy4nE4BlGZZW34t1u
|
||||
sStB1dDHBHIuEkZxs93xwjqXN03yNNfve+FkRcb+guaZJEIBRlNocNxhd+lVDo8J
|
||||
axRTdoOxyEOHGCjg+gyb0i9f/rqEqLnDwnYLZbH9Qbh/yv6OgISUTYOCzH35H0/6
|
||||
unY5JaBhRvmJHI0Z3KtmvMShbUyzoYD+oNLaS31fvoYIekcHsnOjZGBukaIx1bE1
|
||||
4SFjCUSPGDdzJdaYxQb0UXNI7oXKr6e6YeOrglIrVbboa0X3jtqGF1U7rop8ts3v
|
||||
24SeXsvxqJht40itVvGK
|
||||
-----END CERTIFICATE-----`)
|
|
@ -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 webhook checks a webhook for configured operation admission
|
||||
package webhook // import "k8s.io/kubernetes/plugin/pkg/admission/webhook"
|
|
@ -0,0 +1,107 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
|
||||
# gencerts.sh generates the certificates for the generic webhook admission plugin tests.
|
||||
#
|
||||
# It is not expected to be run often (there is no go generate rule), and mainly
|
||||
# exists for documentation purposes.
|
||||
|
||||
CN_BASE="generic_webhook_admission_plugin_tests"
|
||||
|
||||
cat > server.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth, serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
cat > client.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth, serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
# Create a certificate authority
|
||||
openssl genrsa -out caKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=${CN_BASE}_ca"
|
||||
|
||||
# Create a second certificate authority
|
||||
openssl genrsa -out badCAKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=${CN_BASE}_ca"
|
||||
|
||||
# Create a server certiticate
|
||||
openssl genrsa -out serverKey.pem 2048
|
||||
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=${CN_BASE}_server" -config server.conf
|
||||
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
|
||||
|
||||
# Create a client certiticate
|
||||
openssl genrsa -out clientKey.pem 2048
|
||||
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf
|
||||
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
|
||||
|
||||
outfile=certs_test.go
|
||||
|
||||
cat > $outfile << EOF
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
EOF
|
||||
|
||||
echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile
|
||||
echo "// and holds raw certificates for the webhook tests." >> $outfile
|
||||
echo "" >> $outfile
|
||||
echo "package webhook" >> $outfile
|
||||
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
|
||||
data=$(cat ${file}.pem)
|
||||
echo "" >> $outfile
|
||||
echo "var $file = []byte(\`$data\`)" >> $outfile
|
||||
done
|
||||
|
||||
# Clean up after we're done.
|
||||
rm *.pem
|
||||
rm *.csr
|
||||
rm *.srl
|
||||
rm *.conf
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
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 webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
type RuleMatcher struct {
|
||||
Rule admissionregistration.RuleWithOperations
|
||||
Attr admission.Attributes
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) Matches() bool {
|
||||
return r.operation() &&
|
||||
r.group() &&
|
||||
r.version() &&
|
||||
r.resource()
|
||||
}
|
||||
|
||||
func exactOrWildcard(items []string, requested string) bool {
|
||||
for _, item := range items {
|
||||
if item == "*" {
|
||||
return true
|
||||
}
|
||||
if item == requested {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) group() bool {
|
||||
return exactOrWildcard(r.Rule.APIGroups, r.Attr.GetResource().Group)
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) version() bool {
|
||||
return exactOrWildcard(r.Rule.APIVersions, r.Attr.GetResource().Version)
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) operation() bool {
|
||||
attrOp := r.Attr.GetOperation()
|
||||
for _, op := range r.Rule.Operations {
|
||||
if op == admissionregistration.OperationAll {
|
||||
return true
|
||||
}
|
||||
// The constants are the same such that this is a valid cast (and this
|
||||
// is tested).
|
||||
if op == admissionregistration.OperationType(attrOp) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func splitResource(resSub string) (res, sub string) {
|
||||
parts := strings.SplitN(resSub, "/", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
return parts[0], ""
|
||||
}
|
||||
|
||||
func (r *RuleMatcher) resource() bool {
|
||||
opRes, opSub := r.Attr.GetResource().Resource, r.Attr.GetSubresource()
|
||||
for _, res := range r.Rule.Resources {
|
||||
res, sub := splitResource(res)
|
||||
resMatch := res == "*" || res == opRes
|
||||
subMatch := sub == "*" || sub == opSub
|
||||
if resMatch && subMatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
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 webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
adreg "k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
type ruleTest struct {
|
||||
rule adreg.RuleWithOperations
|
||||
match []admission.Attributes
|
||||
noMatch []admission.Attributes
|
||||
}
|
||||
type tests map[string]ruleTest
|
||||
|
||||
func a(group, version, resource, subresource, name string, operation admission.Operation) admission.Attributes {
|
||||
return admission.NewAttributesRecord(
|
||||
nil, nil,
|
||||
schema.GroupVersionKind{Group: group, Version: version, Kind: "k" + resource},
|
||||
"ns", name,
|
||||
schema.GroupVersionResource{Group: group, Version: version, Resource: resource}, subresource,
|
||||
operation,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func attrList(a ...admission.Attributes) []admission.Attributes {
|
||||
return a
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
table := tests{
|
||||
"wildcard": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
APIGroups: []string{"*"},
|
||||
},
|
||||
},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
"exact": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
APIGroups: []string{"g1", "g2"},
|
||||
},
|
||||
},
|
||||
match: attrList(
|
||||
a("g1", "v", "r", "", "name", admission.Create),
|
||||
a("g2", "v2", "r3", "", "name", admission.Create),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g3", "v", "r", "", "name", admission.Create),
|
||||
a("g4", "v", "r", "", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range table {
|
||||
for _, m := range tt.match {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if !r.group() {
|
||||
t.Errorf("%v: expected match %#v", name, m)
|
||||
}
|
||||
}
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if r.group() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
table := tests{
|
||||
"wildcard": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
APIVersions: []string{"*"},
|
||||
},
|
||||
},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
"exact": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
APIVersions: []string{"v1", "v2"},
|
||||
},
|
||||
},
|
||||
match: attrList(
|
||||
a("g1", "v1", "r", "", "name", admission.Create),
|
||||
a("g2", "v2", "r", "", "name", admission.Create),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g1", "v3", "r", "", "name", admission.Create),
|
||||
a("g2", "v4", "r", "", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
}
|
||||
for name, tt := range table {
|
||||
for _, m := range tt.match {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if !r.version() {
|
||||
t.Errorf("%v: expected match %#v", name, m)
|
||||
}
|
||||
}
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if r.version() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperation(t *testing.T) {
|
||||
table := tests{
|
||||
"wildcard": {
|
||||
rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.OperationAll}},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("g", "v", "r", "", "name", admission.Update),
|
||||
a("g", "v", "r", "", "name", admission.Delete),
|
||||
a("g", "v", "r", "", "name", admission.Connect),
|
||||
),
|
||||
},
|
||||
"create": {
|
||||
rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Create}},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Update),
|
||||
a("g", "v", "r", "", "name", admission.Delete),
|
||||
a("g", "v", "r", "", "name", admission.Connect),
|
||||
),
|
||||
},
|
||||
"update": {
|
||||
rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Update}},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Update),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("g", "v", "r", "", "name", admission.Delete),
|
||||
a("g", "v", "r", "", "name", admission.Connect),
|
||||
),
|
||||
},
|
||||
"delete": {
|
||||
rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Delete}},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Delete),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("g", "v", "r", "", "name", admission.Update),
|
||||
a("g", "v", "r", "", "name", admission.Connect),
|
||||
),
|
||||
},
|
||||
"connect": {
|
||||
rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Connect}},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Connect),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("g", "v", "r", "", "name", admission.Update),
|
||||
a("g", "v", "r", "", "name", admission.Delete),
|
||||
),
|
||||
},
|
||||
"multiple": {
|
||||
rule: adreg.RuleWithOperations{Operations: []adreg.OperationType{adreg.Update, adreg.Delete}},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Update),
|
||||
a("g", "v", "r", "", "name", admission.Delete),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("g", "v", "r", "", "name", admission.Connect),
|
||||
),
|
||||
},
|
||||
}
|
||||
for name, tt := range table {
|
||||
for _, m := range tt.match {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if !r.operation() {
|
||||
t.Errorf("%v: expected match %#v", name, m)
|
||||
}
|
||||
}
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if r.operation() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResource(t *testing.T) {
|
||||
table := tests{
|
||||
"no subresources": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
Resources: []string{"*"},
|
||||
},
|
||||
},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("2", "v", "r2", "", "name", admission.Create),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g", "v", "r", "exec", "name", admission.Create),
|
||||
a("2", "v", "r2", "proxy", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
"r & subresources": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
Resources: []string{"r/*"},
|
||||
},
|
||||
},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("g", "v", "r", "exec", "name", admission.Create),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("2", "v", "r2", "", "name", admission.Create),
|
||||
a("2", "v", "r2", "proxy", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
"r & subresources or r2": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
Resources: []string{"r/*", "r2"},
|
||||
},
|
||||
},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("g", "v", "r", "exec", "name", admission.Create),
|
||||
a("2", "v", "r2", "", "name", admission.Create),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("2", "v", "r2", "proxy", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
"proxy or exec": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
Resources: []string{"*/proxy", "*/exec"},
|
||||
},
|
||||
},
|
||||
match: attrList(
|
||||
a("g", "v", "r", "exec", "name", admission.Create),
|
||||
a("2", "v", "r2", "proxy", "name", admission.Create),
|
||||
a("2", "v", "r3", "proxy", "name", admission.Create),
|
||||
),
|
||||
noMatch: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
a("2", "v", "r2", "", "name", admission.Create),
|
||||
a("2", "v", "r4", "scale", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
}
|
||||
for name, tt := range table {
|
||||
for _, m := range tt.match {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if !r.resource() {
|
||||
t.Errorf("%v: expected match %#v", name, m)
|
||||
}
|
||||
}
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if r.resource() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,8 +29,8 @@ import (
|
|||
//
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
type GroupResource struct {
|
||||
Group string `protobuf:"bytes,1,opt,name=group"`
|
||||
Resource string `protobuf:"bytes,2,opt,name=resource"`
|
||||
Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
|
||||
Resource string `json:"resource" protobuf:"bytes,2,opt,name=resource"`
|
||||
}
|
||||
|
||||
func (gr *GroupResource) String() string {
|
||||
|
@ -45,9 +45,9 @@ func (gr *GroupResource) String() string {
|
|||
//
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
type GroupVersionResource struct {
|
||||
Group string `protobuf:"bytes,1,opt,name=group"`
|
||||
Version string `protobuf:"bytes,2,opt,name=version"`
|
||||
Resource string `protobuf:"bytes,3,opt,name=resource"`
|
||||
Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
|
||||
Version string `json:"version" protobuf:"bytes,2,opt,name=version"`
|
||||
Resource string `json:"resource" protobuf:"bytes,3,opt,name=resource"`
|
||||
}
|
||||
|
||||
func (gvr *GroupVersionResource) String() string {
|
||||
|
@ -59,8 +59,8 @@ func (gvr *GroupVersionResource) String() string {
|
|||
//
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
type GroupKind struct {
|
||||
Group string `protobuf:"bytes,1,opt,name=group"`
|
||||
Kind string `protobuf:"bytes,2,opt,name=kind"`
|
||||
Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
|
||||
Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"`
|
||||
}
|
||||
|
||||
func (gk *GroupKind) String() string {
|
||||
|
@ -88,8 +88,8 @@ func (gvk GroupVersionKind) String() string {
|
|||
//
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
type GroupVersion struct {
|
||||
Group string `protobuf:"bytes,1,opt,name=group"`
|
||||
Version string `protobuf:"bytes,2,opt,name=version"`
|
||||
Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
|
||||
Version string `json:"version" protobuf:"bytes,2,opt,name=version"`
|
||||
}
|
||||
|
||||
// Empty returns true if group and version are empty
|
||||
|
|
|
@ -1148,6 +1148,9 @@ func (r Result) Into(obj runtime.Object) error {
|
|||
if r.decoder == nil {
|
||||
return fmt.Errorf("serializer for %s doesn't exist", r.contentType)
|
||||
}
|
||||
if len(r.body) == 0 {
|
||||
return fmt.Errorf("0-length response")
|
||||
}
|
||||
|
||||
out, _, err := r.decoder.Decode(r.body, nil, obj)
|
||||
if err != nil || out == obj {
|
||||
|
|
|
@ -329,6 +329,16 @@ func TestResultIntoWithErrReturnsErr(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResultIntoWithNoBodyReturnsErr(t *testing.T) {
|
||||
res := Result{
|
||||
body: []byte{},
|
||||
decoder: scheme.Codecs.LegacyCodec(v1.SchemeGroupVersion),
|
||||
}
|
||||
if err := res.Into(&v1.Pod{}); err == nil || !strings.Contains(err.Error(), "0-length") {
|
||||
t.Errorf("should have complained about 0 length body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLTemplate(t *testing.T) {
|
||||
uri, _ := url.Parse("http://localhost")
|
||||
r := NewRequest(nil, "POST", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil)
|
||||
|
|
|
@ -381,6 +381,10 @@ var ephemeralWhiteList = createEphemeralWhiteList(
|
|||
// k8s.io/kubernetes/pkg/apis/policy/v1beta1
|
||||
gvr("policy", "v1beta1", "evictions"), // not stored in etcd, deals with evicting kapiv1.Pod
|
||||
// --
|
||||
|
||||
// k8s.io/kubernetes/pkg/apis/admission/v1alpha1
|
||||
gvr("admission.k8s.io", "v1alpha1", "admissionreviews"), // not stored in etcd, call out to webhooks.
|
||||
// --
|
||||
)
|
||||
|
||||
// Only add kinds to this list when there is no mapping from GVK to GVR (and thus there is no way to create the object)
|
||||
|
|
Loading…
Reference in New Issue