Skip to content

Commit

Permalink
Add mutating admission webhook reinvocation
Browse files Browse the repository at this point in the history
  • Loading branch information
jpbetz authored and Chao Xu committed May 30, 2019
1 parent 939a04f commit 95fa928
Show file tree
Hide file tree
Showing 23 changed files with 919 additions and 116 deletions.
2 changes: 2 additions & 0 deletions pkg/apis/admissionregistration/fuzzer/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
obj.MatchPolicy = &m
s := admissionregistration.SideEffectClassUnknown
obj.SideEffects = &s
n := admissionregistration.NeverReinvocationPolicy
obj.ReinvocationPolicy = &n
if obj.TimeoutSeconds == nil {
i := int32(30)
obj.TimeoutSeconds = &i
Expand Down
31 changes: 31 additions & 0 deletions pkg/apis/admissionregistration/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,39 @@ type MutatingWebhook struct {
// does not understand, calls to the webhook will fail and be subject to the failure policy.
// +optional
AdmissionReviewVersions []string

// reinvocationPolicy indicates whether this webhook should be called multiple times as part of a single admission evaluation.
// Allowed values are "Never" and "IfNeeded".
//
// Never: the webhook will not be called more than once in a single admission evaluation.
//
// IfNeeded: the webhook will be called at least one additional time as part of the admission evaluation
// if the object being admitted is modified by other admission plugins after the initial webhook call.
// Webhooks that specify this option *must* be idempotent, and hence able to process objects they previously admitted.
// Note:
// * the number of additional invocations is not guaranteed to be exactly one.
// * if additional invocations result in further modifications to the object, webhooks are not guaranteed to be invoked again.
// * webhooks that use this option may be reordered to minimize the number of additional invocations.
// * to validate an object after all mutations are guaranteed complete, use a validating admission webhook instead.
//
// Defaults to "Never".
// +optional
ReinvocationPolicy *ReinvocationPolicyType
}

// ReinvocationPolicyType specifies what type of policy the admission hook uses.
type ReinvocationPolicyType string

var (
// NeverReinvocationPolicy indicates that the webhook must not be called more than once in a
// single admission evaluation.
NeverReinvocationPolicy ReinvocationPolicyType = "Never"
// IfNeededReinvocationPolicy indicates that the webhook may be called at least one
// additional time as part of the admission evaluation if the object being admitted is
// modified by other admission plugins after the initial webhook call.
IfNeededReinvocationPolicy ReinvocationPolicyType = "IfNeeded"
)

// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
// sure that all the tuple expansions are valid.
type RuleWithOperations struct {
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/admissionregistration/v1beta1/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func SetDefaults_MutatingWebhook(obj *admissionregistrationv1beta1.MutatingWebho
obj.TimeoutSeconds = new(int32)
*obj.TimeoutSeconds = 30
}
if obj.ReinvocationPolicy == nil {
never := admissionregistrationv1beta1.NeverReinvocationPolicy
obj.ReinvocationPolicy = &never
}

if len(obj.AdmissionReviewVersions) == 0 {
obj.AdmissionReviewVersions = []string{admissionregistrationv1beta1.SchemeGroupVersion.Version}
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/admissionregistration/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ func validateMutatingWebhook(hook *admissionregistration.MutatingWebhook, fldPat
if hook.NamespaceSelector != nil {
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
}
if hook.ReinvocationPolicy != nil && !supportedReinvocationPolicies.Has(string(*hook.ReinvocationPolicy)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("reinvocationPolicy"), *hook.ReinvocationPolicy, supportedReinvocationPolicies.List()))
}

cc := hook.ClientConfig
switch {
Expand Down Expand Up @@ -319,6 +322,11 @@ var supportedOperations = sets.NewString(
string(admissionregistration.Connect),
)

var supportedReinvocationPolicies = sets.NewString(
string(admissionregistration.NeverReinvocationPolicy),
string(admissionregistration.IfNeededReinvocationPolicy),
)

func hasWildcardOperation(operations []admissionregistration.OperationType) bool {
for _, o := range operations {
if o == admissionregistration.OperationAll {
Expand Down
31 changes: 31 additions & 0 deletions staging/src/k8s.io/api/admissionregistration/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,39 @@ type MutatingWebhook struct {
// Default to `['v1beta1']`.
// +optional
AdmissionReviewVersions []string `json:"admissionReviewVersions,omitempty" protobuf:"bytes,8,rep,name=admissionReviewVersions"`

// reinvocationPolicy indicates whether this webhook should be called multiple times as part of a single admission evaluation.
// Allowed values are "Never" and "IfNeeded".
//
// Never: the webhook will not be called more than once in a single admission evaluation.
//
// IfNeeded: the webhook will be called at least one additional time as part of the admission evaluation
// if the object being admitted is modified by other admission plugins after the initial webhook call.
// Webhooks that specify this option *must* be idempotent, able to process objects they previously admitted.
// Note:
// * the number of additional invocations is not guaranteed to be exactly one.
// * if additional invocations result in further modifications to the object, webhooks are not guaranteed to be invoked again.
// * webhooks that use this option may be reordered to minimize the number of additional invocations.
// * to validate an object after all mutations are guaranteed complete, use a validating admission webhook instead.
//
// Defaults to "Never".
// +optional
ReinvocationPolicy *ReinvocationPolicyType `json:"reinvocationPolicy,omitempty" protobuf:"bytes,10,opt,name=reinvocationPolicy,casttype=ReinvocationPolicyType"`
}

// ReinvocationPolicyType specifies what type of policy the admission hook uses.
type ReinvocationPolicyType string

const (
// NeverReinvocationPolicy indicates that the webhook must not be called more than once in a
// single admission evaluation.
NeverReinvocationPolicy ReinvocationPolicyType = "Never"
// IfNeededReinvocationPolicy indicates that the webhook may be called at least one
// additional time as part of the admission evaluation if the object being admitted is
// modified by other admission plugins after the initial webhook call.
IfNeededReinvocationPolicy ReinvocationPolicyType = "IfNeeded"
)

// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
// sure that all the tuple expansions are valid.
type RuleWithOperations struct {
Expand Down
1 change: 1 addition & 0 deletions staging/src/k8s.io/apiserver/pkg/admission/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ go_library(
"handler.go",
"interfaces.go",
"plugins.go",
"reinvocation.go",
"util.go",
],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission",
Expand Down
65 changes: 54 additions & 11 deletions staging/src/k8s.io/apiserver/pkg/admission/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,24 @@ type attributesRecord struct {
// But ValidatingAdmissionWebhook add annotations concurrently.
annotations map[string]string
annotationsLock sync.RWMutex

reinvocationContext ReinvocationContext
}

func NewAttributesRecord(object runtime.Object, oldObject runtime.Object, kind schema.GroupVersionKind, namespace, name string, resource schema.GroupVersionResource, subresource string, operation Operation, operationOptions runtime.Object, dryRun bool, userInfo user.Info) Attributes {
return &attributesRecord{
kind: kind,
namespace: namespace,
name: name,
resource: resource,
subresource: subresource,
operation: operation,
options: operationOptions,
dryRun: dryRun,
object: object,
oldObject: oldObject,
userInfo: userInfo,
kind: kind,
namespace: namespace,
name: name,
resource: resource,
subresource: subresource,
operation: operation,
options: operationOptions,
dryRun: dryRun,
object: object,
oldObject: oldObject,
userInfo: userInfo,
reinvocationContext: &reinvocationContext{},
}
}

Expand Down Expand Up @@ -140,6 +143,46 @@ func (record *attributesRecord) AddAnnotation(key, value string) error {
return nil
}

func (record *attributesRecord) GetReinvocationContext() ReinvocationContext {
return record.reinvocationContext
}

type reinvocationContext struct {
// isReinvoke is true when admission plugins are being reinvoked
isReinvoke bool
// reinvokeRequested is true when an admission plugin requested a re-invocation of the chain
reinvokeRequested bool
// values stores reinvoke context values per plugin.
values map[string]interface{}
}

func (rc *reinvocationContext) IsReinvoke() bool {
return rc.isReinvoke
}

func (rc *reinvocationContext) SetIsReinvoke() {
rc.isReinvoke = true
}

func (rc *reinvocationContext) ShouldReinvoke() bool {
return rc.reinvokeRequested
}

func (rc *reinvocationContext) SetShouldReinvoke() {
rc.reinvokeRequested = true
}

func (rc *reinvocationContext) SetValue(plugin string, v interface{}) {
if rc.values == nil {
rc.values = map[string]interface{}{}
}
rc.values[plugin] = v
}

func (rc *reinvocationContext) Value(plugin string) interface{} {
return rc.values[plugin]
}

func checkKeyFormat(key string) error {
parts := strings.Split(key, "/")
if len(parts) != 2 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,14 @@ func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhoo
sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
accessors := []webhook.WebhookAccessor{}
for _, c := range configurations {
// webhook names are not validated for uniqueness, so we check for duplicates and
// add a int suffix to distinguish between them
names := map[string]int{}
for i := range c.Webhooks {
accessors = append(accessors, webhook.NewMutatingWebhookAccessor(&c.Webhooks[i]))
n := c.Webhooks[i].Name
uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
names[n]++
accessors = append(accessors, webhook.NewMutatingWebhookAccessor(uid, &c.Webhooks[i]))
}
}
return accessors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,14 @@ func mergeValidatingWebhookConfigurations(configurations []*v1beta1.ValidatingWe
sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
accessors := []webhook.WebhookAccessor{}
for _, c := range configurations {
// webhook names are not validated for uniqueness, so we check for duplicates and
// add a int suffix to distinguish between them
names := map[string]int{}
for i := range c.Webhooks {
accessors = append(accessors, webhook.NewValidatingWebhookAccessor(&c.Webhooks[i]))
n := c.Webhooks[i].Name
uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
names[n]++
accessors = append(accessors, webhook.NewValidatingWebhookAccessor(uid, &c.Webhooks[i]))
}
}
return accessors
Expand Down
19 changes: 19 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/admission/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ type Attributes interface {
// An error is returned if the format of key is invalid. When trying to overwrite annotation with a new value, an error is returned.
// Both ValidationInterface and MutationInterface are allowed to add Annotations.
AddAnnotation(key, value string) error

// GetReinvocationContext tracks the admission request information relevant to the re-invocation policy.
GetReinvocationContext() ReinvocationContext
}

// ObjectInterfaces is an interface used by AdmissionController to get object interfaces
Expand Down Expand Up @@ -91,6 +94,22 @@ type AnnotationsGetter interface {
GetAnnotations() map[string]string
}

// ReinvocationContext provides access to the admission related state required to implement the re-invocation policy.
type ReinvocationContext interface {
// IsReinvoke returns true if the current admission check is a re-invocation.
IsReinvoke() bool
// SetIsReinvoke sets the current admission check as a re-invocation.
SetIsReinvoke()
// ShouldReinvoke returns true if any plugin has requested a re-invocation.
ShouldReinvoke() bool
// SetShouldReinvoke signals that a re-invocation is desired.
SetShouldReinvoke()
// AddValue set a value for a plugin name, possibly overriding a previous value.
SetValue(plugin string, v interface{})
// Value reads a value for a webhook.
Value(plugin string) interface{}
}

// Interface is an abstract, pluggable interface for Admission Control decisions.
type Interface interface {
// Handles returns true if this admission controller can handle the given operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ import (

// WebhookAccessor provides a common interface to both mutating and validating webhook types.
type WebhookAccessor interface {
// GetName gets the webhook Name field.
// GetUID gets a string that uniquely identifies the webhook.
GetUID() string

// GetName gets the webhook Name field. Note that the name is scoped to the webhook
// configuration and does not provide a globally unique identity, if a unique identity is
// needed, use GetUID.
GetName() string
// GetClientConfig gets the webhook ClientConfig field.
GetClientConfig() v1beta1.WebhookClientConfig
Expand All @@ -49,14 +54,18 @@ type WebhookAccessor interface {
}

// NewMutatingWebhookAccessor creates an accessor for a MutatingWebhook.
func NewMutatingWebhookAccessor(h *v1beta1.MutatingWebhook) WebhookAccessor {
return mutatingWebhookAccessor{h}
func NewMutatingWebhookAccessor(uid string, h *v1beta1.MutatingWebhook) WebhookAccessor {
return mutatingWebhookAccessor{uid: uid, MutatingWebhook: h}
}

type mutatingWebhookAccessor struct {
*v1beta1.MutatingWebhook
uid string
}

func (m mutatingWebhookAccessor) GetUID() string {
return m.Name
}
func (m mutatingWebhookAccessor) GetName() string {
return m.Name
}
Expand Down Expand Up @@ -94,14 +103,18 @@ func (m mutatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebh
}

// NewValidatingWebhookAccessor creates an accessor for a ValidatingWebhook.
func NewValidatingWebhookAccessor(h *v1beta1.ValidatingWebhook) WebhookAccessor {
return validatingWebhookAccessor{h}
func NewValidatingWebhookAccessor(uid string, h *v1beta1.ValidatingWebhook) WebhookAccessor {
return validatingWebhookAccessor{uid: uid, ValidatingWebhook: h}
}

type validatingWebhookAccessor struct {
*v1beta1.ValidatingWebhook
uid string
}

func (v validatingWebhookAccessor) GetUID() string {
return v.uid
}
func (v validatingWebhookAccessor) GetName() string {
return v.Name
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package generic

import (
"fmt"
"strings"
"testing"

Expand Down Expand Up @@ -277,9 +278,9 @@ func TestShouldCallHook(t *testing.T) {
},
}

for _, testcase := range testcases {
for i, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
invocation, err := a.shouldCallHook(webhook.NewValidatingWebhookAccessor(testcase.webhook), testcase.attrs, interfaces)
invocation, err := a.shouldCallHook(webhook.NewValidatingWebhookAccessor(fmt.Sprintf("webhook-%d", i), testcase.webhook), testcase.attrs, interfaces)
if err != nil {
if len(testcase.expectErr) == 0 {
t.Fatal(err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ go_library(
"dispatcher.go",
"doc.go",
"plugin.go",
"reinvocationcontext.go",
],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/api/admission/v1beta1:go_default_library",
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/configuration:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/metrics:go_default_library",
Expand Down
Loading

0 comments on commit 95fa928

Please sign in to comment.