move webhook admission to generic apiserver

This commit is contained in:
David Eads
2017-10-24 08:48:05 -04:00
parent 86f90ecbb8
commit 8c1fe1f61a
47 changed files with 162 additions and 134 deletions

View File

@@ -86,11 +86,11 @@ func newGCPermissionsEnforcement() (*gcPermissionsEnforcement, error) {
whiteList: whiteList,
}
genericPluginInitializer, err := initializer.New(nil, nil, fakeAuthorizer{}, nil, nil)
genericPluginInitializer, err := initializer.New(nil, nil, fakeAuthorizer{}, nil)
if err != nil {
return nil, err
}
pluginInitializer := kubeadmission.NewPluginInitializer(nil, nil, nil, legacyscheme.Registry.RESTMapper(), nil)
pluginInitializer := kubeadmission.NewPluginInitializer(nil, nil, nil, legacyscheme.Registry.RESTMapper(), nil, nil, nil)
initializersChain := admission.PluginInitializers{}
initializersChain = append(initializersChain, genericPluginInitializer)
initializersChain = append(initializersChain, pluginInitializer)

View File

@@ -744,7 +744,7 @@ func newHandlerForTest(c clientset.Interface) (admission.Interface, informers.Sh
if err != nil {
return nil, f, err
}
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil)
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil, nil, nil)
pluginInitializer.Initialize(handler)
err = admission.Validate(handler)
return handler, f, err

View File

@@ -38,7 +38,7 @@ import (
func newHandlerForTest(c clientset.Interface) (admission.Interface, informers.SharedInformerFactory, error) {
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
handler := NewProvision()
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil)
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil, nil, nil)
pluginInitializer.Initialize(handler)
err := admission.Validate(handler)
return handler, f, err

View File

@@ -37,7 +37,7 @@ import (
func newHandlerForTest(c clientset.Interface) (admission.Interface, informers.SharedInformerFactory, error) {
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
handler := NewExists()
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil)
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil, nil, nil)
pluginInitializer.Initialize(handler)
err := admission.Validate(handler)
return handler, f, err

View File

@@ -241,7 +241,7 @@ func TestIgnoreUpdatingInitializedPod(t *testing.T) {
func newHandlerForTest(c clientset.Interface) (*podNodeSelector, informers.SharedInformerFactory, error) {
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
handler := NewPodNodeSelector(nil)
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil)
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil, nil, nil)
pluginInitializer.Initialize(handler)
err := admission.Validate(handler)
return handler, f, err

View File

@@ -342,7 +342,7 @@ func newHandlerForTest(c clientset.Interface) (*podTolerationsPlugin, informers.
return nil, nil, err
}
handler := NewPodTolerationsPlugin(pluginConfig)
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil)
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil, nil, nil)
pluginInitializer.Initialize(handler)
err = admission.Validate(handler)
return handler, f, err

View File

@@ -1,80 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"admission.go",
"admissionreview.go",
"authentication.go",
"config.go",
"doc.go",
"rules.go",
"serviceresolver.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/webhook",
visibility = ["//visibility:public"],
deps = [
"//pkg/apis/admission/install:go_default_library",
"//pkg/kubeapiserver/admission:go_default_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/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/configuration:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//vendor/k8s.io/client-go/kubernetes: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",
],
)
go_test(
name = "go_default_test",
srcs = [
"admission_test.go",
"authentication_test.go",
"certs_test.go",
"rules_test.go",
"serviceresolver_test.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/webhook",
library = ":go_default_library",
deps = [
"//pkg/api:go_default_library",
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/admission/install:go_default_library",
"//vendor/k8s.io/api/admission/v1alpha1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality: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/apimachinery/pkg/util/diff:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -1,300 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package webhook delegates admission checks to dynamically configured webhooks.
package webhook
import (
"context"
"fmt"
"io"
"path"
"sync"
"github.com/golang/glog"
admissionv1alpha1 "k8s.io/api/admission/v1alpha1"
"k8s.io/api/admissionregistration/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/configuration"
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
admissioninit "k8s.io/kubernetes/pkg/kubeapiserver/admission"
// install the clientgo admission API for use with api registry
_ "k8s.io/kubernetes/pkg/apis/admission/install"
)
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(configFile)
if err != nil {
return nil, err
}
return plugin, nil
})
}
// WebhookSource can list dynamic webhook plugins.
type WebhookSource interface {
Run(stopCh <-chan struct{})
ExternalAdmissionHooks() (*v1alpha1.ExternalAdmissionHookConfiguration, error)
}
// NewGenericAdmissionWebhook returns a generic admission webhook plugin.
func NewGenericAdmissionWebhook(configFile io.Reader) (*GenericAdmissionWebhook, error) {
kubeconfigFile := ""
if configFile != nil {
// TODO: move this to a versioned configuration file format
var config AdmissionConfig
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
err := d.Decode(&config)
if err != nil {
return nil, err
}
kubeconfigFile = config.KubeConfigFile
}
authInfoResolver, err := newDefaultAuthenticationInfoResolver(kubeconfigFile)
if err != nil {
return nil, err
}
return &GenericAdmissionWebhook{
Handler: admission.NewHandler(
admission.Connect,
admission.Create,
admission.Delete,
admission.Update,
),
authInfoResolver: authInfoResolver,
serviceResolver: defaultServiceResolver{},
}, nil
}
// GenericAdmissionWebhook is an implementation of admission.Interface.
type GenericAdmissionWebhook struct {
*admission.Handler
hookSource WebhookSource
serviceResolver admissioninit.ServiceResolver
negotiatedSerializer runtime.NegotiatedSerializer
authInfoResolver AuthenticationInfoResolver
}
var (
_ = admissioninit.WantsServiceResolver(&GenericAdmissionWebhook{})
_ = genericadmissioninit.WantsWebhookRESTClientConfig(&GenericAdmissionWebhook{})
_ = genericadmissioninit.WantsExternalKubeClientSet(&GenericAdmissionWebhook{})
)
// TODO find a better way wire this, but keep this pull small for now.
func (a *GenericAdmissionWebhook) SetWebhookRESTClientConfig(in *rest.Config) {
if in != nil {
a.authInfoResolver = &dialOverridingAuthenticationInfoResolver{
dialFn: in.Dial,
delegate: a.authInfoResolver,
}
}
}
// SetServiceResolver sets a service resolver for the webhook admission plugin.
// Passing a nil resolver does not have an effect, instead a default one will be used.
func (a *GenericAdmissionWebhook) SetServiceResolver(sr admissioninit.ServiceResolver) {
if sr != nil {
a.serviceResolver = sr
}
}
// SetScheme sets a serializer(NegotiatedSerializer) which is derived from the scheme
func (a *GenericAdmissionWebhook) SetScheme(scheme *runtime.Scheme) {
if scheme != nil {
a.negotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
Serializer: serializer.NewCodecFactory(scheme).LegacyCodec(admissionv1alpha1.SchemeGroupVersion),
})
}
}
func (a *GenericAdmissionWebhook) SetExternalKubeClientSet(client clientset.Interface) {
a.hookSource = configuration.NewExternalAdmissionHookConfigurationManager(client.Admissionregistration().ExternalAdmissionHookConfigurations())
}
func (a *GenericAdmissionWebhook) Validate() error {
if a.hookSource == nil {
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a Kubernetes client to be provided")
}
if a.negotiatedSerializer == nil {
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a runtime.Scheme to be provided to derive a serializer")
}
go a.hookSource.Run(wait.NeverStop)
return nil
}
func (a *GenericAdmissionWebhook) loadConfiguration(attr admission.Attributes) (*v1alpha1.ExternalAdmissionHookConfiguration, error) {
hookConfig, err := a.hookSource.ExternalAdmissionHooks()
// if ExternalAdmissionHook configuration is disabled, fail open
if err == configuration.ErrDisabled {
return &v1alpha1.ExternalAdmissionHookConfiguration{}, nil
}
if err != nil {
e := apierrors.NewServerTimeout(attr.GetResource().GroupResource(), string(attr.GetOperation()), 1)
e.ErrStatus.Message = fmt.Sprintf("Unable to refresh the ExternalAdmissionHook configuration: %v", err)
e.ErrStatus.Reason = "LoadingConfiguration"
e.ErrStatus.Details.Causes = append(e.ErrStatus.Details.Causes, metav1.StatusCause{
Type: "ExternalAdmissionHookConfigurationFailure",
Message: "An error has occurred while refreshing the externalAdmissionHook configuration, no resources can be created/updated/deleted/connected until a refresh succeeds.",
})
return nil, e
}
return hookConfig, nil
}
// Admit makes an admission decision based on the request attributes.
func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
hookConfig, err := a.loadConfiguration(attr)
if err != nil {
return err
}
hooks := hookConfig.ExternalAdmissionHooks
ctx := context.TODO()
errCh := make(chan error, len(hooks))
wg := sync.WaitGroup{}
wg.Add(len(hooks))
for i := range hooks {
go func(hook *v1alpha1.ExternalAdmissionHook) {
defer wg.Done()
err := a.callHook(ctx, hook, attr)
if err == nil {
return
}
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1alpha1.Ignore
if callErr, ok := err.(*ErrCallingWebhook); ok {
if ignoreClientCallFailures {
glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
// Since we are failing open to begin with, we do not send an error down the channel
return
}
glog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
errCh <- err
return
}
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 *v1alpha1.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 := createAdmissionReview(attr)
client, err := a.hookClient(h)
if err != nil {
return &ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
response := &admissionv1alpha1.AdmissionReview{}
if err := client.Post().Context(ctx).Body(&request).Do().Into(response); err != nil {
return &ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
if response.Status.Allowed {
return nil
}
if response.Status.Result == nil {
return fmt.Errorf("admission webhook %q denied the request without explanation", h.Name)
}
return &apierrors.StatusError{
ErrStatus: *response.Status.Result,
}
}
func (a *GenericAdmissionWebhook) hookClient(h *v1alpha1.ExternalAdmissionHook) (*rest.RESTClient, error) {
serverName := h.ClientConfig.Service.Name + "." + h.ClientConfig.Service.Namespace + ".svc"
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
restConfig, err := a.authInfoResolver.ClientConfigFor(serverName)
if err != nil {
return nil, err
}
cfg := rest.CopyConfig(restConfig)
cfg.Host = u.Host
cfg.APIPath = path.Join(u.Path, h.ClientConfig.URLPath)
cfg.TLSClientConfig.ServerName = serverName
cfg.TLSClientConfig.CAData = h.ClientConfig.CABundle
cfg.ContentConfig.NegotiatedSerializer = a.negotiatedSerializer
return rest.UnversionedRESTClientFor(cfg)
}

View File

@@ -1,333 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"k8s.io/api/admission/v1alpha1"
registrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
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/api/legacyscheme"
"k8s.io/client-go/rest"
_ "k8s.io/kubernetes/pkg/apis/admission/install"
)
type fakeHookSource struct {
hooks []registrationv1alpha1.ExternalAdmissionHook
err error
}
func (f *fakeHookSource) ExternalAdmissionHooks() (*registrationv1alpha1.ExternalAdmissionHookConfiguration, error) {
if f.err != nil {
return nil, f.err
}
return &registrationv1alpha1.ExternalAdmissionHookConfiguration{ExternalAdmissionHooks: f.hooks}, nil
}
func (f *fakeHookSource) Run(stopCh <-chan struct{}) {}
type fakeServiceResolver struct {
base url.URL
}
func (f fakeServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
if namespace == "failResolve" {
return nil, fmt.Errorf("couldn't resolve service location")
}
u := f.base
return &u, nil
}
// 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(nil)
if err != nil {
t.Fatal(err)
}
wh.authInfoResolver = &fakeAuthenticationInfoResolver{
restConfig: &rest.Config{
TLSClientConfig: rest.TLSClientConfig{
CAData: caCert,
CertData: clientCert,
KeyData: 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
path string
expectAllow bool
errorContains string
}
ccfg := func(urlPath string) registrationv1alpha1.AdmissionHookClientConfig {
return registrationv1alpha1.AdmissionHookClientConfig{
Service: registrationv1alpha1.ServiceReference{
Name: "webhook-test",
Namespace: "default",
},
URLPath: urlPath,
CABundle: caCert,
}
}
matchEverythingRules := []registrationv1alpha1.RuleWithOperations{{
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.OperationAll},
Rule: registrationv1alpha1.Rule{
APIGroups: []string{"*"},
APIVersions: []string{"*"},
Resources: []string{"*/*"},
},
}}
policyFail := registrationv1alpha1.Fail
policyIgnore := registrationv1alpha1.Ignore
table := map[string]test{
"no match": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
Name: "nomatch",
ClientConfig: ccfg("disallow"),
Rules: []registrationv1alpha1.RuleWithOperations{{
Operations: []registrationv1alpha1.OperationType{registrationv1alpha1.Create},
}},
}},
},
expectAllow: true,
},
"match & allow": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
Name: "allow",
ClientConfig: ccfg("allow"),
Rules: matchEverythingRules,
}},
},
expectAllow: true,
},
"match & disallow": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
Name: "disallow",
ClientConfig: ccfg("disallow"),
Rules: matchEverythingRules,
}},
},
errorContains: "without explanation",
},
"match & disallow ii": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
Name: "disallowReason",
ClientConfig: ccfg("disallowReason"),
Rules: matchEverythingRules,
}},
},
errorContains: "you shall not pass",
},
"match & fail (but allow because fail open)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
Name: "internalErr A",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}, {
Name: "internalErr B",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}, {
Name: "internalErr C",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyIgnore,
}},
},
expectAllow: true,
},
"match & fail (but disallow because fail closed on nil)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
Name: "internalErr A",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
}, {
Name: "internalErr B",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
}, {
Name: "internalErr C",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
}},
},
expectAllow: false,
},
"match & fail (but fail because fail closed)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
Name: "internalErr A",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}, {
Name: "internalErr B",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}, {
Name: "internalErr C",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
FailurePolicy: &policyFail,
}},
},
expectAllow: false,
},
}
for name, tt := range table {
t.Run(name, func(t *testing.T) {
wh.hookSource = &tt.hookSource
wh.serviceResolver = fakeServiceResolver{base: *serverURL}
wh.SetScheme(legacyscheme.Scheme)
err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, name, resource, subResource, operation, &userInfo))
if tt.expectAllow != (err == nil) {
t.Errorf("expected allowed=%v, but got err=%v", tt.expectAllow, err)
}
// ErrWebhookRejected is not an error for our purposes
if tt.errorContains != "" {
if err == nil || !strings.Contains(err.Error(), tt.errorContains) {
t.Errorf(" expected an error saying %q, but got %v", tt.errorContains, err)
}
}
})
}
}
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)
}
}
type fakeAuthenticationInfoResolver struct {
restConfig *rest.Config
}
func (c *fakeAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
return c.restConfig, nil
}

