mirror of https://github.com/k3s-io/k3s
Add mutating admission webhook reinvocation
parent
939a04f0ed
commit
95fa928ecb
|
@ -55,6 +55,8 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||||
obj.MatchPolicy = &m
|
obj.MatchPolicy = &m
|
||||||
s := admissionregistration.SideEffectClassUnknown
|
s := admissionregistration.SideEffectClassUnknown
|
||||||
obj.SideEffects = &s
|
obj.SideEffects = &s
|
||||||
|
n := admissionregistration.NeverReinvocationPolicy
|
||||||
|
obj.ReinvocationPolicy = &n
|
||||||
if obj.TimeoutSeconds == nil {
|
if obj.TimeoutSeconds == nil {
|
||||||
i := int32(30)
|
i := int32(30)
|
||||||
obj.TimeoutSeconds = &i
|
obj.TimeoutSeconds = &i
|
||||||
|
|
|
@ -383,8 +383,39 @@ type MutatingWebhook struct {
|
||||||
// does not understand, calls to the webhook will fail and be subject to the failure policy.
|
// does not understand, calls to the webhook will fail and be subject to the failure policy.
|
||||||
// +optional
|
// +optional
|
||||||
AdmissionReviewVersions []string
|
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
|
// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
|
||||||
// sure that all the tuple expansions are valid.
|
// sure that all the tuple expansions are valid.
|
||||||
type RuleWithOperations struct {
|
type RuleWithOperations struct {
|
||||||
|
|
|
@ -77,6 +77,10 @@ func SetDefaults_MutatingWebhook(obj *admissionregistrationv1beta1.MutatingWebho
|
||||||
obj.TimeoutSeconds = new(int32)
|
obj.TimeoutSeconds = new(int32)
|
||||||
*obj.TimeoutSeconds = 30
|
*obj.TimeoutSeconds = 30
|
||||||
}
|
}
|
||||||
|
if obj.ReinvocationPolicy == nil {
|
||||||
|
never := admissionregistrationv1beta1.NeverReinvocationPolicy
|
||||||
|
obj.ReinvocationPolicy = &never
|
||||||
|
}
|
||||||
|
|
||||||
if len(obj.AdmissionReviewVersions) == 0 {
|
if len(obj.AdmissionReviewVersions) == 0 {
|
||||||
obj.AdmissionReviewVersions = []string{admissionregistrationv1beta1.SchemeGroupVersion.Version}
|
obj.AdmissionReviewVersions = []string{admissionregistrationv1beta1.SchemeGroupVersion.Version}
|
||||||
|
|
|
@ -281,6 +281,9 @@ func validateMutatingWebhook(hook *admissionregistration.MutatingWebhook, fldPat
|
||||||
if hook.NamespaceSelector != nil {
|
if hook.NamespaceSelector != nil {
|
||||||
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
|
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
|
cc := hook.ClientConfig
|
||||||
switch {
|
switch {
|
||||||
|
@ -319,6 +322,11 @@ var supportedOperations = sets.NewString(
|
||||||
string(admissionregistration.Connect),
|
string(admissionregistration.Connect),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var supportedReinvocationPolicies = sets.NewString(
|
||||||
|
string(admissionregistration.NeverReinvocationPolicy),
|
||||||
|
string(admissionregistration.IfNeededReinvocationPolicy),
|
||||||
|
)
|
||||||
|
|
||||||
func hasWildcardOperation(operations []admissionregistration.OperationType) bool {
|
func hasWildcardOperation(operations []admissionregistration.OperationType) bool {
|
||||||
for _, o := range operations {
|
for _, o := range operations {
|
||||||
if o == admissionregistration.OperationAll {
|
if o == admissionregistration.OperationAll {
|
||||||
|
|
|
@ -404,8 +404,39 @@ type MutatingWebhook struct {
|
||||||
// Default to `['v1beta1']`.
|
// Default to `['v1beta1']`.
|
||||||
// +optional
|
// +optional
|
||||||
AdmissionReviewVersions []string `json:"admissionReviewVersions,omitempty" protobuf:"bytes,8,rep,name=admissionReviewVersions"`
|
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
|
// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
|
||||||
// sure that all the tuple expansions are valid.
|
// sure that all the tuple expansions are valid.
|
||||||
type RuleWithOperations struct {
|
type RuleWithOperations struct {
|
||||||
|
|
|
@ -42,6 +42,7 @@ go_library(
|
||||||
"handler.go",
|
"handler.go",
|
||||||
"interfaces.go",
|
"interfaces.go",
|
||||||
"plugins.go",
|
"plugins.go",
|
||||||
|
"reinvocation.go",
|
||||||
"util.go",
|
"util.go",
|
||||||
],
|
],
|
||||||
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission",
|
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission",
|
||||||
|
|
|
@ -44,21 +44,24 @@ type attributesRecord struct {
|
||||||
// But ValidatingAdmissionWebhook add annotations concurrently.
|
// But ValidatingAdmissionWebhook add annotations concurrently.
|
||||||
annotations map[string]string
|
annotations map[string]string
|
||||||
annotationsLock sync.RWMutex
|
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 {
|
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{
|
return &attributesRecord{
|
||||||
kind: kind,
|
kind: kind,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
name: name,
|
name: name,
|
||||||
resource: resource,
|
resource: resource,
|
||||||
subresource: subresource,
|
subresource: subresource,
|
||||||
operation: operation,
|
operation: operation,
|
||||||
options: operationOptions,
|
options: operationOptions,
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
object: object,
|
object: object,
|
||||||
oldObject: oldObject,
|
oldObject: oldObject,
|
||||||
userInfo: userInfo,
|
userInfo: userInfo,
|
||||||
|
reinvocationContext: &reinvocationContext{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,6 +143,46 @@ func (record *attributesRecord) AddAnnotation(key, value string) error {
|
||||||
return nil
|
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 {
|
func checkKeyFormat(key string) error {
|
||||||
parts := strings.Split(key, "/")
|
parts := strings.Split(key, "/")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
|
|
|
@ -86,8 +86,14 @@ func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhoo
|
||||||
sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
|
sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
|
||||||
accessors := []webhook.WebhookAccessor{}
|
accessors := []webhook.WebhookAccessor{}
|
||||||
for _, c := range configurations {
|
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 {
|
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
|
return accessors
|
||||||
|
|
|
@ -84,8 +84,14 @@ func mergeValidatingWebhookConfigurations(configurations []*v1beta1.ValidatingWe
|
||||||
sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
|
sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
|
||||||
accessors := []webhook.WebhookAccessor{}
|
accessors := []webhook.WebhookAccessor{}
|
||||||
for _, c := range configurations {
|
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 {
|
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
|
return accessors
|
||||||
|
|
|
@ -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.
|
// 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.
|
// Both ValidationInterface and MutationInterface are allowed to add Annotations.
|
||||||
AddAnnotation(key, value string) error
|
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
|
// ObjectInterfaces is an interface used by AdmissionController to get object interfaces
|
||||||
|
@ -91,6 +94,22 @@ type AnnotationsGetter interface {
|
||||||
GetAnnotations() map[string]string
|
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.
|
// Interface is an abstract, pluggable interface for Admission Control decisions.
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
// Handles returns true if this admission controller can handle the given operation
|
// Handles returns true if this admission controller can handle the given operation
|
||||||
|
|
|
@ -23,7 +23,12 @@ import (
|
||||||
|
|
||||||
// WebhookAccessor provides a common interface to both mutating and validating webhook types.
|
// WebhookAccessor provides a common interface to both mutating and validating webhook types.
|
||||||
type WebhookAccessor interface {
|
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
|
GetName() string
|
||||||
// GetClientConfig gets the webhook ClientConfig field.
|
// GetClientConfig gets the webhook ClientConfig field.
|
||||||
GetClientConfig() v1beta1.WebhookClientConfig
|
GetClientConfig() v1beta1.WebhookClientConfig
|
||||||
|
@ -49,14 +54,18 @@ type WebhookAccessor interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMutatingWebhookAccessor creates an accessor for a MutatingWebhook.
|
// NewMutatingWebhookAccessor creates an accessor for a MutatingWebhook.
|
||||||
func NewMutatingWebhookAccessor(h *v1beta1.MutatingWebhook) WebhookAccessor {
|
func NewMutatingWebhookAccessor(uid string, h *v1beta1.MutatingWebhook) WebhookAccessor {
|
||||||
return mutatingWebhookAccessor{h}
|
return mutatingWebhookAccessor{uid: uid, MutatingWebhook: h}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mutatingWebhookAccessor struct {
|
type mutatingWebhookAccessor struct {
|
||||||
*v1beta1.MutatingWebhook
|
*v1beta1.MutatingWebhook
|
||||||
|
uid string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m mutatingWebhookAccessor) GetUID() string {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
func (m mutatingWebhookAccessor) GetName() string {
|
func (m mutatingWebhookAccessor) GetName() string {
|
||||||
return m.Name
|
return m.Name
|
||||||
}
|
}
|
||||||
|
@ -94,14 +103,18 @@ func (m mutatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebh
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewValidatingWebhookAccessor creates an accessor for a ValidatingWebhook.
|
// NewValidatingWebhookAccessor creates an accessor for a ValidatingWebhook.
|
||||||
func NewValidatingWebhookAccessor(h *v1beta1.ValidatingWebhook) WebhookAccessor {
|
func NewValidatingWebhookAccessor(uid string, h *v1beta1.ValidatingWebhook) WebhookAccessor {
|
||||||
return validatingWebhookAccessor{h}
|
return validatingWebhookAccessor{uid: uid, ValidatingWebhook: h}
|
||||||
}
|
}
|
||||||
|
|
||||||
type validatingWebhookAccessor struct {
|
type validatingWebhookAccessor struct {
|
||||||
*v1beta1.ValidatingWebhook
|
*v1beta1.ValidatingWebhook
|
||||||
|
uid string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v validatingWebhookAccessor) GetUID() string {
|
||||||
|
return v.uid
|
||||||
|
}
|
||||||
func (v validatingWebhookAccessor) GetName() string {
|
func (v validatingWebhookAccessor) GetName() string {
|
||||||
return v.Name
|
return v.Name
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package generic
|
package generic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -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) {
|
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 err != nil {
|
||||||
if len(testcase.expectErr) == 0 {
|
if len(testcase.expectErr) == 0 {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -6,6 +6,7 @@ go_library(
|
||||||
"dispatcher.go",
|
"dispatcher.go",
|
||||||
"doc.go",
|
"doc.go",
|
||||||
"plugin.go",
|
"plugin.go",
|
||||||
|
"reinvocationcontext.go",
|
||||||
],
|
],
|
||||||
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
|
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
|
||||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
|
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
|
||||||
|
@ -13,11 +14,13 @@ go_library(
|
||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/api/admission/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/admission/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/admissionregistration/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/api/errors:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured: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:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json: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/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:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/admission/configuration: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",
|
"//staging/src/k8s.io/apiserver/pkg/admission/metrics:go_default_library",
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jsonpatch "github.com/evanphx/json-patch"
|
jsonpatch "github.com/evanphx/json-patch"
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
"k8s.io/klog"
|
"k8s.io/klog"
|
||||||
|
|
||||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||||
|
@ -56,12 +57,32 @@ func newMutatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generi
|
||||||
var _ generic.Dispatcher = &mutatingDispatcher{}
|
var _ generic.Dispatcher = &mutatingDispatcher{}
|
||||||
|
|
||||||
func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, relevantHooks []*generic.WebhookInvocation) error {
|
func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, relevantHooks []*generic.WebhookInvocation) error {
|
||||||
|
reinvokeCtx := attr.GetReinvocationContext()
|
||||||
|
var webhookReinvokeCtx *webhookReinvokeContext
|
||||||
|
if v := reinvokeCtx.Value(PluginName); v != nil {
|
||||||
|
webhookReinvokeCtx = v.(*webhookReinvokeContext)
|
||||||
|
} else {
|
||||||
|
webhookReinvokeCtx = &webhookReinvokeContext{}
|
||||||
|
reinvokeCtx.SetValue(PluginName, webhookReinvokeCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reinvokeCtx.IsReinvoke() && webhookReinvokeCtx.IsOutputChangedSinceLastWebhookInvocation(attr.GetObject()) {
|
||||||
|
// If the object has changed, we know the in-tree plugin re-invocations have mutated the object,
|
||||||
|
// and we need to reinvoke all eligible webhooks.
|
||||||
|
webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
|
||||||
|
}()
|
||||||
var versionedAttr *generic.VersionedAttributes
|
var versionedAttr *generic.VersionedAttributes
|
||||||
for _, invocation := range relevantHooks {
|
for _, invocation := range relevantHooks {
|
||||||
hook, ok := invocation.Webhook.GetMutatingWebhook()
|
hook, ok := invocation.Webhook.GetMutatingWebhook()
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("mutating webhook dispatch requires v1beta1.MutatingWebhook, but got %T", hook)
|
return fmt.Errorf("mutating webhook dispatch requires v1beta1.MutatingWebhook, but got %T", hook)
|
||||||
}
|
}
|
||||||
|
if reinvokeCtx.IsReinvoke() && !webhookReinvokeCtx.ShouldReinvokeWebhook(invocation.Webhook.GetUID()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if versionedAttr == nil {
|
if versionedAttr == nil {
|
||||||
// First webhook, create versioned attributes
|
// First webhook, create versioned attributes
|
||||||
var err error
|
var err error
|
||||||
|
@ -76,8 +97,17 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
|
||||||
}
|
}
|
||||||
|
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o)
|
|
||||||
|
changed, err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o)
|
||||||
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "admit", hook.Name)
|
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "admit", hook.Name)
|
||||||
|
if changed {
|
||||||
|
// Patch had changed the object. Prepare to reinvoke all previous webhooks that are eligible for re-invocation.
|
||||||
|
webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||||
|
reinvokeCtx.SetShouldReinvoke()
|
||||||
|
}
|
||||||
|
if hook.ReinvocationPolicy != nil && *hook.ReinvocationPolicy == v1beta1.IfNeededReinvocationPolicy {
|
||||||
|
webhookReinvokeCtx.AddReinvocableWebhookToPreviouslyInvoked(invocation.Webhook.GetUID())
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -99,32 +129,33 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
|
||||||
if versionedAttr != nil && versionedAttr.VersionedObject != nil && versionedAttr.Dirty {
|
if versionedAttr != nil && versionedAttr.VersionedObject != nil && versionedAttr.Dirty {
|
||||||
return o.GetObjectConvertor().Convert(versionedAttr.VersionedObject, versionedAttr.Attributes.GetObject(), nil)
|
return o.GetObjectConvertor().Convert(versionedAttr.VersionedObject, versionedAttr.Attributes.GetObject(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// note that callAttrMutatingHook updates attr
|
// note that callAttrMutatingHook updates attr
|
||||||
|
|
||||||
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) error {
|
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) (bool, error) {
|
||||||
if attr.Attributes.IsDryRun() {
|
if attr.Attributes.IsDryRun() {
|
||||||
if h.SideEffects == nil {
|
if h.SideEffects == nil {
|
||||||
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
|
||||||
}
|
}
|
||||||
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
|
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
|
||||||
return webhookerrors.NewDryRunUnsupportedErr(h.Name)
|
return false, webhookerrors.NewDryRunUnsupportedErr(h.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently dispatcher only supports `v1beta1` AdmissionReview
|
// Currently dispatcher only supports `v1beta1` AdmissionReview
|
||||||
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
|
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
|
||||||
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
|
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
|
||||||
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the webhook request
|
// Make the webhook request
|
||||||
request := request.CreateAdmissionReview(attr, invocation)
|
request := request.CreateAdmissionReview(attr, invocation)
|
||||||
client, err := a.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
|
client, err := a.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||||
}
|
}
|
||||||
response := &admissionv1beta1.AdmissionReview{}
|
response := &admissionv1beta1.AdmissionReview{}
|
||||||
r := client.Post().Context(ctx).Body(&request)
|
r := client.Post().Context(ctx).Body(&request)
|
||||||
|
@ -132,11 +163,11 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
|
||||||
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
|
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
|
||||||
}
|
}
|
||||||
if err := r.Do().Into(response); err != nil {
|
if err := r.Do().Into(response); err != nil {
|
||||||
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.Response == nil {
|
if response.Response == nil {
|
||||||
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range response.Response.AuditAnnotations {
|
for k, v := range response.Response.AuditAnnotations {
|
||||||
|
@ -147,34 +178,34 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
|
||||||
}
|
}
|
||||||
|
|
||||||
if !response.Response.Allowed {
|
if !response.Response.Allowed {
|
||||||
return webhookerrors.ToStatusErr(h.Name, response.Response.Result)
|
return false, webhookerrors.ToStatusErr(h.Name, response.Response.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
patchJS := response.Response.Patch
|
patchJS := response.Response.Patch
|
||||||
if len(patchJS) == 0 {
|
if len(patchJS) == 0 {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
patchObj, err := jsonpatch.DecodePatch(patchJS)
|
patchObj, err := jsonpatch.DecodePatch(patchJS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
if len(patchObj) == 0 {
|
if len(patchObj) == 0 {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if a non-empty patch was provided, and we have no object we can apply it to (e.g. a DELETE admission operation), error
|
// if a non-empty patch was provided, and we have no object we can apply it to (e.g. a DELETE admission operation), error
|
||||||
if attr.VersionedObject == nil {
|
if attr.VersionedObject == nil {
|
||||||
return apierrors.NewInternalError(fmt.Errorf("admission webhook %q attempted to modify the object, which is not supported for this operation", h.Name))
|
return false, apierrors.NewInternalError(fmt.Errorf("admission webhook %q attempted to modify the object, which is not supported for this operation", h.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false)
|
jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false)
|
||||||
objJS, err := runtime.Encode(jsonSerializer, attr.VersionedObject)
|
objJS, err := runtime.Encode(jsonSerializer, attr.VersionedObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
patchedJS, err := patchObj.Apply(objJS)
|
patchedJS, err := patchObj.Apply(objJS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var newVersionedObject runtime.Object
|
var newVersionedObject runtime.Object
|
||||||
|
@ -185,16 +216,20 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
|
||||||
} else {
|
} else {
|
||||||
newVersionedObject, err = o.GetObjectCreater().New(attr.VersionedKind)
|
newVersionedObject, err = o.GetObjectCreater().New(attr.VersionedKind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: if we have multiple mutating webhooks, we can remember the json
|
// TODO: if we have multiple mutating webhooks, we can remember the json
|
||||||
// instead of encoding and decoding for each one.
|
// instead of encoding and decoding for each one.
|
||||||
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
|
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changed := !apiequality.Semantic.DeepEqual(attr.VersionedObject, newVersionedObject)
|
||||||
|
|
||||||
attr.Dirty = true
|
attr.Dirty = true
|
||||||
attr.VersionedObject = newVersionedObject
|
attr.VersionedObject = newVersionedObject
|
||||||
o.GetObjectDefaulter().Default(attr.VersionedObject)
|
o.GetObjectDefaulter().Default(attr.VersionedObject)
|
||||||
return nil
|
return changed, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,67 +49,77 @@ func TestAdmit(t *testing.T) {
|
||||||
webhooktesting.ConvertToMutatingTestCases(webhooktesting.NewNonMutatingTestCases(serverURL))...)
|
webhooktesting.ConvertToMutatingTestCases(webhooktesting.NewNonMutatingTestCases(serverURL))...)
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
wh, err := NewMutatingWebhook(nil)
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
if err != nil {
|
wh, err := NewMutatingWebhook(nil)
|
||||||
t.Errorf("%s: failed to create mutating webhook: %v", tt.Name, err)
|
if err != nil {
|
||||||
continue
|
t.Errorf("failed to create mutating webhook: %v", err)
|
||||||
}
|
return
|
||||||
|
|
||||||
ns := "webhook-test"
|
|
||||||
client, informer := webhooktesting.NewFakeMutatingDataSource(ns, tt.Webhooks, stopCh)
|
|
||||||
|
|
||||||
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
|
|
||||||
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
|
|
||||||
wh.SetExternalKubeClientSet(client)
|
|
||||||
wh.SetExternalKubeInformerFactory(informer)
|
|
||||||
|
|
||||||
informer.Start(stopCh)
|
|
||||||
informer.WaitForCacheSync(stopCh)
|
|
||||||
|
|
||||||
if err = wh.ValidateInitialization(); err != nil {
|
|
||||||
t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var attr admission.Attributes
|
|
||||||
if tt.IsCRD {
|
|
||||||
attr = webhooktesting.NewAttributeUnstructured(ns, tt.AdditionalLabels, tt.IsDryRun)
|
|
||||||
} else {
|
|
||||||
attr = webhooktesting.NewAttribute(ns, tt.AdditionalLabels, tt.IsDryRun)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wh.Admit(attr, objectInterfaces)
|
|
||||||
if tt.ExpectAllow != (err == nil) {
|
|
||||||
t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
|
|
||||||
}
|
|
||||||
if tt.ExpectLabels != nil {
|
|
||||||
if !reflect.DeepEqual(tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels()) {
|
|
||||||
t.Errorf("%s: expected labels '%v', but got '%v'", tt.Name, tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// ErrWebhookRejected is not an error for our purposes
|
ns := "webhook-test"
|
||||||
if tt.ErrorContains != "" {
|
client, informer := webhooktesting.NewFakeMutatingDataSource(ns, tt.Webhooks, stopCh)
|
||||||
if err == nil || !strings.Contains(err.Error(), tt.ErrorContains) {
|
|
||||||
t.Errorf("%s: expected an error saying %q, but got: %v", tt.Name, tt.ErrorContains, err)
|
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
|
||||||
|
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
|
||||||
|
wh.SetExternalKubeClientSet(client)
|
||||||
|
wh.SetExternalKubeInformerFactory(informer)
|
||||||
|
|
||||||
|
informer.Start(stopCh)
|
||||||
|
informer.WaitForCacheSync(stopCh)
|
||||||
|
|
||||||
|
if err = wh.ValidateInitialization(); err != nil {
|
||||||
|
t.Errorf("failed to validate initialization: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if statusErr, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
|
var attr admission.Attributes
|
||||||
t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
|
if tt.IsCRD {
|
||||||
} else if isStatusErr {
|
attr = webhooktesting.NewAttributeUnstructured(ns, tt.AdditionalLabels, tt.IsDryRun)
|
||||||
if statusErr.ErrStatus.Code != tt.ExpectStatusCode {
|
} else {
|
||||||
t.Errorf("%s: expected status code %d, got %d", tt.Name, tt.ExpectStatusCode, statusErr.ErrStatus.Code)
|
attr = webhooktesting.NewAttribute(ns, tt.AdditionalLabels, tt.IsDryRun)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
|
err = wh.Admit(attr, objectInterfaces)
|
||||||
if !ok {
|
if tt.ExpectAllow != (err == nil) {
|
||||||
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
|
t.Errorf("expected allowed=%v, but got err=%v", tt.ExpectAllow, err)
|
||||||
continue
|
}
|
||||||
}
|
if tt.ExpectLabels != nil {
|
||||||
if len(tt.ExpectAnnotations) == 0 {
|
if !reflect.DeepEqual(tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels()) {
|
||||||
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
|
t.Errorf("expected labels '%v', but got '%v'", tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels())
|
||||||
} else {
|
}
|
||||||
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
|
}
|
||||||
}
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if statusErr, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
|
||||||
|
t.Errorf("expected a StatusError, got %T", err)
|
||||||
|
} else if isStatusErr {
|
||||||
|
if statusErr.ErrStatus.Code != tt.ExpectStatusCode {
|
||||||
|
t.Errorf("expected status code %d, got %d", tt.ExpectStatusCode, statusErr.ErrStatus.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(tt.ExpectAnnotations) == 0 {
|
||||||
|
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
|
||||||
|
}
|
||||||
|
reinvocationCtx := fakeAttr.Attributes.GetReinvocationContext()
|
||||||
|
reinvocationCtx.SetIsReinvoke()
|
||||||
|
for webhook, expectReinvoke := range tt.ExpectReinvokeWebhooks {
|
||||||
|
shouldReinvoke := reinvocationCtx.Value(PluginName).(*webhookReinvokeContext).ShouldReinvokeWebhook(webhook)
|
||||||
|
if expectReinvoke != shouldReinvoke {
|
||||||
|
t.Errorf("expected reinvocationContext.ShouldReinvokeWebhook(%s)=%t, but got %t", webhook, expectReinvoke, shouldReinvoke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 mutating
|
||||||
|
|
||||||
|
import (
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webhookReinvokeContext struct {
|
||||||
|
// lastWebhookOutput holds the result of the last webhook admission plugin call
|
||||||
|
lastWebhookOutput runtime.Object
|
||||||
|
// previouslyInvokedReinvocableWebhooks holds the set of webhooks that have been invoked and
|
||||||
|
// should be reinvoked if a later mutation occurs
|
||||||
|
previouslyInvokedReinvocableWebhooks sets.String
|
||||||
|
// reinvokeWebhooks holds the set of webhooks that should be reinvoked
|
||||||
|
reinvokeWebhooks sets.String
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) ShouldReinvokeWebhook(webhook string) bool {
|
||||||
|
return rc.reinvokeWebhooks.Has(webhook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) IsOutputChangedSinceLastWebhookInvocation(object runtime.Object) bool {
|
||||||
|
return !apiequality.Semantic.DeepEqual(rc.lastWebhookOutput, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) SetLastWebhookInvocationOutput(object runtime.Object) {
|
||||||
|
if object == nil {
|
||||||
|
rc.lastWebhookOutput = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rc.lastWebhookOutput = object.DeepCopyObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) AddReinvocableWebhookToPreviouslyInvoked(webhook string) {
|
||||||
|
if rc.previouslyInvokedReinvocableWebhooks == nil {
|
||||||
|
rc.previouslyInvokedReinvocableWebhooks = sets.NewString()
|
||||||
|
}
|
||||||
|
rc.previouslyInvokedReinvocableWebhooks.Insert(webhook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() {
|
||||||
|
if len(rc.previouslyInvokedReinvocableWebhooks) > 0 {
|
||||||
|
if rc.reinvokeWebhooks == nil {
|
||||||
|
rc.reinvokeWebhooks = sets.NewString()
|
||||||
|
}
|
||||||
|
for s := range rc.previouslyInvokedReinvocableWebhooks {
|
||||||
|
rc.reinvokeWebhooks.Insert(s)
|
||||||
|
}
|
||||||
|
rc.previouslyInvokedReinvocableWebhooks = sets.NewString()
|
||||||
|
}
|
||||||
|
}
|
|
@ -120,7 +120,7 @@ func TestNotExemptClusterScopedResource(t *testing.T) {
|
||||||
}
|
}
|
||||||
attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, &metav1.CreateOptions{}, false, nil)
|
attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||||
matcher := Matcher{}
|
matcher := Matcher{}
|
||||||
matches, err := matcher.MatchNamespaceSelector(webhook.NewValidatingWebhookAccessor(hook), attr)
|
matches, err := matcher.MatchNamespaceSelector(webhook.NewValidatingWebhookAccessor("mock-hook", hook), attr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,9 @@ var sideEffectsNone = registrationv1beta1.SideEffectClassNone
|
||||||
var sideEffectsSome = registrationv1beta1.SideEffectClassSome
|
var sideEffectsSome = registrationv1beta1.SideEffectClassSome
|
||||||
var sideEffectsNoneOnDryRun = registrationv1beta1.SideEffectClassNoneOnDryRun
|
var sideEffectsNoneOnDryRun = registrationv1beta1.SideEffectClassNoneOnDryRun
|
||||||
|
|
||||||
|
var reinvokeNever = registrationv1beta1.NeverReinvocationPolicy
|
||||||
|
var reinvokeIfNeeded = registrationv1beta1.IfNeededReinvocationPolicy
|
||||||
|
|
||||||
// NewFakeValidatingDataSource returns a mock client and informer returning the given webhooks.
|
// NewFakeValidatingDataSource returns a mock client and informer returning the given webhooks.
|
||||||
func NewFakeValidatingDataSource(name string, webhooks []registrationv1beta1.ValidatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
|
func NewFakeValidatingDataSource(name string, webhooks []registrationv1beta1.ValidatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
|
||||||
var objs = []runtime.Object{
|
var objs = []runtime.Object{
|
||||||
|
@ -199,39 +202,41 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1beta1.WebhookC
|
||||||
|
|
||||||
// ValidatingTest is a validating webhook test case.
|
// ValidatingTest is a validating webhook test case.
|
||||||
type ValidatingTest struct {
|
type ValidatingTest struct {
|
||||||
Name string
|
Name string
|
||||||
Webhooks []registrationv1beta1.ValidatingWebhook
|
Webhooks []registrationv1beta1.ValidatingWebhook
|
||||||
Path string
|
Path string
|
||||||
IsCRD bool
|
IsCRD bool
|
||||||
IsDryRun bool
|
IsDryRun bool
|
||||||
AdditionalLabels map[string]string
|
AdditionalLabels map[string]string
|
||||||
ExpectLabels map[string]string
|
ExpectLabels map[string]string
|
||||||
ExpectAllow bool
|
ExpectAllow bool
|
||||||
ErrorContains string
|
ErrorContains string
|
||||||
ExpectAnnotations map[string]string
|
ExpectAnnotations map[string]string
|
||||||
ExpectStatusCode int32
|
ExpectStatusCode int32
|
||||||
|
ExpectReinvokeWebhooks map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MutatingTest is a mutating webhook test case.
|
// MutatingTest is a mutating webhook test case.
|
||||||
type MutatingTest struct {
|
type MutatingTest struct {
|
||||||
Name string
|
Name string
|
||||||
Webhooks []registrationv1beta1.MutatingWebhook
|
Webhooks []registrationv1beta1.MutatingWebhook
|
||||||
Path string
|
Path string
|
||||||
IsCRD bool
|
IsCRD bool
|
||||||
IsDryRun bool
|
IsDryRun bool
|
||||||
AdditionalLabels map[string]string
|
AdditionalLabels map[string]string
|
||||||
ExpectLabels map[string]string
|
ExpectLabels map[string]string
|
||||||
ExpectAllow bool
|
ExpectAllow bool
|
||||||
ErrorContains string
|
ErrorContains string
|
||||||
ExpectAnnotations map[string]string
|
ExpectAnnotations map[string]string
|
||||||
ExpectStatusCode int32
|
ExpectStatusCode int32
|
||||||
|
ExpectReinvokeWebhooks map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertToMutatingTestCases converts a validating test case to a mutating one for test purposes.
|
// ConvertToMutatingTestCases converts a validating test case to a mutating one for test purposes.
|
||||||
func ConvertToMutatingTestCases(tests []ValidatingTest) []MutatingTest {
|
func ConvertToMutatingTestCases(tests []ValidatingTest) []MutatingTest {
|
||||||
r := make([]MutatingTest, len(tests))
|
r := make([]MutatingTest, len(tests))
|
||||||
for i, t := range tests {
|
for i, t := range tests {
|
||||||
r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode}
|
r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode, t.ExpectReinvokeWebhooks}
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -240,7 +245,7 @@ func ConvertToMutatingTestCases(tests []ValidatingTest) []MutatingTest {
|
||||||
func ConvertToMutatingWebhooks(webhooks []registrationv1beta1.ValidatingWebhook) []registrationv1beta1.MutatingWebhook {
|
func ConvertToMutatingWebhooks(webhooks []registrationv1beta1.ValidatingWebhook) []registrationv1beta1.MutatingWebhook {
|
||||||
mutating := make([]registrationv1beta1.MutatingWebhook, len(webhooks))
|
mutating := make([]registrationv1beta1.MutatingWebhook, len(webhooks))
|
||||||
for i, h := range webhooks {
|
for i, h := range webhooks {
|
||||||
mutating[i] = registrationv1beta1.MutatingWebhook{h.Name, h.ClientConfig, h.Rules, h.FailurePolicy, h.MatchPolicy, h.NamespaceSelector, h.SideEffects, h.TimeoutSeconds, h.AdmissionReviewVersions}
|
mutating[i] = registrationv1beta1.MutatingWebhook{h.Name, h.ClientConfig, h.Rules, h.FailurePolicy, h.MatchPolicy, h.NamespaceSelector, h.SideEffects, h.TimeoutSeconds, h.AdmissionReviewVersions, nil}
|
||||||
}
|
}
|
||||||
return mutating
|
return mutating
|
||||||
}
|
}
|
||||||
|
@ -639,6 +644,63 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
|
||||||
},
|
},
|
||||||
// No need to test everything with the url case, since only the
|
// No need to test everything with the url case, since only the
|
||||||
// connection is different.
|
// connection is different.
|
||||||
|
{
|
||||||
|
Name: "match & reinvoke if needed policy",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "addLabel",
|
||||||
|
ClientConfig: ccfgSVC("addLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
ReinvocationPolicy: &reinvokeIfNeeded,
|
||||||
|
}, {
|
||||||
|
Name: "removeLabel",
|
||||||
|
ClientConfig: ccfgSVC("removeLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
ReinvocationPolicy: &reinvokeIfNeeded,
|
||||||
|
}},
|
||||||
|
AdditionalLabels: map[string]string{"remove": "me"},
|
||||||
|
ExpectAllow: true,
|
||||||
|
ExpectReinvokeWebhooks: map[string]bool{"addLabel": true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "match & never reinvoke policy",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "addLabel",
|
||||||
|
ClientConfig: ccfgSVC("addLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
ReinvocationPolicy: &reinvokeNever,
|
||||||
|
}},
|
||||||
|
ExpectAllow: true,
|
||||||
|
ExpectReinvokeWebhooks: map[string]bool{"addLabel": false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "match & never reinvoke policy (by default)",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "addLabel",
|
||||||
|
ClientConfig: ccfgSVC("addLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}},
|
||||||
|
ExpectAllow: true,
|
||||||
|
ExpectReinvokeWebhooks: map[string]bool{"addLabel": false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "match & no reinvoke",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "noop",
|
||||||
|
ClientConfig: ccfgSVC("noop"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}},
|
||||||
|
ExpectAllow: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -138,6 +138,13 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
case "/noop":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
||||||
|
Response: &v1beta1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,7 +160,7 @@ func (ps *Plugins) NewFromPlugins(pluginNames []string, configProvider ConfigPro
|
||||||
if len(validationPlugins) != 0 {
|
if len(validationPlugins) != 0 {
|
||||||
klog.Infof("Loaded %d validating admission controller(s) successfully in the following order: %s.", len(validationPlugins), strings.Join(validationPlugins, ","))
|
klog.Infof("Loaded %d validating admission controller(s) successfully in the following order: %s.", len(validationPlugins), strings.Join(validationPlugins, ","))
|
||||||
}
|
}
|
||||||
return chainAdmissionHandler(handlers), nil
|
return newReinvocationHandler(chainAdmissionHandler(handlers)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitPlugin creates an instance of the named interface.
|
// InitPlugin creates an instance of the named interface.
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 admission
|
||||||
|
|
||||||
|
// newReinvocationHandler creates a handler that wraps the provided admission chain and reinvokes it
|
||||||
|
// if needed according to re-invocation policy of the webhooks.
|
||||||
|
func newReinvocationHandler(admissionChain Interface) Interface {
|
||||||
|
return &reinvoker{admissionChain}
|
||||||
|
}
|
||||||
|
|
||||||
|
type reinvoker struct {
|
||||||
|
admissionChain Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admit performs an admission control check using the wrapped admission chain, reinvoking the
|
||||||
|
// admission chain if needed according to the reinvocation policy. Plugins are expected to check
|
||||||
|
// the admission attributes' reinvocation context against their reinvocation policy to decide if
|
||||||
|
// they should re-run, and to update the reinvocation context if they perform any mutations.
|
||||||
|
func (r *reinvoker) Admit(a Attributes, o ObjectInterfaces) error {
|
||||||
|
if mutator, ok := r.admissionChain.(MutationInterface); ok {
|
||||||
|
err := mutator.Admit(a, o)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s := a.GetReinvocationContext()
|
||||||
|
if s.ShouldReinvoke() {
|
||||||
|
s.SetIsReinvoke()
|
||||||
|
// Calling admit a second time will reinvoke all in-tree plugins
|
||||||
|
// as well as any webhook plugins that need to be reinvoked based on the
|
||||||
|
// reinvocation policy.
|
||||||
|
return mutator.Admit(a, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs an admission control check using the wrapped admission chain, and returns immediately on first error.
|
||||||
|
func (r *reinvoker) Validate(a Attributes, o ObjectInterfaces) error {
|
||||||
|
if validator, ok := r.admissionChain.(ValidationInterface); ok {
|
||||||
|
return validator.Validate(a, o)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles will return true if any of the admission chain handlers handle the given operation.
|
||||||
|
func (r *reinvoker) Handles(operation Operation) bool {
|
||||||
|
return r.admissionChain.Handles(operation)
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ go_test(
|
||||||
"admission_test.go",
|
"admission_test.go",
|
||||||
"broken_webhook_test.go",
|
"broken_webhook_test.go",
|
||||||
"main_test.go",
|
"main_test.go",
|
||||||
|
"reinvocation_test.go",
|
||||||
],
|
],
|
||||||
rundir = ".",
|
rundir = ".",
|
||||||
tags = [
|
tags = [
|
||||||
|
@ -21,6 +22,7 @@ go_test(
|
||||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/extensions/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/extensions/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/api/policy/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/policy/v1beta1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/api/scheduling/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
|
|
@ -0,0 +1,389 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 admissionwebhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/api/admission/v1beta1"
|
||||||
|
admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||||
|
registrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
schedulingv1 "k8s.io/api/scheduling/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testReinvocationClientUsername = "webhook-reinvocation-integration-client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestWebhookReinvocationPolicy ensures that the admission webhook reinvocation policy is applied correctly.
|
||||||
|
func TestWebhookReinvocationPolicy(t *testing.T) {
|
||||||
|
reinvokeNever := registrationv1beta1.NeverReinvocationPolicy
|
||||||
|
reinvokeIfNeeded := registrationv1beta1.IfNeededReinvocationPolicy
|
||||||
|
|
||||||
|
type testWebhook struct {
|
||||||
|
path string
|
||||||
|
policy *registrationv1beta1.ReinvocationPolicyType
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
initialPriorityClass string
|
||||||
|
webhooks []testWebhook
|
||||||
|
expectLabels map[string]string
|
||||||
|
expectInvocations map[string]int
|
||||||
|
expectError bool
|
||||||
|
errorContains string
|
||||||
|
}{
|
||||||
|
{ // in-tree (mutation), webhook (no mutation), no reinvocation required
|
||||||
|
name: "no reinvocation for in-tree only mutation",
|
||||||
|
initialPriorityClass: "low-priority", // trigger initial in-tree mutation
|
||||||
|
webhooks: []testWebhook{
|
||||||
|
{path: "/noop", policy: &reinvokeIfNeeded},
|
||||||
|
},
|
||||||
|
expectInvocations: map[string]int{"/noop": 1},
|
||||||
|
},
|
||||||
|
{ // in-tree (mutation), webhook (mutation), reinvoke in-tree (no-mutation), no webhook reinvocation required
|
||||||
|
name: "no webhook reinvocation for webhook when no in-tree reinvocation mutations",
|
||||||
|
initialPriorityClass: "low-priority", // trigger initial in-tree mutation
|
||||||
|
webhooks: []testWebhook{
|
||||||
|
{path: "/addlabel", policy: &reinvokeIfNeeded},
|
||||||
|
},
|
||||||
|
expectInvocations: map[string]int{"/addlabel": 1},
|
||||||
|
},
|
||||||
|
{ // in-tree (mutation), webhook (mutation), reinvoke in-tree (mutation), webhook (no-mutation), both reinvoked
|
||||||
|
name: "webhook is reinvoked after in-tree reinvocation",
|
||||||
|
initialPriorityClass: "low-priority", // trigger initial in-tree mutation
|
||||||
|
webhooks: []testWebhook{
|
||||||
|
// Priority plugin is ordered to run before mutating webhooks
|
||||||
|
{path: "/setpriority", policy: &reinvokeIfNeeded}, // trigger in-tree reinvoke mutation
|
||||||
|
},
|
||||||
|
expectInvocations: map[string]int{"/setpriority": 2},
|
||||||
|
},
|
||||||
|
{ // in-tree (mutation), webhook A (mutation), webhook B (mutation), reinvoke in-tree (no-mutation), reinvoke webhook A (no-mutation), no reinvocation of webhook B required
|
||||||
|
name: "no reinvocation of webhook B when in-tree or prior webhook mutations",
|
||||||
|
initialPriorityClass: "low-priority", // trigger initial in-tree mutation
|
||||||
|
webhooks: []testWebhook{
|
||||||
|
{path: "/addlabel", policy: &reinvokeIfNeeded},
|
||||||
|
{path: "/conditionaladdlabel", policy: &reinvokeIfNeeded},
|
||||||
|
},
|
||||||
|
expectLabels: map[string]string{"x": "true", "a": "true", "b": "true"},
|
||||||
|
expectInvocations: map[string]int{"/addlabel": 2, "/conditionaladdlabel": 1},
|
||||||
|
},
|
||||||
|
{ // in-tree (mutation), webhook A (mutation), webhook B (mutation), reinvoke in-tree (no-mutation), reinvoke webhook A (mutation), reinvoke webhook B (mutation), both webhooks reinvoked
|
||||||
|
name: "all webhooks reinvoked when any webhook reinvocation causes mutation",
|
||||||
|
initialPriorityClass: "low-priority", // trigger initial in-tree mutation
|
||||||
|
webhooks: []testWebhook{
|
||||||
|
{path: "/settrue", policy: &reinvokeIfNeeded},
|
||||||
|
{path: "/setfalse", policy: &reinvokeIfNeeded},
|
||||||
|
},
|
||||||
|
expectLabels: map[string]string{"x": "true", "fight": "false"},
|
||||||
|
expectInvocations: map[string]int{"/settrue": 2, "/setfalse": 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid priority class set by webhook should result in error from in-tree priority plugin",
|
||||||
|
webhooks: []testWebhook{
|
||||||
|
// Priority plugin is ordered to run before mutating webhooks
|
||||||
|
{path: "/setinvalidpriority", policy: &reinvokeIfNeeded},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "no PriorityClass with name invalid was found",
|
||||||
|
expectInvocations: map[string]int{"/setinvalidpriority": 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "'reinvoke never' policy respected",
|
||||||
|
webhooks: []testWebhook{
|
||||||
|
{path: "/conditionaladdlabel", policy: &reinvokeNever},
|
||||||
|
{path: "/addlabel", policy: &reinvokeNever},
|
||||||
|
},
|
||||||
|
expectLabels: map[string]string{"x": "true", "a": "true"},
|
||||||
|
expectInvocations: map[string]int{"/conditionaladdlabel": 1, "/addlabel": 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "'reinvoke never' (by default) policy respected",
|
||||||
|
webhooks: []testWebhook{
|
||||||
|
{path: "/conditionaladdlabel", policy: nil},
|
||||||
|
{path: "/addlabel", policy: nil},
|
||||||
|
},
|
||||||
|
expectLabels: map[string]string{"x": "true", "a": "true"},
|
||||||
|
expectInvocations: map[string]int{"/conditionaladdlabel": 1, "/addlabel": 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
if !roots.AppendCertsFromPEM(localhostCert) {
|
||||||
|
t.Fatal("Failed to append Cert from PEM")
|
||||||
|
}
|
||||||
|
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to build cert with error: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := &invocationRecorder{counts: map[string]int{}}
|
||||||
|
webhookServer := httptest.NewUnstartedServer(newReinvokeWebhookHandler(recorder))
|
||||||
|
webhookServer.TLS = &tls.Config{
|
||||||
|
|
||||||
|
RootCAs: roots,
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
}
|
||||||
|
webhookServer.StartTLS()
|
||||||
|
defer webhookServer.Close()
|
||||||
|
|
||||||
|
s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
|
||||||
|
"--disable-admission-plugins=ServiceAccount",
|
||||||
|
}, framework.SharedEtcd())
|
||||||
|
defer s.TearDownFn()
|
||||||
|
|
||||||
|
// Configure a client with a distinct user name so that it is easy to distinguish requests
|
||||||
|
// made by the client from requests made by controllers. We use this to filter out requests
|
||||||
|
// before recording them to ensure we don't accidentally mistake requests from controllers
|
||||||
|
// as requests made by the client.
|
||||||
|
clientConfig := rest.CopyConfig(s.ClientConfig)
|
||||||
|
clientConfig.Impersonate.UserName = testReinvocationClientUsername
|
||||||
|
clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
|
||||||
|
client, err := clientset.NewForConfig(clientConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for priorityClass, priority := range map[string]int{"low-priority": 1, "high-priority": 10} {
|
||||||
|
_, err = client.SchedulingV1().PriorityClasses().Create(&schedulingv1.PriorityClass{ObjectMeta: metav1.ObjectMeta{Name: priorityClass}, Value: int32(priority)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
recorder.Reset()
|
||||||
|
ns := fmt.Sprintf("reinvoke-%d", i)
|
||||||
|
_, err = client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, webhook := range tt.webhooks {
|
||||||
|
defer registerWebhook(t, client, fmt.Sprintf("admission.integration.test%d", i), webhookServer.URL+webhook.path, webhook.policy)()
|
||||||
|
}
|
||||||
|
|
||||||
|
pod := &corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: ns,
|
||||||
|
Name: "labeled",
|
||||||
|
Labels: map[string]string{"x": "true"},
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []v1.Container{{
|
||||||
|
Name: "fake-name",
|
||||||
|
Image: "fakeimage",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if tt.initialPriorityClass != "" {
|
||||||
|
pod.Spec.PriorityClassName = tt.initialPriorityClass
|
||||||
|
}
|
||||||
|
obj, err := client.CoreV1().Pods(ns).Create(pod)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error but got none")
|
||||||
|
}
|
||||||
|
if tt.errorContains != "" {
|
||||||
|
if !strings.Contains(err.Error(), tt.errorContains) {
|
||||||
|
t.Errorf("expected an error saying %q, but got: %v", tt.errorContains, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectLabels != nil {
|
||||||
|
labels := obj.GetLabels()
|
||||||
|
if !reflect.DeepEqual(tt.expectLabels, labels) {
|
||||||
|
t.Errorf("expected labels '%v', but got '%v'", tt.expectLabels, labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectInvocations != nil {
|
||||||
|
for k, v := range tt.expectInvocations {
|
||||||
|
if recorder.GetCount(k) != v {
|
||||||
|
t.Errorf("expected %d invocations of %s, but got %d", v, k, recorder.GetCount(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerWebhook(t *testing.T, client clientset.Interface, name, endpoint string, reinvocationPolicy *registrationv1beta1.ReinvocationPolicyType) func() {
|
||||||
|
fail := admissionv1beta1.Fail
|
||||||
|
hook, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: name},
|
||||||
|
Webhooks: []admissionv1beta1.MutatingWebhook{{
|
||||||
|
Name: name,
|
||||||
|
ClientConfig: admissionv1beta1.WebhookClientConfig{
|
||||||
|
URL: &endpoint,
|
||||||
|
CABundle: localhostCert,
|
||||||
|
},
|
||||||
|
Rules: []admissionv1beta1.RuleWithOperations{{
|
||||||
|
Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
|
||||||
|
Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
|
||||||
|
}},
|
||||||
|
FailurePolicy: &fail,
|
||||||
|
ReinvocationPolicy: reinvocationPolicy,
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tearDown := func() {
|
||||||
|
err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(hook.GetName(), &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tearDown
|
||||||
|
}
|
||||||
|
|
||||||
|
type invocationRecorder struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
counts map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *invocationRecorder) Reset() {
|
||||||
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
i.counts = map[string]int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *invocationRecorder) GetCount(path string) int {
|
||||||
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
return i.counts[path]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *invocationRecorder) IncrementCount(path string) {
|
||||||
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
i.counts[path]++
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReinvokeWebhookHandler(recorder *invocationRecorder) http.Handler {
|
||||||
|
patch := func(w http.ResponseWriter, patch string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
pt := v1beta1.PatchTypeJSONPatch
|
||||||
|
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
||||||
|
Response: &v1beta1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
PatchType: &pt,
|
||||||
|
Patch: []byte(patch),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
allow := func(w http.ResponseWriter) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
||||||
|
Response: &v1beta1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
}
|
||||||
|
review := v1beta1.AdmissionReview{}
|
||||||
|
if err := json.Unmarshal(data, &review); err != nil {
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
}
|
||||||
|
if review.Request.UserInfo.Username != testReinvocationClientUsername {
|
||||||
|
// skip requests not originating from this integration test's client
|
||||||
|
allow(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(review.Request.Object.Raw) == 0 {
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
}
|
||||||
|
pod := &corev1.Pod{}
|
||||||
|
if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil {
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder.IncrementCount(r.URL.Path)
|
||||||
|
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/noop":
|
||||||
|
allow(w)
|
||||||
|
case "/settrue":
|
||||||
|
patch(w, `[{"op": "replace", "path": "/metadata/labels/fight", "value": "true"}]`)
|
||||||
|
case "/setfalse":
|
||||||
|
patch(w, `[{"op": "replace", "path": "/metadata/labels/fight", "value": "false"}]`)
|
||||||
|
case "/addlabel":
|
||||||
|
labels := pod.GetLabels()
|
||||||
|
if a, ok := labels["a"]; !ok || a != "true" {
|
||||||
|
patch(w, `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
allow(w)
|
||||||
|
case "/conditionaladdlabel": // if 'a' is set, set 'b' to true
|
||||||
|
labels := pod.GetLabels()
|
||||||
|
if _, ok := labels["a"]; ok {
|
||||||
|
patch(w, `[{"op": "add", "path": "/metadata/labels/b", "value": "true"}]`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
allow(w)
|
||||||
|
case "/setpriority": // sets /spec/priorityClassName to high-priority if it is not already set
|
||||||
|
if pod.Spec.PriorityClassName != "high-priority" {
|
||||||
|
if pod.Spec.Priority != nil {
|
||||||
|
patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "high-priority"},{"op": "remove", "path": "/spec/priority"}]`)
|
||||||
|
} else {
|
||||||
|
patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "high-priority"}]`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
allow(w)
|
||||||
|
case "/setinvalidpriority":
|
||||||
|
patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "invalid"}]`)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue