Unverified Commit bada1c6b authored by Kubernetes Prow Robot's avatar Kubernetes Prow Robot Committed by GitHub

Merge pull request #78505 from caesarxuchao/dynamic-object-selector

Adding ObjectSelector to admission webhooks
parents 6b6bdc76 7738c7ee
...@@ -426,6 +426,7 @@ staging/src/k8s.io/apimachinery/pkg/watch ...@@ -426,6 +426,7 @@ staging/src/k8s.io/apimachinery/pkg/watch
staging/src/k8s.io/apiserver/pkg/admission staging/src/k8s.io/apiserver/pkg/admission
staging/src/k8s.io/apiserver/pkg/admission/configuration staging/src/k8s.io/apiserver/pkg/admission/configuration
staging/src/k8s.io/apiserver/pkg/admission/initializer staging/src/k8s.io/apiserver/pkg/admission/initializer
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1 staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts
......
...@@ -33,7 +33,7 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { ...@@ -33,7 +33,7 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
obj.Scope = &s obj.Scope = &s
} }
}, },
func(obj *admissionregistration.Webhook, c fuzz.Continue) { func(obj *admissionregistration.ValidatingWebhook, c fuzz.Continue) {
c.FuzzNoCustom(obj) // fuzz self without calling this function again c.FuzzNoCustom(obj) // fuzz self without calling this function again
p := admissionregistration.FailurePolicyType("Fail") p := admissionregistration.FailurePolicyType("Fail")
obj.FailurePolicy = &p obj.FailurePolicy = &p
...@@ -47,5 +47,21 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { ...@@ -47,5 +47,21 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
} }
obj.AdmissionReviewVersions = []string{"v1beta1"} obj.AdmissionReviewVersions = []string{"v1beta1"}
}, },
func(obj *admissionregistration.MutatingWebhook, c fuzz.Continue) {
c.FuzzNoCustom(obj) // fuzz self without calling this function again
p := admissionregistration.FailurePolicyType("Fail")
obj.FailurePolicy = &p
m := admissionregistration.MatchPolicyType("Exact")
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
}
obj.AdmissionReviewVersions = []string{"v1beta1"}
},
} }
} }
...@@ -123,7 +123,7 @@ type ValidatingWebhookConfiguration struct { ...@@ -123,7 +123,7 @@ type ValidatingWebhookConfiguration struct {
metav1.ObjectMeta metav1.ObjectMeta
// Webhooks is a list of webhooks and the affected resources and operations. // Webhooks is a list of webhooks and the affected resources and operations.
// +optional // +optional
Webhooks []Webhook Webhooks []ValidatingWebhook
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
...@@ -149,7 +149,7 @@ type MutatingWebhookConfiguration struct { ...@@ -149,7 +149,7 @@ type MutatingWebhookConfiguration struct {
metav1.ObjectMeta metav1.ObjectMeta
// Webhooks is a list of webhooks and the affected resources and operations. // Webhooks is a list of webhooks and the affected resources and operations.
// +optional // +optional
Webhooks []Webhook Webhooks []MutatingWebhook
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
...@@ -165,8 +165,8 @@ type MutatingWebhookConfigurationList struct { ...@@ -165,8 +165,8 @@ type MutatingWebhookConfigurationList struct {
Items []MutatingWebhookConfiguration Items []MutatingWebhookConfiguration
} }
// Webhook describes an admission webhook and the resources and operations it applies to. // ValidatingWebhook describes an admission webhook and the resources and operations it applies to.
type Webhook struct { type ValidatingWebhook struct {
// The name of the admission webhook. // The name of the admission webhook.
// Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where
// "imagepolicy" is the name of the webhook, and kubernetes.io is the name // "imagepolicy" is the name of the webhook, and kubernetes.io is the name
...@@ -249,6 +249,20 @@ type Webhook struct { ...@@ -249,6 +249,20 @@ type Webhook struct {
// +optional // +optional
NamespaceSelector *metav1.LabelSelector NamespaceSelector *metav1.LabelSelector
// ObjectSelector decides whether to run the webhook based on if the
// object has matching labels. objectSelector is evaluated against both
// the oldObject and newObject that would be sent to the webhook, and
// is considered to match if either object matches the selector. A null
// object (oldObject in the case of create, or newObject in the case of
// delete) or an object that cannot have labels (like a
// DeploymentRollback or a PodProxyOptions object) is not considered to
// match.
// Use the object selector only if the webhook is opt-in, because end
// users may skip the admission webhook by setting the labels.
// Default to the empty LabelSelector, which matches everything.
// +optional
ObjectSelector *metav1.LabelSelector
// SideEffects states whether this webhookk has side effects. // SideEffects states whether this webhookk has side effects.
// Acceptable values are: Unknown, None, Some, NoneOnDryRun // Acceptable values are: Unknown, None, Some, NoneOnDryRun
// Webhooks with side effects MUST implement a reconciliation system, since a request may be // Webhooks with side effects MUST implement a reconciliation system, since a request may be
...@@ -275,6 +289,161 @@ type Webhook struct { ...@@ -275,6 +289,161 @@ type Webhook struct {
AdmissionReviewVersions []string AdmissionReviewVersions []string
} }
// MutatingWebhook describes an admission webhook and the resources and operations it applies to.
type MutatingWebhook struct {
// The name of the admission webhook.
// Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where
// "imagepolicy" is the name of the webhook, and kubernetes.io is the name
// of the organization.
// Required.
Name string
// ClientConfig defines how to communicate with the hook.
// Required
ClientConfig WebhookClientConfig
// Rules describes what operations on what resources/subresources the webhook cares about.
// The webhook cares about an operation if it matches _any_ Rule.
Rules []RuleWithOperations
// FailurePolicy defines how unrecognized errors from the admission endpoint are handled -
// allowed values are Ignore or Fail. Defaults to Ignore.
// +optional
FailurePolicy *FailurePolicyType
// matchPolicy defines how the "rules" list is used to match incoming requests.
// Allowed values are "Exact" or "Equivalent".
//
// - Exact: match a request only if it exactly matches a specified rule.
// For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1,
// but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`,
// a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the webhook.
//
// - Equivalent: match a request if modifies a resource listed in rules, even via another API group or version.
// For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1,
// and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`,
// a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the webhook.
//
// +optional
MatchPolicy *MatchPolicyType
// NamespaceSelector decides whether to run the webhook on an object based
// on whether the namespace for that object matches the selector. If the
// object itself is a namespace, the matching is performed on
// object.metadata.labels. If the object is another cluster scoped resource,
// it never skips the webhook.
//
// For example, to run the webhook on any objects whose namespace is not
// associated with "runlevel" of "0" or "1"; you will set the selector as
// follows:
// "namespaceSelector": {
// "matchExpressions": [
// {
// "key": "runlevel",
// "operator": "NotIn",
// "values": [
// "0",
// "1"
// ]
// }
// ]
// }
//
// If instead you want to only run the webhook on any objects whose
// namespace is associated with the "environment" of "prod" or "staging";
// you will set the selector as follows:
// "namespaceSelector": {
// "matchExpressions": [
// {
// "key": "environment",
// "operator": "In",
// "values": [
// "prod",
// "staging"
// ]
// }
// ]
// }
//
// See
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
// for more examples of label selectors.
//
// Default to the empty LabelSelector, which matches everything.
// +optional
NamespaceSelector *metav1.LabelSelector
// ObjectSelector decides whether to run the webhook based on if the
// object has matching labels. objectSelector is evaluated against both
// the oldObject and newObject that would be sent to the webhook, and
// is considered to match if either object matches the selector. A null
// object (oldObject in the case of create, or newObject in the case of
// delete) or an object that cannot have labels (like a
// DeploymentRollback or a PodProxyOptions object) is not considered to
// match.
// Use the object selector only if the webhook is opt-in, because end
// users may skip the admission webhook by setting the labels.
// Default to the empty LabelSelector, which matches everything.
// +optional
ObjectSelector *metav1.LabelSelector
// SideEffects states whether this webhookk has side effects.
// Acceptable values are: Unknown, None, Some, NoneOnDryRun
// Webhooks with side effects MUST implement a reconciliation system, since a request may be
// rejected by a future step in the admission change and the side effects therefore need to be undone.
// Requests with the dryRun attribute will be auto-rejected if they match a webhook with
// sideEffects == Unknown or Some. Defaults to Unknown.
// +optional
SideEffects *SideEffectClass
// TimeoutSeconds specifies the timeout for this webhook. After the timeout passes,
// the webhook call will be ignored or the API call will fail based on the
// failure policy.
// The timeout value must be between 1 and 30 seconds.
// +optional
TimeoutSeconds *int32
// AdmissionReviewVersions is an ordered list of preferred `AdmissionReview`
// versions the Webhook expects. API server will try to use first version in
// the list which it supports. If none of the versions specified in this list
// supported by API server, validation will fail for this object.
// If the webhook configuration has already been persisted with a version apiserver
// 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 // 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 {
......
...@@ -27,7 +27,7 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error { ...@@ -27,7 +27,7 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error {
return RegisterDefaults(scheme) return RegisterDefaults(scheme)
} }
func SetDefaults_Webhook(obj *admissionregistrationv1beta1.Webhook) { func SetDefaults_ValidatingWebhook(obj *admissionregistrationv1beta1.ValidatingWebhook) {
if obj.FailurePolicy == nil { if obj.FailurePolicy == nil {
policy := admissionregistrationv1beta1.Ignore policy := admissionregistrationv1beta1.Ignore
obj.FailurePolicy = &policy obj.FailurePolicy = &policy
...@@ -40,6 +40,42 @@ func SetDefaults_Webhook(obj *admissionregistrationv1beta1.Webhook) { ...@@ -40,6 +40,42 @@ func SetDefaults_Webhook(obj *admissionregistrationv1beta1.Webhook) {
selector := metav1.LabelSelector{} selector := metav1.LabelSelector{}
obj.NamespaceSelector = &selector obj.NamespaceSelector = &selector
} }
if obj.ObjectSelector == nil {
selector := metav1.LabelSelector{}
obj.ObjectSelector = &selector
}
if obj.SideEffects == nil {
// TODO: revisit/remove this default and possibly make the field required when promoting to v1
unknown := admissionregistrationv1beta1.SideEffectClassUnknown
obj.SideEffects = &unknown
}
if obj.TimeoutSeconds == nil {
obj.TimeoutSeconds = new(int32)
*obj.TimeoutSeconds = 30
}
if len(obj.AdmissionReviewVersions) == 0 {
obj.AdmissionReviewVersions = []string{admissionregistrationv1beta1.SchemeGroupVersion.Version}
}
}
func SetDefaults_MutatingWebhook(obj *admissionregistrationv1beta1.MutatingWebhook) {
if obj.FailurePolicy == nil {
policy := admissionregistrationv1beta1.Ignore
obj.FailurePolicy = &policy
}
if obj.MatchPolicy == nil {
policy := admissionregistrationv1beta1.Exact
obj.MatchPolicy = &policy
}
if obj.NamespaceSelector == nil {
selector := metav1.LabelSelector{}
obj.NamespaceSelector = &selector
}
if obj.ObjectSelector == nil {
selector := metav1.LabelSelector{}
obj.ObjectSelector = &selector
}
if obj.SideEffects == nil { if obj.SideEffects == nil {
// TODO: revisit/remove this default and possibly make the field required when promoting to v1 // TODO: revisit/remove this default and possibly make the field required when promoting to v1
unknown := admissionregistrationv1beta1.SideEffectClassUnknown unknown := admissionregistrationv1beta1.SideEffectClassUnknown
...@@ -49,6 +85,10 @@ func SetDefaults_Webhook(obj *admissionregistrationv1beta1.Webhook) { ...@@ -49,6 +85,10 @@ func SetDefaults_Webhook(obj *admissionregistrationv1beta1.Webhook) {
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}
......
...@@ -47,7 +47,7 @@ func RegisterDefaults(scheme *runtime.Scheme) error { ...@@ -47,7 +47,7 @@ func RegisterDefaults(scheme *runtime.Scheme) error {
func SetObjectDefaults_MutatingWebhookConfiguration(in *v1beta1.MutatingWebhookConfiguration) { func SetObjectDefaults_MutatingWebhookConfiguration(in *v1beta1.MutatingWebhookConfiguration) {
for i := range in.Webhooks { for i := range in.Webhooks {
a := &in.Webhooks[i] a := &in.Webhooks[i]
SetDefaults_Webhook(a) SetDefaults_MutatingWebhook(a)
if a.ClientConfig.Service != nil { if a.ClientConfig.Service != nil {
SetDefaults_ServiceReference(a.ClientConfig.Service) SetDefaults_ServiceReference(a.ClientConfig.Service)
} }
...@@ -68,7 +68,7 @@ func SetObjectDefaults_MutatingWebhookConfigurationList(in *v1beta1.MutatingWebh ...@@ -68,7 +68,7 @@ func SetObjectDefaults_MutatingWebhookConfigurationList(in *v1beta1.MutatingWebh
func SetObjectDefaults_ValidatingWebhookConfiguration(in *v1beta1.ValidatingWebhookConfiguration) { func SetObjectDefaults_ValidatingWebhookConfiguration(in *v1beta1.ValidatingWebhookConfiguration) {
for i := range in.Webhooks { for i := range in.Webhooks {
a := &in.Webhooks[i] a := &in.Webhooks[i]
SetDefaults_Webhook(a) SetDefaults_ValidatingWebhook(a)
if a.ClientConfig.Service != nil { if a.ClientConfig.Service != nil {
SetDefaults_ServiceReference(a.ClientConfig.Service) SetDefaults_ServiceReference(a.ClientConfig.Service)
} }
......
...@@ -201,7 +201,7 @@ func ValidateValidatingWebhookConfiguration(e *admissionregistration.ValidatingW ...@@ -201,7 +201,7 @@ func ValidateValidatingWebhookConfiguration(e *admissionregistration.ValidatingW
func validateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration, requireRecognizedVersion bool) field.ErrorList { func validateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration, requireRecognizedVersion bool) field.ErrorList {
allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
for i, hook := range e.Webhooks { for i, hook := range e.Webhooks {
allErrors = append(allErrors, validateWebhook(&hook, field.NewPath("webhooks").Index(i))...) allErrors = append(allErrors, validateValidatingWebhook(&hook, field.NewPath("webhooks").Index(i))...)
allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, requireRecognizedVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...) allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, requireRecognizedVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...)
} }
return allErrors return allErrors
...@@ -214,13 +214,13 @@ func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebho ...@@ -214,13 +214,13 @@ func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebho
func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, requireRecognizedVersion bool) field.ErrorList { func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, requireRecognizedVersion bool) field.ErrorList {
allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
for i, hook := range e.Webhooks { for i, hook := range e.Webhooks {
allErrors = append(allErrors, validateWebhook(&hook, field.NewPath("webhooks").Index(i))...) allErrors = append(allErrors, validateMutatingWebhook(&hook, field.NewPath("webhooks").Index(i))...)
allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, requireRecognizedVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...) allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, requireRecognizedVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...)
} }
return allErrors return allErrors
} }
func validateWebhook(hook *admissionregistration.Webhook, fldPath *field.Path) field.ErrorList { func validateValidatingWebhook(hook *admissionregistration.ValidatingWebhook, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList var allErrors field.ErrorList
// hook.Name must be fully qualified // hook.Name must be fully qualified
allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...) allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...)
...@@ -245,6 +245,53 @@ func validateWebhook(hook *admissionregistration.Webhook, fldPath *field.Path) f ...@@ -245,6 +245,53 @@ func validateWebhook(hook *admissionregistration.Webhook, fldPath *field.Path) f
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...) allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
} }
if hook.ObjectSelector != nil {
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.ObjectSelector, fldPath.Child("objectSelector"))...)
}
cc := hook.ClientConfig
switch {
case (cc.URL == nil) == (cc.Service == nil):
allErrors = append(allErrors, field.Required(fldPath.Child("clientConfig"), "exactly one of url or service is required"))
case cc.URL != nil:
allErrors = append(allErrors, webhook.ValidateWebhookURL(fldPath.Child("clientConfig").Child("url"), *cc.URL, true)...)
case cc.Service != nil:
allErrors = append(allErrors, webhook.ValidateWebhookService(fldPath.Child("clientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, cc.Service.Port)...)
}
return allErrors
}
func validateMutatingWebhook(hook *admissionregistration.MutatingWebhook, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
// hook.Name must be fully qualified
allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...)
for i, rule := range hook.Rules {
allErrors = append(allErrors, validateRuleWithOperations(&rule, fldPath.Child("rules").Index(i))...)
}
if hook.FailurePolicy != nil && !supportedFailurePolicies.Has(string(*hook.FailurePolicy)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List()))
}
if hook.MatchPolicy != nil && !supportedMatchPolicies.Has(string(*hook.MatchPolicy)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *hook.MatchPolicy, supportedMatchPolicies.List()))
}
if hook.SideEffects != nil && !supportedSideEffectClasses.Has(string(*hook.SideEffects)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("sideEffects"), *hook.SideEffects, supportedSideEffectClasses.List()))
}
if hook.TimeoutSeconds != nil && (*hook.TimeoutSeconds > 30 || *hook.TimeoutSeconds < 1) {
allErrors = append(allErrors, field.Invalid(fldPath.Child("timeoutSeconds"), *hook.TimeoutSeconds, "the timeout value must be between 1 and 30 seconds"))
}
if hook.NamespaceSelector != nil {
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
}
if hook.ObjectSelector != nil {
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.ObjectSelector, fldPath.Child("objectSelector"))...)
}
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 {
case (cc.URL == nil) == (cc.Service == nil): case (cc.URL == nil) == (cc.Service == nil):
...@@ -282,6 +329,11 @@ var supportedOperations = sets.NewString( ...@@ -282,6 +329,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 {
...@@ -309,9 +361,27 @@ func validateRuleWithOperations(ruleWithOperations *admissionregistration.RuleWi ...@@ -309,9 +361,27 @@ func validateRuleWithOperations(ruleWithOperations *admissionregistration.RuleWi
return allErrors return allErrors
} }
// hasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one // mutatingHasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one
// admission review version this apiserver accepts.
func mutatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.MutatingWebhook) bool {
for _, hook := range webhooks {
hasRecognizedVersion := false
for _, version := range hook.AdmissionReviewVersions {
if isAcceptedAdmissionReviewVersion(version) {
hasRecognizedVersion = true
break
}
}
if !hasRecognizedVersion && len(hook.AdmissionReviewVersions) > 0 {
return false
}
}
return true
}
// validatingHasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one
// admission review version this apiserver accepts. // admission review version this apiserver accepts.
func hasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.Webhook) bool { func validatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.ValidatingWebhook) bool {
for _, hook := range webhooks { for _, hook := range webhooks {
hasRecognizedVersion := false hasRecognizedVersion := false
for _, version := range hook.AdmissionReviewVersions { for _, version := range hook.AdmissionReviewVersions {
...@@ -328,9 +398,9 @@ func hasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.Webhook ...@@ -328,9 +398,9 @@ func hasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.Webhook
} }
func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList { func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList {
return validateValidatingWebhookConfiguration(newC, hasAcceptedAdmissionReviewVersions(oldC.Webhooks)) return validateValidatingWebhookConfiguration(newC, validatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks))
} }
func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.MutatingWebhookConfiguration) field.ErrorList { func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.MutatingWebhookConfiguration) field.ErrorList {
return validateMutatingWebhookConfiguration(newC, hasAcceptedAdmissionReviewVersions(oldC.Webhooks)) return validateMutatingWebhookConfiguration(newC, mutatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks))
} }
...@@ -26,13 +26,77 @@ import ( ...@@ -26,13 +26,77 @@ import (
) )
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingWebhook) DeepCopyInto(out *MutatingWebhook) {
*out = *in
in.ClientConfig.DeepCopyInto(&out.ClientConfig)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]RuleWithOperations, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailurePolicy != nil {
in, out := &in.FailurePolicy, &out.FailurePolicy
*out = new(FailurePolicyType)
**out = **in
}
if in.MatchPolicy != nil {
in, out := &in.MatchPolicy, &out.MatchPolicy
*out = new(MatchPolicyType)
**out = **in
}
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.ObjectSelector != nil {
in, out := &in.ObjectSelector, &out.ObjectSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.SideEffects != nil {
in, out := &in.SideEffects, &out.SideEffects
*out = new(SideEffectClass)
**out = **in
}
if in.TimeoutSeconds != nil {
in, out := &in.TimeoutSeconds, &out.TimeoutSeconds
*out = new(int32)
**out = **in
}
if in.AdmissionReviewVersions != nil {
in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ReinvocationPolicy != nil {
in, out := &in.ReinvocationPolicy, &out.ReinvocationPolicy
*out = new(ReinvocationPolicyType)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingWebhook.
func (in *MutatingWebhook) DeepCopy() *MutatingWebhook {
if in == nil {
return nil
}
out := new(MutatingWebhook)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingWebhookConfiguration) DeepCopyInto(out *MutatingWebhookConfiguration) { func (in *MutatingWebhookConfiguration) DeepCopyInto(out *MutatingWebhookConfiguration) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Webhooks != nil { if in.Webhooks != nil {
in, out := &in.Webhooks, &out.Webhooks in, out := &in.Webhooks, &out.Webhooks
*out = make([]Webhook, len(*in)) *out = make([]MutatingWebhook, len(*in))
for i := range *in { for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
...@@ -171,13 +235,72 @@ func (in *ServiceReference) DeepCopy() *ServiceReference { ...@@ -171,13 +235,72 @@ func (in *ServiceReference) DeepCopy() *ServiceReference {
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValidatingWebhook) DeepCopyInto(out *ValidatingWebhook) {
*out = *in
in.ClientConfig.DeepCopyInto(&out.ClientConfig)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]RuleWithOperations, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailurePolicy != nil {
in, out := &in.FailurePolicy, &out.FailurePolicy
*out = new(FailurePolicyType)
**out = **in
}
if in.MatchPolicy != nil {
in, out := &in.MatchPolicy, &out.MatchPolicy
*out = new(MatchPolicyType)
**out = **in
}
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.ObjectSelector != nil {
in, out := &in.ObjectSelector, &out.ObjectSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.SideEffects != nil {
in, out := &in.SideEffects, &out.SideEffects
*out = new(SideEffectClass)
**out = **in
}
if in.TimeoutSeconds != nil {
in, out := &in.TimeoutSeconds, &out.TimeoutSeconds
*out = new(int32)
**out = **in
}
if in.AdmissionReviewVersions != nil {
in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValidatingWebhook.
func (in *ValidatingWebhook) DeepCopy() *ValidatingWebhook {
if in == nil {
return nil
}
out := new(ValidatingWebhook)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValidatingWebhookConfiguration) DeepCopyInto(out *ValidatingWebhookConfiguration) { func (in *ValidatingWebhookConfiguration) DeepCopyInto(out *ValidatingWebhookConfiguration) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Webhooks != nil { if in.Webhooks != nil {
in, out := &in.Webhooks, &out.Webhooks in, out := &in.Webhooks, &out.Webhooks
*out = make([]Webhook, len(*in)) *out = make([]ValidatingWebhook, len(*in))
for i := range *in { for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
...@@ -237,60 +360,6 @@ func (in *ValidatingWebhookConfigurationList) DeepCopyObject() runtime.Object { ...@@ -237,60 +360,6 @@ func (in *ValidatingWebhookConfigurationList) DeepCopyObject() runtime.Object {
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Webhook) DeepCopyInto(out *Webhook) {
*out = *in
in.ClientConfig.DeepCopyInto(&out.ClientConfig)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]RuleWithOperations, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailurePolicy != nil {
in, out := &in.FailurePolicy, &out.FailurePolicy
*out = new(FailurePolicyType)
**out = **in
}
if in.MatchPolicy != nil {
in, out := &in.MatchPolicy, &out.MatchPolicy
*out = new(MatchPolicyType)
**out = **in
}
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.SideEffects != nil {
in, out := &in.SideEffects, &out.SideEffects
*out = new(SideEffectClass)
**out = **in
}
if in.TimeoutSeconds != nil {
in, out := &in.TimeoutSeconds, &out.TimeoutSeconds
*out = new(int32)
**out = **in
}
if in.AdmissionReviewVersions != nil {
in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Webhook.
func (in *Webhook) DeepCopy() *Webhook {
if in == nil {
return nil
}
out := new(Webhook)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookClientConfig) DeepCopyInto(out *WebhookClientConfig) { func (in *WebhookClientConfig) DeepCopyInto(out *WebhookClientConfig) {
*out = *in *out = *in
if in.URL != nil { if in.URL != nil {
......
...@@ -26,13 +26,77 @@ import ( ...@@ -26,13 +26,77 @@ import (
) )
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingWebhook) DeepCopyInto(out *MutatingWebhook) {
*out = *in
in.ClientConfig.DeepCopyInto(&out.ClientConfig)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]RuleWithOperations, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailurePolicy != nil {
in, out := &in.FailurePolicy, &out.FailurePolicy
*out = new(FailurePolicyType)
**out = **in
}
if in.MatchPolicy != nil {
in, out := &in.MatchPolicy, &out.MatchPolicy
*out = new(MatchPolicyType)
**out = **in
}
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.ObjectSelector != nil {
in, out := &in.ObjectSelector, &out.ObjectSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.SideEffects != nil {
in, out := &in.SideEffects, &out.SideEffects
*out = new(SideEffectClass)
**out = **in
}
if in.TimeoutSeconds != nil {
in, out := &in.TimeoutSeconds, &out.TimeoutSeconds
*out = new(int32)
**out = **in
}
if in.AdmissionReviewVersions != nil {
in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ReinvocationPolicy != nil {
in, out := &in.ReinvocationPolicy, &out.ReinvocationPolicy
*out = new(ReinvocationPolicyType)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingWebhook.
func (in *MutatingWebhook) DeepCopy() *MutatingWebhook {
if in == nil {
return nil
}
out := new(MutatingWebhook)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MutatingWebhookConfiguration) DeepCopyInto(out *MutatingWebhookConfiguration) { func (in *MutatingWebhookConfiguration) DeepCopyInto(out *MutatingWebhookConfiguration) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Webhooks != nil { if in.Webhooks != nil {
in, out := &in.Webhooks, &out.Webhooks in, out := &in.Webhooks, &out.Webhooks
*out = make([]Webhook, len(*in)) *out = make([]MutatingWebhook, len(*in))
for i := range *in { for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
...@@ -176,13 +240,72 @@ func (in *ServiceReference) DeepCopy() *ServiceReference { ...@@ -176,13 +240,72 @@ func (in *ServiceReference) DeepCopy() *ServiceReference {
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValidatingWebhook) DeepCopyInto(out *ValidatingWebhook) {
*out = *in
in.ClientConfig.DeepCopyInto(&out.ClientConfig)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]RuleWithOperations, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailurePolicy != nil {
in, out := &in.FailurePolicy, &out.FailurePolicy
*out = new(FailurePolicyType)
**out = **in
}
if in.MatchPolicy != nil {
in, out := &in.MatchPolicy, &out.MatchPolicy
*out = new(MatchPolicyType)
**out = **in
}
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.ObjectSelector != nil {
in, out := &in.ObjectSelector, &out.ObjectSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.SideEffects != nil {
in, out := &in.SideEffects, &out.SideEffects
*out = new(SideEffectClass)
**out = **in
}
if in.TimeoutSeconds != nil {
in, out := &in.TimeoutSeconds, &out.TimeoutSeconds
*out = new(int32)
**out = **in
}
if in.AdmissionReviewVersions != nil {
in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValidatingWebhook.
func (in *ValidatingWebhook) DeepCopy() *ValidatingWebhook {
if in == nil {
return nil
}
out := new(ValidatingWebhook)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ValidatingWebhookConfiguration) DeepCopyInto(out *ValidatingWebhookConfiguration) { func (in *ValidatingWebhookConfiguration) DeepCopyInto(out *ValidatingWebhookConfiguration) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Webhooks != nil { if in.Webhooks != nil {
in, out := &in.Webhooks, &out.Webhooks in, out := &in.Webhooks, &out.Webhooks
*out = make([]Webhook, len(*in)) *out = make([]ValidatingWebhook, len(*in))
for i := range *in { for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
...@@ -242,60 +365,6 @@ func (in *ValidatingWebhookConfigurationList) DeepCopyObject() runtime.Object { ...@@ -242,60 +365,6 @@ func (in *ValidatingWebhookConfigurationList) DeepCopyObject() runtime.Object {
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Webhook) DeepCopyInto(out *Webhook) {
*out = *in
in.ClientConfig.DeepCopyInto(&out.ClientConfig)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]RuleWithOperations, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.FailurePolicy != nil {
in, out := &in.FailurePolicy, &out.FailurePolicy
*out = new(FailurePolicyType)
**out = **in
}
if in.MatchPolicy != nil {
in, out := &in.MatchPolicy, &out.MatchPolicy
*out = new(MatchPolicyType)
**out = **in
}
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.SideEffects != nil {
in, out := &in.SideEffects, &out.SideEffects
*out = new(SideEffectClass)
**out = **in
}
if in.TimeoutSeconds != nil {
in, out := &in.TimeoutSeconds, &out.TimeoutSeconds
*out = new(int32)
**out = **in
}
if in.AdmissionReviewVersions != nil {
in, out := &in.AdmissionReviewVersions, &out.AdmissionReviewVersions
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Webhook.
func (in *Webhook) DeepCopy() *Webhook {
if in == nil {
return nil
}
out := new(Webhook)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookClientConfig) DeepCopyInto(out *WebhookClientConfig) { func (in *WebhookClientConfig) DeepCopyInto(out *WebhookClientConfig) {
*out = *in *out = *in
if in.URL != nil { if in.URL != nil {
......
package(default_visibility = ["//visibility:public"]) load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = [
"attributes_test.go",
"audit_test.go",
"chain_test.go",
"config_test.go",
"errors_test.go",
"handler_test.go",
],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/apiserver:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
],
)
go_library( go_library(
name = "go_default_library", name = "go_default_library",
...@@ -42,10 +12,12 @@ go_library( ...@@ -42,10 +12,12 @@ 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",
importpath = "k8s.io/apiserver/pkg/admission", importpath = "k8s.io/apiserver/pkg/admission",
visibility = ["//visibility:public"],
deps = [ deps = [
"//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/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
...@@ -65,6 +37,30 @@ go_library( ...@@ -65,6 +37,30 @@ go_library(
], ],
) )
go_test(
name = "go_default_test",
srcs = [
"attributes_test.go",
"audit_test.go",
"chain_test.go",
"config_test.go",
"errors_test.go",
"handler_test.go",
],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/apiserver:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
],
)
filegroup( filegroup(
name = "package-srcs", name = "package-srcs",
srcs = glob(["**"]), srcs = glob(["**"]),
...@@ -80,19 +76,9 @@ filegroup( ...@@ -80,19 +76,9 @@ filegroup(
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/initializer:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/metrics:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/metrics:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/initializer:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/testing:all-srcs", "//staging/src/k8s.io/apiserver/pkg/admission/testing:all-srcs",
], ],
tags = ["automanaged"], tags = ["automanaged"],
visibility = ["//visibility:public"],
) )
...@@ -44,21 +44,24 @@ type attributesRecord struct { ...@@ -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 { ...@@ -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 {
......
...@@ -39,6 +39,7 @@ go_library( ...@@ -39,6 +39,7 @@ go_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/util/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
"//staging/src/k8s.io/client-go/informers:go_default_library", "//staging/src/k8s.io/client-go/informers:go_default_library",
"//staging/src/k8s.io/client-go/listers/admissionregistration/v1beta1:go_default_library", "//staging/src/k8s.io/client-go/listers/admissionregistration/v1beta1:go_default_library",
......
...@@ -24,6 +24,7 @@ import ( ...@@ -24,6 +24,7 @@ import (
"k8s.io/api/admissionregistration/v1beta1" "k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1" admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
...@@ -48,7 +49,7 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g ...@@ -48,7 +49,7 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g
} }
// Start with an empty list // Start with an empty list
manager.configuration.Store(&v1beta1.MutatingWebhookConfiguration{}) manager.configuration.Store([]webhook.WebhookAccessor{})
// On any change, rebuild the config // On any change, rebuild the config
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
...@@ -61,8 +62,8 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g ...@@ -61,8 +62,8 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g
} }
// Webhooks returns the merged MutatingWebhookConfiguration. // Webhooks returns the merged MutatingWebhookConfiguration.
func (m *mutatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook { func (m *mutatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor {
return m.configuration.Load().(*v1beta1.MutatingWebhookConfiguration).Webhooks return m.configuration.Load().([]webhook.WebhookAccessor)
} }
func (m *mutatingWebhookConfigurationManager) HasSynced() bool { func (m *mutatingWebhookConfigurationManager) HasSynced() bool {
...@@ -78,16 +79,24 @@ func (m *mutatingWebhookConfigurationManager) updateConfiguration() { ...@@ -78,16 +79,24 @@ func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
m.configuration.Store(mergeMutatingWebhookConfigurations(configurations)) m.configuration.Store(mergeMutatingWebhookConfigurations(configurations))
} }
func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) *v1beta1.MutatingWebhookConfiguration { func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
var ret v1beta1.MutatingWebhookConfiguration
// The internal order of webhooks for each configuration is provided by the user // The internal order of webhooks for each configuration is provided by the user
// but configurations themselves can be in any order. As we are going to run these // but configurations themselves can be in any order. As we are going to run these
// webhooks in serial, they are sorted here to have a deterministic order. // webhooks in serial, they are sorted here to have a deterministic order.
sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName) sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
accessors := []webhook.WebhookAccessor{}
for _, c := range configurations { for _, c := range configurations {
ret.Webhooks = append(ret.Webhooks, c.Webhooks...) // 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 {
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 &ret return accessors
} }
type MutatingWebhookConfigurationSorter []*v1beta1.MutatingWebhookConfiguration type MutatingWebhookConfigurationSorter []*v1beta1.MutatingWebhookConfiguration
......
...@@ -45,7 +45,7 @@ func TestGetMutatingWebhookConfig(t *testing.T) { ...@@ -45,7 +45,7 @@ func TestGetMutatingWebhookConfig(t *testing.T) {
webhookConfiguration := &v1beta1.MutatingWebhookConfiguration{ webhookConfiguration := &v1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"}, ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}}, Webhooks: []v1beta1.MutatingWebhook{{Name: "webhook1.1"}},
} }
mutatingInformer := informerFactory.Admissionregistration().V1beta1().MutatingWebhookConfigurations() mutatingInformer := informerFactory.Admissionregistration().V1beta1().MutatingWebhookConfigurations()
...@@ -57,7 +57,14 @@ func TestGetMutatingWebhookConfig(t *testing.T) { ...@@ -57,7 +57,14 @@ func TestGetMutatingWebhookConfig(t *testing.T) {
if len(configurations) == 0 { if len(configurations) == 0 {
t.Errorf("expected non empty webhooks") t.Errorf("expected non empty webhooks")
} }
if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) { for i := range configurations {
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations) h, ok := configurations[i].GetMutatingWebhook()
if !ok {
t.Errorf("Expected mutating webhook")
continue
}
if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) {
t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h)
}
} }
} }
...@@ -24,6 +24,7 @@ import ( ...@@ -24,6 +24,7 @@ import (
"k8s.io/api/admissionregistration/v1beta1" "k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1" admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
...@@ -48,7 +49,7 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) ...@@ -48,7 +49,7 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory)
} }
// Start with an empty list // Start with an empty list
manager.configuration.Store(&v1beta1.ValidatingWebhookConfiguration{}) manager.configuration.Store([]webhook.WebhookAccessor{})
// On any change, rebuild the config // On any change, rebuild the config
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
...@@ -61,8 +62,8 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) ...@@ -61,8 +62,8 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory)
} }
// Webhooks returns the merged ValidatingWebhookConfiguration. // Webhooks returns the merged ValidatingWebhookConfiguration.
func (v *validatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook { func (v *validatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor {
return v.configuration.Load().(*v1beta1.ValidatingWebhookConfiguration).Webhooks return v.configuration.Load().([]webhook.WebhookAccessor)
} }
// HasSynced returns true if the shared informers have synced. // HasSynced returns true if the shared informers have synced.
...@@ -79,15 +80,21 @@ func (v *validatingWebhookConfigurationManager) updateConfiguration() { ...@@ -79,15 +80,21 @@ func (v *validatingWebhookConfigurationManager) updateConfiguration() {
v.configuration.Store(mergeValidatingWebhookConfigurations(configurations)) v.configuration.Store(mergeValidatingWebhookConfigurations(configurations))
} }
func mergeValidatingWebhookConfigurations( func mergeValidatingWebhookConfigurations(configurations []*v1beta1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
configurations []*v1beta1.ValidatingWebhookConfiguration,
) *v1beta1.ValidatingWebhookConfiguration {
sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName) sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
var ret v1beta1.ValidatingWebhookConfiguration accessors := []webhook.WebhookAccessor{}
for _, c := range configurations { for _, c := range configurations {
ret.Webhooks = append(ret.Webhooks, c.Webhooks...) // 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 {
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 &ret return accessors
} }
type ValidatingWebhookConfigurationSorter []*v1beta1.ValidatingWebhookConfiguration type ValidatingWebhookConfigurationSorter []*v1beta1.ValidatingWebhookConfiguration
......
...@@ -46,7 +46,7 @@ func TestGetValidatingWebhookConfig(t *testing.T) { ...@@ -46,7 +46,7 @@ func TestGetValidatingWebhookConfig(t *testing.T) {
webhookConfiguration := &v1beta1.ValidatingWebhookConfiguration{ webhookConfiguration := &v1beta1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"}, ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}}, Webhooks: []v1beta1.ValidatingWebhook{{Name: "webhook1.1"}},
} }
validatingInformer := informerFactory.Admissionregistration().V1beta1().ValidatingWebhookConfigurations() validatingInformer := informerFactory.Admissionregistration().V1beta1().ValidatingWebhookConfigurations()
...@@ -59,7 +59,14 @@ func TestGetValidatingWebhookConfig(t *testing.T) { ...@@ -59,7 +59,14 @@ func TestGetValidatingWebhookConfig(t *testing.T) {
if len(configurations) == 0 { if len(configurations) == 0 {
t.Errorf("expected non empty webhooks") t.Errorf("expected non empty webhooks")
} }
if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) { for i := range configurations {
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations) h, ok := configurations[i].GetValidatingWebhook()
if !ok {
t.Errorf("Expected validating webhook")
continue
}
if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) {
t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h)
}
} }
} }
...@@ -62,6 +62,9 @@ type Attributes interface { ...@@ -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 { ...@@ -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
......
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["accessors.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook",
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/initializer:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/object:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
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 webhook
import (
"k8s.io/api/admissionregistration/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// WebhookAccessor provides a common interface to both mutating and validating webhook types.
type WebhookAccessor interface {
// 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
// GetRules gets the webhook Rules field.
GetRules() []v1beta1.RuleWithOperations
// GetFailurePolicy gets the webhook FailurePolicy field.
GetFailurePolicy() *v1beta1.FailurePolicyType
// GetMatchPolicy gets the webhook MatchPolicy field.
GetMatchPolicy() *v1beta1.MatchPolicyType
// GetNamespaceSelector gets the webhook NamespaceSelector field.
GetNamespaceSelector() *metav1.LabelSelector
// GetObjectSelector gets the webhook ObjectSelector field.
GetObjectSelector() *metav1.LabelSelector
// GetSideEffects gets the webhook SideEffects field.
GetSideEffects() *v1beta1.SideEffectClass
// GetTimeoutSeconds gets the webhook TimeoutSeconds field.
GetTimeoutSeconds() *int32
// GetAdmissionReviewVersions gets the webhook AdmissionReviewVersions field.
GetAdmissionReviewVersions() []string
// GetMutatingWebhook if the accessor contains a MutatingWebhook, returns it and true, else returns false.
GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool)
// GetValidatingWebhook if the accessor contains a ValidatingWebhook, returns it and true, else returns false.
GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool)
}
// NewMutatingWebhookAccessor creates an accessor for a MutatingWebhook.
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
}
func (m mutatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig {
return m.ClientConfig
}
func (m mutatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations {
return m.Rules
}
func (m mutatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType {
return m.FailurePolicy
}
func (m mutatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
return m.MatchPolicy
}
func (m mutatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
return m.NamespaceSelector
}
func (m mutatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
return m.ObjectSelector
}
func (m mutatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
return m.SideEffects
}
func (m mutatingWebhookAccessor) GetTimeoutSeconds() *int32 {
return m.TimeoutSeconds
}
func (m mutatingWebhookAccessor) GetAdmissionReviewVersions() []string {
return m.AdmissionReviewVersions
}
func (m mutatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) {
return m.MutatingWebhook, true
}
func (m mutatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) {
return nil, false
}
// NewValidatingWebhookAccessor creates an accessor for a ValidatingWebhook.
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
}
func (v validatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig {
return v.ClientConfig
}
func (v validatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations {
return v.Rules
}
func (v validatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType {
return v.FailurePolicy
}
func (v validatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
return v.MatchPolicy
}
func (v validatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
return v.NamespaceSelector
}
func (v validatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
return v.ObjectSelector
}
func (v validatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
return v.SideEffects
}
func (v validatingWebhookAccessor) GetTimeoutSeconds() *int32 {
return v.TimeoutSeconds
}
func (v validatingWebhookAccessor) GetAdmissionReviewVersions() []string {
return v.AdmissionReviewVersions
}
func (v validatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) {
return nil, false
}
func (v validatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) {
return v.ValidatingWebhook, true
}
...@@ -18,8 +18,10 @@ go_library( ...@@ -18,8 +18,10 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema: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/initializer:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/object:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
"//staging/src/k8s.io/client-go/informers:go_default_library", "//staging/src/k8s.io/client-go/informers:go_default_library",
...@@ -56,7 +58,9 @@ go_test( ...@@ -56,7 +58,9 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff: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/plugin/webhook:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/object:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library",
......
...@@ -19,15 +19,15 @@ package generic ...@@ -19,15 +19,15 @@ package generic
import ( import (
"context" "context"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
) )
// Source can list dynamic webhook plugins. // Source can list dynamic webhook plugins.
type Source interface { type Source interface {
Webhooks() []v1beta1.Webhook Webhooks() []webhook.WebhookAccessor
HasSynced() bool HasSynced() bool
} }
...@@ -35,7 +35,7 @@ type Source interface { ...@@ -35,7 +35,7 @@ type Source interface {
// variants of the object and old object. // variants of the object and old object.
type VersionedAttributes struct { type VersionedAttributes struct {
// Attributes holds the original admission attributes // Attributes holds the original admission attributes
Attributes admission.Attributes admission.Attributes
// VersionedOldObject holds Attributes.OldObject (if non-nil), converted to VersionedKind. // VersionedOldObject holds Attributes.OldObject (if non-nil), converted to VersionedKind.
// It must never be mutated. // It must never be mutated.
VersionedOldObject runtime.Object VersionedOldObject runtime.Object
...@@ -48,11 +48,18 @@ type VersionedAttributes struct { ...@@ -48,11 +48,18 @@ type VersionedAttributes struct {
Dirty bool Dirty bool
} }
// GetObject overrides the Attributes.GetObject()
func (v *VersionedAttributes) GetObject() runtime.Object {
if v.VersionedObject != nil {
return v.VersionedObject
}
return v.Attributes.GetObject()
}
// WebhookInvocation describes how to call a webhook, including the resource and subresource the webhook registered for, // WebhookInvocation describes how to call a webhook, including the resource and subresource the webhook registered for,
// and the kind that should be sent to the webhook. // and the kind that should be sent to the webhook.
type WebhookInvocation struct { type WebhookInvocation struct {
Webhook *v1beta1.Webhook Webhook webhook.WebhookAccessor
Resource schema.GroupVersionResource Resource schema.GroupVersionResource
Subresource string Subresource string
Kind schema.GroupVersionKind Kind schema.GroupVersionKind
...@@ -60,6 +67,9 @@ type WebhookInvocation struct { ...@@ -60,6 +67,9 @@ type WebhookInvocation struct {
// Dispatcher dispatches webhook call to a list of webhooks with admission attributes as argument. // Dispatcher dispatches webhook call to a list of webhooks with admission attributes as argument.
type Dispatcher interface { type Dispatcher interface {
// Dispatch a request to the webhooks using the given webhooks. A non-nil error means the request is rejected. // Dispatch a request to the webhooks. Dispatcher may choose not to
Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []*WebhookInvocation) error // call a hook, either because the rules of the hook does not match, or
// the namespaceSelector or the objectSelector of the hook does not
// match. A non-nil error means the request is rejected.
Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error
} }
...@@ -27,10 +27,12 @@ import ( ...@@ -27,10 +27,12 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer" genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config" "k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace" "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/object"
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules" "k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
"k8s.io/apiserver/pkg/util/webhook" webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
) )
...@@ -42,8 +44,9 @@ type Webhook struct { ...@@ -42,8 +44,9 @@ type Webhook struct {
sourceFactory sourceFactory sourceFactory sourceFactory
hookSource Source hookSource Source
clientManager *webhook.ClientManager clientManager *webhookutil.ClientManager
namespaceMatcher *namespace.Matcher namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
dispatcher Dispatcher dispatcher Dispatcher
} }
...@@ -53,7 +56,7 @@ var ( ...@@ -53,7 +56,7 @@ var (
) )
type sourceFactory func(f informers.SharedInformerFactory) Source type sourceFactory func(f informers.SharedInformerFactory) Source
type dispatcherFactory func(cm *webhook.ClientManager) Dispatcher type dispatcherFactory func(cm *webhookutil.ClientManager) Dispatcher
// NewWebhook creates a new generic admission webhook. // NewWebhook creates a new generic admission webhook.
func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) { func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) {
...@@ -62,23 +65,24 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory ...@@ -62,23 +65,24 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
return nil, err return nil, err
} }
cm, err := webhook.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme) cm, err := webhookutil.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme)
if err != nil { if err != nil {
return nil, err return nil, err
} }
authInfoResolver, err := webhook.NewDefaultAuthenticationInfoResolver(kubeconfigFile) authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Set defaults which may be overridden later. // Set defaults which may be overridden later.
cm.SetAuthenticationInfoResolver(authInfoResolver) cm.SetAuthenticationInfoResolver(authInfoResolver)
cm.SetServiceResolver(webhook.NewDefaultServiceResolver()) cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver())
return &Webhook{ return &Webhook{
Handler: handler, Handler: handler,
sourceFactory: sourceFactory, sourceFactory: sourceFactory,
clientManager: &cm, clientManager: &cm,
namespaceMatcher: &namespace.Matcher{}, namespaceMatcher: &namespace.Matcher{},
objectMatcher: &object.Matcher{},
dispatcher: dispatcherFactory(&cm), dispatcher: dispatcherFactory(&cm),
}, nil }, nil
} }
...@@ -86,13 +90,13 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory ...@@ -86,13 +90,13 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
// SetAuthenticationInfoResolverWrapper sets the // SetAuthenticationInfoResolverWrapper sets the
// AuthenticationInfoResolverWrapper. // AuthenticationInfoResolverWrapper.
// TODO find a better way wire this, but keep this pull small for now. // TODO find a better way wire this, but keep this pull small for now.
func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhook.AuthenticationInfoResolverWrapper) { func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhookutil.AuthenticationInfoResolverWrapper) {
a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper) a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper)
} }
// SetServiceResolver sets a service resolver for the webhook admission plugin. // SetServiceResolver sets a service resolver for the webhook admission plugin.
// Passing a nil resolver does not have an effect, instead a default one will be used. // Passing a nil resolver does not have an effect, instead a default one will be used.
func (a *Webhook) SetServiceResolver(sr webhook.ServiceResolver) { func (a *Webhook) SetServiceResolver(sr webhookutil.ServiceResolver) {
a.clientManager.SetServiceResolver(sr) a.clientManager.SetServiceResolver(sr)
} }
...@@ -126,12 +130,12 @@ func (a *Webhook) ValidateInitialization() error { ...@@ -126,12 +130,12 @@ func (a *Webhook) ValidateInitialization() error {
return nil return nil
} }
// shouldCallHook returns invocation details if the webhook should be called, nil if the webhook should not be called, // ShouldCallHook returns invocation details if the webhook should be called, nil if the webhook should not be called,
// or an error if an error was encountered during evaluation. // or an error if an error was encountered during evaluation.
func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) { func (a *Webhook) ShouldCallHook(h webhook.WebhookAccessor, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) {
var err *apierrors.StatusError var err *apierrors.StatusError
var invocation *WebhookInvocation var invocation *WebhookInvocation
for _, r := range h.Rules { for _, r := range h.GetRules() {
m := rules.Matcher{Rule: r, Attr: attr} m := rules.Matcher{Rule: r, Attr: attr}
if m.Matches() { if m.Matches() {
invocation = &WebhookInvocation{ invocation = &WebhookInvocation{
...@@ -143,12 +147,12 @@ func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes, ...@@ -143,12 +147,12 @@ func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes,
break break
} }
} }
if invocation == nil && h.MatchPolicy != nil && *h.MatchPolicy == v1beta1.Equivalent { if invocation == nil && h.GetMatchPolicy() != nil && *h.GetMatchPolicy() == v1beta1.Equivalent {
attrWithOverride := &attrWithResourceOverride{Attributes: attr} attrWithOverride := &attrWithResourceOverride{Attributes: attr}
equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource()) equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
// honor earlier rules first // honor earlier rules first
OuterLoop: OuterLoop:
for _, r := range h.Rules { for _, r := range h.GetRules() {
// see if the rule matches any of the equivalent resources // see if the rule matches any of the equivalent resources
for _, equivalent := range equivalents { for _, equivalent := range equivalents {
if equivalent == attr.GetResource() { if equivalent == attr.GetResource() {
...@@ -183,6 +187,11 @@ func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes, ...@@ -183,6 +187,11 @@ func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes,
return nil, err return nil, err
} }
matches, err = a.objectMatcher.MatchObjectSelector(h, attr)
if !matches || err != nil {
return nil, err
}
return invocation, nil return invocation, nil
} }
...@@ -205,21 +214,5 @@ func (a *Webhook) Dispatch(attr admission.Attributes, o admission.ObjectInterfac ...@@ -205,21 +214,5 @@ func (a *Webhook) Dispatch(attr admission.Attributes, o admission.ObjectInterfac
// TODO: Figure out if adding one second timeout make sense here. // TODO: Figure out if adding one second timeout make sense here.
ctx := context.TODO() ctx := context.TODO()
var relevantHooks []*WebhookInvocation return a.dispatcher.Dispatch(ctx, attr, o, hooks)
for i := range hooks {
invocation, err := a.shouldCallHook(&hooks[i], attr, o)
if err != nil {
return err
}
if invocation != nil {
relevantHooks = append(relevantHooks, invocation)
}
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
}
return a.dispatcher.Dispatch(ctx, attr, o, relevantHooks)
} }
...@@ -17,6 +17,7 @@ limitations under the License. ...@@ -17,6 +17,7 @@ limitations under the License.
package generic package generic
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
...@@ -25,11 +26,13 @@ import ( ...@@ -25,11 +26,13 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace" "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/object"
) )
func TestShouldCallHook(t *testing.T) { func TestShouldCallHook(t *testing.T) {
a := &Webhook{namespaceMatcher: &namespace.Matcher{}} a := &Webhook{namespaceMatcher: &namespace.Matcher{}, objectMatcher: &object.Matcher{}}
allScopes := v1beta1.AllScopes allScopes := v1beta1.AllScopes
exactMatch := v1beta1.Exact exactMatch := v1beta1.Exact
...@@ -61,7 +64,7 @@ func TestShouldCallHook(t *testing.T) { ...@@ -61,7 +64,7 @@ func TestShouldCallHook(t *testing.T) {
testcases := []struct { testcases := []struct {
name string name string
webhook *v1beta1.Webhook webhook *v1beta1.ValidatingWebhook
attrs admission.Attributes attrs admission.Attributes
expectCall bool expectCall bool
...@@ -72,14 +75,15 @@ func TestShouldCallHook(t *testing.T) { ...@@ -72,14 +75,15 @@ func TestShouldCallHook(t *testing.T) {
}{ }{
{ {
name: "no rules (just write)", name: "no rules (just write)",
webhook: &v1beta1.Webhook{Rules: []v1beta1.RuleWithOperations{}}, webhook: &v1beta1.ValidatingWebhook{Rules: []v1beta1.RuleWithOperations{}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil), attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectCall: false, expectCall: false,
}, },
{ {
name: "invalid kind lookup", name: "invalid kind lookup",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
MatchPolicy: &equivalentMatch, MatchPolicy: &equivalentMatch,
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
...@@ -91,8 +95,9 @@ func TestShouldCallHook(t *testing.T) { ...@@ -91,8 +95,9 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "wildcard rule, match as requested", name: "wildcard rule, match as requested",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
...@@ -105,8 +110,9 @@ func TestShouldCallHook(t *testing.T) { ...@@ -105,8 +110,9 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, prefer exact match", name: "specific rules, prefer exact match",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
...@@ -125,8 +131,9 @@ func TestShouldCallHook(t *testing.T) { ...@@ -125,8 +131,9 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, match miss", name: "specific rules, match miss",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
...@@ -139,9 +146,10 @@ func TestShouldCallHook(t *testing.T) { ...@@ -139,9 +146,10 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, exact match miss", name: "specific rules, exact match miss",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &exactMatch, MatchPolicy: &exactMatch,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
...@@ -154,9 +162,10 @@ func TestShouldCallHook(t *testing.T) { ...@@ -154,9 +162,10 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, equivalent match, prefer extensions", name: "specific rules, equivalent match, prefer extensions",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &equivalentMatch, MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
...@@ -172,9 +181,10 @@ func TestShouldCallHook(t *testing.T) { ...@@ -172,9 +181,10 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, equivalent match, prefer apps", name: "specific rules, equivalent match, prefer apps",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &equivalentMatch, MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
...@@ -191,8 +201,9 @@ func TestShouldCallHook(t *testing.T) { ...@@ -191,8 +201,9 @@ func TestShouldCallHook(t *testing.T) {
{ {
name: "specific rules, subresource prefer exact match", name: "specific rules, subresource prefer exact match",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
...@@ -211,8 +222,9 @@ func TestShouldCallHook(t *testing.T) { ...@@ -211,8 +222,9 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, subresource match miss", name: "specific rules, subresource match miss",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
...@@ -225,9 +237,10 @@ func TestShouldCallHook(t *testing.T) { ...@@ -225,9 +237,10 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, subresource exact match miss", name: "specific rules, subresource exact match miss",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &exactMatch, MatchPolicy: &exactMatch,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
...@@ -240,9 +253,10 @@ func TestShouldCallHook(t *testing.T) { ...@@ -240,9 +253,10 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, subresource equivalent match, prefer extensions", name: "specific rules, subresource equivalent match, prefer extensions",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &equivalentMatch, MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
...@@ -258,9 +272,10 @@ func TestShouldCallHook(t *testing.T) { ...@@ -258,9 +272,10 @@ func TestShouldCallHook(t *testing.T) {
}, },
{ {
name: "specific rules, subresource equivalent match, prefer apps", name: "specific rules, subresource equivalent match, prefer apps",
webhook: &v1beta1.Webhook{ webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &equivalentMatch, MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"}, Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes}, Rule: v1beta1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
...@@ -276,9 +291,9 @@ func TestShouldCallHook(t *testing.T) { ...@@ -276,9 +291,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(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( ...@@ -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,14 +14,17 @@ go_library( ...@@ -13,14 +14,17 @@ 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",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library",
......
...@@ -46,70 +46,80 @@ func TestAdmit(t *testing.T) { ...@@ -46,70 +46,80 @@ func TestAdmit(t *testing.T) {
defer close(stopCh) defer close(stopCh)
testCases := append(webhooktesting.NewMutatingTestCases(serverURL), testCases := append(webhooktesting.NewMutatingTestCases(serverURL),
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" ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh) client, informer := webhooktesting.NewFakeMutatingDataSource(ns, tt.Webhooks, stopCh)
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32)))) wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL)) wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
wh.SetExternalKubeClientSet(client) wh.SetExternalKubeClientSet(client)
wh.SetExternalKubeInformerFactory(informer) wh.SetExternalKubeInformerFactory(informer)
informer.Start(stopCh) informer.Start(stopCh)
informer.WaitForCacheSync(stopCh) informer.WaitForCacheSync(stopCh)
if err = wh.ValidateInitialization(); err != nil { if err = wh.ValidateInitialization(); err != nil {
t.Errorf("%s: failed to validate initialization: %v", tt.Name, err) t.Errorf("failed to validate initialization: %v", err)
continue return
} }
var attr admission.Attributes var attr admission.Attributes
if tt.IsCRD { if tt.IsCRD {
attr = webhooktesting.NewAttributeUnstructured(ns, tt.AdditionalLabels, tt.IsDryRun) attr = webhooktesting.NewAttributeUnstructured(ns, tt.AdditionalLabels, tt.IsDryRun)
} else { } else {
attr = webhooktesting.NewAttribute(ns, tt.AdditionalLabels, tt.IsDryRun) attr = webhooktesting.NewAttribute(ns, tt.AdditionalLabels, tt.IsDryRun)
} }
err = wh.Admit(attr, objectInterfaces) err = wh.Admit(attr, objectInterfaces)
if tt.ExpectAllow != (err == nil) { if tt.ExpectAllow != (err == nil) {
t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err) t.Errorf("expected allowed=%v, but got err=%v", 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())
} }
} if tt.ExpectLabels != nil {
// ErrWebhookRejected is not an error for our purposes if !reflect.DeepEqual(tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels()) {
if tt.ErrorContains != "" { t.Errorf("expected labels '%v', but got '%v'", tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels())
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)
} }
} // ErrWebhookRejected is not an error for our purposes
if statusErr, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr { if tt.ErrorContains != "" {
t.Errorf("%s: expected a StatusError, got %T", tt.Name, err) if err == nil || !strings.Contains(err.Error(), tt.ErrorContains) {
} else if isStatusErr { t.Errorf("expected an error saying %q, but got: %v", tt.ErrorContains, err)
if statusErr.ErrStatus.Code != tt.ExpectStatusCode { }
t.Errorf("%s: expected status code %d, got %d", tt.Name, tt.ExpectStatusCode, statusErr.ErrStatus.Code)
} }
} if statusErr, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes) t.Errorf("expected a StatusError, got %T", err)
if !ok { } else if isStatusErr {
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes") if statusErr.ErrStatus.Code != tt.ExpectStatusCode {
continue t.Errorf("expected status code %d, got %d", tt.ExpectStatusCode, statusErr.ErrStatus.Code)
} }
if len(tt.ExpectAnnotations) == 0 { }
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.") fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
} else { if !ok {
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.") 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)
}
}
})
} }
} }
...@@ -136,7 +146,7 @@ func TestAdmitCachedClient(t *testing.T) { ...@@ -136,7 +146,7 @@ func TestAdmitCachedClient(t *testing.T) {
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) { for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
ns := "webhook-test" ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh) client, informer := webhooktesting.NewFakeMutatingDataSource(ns, webhooktesting.ConvertToMutatingWebhooks(tt.Webhooks), stopCh)
// override the webhook source. The client cache will stay the same. // override the webhook source. The client cache will stay the same.
cacheMisses := new(int32) cacheMisses := new(int32)
......
/*
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()
}
}
...@@ -10,13 +10,13 @@ go_library( ...@@ -10,13 +10,13 @@ go_library(
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace", importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//staging/src/k8s.io/api/admissionregistration/v1beta1: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/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/meta: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",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/errors: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/plugin/webhook:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library", "//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
], ],
...@@ -34,6 +34,7 @@ go_test( ...@@ -34,6 +34,7 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema: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/plugin/webhook:go_default_library",
], ],
) )
......
...@@ -19,13 +19,13 @@ package namespace ...@@ -19,13 +19,13 @@ package namespace
import ( import (
"fmt" "fmt"
"k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
utilerrors "k8s.io/apimachinery/pkg/util/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
corelisters "k8s.io/client-go/listers/core/v1" corelisters "k8s.io/client-go/listers/core/v1"
) )
...@@ -86,7 +86,7 @@ func (m *Matcher) GetNamespaceLabels(attr admission.Attributes) (map[string]stri ...@@ -86,7 +86,7 @@ func (m *Matcher) GetNamespaceLabels(attr admission.Attributes) (map[string]stri
// MatchNamespaceSelector decideds whether the request matches the // MatchNamespaceSelector decideds whether the request matches the
// namespaceSelctor of the webhook. Only when they match, the webhook is called. // namespaceSelctor of the webhook. Only when they match, the webhook is called.
func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) { func (m *Matcher) MatchNamespaceSelector(h webhook.WebhookAccessor, attr admission.Attributes) (bool, *apierrors.StatusError) {
namespaceName := attr.GetNamespace() namespaceName := attr.GetNamespace()
if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" { if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" {
// If the request is about a cluster scoped resource, and it is not a // If the request is about a cluster scoped resource, and it is not a
...@@ -96,7 +96,7 @@ func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attr ...@@ -96,7 +96,7 @@ func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attr
return true, nil return true, nil
} }
// TODO: adding an LRU cache to cache the translation // TODO: adding an LRU cache to cache the translation
selector, err := metav1.LabelSelectorAsSelector(h.NamespaceSelector) selector, err := metav1.LabelSelectorAsSelector(h.GetNamespaceSelector())
if err != nil { if err != nil {
return false, apierrors.NewInternalError(err) return false, apierrors.NewInternalError(err)
} }
......
...@@ -27,6 +27,7 @@ import ( ...@@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
) )
type fakeNamespaceLister struct { type fakeNamespaceLister struct {
...@@ -114,12 +115,12 @@ func TestGetNamespaceLabels(t *testing.T) { ...@@ -114,12 +115,12 @@ func TestGetNamespaceLabels(t *testing.T) {
} }
func TestNotExemptClusterScopedResource(t *testing.T) { func TestNotExemptClusterScopedResource(t *testing.T) {
hook := &registrationv1beta1.Webhook{ hook := &registrationv1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
} }
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(hook, attr) matches, err := matcher.MatchNamespaceSelector(webhook.NewValidatingWebhookAccessor("mock-hook", hook), attr)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"matcher.go",
],
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/object",
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/object",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["matcher_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
],
)
/*
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 object defines the utilities that are used by the webhook plugin to
// decide if a webhook should run, as long as either the old object or the new
// object has labels matching the webhook config's objectSelector.
package object // import "k8s.io/apiserver/pkg/admission/plugin/webhook/object"
/*
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 object
import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/klog"
)
// Matcher decides if a request selected by the ObjectSelector.
type Matcher struct {
}
func matchObject(obj runtime.Object, selector labels.Selector) bool {
if obj == nil {
return false
}
accessor, err := meta.Accessor(obj)
if err != nil {
klog.V(5).Infof("cannot access metadata of %v: %v", obj, err)
return false
}
return selector.Matches(labels.Set(accessor.GetLabels()))
}
// MatchObjectSelector decideds whether the request matches the ObjectSelector
// of the webhook. Only when they match, the webhook is called.
func (m *Matcher) MatchObjectSelector(h webhook.WebhookAccessor, attr admission.Attributes) (bool, *apierrors.StatusError) {
// TODO: adding an LRU cache to cache the translation
selector, err := metav1.LabelSelectorAsSelector(h.GetObjectSelector())
if err != nil {
return false, apierrors.NewInternalError(err)
}
if selector.Empty() {
return true, nil
}
return matchObject(attr.GetObject(), selector) || matchObject(attr.GetOldObject(), selector), nil
}
/*
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 object
import (
"testing"
"k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
)
func TestObjectSelector(t *testing.T) {
nodeLevel1 := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"runlevel": "1",
},
},
}
nodeLevel2 := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"runlevel": "2",
},
},
}
runLevel1Excluder := &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "runlevel",
Operator: metav1.LabelSelectorOpNotIn,
Values: []string{"1"},
},
},
}
matcher := &Matcher{}
allScopes := v1beta1.AllScopes
testcases := []struct {
name string
objectSelector *metav1.LabelSelector
attrs admission.Attributes
expectCall bool
}{
{
name: "empty object selector matches everything",
objectSelector: &metav1.LabelSelector{},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectCall: true,
},
{
name: "matches new object",
objectSelector: runLevel1Excluder,
attrs: admission.NewAttributesRecord(nodeLevel2, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectCall: true,
},
{
name: "matches old object",
objectSelector: runLevel1Excluder,
attrs: admission.NewAttributesRecord(nil, nodeLevel2, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Delete, &metav1.DeleteOptions{}, false, nil),
expectCall: true,
},
{
name: "does not match new object",
objectSelector: runLevel1Excluder,
attrs: admission.NewAttributesRecord(nodeLevel1, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectCall: false,
},
{
name: "does not match old object",
objectSelector: runLevel1Excluder,
attrs: admission.NewAttributesRecord(nil, nodeLevel1, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectCall: false,
},
{
name: "does not match object that does not implement Object interface",
objectSelector: runLevel1Excluder,
attrs: admission.NewAttributesRecord(&corev1.NodeProxyOptions{}, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectCall: false,
},
{
name: "empty selector matches everything, including object that does not implement Object interface",
objectSelector: &metav1.LabelSelector{},
attrs: admission.NewAttributesRecord(&corev1.NodeProxyOptions{}, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectCall: true,
},
}
for _, testcase := range testcases {
hook := &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: testcase.objectSelector,
Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"},
Rule: v1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
}}}
t.Run(testcase.name, func(t *testing.T) {
match, err := matcher.MatchObjectSelector(webhook.NewValidatingWebhookAccessor("mock-hook", hook), testcase.attrs)
if err != nil {
t.Error(err)
}
if testcase.expectCall && !match {
t.Errorf("expected the webhook to be called")
}
if !testcase.expectCall && match {
t.Errorf("expected the webhook to be called")
}
})
}
}
...@@ -82,6 +82,17 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) { ...@@ -82,6 +82,17 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
}, },
}, },
}) })
case "/shouldNotBeCalled":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
Response: &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: "doesn't expect labels to match object selector",
Code: http.StatusForbidden,
},
},
})
case "/allow": case "/allow":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
...@@ -138,6 +149,13 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) { ...@@ -138,6 +149,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)
} }
......
...@@ -7,7 +7,7 @@ go_library( ...@@ -7,7 +7,7 @@ go_library(
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/util", importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/util",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
], ],
) )
......
...@@ -17,38 +17,39 @@ limitations under the License. ...@@ -17,38 +17,39 @@ limitations under the License.
package util package util
import ( import (
"k8s.io/api/admissionregistration/v1beta1" "k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/util/webhook" webhookutil "k8s.io/apiserver/pkg/util/webhook"
) )
// HookClientConfigForWebhook construct a webhook.ClientConfig using a v1beta1.Webhook API object. // HookClientConfigForWebhook construct a webhookutil.ClientConfig using a WebhookAccessor to access
// webhook.ClientConfig is used to create a HookClient and the purpose of the config struct is to // v1beta1.MutatingWebhook and v1beta1.ValidatingWebhook API objects. webhookutil.ClientConfig is used
// share that with other packages that need to create a HookClient. // to create a HookClient and the purpose of the config struct is to share that with other packages
func HookClientConfigForWebhook(w *v1beta1.Webhook) webhook.ClientConfig { // that need to create a HookClient.
ret := webhook.ClientConfig{Name: w.Name, CABundle: w.ClientConfig.CABundle} func HookClientConfigForWebhook(w webhook.WebhookAccessor) webhookutil.ClientConfig {
if w.ClientConfig.URL != nil { ret := webhookutil.ClientConfig{Name: w.GetName(), CABundle: w.GetClientConfig().CABundle}
ret.URL = *w.ClientConfig.URL if w.GetClientConfig().URL != nil {
ret.URL = *w.GetClientConfig().URL
} }
if w.ClientConfig.Service != nil { if w.GetClientConfig().Service != nil {
ret.Service = &webhook.ClientConfigService{ ret.Service = &webhookutil.ClientConfigService{
Name: w.ClientConfig.Service.Name, Name: w.GetClientConfig().Service.Name,
Namespace: w.ClientConfig.Service.Namespace, Namespace: w.GetClientConfig().Service.Namespace,
} }
if w.ClientConfig.Service.Port != nil { if w.GetClientConfig().Service.Port != nil {
ret.Service.Port = *w.ClientConfig.Service.Port ret.Service.Port = *w.GetClientConfig().Service.Port
} else { } else {
ret.Service.Port = 443 ret.Service.Port = 443
} }
if w.ClientConfig.Service.Path != nil { if w.GetClientConfig().Service.Path != nil {
ret.Service.Path = *w.ClientConfig.Service.Path ret.Service.Path = *w.GetClientConfig().Service.Path
} }
} }
return ret return ret
} }
// HasAdmissionReviewVersion check whether a version is accepted by a given webhook. // HasAdmissionReviewVersion check whether a version is accepted by a given webhook.
func HasAdmissionReviewVersion(a string, w *v1beta1.Webhook) bool { func HasAdmissionReviewVersion(a string, w webhook.WebhookAccessor) bool {
for _, b := range w.AdmissionReviewVersions { for _, b := range w.GetAdmissionReviewVersions() {
if b == a { if b == a {
return true return true
} }
......
...@@ -19,6 +19,7 @@ go_library( ...@@ -19,6 +19,7 @@ go_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",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library",
......
...@@ -22,8 +22,6 @@ import ( ...@@ -22,8 +22,6 @@ import (
"sync" "sync"
"time" "time"
"k8s.io/klog"
admissionv1beta1 "k8s.io/api/admission/v1beta1" admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1beta1" "k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
...@@ -31,36 +29,55 @@ import ( ...@@ -31,36 +29,55 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors" webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/request" "k8s.io/apiserver/pkg/admission/plugin/webhook/request"
"k8s.io/apiserver/pkg/admission/plugin/webhook/util" "k8s.io/apiserver/pkg/admission/plugin/webhook/util"
"k8s.io/apiserver/pkg/util/webhook" webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/klog"
) )
type validatingDispatcher struct { type validatingDispatcher struct {
cm *webhook.ClientManager cm *webhookutil.ClientManager
plugin *Plugin
} }
func newValidatingDispatcher(cm *webhook.ClientManager) generic.Dispatcher { func newValidatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher {
return &validatingDispatcher{cm} return func(cm *webhookutil.ClientManager) generic.Dispatcher {
return &validatingDispatcher{cm, p}
}
} }
var _ generic.Dispatcher = &validatingDispatcher{} var _ generic.Dispatcher = &validatingDispatcher{}
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, relevantHooks []*generic.WebhookInvocation) error { func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
var relevantHooks []*generic.WebhookInvocation
// Construct all the versions we need to call our webhooks // Construct all the versions we need to call our webhooks
versionedAttrs := map[schema.GroupVersionKind]*generic.VersionedAttributes{} versionedAttrs := map[schema.GroupVersionKind]*generic.VersionedAttributes{}
for _, call := range relevantHooks { for _, hook := range hooks {
invocation, statusError := d.plugin.ShouldCallHook(hook, attr, o)
if statusError != nil {
return statusError
}
if invocation == nil {
continue
}
relevantHooks = append(relevantHooks, invocation)
// If we already have this version, continue // If we already have this version, continue
if _, ok := versionedAttrs[call.Kind]; ok { if _, ok := versionedAttrs[invocation.Kind]; ok {
continue continue
} }
versionedAttr, err := generic.NewVersionedAttributes(attr, call.Kind, o) versionedAttr, err := generic.NewVersionedAttributes(attr, invocation.Kind, o)
if err != nil { if err != nil {
return apierrors.NewInternalError(err) return apierrors.NewInternalError(err)
} }
versionedAttrs[call.Kind] = versionedAttr versionedAttrs[invocation.Kind] = versionedAttr
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
} }
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
...@@ -69,18 +86,21 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr ...@@ -69,18 +86,21 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
for i := range relevantHooks { for i := range relevantHooks {
go func(invocation *generic.WebhookInvocation) { go func(invocation *generic.WebhookInvocation) {
defer wg.Done() defer wg.Done()
hook := invocation.Webhook hook, ok := invocation.Webhook.GetValidatingWebhook()
if !ok {
utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1beta1.ValidatingWebhook, but got %T", hook))
return
}
versionedAttr := versionedAttrs[invocation.Kind] versionedAttr := versionedAttrs[invocation.Kind]
t := time.Now() t := time.Now()
err := d.callHook(ctx, invocation, versionedAttr) err := d.callHook(ctx, hook, invocation, versionedAttr)
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "validating", hook.Name) admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "validating", hook.Name)
if err == nil { if err == nil {
return return
} }
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
if callErr, ok := err.(*webhook.ErrCallingWebhook); ok { if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
if ignoreClientCallFailures { if ignoreClientCallFailures {
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr) klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr) utilruntime.HandleError(callErr)
...@@ -115,11 +135,10 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr ...@@ -115,11 +135,10 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
return errs[0] return errs[0]
} }
func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error { func (d *validatingDispatcher) callHook(ctx context.Context, h *v1beta1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
h := invocation.Webhook
if attr.Attributes.IsDryRun() { if attr.Attributes.IsDryRun() {
if h.SideEffects == nil { if h.SideEffects == nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")} return &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 webhookerrors.NewDryRunUnsupportedErr(h.Name)
...@@ -128,15 +147,15 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic ...@@ -128,15 +147,15 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic
// 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, h) { if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")} return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")}
} }
// Make the webhook request // Make the webhook request
request := request.CreateAdmissionReview(attr, invocation) request := request.CreateAdmissionReview(attr, invocation)
client, err := d.cm.HookClient(util.HookClientConfigForWebhook(h)) client, err := d.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
if err != nil { if err != nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err} return &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)
...@@ -144,11 +163,11 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic ...@@ -144,11 +163,11 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic
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 &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err} return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
} }
if response.Response == nil { if response.Response == nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")} return &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 {
key := h.Name + "/" + k key := h.Name + "/" + k
......
...@@ -51,11 +51,13 @@ var _ admission.ValidationInterface = &Plugin{} ...@@ -51,11 +51,13 @@ var _ admission.ValidationInterface = &Plugin{}
// NewValidatingAdmissionWebhook returns a generic admission webhook plugin. // NewValidatingAdmissionWebhook returns a generic admission webhook plugin.
func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) { func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) {
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update) handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
webhook, err := generic.NewWebhook(handler, configFile, configuration.NewValidatingWebhookConfigurationManager, newValidatingDispatcher) p := &Plugin{}
var err error
p.Webhook, err = generic.NewWebhook(handler, configFile, configuration.NewValidatingWebhookConfigurationManager, newValidatingDispatcher(p))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Plugin{webhook}, nil return p, nil
} }
// Validate makes an admission decision based on the request attributes. // Validate makes an admission decision based on the request attributes.
......
...@@ -51,7 +51,7 @@ func TestValidate(t *testing.T) { ...@@ -51,7 +51,7 @@ func TestValidate(t *testing.T) {
} }
ns := "webhook-test" ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh) client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32)))) wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL)) wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
...@@ -116,7 +116,7 @@ func TestValidateCachedClient(t *testing.T) { ...@@ -116,7 +116,7 @@ func TestValidateCachedClient(t *testing.T) {
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) { for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
ns := "webhook-test" ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh) client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
// override the webhook source. The client cache will stay the same. // override the webhook source. The client cache will stay the same.
cacheMisses := new(int32) cacheMisses := new(int32)
......
...@@ -160,7 +160,7 @@ func (ps *Plugins) NewFromPlugins(pluginNames []string, configProvider ConfigPro ...@@ -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.
......
/*
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)
}
...@@ -436,7 +436,7 @@ func registerWebhook(f *framework.Framework, context *certContext) func() { ...@@ -436,7 +436,7 @@ func registerWebhook(f *framework.Framework, context *certContext) func() {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.ValidatingWebhook{
{ {
Name: "deny-unwanted-pod-container-name-and-label.k8s.io", Name: "deny-unwanted-pod-container-name-and-label.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -517,7 +517,7 @@ func registerWebhookForAttachingPod(f *framework.Framework, context *certContext ...@@ -517,7 +517,7 @@ func registerWebhookForAttachingPod(f *framework.Framework, context *certContext
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.ValidatingWebhook{
{ {
Name: "deny-attaching-pod.k8s.io", Name: "deny-attaching-pod.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -561,7 +561,7 @@ func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certCo ...@@ -561,7 +561,7 @@ func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certCo
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.MutatingWebhook{
{ {
Name: "adding-configmap-data-stage-1.k8s.io", Name: "adding-configmap-data-stage-1.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -638,7 +638,7 @@ func registerMutatingWebhookForPod(f *framework.Framework, context *certContext) ...@@ -638,7 +638,7 @@ func registerMutatingWebhookForPod(f *framework.Framework, context *certContext)
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.MutatingWebhook{
{ {
Name: "adding-init-container.k8s.io", Name: "adding-init-container.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -841,8 +841,8 @@ func testAttachingPodWebhook(f *framework.Framework) { ...@@ -841,8 +841,8 @@ func testAttachingPodWebhook(f *framework.Framework) {
// failingWebhook returns a webhook with rule of create configmaps, // failingWebhook returns a webhook with rule of create configmaps,
// but with an invalid client config so that server cannot communicate with it // but with an invalid client config so that server cannot communicate with it
func failingWebhook(namespace, name string) v1beta1.Webhook { func failingWebhook(namespace, name string) v1beta1.ValidatingWebhook {
return v1beta1.Webhook{ return v1beta1.ValidatingWebhook{
Name: name, Name: name,
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{v1beta1.Create}, Operations: []v1beta1.OperationType{v1beta1.Create},
...@@ -889,7 +889,7 @@ func registerFailClosedWebhook(f *framework.Framework, context *certContext) fun ...@@ -889,7 +889,7 @@ func registerFailClosedWebhook(f *framework.Framework, context *certContext) fun
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.ValidatingWebhook{
// Server cannot talk to this webhook, so it always fails. // Server cannot talk to this webhook, so it always fails.
// Because this webhook is configured fail-closed, request should be rejected after the call fails. // Because this webhook is configured fail-closed, request should be rejected after the call fails.
hook, hook,
...@@ -945,7 +945,7 @@ func registerValidatingWebhookForWebhookConfigurations(f *framework.Framework, c ...@@ -945,7 +945,7 @@ func registerValidatingWebhookForWebhookConfigurations(f *framework.Framework, c
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.ValidatingWebhook{
{ {
Name: "deny-webhook-configuration-deletions.k8s.io", Name: "deny-webhook-configuration-deletions.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -998,7 +998,7 @@ func registerMutatingWebhookForWebhookConfigurations(f *framework.Framework, con ...@@ -998,7 +998,7 @@ func registerMutatingWebhookForWebhookConfigurations(f *framework.Framework, con
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.MutatingWebhook{
{ {
Name: "add-label-to-webhook-configurations.k8s.io", Name: "add-label-to-webhook-configurations.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -1050,7 +1050,7 @@ func testWebhooksForWebhookConfigurations(f *framework.Framework) { ...@@ -1050,7 +1050,7 @@ func testWebhooksForWebhookConfigurations(f *framework.Framework) {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: dummyValidatingWebhookConfigName, Name: dummyValidatingWebhookConfigName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.ValidatingWebhook{
{ {
Name: "dummy-validating-webhook.k8s.io", Name: "dummy-validating-webhook.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -1098,7 +1098,7 @@ func testWebhooksForWebhookConfigurations(f *framework.Framework) { ...@@ -1098,7 +1098,7 @@ func testWebhooksForWebhookConfigurations(f *framework.Framework) {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: dummyMutatingWebhookConfigName, Name: dummyMutatingWebhookConfigName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.MutatingWebhook{
{ {
Name: "dummy-mutating-webhook.k8s.io", Name: "dummy-mutating-webhook.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -1306,7 +1306,7 @@ func registerWebhookForCustomResource(f *framework.Framework, context *certConte ...@@ -1306,7 +1306,7 @@ func registerWebhookForCustomResource(f *framework.Framework, context *certConte
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.ValidatingWebhook{
{ {
Name: "deny-unwanted-custom-resource-data.k8s.io", Name: "deny-unwanted-custom-resource-data.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -1348,7 +1348,7 @@ func registerMutatingWebhookForCustomResource(f *framework.Framework, context *c ...@@ -1348,7 +1348,7 @@ func registerMutatingWebhookForCustomResource(f *framework.Framework, context *c
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.MutatingWebhook{
{ {
Name: "mutate-custom-resource-data-stage-1.k8s.io", Name: "mutate-custom-resource-data-stage-1.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -1543,7 +1543,7 @@ func registerValidatingWebhookForCRD(f *framework.Framework, context *certContex ...@@ -1543,7 +1543,7 @@ func registerValidatingWebhookForCRD(f *framework.Framework, context *certContex
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.ValidatingWebhook{
{ {
Name: "deny-crd-with-unwanted-label.k8s.io", Name: "deny-crd-with-unwanted-label.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
...@@ -1649,7 +1649,7 @@ func registerSlowWebhook(f *framework.Framework, context *certContext, policy *v ...@@ -1649,7 +1649,7 @@ func registerSlowWebhook(f *framework.Framework, context *certContext, policy *v
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: configName, Name: configName,
}, },
Webhooks: []v1beta1.Webhook{ Webhooks: []v1beta1.ValidatingWebhook{
{ {
Name: "allow-configmap-with-delay-webhook.k8s.io", Name: "allow-configmap-with-delay-webhook.k8s.io",
Rules: []v1beta1.RuleWithOperations{{ Rules: []v1beta1.RuleWithOperations{{
......
...@@ -6,6 +6,7 @@ go_test( ...@@ -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( ...@@ -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",
......
...@@ -283,6 +283,9 @@ func (h *holder) record(phase string, converted bool, request *v1beta1.Admission ...@@ -283,6 +283,9 @@ func (h *holder) record(phase string, converted bool, request *v1beta1.Admission
return return
} }
if debug {
h.t.Logf("recording: %#v = %s %#v %v", webhookOptions{phase: phase, converted: converted}, request.Operation, request.Resource, request.SubResource)
}
h.recorded[webhookOptions{phase: phase, converted: converted}] = request h.recorded[webhookOptions{phase: phase, converted: converted}] = request
} }
...@@ -1287,7 +1290,7 @@ func createV1beta1ValidationWebhook(client clientset.Interface, endpoint, conver ...@@ -1287,7 +1290,7 @@ func createV1beta1ValidationWebhook(client clientset.Interface, endpoint, conver
// Attaching Admission webhook to API server // Attaching Admission webhook to API server
_, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&admissionv1beta1.ValidatingWebhookConfiguration{ _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&admissionv1beta1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"}, ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"},
Webhooks: []admissionv1beta1.Webhook{ Webhooks: []admissionv1beta1.ValidatingWebhook{
{ {
Name: "admission.integration.test", Name: "admission.integration.test",
ClientConfig: admissionv1beta1.WebhookClientConfig{ ClientConfig: admissionv1beta1.WebhookClientConfig{
...@@ -1323,7 +1326,7 @@ func createV1beta1MutationWebhook(client clientset.Interface, endpoint, converte ...@@ -1323,7 +1326,7 @@ func createV1beta1MutationWebhook(client clientset.Interface, endpoint, converte
// Attaching Mutation webhook to API server // Attaching Mutation webhook to API server
_, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{ _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"}, ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"},
Webhooks: []admissionv1beta1.Webhook{ Webhooks: []admissionv1beta1.MutatingWebhook{
{ {
Name: "mutation.integration.test", Name: "mutation.integration.test",
ClientConfig: admissionv1beta1.WebhookClientConfig{ ClientConfig: admissionv1beta1.WebhookClientConfig{
......
...@@ -155,7 +155,7 @@ func brokenWebhookConfig(name string) *admissionregistrationv1beta1.ValidatingWe ...@@ -155,7 +155,7 @@ func brokenWebhookConfig(name string) *admissionregistrationv1beta1.ValidatingWe
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
}, },
Webhooks: []admissionregistrationv1beta1.Webhook{ Webhooks: []admissionregistrationv1beta1.ValidatingWebhook{
{ {
Name: "broken-webhook.k8s.io", Name: "broken-webhook.k8s.io",
Rules: []admissionregistrationv1beta1.RuleWithOperations{{ Rules: []admissionregistrationv1beta1.RuleWithOperations{{
......
...@@ -22,7 +22,7 @@ import ( ...@@ -22,7 +22,7 @@ import (
"time" "time"
admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1" admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
auditinternal "k8s.io/apiserver/pkg/apis/audit" auditinternal "k8s.io/apiserver/pkg/apis/audit"
...@@ -65,7 +65,7 @@ func TestWebhookLoopback(t *testing.T) { ...@@ -65,7 +65,7 @@ func TestWebhookLoopback(t *testing.T) {
fail := admissionv1beta1.Fail fail := admissionv1beta1.Fail
_, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{ _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "webhooktest.example.com"}, ObjectMeta: metav1.ObjectMeta{Name: "webhooktest.example.com"},
Webhooks: []admissionv1beta1.Webhook{{ Webhooks: []admissionv1beta1.MutatingWebhook{{
Name: "webhooktest.example.com", Name: "webhooktest.example.com",
ClientConfig: admissionv1beta1.WebhookClientConfig{ ClientConfig: admissionv1beta1.WebhookClientConfig{
Service: &admissionv1beta1.ServiceReference{Namespace: "default", Name: "kubernetes", Path: &webhookPath}, Service: &admissionv1beta1.ServiceReference{Namespace: "default", Name: "kubernetes", Path: &webhookPath},
......
...@@ -1185,6 +1185,7 @@ k8s.io/apiserver/pkg/admission/configuration ...@@ -1185,6 +1185,7 @@ k8s.io/apiserver/pkg/admission/configuration
k8s.io/apiserver/pkg/admission/initializer k8s.io/apiserver/pkg/admission/initializer
k8s.io/apiserver/pkg/admission/metrics k8s.io/apiserver/pkg/admission/metrics
k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle
k8s.io/apiserver/pkg/admission/plugin/webhook
k8s.io/apiserver/pkg/admission/plugin/webhook/config k8s.io/apiserver/pkg/admission/plugin/webhook/config
k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission
k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1 k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1
...@@ -1193,6 +1194,7 @@ k8s.io/apiserver/pkg/admission/plugin/webhook/generic ...@@ -1193,6 +1194,7 @@ k8s.io/apiserver/pkg/admission/plugin/webhook/generic
k8s.io/apiserver/pkg/admission/plugin/webhook/initializer k8s.io/apiserver/pkg/admission/plugin/webhook/initializer
k8s.io/apiserver/pkg/admission/plugin/webhook/mutating k8s.io/apiserver/pkg/admission/plugin/webhook/mutating
k8s.io/apiserver/pkg/admission/plugin/webhook/namespace k8s.io/apiserver/pkg/admission/plugin/webhook/namespace
k8s.io/apiserver/pkg/admission/plugin/webhook/object
k8s.io/apiserver/pkg/admission/plugin/webhook/request k8s.io/apiserver/pkg/admission/plugin/webhook/request
k8s.io/apiserver/pkg/admission/plugin/webhook/rules k8s.io/apiserver/pkg/admission/plugin/webhook/rules
k8s.io/apiserver/pkg/admission/plugin/webhook/util k8s.io/apiserver/pkg/admission/plugin/webhook/util
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment