Implement dynamic admission webhooks
Also fix a bug in rest client
This commit is contained in:
@@ -14,25 +14,31 @@ 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 delegates admission checks to dynamically configured webhooks.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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/util/yaml"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
|
||||
"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 admissiony API for use with api registry
|
||||
// install the clientgo admission API for use with api registry
|
||||
_ "k8s.io/kubernetes/pkg/apis/admission/install"
|
||||
)
|
||||
|
||||
@@ -42,22 +48,22 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
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) {
|
||||
var gwhConfig struct {
|
||||
WebhookConfig GenericAdmissionWebhookConfig `json:"webhook"`
|
||||
}
|
||||
|
||||
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
|
||||
err := d.Decode(&gwhConfig)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plugin, err := NewGenericAdmissionWebhook(&gwhConfig.WebhookConfig)
|
||||
|
||||
plugin, err := NewGenericAdmissionWebhook()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,98 +73,150 @@ func Register(plugins *admission.Plugins) {
|
||||
}
|
||||
|
||||
// NewGenericAdmissionWebhook returns a generic admission webhook plugin.
|
||||
func NewGenericAdmissionWebhook(config *GenericAdmissionWebhookConfig) (admission.Interface, error) {
|
||||
err := normalizeConfig(config)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gw, err := webhook.NewGenericWebhook(api.Registry, api.Codecs, config.KubeConfigFile, groupVersions, config.RetryBackoff)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func NewGenericAdmissionWebhook() (*GenericAdmissionWebhook, error) {
|
||||
return &GenericAdmissionWebhook{
|
||||
Handler: admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update),
|
||||
webhook: gw,
|
||||
rules: config.Rules,
|
||||
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
|
||||
webhook *webhook.GenericWebhook
|
||||
rules []Rule
|
||||
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) (err error) {
|
||||
var matched *Rule
|
||||
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()
|
||||
|
||||
// Process all declared rules to attempt to find a match
|
||||
for i, rule := range a.rules {
|
||||
if Matches(rule, attr) {
|
||||
glog.V(2).Infof("rule at index %d matched request", i)
|
||||
matched = &a.rules[i]
|
||||
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 matched == nil {
|
||||
glog.V(2).Infof("rule explicitly allowed the request: no rule matched the admission request")
|
||||
return nil
|
||||
}
|
||||
|
||||
// The matched rule skips processing this request
|
||||
if matched.Type == Skip {
|
||||
glog.V(2).Infof("rule explicitly allowed the request")
|
||||
if !matches {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make the webhook request
|
||||
request := admissionv1alpha1.NewAdmissionReview(attr)
|
||||
response := a.webhook.RestClient.Post().Body(&request).Do()
|
||||
|
||||
// Handle webhook response
|
||||
if err := response.Error(); err != nil {
|
||||
return a.handleError(attr, matched.FailAction, err)
|
||||
}
|
||||
|
||||
var statusCode int
|
||||
if response.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
|
||||
return a.handleError(attr, matched.FailAction, fmt.Errorf("error contacting webhook: %d", statusCode))
|
||||
}
|
||||
|
||||
if err := response.Into(&request); err != nil {
|
||||
return a.handleError(attr, matched.FailAction, err)
|
||||
}
|
||||
|
||||
if !request.Status.Allowed {
|
||||
if request.Status.Result != nil && len(request.Status.Result.Reason) > 0 {
|
||||
return a.handleError(attr, Deny, fmt.Errorf("webhook backend denied the request: %s", request.Status.Result.Reason))
|
||||
}
|
||||
return a.handleError(attr, Deny, errors.New("webhook backend denied the request"))
|
||||
}
|
||||
|
||||
// The webhook admission controller DOES NOT allow mutation of the admission request so nothing else is required
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) handleError(attr admission.Attributes, allowIfErr FailAction, err error) error {
|
||||
client, err := a.hookClient(h)
|
||||
if err != nil {
|
||||
glog.V(2).Infof("error contacting webhook backend: %s", err)
|
||||
if allowIfErr != Allow {
|
||||
glog.V(2).Infof("resource not allowed due to webhook backend failure: %s", err)
|
||||
return admission.NewForbidden(attr, err)
|
||||
}
|
||||
glog.V(2).Infof("resource allowed in spite of webhook backend failure")
|
||||
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}
|
||||
}
|
||||
|
||||
return nil
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Allow configuring the serialization strategy
|
||||
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)
|
||||
}
|
||||
|
@@ -17,171 +17,81 @@ limitations under the License.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"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/client-go/pkg/api"
|
||||
"k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
errAdmitPrefix = `pods "my-pod" is forbidden: `
|
||||
)
|
||||
|
||||
var (
|
||||
requestCount int
|
||||
testRoot string
|
||||
testServer *httptest.Server
|
||||
)
|
||||
|
||||
type genericAdmissionWebhookTest struct {
|
||||
test string
|
||||
config *GenericAdmissionWebhookConfig
|
||||
value interface{}
|
||||
type fakeHookSource struct {
|
||||
hooks []admissionregistration.ExternalAdmissionHook
|
||||
err error
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
tmpRoot, err := ioutil.TempDir("", "")
|
||||
|
||||
if err != nil {
|
||||
killTests(err)
|
||||
func (f *fakeHookSource) List() ([]admissionregistration.ExternalAdmissionHook, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
|
||||
testRoot = tmpRoot
|
||||
|
||||
// Cleanup
|
||||
defer os.RemoveAll(testRoot)
|
||||
|
||||
// Create the test webhook server
|
||||
cert, err := tls.X509KeyPair(clientCert, clientKey)
|
||||
|
||||
if err != nil {
|
||||
killTests(err)
|
||||
}
|
||||
|
||||
rootCAs := x509.NewCertPool()
|
||||
|
||||
rootCAs.AppendCertsFromPEM(caCert)
|
||||
|
||||
testServer = httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler))
|
||||
|
||||
testServer.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ClientCAs: rootCAs,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
|
||||
// Create the test webhook server
|
||||
testServer.StartTLS()
|
||||
|
||||
// Cleanup
|
||||
defer testServer.Close()
|
||||
|
||||
// Create an invalid and valid Kubernetes configuration file
|
||||
var kubeConfig bytes.Buffer
|
||||
|
||||
err = json.NewEncoder(&kubeConfig).Encode(v1.Config{
|
||||
Clusters: []v1.NamedCluster{
|
||||
{
|
||||
Cluster: v1.Cluster{
|
||||
Server: testServer.URL,
|
||||
CertificateAuthorityData: caCert,
|
||||
},
|
||||
},
|
||||
},
|
||||
AuthInfos: []v1.NamedAuthInfo{
|
||||
{
|
||||
AuthInfo: v1.AuthInfo{
|
||||
ClientCertificateData: clientCert,
|
||||
ClientKeyData: clientKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
killTests(err)
|
||||
}
|
||||
|
||||
// The files needed on disk for the webhook tests
|
||||
files := map[string][]byte{
|
||||
"ca.pem": caCert,
|
||||
"client.pem": clientCert,
|
||||
"client-key.pem": clientKey,
|
||||
"kube-config": kubeConfig.Bytes(),
|
||||
}
|
||||
|
||||
// Write the certificate files to disk or fail
|
||||
for fileName, fileData := range files {
|
||||
if err := ioutil.WriteFile(filepath.Join(testRoot, fileName), fileData, 0400); err != nil {
|
||||
killTests(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the tests
|
||||
m.Run()
|
||||
return f.hooks, nil
|
||||
}
|
||||
|
||||
// TestNewGenericAdmissionWebhook tests that NewGenericAdmissionWebhook works as expected
|
||||
func TestNewGenericAdmissionWebhook(t *testing.T) {
|
||||
tests := []genericAdmissionWebhookTest{
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Empty webhook config",
|
||||
config: &GenericAdmissionWebhookConfig{},
|
||||
value: fmt.Errorf("kubeConfigFile is required"),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Broken webhook config",
|
||||
config: &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config.missing"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
value: fmt.Errorf("stat .*kube-config.missing.*"),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Valid webhook config",
|
||||
config: &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
value: nil,
|
||||
},
|
||||
}
|
||||
type fakeServiceResolver struct {
|
||||
base url.URL
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, err := NewGenericAdmissionWebhook(tt.config)
|
||||
|
||||
checkForError(t, tt.test, tt.value, err)
|
||||
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) {
|
||||
configWithFailAction := makeGoodConfig()
|
||||
// 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"
|
||||
@@ -209,161 +119,145 @@ func TestAdmit(t *testing.T) {
|
||||
UID: "webhook-test",
|
||||
}
|
||||
|
||||
configWithFailAction.Rules[0].FailAction = Allow
|
||||
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{"*/*"},
|
||||
},
|
||||
}}
|
||||
|
||||
tests := []genericAdmissionWebhookTest{
|
||||
genericAdmissionWebhookTest{
|
||||
test: "No matching rule",
|
||||
config: &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Operations: []admission.Operation{
|
||||
admission.Create,
|
||||
},
|
||||
Type: Send,
|
||||
},
|
||||
},
|
||||
table := map[string]test{
|
||||
"no match": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "nomatch",
|
||||
ClientConfig: ccfg("disallow"),
|
||||
Rules: []admissionregistration.RuleWithOperations{{
|
||||
Operations: []admissionregistration.OperationType{admissionregistration.Create},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
value: nil,
|
||||
expectAllow: true,
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule skips",
|
||||
config: &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Operations: []admission.Operation{
|
||||
admission.Update,
|
||||
},
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
"match & allow": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "allow",
|
||||
ClientConfig: ccfg("allow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
value: nil,
|
||||
expectAllow: true,
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook internal server error)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf(`%san error on the server ("webhook internal server error") has prevented the request from succeeding`, errAdmitPrefix),
|
||||
"match & disallow": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfg("disallow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "without explanation",
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook unsuccessful response)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf("%serror contacting webhook: 101", errAdmitPrefix),
|
||||
"match & disallow ii": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []admissionregistration.ExternalAdmissionHook{{
|
||||
Name: "disallowReason",
|
||||
ClientConfig: ccfg("disallowReason"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "you shall not pass",
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook unmarshallable response)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf("%scouldn't get version/kind; json parse error: json: cannot unmarshal string into Go value of type struct.*", errAdmitPrefix),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook error but allowed via fail action)",
|
||||
config: configWithFailAction,
|
||||
value: nil,
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook request denied without reason)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf("%swebhook backend denied the request", errAdmitPrefix),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook request denied with reason)",
|
||||
config: makeGoodConfig(),
|
||||
value: fmt.Errorf("%swebhook backend denied the request: you shall not pass", errAdmitPrefix),
|
||||
},
|
||||
genericAdmissionWebhookTest{
|
||||
test: "Matching rule sends (webhook request allowed)",
|
||||
config: makeGoodConfig(),
|
||||
value: nil,
|
||||
"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 _, tt := range tests {
|
||||
wh, err := NewGenericAdmissionWebhook(tt.config)
|
||||
for name, tt := range table {
|
||||
wh.hookSource = &tt.hookSource
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tt.test, err)
|
||||
} else {
|
||||
err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, name, resource, subResource, operation, &userInfo))
|
||||
|
||||
checkForError(t, tt.test, tt.value, err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkForError(t *testing.T, test string, expected, actual interface{}) {
|
||||
aErr, _ := actual.(error)
|
||||
eErr, _ := expected.(error)
|
||||
|
||||
if eErr != nil {
|
||||
if aErr == nil {
|
||||
t.Errorf("%s: expected an error", test)
|
||||
} else if eErr.Error() != aErr.Error() && !regexp.MustCompile(eErr.Error()).MatchString(aErr.Error()) {
|
||||
t.Errorf("%s: unexpected error message to match:\n Expected: %s\n Actual: %s", test, eErr.Error(), aErr.Error())
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if aErr != nil {
|
||||
t.Errorf("%s: unexpected error: %v", test, aErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func killTests(err error) {
|
||||
panic(fmt.Sprintf("Unable to bootstrap tests: %v", err))
|
||||
}
|
||||
|
||||
func makeGoodConfig() *GenericAdmissionWebhookConfig {
|
||||
return &GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: filepath.Join(testRoot, "kube-config"),
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Operations: []admission.Operation{
|
||||
admission.Update,
|
||||
},
|
||||
Type: Send,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
requestCount++
|
||||
|
||||
switch requestCount {
|
||||
case 1, 4:
|
||||
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 2:
|
||||
case "/invalidReq":
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
w.Write([]byte("webhook invalid request"))
|
||||
return
|
||||
case 3:
|
||||
case "/invalidResp":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("webhook invalid response"))
|
||||
case 5:
|
||||
case "/disallow":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
|
||||
Status: v1alpha1.AdmissionReviewStatus{
|
||||
Allowed: false,
|
||||
},
|
||||
})
|
||||
case 6:
|
||||
case "/disallowReason":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1alpha1.AdmissionReview{
|
||||
Status: v1alpha1.AdmissionReviewStatus{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Reason: "you shall not pass",
|
||||
Message: "you shall not pass",
|
||||
},
|
||||
},
|
||||
})
|
||||
case 7:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -1,120 +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"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFailAction = Deny
|
||||
defaultRetryBackoff = time.Duration(500) * time.Millisecond
|
||||
errInvalidFailAction = "webhook rule (%d) has an invalid fail action: %s should be either %v or %v"
|
||||
errInvalidRuleOperation = "webhook rule (%d) has an invalid operation (%d): %s"
|
||||
errInvalidRuleType = "webhook rule (%d) has an invalid type: %s should be either %v or %v"
|
||||
errMissingKubeConfigFile = "kubeConfigFile is required"
|
||||
errOneRuleRequired = "webhook requires at least one rule defined"
|
||||
errRetryBackoffOutOfRange = "webhook retry backoff is invalid: %v should be between %v and %v"
|
||||
minRetryBackoff = time.Duration(1) * time.Millisecond
|
||||
maxRetryBackoff = time.Duration(5) * time.Minute
|
||||
)
|
||||
|
||||
func normalizeConfig(config *GenericAdmissionWebhookConfig) error {
|
||||
allowFailAction := string(Allow)
|
||||
denyFailAction := string(Deny)
|
||||
connectOperation := string(admission.Connect)
|
||||
createOperation := string(admission.Create)
|
||||
deleteOperation := string(admission.Delete)
|
||||
updateOperation := string(admission.Update)
|
||||
sendRuleType := string(Send)
|
||||
skipRuleType := string(Skip)
|
||||
|
||||
// Validate the kubeConfigFile property is present
|
||||
if config.KubeConfigFile == "" {
|
||||
return fmt.Errorf(errMissingKubeConfigFile)
|
||||
}
|
||||
|
||||
// Normalize and validate the retry backoff
|
||||
if config.RetryBackoff == 0 {
|
||||
config.RetryBackoff = defaultRetryBackoff
|
||||
} else {
|
||||
// Unmarshalling gives nanoseconds so convert to milliseconds
|
||||
config.RetryBackoff *= time.Millisecond
|
||||
}
|
||||
|
||||
if config.RetryBackoff < minRetryBackoff || config.RetryBackoff > maxRetryBackoff {
|
||||
return fmt.Errorf(errRetryBackoffOutOfRange, config.RetryBackoff, minRetryBackoff, maxRetryBackoff)
|
||||
}
|
||||
|
||||
// Validate that there is at least one rule
|
||||
if len(config.Rules) == 0 {
|
||||
return fmt.Errorf(errOneRuleRequired)
|
||||
}
|
||||
|
||||
for i, rule := range config.Rules {
|
||||
// Normalize and validate the fail action
|
||||
failAction := strings.ToUpper(string(rule.FailAction))
|
||||
|
||||
switch failAction {
|
||||
case "":
|
||||
config.Rules[i].FailAction = defaultFailAction
|
||||
case allowFailAction:
|
||||
config.Rules[i].FailAction = Allow
|
||||
case denyFailAction:
|
||||
config.Rules[i].FailAction = Deny
|
||||
default:
|
||||
return fmt.Errorf(errInvalidFailAction, i, rule.FailAction, Allow, Deny)
|
||||
}
|
||||
|
||||
// Normalize and validate the rule operation(s)
|
||||
for j, operation := range rule.Operations {
|
||||
operation := strings.ToUpper(string(operation))
|
||||
|
||||
switch operation {
|
||||
case connectOperation:
|
||||
config.Rules[i].Operations[j] = admission.Connect
|
||||
case createOperation:
|
||||
config.Rules[i].Operations[j] = admission.Create
|
||||
case deleteOperation:
|
||||
config.Rules[i].Operations[j] = admission.Delete
|
||||
case updateOperation:
|
||||
config.Rules[i].Operations[j] = admission.Update
|
||||
default:
|
||||
return fmt.Errorf(errInvalidRuleOperation, i, j, rule.Operations[j])
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and validate the rule type
|
||||
ruleType := strings.ToUpper(string(rule.Type))
|
||||
|
||||
switch ruleType {
|
||||
case sendRuleType:
|
||||
config.Rules[i].Type = Send
|
||||
case skipRuleType:
|
||||
config.Rules[i].Type = Skip
|
||||
default:
|
||||
return fmt.Errorf(errInvalidRuleType, i, rule.Type, Send, Skip)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -1,220 +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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
func TestConfigNormalization(t *testing.T) {
|
||||
defaultRules := []Rule{
|
||||
Rule{
|
||||
Type: Skip,
|
||||
},
|
||||
}
|
||||
highRetryBackoff := (maxRetryBackoff / time.Millisecond) + (time.Duration(1) * time.Millisecond)
|
||||
kubeConfigFile := "/tmp/kube/config"
|
||||
lowRetryBackoff := time.Duration(-1)
|
||||
normalizedValidRules := []Rule{
|
||||
Rule{
|
||||
APIGroups: []string{""},
|
||||
FailAction: Allow,
|
||||
Namespaces: []string{"my-ns"},
|
||||
Operations: []admission.Operation{
|
||||
admission.Connect,
|
||||
admission.Create,
|
||||
admission.Delete,
|
||||
admission.Update,
|
||||
},
|
||||
Resources: []string{"pods"},
|
||||
ResourceNames: []string{"my-name"},
|
||||
Type: Send,
|
||||
},
|
||||
Rule{
|
||||
FailAction: Deny,
|
||||
Type: Skip,
|
||||
},
|
||||
}
|
||||
rawValidRules := []Rule{
|
||||
Rule{
|
||||
APIGroups: []string{""},
|
||||
FailAction: FailAction("AlLoW"),
|
||||
Namespaces: []string{"my-ns"},
|
||||
Operations: []admission.Operation{
|
||||
admission.Operation("connect"),
|
||||
admission.Operation("CREATE"),
|
||||
admission.Operation("DeLeTe"),
|
||||
admission.Operation("UPdaTE"),
|
||||
},
|
||||
Resources: []string{"pods"},
|
||||
ResourceNames: []string{"my-name"},
|
||||
Type: RuleType("SenD"),
|
||||
},
|
||||
Rule{
|
||||
FailAction: Deny,
|
||||
Type: Skip,
|
||||
},
|
||||
}
|
||||
unknownFailAction := FailAction("Unknown")
|
||||
unknownOperation := admission.Operation("Unknown")
|
||||
unknownRuleType := RuleType("Allow")
|
||||
tests := []struct {
|
||||
test string
|
||||
rawConfig GenericAdmissionWebhookConfig
|
||||
normalizedConfig GenericAdmissionWebhookConfig
|
||||
err error
|
||||
}{
|
||||
{
|
||||
test: "kubeConfigFile was not provided (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{},
|
||||
err: fmt.Errorf(errMissingKubeConfigFile),
|
||||
},
|
||||
{
|
||||
test: "retryBackoff was not provided (use default)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: defaultRules,
|
||||
},
|
||||
normalizedConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: defaultRetryBackoff,
|
||||
Rules: defaultRules,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: "retryBackoff was below minimum value (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: lowRetryBackoff,
|
||||
Rules: defaultRules,
|
||||
},
|
||||
err: fmt.Errorf(errRetryBackoffOutOfRange, lowRetryBackoff*time.Millisecond, minRetryBackoff, maxRetryBackoff),
|
||||
},
|
||||
{
|
||||
test: "retryBackoff was above maximum value (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: highRetryBackoff,
|
||||
Rules: defaultRules,
|
||||
},
|
||||
err: fmt.Errorf(errRetryBackoffOutOfRange, highRetryBackoff*time.Millisecond, minRetryBackoff, maxRetryBackoff),
|
||||
},
|
||||
{
|
||||
test: "rules should have at least one rule (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
},
|
||||
err: fmt.Errorf(errOneRuleRequired),
|
||||
},
|
||||
{
|
||||
test: "fail action was not provided (use default)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
normalizedConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: defaultRetryBackoff,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
FailAction: defaultFailAction,
|
||||
Type: Skip,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: "rule has invalid fail action (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
FailAction: unknownFailAction,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf(errInvalidFailAction, 0, unknownFailAction, Allow, Deny),
|
||||
},
|
||||
{
|
||||
test: "rule has invalid operation (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Operations: []admission.Operation{unknownOperation},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf(errInvalidRuleOperation, 0, 0, unknownOperation),
|
||||
},
|
||||
{
|
||||
test: "rule has invalid type (error)",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: []Rule{
|
||||
Rule{
|
||||
Type: unknownRuleType,
|
||||
},
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf(errInvalidRuleType, 0, unknownRuleType, Send, Skip),
|
||||
},
|
||||
{
|
||||
test: "valid configuration",
|
||||
rawConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
Rules: rawValidRules,
|
||||
},
|
||||
normalizedConfig: GenericAdmissionWebhookConfig{
|
||||
KubeConfigFile: kubeConfigFile,
|
||||
RetryBackoff: defaultRetryBackoff,
|
||||
Rules: normalizedValidRules,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
err := normalizeConfig(&tt.rawConfig)
|
||||
if err == nil {
|
||||
if tt.err != nil {
|
||||
// Ensure that expected errors are produced
|
||||
t.Errorf("%s: expected error but did not produce one", tt.test)
|
||||
} else if !reflect.DeepEqual(tt.rawConfig, tt.normalizedConfig) {
|
||||
// Ensure that valid configurations are structured properly
|
||||
t.Errorf("%s: normalized config mismtach. got: %v expected: %v", tt.test, tt.rawConfig, tt.normalizedConfig)
|
||||
}
|
||||
} else {
|
||||
if tt.err == nil {
|
||||
// Ensure that unexpected errors are not produced
|
||||
t.Errorf("%s: unexpected error: %v", tt.test, err)
|
||||
} else if err != nil && tt.err != nil && err.Error() != tt.err.Error() {
|
||||
// Ensure that expected errors are formated properly
|
||||
t.Errorf("%s: error message mismatch. got: '%v' expected: '%v'", tt.test, err, tt.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,46 +17,31 @@ limitations under the License.
|
||||
// Package webhook checks a webhook for configured operation admission
|
||||
package webhook
|
||||
|
||||
import "k8s.io/apiserver/pkg/admission"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
func indexOf(items []string, requested string) int {
|
||||
for i, item := range items {
|
||||
if item == requested {
|
||||
return i
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// APIGroupMatches returns if the admission.Attributes matches the rule's API groups
|
||||
func APIGroupMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.APIGroups) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return indexOf(rule.APIGroups, attr.GetResource().Group) > -1
|
||||
}
|
||||
|
||||
// Matches returns if the admission.Attributes matches the rule
|
||||
func Matches(rule Rule, attr admission.Attributes) bool {
|
||||
return APIGroupMatches(rule, attr) &&
|
||||
NamespaceMatches(rule, attr) &&
|
||||
OperationMatches(rule, attr) &&
|
||||
ResourceMatches(rule, attr) &&
|
||||
ResourceNamesMatches(rule, attr)
|
||||
}
|
||||
|
||||
// OperationMatches returns if the admission.Attributes matches the rule's operation
|
||||
func OperationMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.Operations) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
aOp := attr.GetOperation()
|
||||
|
||||
for _, rOp := range rule.Operations {
|
||||
if aOp == rOp {
|
||||
if item == requested {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -64,35 +49,46 @@ func OperationMatches(rule Rule, attr admission.Attributes) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// NamespaceMatches returns if the admission.Attributes matches the rule's namespaces
|
||||
func NamespaceMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.Namespaces) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return indexOf(rule.Namespaces, attr.GetNamespace()) > -1
|
||||
func (r *RuleMatcher) group() bool {
|
||||
return exactOrWildcard(r.Rule.APIGroups, r.Attr.GetResource().Group)
|
||||
}
|
||||
|
||||
// ResourceMatches returns if the admission.Attributes matches the rule's resource (and optional subresource)
|
||||
func ResourceMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.Resources) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
resource := attr.GetResource().Resource
|
||||
|
||||
if len(attr.GetSubresource()) > 0 {
|
||||
resource = attr.GetResource().Resource + "/" + attr.GetSubresource()
|
||||
}
|
||||
|
||||
return indexOf(rule.Resources, resource) > -1
|
||||
func (r *RuleMatcher) version() bool {
|
||||
return exactOrWildcard(r.Rule.APIVersions, r.Attr.GetResource().Version)
|
||||
}
|
||||
|
||||
// ResourceNamesMatches returns if the admission.Attributes matches the rule's resource names
|
||||
func ResourceNamesMatches(rule Rule, attr admission.Attributes) bool {
|
||||
if len(rule.ResourceNames) == 0 {
|
||||
return true
|
||||
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 indexOf(rule.ResourceNames, attr.GetName()) > -1
|
||||
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
|
||||
}
|
||||
|
@@ -17,293 +17,284 @@ limitations under the License.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/client-go/pkg/api"
|
||||
adreg "k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
type ruleTest struct {
|
||||
test string
|
||||
rule Rule
|
||||
attrAPIGroup string
|
||||
attrNamespace string
|
||||
attrOperation admission.Operation
|
||||
attrResource string
|
||||
attrResourceName string
|
||||
attrSubResource string
|
||||
shouldMatch bool
|
||||
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 TestAPIGroupMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "apiGroups empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "apiGroup match",
|
||||
rule: Rule{
|
||||
APIGroups: []string{"my-group"},
|
||||
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{"*"},
|
||||
},
|
||||
},
|
||||
attrAPIGroup: "my-group",
|
||||
shouldMatch: true,
|
||||
match: attrList(
|
||||
a("g", "v", "r", "", "name", admission.Create),
|
||||
),
|
||||
},
|
||||
ruleTest{
|
||||
test: "apiGroup mismatch",
|
||||
rule: Rule{
|
||||
APIGroups: []string{"my-group"},
|
||||
"exact": {
|
||||
rule: adreg.RuleWithOperations{
|
||||
Rule: adreg.Rule{
|
||||
APIGroups: []string{"g1", "g2"},
|
||||
},
|
||||
},
|
||||
attrAPIGroup: "your-group",
|
||||
shouldMatch: false,
|
||||
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),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "apiGroups", tests)
|
||||
}
|
||||
|
||||
func TestMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "empty rule matches",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "all properties match",
|
||||
rule: Rule{
|
||||
APIGroups: []string{"my-group"},
|
||||
Namespaces: []string{"my-ns"},
|
||||
Operations: []admission.Operation{admission.Create},
|
||||
ResourceNames: []string{"my-name"},
|
||||
Resources: []string{"pods/status"},
|
||||
},
|
||||
shouldMatch: true,
|
||||
attrAPIGroup: "my-group",
|
||||
attrNamespace: "my-ns",
|
||||
attrOperation: admission.Create,
|
||||
attrResource: "pods",
|
||||
attrResourceName: "my-name",
|
||||
attrSubResource: "status",
|
||||
},
|
||||
ruleTest{
|
||||
test: "no properties match",
|
||||
rule: Rule{
|
||||
APIGroups: []string{"my-group"},
|
||||
Namespaces: []string{"my-ns"},
|
||||
Operations: []admission.Operation{admission.Create},
|
||||
ResourceNames: []string{"my-name"},
|
||||
Resources: []string{"pods/status"},
|
||||
},
|
||||
shouldMatch: false,
|
||||
attrAPIGroup: "your-group",
|
||||
attrNamespace: "your-ns",
|
||||
attrOperation: admission.Delete,
|
||||
attrResource: "secrets",
|
||||
attrResourceName: "your-name",
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "", tests)
|
||||
}
|
||||
|
||||
func TestNamespaceMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "namespaces empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "namespace match",
|
||||
rule: Rule{
|
||||
Namespaces: []string{"my-ns"},
|
||||
},
|
||||
attrNamespace: "my-ns",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "namespace mismatch",
|
||||
rule: Rule{
|
||||
Namespaces: []string{"my-ns"},
|
||||
},
|
||||
attrNamespace: "your-ns",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "namespaces", tests)
|
||||
}
|
||||
|
||||
func TestOperationMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "operations empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "operation match",
|
||||
rule: Rule{
|
||||
Operations: []admission.Operation{admission.Create},
|
||||
},
|
||||
attrOperation: admission.Create,
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "operation mismatch",
|
||||
rule: Rule{
|
||||
Operations: []admission.Operation{admission.Create},
|
||||
},
|
||||
attrOperation: admission.Delete,
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "operations", tests)
|
||||
}
|
||||
|
||||
func TestResourceMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "resources empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resource match",
|
||||
rule: Rule{
|
||||
Resources: []string{"pods"},
|
||||
},
|
||||
attrResource: "pods",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resource mismatch",
|
||||
rule: Rule{
|
||||
Resources: []string{"pods"},
|
||||
},
|
||||
attrResource: "secrets",
|
||||
shouldMatch: false,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resource with subresource match",
|
||||
rule: Rule{
|
||||
Resources: []string{"pods/status"},
|
||||
},
|
||||
attrResource: "pods",
|
||||
attrSubResource: "status",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resource with subresource mismatch",
|
||||
rule: Rule{
|
||||
Resources: []string{"pods"},
|
||||
},
|
||||
attrResource: "pods",
|
||||
attrSubResource: "status",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "resources", tests)
|
||||
}
|
||||
|
||||
func TestResourceNameMatches(t *testing.T) {
|
||||
tests := []ruleTest{
|
||||
ruleTest{
|
||||
test: "resourceNames empty match",
|
||||
rule: Rule{},
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resourceName match",
|
||||
rule: Rule{
|
||||
ResourceNames: []string{"my-name"},
|
||||
},
|
||||
attrResourceName: "my-name",
|
||||
shouldMatch: true,
|
||||
},
|
||||
ruleTest{
|
||||
test: "resourceName mismatch",
|
||||
rule: Rule{
|
||||
ResourceNames: []string{"my-name"},
|
||||
},
|
||||
attrResourceName: "your-name",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, "resourceNames", tests)
|
||||
}
|
||||
|
||||
func runTests(t *testing.T, prop string, tests []ruleTest) {
|
||||
for _, tt := range tests {
|
||||
if tt.attrResource == "" {
|
||||
tt.attrResource = "pods"
|
||||
}
|
||||
|
||||
res := api.Resource(tt.attrResource).WithVersion("version")
|
||||
|
||||
if tt.attrAPIGroup != "" {
|
||||
res.Group = tt.attrAPIGroup
|
||||
}
|
||||
|
||||
attr := admission.NewAttributesRecord(nil, nil, api.Kind("Pod").WithVersion("version"), tt.attrNamespace, tt.attrResourceName, res, tt.attrSubResource, tt.attrOperation, nil)
|
||||
var attrVal string
|
||||
var ruleVal []string
|
||||
var matches bool
|
||||
|
||||
switch prop {
|
||||
case "":
|
||||
matches = Matches(tt.rule, attr)
|
||||
case "apiGroups":
|
||||
attrVal = tt.attrAPIGroup
|
||||
matches = APIGroupMatches(tt.rule, attr)
|
||||
ruleVal = tt.rule.APIGroups
|
||||
case "namespaces":
|
||||
attrVal = tt.attrNamespace
|
||||
matches = NamespaceMatches(tt.rule, attr)
|
||||
ruleVal = tt.rule.Namespaces
|
||||
case "operations":
|
||||
attrVal = string(tt.attrOperation)
|
||||
matches = OperationMatches(tt.rule, attr)
|
||||
ruleVal = make([]string, len(tt.rule.Operations))
|
||||
|
||||
for _, rOp := range tt.rule.Operations {
|
||||
ruleVal = append(ruleVal, string(rOp))
|
||||
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)
|
||||
}
|
||||
case "resources":
|
||||
attrVal = tt.attrResource
|
||||
matches = ResourceMatches(tt.rule, attr)
|
||||
ruleVal = tt.rule.Resources
|
||||
case "resourceNames":
|
||||
attrVal = tt.attrResourceName
|
||||
matches = ResourceNamesMatches(tt.rule, attr)
|
||||
ruleVal = tt.rule.ResourceNames
|
||||
default:
|
||||
t.Errorf("Unexpected test property: %s", prop)
|
||||
}
|
||||
|
||||
if matches && !tt.shouldMatch {
|
||||
if prop == "" {
|
||||
testError(t, tt.test, "Expected match")
|
||||
} else {
|
||||
testError(t, tt.test, fmt.Sprintf("Expected %s rule property not to match %s against one of the following: %s", prop, attrVal, strings.Join(ruleVal, ", ")))
|
||||
}
|
||||
} else if !matches && tt.shouldMatch {
|
||||
if prop == "" {
|
||||
testError(t, tt.test, "Unexpected match")
|
||||
} else {
|
||||
testError(t, tt.test, fmt.Sprintf("Expected %s rule property to match %s against one of the following: %s", prop, attrVal, strings.Join(ruleVal, ", ")))
|
||||
for _, m := range tt.noMatch {
|
||||
r := RuleMatcher{tt.rule, m}
|
||||
if r.group() {
|
||||
t.Errorf("%v: expected no match %#v", name, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testError(t *testing.T, name, msg string) {
|
||||
t.Errorf("test failed (%s): %s", name, msg)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,69 +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 (
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
// FailAction is the action to take whenever a webhook fails and there are no more retries available
|
||||
type FailAction string
|
||||
|
||||
const (
|
||||
// Allow is the fail action taken whenever the webhook call fails and there are no more retries
|
||||
Allow FailAction = "ALLOW"
|
||||
// Deny is the fail action taken whenever the webhook call fails and there are no more retries
|
||||
Deny FailAction = "DENY"
|
||||
)
|
||||
|
||||
// GenericAdmissionWebhookConfig holds configuration for an admission webhook
|
||||
type GenericAdmissionWebhookConfig struct {
|
||||
KubeConfigFile string `json:"kubeConfigFile"`
|
||||
RetryBackoff time.Duration `json:"retryBackoff"`
|
||||
Rules []Rule `json:"rules"`
|
||||
}
|
||||
|
||||
// Rule is the type defining an admission rule in the admission controller configuration file
|
||||
type Rule struct {
|
||||
// APIGroups is a list of API groups that contain the resource this rule applies to.
|
||||
APIGroups []string `json:"apiGroups"`
|
||||
// FailAction is the action to take whenever the webhook fails and there are no more retries (Default: DENY)
|
||||
FailAction FailAction `json:"failAction"`
|
||||
// Namespaces is a list of namespaces this rule applies to.
|
||||
Namespaces []string `json:"namespaces"`
|
||||
// Operations is a list of admission operations this rule applies to.
|
||||
Operations []admission.Operation `json:"operations"`
|
||||
// Resources is a list of resources this rule applies to.
|
||||
Resources []string `json:"resources"`
|
||||
// ResourceNames is a list of resource names this rule applies to.
|
||||
ResourceNames []string `json:"resourceNames"`
|
||||
// Type is the admission rule type
|
||||
Type RuleType `json:"type"`
|
||||
}
|
||||
|
||||
// RuleType is the type of admission rule
|
||||
type RuleType string
|
||||
|
||||
const (
|
||||
// Send is the rule type for when a matching admission.Attributes should be sent to the webhook
|
||||
Send RuleType = "SEND"
|
||||
// Skip is the rule type for when a matching admission.Attributes should not be sent to the webhook
|
||||
Skip RuleType = "SKIP"
|
||||
)
|
Reference in New Issue
Block a user