View File

@@ -1,70 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package webhook delegates admission checks to dynamically configured webhooks.
package webhook
import (
admissionv1alpha1 "k8s.io/api/admission/v1alpha1"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
)
// 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()
userInfo := authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
return admissionv1alpha1.AdmissionReview{
Spec: admissionv1alpha1.AdmissionReviewSpec{
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: attr.GetSubresource(),
Operation: admissionv1alpha1.Operation(attr.GetOperation()),
Object: runtime.RawExtension{
Object: attr.GetObject(),
},
OldObject: runtime.RawExtension{
Object: attr.GetOldObject(),
},
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
UserInfo: userInfo,
},
}
}

View File

@@ -1,157 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"io/ioutil"
"net"
"strings"
"time"
"fmt"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
type AuthenticationInfoResolver interface {
ClientConfigFor(server string) (*rest.Config, error)
}
type defaultAuthenticationInfoResolver struct {
kubeconfig clientcmdapi.Config
}
func newDefaultAuthenticationInfoResolver(kubeconfigFile string) (AuthenticationInfoResolver, error) {
if len(kubeconfigFile) == 0 {
return &defaultAuthenticationInfoResolver{}, nil
}
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeconfigFile
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
clientConfig, err := loader.RawConfig()
if err != nil {
return nil, err
}
return &defaultAuthenticationInfoResolver{kubeconfig: clientConfig}, nil
}
func (c *defaultAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
// exact match
if authConfig, ok := c.kubeconfig.AuthInfos[server]; ok {
return restConfigFromKubeconfig(authConfig)
}
// star prefixed match
serverSteps := strings.Split(server, ".")
for i := 1; i < len(serverSteps); i++ {
nickName := "*." + strings.Join(serverSteps[i:], ".")
if authConfig, ok := c.kubeconfig.AuthInfos[nickName]; ok {
return restConfigFromKubeconfig(authConfig)
}
}
// if we're trying to hit the kube-apiserver and there wasn't an explicit config, use the in-cluster config
if server == "kubernetes.default.svc" {
// if we can find an in-cluster-config use that. If we can't, fall through.
inClusterConfig, err := rest.InClusterConfig()
if err == nil {
return setGlobalDefaults(inClusterConfig), nil
}
}
// star (default) match
if authConfig, ok := c.kubeconfig.AuthInfos["*"]; ok {
return restConfigFromKubeconfig(authConfig)
}
// use the current context from the kubeconfig if possible
if len(c.kubeconfig.CurrentContext) > 0 {
if currContext, ok := c.kubeconfig.Contexts[c.kubeconfig.CurrentContext]; ok {
if len(currContext.AuthInfo) > 0 {
if currAuth, ok := c.kubeconfig.AuthInfos[currContext.AuthInfo]; ok {
return restConfigFromKubeconfig(currAuth)
}
}
}
}
// anonymous
return setGlobalDefaults(&rest.Config{}), nil
}
func restConfigFromKubeconfig(configAuthInfo *clientcmdapi.AuthInfo) (*rest.Config, error) {
config := &rest.Config{}
// blindly overwrite existing values based on precedence
if len(configAuthInfo.Token) > 0 {
config.BearerToken = configAuthInfo.Token
} else if len(configAuthInfo.TokenFile) > 0 {
tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile)
if err != nil {
return nil, err
}
config.BearerToken = string(tokenBytes)
}
if len(configAuthInfo.Impersonate) > 0 {
config.Impersonate = rest.ImpersonationConfig{
UserName: configAuthInfo.Impersonate,
Groups: configAuthInfo.ImpersonateGroups,
Extra: configAuthInfo.ImpersonateUserExtra,
}
}
if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 {
config.CertFile = configAuthInfo.ClientCertificate
config.CertData = configAuthInfo.ClientCertificateData
config.KeyFile = configAuthInfo.ClientKey
config.KeyData = configAuthInfo.ClientKeyData
}
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
config.Username = configAuthInfo.Username
config.Password = configAuthInfo.Password
}
if configAuthInfo.AuthProvider != nil {
return nil, fmt.Errorf("auth provider not supported")
}
return setGlobalDefaults(config), nil
}
func setGlobalDefaults(config *rest.Config) *rest.Config {
config.UserAgent = "kube-apiserver-admission"
config.Timeout = 30 * time.Second
return config
}
type dialOverridingAuthenticationInfoResolver struct {
dialFn func(network, addr string) (net.Conn, error)
delegate AuthenticationInfoResolver
}
func (c *dialOverridingAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
cfg, err := c.delegate.ClientConfigFor(server)
if err != nil {
return nil, err
}
cfg.Dial = c.dialFn
return cfg, nil
}

View File

@@ -1,130 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"testing"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
func TestAuthenticationDetection(t *testing.T) {
tests := []struct {
name string
kubeconfig clientcmdapi.Config
serverName string
expected rest.Config
}{
{
name: "empty",
serverName: "foo.com",
},
{
name: "fallback to current context",
serverName: "foo.com",
kubeconfig: clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"bar.com": {Token: "bar"},
},
Contexts: map[string]*clientcmdapi.Context{
"ctx": {
AuthInfo: "bar.com",
},
},
CurrentContext: "ctx",
},
expected: rest.Config{BearerToken: "bar"},
},
{
name: "exact match",
serverName: "foo.com",
kubeconfig: clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"foo.com": {Token: "foo"},
"*.com": {Token: "foo-star"},
"bar.com": {Token: "bar"},
},
},
expected: rest.Config{BearerToken: "foo"},
},
{
name: "partial star match",
serverName: "foo.com",
kubeconfig: clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"*.com": {Token: "foo-star"},
"bar.com": {Token: "bar"},
},
},
expected: rest.Config{BearerToken: "foo-star"},
},
{
name: "full star match",
serverName: "foo.com",
kubeconfig: clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"*": {Token: "star"},
"bar.com": {Token: "bar"},
},
},
expected: rest.Config{BearerToken: "star"},
},
{
name: "skip bad in cluster config",
serverName: "kubernetes.default.svc",
kubeconfig: clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"*": {Token: "star"},
"bar.com": {Token: "bar"},
},
},
expected: rest.Config{BearerToken: "star"},
},
{
name: "most selective",
serverName: "one.two.three.com",
kubeconfig: clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"*.two.three.com": {Token: "first"},
"*.three.com": {Token: "second"},
"*.com": {Token: "third"},
},
},
expected: rest.Config{BearerToken: "first"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
resolver := defaultAuthenticationInfoResolver{kubeconfig: tc.kubeconfig}
actual, err := resolver.ClientConfigFor(tc.serverName)
if err != nil {
t.Fatal(err)
}
actual.UserAgent = ""
actual.Timeout = 0
if !equality.Semantic.DeepEqual(*actual, tc.expected) {
t.Errorf("%v", diff.ObjectReflectDiff(tc.expected, *actual))
}
})
}
}

View File

@@ -1,216 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// This file was generated using openssl by the gencerts.sh script
// and holds raw certificates for the webhook tests.
package webhook
var caKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAt8E1XykA4860Tj7mypnsSU+hW0taUEvz26a5rgFSrwgKe1g+
zOXc0XoAdnWivWKwWXTW+P1mmjMApEf8ndfPy+juKIrPKP6ccF31iPvOGfNRm/g/
ulZAJnAjBn0zkZ9ARhpdDxKwDpwIKSrTna5GB/gX0VbHQ/M23u0RjVUZuNM9cayW
HReRx7xlOQD+uREQ/wh1zkgQime+rji5U3jxB9YD3zfTeGBJdrq9ptTdkPEIQUKf
DM8SnM8fleEPkBq3XrhfmAEfkHBGpn4Hc82tk/oEZ+LMyMaR/GmzdJXU2/d4zixC
Dgqdg1nB76uXJ4aITX2BuR+ttmAI0b6Si4UvHwIDAQABAoIBAA/Msg0blnL/++ra
Z7e14mYvTZ1u7jYHQdF6FW8LuBNKqrQOU2AEx6bPSajl1ndYO/eFH1LLXv4VMpHt
ip/7xWcwAQJFZSiOM99JhOohVIhQroytnLUl42AqtihBraRwv/MHI0c/gRnQercn
coiVSno2771VK88A44/pbF/tmEeW5Nq7bwHrtjdt3MDKjv2LQaPToDzBivSwz/F8
3dMBpCUKT3tKC6QiDAFi4WaVOqXZrDfm/HJ1L8LWYjrcGTwwzMGpDfQEMxhq+2AR
Ya89jKF1I2+3kgXrZER7eHktUEQ0bGckSmAN9yo01rdm7E5gmmPuTK13riSFWrJn
/Dg21PECgYEA3vM0OLGevaUP4geBv1nPLpANC551Wf8wu2QG+Ts1/LFQHtFTOTst
JjFy/XT90ki1wni8P/pcIbMEXDJJezR4giWfiwMzr/E4arkJc5rJwV3kcmGrVihS
9BIJVlWq8kPmklTctfoqjDMa7tkYZoYStg+1Xljvw/HJFqZ6VoWyxFcCgYEA0v6S
Fx960kQyqPYyQMaZpce9rAsgGBJ4uMU6dXVfYxDy0CEKZ1lV1xwUg9eWNFj7E46A
RJDl9fR2KztTbgBobEOCVlO9QftY8RiIzibq8R4P5XyEV+TCkPk+eYffDZfOueGK
uCzBcAcl12SkAy/KMeS0+/+KYfetGyh23GH/bnkCgYEAnqMefVilQvu4GXSN9cHJ
kbAeGC5gAfF6k1vROnXPLEZeZA890HMy5QI6d+5OzNm/uuh9ymgyNihS6ec+MdRc
Cv8KTrewh3h0VDvlZcS12kkcy+aDK4L1w4Ux76R1RnzaCzUm9rVSoP+cImeG3Sx5
E+KJguB1ek8Ibn12fyoS0XECgYEAvKyAHsU7o0Lwuj5NebceNiyC45GfRWdfJHrZ
Z6dpgMDrIEorb3dnV0/42Fy0KGNZQYewE6Auwt2zvbzzQe6Dcix8JI4FMzd7tTxn
OVF7zdlABcpu3dnmUpVO1IY3Y4RYi8evsDn1UCRUJmQMdf0KJcuKO72rFSfRV/O7
Nh87tqECgYAN21eEb68fg7z7WqnUhL24SilIFNx7qmyhFZ5ZbIuNBrCOW4lxj7z4
1A4WyB3Wj2CeTeFrFamq+bgI5gl4DNzcml8bFxij3WDbwzq6C2FTJ1U+Ax/Z9Y60
qdc1fMRy+0Wkglk26VZ+xA4gW0eetIDXf/IocFYZ56ti36lWfXMKFA==
-----END RSA PRIVATE KEY-----`)
var caCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDPTCCAiWgAwIBAgIJALl4JUWeGrsQMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTAxNzAxMDcyM1oYDzIyOTEwODAyMDEwNzIzWjA0MTIwMAYDVQQDDClnZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBALfBNV8pAOPOtE4+5sqZ7ElPoVtLWlBL89um
ua4BUq8ICntYPszl3NF6AHZ1or1isFl01vj9ZpozAKRH/J3Xz8vo7iiKzyj+nHBd
9Yj7zhnzUZv4P7pWQCZwIwZ9M5GfQEYaXQ8SsA6cCCkq052uRgf4F9FWx0PzNt7t
EY1VGbjTPXGslh0Xkce8ZTkA/rkREP8Idc5IEIpnvq44uVN48QfWA98303hgSXa6
vabU3ZDxCEFCnwzPEpzPH5XhD5Aat164X5gBH5BwRqZ+B3PNrZP6BGfizMjGkfxp
s3SV1Nv3eM4sQg4KnYNZwe+rlyeGiE19gbkfrbZgCNG+kouFLx8CAwEAAaNQME4w
HQYDVR0OBBYEFJ+UXeXeN9DfxuCA65LuhRaXI5bnMB8GA1UdIwQYMBaAFJ+UXeXe
N9DfxuCA65LuhRaXI5bnMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
AJlfuK7LcFxXCjB6rnRYvjIFF3JewaPGx273YDpV2DkvCJXvez+mDC6ZfD1U7nEw
P+0A+NJctU2Vv+bbhh1vXlmBUoA9zKyIPld0pXDt8PxK8L9QRdLdzN96MtUjE0Cr
jBDxy3eIke+ElQQyU3MSx1uohZao40WOTDvR1fBswrFGhFNtELgwT3zIJ7tMO4ws
1f6LRVB4xsD6cPNUmFyJW6UqecJ3ZSeErF0r8uN4Ta0zUoJ03CflwsgujoVNpIPR
/VVElRE6Cd2C1i3qLBMOZ+aQrxGw4teNXilXbKtwJzpDYU+bypPgim+vxAwD9XnQ
J/rCqIaSYOI/3WkZmQ+E6aE=
-----END CERTIFICATE-----`)
var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEAsLjPsYMRv5T3+OoPbE+mKqUkBrn6ZQZU28UulLaS8UsaX/Dx
o/N2P6YGSlBeEnO0IV7fHqfdlgTRqqhlhX9L+suj963skZUmwoHB54lsHBYnXEBr
UlkVn5Wv+BEp5FX9iC9EV+wyWxuddGqElNUSMvuC71lRAE1edKbFZYIgqpGson6u
kcn5cpBoyFjFyxKhuKh5WTCQGYFZBtIhcggyduUw5+JzbPUlrlOcCiW8aQwL0f+s
9vA+4lDcngyOXDgAOip/Cr3lYiajhv41TyMNR9aSCH3gwI06QabQpXeHmYKjdMDO
epo9czAOJ2ZI+o2sKRZGUvyfACg6VJpU2jiWdwIDAQABAoIBAQCClDxbFNcDcaZ8
1S4KQRwt/JIPKlJ7XV9MeHl/xxvykSTu6VETbOzeAOY6+QFZrwbVdY110GGp3Ouz
pvRE3ReeO+RvOaNIuyXFqS1G0UMBydjRkIP8d/jDT06UBNKodmV8wDhGoy9eJJyG
jcJjWsE0zKUmCCATEhgOJ8BJzgonHXwSkgjhBophYpDNGi4W1Ke8a80V0S26UOsV
b72mBqPYDAcGNR9dFpJGYRRdZ77SjcAAOFOqnU6KXUFHvitNYSkUG22wHa4+yuO6
fTDUitdXhxCcFCWEFyX6Chm3lmf1NgSI4bni46K6/BC9mlNjZ+7QrTJTTp1/WyHj
fV2TnwRhAoGBAN2kTatss76FZwKFamPSvX45ulo0/U9LB1Eb9A7/M+enfAJGPPuk
LRdy3JjDiEKxgsBlA7zy0Gc9wUjBGmPBOiJmCTfmMykoqV1qVH5YQcuVUaKE589a
iUAUn6PNcNsnu2Z8BIDU3Fvyq2KJammSz/zqNtkH7I6SDur6Sb9SRJORAoGBAMwd
5OkOOGvZKxnJz8s5hwDicN2Fk5zxWpA74mgUt44sOiG3F8/7y/ntT/O3DGKz/yDb
2Ju6Yf3ojqYnirAPlWLxIm0Y3K9gNQffJ5cpe/kCzbhCLAnLk6YXu8ZVoMMq/0LF
f0bh+UAktqlFi5Tl+0LGAGd9wWxAe7/DHmolBfWHAoGBAJxLy/Wx3wLgQebWPFMO
flAv10jbizHKX+uDgdS9hFW8lsdnzoNJn/6kIgmcAU++q8yOr1ckB3B2bQGoIrrr
vNobCC8iJzvED8LvQ4whIqy0rG+lt25SkuzcXkL9kbMJzq4TkH1lHcu9UbxX2PF/
9SmN5IWhf+B+AQUU4MKI+hDxAoGBAIRXrZ/d9H8Yo3VpAC2H8xyDtSIsBXVwl4OF
EFrjc8/epSJPEEVtwOcfEwO133Xvtq+bW2o9AmQacMMSSD23HOi159hMkmmzOy8L
ZSQBZbwiMTgSz3LaZ7T9FmaWBlIEgtTMMKXIxk7sfvJpgQLdyneU4ZY4VzzU4meH
HyU7NA3pAoGBAM53r4pKMsdH9c4w/aRiRZeRge+MdPqdBo4Kje50uXVUaplJr1fJ
9Bm2P1oSfr+Zh6pNyDQE2OhptOxskd/+XDC4i6+MZ1iR1JHFq4oTF5OUYc3Je23G
FjD/vUn+ha2d750IPhsDztb8XyGfQdn7oo0ikhg1Ayjix6LE0GAd+Ve2
-----END RSA PRIVATE KEY-----`)
var badCACert = []byte(`-----BEGIN CERTIFICATE-----
MIIDPTCCAiWgAwIBAgIJANumDUaVJHIhMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTAxNzAxMDcyNFoYDzIyOTEwODAyMDEwNzI0WjA0MTIwMAYDVQQDDClnZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBALC4z7GDEb+U9/jqD2xPpiqlJAa5+mUGVNvF
LpS2kvFLGl/w8aPzdj+mBkpQXhJztCFe3x6n3ZYE0aqoZYV/S/rLo/et7JGVJsKB
weeJbBwWJ1xAa1JZFZ+Vr/gRKeRV/YgvRFfsMlsbnXRqhJTVEjL7gu9ZUQBNXnSm
xWWCIKqRrKJ+rpHJ+XKQaMhYxcsSobioeVkwkBmBWQbSIXIIMnblMOfic2z1Ja5T
nAolvGkMC9H/rPbwPuJQ3J4Mjlw4ADoqfwq95WImo4b+NU8jDUfWkgh94MCNOkGm
0KV3h5mCo3TAznqaPXMwDidmSPqNrCkWRlL8nwAoOlSaVNo4lncCAwEAAaNQME4w
HQYDVR0OBBYEFAYhiaN5L1bHf3VQO9bAwLTCFOyZMB8GA1UdIwQYMBaAFAYhiaN5
L1bHf3VQO9bAwLTCFOyZMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
AAb6qDKiWDuYbFI3f/AGkHaiWnVrdU/2oQ1P03N/CD0DEGzRTmEQrl5l0pHDUJ5g
XTszW/5Bgjgzx8HLG3VeMQlZpCeGrUBsWWIlGFsdfAKQ8xkB1JYNCtb920WBCOpZ
FewLzQwbRyeYju+VP7lq+IF3htOTbeXRax61c0qu2o402NbKCNMlwAWMWl92dji5
zJG0U0g+U+GC1QVyRlpf6hsXONgNOWTuZJgZDmN0exZOH7rF9syr3lfDwX0I/no7
zIJHt7Tx9oanqZbj/Oe8FC4jYNJiZryQ4MaWBk7Op+aR5bUgk5SwA9pLuKLm3Zyl
KkC1zS2x3RD1WxxiJjXcJ3M=
-----END CERTIFICATE-----`)
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA4Slves29h8gstITXD8cuHT0sZzP9x7Ip5pcSrQgjIpk9V7zB
ulRsFfTaPVMAzq5nlX5qbM6OezidzdC0oVQi7DCRd+TqQi2zNDJNeWAjDMkRa6ir
vq9XNuQJlMxi/YdFzjWl8LPMyq+WoW+0gtM86jPOEk69iw0nFqmUTt/4WXLOIDH9
ffIiZ6h+x6IJGFqcn4heNE9SK++bMmjijmhIF3i0dKquApVm0+E/NYDDHLzeQ42r
BhgPWmuLqGBvVavTHweXRJ9qdZ5LVzMJj1KNNLA4T/vsk17jADJGPikrvZWZ0MJE
SPKpDAJ9uxDpIGNg8EBdUoStfrQMMsDEnNE1/QIDAQABAoIBAQC/ROumbk+qoKkZ
UB9BD/pkbCrkII5crURa1crPojH2miY5+ea32i9XF4Csx23QJOdpXtIZS/5NPnMO
+1P2F/rymO954cP+I8QveuvFR51+pu9dfRMoENsNjfl1pYoRxG/QFFK6foJhS3ex
+6pj1/3PFeLgOnNZQ/sIjVWnCyt1DzFUaELE1jsDWtsQ9j2ed1ceqjrEVG0BbB8O
LeXUp0lDWgRmF9LexFVd361ew6DM4Zr3l9W3Dubd/6gIPNSRCB7T1usErFKPi8Dq
ML2lnO98+gKZHp6DKUISWEwt0By3QRLV09fBTes8fr+gylZfhwX2Q2EjiZ/v0Nlb
YGLAZwMhAoGBAPigApUlA8L0AGgVoSuBRthdFVzqLKTIKYimiZirRnXljTs3HvS4
Z/RCkXRVP6aQ8tORVUyct0drBUgddu+YPS/hfk8ipYraGonAKWfxL2+sFcxHg9iy
O6OxR7jOlI7bG8Ue78lJjqfzpXyi2/ikL2udHLzMeCh3czzix4wKXl2LAoGBAOfX
QKCudMGOHb2LCkC1jP2VLUAg09Y7q7sX5avNXhZsLNhVgu1i8FNTTXXCHwyBLvxY
AX7AnYaUP5K1YOyHVXnfqp2uuctR8tm7TUh9mw3RGnE+D9yw39RjlF4ZjZMtBC0+
5/A2upRqY5H2pepxi/bPCAC58UgHbvjTS3LD6zuXAoGAQUj7BKDgmPurc6liVeMv
cDcZGfnf2TE6PsjETtOCwAiUCl2SAl695VTpjuunuBxNtyJtjJ2GPvmqPGKITafj
QURr/2mwoIJe/5b3CHU7qI4+dxK8W1WJ9ZTiqXONbOm6JAvYmTl4fT+sT8sQCf1K
+m4aErV6Q94B45YFIg/C8bsCgYEAx2pkAZHthasrM6UL3ZsLufb9pCJYc/aBgX1N
pRgRrPHBJRdwdaXbl6CYiQi/Ui8v7gf4yUD+fgq4IAX5Z5oE0L6tb9Ihp5xGajfs
gsTfgOPyfaNnW2mcLYC11rbeCtD2vcBVGk7I7+4O1Tc1gVHHlTSA6rcFrfIO5uJA
DGguxuMCgYB9y4JY44YLVpxbHOmQcp5XzN2uDESkBDmzXhn6Kx4wyyMI1/J2a44p
68AL0TaVWK1vvzV8X4f+92ufvUCXuuItIVDvkSdKMl6kL54djCA25tGuxNHxMk7e
/l4fshoaRwF3ybwHbREMOy8pQHrsek7m21sC/q/DIDN/IPdqo7aaVw==
-----END RSA PRIVATE KEY-----`)
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIJAM2Hyhl1N+5sMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTAxNzAxMDcyNFoYDzIyOTEwODAyMDEwNzI0WjAjMSEwHwYDVQQDDBh3ZWJo
b29rLXRlc3QuZGVmYXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDhKW96zb2HyCy0hNcPxy4dPSxnM/3HsinmlxKtCCMimT1XvMG6VGwV9No9
UwDOrmeVfmpszo57OJ3N0LShVCLsMJF35OpCLbM0Mk15YCMMyRFrqKu+r1c25AmU
zGL9h0XONaXws8zKr5ahb7SC0zzqM84STr2LDScWqZRO3/hZcs4gMf198iJnqH7H
ogkYWpyfiF40T1Ir75syaOKOaEgXeLR0qq4ClWbT4T81gMMcvN5DjasGGA9aa4uo
YG9Vq9MfB5dEn2p1nktXMwmPUo00sDhP++yTXuMAMkY+KSu9lZnQwkRI8qkMAn27
EOkgY2DwQF1ShK1+tAwywMSc0TX9AgMBAAGjZDBiMAkGA1UdEwQCMAAwCwYDVR0P
BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATApBgNVHREEIjAg
hwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVsdC5zdmMwDQYJKoZIhvcNAQELBQAD
ggEBAFwJ5UtHJsSx7mrJ7X51XDVg8ocNdWYLuehWfpM2Tlrv9kklONF7VehS+9kg
MjSiuXtIhtEEN7GIy08sl6rhANtwXxWhj5b+qPNSNiHGNRvmHkCJuO2PGG7TpSpH
CfOgX+HH9CnX/piC7Uqr+vmS+SmhSjIyw1bUtP9cDmFvNQB9/0qvcXZP/oX90jsa
qF6fQvKP/OtRcW/kyWmhzqeIMufru82Hbrf/WJuQXCvpgtY43cOlHVEr8X2PbF9F
t4eliujfewSu1cyXNcT5KcriCvZyXU/d8UHm+z9rnMJdC4bfvwOLeG7VmEG1Vp/X
sRiMsjRcun8Jvbl7BbH86nu1xus=
-----END CERTIFICATE-----`)
var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA05j8mb6tyOYOxfdmra9nlBu+VefZ46d+Q018EEi8LLpXFovP
+4D6mdZiG2K1Or9Kx7wpiG4gyrqn8aENTEbREtw7+GWpsrtWcOnmtCoMrcqV97S+
TurL5DY1Rh9XOBxa/QninOnP4O1dASYmi0uZBZW/Neidwt70/sb3VGxj8Ex8rzD/
LWsiVj+ijkK2A18+S2m01HNuNramJiJ8Ns5VxHj5TspRIBik1NEfEDFEi42ddZ0n
9q4u3eZmFC1QqMLB/371+oFrieC+gmbg2FCMUw4LIiUMWURhUP8kXNEFCDWvTLbM
40wU6lejJw+JF0SBs3AIcI4GJeSfxEuT2AFCBwIDAQABAoIBAC3Rb8ke1+SrpEFL
vAkZ9TTF+SYC6VR5XUbXjWi9RznsM5VnOub728fZ+y5w5ktNRrUPUnL/XcxoNJuG
wylkIDuUQswbv247UJFspI8Yl9w+BNE5awgNoY7OCiUf/jPhN/aY4GAX5PKQk1X/
W9NH0F+8OEZFE3wx6R0OGlpGijFrCbEoqfMcReOVkXt9jjHAxHxI6erMlQvQ6Jop
KZ+OndRt8ilNtcjZLxAK8d3odgXN7OGezi74/VnG8b6NVmXsZMRkgS5xcP/42h2e
GDGGm4Gia8x0lcosgr+LdZ4FMEITj2p84PGNoeoh7PtTMUQs2qnfPPbpQpab6w9/
7j7jRXECgYEA7/R8ByzNFUfcBOW0F8mmQ4bAmJcjuVEBay/XFf8gxWrefxsIEBu/
n3GPI10bxGdEpSQGln1P9VCyZPauHPQC6DNB2OQjbmAujuigj2uM3GbXmYt20z2s
iUHhZQstznEO+BLqOMhk2SICnwsecPn0jP2MvWS7tY+szwvdLROHZj8CgYEA4b8V
iGU8/3mXGYRsT/tuuWjnalstCYzKosK92K8PS6LnAjv8t/8CMrL5gHAGXdf6fEAh
qDndlB1VkQ/ymiqR35el/ErVRt1/2pbwTLSQzAGJawY/osnClMnShO8gdQnsJnmi
zx909lWVxrKkN38szLQfK2bq4C8z6Bw5+6IxAjkCgYBnEUas9kto1qLk352Jki3+
V0UmxdSsZuULG1NxuVJkOdE0G3JNKP4YCHkJIZcpt4m+vUivH0hXAMB/qY2EFjOh
dVLVTLkDUgDtlXJR6Epq6Sm2ZDc36QfRNSERe8nDIMDjQYylsz3OHlOt6OK8eEDY
xpfLShdulzYNAPWRxQ+llQKBgQCmWEvhqdf82PgCkZXOihPZA/giYvUY6GoY7S8/
kB/ROETJXLKoUnyoJ0G65tGKLTAiho9Giv0/uy3mKr4149CB1hk1g18NTQJ9bGO9
4gAgk7FS79PMfKepQ96gniRomdstrsvNm/xv2Dj5pYFkc43reX7OWJQShjXVf5cq
WSWL4QKBgGVoe8yldF7dijRgB0NYJ7LV+xTQxLpSzK+3b5LyrvSTMwGut0O6QsbK
S060B37PdwBxMD05yTy8Jcr0abl9+tiLGAdok5Ufnm2e7sNsxf8fLhsyPuReutB8
zg1721jMeo9rwnduEi4/U1PIjqYUBIH/jjc9RHSvhk+MVUKXS95R
-----END RSA PRIVATE KEY-----`)
var clientCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDVTCCAj2gAwIBAgIJAM2Hyhl1N+5tMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
DTE3MTAxNzAxMDcyNFoYDzIyOTEwODAyMDEwNzI0WjA4MTYwNAYDVQQDDC1nZW5l
cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jbGllbnQwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTmPyZvq3I5g7F92atr2eUG75V59nj
p35DTXwQSLwsulcWi8/7gPqZ1mIbYrU6v0rHvCmIbiDKuqfxoQ1MRtES3Dv4Zamy
u1Zw6ea0KgytypX3tL5O6svkNjVGH1c4HFr9CeKc6c/g7V0BJiaLS5kFlb816J3C
3vT+xvdUbGPwTHyvMP8tayJWP6KOQrYDXz5LabTUc242tqYmInw2zlXEePlOylEg
GKTU0R8QMUSLjZ11nSf2ri7d5mYULVCowsH/fvX6gWuJ4L6CZuDYUIxTDgsiJQxZ
RGFQ/yRc0QUINa9MtszjTBTqV6MnD4kXRIGzcAhwjgYl5J/ES5PYAUIHAgMBAAGj
ZDBiMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC
BggrBgEFBQcDATApBgNVHREEIjAghwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVs
dC5zdmMwDQYJKoZIhvcNAQELBQADggEBAAgu8K/+UA6V7+AiOPP0Hs3jGTsVjnPB
3XRCSWof5LL93iSRu1rI5LYmjS4N80lV0JkaJNvsoAKxETS3MW4rgv6t3kFOyLMw
mTfIli3iSBMz4WF55px1yhgF85wghEv2+YRF9aSUqAyz4DmlTGlFCEUx+ntkysUD
F97k/jB56EJVqMpSoY5O81vxr21Jpzlryd/UoMVwhYuO3tN0FP+PjoRiQhCGdQTz
2H+TQytZ6Xx6B8BE/joh3WBnQ4705jFhFaDSP8DSH45r48dzbxNLJVNeqLQQ0PhI
clrHwa1WiAnv+4Ydc5CiXGjLjU0sIvETjVtQGPv/gAykeVJo/4nXM8c=
-----END CERTIFICATE-----`)

View File

@@ -1,22 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
// AdmissionConfig holds config data that is unique to each API server.
type AdmissionConfig struct {
KubeConfigFile string `json:"kubeConfigFile"`
}

View File

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

View File

@@ -1,109 +0,0 @@
#!/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
DNS.1 = webhook-test.default.svc
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
DNS.1 = webhook-test.default.svc
EOF
# Create a certificate authority
openssl genrsa -out caKey.pem 2048
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=${CN_BASE}_ca"
# 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=webhook-test.default.svc" -config server.conf
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
# Create a client certiticate
openssl genrsa -out clientKey.pem 2048
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
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

View File

@@ -1,94 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package webhook checks a webhook for configured operation admission
package webhook
import (
"strings"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apiserver/pkg/admission"
)
type RuleMatcher struct {
Rule v1alpha1.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 == v1alpha1.OperationAll {
return true
}
// The constants are the same such that this is a valid cast (and this
// is tested).
if op == v1alpha1.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
}

View File

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

View File

@@ -1,41 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package webhook checks a webhook for configured operation admission
package webhook
import (
"errors"
"fmt"
"net/url"
admissioninit "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
type defaultServiceResolver struct{}
var _ admissioninit.ServiceResolver = defaultServiceResolver{}
// ResolveEndpoint constructs a service URL from a given namespace and name
// note that the name and namespace are required and by default all created addresses use HTTPS scheme.
// for example:
// name=ross namespace=andromeda resolves to https://ross.andromeda.svc:443
func (sr defaultServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
if len(name) == 0 || len(namespace) == 0 {
return nil, errors.New("cannot resolve an empty service name or namespace")
}
return &url.URL{Scheme: "https", Host: fmt.Sprintf("%s.%s.svc:443", name, namespace)}, nil
}

View File

@@ -1,59 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package webhook checks a webhook for configured operation admission
package webhook
import (
"fmt"
"testing"
)
func TestDefaultServiceResolver(t *testing.T) {
scenarios := []struct {
serviceName string
serviceNamespace string
expectedOutput string
expectError bool
}{
// scenario 1: a service name along with a namespace resolves
{serviceName: "ross", serviceNamespace: "andromeda", expectedOutput: "https://ross.andromeda.svc:443"},
// scenario 2: a service name without a namespace does not resolve
{serviceName: "ross", expectError: true},
// scenario 3: cannot resolve an empty service name
{serviceNamespace: "andromeda", expectError: true},
}
// act
for index, scenario := range scenarios {
t.Run(fmt.Sprintf("scenario %d", index), func(t *testing.T) {
target := defaultServiceResolver{}
serviceURL, err := target.ResolveEndpoint(scenario.serviceNamespace, scenario.serviceName)
if err != nil && !scenario.expectError {
t.Errorf("unexpected error has occurred = %v", err)
}
if err == nil && scenario.expectError {
t.Error("expected an error but got nothing")
}
if !scenario.expectError {
if serviceURL.String() != scenario.expectedOutput {
t.Errorf("expected = %s, got = %s", scenario.expectedOutput, serviceURL.String())
}
}
})
}
}