From 5e5b3029f3bbfc93c3569f07ad300a5c6057fc58 Mon Sep 17 00:00:00 2001 From: Igor Velichkovich Date: Tue, 14 Mar 2023 22:28:26 -0500 Subject: [PATCH] Matchconditions admission webhooks alpha implementation for kep-3716 (#116261) * api changes adding match conditions * feature gate and registry strategy to drop fields * matchConditions logic for admission webhooks * feedback * update test * import order * bears.com * update fail policy ignore behavior * update docs and matcher to hold fail policy as non-pointer * update matcher error aggregation, fix early fail failpolicy ignore, update docs * final cleanup * openapi gen --- api/openapi-spec/swagger.json | 44 ++ ...issionregistration.k8s.io__v1_openapi.json | 56 ++ pkg/apis/admissionregistration/types.go | 65 ++ pkg/apis/admissionregistration/util.go | 66 ++ pkg/apis/admissionregistration/util_test.go | 417 ++++++++++ .../v1/zz_generated.conversion.go | 36 + .../v1beta1/zz_generated.conversion.go | 36 + .../validation/validation.go | 83 +- .../validation/validation_test.go | 459 ++++++++++- .../zz_generated.deepcopy.go | 26 + pkg/features/kube_features.go | 2 + pkg/generated/openapi/zz_generated.openapi.go | 166 +++- .../mutatingwebhookconfiguration/strategy.go | 3 + .../strategy.go | 3 + .../admissionregistration/v1/generated.pb.go | 484 ++++++++++-- .../admissionregistration/v1/generated.proto | 73 ++ .../api/admissionregistration/v1/types.go | 73 ++ .../v1/types_swagger_doc_generated.go | 12 + .../v1/zz_generated.deepcopy.go | 26 + .../v1beta1/generated.pb.go | 465 ++++++++++-- .../v1beta1/generated.proto | 73 ++ .../admissionregistration/v1beta1/types.go | 73 ++ .../v1beta1/types_swagger_doc_generated.go | 12 + .../v1beta1/zz_generated.deepcopy.go | 26 + ...8s.io.v1.MutatingWebhookConfiguration.json | 8 +- ....k8s.io.v1.MutatingWebhookConfiguration.pb | Bin 854 -> 884 bytes ...8s.io.v1.MutatingWebhookConfiguration.yaml | 3 + ....io.v1.ValidatingWebhookConfiguration.json | 6 + ...8s.io.v1.ValidatingWebhookConfiguration.pb | Bin 831 -> 861 bytes ....io.v1.ValidatingWebhookConfiguration.yaml | 3 + ....v1beta1.MutatingWebhookConfiguration.json | 8 +- ...io.v1beta1.MutatingWebhookConfiguration.pb | Bin 859 -> 889 bytes ....v1beta1.MutatingWebhookConfiguration.yaml | 3 + ...1beta1.ValidatingWebhookConfiguration.json | 6 + ....v1beta1.ValidatingWebhookConfiguration.pb | Bin 836 -> 866 bytes ...1beta1.ValidatingWebhookConfiguration.yaml | 3 + .../pkg/admission/metrics/metrics.go | 31 +- .../pkg/admission/plugin/cel/interface.go | 15 - .../validatingadmissionpolicy/validator.go | 3 +- .../pkg/admission/plugin/webhook/accessors.go | 69 +- .../plugin/webhook/accessors_test.go | 2 + .../plugin/webhook/generic/interfaces.go | 4 + .../plugin/webhook/generic/webhook.go | 33 +- .../plugin/webhook/generic/webhook_test.go | 199 ++++- .../webhook/matchconditions/interface.go | 36 + .../plugin/webhook/matchconditions/matcher.go | 139 ++++ .../webhook/matchconditions/matcher_test.go | 360 +++++++++ .../plugin/webhook/mutating/dispatcher.go | 61 +- .../plugin/webhook/validating/dispatcher.go | 39 +- .../k8s.io/apiserver/pkg/apis/cel/config.go | 5 + .../apiserver/pkg/features/kube_features.go | 10 + .../v1/matchcondition.go | 48 ++ .../v1/mutatingwebhook.go | 14 + .../v1/validatingwebhook.go | 14 + .../v1beta1/matchcondition.go | 48 ++ .../v1beta1/mutatingwebhook.go | 14 + .../v1beta1/validatingwebhook.go | 14 + .../applyconfigurations/internal/internal.go | 54 ++ .../client-go/applyconfigurations/utils.go | 4 + .../admissionwebhook/match_conditions_test.go | 712 ++++++++++++++++++ .../mutating_webhook_gvk_conversion_test.go | 400 ++++++++++ vendor/modules.txt | 1 + 62 files changed, 4909 insertions(+), 239 deletions(-) create mode 100644 pkg/apis/admissionregistration/util.go create mode 100644 pkg/apis/admissionregistration/util_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/interface.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/matchcondition.go create mode 100644 staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/matchcondition.go create mode 100644 test/integration/apiserver/admissionwebhook/match_conditions_test.go create mode 100644 test/integration/apiserver/admissionwebhook/mutating_webhook_gvk_conversion_test.go diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 96b06a7cce9..b94fc8308ee 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -1,5 +1,23 @@ { "definitions": { + "io.k8s.api.admissionregistration.v1.MatchCondition": { + "description": "MatchCondition represents a condition which must by fulfilled for a request to be sent to a webhook.", + "properties": { + "expression": { + "description": "Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\nDocumentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/\n\nRequired.", + "type": "string" + }, + "name": { + "description": "Name is an identifier for this match condition, used for strategic merging of MatchConditions, as well as providing an identifier for logging purposes. A good name should be descriptive of the associated expression. Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')\n\nRequired.", + "type": "string" + } + }, + "required": [ + "name", + "expression" + ], + "type": "object" + }, "io.k8s.api.admissionregistration.v1.MutatingWebhook": { "description": "MutatingWebhook describes an admission webhook and the resources and operations it applies to.", "properties": { @@ -18,6 +36,19 @@ "description": "FailurePolicy defines how unrecognized errors from the admission endpoint are handled - allowed values are Ignore or Fail. Defaults to Fail.", "type": "string" }, + "matchConditions": { + "description": "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", + "items": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1.MatchCondition" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, "matchPolicy": { "description": "matchPolicy defines how the \"rules\" list is used to match incoming requests. Allowed values are \"Exact\" or \"Equivalent\".\n\n- 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.\n\n- 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.\n\nDefaults to \"Equivalent\"", "type": "string" @@ -219,6 +250,19 @@ "description": "FailurePolicy defines how unrecognized errors from the admission endpoint are handled - allowed values are Ignore or Fail. Defaults to Fail.", "type": "string" }, + "matchConditions": { + "description": "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", + "items": { + "$ref": "#/definitions/io.k8s.api.admissionregistration.v1.MatchCondition" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, "matchPolicy": { "description": "matchPolicy defines how the \"rules\" list is used to match incoming requests. Allowed values are \"Exact\" or \"Equivalent\".\n\n- 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.\n\n- 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.\n\nDefaults to \"Equivalent\"", "type": "string" diff --git a/api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json b/api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json index 1afcc986a60..e1f0301d020 100644 --- a/api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json +++ b/api/openapi-spec/v3/apis__admissionregistration.k8s.io__v1_openapi.json @@ -1,6 +1,26 @@ { "components": { "schemas": { + "io.k8s.api.admissionregistration.v1.MatchCondition": { + "description": "MatchCondition represents a condition which must by fulfilled for a request to be sent to a webhook.", + "properties": { + "expression": { + "default": "", + "description": "Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\nDocumentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/\n\nRequired.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name is an identifier for this match condition, used for strategic merging of MatchConditions, as well as providing an identifier for logging purposes. A good name should be descriptive of the associated expression. Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')\n\nRequired.", + "type": "string" + } + }, + "required": [ + "name", + "expression" + ], + "type": "object" + }, "io.k8s.api.admissionregistration.v1.MutatingWebhook": { "description": "MutatingWebhook describes an admission webhook and the resources and operations it applies to.", "properties": { @@ -25,6 +45,24 @@ "description": "FailurePolicy defines how unrecognized errors from the admission endpoint are handled - allowed values are Ignore or Fail. Defaults to Fail.", "type": "string" }, + "matchConditions": { + "description": "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1.MatchCondition" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, "matchPolicy": { "description": "matchPolicy defines how the \"rules\" list is used to match incoming requests. Allowed values are \"Exact\" or \"Equivalent\".\n\n- 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.\n\n- 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.\n\nDefaults to \"Equivalent\"", "type": "string" @@ -272,6 +310,24 @@ "description": "FailurePolicy defines how unrecognized errors from the admission endpoint are handled - allowed values are Ignore or Fail. Defaults to Fail.", "type": "string" }, + "matchConditions": { + "description": "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.admissionregistration.v1.MatchCondition" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, "matchPolicy": { "description": "matchPolicy defines how the \"rules\" list is used to match incoming requests. Allowed values are \"Exact\" or \"Equivalent\".\n\n- 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.\n\n- 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.\n\nDefaults to \"Equivalent\"", "type": "string" diff --git a/pkg/apis/admissionregistration/types.go b/pkg/apis/admissionregistration/types.go index 9288057ef01..da2194a026d 100644 --- a/pkg/apis/admissionregistration/types.go +++ b/pkg/apis/admissionregistration/types.go @@ -740,6 +740,24 @@ type ValidatingWebhook struct { // does not understand, calls to the webhook will fail and be subject to the failure policy. // +optional AdmissionReviewVersions []string + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +featureGate=AdmissionWebhookMatchConditions + // +optional + MatchConditions []MatchCondition } // MutatingWebhook describes an admission webhook and the resources and operations it applies to. @@ -882,6 +900,24 @@ type MutatingWebhook struct { // Defaults to "Never". // +optional ReinvocationPolicy *ReinvocationPolicyType + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +featureGate=AdmissionWebhookMatchConditions + // +optional + MatchConditions []MatchCondition } // ReinvocationPolicyType specifies what type of policy the admission hook uses. @@ -987,3 +1023,32 @@ type ServiceReference struct { // +optional Port int32 } + +// MatchCondition represents a condition which must by fulfilled for a request to be sent to a webhook. +type MatchCondition struct { + // Name is an identifier for this match condition, used for strategic merging of MatchConditions, + // as well as providing an identifier for logging purposes. A good name should be descriptive of + // the associated expression. + // Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and + // must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or + // '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an + // optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName') + // + // Required. + Name string + + // Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. + // CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables: + // + // 'object' - The object from the incoming request. The value is null for DELETE requests. + // 'oldObject' - The existing object. The value is null for CREATE requests. + // 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). + // 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Required. + Expression string +} diff --git a/pkg/apis/admissionregistration/util.go b/pkg/apis/admissionregistration/util.go new file mode 100644 index 00000000000..dd6e1acaa15 --- /dev/null +++ b/pkg/apis/admissionregistration/util.go @@ -0,0 +1,66 @@ +/* +Copyright 2023 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 admissionregistration + +import ( + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" +) + +// DropDisabledMutatingWebhookConfigurationFields removes disabled fields from the mutatingWebhookConfiguration metadata and spec. +// This should be called from PrepareForCreate/PrepareForUpdate for all resources containing a mutatingWebhookConfiguration +func DropDisabledMutatingWebhookConfigurationFields(mutatingWebhookConfiguration, oldMutatingWebhookConfiguration *MutatingWebhookConfiguration) { + if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AdmissionWebhookMatchConditions) && !matchConditionsInUseMutatingWebhook(oldMutatingWebhookConfiguration) { + for i := range mutatingWebhookConfiguration.Webhooks { + mutatingWebhookConfiguration.Webhooks[i].MatchConditions = nil + } + } +} + +func matchConditionsInUseMutatingWebhook(mutatingWebhookConfiguration *MutatingWebhookConfiguration) bool { + if mutatingWebhookConfiguration == nil { + return false + } + for _, webhook := range mutatingWebhookConfiguration.Webhooks { + if len(webhook.MatchConditions) != 0 { + return true + } + } + return false +} + +// DropDisabledValidatingWebhookConfigurationFields removes disabled fields from the validatingWebhookConfiguration metadata and spec. +// This should be called from PrepareForCreate/PrepareForUpdate for all resources containing a validatingWebhookConfiguration +func DropDisabledValidatingWebhookConfigurationFields(validatingWebhookConfiguration, oldValidatingWebhookConfiguration *ValidatingWebhookConfiguration) { + if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AdmissionWebhookMatchConditions) && !matchConditionsInUseValidatingWebhook(oldValidatingWebhookConfiguration) { + for i := range validatingWebhookConfiguration.Webhooks { + validatingWebhookConfiguration.Webhooks[i].MatchConditions = nil + } + } +} + +func matchConditionsInUseValidatingWebhook(validatingWebhookConfiguration *ValidatingWebhookConfiguration) bool { + if validatingWebhookConfiguration == nil { + return false + } + for _, webhook := range validatingWebhookConfiguration.Webhooks { + if len(webhook.MatchConditions) != 0 { + return true + } + } + return false +} diff --git a/pkg/apis/admissionregistration/util_test.go b/pkg/apis/admissionregistration/util_test.go new file mode 100644 index 00000000000..299ebd298d3 --- /dev/null +++ b/pkg/apis/admissionregistration/util_test.go @@ -0,0 +1,417 @@ +/* +Copyright 2023 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 admissionregistration + +import ( + "testing" + + "github.com/stretchr/testify/require" + + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +func TestDropDisabledMutatingWebhookConfigurationFields(t *testing.T) { + tests := []struct { + name string + old *MutatingWebhookConfiguration + new *MutatingWebhookConfiguration + featureGateEnabled bool + expected []MatchCondition + }{ + { + name: "create with no match conditions, feature gate off", + old: nil, + new: &MutatingWebhookConfiguration{ + Webhooks: []MutatingWebhook{ + {}, + }, + }, + featureGateEnabled: false, + expected: nil, + }, + { + name: "create with match conditions, feature gate off", + old: nil, + new: &MutatingWebhookConfiguration{ + Webhooks: []MutatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + featureGateEnabled: false, + expected: nil, + }, + { + name: "create with no match conditions, feature gate on", + old: nil, + new: &MutatingWebhookConfiguration{ + Webhooks: []MutatingWebhook{ + {}, + {}, + }, + }, + featureGateEnabled: true, + expected: nil, + }, + { + name: "create with match conditions, feature gate on", + old: nil, + new: &MutatingWebhookConfiguration{ + Webhooks: []MutatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + featureGateEnabled: true, + expected: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + name: "update with old has match conditions feature gate on", + old: &MutatingWebhookConfiguration{ + Webhooks: []MutatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + new: &MutatingWebhookConfiguration{ + Webhooks: []MutatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + featureGateEnabled: true, + expected: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + name: "update with old has match conditions feature gate off", + old: &MutatingWebhookConfiguration{ + Webhooks: []MutatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + new: &MutatingWebhookConfiguration{ + Webhooks: []MutatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + featureGateEnabled: false, + expected: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AdmissionWebhookMatchConditions, test.featureGateEnabled)() + DropDisabledMutatingWebhookConfigurationFields(test.new, test.old) + + for _, hook := range test.new.Webhooks { + if test.expected == nil { + if hook.MatchConditions != nil { + t.Error("expected all hooks matchConditions to be nil") + } + } else { + require.Equal(t, len(test.expected), len(hook.MatchConditions)) + for i, matchCondition := range hook.MatchConditions { + require.Equal(t, test.expected[i], matchCondition) + } + } + } + }) + } +} + +func TestDropDisabledValidatingWebhookConfigurationFields(t *testing.T) { + tests := []struct { + name string + old *ValidatingWebhookConfiguration + new *ValidatingWebhookConfiguration + featureGateEnabled bool + expected []MatchCondition + }{ + { + name: "create with no match conditions, feature gate off", + old: nil, + new: &ValidatingWebhookConfiguration{ + Webhooks: []ValidatingWebhook{ + {}, + }, + }, + featureGateEnabled: false, + expected: nil, + }, + { + name: "create with match conditions, feature gate off", + old: nil, + new: &ValidatingWebhookConfiguration{ + Webhooks: []ValidatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + featureGateEnabled: false, + expected: nil, + }, + { + name: "create with no match conditions, feature gate on", + old: nil, + new: &ValidatingWebhookConfiguration{ + Webhooks: []ValidatingWebhook{ + {}, + {}, + }, + }, + featureGateEnabled: true, + expected: nil, + }, + { + name: "create with match conditions, feature gate on", + old: nil, + new: &ValidatingWebhookConfiguration{ + Webhooks: []ValidatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + featureGateEnabled: true, + expected: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + name: "update with old has match conditions feature gate on", + old: &ValidatingWebhookConfiguration{ + Webhooks: []ValidatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + new: &ValidatingWebhookConfiguration{ + Webhooks: []ValidatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + featureGateEnabled: true, + expected: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + name: "update with old has match conditions feature gate off", + old: &ValidatingWebhookConfiguration{ + Webhooks: []ValidatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + new: &ValidatingWebhookConfiguration{ + Webhooks: []ValidatingWebhook{ + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + { + MatchConditions: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + }, + }, + featureGateEnabled: false, + expected: []MatchCondition{ + { + Name: "test1", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AdmissionWebhookMatchConditions, test.featureGateEnabled)() + DropDisabledValidatingWebhookConfigurationFields(test.new, test.old) + + for _, hook := range test.new.Webhooks { + if test.expected == nil { + if hook.MatchConditions != nil { + t.Error("expected all hooks matchConditions to be nil") + } + } else { + require.Equal(t, len(test.expected), len(hook.MatchConditions)) + for i, matchCondition := range hook.MatchConditions { + require.Equal(t, test.expected[i], matchCondition) + } + } + } + }) + } +} diff --git a/pkg/apis/admissionregistration/v1/zz_generated.conversion.go b/pkg/apis/admissionregistration/v1/zz_generated.conversion.go index a206e20d844..c732ee93dab 100644 --- a/pkg/apis/admissionregistration/v1/zz_generated.conversion.go +++ b/pkg/apis/admissionregistration/v1/zz_generated.conversion.go @@ -38,6 +38,16 @@ func init() { // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*v1.MatchCondition)(nil), (*admissionregistration.MatchCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_MatchCondition_To_admissionregistration_MatchCondition(a.(*v1.MatchCondition), b.(*admissionregistration.MatchCondition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*admissionregistration.MatchCondition)(nil), (*v1.MatchCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_admissionregistration_MatchCondition_To_v1_MatchCondition(a.(*admissionregistration.MatchCondition), b.(*v1.MatchCondition), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*v1.MutatingWebhook)(nil), (*admissionregistration.MutatingWebhook)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_MutatingWebhook_To_admissionregistration_MutatingWebhook(a.(*v1.MutatingWebhook), b.(*admissionregistration.MutatingWebhook), scope) }); err != nil { @@ -141,6 +151,28 @@ func RegisterConversions(s *runtime.Scheme) error { return nil } +func autoConvert_v1_MatchCondition_To_admissionregistration_MatchCondition(in *v1.MatchCondition, out *admissionregistration.MatchCondition, s conversion.Scope) error { + out.Name = in.Name + out.Expression = in.Expression + return nil +} + +// Convert_v1_MatchCondition_To_admissionregistration_MatchCondition is an autogenerated conversion function. +func Convert_v1_MatchCondition_To_admissionregistration_MatchCondition(in *v1.MatchCondition, out *admissionregistration.MatchCondition, s conversion.Scope) error { + return autoConvert_v1_MatchCondition_To_admissionregistration_MatchCondition(in, out, s) +} + +func autoConvert_admissionregistration_MatchCondition_To_v1_MatchCondition(in *admissionregistration.MatchCondition, out *v1.MatchCondition, s conversion.Scope) error { + out.Name = in.Name + out.Expression = in.Expression + return nil +} + +// Convert_admissionregistration_MatchCondition_To_v1_MatchCondition is an autogenerated conversion function. +func Convert_admissionregistration_MatchCondition_To_v1_MatchCondition(in *admissionregistration.MatchCondition, out *v1.MatchCondition, s conversion.Scope) error { + return autoConvert_admissionregistration_MatchCondition_To_v1_MatchCondition(in, out, s) +} + func autoConvert_v1_MutatingWebhook_To_admissionregistration_MutatingWebhook(in *v1.MutatingWebhook, out *admissionregistration.MutatingWebhook, s conversion.Scope) error { out.Name = in.Name if err := Convert_v1_WebhookClientConfig_To_admissionregistration_WebhookClientConfig(&in.ClientConfig, &out.ClientConfig, s); err != nil { @@ -165,6 +197,7 @@ func autoConvert_v1_MutatingWebhook_To_admissionregistration_MutatingWebhook(in out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) out.ReinvocationPolicy = (*admissionregistration.ReinvocationPolicyType)(unsafe.Pointer(in.ReinvocationPolicy)) + out.MatchConditions = *(*[]admissionregistration.MatchCondition)(unsafe.Pointer(&in.MatchConditions)) return nil } @@ -197,6 +230,7 @@ func autoConvert_admissionregistration_MutatingWebhook_To_v1_MutatingWebhook(in out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) out.ReinvocationPolicy = (*v1.ReinvocationPolicyType)(unsafe.Pointer(in.ReinvocationPolicy)) + out.MatchConditions = *(*[]v1.MatchCondition)(unsafe.Pointer(&in.MatchConditions)) return nil } @@ -374,6 +408,7 @@ func autoConvert_v1_ValidatingWebhook_To_admissionregistration_ValidatingWebhook out.SideEffects = (*admissionregistration.SideEffectClass)(unsafe.Pointer(in.SideEffects)) out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) + out.MatchConditions = *(*[]admissionregistration.MatchCondition)(unsafe.Pointer(&in.MatchConditions)) return nil } @@ -405,6 +440,7 @@ func autoConvert_admissionregistration_ValidatingWebhook_To_v1_ValidatingWebhook out.SideEffects = (*v1.SideEffectClass)(unsafe.Pointer(in.SideEffects)) out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) + out.MatchConditions = *(*[]v1.MatchCondition)(unsafe.Pointer(&in.MatchConditions)) return nil } diff --git a/pkg/apis/admissionregistration/v1beta1/zz_generated.conversion.go b/pkg/apis/admissionregistration/v1beta1/zz_generated.conversion.go index e132ef42002..5ece31ae590 100644 --- a/pkg/apis/admissionregistration/v1beta1/zz_generated.conversion.go +++ b/pkg/apis/admissionregistration/v1beta1/zz_generated.conversion.go @@ -40,6 +40,16 @@ func init() { // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*v1beta1.MatchCondition)(nil), (*admissionregistration.MatchCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_MatchCondition_To_admissionregistration_MatchCondition(a.(*v1beta1.MatchCondition), b.(*admissionregistration.MatchCondition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*admissionregistration.MatchCondition)(nil), (*v1beta1.MatchCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_admissionregistration_MatchCondition_To_v1beta1_MatchCondition(a.(*admissionregistration.MatchCondition), b.(*v1beta1.MatchCondition), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*v1beta1.MutatingWebhook)(nil), (*admissionregistration.MutatingWebhook)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_MutatingWebhook_To_admissionregistration_MutatingWebhook(a.(*v1beta1.MutatingWebhook), b.(*admissionregistration.MutatingWebhook), scope) }); err != nil { @@ -123,6 +133,28 @@ func RegisterConversions(s *runtime.Scheme) error { return nil } +func autoConvert_v1beta1_MatchCondition_To_admissionregistration_MatchCondition(in *v1beta1.MatchCondition, out *admissionregistration.MatchCondition, s conversion.Scope) error { + out.Name = in.Name + out.Expression = in.Expression + return nil +} + +// Convert_v1beta1_MatchCondition_To_admissionregistration_MatchCondition is an autogenerated conversion function. +func Convert_v1beta1_MatchCondition_To_admissionregistration_MatchCondition(in *v1beta1.MatchCondition, out *admissionregistration.MatchCondition, s conversion.Scope) error { + return autoConvert_v1beta1_MatchCondition_To_admissionregistration_MatchCondition(in, out, s) +} + +func autoConvert_admissionregistration_MatchCondition_To_v1beta1_MatchCondition(in *admissionregistration.MatchCondition, out *v1beta1.MatchCondition, s conversion.Scope) error { + out.Name = in.Name + out.Expression = in.Expression + return nil +} + +// Convert_admissionregistration_MatchCondition_To_v1beta1_MatchCondition is an autogenerated conversion function. +func Convert_admissionregistration_MatchCondition_To_v1beta1_MatchCondition(in *admissionregistration.MatchCondition, out *v1beta1.MatchCondition, s conversion.Scope) error { + return autoConvert_admissionregistration_MatchCondition_To_v1beta1_MatchCondition(in, out, s) +} + func autoConvert_v1beta1_MutatingWebhook_To_admissionregistration_MutatingWebhook(in *v1beta1.MutatingWebhook, out *admissionregistration.MutatingWebhook, s conversion.Scope) error { out.Name = in.Name if err := Convert_v1beta1_WebhookClientConfig_To_admissionregistration_WebhookClientConfig(&in.ClientConfig, &out.ClientConfig, s); err != nil { @@ -147,6 +179,7 @@ func autoConvert_v1beta1_MutatingWebhook_To_admissionregistration_MutatingWebhoo out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) out.ReinvocationPolicy = (*admissionregistration.ReinvocationPolicyType)(unsafe.Pointer(in.ReinvocationPolicy)) + out.MatchConditions = *(*[]admissionregistration.MatchCondition)(unsafe.Pointer(&in.MatchConditions)) return nil } @@ -179,6 +212,7 @@ func autoConvert_admissionregistration_MutatingWebhook_To_v1beta1_MutatingWebhoo out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) out.ReinvocationPolicy = (*v1beta1.ReinvocationPolicyType)(unsafe.Pointer(in.ReinvocationPolicy)) + out.MatchConditions = *(*[]v1beta1.MatchCondition)(unsafe.Pointer(&in.MatchConditions)) return nil } @@ -324,6 +358,7 @@ func autoConvert_v1beta1_ValidatingWebhook_To_admissionregistration_ValidatingWe out.SideEffects = (*admissionregistration.SideEffectClass)(unsafe.Pointer(in.SideEffects)) out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) + out.MatchConditions = *(*[]admissionregistration.MatchCondition)(unsafe.Pointer(&in.MatchConditions)) return nil } @@ -355,6 +390,7 @@ func autoConvert_admissionregistration_ValidatingWebhook_To_v1beta1_ValidatingWe out.SideEffects = (*v1beta1.SideEffectClass)(unsafe.Pointer(in.SideEffects)) out.TimeoutSeconds = (*int32)(unsafe.Pointer(in.TimeoutSeconds)) out.AdmissionReviewVersions = *(*[]string)(unsafe.Pointer(&in.AdmissionReviewVersions)) + out.MatchConditions = *(*[]v1beta1.MatchCondition)(unsafe.Pointer(&in.MatchConditions)) return nil } diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index a3f0b459788..204ed745b91 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -18,6 +18,7 @@ package validation import ( "fmt" + "reflect" "regexp" "strings" @@ -34,7 +35,6 @@ import ( "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/util/jsonpath" - "k8s.io/kubernetes/pkg/apis/admissionregistration" admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1" admissionregistrationv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1" @@ -212,6 +212,7 @@ func validateAdmissionReviewVersions(versions []string, requireRecognizedAdmissi // ValidateValidatingWebhookConfiguration validates a webhook before creation. func ValidateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList { return validateValidatingWebhookConfiguration(e, validationOptions{ + ignoreMatchConditions: false, requireNoSideEffects: true, requireRecognizedAdmissionReviewVersion: true, requireUniqueWebhookNames: true, @@ -239,6 +240,7 @@ func validateValidatingWebhookConfiguration(e *admissionregistration.ValidatingW // ValidateMutatingWebhookConfiguration validates a webhook before creation. func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration) field.ErrorList { return validateMutatingWebhookConfiguration(e, validationOptions{ + ignoreMatchConditions: false, requireNoSideEffects: true, requireRecognizedAdmissionReviewVersion: true, requireUniqueWebhookNames: true, @@ -247,6 +249,7 @@ func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebho } type validationOptions struct { + ignoreMatchConditions bool requireNoSideEffects bool requireRecognizedAdmissionReviewVersion bool requireUniqueWebhookNames bool @@ -319,6 +322,11 @@ func validateValidatingWebhook(hook *admissionregistration.ValidatingWebhook, op 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)...) } + + if !opts.ignoreMatchConditions { + allErrors = append(allErrors, validateMatchConditions(hook.MatchConditions, fldPath.Child("matchConditions"))...) + } + return allErrors } @@ -372,6 +380,11 @@ func validateMutatingWebhook(hook *admissionregistration.MutatingWebhook, opts v 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)...) } + + if !opts.ignoreMatchConditions { + allErrors = append(allErrors, validateMatchConditions(hook.MatchConditions, fldPath.Child("matchConditions"))...) + } + return allErrors } @@ -479,6 +492,34 @@ func validatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistrati return true } +// ignoreMatchConditions returns false if any change to match conditions +func ignoreMutatingWebhookMatchConditions(new, old []admissionregistration.MutatingWebhook) bool { + if len(new) != len(old) { + return false + } + for i := range old { + if !reflect.DeepEqual(new[i].MatchConditions, old[i].MatchConditions) { + return false + } + } + + return true +} + +// ignoreMatchConditions returns true if any new expressions are added +func ignoreValidatingWebhookMatchConditions(new, old []admissionregistration.ValidatingWebhook) bool { + if len(new) != len(old) { + return false + } + for i := range old { + if !reflect.DeepEqual(new[i].MatchConditions, old[i].MatchConditions) { + return false + } + } + + return true +} + // mutatingHasUniqueWebhookNames returns true if all webhooks have unique names func mutatingHasUniqueWebhookNames(webhooks []admissionregistration.MutatingWebhook) bool { names := sets.NewString() @@ -568,6 +609,7 @@ func mutatingWebhookHasInvalidLabelValueInSelector(webhooks []admissionregistrat // ValidateValidatingWebhookConfigurationUpdate validates update of validating webhook configuration func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList { return validateValidatingWebhookConfiguration(newC, validationOptions{ + ignoreMatchConditions: ignoreValidatingWebhookMatchConditions(newC.Webhooks, oldC.Webhooks), requireNoSideEffects: validatingHasNoSideEffects(oldC.Webhooks), requireRecognizedAdmissionReviewVersion: validatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks), requireUniqueWebhookNames: validatingHasUniqueWebhookNames(oldC.Webhooks), @@ -578,6 +620,7 @@ func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistrat // ValidateMutatingWebhookConfigurationUpdate validates update of mutating webhook configuration func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.MutatingWebhookConfiguration) field.ErrorList { return validateMutatingWebhookConfiguration(newC, validationOptions{ + ignoreMatchConditions: ignoreMutatingWebhookMatchConditions(newC.Webhooks, oldC.Webhooks), requireNoSideEffects: mutatingHasNoSideEffects(oldC.Webhooks), requireRecognizedAdmissionReviewVersion: mutatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks), requireUniqueWebhookNames: mutatingHasUniqueWebhookNames(oldC.Webhooks), @@ -779,6 +822,44 @@ func validateNamedRuleWithOperations(n *admissionregistration.NamedRuleWithOpera return allErrors } +func validateMatchConditions(m []admissionregistration.MatchCondition, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + conditionNames := sets.NewString() + if len(m) > 64 { + allErrors = append(allErrors, field.TooMany(fldPath, len(m), 64)) + } + for i, matchCondition := range m { + allErrors = append(allErrors, validateMatchCondition(&matchCondition, fldPath.Index(i))...) + if len(matchCondition.Name) > 0 { + if conditionNames.Has(matchCondition.Name) { + allErrors = append(allErrors, field.Duplicate(fldPath.Index(i).Child("name"), matchCondition.Name)) + } else { + conditionNames.Insert(matchCondition.Name) + } + } + } + return allErrors +} + +func validateMatchCondition(v *admissionregistration.MatchCondition, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + trimmedExpression := strings.TrimSpace(v.Expression) + if len(trimmedExpression) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "")) + } else { + allErrors = append(allErrors, validateCELExpression(trimmedExpression, plugincel.OptionalVariableDeclarations{ + HasParams: false, + HasAuthorizer: true, + }, fldPath.Child("expression"))...) + } + if len(v.Name) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("name"), "")) + } else { + allErrors = append(allErrors, apivalidation.ValidateQualifiedName(v.Name, fldPath.Child("name"))...) + } + return allErrors +} + func validateValidation(v *admissionregistration.Validation, paramKind *admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList { var allErrors field.ErrorList trimmedExpression := strings.TrimSpace(v.Expression) diff --git a/pkg/apis/admissionregistration/validation/validation_test.go b/pkg/apis/admissionregistration/validation/validation_test.go index 8db59efa1f1..7f1ee76a54d 100644 --- a/pkg/apis/admissionregistration/validation/validation_test.go +++ b/pkg/apis/admissionregistration/validation/validation_test.go @@ -17,12 +17,12 @@ limitations under the License. package validation import ( + "fmt" "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/kubernetes/pkg/apis/admissionregistration" ) @@ -752,6 +752,229 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) { }, }, true), }, + { + name: "single match condition must have a name", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Expression: "true", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].name: Required value`, + }, + { + name: "all match conditions must have a name", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Expression: "true", + }, + { + Expression: "true", + }, + }, + }, + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "", + Expression: "true", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].name: Required value, webhooks[0].matchConditions[1].name: Required value, webhooks[1].matchConditions[0].name: Required value`, + }, + { + name: "single match condition must have a qualified name", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "-hello", + Expression: "true", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].name: Invalid value: "-hello": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`, + }, + { + name: "all match conditions must have qualified names", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: ".io", + Expression: "true", + }, + { + Name: "thing.test.com", + Expression: "true", + }, + }, + }, + { + Name: "webhook2.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "some name", + Expression: "true", + }, + }, + }, + }, true), + expectedError: `[webhooks[0].matchConditions[0].name: Invalid value: ".io": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]'), webhooks[1].matchConditions[0].name: Invalid value: "some name": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')]`, + }, + { + name: "expression is required", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].expression: Required value`, + }, + { + name: "expression is required to have some value", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].expression: Required value`, + }, + { + name: "invalid expression", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "object.x in [1, 2, ", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].expression: Invalid value: "object.x in [1, 2,": compilation failed: ERROR: :1:19: Syntax error: missing ']' at ''`, + }, + { + name: "unique names same hook", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "true", + }, + { + Name: "webhook.k8s.io", + Expression: "true", + }, + }, + }, + }, true), + expectedError: `matchConditions[1].name: Duplicate value: "webhook.k8s.io"`, + }, + { + name: "repeat names allowed across different hooks", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "true", + }, + }, + }, + { + Name: "webhook2.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "true", + }, + }, + }, + }, true), + expectedError: ``, + }, + { + name: "must evaluate to bool", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "6", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].expression: Invalid value: "6": must evaluate to bool`, + }, + { + name: "max of 64 match conditions", + config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: get65MatchConditions(), + }, + }, true), + expectedError: `webhooks[0].matchConditions: Too many: 65: must have at most 64 items`, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -1652,6 +1875,229 @@ func TestValidateMutatingWebhookConfiguration(t *testing.T) { }, }, true), }, + { + name: "single match condition must have a name", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Expression: "true", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].name: Required value`, + }, + { + name: "all match conditions must have a name", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Expression: "true", + }, + { + Expression: "true", + }, + }, + }, + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "", + Expression: "true", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].name: Required value, webhooks[0].matchConditions[1].name: Required value, webhooks[1].matchConditions[0].name: Required value`, + }, + { + name: "single match condition must have a qualified name", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "-hello", + Expression: "true", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].name: Invalid value: "-hello": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`, + }, + { + name: "all match conditions must have qualified names", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: ".io", + Expression: "true", + }, + { + Name: "thing.test.com", + Expression: "true", + }, + }, + }, + { + Name: "webhook2.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "some name", + Expression: "true", + }, + }, + }, + }, true), + expectedError: `[webhooks[0].matchConditions[0].name: Invalid value: ".io": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]'), webhooks[1].matchConditions[0].name: Invalid value: "some name": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')]`, + }, + { + name: "expression is required", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].expression: Required value`, + }, + { + name: "expression is required to have some value", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].expression: Required value`, + }, + { + name: "invalid expression", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "object.x in [1, 2, ", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].expression: Invalid value: "object.x in [1, 2,": compilation failed: ERROR: :1:19: Syntax error: missing ']' at ''`, + }, + { + name: "unique names same hook", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "true", + }, + { + Name: "webhook.k8s.io", + Expression: "true", + }, + }, + }, + }, true), + expectedError: `matchConditions[1].name: Duplicate value: "webhook.k8s.io"`, + }, + { + name: "repeat names allowed across different hooks", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "true", + }, + }, + }, + { + Name: "webhook2.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "true", + }, + }, + }, + }, true), + expectedError: ``, + }, + { + name: "must evaluate to bool", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: []admissionregistration.MatchCondition{ + { + Name: "webhook.k8s.io", + Expression: "6", + }, + }, + }, + }, true), + expectedError: `webhooks[0].matchConditions[0].expression: Invalid value: "6": must evaluate to bool`, + }, + { + name: "max of 64 match conditions", + config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{ + { + Name: "webhook.k8s.io", + ClientConfig: validClientConfig, + SideEffects: &noSideEffect, + MatchConditions: get65MatchConditions(), + }, + }, true), + expectedError: `webhooks[0].matchConditions: Too many: 65: must have at most 64 items`, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -3534,3 +3980,14 @@ func TestValidateValidatingAdmissionPolicyStatus(t *testing.T) { }) } } + +func get65MatchConditions() []admissionregistration.MatchCondition { + result := []admissionregistration.MatchCondition{} + for i := 0; i < 65; i++ { + result = append(result, admissionregistration.MatchCondition{ + Name: fmt.Sprintf("test%v", i), + Expression: "true", + }) + } + return result +} diff --git a/pkg/apis/admissionregistration/zz_generated.deepcopy.go b/pkg/apis/admissionregistration/zz_generated.deepcopy.go index 03a65c6c1cf..5c2250b4d1f 100644 --- a/pkg/apis/admissionregistration/zz_generated.deepcopy.go +++ b/pkg/apis/admissionregistration/zz_generated.deepcopy.go @@ -58,6 +58,22 @@ func (in *ExpressionWarning) DeepCopy() *ExpressionWarning { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MatchCondition) DeepCopyInto(out *MatchCondition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchCondition. +func (in *MatchCondition) DeepCopy() *MatchCondition { + if in == nil { + return nil + } + out := new(MatchCondition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MatchResources) DeepCopyInto(out *MatchResources) { *out = *in @@ -154,6 +170,11 @@ func (in *MutatingWebhook) DeepCopyInto(out *MutatingWebhook) { *out = new(ReinvocationPolicyType) **out = **in } + if in.MatchConditions != nil { + in, out := &in.MatchConditions, &out.MatchConditions + *out = make([]MatchCondition, len(*in)) + copy(*out, *in) + } return } @@ -656,6 +677,11 @@ func (in *ValidatingWebhook) DeepCopyInto(out *ValidatingWebhook) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.MatchConditions != nil { + in, out := &in.MatchConditions, &out.MatchConditions + *out = make([]MatchCondition, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index dbc59069281..efce548a905 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -1149,6 +1149,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: + genericfeatures.AdmissionWebhookMatchConditions: {Default: false, PreRelease: featuregate.Alpha}, + genericfeatures.AggregatedDiscoveryEndpoint: {Default: true, PreRelease: featuregate.Beta}, genericfeatures.APIListChunking: {Default: true, PreRelease: featuregate.Beta}, diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 2f26dd9d4c1..48bf8ee50df 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -35,6 +35,7 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ + "k8s.io/api/admissionregistration/v1.MatchCondition": schema_k8sio_api_admissionregistration_v1_MatchCondition(ref), "k8s.io/api/admissionregistration/v1.MutatingWebhook": schema_k8sio_api_admissionregistration_v1_MutatingWebhook(ref), "k8s.io/api/admissionregistration/v1.MutatingWebhookConfiguration": schema_k8sio_api_admissionregistration_v1_MutatingWebhookConfiguration(ref), "k8s.io/api/admissionregistration/v1.MutatingWebhookConfigurationList": schema_k8sio_api_admissionregistration_v1_MutatingWebhookConfigurationList(ref), @@ -60,6 +61,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "k8s.io/api/admissionregistration/v1alpha1.ValidatingAdmissionPolicySpec": schema_k8sio_api_admissionregistration_v1alpha1_ValidatingAdmissionPolicySpec(ref), "k8s.io/api/admissionregistration/v1alpha1.ValidatingAdmissionPolicyStatus": schema_k8sio_api_admissionregistration_v1alpha1_ValidatingAdmissionPolicyStatus(ref), "k8s.io/api/admissionregistration/v1alpha1.Validation": schema_k8sio_api_admissionregistration_v1alpha1_Validation(ref), + "k8s.io/api/admissionregistration/v1beta1.MatchCondition": schema_k8sio_api_admissionregistration_v1beta1_MatchCondition(ref), "k8s.io/api/admissionregistration/v1beta1.MutatingWebhook": schema_k8sio_api_admissionregistration_v1beta1_MutatingWebhook(ref), "k8s.io/api/admissionregistration/v1beta1.MutatingWebhookConfiguration": schema_k8sio_api_admissionregistration_v1beta1_MutatingWebhookConfiguration(ref), "k8s.io/api/admissionregistration/v1beta1.MutatingWebhookConfigurationList": schema_k8sio_api_admissionregistration_v1beta1_MutatingWebhookConfigurationList(ref), @@ -1181,6 +1183,36 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA } } +func schema_k8sio_api_admissionregistration_v1_MatchCondition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MatchCondition represents a condition which must by fulfilled for a request to be sent to a webhook.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is an identifier for this match condition, used for strategic merging of MatchConditions, as well as providing an identifier for logging purposes. A good name should be descriptive of the associated expression. Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')\n\nRequired.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "expression": { + SchemaProps: spec.SchemaProps{ + Description: "Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\nDocumentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/\n\nRequired.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "expression"}, + }, + }, + } +} + func schema_k8sio_api_admissionregistration_v1_MutatingWebhook(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1283,12 +1315,36 @@ func schema_k8sio_api_admissionregistration_v1_MutatingWebhook(ref common.Refere Enum: []interface{}{"IfNeeded", "Never"}, }, }, + "matchConditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "name", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/api/admissionregistration/v1.MatchCondition"), + }, + }, + }, + }, + }, }, Required: []string{"name", "clientConfig", "sideEffects", "admissionReviewVersions"}, }, }, Dependencies: []string{ - "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/api/admissionregistration/v1.WebhookClientConfig", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, + "k8s.io/api/admissionregistration/v1.MatchCondition", "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/api/admissionregistration/v1.WebhookClientConfig", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, } } @@ -1717,12 +1773,36 @@ func schema_k8sio_api_admissionregistration_v1_ValidatingWebhook(ref common.Refe }, }, }, + "matchConditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "name", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/api/admissionregistration/v1.MatchCondition"), + }, + }, + }, + }, + }, }, Required: []string{"name", "clientConfig", "sideEffects", "admissionReviewVersions"}, }, }, Dependencies: []string{ - "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/api/admissionregistration/v1.WebhookClientConfig", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, + "k8s.io/api/admissionregistration/v1.MatchCondition", "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/api/admissionregistration/v1.WebhookClientConfig", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, } } @@ -2642,6 +2722,36 @@ func schema_k8sio_api_admissionregistration_v1alpha1_Validation(ref common.Refer } } +func schema_k8sio_api_admissionregistration_v1beta1_MatchCondition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MatchCondition represents a condition which must be fulfilled for a request to be sent to a webhook.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is an identifier for this match condition, used for strategic merging of MatchConditions, as well as providing an identifier for logging purposes. A good name should be descriptive of the associated expression. Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')\n\nRequired.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "expression": { + SchemaProps: spec.SchemaProps{ + Description: "Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\nDocumentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/\n\nRequired.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "expression"}, + }, + }, + } +} + func schema_k8sio_api_admissionregistration_v1beta1_MutatingWebhook(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2740,12 +2850,36 @@ func schema_k8sio_api_admissionregistration_v1beta1_MutatingWebhook(ref common.R Format: "", }, }, + "matchConditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "name", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/api/admissionregistration/v1beta1.MatchCondition"), + }, + }, + }, + }, + }, }, Required: []string{"name", "clientConfig"}, }, }, Dependencies: []string{ - "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/api/admissionregistration/v1beta1.WebhookClientConfig", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, + "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/api/admissionregistration/v1beta1.MatchCondition", "k8s.io/api/admissionregistration/v1beta1.WebhookClientConfig", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, } } @@ -2991,12 +3125,36 @@ func schema_k8sio_api_admissionregistration_v1beta1_ValidatingWebhook(ref common }, }, }, + "matchConditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "name", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/api/admissionregistration/v1beta1.MatchCondition"), + }, + }, + }, + }, + }, }, Required: []string{"name", "clientConfig"}, }, }, Dependencies: []string{ - "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/api/admissionregistration/v1beta1.WebhookClientConfig", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, + "k8s.io/api/admissionregistration/v1.RuleWithOperations", "k8s.io/api/admissionregistration/v1beta1.MatchCondition", "k8s.io/api/admissionregistration/v1beta1.WebhookClientConfig", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, } } diff --git a/pkg/registry/admissionregistration/mutatingwebhookconfiguration/strategy.go b/pkg/registry/admissionregistration/mutatingwebhookconfiguration/strategy.go index 0ae88c5dd6c..252a3c3dde4 100644 --- a/pkg/registry/admissionregistration/mutatingwebhookconfiguration/strategy.go +++ b/pkg/registry/admissionregistration/mutatingwebhookconfiguration/strategy.go @@ -46,6 +46,8 @@ func (mutatingWebhookConfigurationStrategy) NamespaceScoped() bool { func (mutatingWebhookConfigurationStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { ic := obj.(*admissionregistration.MutatingWebhookConfiguration) ic.Generation = 1 + + admissionregistration.DropDisabledMutatingWebhookConfigurationFields(ic, nil) } // WarningsOnCreate returns warnings for the creation of the given object. @@ -58,6 +60,7 @@ func (mutatingWebhookConfigurationStrategy) PrepareForUpdate(ctx context.Context newIC := obj.(*admissionregistration.MutatingWebhookConfiguration) oldIC := old.(*admissionregistration.MutatingWebhookConfiguration) + admissionregistration.DropDisabledMutatingWebhookConfigurationFields(newIC, oldIC) // Any changes to the spec increment the generation number, any changes to the // status should reflect the generation number of the corresponding object. // See metav1.ObjectMeta description for more information on Generation. diff --git a/pkg/registry/admissionregistration/validatingwebhookconfiguration/strategy.go b/pkg/registry/admissionregistration/validatingwebhookconfiguration/strategy.go index 20bc653eeae..67931f9d9a7 100644 --- a/pkg/registry/admissionregistration/validatingwebhookconfiguration/strategy.go +++ b/pkg/registry/admissionregistration/validatingwebhookconfiguration/strategy.go @@ -46,6 +46,8 @@ func (validatingWebhookConfigurationStrategy) NamespaceScoped() bool { func (validatingWebhookConfigurationStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { ic := obj.(*admissionregistration.ValidatingWebhookConfiguration) ic.Generation = 1 + + admissionregistration.DropDisabledValidatingWebhookConfigurationFields(ic, nil) } // PrepareForUpdate clears fields that are not allowed to be set by end users on update. @@ -53,6 +55,7 @@ func (validatingWebhookConfigurationStrategy) PrepareForUpdate(ctx context.Conte newIC := obj.(*admissionregistration.ValidatingWebhookConfiguration) oldIC := old.(*admissionregistration.ValidatingWebhookConfiguration) + admissionregistration.DropDisabledValidatingWebhookConfigurationFields(newIC, oldIC) // Any changes to the spec increment the generation number, any changes to the // status should reflect the generation number of the corresponding object. // See metav1.ObjectMeta description for more information on Generation. diff --git a/staging/src/k8s.io/api/admissionregistration/v1/generated.pb.go b/staging/src/k8s.io/api/admissionregistration/v1/generated.pb.go index 6ac9e80ffca..9a2d0bccdda 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1/generated.pb.go +++ b/staging/src/k8s.io/api/admissionregistration/v1/generated.pb.go @@ -44,10 +44,38 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package +func (m *MatchCondition) Reset() { *m = MatchCondition{} } +func (*MatchCondition) ProtoMessage() {} +func (*MatchCondition) Descriptor() ([]byte, []int) { + return fileDescriptor_aaac5994f79683e8, []int{0} +} +func (m *MatchCondition) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MatchCondition) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *MatchCondition) XXX_Merge(src proto.Message) { + xxx_messageInfo_MatchCondition.Merge(m, src) +} +func (m *MatchCondition) XXX_Size() int { + return m.Size() +} +func (m *MatchCondition) XXX_DiscardUnknown() { + xxx_messageInfo_MatchCondition.DiscardUnknown(m) +} + +var xxx_messageInfo_MatchCondition proto.InternalMessageInfo + func (m *MutatingWebhook) Reset() { *m = MutatingWebhook{} } func (*MutatingWebhook) ProtoMessage() {} func (*MutatingWebhook) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{0} + return fileDescriptor_aaac5994f79683e8, []int{1} } func (m *MutatingWebhook) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -75,7 +103,7 @@ var xxx_messageInfo_MutatingWebhook proto.InternalMessageInfo func (m *MutatingWebhookConfiguration) Reset() { *m = MutatingWebhookConfiguration{} } func (*MutatingWebhookConfiguration) ProtoMessage() {} func (*MutatingWebhookConfiguration) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{1} + return fileDescriptor_aaac5994f79683e8, []int{2} } func (m *MutatingWebhookConfiguration) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -103,7 +131,7 @@ var xxx_messageInfo_MutatingWebhookConfiguration proto.InternalMessageInfo func (m *MutatingWebhookConfigurationList) Reset() { *m = MutatingWebhookConfigurationList{} } func (*MutatingWebhookConfigurationList) ProtoMessage() {} func (*MutatingWebhookConfigurationList) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{2} + return fileDescriptor_aaac5994f79683e8, []int{3} } func (m *MutatingWebhookConfigurationList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -131,7 +159,7 @@ var xxx_messageInfo_MutatingWebhookConfigurationList proto.InternalMessageInfo func (m *Rule) Reset() { *m = Rule{} } func (*Rule) ProtoMessage() {} func (*Rule) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{3} + return fileDescriptor_aaac5994f79683e8, []int{4} } func (m *Rule) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -159,7 +187,7 @@ var xxx_messageInfo_Rule proto.InternalMessageInfo func (m *RuleWithOperations) Reset() { *m = RuleWithOperations{} } func (*RuleWithOperations) ProtoMessage() {} func (*RuleWithOperations) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{4} + return fileDescriptor_aaac5994f79683e8, []int{5} } func (m *RuleWithOperations) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -187,7 +215,7 @@ var xxx_messageInfo_RuleWithOperations proto.InternalMessageInfo func (m *ServiceReference) Reset() { *m = ServiceReference{} } func (*ServiceReference) ProtoMessage() {} func (*ServiceReference) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{5} + return fileDescriptor_aaac5994f79683e8, []int{6} } func (m *ServiceReference) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -215,7 +243,7 @@ var xxx_messageInfo_ServiceReference proto.InternalMessageInfo func (m *ValidatingWebhook) Reset() { *m = ValidatingWebhook{} } func (*ValidatingWebhook) ProtoMessage() {} func (*ValidatingWebhook) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{6} + return fileDescriptor_aaac5994f79683e8, []int{7} } func (m *ValidatingWebhook) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -243,7 +271,7 @@ var xxx_messageInfo_ValidatingWebhook proto.InternalMessageInfo func (m *ValidatingWebhookConfiguration) Reset() { *m = ValidatingWebhookConfiguration{} } func (*ValidatingWebhookConfiguration) ProtoMessage() {} func (*ValidatingWebhookConfiguration) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{7} + return fileDescriptor_aaac5994f79683e8, []int{8} } func (m *ValidatingWebhookConfiguration) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -271,7 +299,7 @@ var xxx_messageInfo_ValidatingWebhookConfiguration proto.InternalMessageInfo func (m *ValidatingWebhookConfigurationList) Reset() { *m = ValidatingWebhookConfigurationList{} } func (*ValidatingWebhookConfigurationList) ProtoMessage() {} func (*ValidatingWebhookConfigurationList) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{8} + return fileDescriptor_aaac5994f79683e8, []int{9} } func (m *ValidatingWebhookConfigurationList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -299,7 +327,7 @@ var xxx_messageInfo_ValidatingWebhookConfigurationList proto.InternalMessageInfo func (m *WebhookClientConfig) Reset() { *m = WebhookClientConfig{} } func (*WebhookClientConfig) ProtoMessage() {} func (*WebhookClientConfig) Descriptor() ([]byte, []int) { - return fileDescriptor_aaac5994f79683e8, []int{9} + return fileDescriptor_aaac5994f79683e8, []int{10} } func (m *WebhookClientConfig) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -325,6 +353,7 @@ func (m *WebhookClientConfig) XXX_DiscardUnknown() { var xxx_messageInfo_WebhookClientConfig proto.InternalMessageInfo func init() { + proto.RegisterType((*MatchCondition)(nil), "k8s.io.api.admissionregistration.v1.MatchCondition") proto.RegisterType((*MutatingWebhook)(nil), "k8s.io.api.admissionregistration.v1.MutatingWebhook") proto.RegisterType((*MutatingWebhookConfiguration)(nil), "k8s.io.api.admissionregistration.v1.MutatingWebhookConfiguration") proto.RegisterType((*MutatingWebhookConfigurationList)(nil), "k8s.io.api.admissionregistration.v1.MutatingWebhookConfigurationList") @@ -342,79 +371,116 @@ func init() { } var fileDescriptor_aaac5994f79683e8 = []byte{ - // 1105 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x57, 0x4f, 0x6f, 0x1b, 0x45, - 0x14, 0xcf, 0xc6, 0x76, 0x63, 0x8f, 0xf3, 0xa7, 0x19, 0xa0, 0x35, 0xa1, 0xf2, 0x5a, 0xae, 0x84, - 0x8c, 0x80, 0xdd, 0x26, 0x94, 0x52, 0x71, 0x41, 0xd9, 0xf0, 0x47, 0x11, 0x49, 0x1b, 0x4d, 0xda, - 0x14, 0xa1, 0x1c, 0x3a, 0x5e, 0x8f, 0xed, 0x21, 0xf6, 0xce, 0x6a, 0x66, 0xd6, 0x90, 0x1b, 0x1f, - 0x81, 0xaf, 0x00, 0x9f, 0x82, 0x1b, 0xe2, 0x96, 0x63, 0x8f, 0x39, 0xa0, 0x85, 0x2c, 0x17, 0x0e, - 0x7c, 0x82, 0x9c, 0xd0, 0xcc, 0xae, 0x77, 0xfd, 0x27, 0x09, 0x56, 0x0e, 0x3d, 0xe5, 0xe6, 0xf9, - 0xbd, 0x79, 0xbf, 0x37, 0xef, 0xed, 0x7b, 0xef, 0x27, 0x83, 0x9d, 0xa3, 0xc7, 0xc2, 0xa2, 0xcc, - 0x3e, 0x0a, 0x9a, 0x84, 0x7b, 0x44, 0x12, 0x61, 0x0f, 0x88, 0xd7, 0x62, 0xdc, 0x4e, 0x0c, 0xd8, - 0xa7, 0x36, 0x6e, 0xf5, 0xa9, 0x10, 0x94, 0x79, 0x9c, 0x74, 0xa8, 0x90, 0x1c, 0x4b, 0xca, 0x3c, - 0x7b, 0xb0, 0x6e, 0x77, 0x88, 0x47, 0x38, 0x96, 0xa4, 0x65, 0xf9, 0x9c, 0x49, 0x06, 0xef, 0xc7, - 0x4e, 0x16, 0xf6, 0xa9, 0x75, 0xa1, 0x93, 0x35, 0x58, 0x5f, 0xfb, 0xb0, 0x43, 0x65, 0x37, 0x68, - 0x5a, 0x2e, 0xeb, 0xdb, 0x1d, 0xd6, 0x61, 0xb6, 0xf6, 0x6d, 0x06, 0x6d, 0x7d, 0xd2, 0x07, 0xfd, - 0x2b, 0xe6, 0x5c, 0x7b, 0x98, 0x3d, 0xa4, 0x8f, 0xdd, 0x2e, 0xf5, 0x08, 0x3f, 0xb6, 0xfd, 0xa3, - 0x8e, 0x02, 0x84, 0xdd, 0x27, 0x12, 0x5f, 0xf0, 0x92, 0x35, 0xfb, 0x32, 0x2f, 0x1e, 0x78, 0x92, - 0xf6, 0xc9, 0x94, 0xc3, 0xa3, 0xff, 0x73, 0x10, 0x6e, 0x97, 0xf4, 0xf1, 0xa4, 0x5f, 0xfd, 0xb7, - 0x05, 0xb0, 0xb2, 0x1b, 0x48, 0x2c, 0xa9, 0xd7, 0x79, 0x41, 0x9a, 0x5d, 0xc6, 0x8e, 0x60, 0x0d, - 0xe4, 0x3d, 0xdc, 0x27, 0x15, 0xa3, 0x66, 0x34, 0x4a, 0xce, 0xe2, 0x49, 0x68, 0xce, 0x45, 0xa1, - 0x99, 0x7f, 0x82, 0xfb, 0x04, 0x69, 0x0b, 0xe4, 0x60, 0xd1, 0xed, 0x51, 0xe2, 0xc9, 0x2d, 0xe6, - 0xb5, 0x69, 0xa7, 0x32, 0x5f, 0x33, 0x1a, 0xe5, 0x8d, 0xc7, 0xd6, 0x0c, 0xf5, 0xb3, 0x92, 0x28, - 0x5b, 0x23, 0xfe, 0xce, 0x9b, 0x49, 0x8c, 0xc5, 0x51, 0x14, 0x8d, 0xc5, 0x80, 0x87, 0xa0, 0xc0, - 0x83, 0x1e, 0x11, 0x95, 0x5c, 0x2d, 0xd7, 0x28, 0x6f, 0x7c, 0x32, 0x53, 0x30, 0x14, 0xf4, 0xc8, - 0x0b, 0x2a, 0xbb, 0x4f, 0x7d, 0x12, 0x83, 0xc2, 0x59, 0x4a, 0x62, 0x15, 0x94, 0x4d, 0xa0, 0x98, - 0x14, 0xee, 0x80, 0xa5, 0x36, 0xa6, 0xbd, 0x80, 0x93, 0x3d, 0xd6, 0xa3, 0xee, 0x71, 0x25, 0xaf, - 0x93, 0x7f, 0x37, 0x0a, 0xcd, 0xa5, 0x2f, 0x47, 0x0d, 0xe7, 0xa1, 0xb9, 0x3a, 0x06, 0x3c, 0x3b, - 0xf6, 0x09, 0x1a, 0x77, 0x86, 0x9f, 0x83, 0x72, 0x1f, 0x4b, 0xb7, 0x9b, 0x70, 0x95, 0x34, 0x57, - 0x3d, 0x0a, 0xcd, 0xf2, 0x6e, 0x06, 0x9f, 0x87, 0xe6, 0xca, 0xc8, 0x51, 0xf3, 0x8c, 0xba, 0xc1, - 0x1f, 0xc0, 0xaa, 0xaa, 0xb6, 0xf0, 0xb1, 0x4b, 0xf6, 0x49, 0x8f, 0xb8, 0x92, 0xf1, 0x4a, 0x41, - 0x97, 0xfa, 0xa3, 0x91, 0xec, 0xd3, 0xef, 0x6d, 0xf9, 0x47, 0x1d, 0x05, 0x08, 0x4b, 0xb5, 0x95, - 0x4a, 0x7f, 0x07, 0x37, 0x49, 0x6f, 0xe8, 0xea, 0xbc, 0x15, 0x85, 0xe6, 0xea, 0x93, 0x49, 0x46, - 0x34, 0x1d, 0x04, 0x32, 0xb0, 0xcc, 0x9a, 0xdf, 0x11, 0x57, 0xa6, 0x61, 0xcb, 0xd7, 0x0f, 0x0b, - 0xa3, 0xd0, 0x5c, 0x7e, 0x3a, 0x46, 0x87, 0x26, 0xe8, 0x55, 0xc1, 0x04, 0x6d, 0x91, 0x2f, 0xda, - 0x6d, 0xe2, 0x4a, 0x51, 0xb9, 0x95, 0x15, 0x6c, 0x3f, 0x83, 0x55, 0xc1, 0xb2, 0xe3, 0x56, 0x0f, - 0x0b, 0x81, 0x46, 0xdd, 0xe0, 0xa7, 0x60, 0x59, 0xf5, 0x3a, 0x0b, 0xe4, 0x3e, 0x71, 0x99, 0xd7, - 0x12, 0x95, 0x85, 0x9a, 0xd1, 0x28, 0xc4, 0x2f, 0x78, 0x36, 0x66, 0x41, 0x13, 0x37, 0xe1, 0x73, - 0x70, 0x37, 0xed, 0x22, 0x44, 0x06, 0x94, 0x7c, 0x7f, 0x40, 0xb8, 0x3a, 0x88, 0x4a, 0xb1, 0x96, - 0x6b, 0x94, 0x9c, 0x77, 0xa2, 0xd0, 0xbc, 0xbb, 0x79, 0xf1, 0x15, 0x74, 0x99, 0x2f, 0x7c, 0x09, - 0x20, 0x27, 0xd4, 0x1b, 0x30, 0x57, 0xb7, 0x5f, 0xd2, 0x10, 0x40, 0xe7, 0xf7, 0x20, 0x0a, 0x4d, - 0x88, 0xa6, 0xac, 0xe7, 0xa1, 0x79, 0x67, 0x1a, 0xd5, 0xed, 0x71, 0x01, 0x57, 0xfd, 0xd4, 0x00, - 0xf7, 0x26, 0x26, 0x38, 0x9e, 0x98, 0x20, 0xee, 0x78, 0xf8, 0x12, 0x14, 0xd5, 0x87, 0x69, 0x61, - 0x89, 0xf5, 0x48, 0x97, 0x37, 0x1e, 0xcc, 0xf6, 0x19, 0xe3, 0x6f, 0xb6, 0x4b, 0x24, 0x76, 0x60, - 0x32, 0x34, 0x20, 0xc3, 0x50, 0xca, 0x0a, 0x0f, 0x40, 0x31, 0x89, 0x2c, 0x2a, 0xf3, 0x7a, 0x3a, - 0x1f, 0xce, 0x34, 0x9d, 0x13, 0xcf, 0x76, 0xf2, 0x2a, 0x0a, 0x4a, 0xb9, 0xea, 0xff, 0x18, 0xa0, - 0x76, 0x55, 0x6a, 0x3b, 0x54, 0x48, 0x78, 0x38, 0x95, 0x9e, 0x35, 0x63, 0x97, 0x52, 0x11, 0x27, - 0x77, 0x3b, 0x49, 0xae, 0x38, 0x44, 0x46, 0x52, 0x6b, 0x83, 0x02, 0x95, 0xa4, 0x3f, 0xcc, 0x6b, - 0xf3, 0x3a, 0x79, 0x8d, 0xbd, 0x39, 0xdb, 0x3f, 0xdb, 0x8a, 0x17, 0xc5, 0xf4, 0xf5, 0xdf, 0x0d, - 0x90, 0x57, 0x0b, 0x09, 0xbe, 0x0f, 0x4a, 0xd8, 0xa7, 0x5f, 0x71, 0x16, 0xf8, 0xa2, 0x62, 0xe8, - 0xce, 0x5b, 0x8a, 0x42, 0xb3, 0xb4, 0xb9, 0xb7, 0x1d, 0x83, 0x28, 0xb3, 0xc3, 0x75, 0x50, 0xc6, - 0x3e, 0x4d, 0x1b, 0x75, 0x5e, 0x5f, 0x5f, 0x51, 0x63, 0xb3, 0xb9, 0xb7, 0x9d, 0x36, 0xe7, 0xe8, - 0x1d, 0xc5, 0xcf, 0x89, 0x60, 0x01, 0x77, 0x93, 0x55, 0x9a, 0xf0, 0xa3, 0x21, 0x88, 0x32, 0x3b, - 0xfc, 0x00, 0x14, 0x84, 0xcb, 0x7c, 0x92, 0x6c, 0xc3, 0x3b, 0xea, 0xd9, 0xfb, 0x0a, 0x38, 0x0f, - 0xcd, 0x92, 0xfe, 0xa1, 0xdb, 0x32, 0xbe, 0x54, 0xff, 0xc5, 0x00, 0x70, 0x7a, 0xe1, 0xc2, 0xcf, - 0x00, 0x60, 0xe9, 0x29, 0x49, 0xc9, 0xd4, 0xbd, 0x94, 0xa2, 0xe7, 0xa1, 0xb9, 0x94, 0x9e, 0x34, - 0xe5, 0x88, 0x0b, 0xfc, 0x1a, 0xe4, 0xd5, 0x92, 0x4e, 0x54, 0xe6, 0xbd, 0x99, 0x17, 0x7f, 0x26, - 0x5d, 0xea, 0x84, 0x34, 0x49, 0xfd, 0x67, 0x03, 0xdc, 0xde, 0x27, 0x7c, 0x40, 0x5d, 0x82, 0x48, - 0x9b, 0x70, 0xe2, 0xb9, 0x04, 0xda, 0xa0, 0x94, 0x2e, 0xc1, 0x44, 0xf6, 0x56, 0x13, 0xdf, 0x52, - 0xba, 0x30, 0x51, 0x76, 0x27, 0x95, 0xc8, 0xf9, 0x4b, 0x25, 0xf2, 0x1e, 0xc8, 0xfb, 0x58, 0x76, - 0x2b, 0x39, 0x7d, 0xa3, 0xa8, 0xac, 0x7b, 0x58, 0x76, 0x91, 0x46, 0xb5, 0x95, 0x71, 0xa9, 0xeb, - 0x5a, 0x48, 0xac, 0x8c, 0x4b, 0xa4, 0xd1, 0xfa, 0x9f, 0xb7, 0xc0, 0xea, 0x01, 0xee, 0xd1, 0xd6, - 0x8d, 0x2c, 0xdf, 0xc8, 0xf2, 0x95, 0xb2, 0x0c, 0x6e, 0x64, 0xf9, 0x3a, 0xb2, 0x5c, 0xff, 0xc3, - 0x00, 0xd5, 0xa9, 0x09, 0x7b, 0xdd, 0xb2, 0xf9, 0xcd, 0x94, 0x6c, 0x3e, 0x9a, 0x69, 0x7a, 0xa6, - 0x1e, 0x3e, 0x25, 0x9c, 0xff, 0x1a, 0xa0, 0x7e, 0x75, 0x7a, 0xaf, 0x41, 0x3a, 0xbb, 0xe3, 0xd2, - 0xb9, 0x75, 0xbd, 0xdc, 0x66, 0x11, 0xcf, 0x5f, 0x0d, 0xf0, 0xc6, 0x05, 0xfb, 0x0b, 0xbe, 0x0d, - 0x72, 0x01, 0xef, 0x25, 0x2b, 0x78, 0x21, 0x0a, 0xcd, 0xdc, 0x73, 0xb4, 0x83, 0x14, 0x06, 0x0f, - 0xc1, 0x82, 0x88, 0x55, 0x20, 0xc9, 0xfc, 0xe3, 0x99, 0x9e, 0x37, 0xa9, 0x1c, 0x4e, 0x39, 0x0a, - 0xcd, 0x85, 0x21, 0x3a, 0xa4, 0x84, 0x0d, 0x50, 0x74, 0xb1, 0x13, 0x78, 0xad, 0x44, 0xb5, 0x16, - 0x9d, 0x45, 0x55, 0xa4, 0xad, 0xcd, 0x18, 0x43, 0xa9, 0xd5, 0xd9, 0x3e, 0x39, 0xab, 0xce, 0xbd, - 0x3a, 0xab, 0xce, 0x9d, 0x9e, 0x55, 0xe7, 0x7e, 0x8c, 0xaa, 0xc6, 0x49, 0x54, 0x35, 0x5e, 0x45, - 0x55, 0xe3, 0x34, 0xaa, 0x1a, 0x7f, 0x45, 0x55, 0xe3, 0xa7, 0xbf, 0xab, 0x73, 0xdf, 0xde, 0x9f, - 0xe1, 0xdf, 0xec, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x43, 0x44, 0x86, 0xf5, 0x0c, 0x0f, 0x00, + // 1169 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x58, 0x4d, 0x6f, 0x1b, 0x45, + 0x18, 0xce, 0xc6, 0x36, 0xb1, 0xc7, 0x4e, 0xd2, 0x0c, 0xd0, 0x2e, 0xa5, 0xf2, 0x5a, 0xae, 0x84, + 0x82, 0x00, 0x6f, 0x9b, 0x96, 0x52, 0x71, 0x41, 0xb1, 0x29, 0x28, 0x22, 0x69, 0xa3, 0x49, 0x3f, + 0x10, 0xea, 0xa1, 0xe3, 0xf5, 0xd8, 0x1e, 0x62, 0xef, 0xac, 0x66, 0x66, 0x4d, 0x7b, 0xe3, 0x27, + 0xf0, 0x17, 0xe0, 0x4f, 0xc0, 0x95, 0x5b, 0x8f, 0xbd, 0x91, 0x03, 0x5a, 0x91, 0xe5, 0xc2, 0x81, + 0x5f, 0x90, 0x13, 0x9a, 0xd9, 0xf5, 0xae, 0xbf, 0x12, 0x56, 0x39, 0xe4, 0x94, 0x5b, 0xe6, 0x79, + 0xdf, 0xf7, 0x79, 0xe7, 0x19, 0xbf, 0x1f, 0xab, 0x80, 0xdd, 0xc3, 0xfb, 0xa2, 0x41, 0x99, 0x7d, + 0xe8, 0xb7, 0x09, 0x77, 0x89, 0x24, 0xc2, 0x1e, 0x11, 0xb7, 0xc3, 0xb8, 0x1d, 0x1b, 0xb0, 0x47, + 0x6d, 0xdc, 0x19, 0x52, 0x21, 0x28, 0x73, 0x39, 0xe9, 0x51, 0x21, 0x39, 0x96, 0x94, 0xb9, 0xf6, + 0xe8, 0xb6, 0xdd, 0x23, 0x2e, 0xe1, 0x58, 0x92, 0x4e, 0xc3, 0xe3, 0x4c, 0x32, 0x78, 0x33, 0x0a, + 0x6a, 0x60, 0x8f, 0x36, 0x16, 0x06, 0x35, 0x46, 0xb7, 0xaf, 0x7f, 0xd2, 0xa3, 0xb2, 0xef, 0xb7, + 0x1b, 0x0e, 0x1b, 0xda, 0x3d, 0xd6, 0x63, 0xb6, 0x8e, 0x6d, 0xfb, 0x5d, 0x7d, 0xd2, 0x07, 0xfd, + 0x57, 0xc4, 0x79, 0xfd, 0x6e, 0x7a, 0x91, 0x21, 0x76, 0xfa, 0xd4, 0x25, 0xfc, 0x95, 0xed, 0x1d, + 0xf6, 0x14, 0x20, 0xec, 0x21, 0x91, 0x78, 0xc1, 0x4d, 0xae, 0xdb, 0xa7, 0x45, 0x71, 0xdf, 0x95, + 0x74, 0x48, 0xe6, 0x02, 0xee, 0xfd, 0x5f, 0x80, 0x70, 0xfa, 0x64, 0x88, 0x67, 0xe3, 0xea, 0x5d, + 0xb0, 0xb6, 0x87, 0xa5, 0xd3, 0x6f, 0x31, 0xb7, 0x43, 0x95, 0x44, 0x58, 0x03, 0x79, 0x17, 0x0f, + 0x89, 0x69, 0xd4, 0x8c, 0xcd, 0x52, 0xb3, 0xf2, 0x3a, 0xb0, 0x96, 0xc2, 0xc0, 0xca, 0x3f, 0xc4, + 0x43, 0x82, 0xb4, 0x05, 0x6e, 0x01, 0x40, 0x5e, 0x7a, 0x9c, 0xe8, 0xe7, 0x31, 0x97, 0xb5, 0x1f, + 0x8c, 0xfd, 0xc0, 0x83, 0xc4, 0x82, 0x26, 0xbc, 0xea, 0xbf, 0x16, 0xc1, 0xfa, 0x9e, 0x2f, 0xb1, + 0xa4, 0x6e, 0xef, 0x19, 0x69, 0xf7, 0x19, 0x3b, 0xcc, 0x90, 0x89, 0x83, 0x8a, 0x33, 0xa0, 0xc4, + 0x95, 0x2d, 0xe6, 0x76, 0x69, 0x4f, 0xe7, 0x2a, 0x6f, 0xdd, 0x6f, 0x64, 0xf8, 0x9d, 0x1a, 0x71, + 0x96, 0xd6, 0x44, 0x7c, 0xf3, 0x9d, 0x38, 0x47, 0x65, 0x12, 0x45, 0x53, 0x39, 0xe0, 0x73, 0x50, + 0xe0, 0xfe, 0x80, 0x08, 0x33, 0x57, 0xcb, 0x6d, 0x96, 0xb7, 0x3e, 0xcb, 0x94, 0x0c, 0xf9, 0x03, + 0xf2, 0x8c, 0xca, 0xfe, 0x23, 0x8f, 0x44, 0xa0, 0x68, 0xae, 0xc6, 0xb9, 0x0a, 0xca, 0x26, 0x50, + 0x44, 0x0a, 0x77, 0xc1, 0x6a, 0x17, 0xd3, 0x81, 0xcf, 0xc9, 0x3e, 0x1b, 0x50, 0xe7, 0x95, 0x99, + 0xd7, 0xe2, 0x3f, 0x08, 0x03, 0x6b, 0xf5, 0xab, 0x49, 0xc3, 0x49, 0x60, 0x6d, 0x4c, 0x01, 0x8f, + 0x5f, 0x79, 0x04, 0x4d, 0x07, 0xc3, 0x2f, 0x41, 0x79, 0xa8, 0x7e, 0xbd, 0x98, 0xab, 0xa4, 0xb9, + 0xea, 0x61, 0x60, 0x95, 0xf7, 0x52, 0xf8, 0x24, 0xb0, 0xd6, 0x27, 0x8e, 0x9a, 0x67, 0x32, 0x0c, + 0xbe, 0x04, 0x1b, 0xea, 0xb5, 0x85, 0x87, 0x1d, 0x72, 0x40, 0x06, 0xc4, 0x91, 0x8c, 0x9b, 0x05, + 0xfd, 0xd4, 0x77, 0x26, 0xd4, 0x27, 0x75, 0xd5, 0xf0, 0x0e, 0x7b, 0x0a, 0x10, 0x0d, 0x55, 0xbe, + 0x4a, 0xfe, 0x2e, 0x6e, 0x93, 0xc1, 0x38, 0xb4, 0xf9, 0x6e, 0x18, 0x58, 0x1b, 0x0f, 0x67, 0x19, + 0xd1, 0x7c, 0x12, 0xc8, 0xc0, 0x1a, 0x6b, 0x7f, 0x4f, 0x1c, 0x99, 0xa4, 0x2d, 0x9f, 0x3f, 0x2d, + 0x0c, 0x03, 0x6b, 0xed, 0xd1, 0x14, 0x1d, 0x9a, 0xa1, 0x57, 0x0f, 0x26, 0x68, 0x87, 0x3c, 0xe8, + 0x76, 0x89, 0x23, 0x85, 0xf9, 0x56, 0xfa, 0x60, 0x07, 0x29, 0xac, 0x1e, 0x2c, 0x3d, 0xb6, 0x06, + 0x58, 0x08, 0x34, 0x19, 0x06, 0x3f, 0x07, 0x6b, 0xaa, 0xa7, 0x98, 0x2f, 0x0f, 0x88, 0xc3, 0xdc, + 0x8e, 0x30, 0x57, 0x6a, 0xc6, 0x66, 0x21, 0xba, 0xc1, 0xe3, 0x29, 0x0b, 0x9a, 0xf1, 0x84, 0x4f, + 0xc0, 0xb5, 0xa4, 0x8a, 0x10, 0x19, 0x51, 0xf2, 0xc3, 0x53, 0xc2, 0xd5, 0x41, 0x98, 0xc5, 0x5a, + 0x6e, 0xb3, 0xd4, 0x7c, 0x3f, 0x0c, 0xac, 0x6b, 0xdb, 0x8b, 0x5d, 0xd0, 0x69, 0xb1, 0xf0, 0x05, + 0x80, 0x9c, 0x50, 0x77, 0xc4, 0x1c, 0x5d, 0x7e, 0x71, 0x41, 0x00, 0xad, 0xef, 0x56, 0x18, 0x58, + 0x10, 0xcd, 0x59, 0x4f, 0x02, 0xeb, 0xea, 0x3c, 0xaa, 0xcb, 0x63, 0x01, 0x17, 0x1c, 0x81, 0xf5, + 0xe1, 0xd4, 0xa4, 0x10, 0x66, 0x45, 0x77, 0xc8, 0x9d, 0x4c, 0x1d, 0x32, 0x3d, 0x65, 0x9a, 0xd7, + 0xe2, 0xee, 0x58, 0x9f, 0xc6, 0x05, 0x9a, 0x4d, 0x52, 0x3f, 0x32, 0xc0, 0x8d, 0x99, 0xc9, 0x11, + 0x75, 0xaa, 0x1f, 0x91, 0xc3, 0x17, 0xa0, 0xa8, 0x0a, 0xa2, 0x83, 0x25, 0xd6, 0xa3, 0xa4, 0xbc, + 0x75, 0x2b, 0x5b, 0xf9, 0x44, 0xb5, 0xb2, 0x47, 0x24, 0x4e, 0xc7, 0x57, 0x8a, 0xa1, 0x84, 0x15, + 0x3e, 0x05, 0xc5, 0x38, 0xb3, 0x30, 0x97, 0xb5, 0xe6, 0xbb, 0xd9, 0x34, 0x4f, 0x5f, 0xbb, 0x99, + 0x57, 0x59, 0x50, 0xc2, 0x55, 0xff, 0xc7, 0x00, 0xb5, 0xb3, 0xa4, 0xed, 0x52, 0x21, 0xe1, 0xf3, + 0x39, 0x79, 0x8d, 0x8c, 0xdd, 0x41, 0x45, 0x24, 0xee, 0x4a, 0x2c, 0xae, 0x38, 0x46, 0x26, 0xa4, + 0x75, 0x41, 0x81, 0x4a, 0x32, 0x1c, 0xeb, 0xda, 0x3e, 0x8f, 0xae, 0xa9, 0x3b, 0xa7, 0x73, 0x6f, + 0x47, 0xf1, 0xa2, 0x88, 0xbe, 0xfe, 0xbb, 0x01, 0xf2, 0x6a, 0x10, 0xc2, 0x8f, 0x40, 0x09, 0x7b, + 0xf4, 0x6b, 0xce, 0x7c, 0x4f, 0x98, 0x86, 0xae, 0xf8, 0xd5, 0x30, 0xb0, 0x4a, 0xdb, 0xfb, 0x3b, + 0x11, 0x88, 0x52, 0x3b, 0xbc, 0x0d, 0xca, 0xd8, 0xa3, 0x49, 0x83, 0x2c, 0x6b, 0xf7, 0x75, 0xd5, + 0xae, 0xdb, 0xfb, 0x3b, 0x49, 0x53, 0x4c, 0xfa, 0x28, 0x7e, 0x4e, 0x04, 0xf3, 0xb9, 0x13, 0x8f, + 0xf0, 0x98, 0x1f, 0x8d, 0x41, 0x94, 0xda, 0xe1, 0xc7, 0xa0, 0x20, 0x1c, 0xe6, 0x91, 0x78, 0x0a, + 0x5f, 0x55, 0xd7, 0x3e, 0x50, 0xc0, 0x49, 0x60, 0x95, 0xf4, 0x1f, 0xba, 0x1d, 0x22, 0xa7, 0xfa, + 0x2f, 0x06, 0x80, 0xf3, 0x83, 0x1e, 0x7e, 0x01, 0x00, 0x4b, 0x4e, 0xb1, 0x24, 0x4b, 0xd7, 0x52, + 0x82, 0x9e, 0x04, 0xd6, 0x6a, 0x72, 0xd2, 0x94, 0x13, 0x21, 0xf0, 0x1b, 0x90, 0x57, 0xcb, 0x21, + 0xde, 0x6e, 0x1f, 0x66, 0x5e, 0x38, 0xe9, 0xca, 0x54, 0x27, 0xa4, 0x49, 0xea, 0x3f, 0x1b, 0xe0, + 0xca, 0x01, 0xe1, 0x23, 0xea, 0x10, 0x44, 0xba, 0x84, 0x13, 0xd7, 0x21, 0xd0, 0x06, 0xa5, 0x64, + 0xf8, 0xc6, 0xeb, 0x76, 0x23, 0x8e, 0x2d, 0x25, 0x83, 0x1a, 0xa5, 0x3e, 0xc9, 0x6a, 0x5e, 0x3e, + 0x75, 0x35, 0xdf, 0x00, 0x79, 0x0f, 0xcb, 0xbe, 0x99, 0xd3, 0x1e, 0x45, 0x65, 0xdd, 0xc7, 0xb2, + 0x8f, 0x34, 0xaa, 0xad, 0x8c, 0x4b, 0xfd, 0xae, 0x85, 0xd8, 0xca, 0xb8, 0x44, 0x1a, 0xad, 0xff, + 0xb1, 0x02, 0x36, 0x9e, 0xe2, 0x01, 0xed, 0x5c, 0x7e, 0x0e, 0x5c, 0x7e, 0x0e, 0x9c, 0xf9, 0x39, + 0x00, 0x2e, 0x3f, 0x07, 0xce, 0xf5, 0x39, 0xb0, 0x60, 0x59, 0x97, 0x2f, 0x62, 0x59, 0xff, 0x69, + 0x80, 0xea, 0x5c, 0x67, 0x5f, 0xf4, 0xba, 0xfe, 0x76, 0x6e, 0x5d, 0xdf, 0xcb, 0xa4, 0x7a, 0xee, + 0xe2, 0x73, 0x0b, 0xfb, 0x5f, 0x03, 0xd4, 0xcf, 0x96, 0x77, 0x01, 0x2b, 0xbb, 0x3f, 0xbd, 0xb2, + 0x5b, 0xe7, 0xd3, 0x96, 0x65, 0x69, 0xff, 0x66, 0x80, 0xb7, 0x17, 0xcc, 0x4d, 0xf8, 0x1e, 0xc8, + 0xf9, 0x7c, 0x10, 0x8f, 0xfe, 0x95, 0x30, 0xb0, 0x72, 0x4f, 0xd0, 0x2e, 0x52, 0x18, 0x7c, 0x0e, + 0x56, 0x44, 0xb4, 0x7d, 0x62, 0xe5, 0x9f, 0x66, 0xba, 0xde, 0xec, 0xc6, 0x6a, 0x96, 0xc3, 0xc0, + 0x5a, 0x19, 0xa3, 0x63, 0x4a, 0xb8, 0x09, 0x8a, 0x0e, 0x6e, 0xfa, 0x6e, 0x27, 0xde, 0x96, 0x95, + 0x66, 0x45, 0x3d, 0x52, 0x6b, 0x3b, 0xc2, 0x50, 0x62, 0x6d, 0xee, 0xbc, 0x3e, 0xae, 0x2e, 0xbd, + 0x39, 0xae, 0x2e, 0x1d, 0x1d, 0x57, 0x97, 0x7e, 0x0c, 0xab, 0xc6, 0xeb, 0xb0, 0x6a, 0xbc, 0x09, + 0xab, 0xc6, 0x51, 0x58, 0x35, 0xfe, 0x0a, 0xab, 0xc6, 0x4f, 0x7f, 0x57, 0x97, 0xbe, 0xbb, 0x99, + 0xe1, 0xbf, 0x04, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x7f, 0xe1, 0x3a, 0x73, 0x64, 0x10, 0x00, 0x00, } +func (m *MatchCondition) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MatchCondition) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MatchCondition) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i -= len(m.Expression) + copy(dAtA[i:], m.Expression) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Expression))) + i-- + dAtA[i] = 0x12 + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + func (m *MutatingWebhook) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -435,6 +501,20 @@ func (m *MutatingWebhook) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.MatchConditions) > 0 { + for iNdEx := len(m.MatchConditions) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MatchConditions[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x62 + } + } if m.ObjectSelector != nil { { size, err := m.ObjectSelector.MarshalToSizedBuffer(dAtA[:i]) @@ -791,6 +871,20 @@ func (m *ValidatingWebhook) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.MatchConditions) > 0 { + for iNdEx := len(m.MatchConditions) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MatchConditions[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x5a + } + } if m.ObjectSelector != nil { { size, err := m.ObjectSelector.MarshalToSizedBuffer(dAtA[:i]) @@ -1036,6 +1130,19 @@ func encodeVarintGenerated(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) return base } +func (m *MatchCondition) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + n += 1 + l + sovGenerated(uint64(l)) + l = len(m.Expression) + n += 1 + l + sovGenerated(uint64(l)) + return n +} + func (m *MutatingWebhook) Size() (n int) { if m == nil { return 0 @@ -1085,6 +1192,12 @@ func (m *MutatingWebhook) Size() (n int) { l = m.ObjectSelector.Size() n += 1 + l + sovGenerated(uint64(l)) } + if len(m.MatchConditions) > 0 { + for _, e := range m.MatchConditions { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } return n } @@ -1235,6 +1348,12 @@ func (m *ValidatingWebhook) Size() (n int) { l = m.ObjectSelector.Size() n += 1 + l + sovGenerated(uint64(l)) } + if len(m.MatchConditions) > 0 { + for _, e := range m.MatchConditions { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } return n } @@ -1299,6 +1418,17 @@ func sovGenerated(x uint64) (n int) { func sozGenerated(x uint64) (n int) { return sovGenerated(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } +func (this *MatchCondition) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&MatchCondition{`, + `Name:` + fmt.Sprintf("%v", this.Name) + `,`, + `Expression:` + fmt.Sprintf("%v", this.Expression) + `,`, + `}`, + }, "") + return s +} func (this *MutatingWebhook) String() string { if this == nil { return "nil" @@ -1308,6 +1438,11 @@ func (this *MutatingWebhook) String() string { repeatedStringForRules += strings.Replace(strings.Replace(f.String(), "RuleWithOperations", "RuleWithOperations", 1), `&`, ``, 1) + "," } repeatedStringForRules += "}" + repeatedStringForMatchConditions := "[]MatchCondition{" + for _, f := range this.MatchConditions { + repeatedStringForMatchConditions += strings.Replace(strings.Replace(f.String(), "MatchCondition", "MatchCondition", 1), `&`, ``, 1) + "," + } + repeatedStringForMatchConditions += "}" s := strings.Join([]string{`&MutatingWebhook{`, `Name:` + fmt.Sprintf("%v", this.Name) + `,`, `ClientConfig:` + strings.Replace(strings.Replace(this.ClientConfig.String(), "WebhookClientConfig", "WebhookClientConfig", 1), `&`, ``, 1) + `,`, @@ -1320,6 +1455,7 @@ func (this *MutatingWebhook) String() string { `MatchPolicy:` + valueToStringGenerated(this.MatchPolicy) + `,`, `ReinvocationPolicy:` + valueToStringGenerated(this.ReinvocationPolicy) + `,`, `ObjectSelector:` + strings.Replace(fmt.Sprintf("%v", this.ObjectSelector), "LabelSelector", "v1.LabelSelector", 1) + `,`, + `MatchConditions:` + repeatedStringForMatchConditions + `,`, `}`, }, "") return s @@ -1402,6 +1538,11 @@ func (this *ValidatingWebhook) String() string { repeatedStringForRules += strings.Replace(strings.Replace(f.String(), "RuleWithOperations", "RuleWithOperations", 1), `&`, ``, 1) + "," } repeatedStringForRules += "}" + repeatedStringForMatchConditions := "[]MatchCondition{" + for _, f := range this.MatchConditions { + repeatedStringForMatchConditions += strings.Replace(strings.Replace(f.String(), "MatchCondition", "MatchCondition", 1), `&`, ``, 1) + "," + } + repeatedStringForMatchConditions += "}" s := strings.Join([]string{`&ValidatingWebhook{`, `Name:` + fmt.Sprintf("%v", this.Name) + `,`, `ClientConfig:` + strings.Replace(strings.Replace(this.ClientConfig.String(), "WebhookClientConfig", "WebhookClientConfig", 1), `&`, ``, 1) + `,`, @@ -1413,6 +1554,7 @@ func (this *ValidatingWebhook) String() string { `AdmissionReviewVersions:` + fmt.Sprintf("%v", this.AdmissionReviewVersions) + `,`, `MatchPolicy:` + valueToStringGenerated(this.MatchPolicy) + `,`, `ObjectSelector:` + strings.Replace(fmt.Sprintf("%v", this.ObjectSelector), "LabelSelector", "v1.LabelSelector", 1) + `,`, + `MatchConditions:` + repeatedStringForMatchConditions + `,`, `}`, }, "") return s @@ -1469,6 +1611,120 @@ func valueToStringGenerated(v interface{}) string { pv := reflect.Indirect(rv).Interface() return fmt.Sprintf("*%v", pv) } +func (m *MatchCondition) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MatchCondition: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MatchCondition: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Expression", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Expression = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *MutatingWebhook) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1853,6 +2109,40 @@ func (m *MutatingWebhook) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 12: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MatchConditions", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MatchConditions = append(m.MatchConditions, MatchCondition{}) + if err := m.MatchConditions[len(m.MatchConditions)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -2920,6 +3210,40 @@ func (m *ValidatingWebhook) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 11: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MatchConditions", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MatchConditions = append(m.MatchConditions, MatchCondition{}) + if err := m.MatchConditions[len(m.MatchConditions)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/staging/src/k8s.io/api/admissionregistration/v1/generated.proto b/staging/src/k8s.io/api/admissionregistration/v1/generated.proto index aa266a2a508..cdf1f47655f 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1/generated.proto +++ b/staging/src/k8s.io/api/admissionregistration/v1/generated.proto @@ -28,6 +28,35 @@ import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto"; // Package-wide variables from generator "generated". option go_package = "k8s.io/api/admissionregistration/v1"; +// MatchCondition represents a condition which must by fulfilled for a request to be sent to a webhook. +message MatchCondition { + // Name is an identifier for this match condition, used for strategic merging of MatchConditions, + // as well as providing an identifier for logging purposes. A good name should be descriptive of + // the associated expression. + // Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and + // must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or + // '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an + // optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName') + // + // Required. + optional string name = 1; + + // Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. + // CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables: + // + // 'object' - The object from the incoming request. The value is null for DELETE requests. + // 'oldObject' - The existing object. The value is null for CREATE requests. + // 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). + // 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Required. + optional string expression = 2; +} + // MutatingWebhook describes an admission webhook and the resources and operations it applies to. message MutatingWebhook { // The name of the admission webhook. @@ -173,6 +202,28 @@ message MutatingWebhook { // Defaults to "Never". // +optional optional string reinvocationPolicy = 10; + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +featureGate=AdmissionWebhookMatchConditions + // +optional + repeated MatchCondition matchConditions = 12; } // MutatingWebhookConfiguration describes the configuration of and admission webhook that accept or reject and may change the object. @@ -409,6 +460,28 @@ message ValidatingWebhook { // include any versions known to the API Server, calls to the webhook will fail // and be subject to the failure policy. repeated string admissionReviewVersions = 8; + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +featureGate=AdmissionWebhookMatchConditions + // +optional + repeated MatchCondition matchConditions = 11; } // ValidatingWebhookConfiguration describes the configuration of and admission webhook that accept or reject and object without changing it. diff --git a/staging/src/k8s.io/api/admissionregistration/v1/types.go b/staging/src/k8s.io/api/admissionregistration/v1/types.go index e74b276f654..74f17d54a2b 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1/types.go +++ b/staging/src/k8s.io/api/admissionregistration/v1/types.go @@ -307,6 +307,28 @@ type ValidatingWebhook struct { // include any versions known to the API Server, calls to the webhook will fail // and be subject to the failure policy. AdmissionReviewVersions []string `json:"admissionReviewVersions" protobuf:"bytes,8,rep,name=admissionReviewVersions"` + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +featureGate=AdmissionWebhookMatchConditions + // +optional + MatchConditions []MatchCondition `json:"matchConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,11,opt,name=matchConditions"` } // MutatingWebhook describes an admission webhook and the resources and operations it applies to. @@ -454,6 +476,28 @@ type MutatingWebhook struct { // Defaults to "Never". // +optional ReinvocationPolicy *ReinvocationPolicyType `json:"reinvocationPolicy,omitempty" protobuf:"bytes,10,opt,name=reinvocationPolicy,casttype=ReinvocationPolicyType"` + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +featureGate=AdmissionWebhookMatchConditions + // +optional + MatchConditions []MatchCondition `json:"matchConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,12,opt,name=matchConditions"` } // ReinvocationPolicyType specifies what type of policy the admission hook uses. @@ -563,3 +607,32 @@ type ServiceReference struct { // +optional Port *int32 `json:"port,omitempty" protobuf:"varint,4,opt,name=port"` } + +// MatchCondition represents a condition which must by fulfilled for a request to be sent to a webhook. +type MatchCondition struct { + // Name is an identifier for this match condition, used for strategic merging of MatchConditions, + // as well as providing an identifier for logging purposes. A good name should be descriptive of + // the associated expression. + // Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and + // must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or + // '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an + // optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName') + // + // Required. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + + // Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. + // CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables: + // + // 'object' - The object from the incoming request. The value is null for DELETE requests. + // 'oldObject' - The existing object. The value is null for CREATE requests. + // 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). + // 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Required. + Expression string `json:"expression" protobuf:"bytes,2,opt,name=expression"` +} diff --git a/staging/src/k8s.io/api/admissionregistration/v1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/admissionregistration/v1/types_swagger_doc_generated.go index 958636f7006..ce306b307a8 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/admissionregistration/v1/types_swagger_doc_generated.go @@ -27,6 +27,16 @@ package v1 // Those methods can be generated by using hack/update-codegen.sh // AUTO-GENERATED FUNCTIONS START HERE. DO NOT EDIT. +var map_MatchCondition = map[string]string{ + "": "MatchCondition represents a condition which must by fulfilled for a request to be sent to a webhook.", + "name": "Name is an identifier for this match condition, used for strategic merging of MatchConditions, as well as providing an identifier for logging purposes. A good name should be descriptive of the associated expression. Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')\n\nRequired.", + "expression": "Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\nDocumentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/\n\nRequired.", +} + +func (MatchCondition) SwaggerDoc() map[string]string { + return map_MatchCondition +} + var map_MutatingWebhook = map[string]string{ "": "MutatingWebhook describes an admission webhook and the resources and operations it applies to.", "name": "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.", @@ -40,6 +50,7 @@ var map_MutatingWebhook = map[string]string{ "timeoutSeconds": "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. Default to 10 seconds.", "admissionReviewVersions": "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 a persisted webhook configuration specifies allowed versions and does not include any versions known to the API Server, calls to the webhook will fail and be subject to the failure policy.", "reinvocationPolicy": "reinvocationPolicy indicates whether this webhook should be called multiple times as part of a single admission evaluation. Allowed values are \"Never\" and \"IfNeeded\".\n\nNever: the webhook will not be called more than once in a single admission evaluation.\n\nIfNeeded: the webhook will be called at least one additional time as part of the admission evaluation if the object being admitted is modified by other admission plugins after the initial webhook call. Webhooks that specify this option *must* be idempotent, able to process objects they previously admitted. Note: * the number of additional invocations is not guaranteed to be exactly one. * if additional invocations result in further modifications to the object, webhooks are not guaranteed to be invoked again. * webhooks that use this option may be reordered to minimize the number of additional invocations. * to validate an object after all mutations are guaranteed complete, use a validating admission webhook instead.\n\nDefaults to \"Never\".", + "matchConditions": "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", } func (MutatingWebhook) SwaggerDoc() map[string]string { @@ -111,6 +122,7 @@ var map_ValidatingWebhook = map[string]string{ "sideEffects": "SideEffects states whether this webhook has side effects. Acceptable values are: None, NoneOnDryRun (webhooks created via v1beta1 may also specify Some or Unknown). Webhooks with side effects MUST implement a reconciliation system, since a request may be rejected by a future step in the admission chain 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.", "timeoutSeconds": "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. Default to 10 seconds.", "admissionReviewVersions": "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 a persisted webhook configuration specifies allowed versions and does not include any versions known to the API Server, calls to the webhook will fail and be subject to the failure policy.", + "matchConditions": "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", } func (ValidatingWebhook) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/api/admissionregistration/v1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/admissionregistration/v1/zz_generated.deepcopy.go index cff7377a557..b9560991384 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/admissionregistration/v1/zz_generated.deepcopy.go @@ -26,6 +26,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MatchCondition) DeepCopyInto(out *MatchCondition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchCondition. +func (in *MatchCondition) DeepCopy() *MatchCondition { + if in == nil { + return nil + } + out := new(MatchCondition) + in.DeepCopyInto(out) + return out +} + // 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 @@ -77,6 +93,11 @@ func (in *MutatingWebhook) DeepCopyInto(out *MutatingWebhook) { *out = new(ReinvocationPolicyType) **out = **in } + if in.MatchConditions != nil { + in, out := &in.MatchConditions, &out.MatchConditions + *out = make([]MatchCondition, len(*in)) + copy(*out, *in) + } return } @@ -286,6 +307,11 @@ func (in *ValidatingWebhook) DeepCopyInto(out *ValidatingWebhook) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.MatchConditions != nil { + in, out := &in.MatchConditions, &out.MatchConditions + *out = make([]MatchCondition, len(*in)) + copy(*out, *in) + } return } diff --git a/staging/src/k8s.io/api/admissionregistration/v1beta1/generated.pb.go b/staging/src/k8s.io/api/admissionregistration/v1beta1/generated.pb.go index 56a9f10e5cd..8fb354c319a 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1beta1/generated.pb.go +++ b/staging/src/k8s.io/api/admissionregistration/v1beta1/generated.pb.go @@ -45,10 +45,38 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package +func (m *MatchCondition) Reset() { *m = MatchCondition{} } +func (*MatchCondition) ProtoMessage() {} +func (*MatchCondition) Descriptor() ([]byte, []int) { + return fileDescriptor_abeea74cbc46f55a, []int{0} +} +func (m *MatchCondition) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MatchCondition) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *MatchCondition) XXX_Merge(src proto.Message) { + xxx_messageInfo_MatchCondition.Merge(m, src) +} +func (m *MatchCondition) XXX_Size() int { + return m.Size() +} +func (m *MatchCondition) XXX_DiscardUnknown() { + xxx_messageInfo_MatchCondition.DiscardUnknown(m) +} + +var xxx_messageInfo_MatchCondition proto.InternalMessageInfo + func (m *MutatingWebhook) Reset() { *m = MutatingWebhook{} } func (*MutatingWebhook) ProtoMessage() {} func (*MutatingWebhook) Descriptor() ([]byte, []int) { - return fileDescriptor_abeea74cbc46f55a, []int{0} + return fileDescriptor_abeea74cbc46f55a, []int{1} } func (m *MutatingWebhook) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -76,7 +104,7 @@ var xxx_messageInfo_MutatingWebhook proto.InternalMessageInfo func (m *MutatingWebhookConfiguration) Reset() { *m = MutatingWebhookConfiguration{} } func (*MutatingWebhookConfiguration) ProtoMessage() {} func (*MutatingWebhookConfiguration) Descriptor() ([]byte, []int) { - return fileDescriptor_abeea74cbc46f55a, []int{1} + return fileDescriptor_abeea74cbc46f55a, []int{2} } func (m *MutatingWebhookConfiguration) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -104,7 +132,7 @@ var xxx_messageInfo_MutatingWebhookConfiguration proto.InternalMessageInfo func (m *MutatingWebhookConfigurationList) Reset() { *m = MutatingWebhookConfigurationList{} } func (*MutatingWebhookConfigurationList) ProtoMessage() {} func (*MutatingWebhookConfigurationList) Descriptor() ([]byte, []int) { - return fileDescriptor_abeea74cbc46f55a, []int{2} + return fileDescriptor_abeea74cbc46f55a, []int{3} } func (m *MutatingWebhookConfigurationList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -132,7 +160,7 @@ var xxx_messageInfo_MutatingWebhookConfigurationList proto.InternalMessageInfo func (m *ServiceReference) Reset() { *m = ServiceReference{} } func (*ServiceReference) ProtoMessage() {} func (*ServiceReference) Descriptor() ([]byte, []int) { - return fileDescriptor_abeea74cbc46f55a, []int{3} + return fileDescriptor_abeea74cbc46f55a, []int{4} } func (m *ServiceReference) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -160,7 +188,7 @@ var xxx_messageInfo_ServiceReference proto.InternalMessageInfo func (m *ValidatingWebhook) Reset() { *m = ValidatingWebhook{} } func (*ValidatingWebhook) ProtoMessage() {} func (*ValidatingWebhook) Descriptor() ([]byte, []int) { - return fileDescriptor_abeea74cbc46f55a, []int{4} + return fileDescriptor_abeea74cbc46f55a, []int{5} } func (m *ValidatingWebhook) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -188,7 +216,7 @@ var xxx_messageInfo_ValidatingWebhook proto.InternalMessageInfo func (m *ValidatingWebhookConfiguration) Reset() { *m = ValidatingWebhookConfiguration{} } func (*ValidatingWebhookConfiguration) ProtoMessage() {} func (*ValidatingWebhookConfiguration) Descriptor() ([]byte, []int) { - return fileDescriptor_abeea74cbc46f55a, []int{5} + return fileDescriptor_abeea74cbc46f55a, []int{6} } func (m *ValidatingWebhookConfiguration) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -216,7 +244,7 @@ var xxx_messageInfo_ValidatingWebhookConfiguration proto.InternalMessageInfo func (m *ValidatingWebhookConfigurationList) Reset() { *m = ValidatingWebhookConfigurationList{} } func (*ValidatingWebhookConfigurationList) ProtoMessage() {} func (*ValidatingWebhookConfigurationList) Descriptor() ([]byte, []int) { - return fileDescriptor_abeea74cbc46f55a, []int{6} + return fileDescriptor_abeea74cbc46f55a, []int{7} } func (m *ValidatingWebhookConfigurationList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -244,7 +272,7 @@ var xxx_messageInfo_ValidatingWebhookConfigurationList proto.InternalMessageInfo func (m *WebhookClientConfig) Reset() { *m = WebhookClientConfig{} } func (*WebhookClientConfig) ProtoMessage() {} func (*WebhookClientConfig) Descriptor() ([]byte, []int) { - return fileDescriptor_abeea74cbc46f55a, []int{7} + return fileDescriptor_abeea74cbc46f55a, []int{8} } func (m *WebhookClientConfig) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -270,6 +298,7 @@ func (m *WebhookClientConfig) XXX_DiscardUnknown() { var xxx_messageInfo_WebhookClientConfig proto.InternalMessageInfo func init() { + proto.RegisterType((*MatchCondition)(nil), "k8s.io.api.admissionregistration.v1beta1.MatchCondition") proto.RegisterType((*MutatingWebhook)(nil), "k8s.io.api.admissionregistration.v1beta1.MutatingWebhook") proto.RegisterType((*MutatingWebhookConfiguration)(nil), "k8s.io.api.admissionregistration.v1beta1.MutatingWebhookConfiguration") proto.RegisterType((*MutatingWebhookConfigurationList)(nil), "k8s.io.api.admissionregistration.v1beta1.MutatingWebhookConfigurationList") @@ -285,68 +314,106 @@ func init() { } var fileDescriptor_abeea74cbc46f55a = []byte{ - // 974 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x57, 0x49, 0x6f, 0xdb, 0x46, - 0x14, 0x36, 0x2d, 0x29, 0x92, 0x46, 0xb2, 0x13, 0x4d, 0x97, 0xb0, 0x6e, 0x40, 0x0a, 0x3a, 0x14, - 0xba, 0x94, 0x4c, 0x9c, 0xa2, 0x4b, 0x8a, 0x1e, 0x42, 0xb7, 0x41, 0x0b, 0xd8, 0x4e, 0x3a, 0xce, - 0x02, 0xb4, 0x29, 0x90, 0x11, 0xf5, 0x24, 0x4d, 0x45, 0x72, 0x04, 0xce, 0x50, 0xa9, 0x6f, 0xfd, - 0x09, 0xfd, 0x0b, 0xfd, 0x21, 0xbd, 0xf5, 0xe0, 0x63, 0x8e, 0xb9, 0x94, 0xa8, 0xd9, 0x5e, 0x7b, - 0xe8, 0xd5, 0xa7, 0x82, 0x8b, 0x76, 0x39, 0x21, 0x5c, 0x20, 0x27, 0xdf, 0x34, 0xdf, 0xe3, 0xf7, - 0xbd, 0x79, 0x6f, 0xde, 0x02, 0xa1, 0x6f, 0x87, 0x9f, 0x0a, 0x83, 0x71, 0x73, 0x18, 0x74, 0xc0, - 0xf7, 0x40, 0x82, 0x30, 0xc7, 0xe0, 0x75, 0xb9, 0x6f, 0x66, 0x06, 0x3a, 0x62, 0x26, 0xed, 0xba, - 0x4c, 0x08, 0xc6, 0x3d, 0x1f, 0xfa, 0x4c, 0x48, 0x9f, 0x4a, 0xc6, 0x3d, 0x73, 0x7c, 0xab, 0x03, - 0x92, 0xde, 0x32, 0xfb, 0xe0, 0x81, 0x4f, 0x25, 0x74, 0x8d, 0x91, 0xcf, 0x25, 0xc7, 0xed, 0x94, - 0x69, 0xd0, 0x11, 0x33, 0xd6, 0x32, 0x8d, 0x8c, 0xb9, 0xf3, 0x61, 0x9f, 0xc9, 0x41, 0xd0, 0x31, - 0x6c, 0xee, 0x9a, 0x7d, 0xde, 0xe7, 0x66, 0x22, 0xd0, 0x09, 0x7a, 0xc9, 0x29, 0x39, 0x24, 0xbf, - 0x52, 0xe1, 0x9d, 0xdb, 0x39, 0xae, 0xb4, 0x7c, 0x9b, 0x9d, 0x8f, 0x66, 0x24, 0x97, 0xda, 0x03, - 0xe6, 0x81, 0x7f, 0x6c, 0x8e, 0x86, 0xfd, 0x18, 0x10, 0xa6, 0x0b, 0x92, 0xae, 0x63, 0x99, 0xe7, - 0xb1, 0xfc, 0xc0, 0x93, 0xcc, 0x85, 0x15, 0xc2, 0xc7, 0xaf, 0x23, 0x08, 0x7b, 0x00, 0x2e, 0x5d, - 0xe6, 0xb5, 0x7e, 0x2f, 0xa3, 0xab, 0x07, 0x81, 0xa4, 0x92, 0x79, 0xfd, 0x27, 0xd0, 0x19, 0x70, - 0x3e, 0xc4, 0x4d, 0x54, 0xf4, 0xa8, 0x0b, 0xaa, 0xd2, 0x54, 0xda, 0x55, 0xab, 0x7e, 0x12, 0xea, - 0x1b, 0x51, 0xa8, 0x17, 0x0f, 0xa9, 0x0b, 0x24, 0xb1, 0xe0, 0xe7, 0xa8, 0x6e, 0x3b, 0x0c, 0x3c, - 0xb9, 0xc7, 0xbd, 0x1e, 0xeb, 0xab, 0x9b, 0x4d, 0xa5, 0x5d, 0xdb, 0xfd, 0xc2, 0xc8, 0x9b, 0x79, - 0x23, 0x73, 0xb5, 0x37, 0x27, 0x62, 0xbd, 0x9d, 0x39, 0xaa, 0xcf, 0xa3, 0x64, 0xc1, 0x11, 0x7e, - 0x8a, 0x4a, 0x7e, 0xe0, 0x80, 0x50, 0x0b, 0xcd, 0x42, 0xbb, 0xb6, 0xfb, 0x49, 0x1e, 0x8f, 0x06, - 0x09, 0x1c, 0x78, 0xc2, 0xe4, 0xe0, 0xfe, 0x08, 0x52, 0x50, 0x58, 0x5b, 0x99, 0xaf, 0x52, 0x6c, - 0x13, 0x24, 0x15, 0xc5, 0xfb, 0x68, 0xab, 0x47, 0x99, 0x13, 0xf8, 0xf0, 0x80, 0x3b, 0xcc, 0x3e, - 0x56, 0x8b, 0x49, 0x06, 0x3e, 0x88, 0x42, 0x7d, 0xeb, 0xde, 0xbc, 0xe1, 0x2c, 0xd4, 0x1b, 0x0b, - 0xc0, 0xc3, 0xe3, 0x11, 0x90, 0x45, 0x32, 0xfe, 0x12, 0xd5, 0x5c, 0x2a, 0xed, 0x41, 0xa6, 0x55, - 0x4d, 0xb4, 0x5a, 0x51, 0xa8, 0xd7, 0x0e, 0x66, 0xf0, 0x59, 0xa8, 0x5f, 0x9d, 0x3b, 0x26, 0x3a, - 0xf3, 0x34, 0xfc, 0x13, 0x6a, 0xc4, 0x29, 0x17, 0x23, 0x6a, 0xc3, 0x11, 0x38, 0x60, 0x4b, 0xee, - 0xab, 0xa5, 0x24, 0xdf, 0xb7, 0xe7, 0xa2, 0x9f, 0x3e, 0xba, 0x31, 0x1a, 0xf6, 0x63, 0x40, 0x18, - 0x71, 0x6d, 0xc5, 0xe1, 0xef, 0xd3, 0x0e, 0x38, 0x13, 0xaa, 0xf5, 0x4e, 0x14, 0xea, 0x8d, 0xc3, - 0x65, 0x45, 0xb2, 0xea, 0x04, 0x73, 0xb4, 0xcd, 0x3b, 0x3f, 0x82, 0x2d, 0xa7, 0x6e, 0x6b, 0x17, - 0x77, 0x8b, 0xa3, 0x50, 0xdf, 0xbe, 0xbf, 0x20, 0x47, 0x96, 0xe4, 0xe3, 0x84, 0x09, 0xd6, 0x85, - 0xaf, 0x7a, 0x3d, 0xb0, 0xa5, 0x50, 0xaf, 0xcc, 0x12, 0x76, 0x34, 0x83, 0xe3, 0x84, 0xcd, 0x8e, - 0x7b, 0x0e, 0x15, 0x82, 0xcc, 0xd3, 0xf0, 0x1d, 0xb4, 0x1d, 0x17, 0x3c, 0x0f, 0xe4, 0x11, 0xd8, - 0xdc, 0xeb, 0x0a, 0xb5, 0xdc, 0x54, 0xda, 0xa5, 0xf4, 0x06, 0x0f, 0x17, 0x2c, 0x64, 0xe9, 0x4b, - 0xfc, 0x08, 0x5d, 0x9f, 0x56, 0x11, 0x81, 0x31, 0x83, 0xe7, 0x8f, 0xc1, 0x8f, 0x0f, 0x42, 0xad, - 0x34, 0x0b, 0xed, 0xaa, 0xf5, 0x7e, 0x14, 0xea, 0xd7, 0xef, 0xae, 0xff, 0x84, 0x9c, 0xc7, 0xc5, - 0xcf, 0x10, 0xf6, 0x81, 0x79, 0x63, 0x6e, 0x27, 0xe5, 0x97, 0x15, 0x04, 0x4a, 0xe2, 0xbb, 0x19, - 0x85, 0x3a, 0x26, 0x2b, 0xd6, 0xb3, 0x50, 0x7f, 0x77, 0x15, 0x4d, 0xca, 0x63, 0x8d, 0x56, 0xeb, - 0x0f, 0x05, 0xdd, 0x58, 0x6a, 0xe3, 0xb4, 0x63, 0x82, 0xb4, 0xe2, 0xf1, 0x33, 0x54, 0x89, 0x1f, - 0xa6, 0x4b, 0x25, 0x4d, 0xfa, 0xba, 0xb6, 0x7b, 0x33, 0xdf, 0x33, 0xa6, 0x6f, 0x76, 0x00, 0x92, - 0x5a, 0x38, 0x6b, 0x1a, 0x34, 0xc3, 0xc8, 0x54, 0x15, 0x7f, 0x8f, 0x2a, 0x99, 0x67, 0xa1, 0x6e, - 0x26, 0xdd, 0xf9, 0x59, 0xfe, 0x79, 0xb0, 0x74, 0x77, 0xab, 0x18, 0xbb, 0x22, 0x53, 0xc1, 0xd6, - 0x3f, 0x0a, 0x6a, 0xbe, 0x2a, 0xbe, 0x7d, 0x26, 0x24, 0x7e, 0xba, 0x12, 0xa3, 0x91, 0xb3, 0x54, - 0x99, 0x48, 0x23, 0xbc, 0x96, 0x45, 0x58, 0x99, 0x20, 0x73, 0xf1, 0x0d, 0x51, 0x89, 0x49, 0x70, - 0x27, 0xc1, 0xdd, 0xbb, 0x70, 0x70, 0x0b, 0x17, 0x9f, 0x4d, 0xa2, 0x6f, 0x62, 0x71, 0x92, 0xfa, - 0x68, 0xfd, 0xaa, 0xa0, 0x6b, 0x47, 0xe0, 0x8f, 0x99, 0x0d, 0x04, 0x7a, 0xe0, 0x83, 0x67, 0x03, - 0x36, 0x51, 0x75, 0xda, 0xa5, 0xd9, 0x70, 0x6e, 0x64, 0xec, 0xea, 0xb4, 0xa3, 0xc9, 0xec, 0x9b, - 0xe9, 0x20, 0xdf, 0x3c, 0x77, 0x90, 0xdf, 0x40, 0xc5, 0x11, 0x95, 0x03, 0xb5, 0x90, 0x7c, 0x51, - 0x89, 0xad, 0x0f, 0xa8, 0x1c, 0x90, 0x04, 0x4d, 0xac, 0xdc, 0x97, 0xc9, 0x18, 0x2c, 0x65, 0x56, - 0xee, 0x4b, 0x92, 0xa0, 0xad, 0xbf, 0xaf, 0xa0, 0xc6, 0x63, 0xea, 0xb0, 0xee, 0xe5, 0xf2, 0xb8, - 0x5c, 0x1e, 0xaf, 0x5f, 0x1e, 0xe8, 0x72, 0x79, 0x5c, 0x64, 0x79, 0xb4, 0x4e, 0x15, 0xa4, 0xad, - 0xb4, 0xd9, 0x9b, 0x1e, 0xee, 0x3f, 0xac, 0x0c, 0xf7, 0xcf, 0xf3, 0xf7, 0xeb, 0xca, 0xed, 0x57, - 0xc6, 0xfb, 0xbf, 0x0a, 0x6a, 0xbd, 0x3a, 0xc6, 0x37, 0x30, 0xe0, 0xdd, 0xc5, 0x01, 0xff, 0xf5, - 0xff, 0x08, 0x30, 0xcf, 0x88, 0xff, 0x4d, 0x41, 0x6f, 0xad, 0x99, 0x64, 0xf8, 0x3d, 0x54, 0x08, - 0x7c, 0x27, 0x9b, 0xc8, 0xe5, 0x28, 0xd4, 0x0b, 0x8f, 0xc8, 0x3e, 0x89, 0x31, 0x4c, 0x51, 0x59, - 0xa4, 0x4b, 0x21, 0x0b, 0xff, 0x4e, 0xfe, 0x3b, 0x2e, 0x6f, 0x13, 0xab, 0x16, 0x85, 0x7a, 0x79, - 0x82, 0x4e, 0x74, 0x71, 0x1b, 0x55, 0x6c, 0x6a, 0x05, 0x5e, 0xd7, 0x49, 0xd7, 0x46, 0xdd, 0xaa, - 0xc7, 0xe9, 0xda, 0xbb, 0x9b, 0x62, 0x64, 0x6a, 0xb5, 0x0e, 0x4f, 0x4e, 0xb5, 0x8d, 0x17, 0xa7, - 0xda, 0xc6, 0xcb, 0x53, 0x6d, 0xe3, 0xe7, 0x48, 0x53, 0x4e, 0x22, 0x4d, 0x79, 0x11, 0x69, 0xca, - 0xcb, 0x48, 0x53, 0xfe, 0x8c, 0x34, 0xe5, 0x97, 0xbf, 0xb4, 0x8d, 0xef, 0xda, 0x79, 0xff, 0xc6, - 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, 0x7e, 0xc9, 0x34, 0x4c, 0x0a, 0x0e, 0x00, 0x00, + // 1041 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x57, 0x4f, 0x73, 0xdb, 0xc4, + 0x1b, 0x8e, 0xe2, 0xf8, 0x17, 0x67, 0xed, 0x24, 0xcd, 0xfe, 0x80, 0x88, 0xd0, 0xb1, 0x3c, 0x3e, + 0x30, 0xbe, 0x20, 0xb5, 0x29, 0x03, 0xa5, 0x0c, 0x87, 0x2a, 0xb4, 0x03, 0x33, 0x49, 0x5a, 0x36, + 0xfd, 0x33, 0x03, 0x65, 0xa6, 0x6b, 0xf9, 0xb5, 0xbd, 0x58, 0xd2, 0x7a, 0xb4, 0xab, 0xb4, 0x19, + 0x2e, 0x7c, 0x04, 0xbe, 0x02, 0x1f, 0x84, 0x03, 0xb7, 0x1c, 0x7b, 0xec, 0x05, 0x0d, 0x11, 0x67, + 0x0e, 0x5c, 0x73, 0x62, 0xb4, 0x52, 0x6c, 0xcb, 0x76, 0x5a, 0x11, 0x66, 0x72, 0xca, 0xcd, 0xfb, + 0xbc, 0xfb, 0xbe, 0xcf, 0x3e, 0xab, 0x77, 0xdf, 0x67, 0x8c, 0xbe, 0x19, 0xdc, 0x16, 0x26, 0xe3, + 0xd6, 0x20, 0x6c, 0x43, 0xe0, 0x83, 0x04, 0x61, 0x1d, 0x82, 0xdf, 0xe1, 0x81, 0x95, 0x05, 0xe8, + 0x90, 0x59, 0xb4, 0xe3, 0x31, 0x21, 0x18, 0xf7, 0x03, 0xe8, 0x31, 0x21, 0x03, 0x2a, 0x19, 0xf7, + 0xad, 0xc3, 0x9b, 0x6d, 0x90, 0xf4, 0xa6, 0xd5, 0x03, 0x1f, 0x02, 0x2a, 0xa1, 0x63, 0x0e, 0x03, + 0x2e, 0x39, 0x6e, 0xa5, 0x99, 0x26, 0x1d, 0x32, 0x73, 0x6e, 0xa6, 0x99, 0x65, 0x6e, 0x7d, 0xd4, + 0x63, 0xb2, 0x1f, 0xb6, 0x4d, 0x87, 0x7b, 0x56, 0x8f, 0xf7, 0xb8, 0xa5, 0x0a, 0xb4, 0xc3, 0xae, + 0x5a, 0xa9, 0x85, 0xfa, 0x95, 0x16, 0xde, 0xba, 0x55, 0xe0, 0x48, 0xd3, 0xa7, 0xd9, 0xfa, 0x78, + 0x9c, 0xe4, 0x51, 0xa7, 0xcf, 0x7c, 0x08, 0x8e, 0xac, 0xe1, 0xa0, 0x97, 0x00, 0xc2, 0xf2, 0x40, + 0xd2, 0x79, 0x59, 0xd6, 0x79, 0x59, 0x41, 0xe8, 0x4b, 0xe6, 0xc1, 0x4c, 0xc2, 0x27, 0x6f, 0x4b, + 0x10, 0x4e, 0x1f, 0x3c, 0x3a, 0x9d, 0xd7, 0xec, 0xa2, 0xb5, 0x3d, 0x2a, 0x9d, 0xfe, 0x0e, 0xf7, + 0x3b, 0x2c, 0xd1, 0x80, 0x1b, 0x68, 0xc9, 0xa7, 0x1e, 0xe8, 0x5a, 0x43, 0x6b, 0xad, 0xd8, 0xb5, + 0xe3, 0xc8, 0x58, 0x88, 0x23, 0x63, 0x69, 0x9f, 0x7a, 0x40, 0x54, 0x04, 0x6f, 0x23, 0x04, 0x2f, + 0x87, 0x01, 0x28, 0xfd, 0xfa, 0xa2, 0xda, 0x87, 0xb3, 0x7d, 0xe8, 0xde, 0x28, 0x42, 0x26, 0x76, + 0x35, 0x7f, 0xab, 0xa0, 0xf5, 0xbd, 0x50, 0x52, 0xc9, 0xfc, 0xde, 0x53, 0x68, 0xf7, 0x39, 0x1f, + 0x14, 0x60, 0x7a, 0x81, 0x6a, 0x8e, 0xcb, 0xc0, 0x97, 0x3b, 0xdc, 0xef, 0xb2, 0x9e, 0xe2, 0xaa, + 0x6e, 0x7f, 0x61, 0x16, 0xfd, 0xc2, 0x66, 0x46, 0xb5, 0x33, 0x51, 0xc4, 0x7e, 0x27, 0x23, 0xaa, + 0x4d, 0xa2, 0x24, 0x47, 0x84, 0x9f, 0xa1, 0x72, 0x10, 0xba, 0x20, 0xf4, 0x52, 0xa3, 0xd4, 0xaa, + 0x6e, 0x7f, 0x5a, 0x84, 0xd1, 0x24, 0xa1, 0x0b, 0x4f, 0x99, 0xec, 0x3f, 0x18, 0x42, 0x0a, 0x0a, + 0x7b, 0x35, 0xe3, 0x2a, 0x27, 0x31, 0x41, 0xd2, 0xa2, 0x78, 0x17, 0xad, 0x76, 0x29, 0x73, 0xc3, + 0x00, 0x1e, 0x72, 0x97, 0x39, 0x47, 0xfa, 0x92, 0xba, 0x81, 0x0f, 0xe3, 0xc8, 0x58, 0xbd, 0x3f, + 0x19, 0x38, 0x8d, 0x8c, 0x8d, 0x1c, 0xf0, 0xe8, 0x68, 0x08, 0x24, 0x9f, 0x8c, 0xbf, 0x44, 0x55, + 0x2f, 0xf9, 0x84, 0x59, 0xad, 0x15, 0x55, 0xab, 0x19, 0x47, 0x46, 0x75, 0x6f, 0x0c, 0x9f, 0x46, + 0xc6, 0xfa, 0xc4, 0x52, 0xd5, 0x99, 0x4c, 0xc3, 0x2f, 0xd1, 0x46, 0x72, 0xe5, 0x62, 0x48, 0x1d, + 0x38, 0x00, 0x17, 0x1c, 0xc9, 0x03, 0xbd, 0xac, 0xee, 0xfb, 0xd6, 0x84, 0xfa, 0x51, 0x73, 0x99, + 0xc3, 0x41, 0x2f, 0x01, 0x84, 0x99, 0xf4, 0x70, 0x22, 0x7f, 0x97, 0xb6, 0xc1, 0x3d, 0x4b, 0xb5, + 0xdf, 0x8d, 0x23, 0x63, 0x63, 0x7f, 0xba, 0x22, 0x99, 0x25, 0xc1, 0x1c, 0xad, 0xf1, 0xf6, 0x0f, + 0xe0, 0xc8, 0x11, 0x6d, 0xf5, 0xe2, 0xb4, 0x38, 0x8e, 0x8c, 0xb5, 0x07, 0xb9, 0x72, 0x64, 0xaa, + 0x7c, 0x72, 0x61, 0x82, 0x75, 0xe0, 0x5e, 0xb7, 0x0b, 0x8e, 0x14, 0xfa, 0xff, 0xc6, 0x17, 0x76, + 0x30, 0x86, 0x93, 0x0b, 0x1b, 0x2f, 0x77, 0x5c, 0x2a, 0x04, 0x99, 0x4c, 0xc3, 0x77, 0xd0, 0x5a, + 0xf2, 0xb0, 0x78, 0x28, 0x0f, 0xc0, 0xe1, 0x7e, 0x47, 0xe8, 0xcb, 0x0d, 0xad, 0x55, 0x4e, 0x4f, + 0xf0, 0x28, 0x17, 0x21, 0x53, 0x3b, 0xf1, 0x63, 0xb4, 0x39, 0xea, 0x22, 0x02, 0x87, 0x0c, 0x5e, + 0x3c, 0x81, 0x20, 0x59, 0x08, 0xbd, 0xd2, 0x28, 0xb5, 0x56, 0xec, 0x0f, 0xe2, 0xc8, 0xd8, 0xbc, + 0x3b, 0x7f, 0x0b, 0x39, 0x2f, 0x17, 0x3f, 0x47, 0x38, 0x00, 0xe6, 0x1f, 0x72, 0x47, 0xb5, 0x5f, + 0xd6, 0x10, 0x48, 0xe9, 0xbb, 0x11, 0x47, 0x06, 0x26, 0x33, 0xd1, 0xd3, 0xc8, 0x78, 0x6f, 0x16, + 0x55, 0xed, 0x31, 0xa7, 0x16, 0xfe, 0x11, 0xad, 0x7b, 0xb9, 0x71, 0x21, 0xf4, 0x9a, 0x7a, 0x21, + 0xb7, 0x8b, 0xbf, 0xc9, 0xfc, 0xbc, 0xb1, 0x37, 0xb3, 0x27, 0xb2, 0x9e, 0xc7, 0x05, 0x99, 0x66, + 0x6a, 0xfe, 0xae, 0xa1, 0xeb, 0x53, 0x33, 0x24, 0x7d, 0xae, 0x61, 0xca, 0x80, 0x9f, 0xa3, 0x4a, + 0xd2, 0x15, 0x1d, 0x2a, 0xa9, 0x1a, 0x2a, 0xd5, 0xed, 0x1b, 0xc5, 0x7a, 0x28, 0x6d, 0x98, 0x3d, + 0x90, 0x74, 0x3c, 0xc8, 0xc6, 0x18, 0x19, 0x55, 0xc5, 0xdf, 0xa1, 0x4a, 0xc6, 0x2c, 0xf4, 0x45, + 0x25, 0xfc, 0xb3, 0x7f, 0x21, 0x3c, 0x7f, 0x76, 0x7b, 0x29, 0xa1, 0x22, 0xa3, 0x82, 0xcd, 0xbf, + 0x34, 0xd4, 0x78, 0x93, 0xbe, 0x5d, 0x26, 0x24, 0x7e, 0x36, 0xa3, 0xd1, 0x2c, 0xf8, 0x4e, 0x98, + 0x48, 0x15, 0x5e, 0xcb, 0x14, 0x56, 0xce, 0x90, 0x09, 0x7d, 0x03, 0x54, 0x66, 0x12, 0xbc, 0x33, + 0x71, 0xf7, 0x2f, 0x2c, 0x2e, 0x77, 0xf0, 0xf1, 0x18, 0xfc, 0x3a, 0x29, 0x4e, 0x52, 0x8e, 0xe6, + 0x2f, 0x1a, 0xba, 0x76, 0x00, 0xc1, 0x21, 0x73, 0x80, 0x40, 0x17, 0x02, 0xf0, 0x1d, 0xc0, 0x16, + 0x5a, 0x19, 0x8d, 0x88, 0xcc, 0x19, 0x36, 0xb2, 0xec, 0x95, 0xd1, 0x38, 0x21, 0xe3, 0x3d, 0x23, + 0x17, 0x59, 0x3c, 0xd7, 0x45, 0xae, 0xa3, 0xa5, 0x21, 0x95, 0x7d, 0xbd, 0xa4, 0x76, 0x54, 0x92, + 0xe8, 0x43, 0x2a, 0xfb, 0x44, 0xa1, 0x2a, 0xca, 0x03, 0xa9, 0x66, 0x70, 0x39, 0x8b, 0xf2, 0x40, + 0x12, 0x85, 0x36, 0x4f, 0x96, 0xd1, 0xc6, 0x13, 0xea, 0xb2, 0xce, 0x95, 0x73, 0x5d, 0x39, 0xd7, + 0xdb, 0x9d, 0x0b, 0x5d, 0x39, 0xd7, 0x85, 0x9c, 0x6b, 0x8e, 0xaf, 0x54, 0x2f, 0xcd, 0x57, 0x4e, + 0x34, 0x54, 0x9f, 0x79, 0xe3, 0x97, 0xed, 0x2c, 0xdf, 0xcf, 0x38, 0xcb, 0xe7, 0xc5, 0xa5, 0xcf, + 0x9c, 0x7e, 0xc6, 0x5b, 0xfe, 0xd6, 0x50, 0xf3, 0xcd, 0x1a, 0x2f, 0xc1, 0x5d, 0xbc, 0xbc, 0xbb, + 0x7c, 0xf5, 0x1f, 0x04, 0x16, 0xf1, 0x97, 0x5f, 0x35, 0xf4, 0xff, 0x39, 0x63, 0x14, 0xbf, 0x8f, + 0x4a, 0x61, 0xe0, 0x66, 0x76, 0xb0, 0x1c, 0x47, 0x46, 0xe9, 0x31, 0xd9, 0x25, 0x09, 0x86, 0x29, + 0x5a, 0x16, 0xa9, 0x23, 0x65, 0xf2, 0xef, 0x14, 0x3f, 0xe3, 0xb4, 0x95, 0xd9, 0xd5, 0x38, 0x32, + 0x96, 0xcf, 0xd0, 0xb3, 0xba, 0xb8, 0x85, 0x2a, 0x0e, 0xb5, 0x43, 0xbf, 0xe3, 0xa6, 0x9e, 0x55, + 0xb3, 0x6b, 0xc9, 0x75, 0xed, 0xdc, 0x4d, 0x31, 0x32, 0x8a, 0xda, 0xfb, 0xc7, 0x27, 0xf5, 0x85, + 0x57, 0x27, 0xf5, 0x85, 0xd7, 0x27, 0xf5, 0x85, 0x9f, 0xe2, 0xba, 0x76, 0x1c, 0xd7, 0xb5, 0x57, + 0x71, 0x5d, 0x7b, 0x1d, 0xd7, 0xb5, 0x3f, 0xe2, 0xba, 0xf6, 0xf3, 0x9f, 0xf5, 0x85, 0x6f, 0x5b, + 0x45, 0xff, 0x28, 0xff, 0x13, 0x00, 0x00, 0xff, 0xff, 0x1f, 0xf5, 0x97, 0x1c, 0x6c, 0x0f, 0x00, + 0x00, +} + +func (m *MatchCondition) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MatchCondition) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MatchCondition) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i -= len(m.Expression) + copy(dAtA[i:], m.Expression) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Expression))) + i-- + dAtA[i] = 0x12 + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil } func (m *MutatingWebhook) Marshal() (dAtA []byte, err error) { @@ -369,6 +436,20 @@ func (m *MutatingWebhook) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.MatchConditions) > 0 { + for iNdEx := len(m.MatchConditions) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MatchConditions[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x62 + } + } if m.ObjectSelector != nil { { size, err := m.ObjectSelector.MarshalToSizedBuffer(dAtA[:i]) @@ -626,6 +707,20 @@ func (m *ValidatingWebhook) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.MatchConditions) > 0 { + for iNdEx := len(m.MatchConditions) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MatchConditions[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x5a + } + } if m.ObjectSelector != nil { { size, err := m.ObjectSelector.MarshalToSizedBuffer(dAtA[:i]) @@ -871,6 +966,19 @@ func encodeVarintGenerated(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) return base } +func (m *MatchCondition) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + n += 1 + l + sovGenerated(uint64(l)) + l = len(m.Expression) + n += 1 + l + sovGenerated(uint64(l)) + return n +} + func (m *MutatingWebhook) Size() (n int) { if m == nil { return 0 @@ -920,6 +1028,12 @@ func (m *MutatingWebhook) Size() (n int) { l = m.ObjectSelector.Size() n += 1 + l + sovGenerated(uint64(l)) } + if len(m.MatchConditions) > 0 { + for _, e := range m.MatchConditions { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } return n } @@ -1022,6 +1136,12 @@ func (m *ValidatingWebhook) Size() (n int) { l = m.ObjectSelector.Size() n += 1 + l + sovGenerated(uint64(l)) } + if len(m.MatchConditions) > 0 { + for _, e := range m.MatchConditions { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } return n } @@ -1086,6 +1206,17 @@ func sovGenerated(x uint64) (n int) { func sozGenerated(x uint64) (n int) { return sovGenerated(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } +func (this *MatchCondition) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&MatchCondition{`, + `Name:` + fmt.Sprintf("%v", this.Name) + `,`, + `Expression:` + fmt.Sprintf("%v", this.Expression) + `,`, + `}`, + }, "") + return s +} func (this *MutatingWebhook) String() string { if this == nil { return "nil" @@ -1095,6 +1226,11 @@ func (this *MutatingWebhook) String() string { repeatedStringForRules += fmt.Sprintf("%v", f) + "," } repeatedStringForRules += "}" + repeatedStringForMatchConditions := "[]MatchCondition{" + for _, f := range this.MatchConditions { + repeatedStringForMatchConditions += strings.Replace(strings.Replace(f.String(), "MatchCondition", "MatchCondition", 1), `&`, ``, 1) + "," + } + repeatedStringForMatchConditions += "}" s := strings.Join([]string{`&MutatingWebhook{`, `Name:` + fmt.Sprintf("%v", this.Name) + `,`, `ClientConfig:` + strings.Replace(strings.Replace(this.ClientConfig.String(), "WebhookClientConfig", "WebhookClientConfig", 1), `&`, ``, 1) + `,`, @@ -1107,6 +1243,7 @@ func (this *MutatingWebhook) String() string { `MatchPolicy:` + valueToStringGenerated(this.MatchPolicy) + `,`, `ReinvocationPolicy:` + valueToStringGenerated(this.ReinvocationPolicy) + `,`, `ObjectSelector:` + strings.Replace(fmt.Sprintf("%v", this.ObjectSelector), "LabelSelector", "v11.LabelSelector", 1) + `,`, + `MatchConditions:` + repeatedStringForMatchConditions + `,`, `}`, }, "") return s @@ -1165,6 +1302,11 @@ func (this *ValidatingWebhook) String() string { repeatedStringForRules += fmt.Sprintf("%v", f) + "," } repeatedStringForRules += "}" + repeatedStringForMatchConditions := "[]MatchCondition{" + for _, f := range this.MatchConditions { + repeatedStringForMatchConditions += strings.Replace(strings.Replace(f.String(), "MatchCondition", "MatchCondition", 1), `&`, ``, 1) + "," + } + repeatedStringForMatchConditions += "}" s := strings.Join([]string{`&ValidatingWebhook{`, `Name:` + fmt.Sprintf("%v", this.Name) + `,`, `ClientConfig:` + strings.Replace(strings.Replace(this.ClientConfig.String(), "WebhookClientConfig", "WebhookClientConfig", 1), `&`, ``, 1) + `,`, @@ -1176,6 +1318,7 @@ func (this *ValidatingWebhook) String() string { `AdmissionReviewVersions:` + fmt.Sprintf("%v", this.AdmissionReviewVersions) + `,`, `MatchPolicy:` + valueToStringGenerated(this.MatchPolicy) + `,`, `ObjectSelector:` + strings.Replace(fmt.Sprintf("%v", this.ObjectSelector), "LabelSelector", "v11.LabelSelector", 1) + `,`, + `MatchConditions:` + repeatedStringForMatchConditions + `,`, `}`, }, "") return s @@ -1232,6 +1375,120 @@ func valueToStringGenerated(v interface{}) string { pv := reflect.Indirect(rv).Interface() return fmt.Sprintf("*%v", pv) } +func (m *MatchCondition) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MatchCondition: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MatchCondition: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Expression", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Expression = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *MutatingWebhook) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1616,6 +1873,40 @@ func (m *MutatingWebhook) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 12: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MatchConditions", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MatchConditions = append(m.MatchConditions, MatchCondition{}) + if err := m.MatchConditions[len(m.MatchConditions)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -2389,6 +2680,40 @@ func (m *ValidatingWebhook) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 11: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MatchConditions", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MatchConditions = append(m.MatchConditions, MatchCondition{}) + if err := m.MatchConditions[len(m.MatchConditions)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto b/staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto index c7016afbf49..cfd75928541 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/admissionregistration/v1beta1/generated.proto @@ -29,6 +29,35 @@ import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto"; // Package-wide variables from generator "generated". option go_package = "k8s.io/api/admissionregistration/v1beta1"; +// MatchCondition represents a condition which must be fulfilled for a request to be sent to a webhook. +message MatchCondition { + // Name is an identifier for this match condition, used for strategic merging of MatchConditions, + // as well as providing an identifier for logging purposes. A good name should be descriptive of + // the associated expression. + // Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and + // must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or + // '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an + // optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName') + // + // Required. + optional string name = 1; + + // Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. + // CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables: + // + // 'object' - The object from the incoming request. The value is null for DELETE requests. + // 'oldObject' - The existing object. The value is null for CREATE requests. + // 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). + // 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Required. + optional string expression = 2; +} + // MutatingWebhook describes an admission webhook and the resources and operations it applies to. message MutatingWebhook { // The name of the admission webhook. @@ -177,6 +206,28 @@ message MutatingWebhook { // Defaults to "Never". // +optional optional string reinvocationPolicy = 10; + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +featureGate=AdmissionWebhookMatchConditions + // +optional + repeated MatchCondition matchConditions = 12; } // MutatingWebhookConfiguration describes the configuration of and admission webhook that accept or reject and may change the object. @@ -356,6 +407,28 @@ message ValidatingWebhook { // Default to `['v1beta1']`. // +optional repeated string admissionReviewVersions = 8; + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +featureGate=AdmissionWebhookMatchConditions + // +optional + repeated MatchCondition matchConditions = 11; } // ValidatingWebhookConfiguration describes the configuration of and admission webhook that accept or reject and object without changing it. diff --git a/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go b/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go index 5fdf8e3fa78..82ee7df9bad 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go +++ b/staging/src/k8s.io/api/admissionregistration/v1beta1/types.go @@ -283,6 +283,28 @@ type ValidatingWebhook struct { // Default to `['v1beta1']`. // +optional AdmissionReviewVersions []string `json:"admissionReviewVersions,omitempty" protobuf:"bytes,8,rep,name=admissionReviewVersions"` + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +featureGate=AdmissionWebhookMatchConditions + // +optional + MatchConditions []MatchCondition `json:"matchConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,11,rep,name=matchConditions"` } // MutatingWebhook describes an admission webhook and the resources and operations it applies to. @@ -433,6 +455,28 @@ type MutatingWebhook struct { // Defaults to "Never". // +optional ReinvocationPolicy *ReinvocationPolicyType `json:"reinvocationPolicy,omitempty" protobuf:"bytes,10,opt,name=reinvocationPolicy,casttype=ReinvocationPolicyType"` + + // MatchConditions is a list of conditions that must be met for a request to be sent to this + // webhook. Match conditions filter requests that have already been matched by the rules, + // namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. + // There are a maximum of 64 match conditions allowed. + // + // The exact matching logic is (in order): + // 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped. + // 2. If ALL matchConditions evaluate to TRUE, the webhook is called. + // 3. If any matchCondition evaluates to an error (but none are FALSE): + // - If failurePolicy=Fail, reject the request + // - If failurePolicy=Ignore, the error is ignored and the webhook is skipped + // + // This is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate. + // + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + // +featureGate=AdmissionWebhookMatchConditions + // +optional + MatchConditions []MatchCondition `json:"matchConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,12,rep,name=matchConditions"` } // ReinvocationPolicyType specifies what type of policy the admission hook uses. @@ -531,3 +575,32 @@ type ServiceReference struct { // +optional Port *int32 `json:"port,omitempty" protobuf:"varint,4,opt,name=port"` } + +// MatchCondition represents a condition which must be fulfilled for a request to be sent to a webhook. +type MatchCondition struct { + // Name is an identifier for this match condition, used for strategic merging of MatchConditions, + // as well as providing an identifier for logging purposes. A good name should be descriptive of + // the associated expression. + // Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and + // must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or + // '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an + // optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName') + // + // Required. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + + // Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. + // CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables: + // + // 'object' - The object from the incoming request. The value is null for DELETE requests. + // 'oldObject' - The existing object. The value is null for CREATE requests. + // 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). + // 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request. + // See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz + // 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the + // request resource. + // Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/ + // + // Required. + Expression string `json:"expression" protobuf:"bytes,2,opt,name=expression"` +} diff --git a/staging/src/k8s.io/api/admissionregistration/v1beta1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/admissionregistration/v1beta1/types_swagger_doc_generated.go index 1a6c77e4a89..2c0a9f01179 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1beta1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/admissionregistration/v1beta1/types_swagger_doc_generated.go @@ -27,6 +27,16 @@ package v1beta1 // Those methods can be generated by using hack/update-codegen.sh // AUTO-GENERATED FUNCTIONS START HERE. DO NOT EDIT. +var map_MatchCondition = map[string]string{ + "": "MatchCondition represents a condition which must be fulfilled for a request to be sent to a webhook.", + "name": "Name is an identifier for this match condition, used for strategic merging of MatchConditions, as well as providing an identifier for logging purposes. A good name should be descriptive of the associated expression. Name must be a qualified name consisting of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]') with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')\n\nRequired.", + "expression": "Expression represents the expression which will be evaluated by CEL. Must evaluate to bool. CEL expressions have access to the contents of the AdmissionRequest and Authorizer, organized into CEL variables:\n\n'object' - The object from the incoming request. The value is null for DELETE requests. 'oldObject' - The existing object. The value is null for CREATE requests. 'request' - Attributes of the admission request(/pkg/apis/admission/types.go#AdmissionRequest). 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.\n See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz\n'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the\n request resource.\nDocumentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/\n\nRequired.", +} + +func (MatchCondition) SwaggerDoc() map[string]string { + return map_MatchCondition +} + var map_MutatingWebhook = map[string]string{ "": "MutatingWebhook describes an admission webhook and the resources and operations it applies to.", "name": "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.", @@ -40,6 +50,7 @@ var map_MutatingWebhook = map[string]string{ "timeoutSeconds": "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. Default to 30 seconds.", "admissionReviewVersions": "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 a persisted webhook configuration specifies allowed versions and does not include any versions known to the API Server, calls to the webhook will fail and be subject to the failure policy. Default to `['v1beta1']`.", "reinvocationPolicy": "reinvocationPolicy indicates whether this webhook should be called multiple times as part of a single admission evaluation. Allowed values are \"Never\" and \"IfNeeded\".\n\nNever: the webhook will not be called more than once in a single admission evaluation.\n\nIfNeeded: the webhook will be called at least one additional time as part of the admission evaluation if the object being admitted is modified by other admission plugins after the initial webhook call. Webhooks that specify this option *must* be idempotent, able to process objects they previously admitted. Note: * the number of additional invocations is not guaranteed to be exactly one. * if additional invocations result in further modifications to the object, webhooks are not guaranteed to be invoked again. * webhooks that use this option may be reordered to minimize the number of additional invocations. * to validate an object after all mutations are guaranteed complete, use a validating admission webhook instead.\n\nDefaults to \"Never\".", + "matchConditions": "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", } func (MutatingWebhook) SwaggerDoc() map[string]string { @@ -90,6 +101,7 @@ var map_ValidatingWebhook = map[string]string{ "sideEffects": "SideEffects states whether this webhook 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 chain 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.", "timeoutSeconds": "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. Default to 30 seconds.", "admissionReviewVersions": "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 a persisted webhook configuration specifies allowed versions and does not include any versions known to the API Server, calls to the webhook will fail and be subject to the failure policy. Default to `['v1beta1']`.", + "matchConditions": "MatchConditions is a list of conditions that must be met for a request to be sent to this webhook. Match conditions filter requests that have already been matched by the rules, namespaceSelector, and objectSelector. An empty list of matchConditions matches all requests. There are a maximum of 64 match conditions allowed.\n\nThe exact matching logic is (in order):\n 1. If ANY matchCondition evaluates to FALSE, the webhook is skipped.\n 2. If ALL matchConditions evaluate to TRUE, the webhook is called.\n 3. If any matchCondition evaluates to an error (but none are FALSE):\n - If failurePolicy=Fail, reject the request\n - If failurePolicy=Ignore, the error is ignored and the webhook is skipped\n\nThis is an alpha feature and managed by the AdmissionWebhookMatchConditions feature gate.", } func (ValidatingWebhook) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/api/admissionregistration/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/admissionregistration/v1beta1/zz_generated.deepcopy.go index ced4af19c6b..9c5299bdfa2 100644 --- a/staging/src/k8s.io/api/admissionregistration/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/admissionregistration/v1beta1/zz_generated.deepcopy.go @@ -27,6 +27,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MatchCondition) DeepCopyInto(out *MatchCondition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchCondition. +func (in *MatchCondition) DeepCopy() *MatchCondition { + if in == nil { + return nil + } + out := new(MatchCondition) + in.DeepCopyInto(out) + return out +} + // 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 @@ -78,6 +94,11 @@ func (in *MutatingWebhook) DeepCopyInto(out *MutatingWebhook) { *out = new(ReinvocationPolicyType) **out = **in } + if in.MatchConditions != nil { + in, out := &in.MatchConditions, &out.MatchConditions + *out = make([]MatchCondition, len(*in)) + copy(*out, *in) + } return } @@ -229,6 +250,11 @@ func (in *ValidatingWebhook) DeepCopyInto(out *ValidatingWebhook) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.MatchConditions != nil { + in, out := &in.MatchConditions, &out.MatchConditions + *out = make([]MatchCondition, len(*in)) + copy(*out, *in) + } return } diff --git a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.json b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.json index 86c3ef588ce..520763fcd47 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.json +++ b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.json @@ -108,7 +108,13 @@ "admissionReviewVersions": [ "admissionReviewVersionsValue" ], - "reinvocationPolicy": "reinvocationPolicyValue" + "reinvocationPolicy": "reinvocationPolicyValue", + "matchConditions": [ + { + "name": "nameValue", + "expression": "expressionValue" + } + ] } ] } \ No newline at end of file diff --git a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.pb b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.pb index ab7c8339db9e9e131b1b4dc980d46475b44781b9..d9cd866cf19ca0b4cb5f58429a5114025742e62a 100644 GIT binary patch delta 54 zcmcb{_JwVN8{>+N?x~E7hc>TeT*|1FB*Vp-mzbLxmY7qTD#V{!QBagxT%4Jo2NsZG HP+|Z88nP03 delta 24 gcmeyuc8zU<8)Mf-_f$s4C7ahWE@fnqVo+iL0CHLff&c&j diff --git a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.yaml b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.yaml index 2d615efafbe..34b84f21791 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.yaml +++ b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.MutatingWebhookConfiguration.yaml @@ -44,6 +44,9 @@ webhooks: port: 4 url: urlValue failurePolicy: failurePolicyValue + matchConditions: + - expression: expressionValue + name: nameValue matchPolicy: matchPolicyValue name: nameValue namespaceSelector: diff --git a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.ValidatingWebhookConfiguration.json b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.ValidatingWebhookConfiguration.json index 5cde45f9bd6..a61187e4493 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.ValidatingWebhookConfiguration.json +++ b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.ValidatingWebhookConfiguration.json @@ -107,6 +107,12 @@ "timeoutSeconds": 7, "admissionReviewVersions": [ "admissionReviewVersionsValue" + ], + "matchConditions": [ + { + "name": "nameValue", + "expression": "expressionValue" + } ] } ] diff --git a/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.ValidatingWebhookConfiguration.pb b/staging/src/k8s.io/api/testdata/HEAD/admissionregistration.k8s.io.v1.ValidatingWebhookConfiguration.pb index 2f1752dec2edce62ee1f34d4ea4cacf4d04d9a19..6ece9204a7d5a2145aa69ba081ccf17f5471ecac 100644 GIT binary patch delta 55 zcmdnbc9(5}2U9=WMz0h`#+94bGxjhlMagh+<|XE)h9%~drV8<=RumMa78hsc=Ya*J H7?c#+945F!nGiMagh+<|XE)h9%~drV8<=RumMa78hsc=Ya*J H7?c 0 { + versionedAttr, err := v.VersionedAttribute(invocation.Kind) + if err != nil { + return nil, apierrors.NewInternalError(err) + } + + matcher := h.GetCompiledMatcher(a.filterCompiler, a.authorizer) + matchResult := matcher.Match(ctx, versionedAttr, nil) + + if matchResult.Error != nil { + klog.Warningf("Failed evaluating match conditions, failing closed %v: %v", h.GetName(), matchResult.Error) + return nil, apierrors.NewForbidden(attr.GetResource().GroupResource(), attr.GetName(), matchResult.Error) + } else if !matchResult.Matches { + // if no match, always skip webhook + return nil, nil + } + } + return invocation, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go index 94d5b162b8a..64595cca202 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go @@ -17,21 +17,26 @@ limitations under the License. package generic import ( + "context" + "errors" "fmt" "strings" "testing" v1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/webhook" + "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace" "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object" + "k8s.io/apiserver/pkg/authorization/authorizer" ) func gvr(group, version, resource string) schema.GroupVersionResource { @@ -42,8 +47,55 @@ func gvk(group, version, kind string) schema.GroupVersionKind { return schema.GroupVersionKind{Group: group, Version: version, Kind: kind} } +var _ matchconditions.Matcher = &fakeMatcher{} + +type fakeMatcher struct { + throwError error + matchResult bool +} + +func (f *fakeMatcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) matchconditions.MatchResult { + if f.throwError != nil { + return matchconditions.MatchResult{ + Matches: true, + FailedConditionName: "", + Error: f.throwError, + } + } + return matchconditions.MatchResult{ + Matches: f.matchResult, + FailedConditionName: "", + } +} + +var _ webhook.WebhookAccessor = &fakeWebhookAccessor{} + +type fakeWebhookAccessor struct { + webhook.WebhookAccessor + throwError error + matchResult bool +} + +func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) matchconditions.Matcher { + return &fakeMatcher{ + throwError: f.throwError, + matchResult: f.matchResult, + } +} + +var _ VersionedAttributeAccessor = &fakeVersionedAttributeAccessor{} + +type fakeVersionedAttributeAccessor struct{} + +func (v *fakeVersionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) { + return nil, nil +} + func TestShouldCallHook(t *testing.T) { - a := &Webhook{namespaceMatcher: &namespace.Matcher{}, objectMatcher: &object.Matcher{}} + a := &Webhook{ + namespaceMatcher: &namespace.Matcher{}, + objectMatcher: &object.Matcher{}, + } allScopes := v1.AllScopes exactMatch := v1.Exact @@ -83,12 +135,15 @@ func TestShouldCallHook(t *testing.T) { expectCallResource schema.GroupVersionResource expectCallSubresource string expectCallKind schema.GroupVersionKind + matchError error + matchResult bool }{ { - name: "no rules (just write)", - webhook: &v1.ValidatingWebhook{NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1.RuleWithOperations{}}, - attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), - expectCall: false, + name: "no rules (just write)", + webhook: &v1.ValidatingWebhook{NamespaceSelector: &metav1.LabelSelector{}, Rules: []v1.RuleWithOperations{}}, + attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: false, + matchResult: true, }, { name: "invalid kind lookup", @@ -100,9 +155,10 @@ func TestShouldCallHook(t *testing.T) { Operations: []v1.OperationType{"*"}, Rule: v1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"v1"}, Resources: []string{"widgets"}, Scope: &allScopes}, }}}, - attrs: admission.NewAttributesRecord(nil, nil, gvk("example.com", "v2", "Widget"), "ns", "name", gvr("example.com", "v2", "widgets"), "", admission.Create, &metav1.CreateOptions{}, false, nil), - expectCall: false, - expectErr: "unknown kind", + attrs: admission.NewAttributesRecord(nil, nil, gvk("example.com", "v2", "Widget"), "ns", "name", gvr("example.com", "v2", "widgets"), "", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: false, + expectErr: "unknown kind", + matchResult: true, }, { name: "wildcard rule, match as requested", @@ -118,6 +174,7 @@ func TestShouldCallHook(t *testing.T) { expectCallKind: gvk("apps", "v1", "Deployment"), expectCallResource: gvr("apps", "v1", "deployments"), expectCallSubresource: "", + matchResult: true, }, { name: "specific rules, prefer exact match", @@ -139,6 +196,7 @@ func TestShouldCallHook(t *testing.T) { expectCallKind: gvk("apps", "v1", "Deployment"), expectCallResource: gvr("apps", "v1", "deployments"), expectCallSubresource: "", + matchResult: true, }, { name: "specific rules, match miss", @@ -152,8 +210,9 @@ func TestShouldCallHook(t *testing.T) { Operations: []v1.OperationType{"*"}, Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes}, }}}, - attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), - expectCall: false, + attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: false, + matchResult: true, }, { name: "specific rules, exact match miss", @@ -168,8 +227,9 @@ func TestShouldCallHook(t *testing.T) { Operations: []v1.OperationType{"*"}, Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes}, }}}, - attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), - expectCall: false, + attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: false, + matchResult: true, }, { name: "specific rules, equivalent match, prefer extensions", @@ -189,6 +249,7 @@ func TestShouldCallHook(t *testing.T) { expectCallKind: gvk("extensions", "v1beta1", "Deployment"), expectCallResource: gvr("extensions", "v1beta1", "deployments"), expectCallSubresource: "", + matchResult: true, }, { name: "specific rules, equivalent match, prefer apps", @@ -208,6 +269,7 @@ func TestShouldCallHook(t *testing.T) { expectCallKind: gvk("apps", "v1beta1", "Deployment"), expectCallResource: gvr("apps", "v1beta1", "deployments"), expectCallSubresource: "", + matchResult: true, }, { @@ -230,6 +292,7 @@ func TestShouldCallHook(t *testing.T) { expectCallKind: gvk("autoscaling", "v1", "Scale"), expectCallResource: gvr("apps", "v1", "deployments"), expectCallSubresource: "scale", + matchResult: true, }, { name: "specific rules, subresource match miss", @@ -243,8 +306,9 @@ func TestShouldCallHook(t *testing.T) { Operations: []v1.OperationType{"*"}, Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes}, }}}, - attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil), - expectCall: false, + attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: false, + matchResult: true, }, { name: "specific rules, subresource exact match miss", @@ -259,8 +323,9 @@ func TestShouldCallHook(t *testing.T) { Operations: []v1.OperationType{"*"}, Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes}, }}}, - attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil), - expectCall: false, + attrs: admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: false, + matchResult: true, }, { name: "specific rules, subresource equivalent match, prefer extensions", @@ -280,6 +345,7 @@ func TestShouldCallHook(t *testing.T) { expectCallKind: gvk("extensions", "v1beta1", "Scale"), expectCallResource: gvr("extensions", "v1beta1", "deployments"), expectCallSubresource: "scale", + matchResult: true, }, { name: "specific rules, subresource equivalent match, prefer apps", @@ -299,12 +365,86 @@ func TestShouldCallHook(t *testing.T) { expectCallKind: gvk("apps", "v1beta1", "Scale"), expectCallResource: gvr("apps", "v1beta1", "deployments"), expectCallSubresource: "scale", + matchResult: true, + }, + { + name: "wildcard rule, match conditions also match", + webhook: &v1.ValidatingWebhook{ + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + Rules: []v1.RuleWithOperations{{ + Operations: []v1.OperationType{"*"}, + Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes}, + }}, + MatchConditions: []v1.MatchCondition{ + { + Name: "test1", + Expression: "test expression", + }, + }, + }, + attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: true, + expectCallKind: gvk("apps", "v1", "Deployment"), + expectCallResource: gvr("apps", "v1", "deployments"), + expectCallSubresource: "", + matchResult: true, + }, + { + name: "wildcard rule, match conditions do not match", + webhook: &v1.ValidatingWebhook{ + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + Rules: []v1.RuleWithOperations{{ + Operations: []v1.OperationType{"*"}, + Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes}, + }}, + MatchConditions: []v1.MatchCondition{ + { + Name: "test1", + Expression: "test expression", + }, + }, + }, + attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: false, + matchResult: false, + }, + { + name: "wildcard rule, match conditions error", + webhook: &v1.ValidatingWebhook{ + NamespaceSelector: &metav1.LabelSelector{}, + ObjectSelector: &metav1.LabelSelector{}, + Rules: []v1.RuleWithOperations{{ + Operations: []v1.OperationType{"*"}, + Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes}, + }}, + MatchConditions: []v1.MatchCondition{ + { + Name: "test1", + Expression: "test expression", + }, + }, + }, + attrs: admission.NewAttributesRecord(nil, nil, gvk("apps", "v1", "Deployment"), "ns", "name", gvr("apps", "v1", "deployments"), "", admission.Create, &metav1.CreateOptions{}, false, nil), + expectCall: false, + expectErr: "deployments.apps \"name\" is forbidden: test error", + expectCallKind: gvk("apps", "v1", "Deployment"), + expectCallResource: gvr("apps", "v1", "deployments"), + expectCallSubresource: "", + matchError: errors.New("test error"), }, } for i, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { - invocation, err := a.ShouldCallHook(webhook.NewValidatingWebhookAccessor(fmt.Sprintf("webhook-%d", i), fmt.Sprintf("webhook-cfg-%d", i), testcase.webhook), testcase.attrs, interfaces) + fakeWebhook := &fakeWebhookAccessor{ + WebhookAccessor: webhook.NewValidatingWebhookAccessor(fmt.Sprintf("webhook-%d", i), fmt.Sprintf("webhook-cfg-%d", i), testcase.webhook), + matchResult: testcase.matchResult, + throwError: testcase.matchError, + } + + invocation, err := a.ShouldCallHook(context.TODO(), fakeWebhook, testcase.attrs, interfaces, &fakeVersionedAttributeAccessor{}) if err != nil { if len(testcase.expectErr) == 0 { t.Fatal(err) @@ -353,7 +493,7 @@ func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) { if ok { return ns, nil } - return nil, errors.NewNotFound(corev1.Resource("namespaces"), name) + return nil, k8serrors.NewNotFound(corev1.Resource("namespaces"), name) } func BenchmarkShouldCallHookWithComplexSelector(b *testing.B) { @@ -415,13 +555,16 @@ func BenchmarkShouldCallHookWithComplexSelector(b *testing.B) { }, } - wbAccessor := webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb) + wbAccessor := &fakeWebhookAccessor{ + WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb), + matchResult: true, + } attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil) interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper} a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}} for i := 0; i < b.N; i++ { - a.ShouldCallHook(wbAccessor, attrs, interfaces) + a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, nil) } } @@ -483,13 +626,16 @@ func BenchmarkShouldCallHookWithComplexRule(b *testing.B) { wb.Rules = append(wb.Rules, rule) } - wbAccessor := webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb) + wbAccessor := &fakeWebhookAccessor{ + WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb), + matchResult: true, + } attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil) interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper} a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}} for i := 0; i < b.N; i++ { - a.ShouldCallHook(wbAccessor, attrs, interfaces) + a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, &fakeVersionedAttributeAccessor{}) } } @@ -556,12 +702,15 @@ func BenchmarkShouldCallHookWithComplexSelectorAndRule(b *testing.B) { wb.Rules = append(wb.Rules, rule) } - wbAccessor := webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb) + wbAccessor := &fakeWebhookAccessor{ + WebhookAccessor: webhook.NewValidatingWebhookAccessor("webhook", "webhook-cfg", wb), + matchResult: true, + } attrs := admission.NewAttributesRecord(nil, nil, gvk("autoscaling", "v1", "Scale"), "ns", "name", gvr("apps", "v1", "deployments"), "scale", admission.Create, &metav1.CreateOptions{}, false, nil) interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper} a := &Webhook{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}} for i := 0; i < b.N; i++ { - a.ShouldCallHook(wbAccessor, attrs, interfaces) + a.ShouldCallHook(context.TODO(), wbAccessor, attrs, interfaces, nil) } } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/interface.go new file mode 100644 index 00000000000..09468655bd0 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/interface.go @@ -0,0 +1,36 @@ +/* +Copyright 2023 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 matchconditions + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/admission" +) + +type MatchResult struct { + Matches bool + Error error + FailedConditionName string +} + +// Matcher contains logic for converting Evaluations to bool of matches or does not match +type Matcher interface { + // Match is used to take cel evaluations and convert into decisions + Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) MatchResult +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go new file mode 100644 index 00000000000..09a500dd39c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go @@ -0,0 +1,139 @@ +/* +Copyright 2023 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 matchconditions + +import ( + "context" + "errors" + "fmt" + + "github.com/google/cel-go/cel" + celtypes "github.com/google/cel-go/common/types" + + v1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/runtime" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/admission" + admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" + celplugin "k8s.io/apiserver/pkg/admission/plugin/cel" + celconfig "k8s.io/apiserver/pkg/apis/cel" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/klog/v2" +) + +var _ celplugin.ExpressionAccessor = &MatchCondition{} + +// MatchCondition contains the inputs needed to compile, evaluate and match a cel expression +type MatchCondition v1.MatchCondition + +func (v *MatchCondition) GetExpression() string { + return v.Expression +} + +func (v *MatchCondition) ReturnTypes() []*cel.Type { + return []*cel.Type{cel.BoolType} +} + +var _ Matcher = &matcher{} + +// matcher evaluates compiled cel expressions and determines if they match the given request or not +type matcher struct { + filter celplugin.Filter + authorizer authorizer.Authorizer + failPolicy v1.FailurePolicyType + matcherType string + objectName string +} + +func NewMatcher(filter celplugin.Filter, authorizer authorizer.Authorizer, failPolicy *v1.FailurePolicyType, matcherType, objectName string) Matcher { + var f v1.FailurePolicyType + if failPolicy == nil { + f = v1.Fail + } else { + f = *failPolicy + } + return &matcher{ + filter: filter, + authorizer: authorizer, + failPolicy: f, + matcherType: matcherType, + objectName: objectName, + } +} + +func (m *matcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) MatchResult { + evalResults, _, err := m.filter.ForInput(ctx, versionedAttr, celplugin.CreateAdmissionRequest(versionedAttr.Attributes), celplugin.OptionalVariableBindings{ + VersionedParams: versionedParams, + Authorizer: m.authorizer, + }, celconfig.RuntimeCELCostBudgetMatchConditions) + + if err != nil { + // filter returning error is unexpected and not an evaluation error so not incrementing metric here + if m.failPolicy == v1.Fail { + return MatchResult{ + Error: err, + } + } else if m.failPolicy == v1.Ignore { + return MatchResult{ + Matches: false, + } + } + //TODO: add default so that if in future we add different failure types it doesn't fall through + } + + errorList := []error{} + for _, evalResult := range evalResults { + matchCondition, ok := evalResult.ExpressionAccessor.(*MatchCondition) + if !ok { + // This shouldnt happen, but if it does treat same as eval error + klog.Error("Invalid type conversion to MatchCondition") + errorList = append(errorList, errors.New(fmt.Sprintf("internal error converting ExpressionAccessor to MatchCondition"))) + continue + } + if evalResult.Error != nil { + errorList = append(errorList, evalResult.Error) + //TODO: what's the best way to handle this metric since its reused by VAP for match conditions + admissionmetrics.Metrics.ObserveMatchConditionEvalError(ctx, m.objectName, m.matcherType) + } + if evalResult.EvalResult == celtypes.False { + // If any condition false, skip calling webhook always + return MatchResult{ + Matches: false, + FailedConditionName: matchCondition.Name, + } + } + } + if len(errorList) > 0 { + // If mix of true and eval errors then resort to fail policy + if m.failPolicy == v1.Fail { + // mix of true and errors with fail policy fail should fail request without calling webhook + err = utilerrors.NewAggregate(errorList) + return MatchResult{ + Error: err, + } + } else if m.failPolicy == v1.Ignore { + // if fail policy ignore then skip call to webhook + return MatchResult{ + Matches: false, + } + } + } + // if no results eval to false, return matches true with list of any errors encountered + return MatchResult{ + Matches: true, + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go new file mode 100644 index 00000000000..2ba83cbf4ef --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go @@ -0,0 +1,360 @@ +/* +Copyright 2023 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 matchconditions + +import ( + "context" + "errors" + "strings" + "testing" + + v1 "k8s.io/api/admissionregistration/v1" + + celtypes "github.com/google/cel-go/common/types" + "github.com/stretchr/testify/require" + + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/cel" +) + +var _ cel.Filter = &fakeCelFilter{} + +type fakeCelFilter struct { + evaluations []cel.EvaluationResult + throwError bool +} + +func (f *fakeCelFilter) ForInput(context.Context, *admission.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, int64) ([]cel.EvaluationResult, int64, error) { + if f.throwError { + return nil, 0, errors.New("test error") + } + return f.evaluations, 0, nil +} + +func (f *fakeCelFilter) CompilationErrors() []error { + return []error{} +} + +func TestMatch(t *testing.T) { + fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil) + fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil) + fail := v1.Fail + ignore := v1.Ignore + + cases := []struct { + name string + evaluations []cel.EvaluationResult + throwError bool + shouldMatch bool + returnedName string + failPolicy *v1.FailurePolicyType + expectError string + }{ + { + name: "test single matches", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + }, + shouldMatch: true, + }, + { + name: "test multiple match", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + }, + shouldMatch: true, + }, + { + name: "test empty evals", + evaluations: []cel.EvaluationResult{}, + shouldMatch: true, + }, + { + name: "test single no match", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.False, + ExpressionAccessor: &MatchCondition{ + Name: "test1", + }, + }, + }, + shouldMatch: false, + returnedName: "test1", + }, + { + name: "test multiple no match", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.False, + ExpressionAccessor: &MatchCondition{ + Name: "test1", + }, + }, + { + EvalResult: celtypes.False, + ExpressionAccessor: &MatchCondition{ + Name: "test2", + }, + }, + }, + shouldMatch: false, + returnedName: "test1", + }, + { + name: "test mixed with no match first", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.False, + ExpressionAccessor: &MatchCondition{ + Name: "test1", + }, + }, + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{ + Name: "test2", + }, + }, + }, + shouldMatch: false, + returnedName: "test1", + }, + { + name: "test mixed with no match last", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{ + Name: "test2", + }, + }, + { + EvalResult: celtypes.False, + ExpressionAccessor: &MatchCondition{ + Name: "test1", + }, + }, + }, + shouldMatch: false, + returnedName: "test1", + }, + { + name: "test mixed with no match middle", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{ + Name: "test2", + }, + }, + { + EvalResult: celtypes.False, + ExpressionAccessor: &MatchCondition{ + Name: "test1", + }, + }, + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{ + Name: "test2", + }, + }, + }, + shouldMatch: false, + returnedName: "test1", + }, + { + name: "test error, no fail policy", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + }, + shouldMatch: true, + throwError: true, + expectError: "test error", + }, + { + name: "test error, fail policy fail", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + }, + failPolicy: &fail, + shouldMatch: true, + throwError: true, + expectError: "test error", + }, + { + name: "test error, fail policy ignore", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + }, + failPolicy: &ignore, + shouldMatch: false, + throwError: true, + }, + { + name: "test mix of true, errors and false", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + Error: errors.New("test error"), + ExpressionAccessor: &MatchCondition{}, + }, + { + EvalResult: celtypes.False, + ExpressionAccessor: &MatchCondition{}, + }, + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + Error: errors.New("test error"), + ExpressionAccessor: &MatchCondition{}, + }, + }, + shouldMatch: false, + throwError: false, + }, + { + name: "test mix of true, errors and fail policy not set", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + Error: errors.New("test error"), + ExpressionAccessor: &MatchCondition{}, + }, + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + Error: errors.New("test error"), + ExpressionAccessor: &MatchCondition{}, + }, + }, + shouldMatch: false, + throwError: false, + expectError: "test error", + }, + { + name: "test mix of true, errors and fail policy fail", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + Error: errors.New("test error"), + ExpressionAccessor: &MatchCondition{}, + }, + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + Error: errors.New("test error"), + ExpressionAccessor: &MatchCondition{}, + }, + }, + failPolicy: &fail, + shouldMatch: false, + throwError: false, + expectError: "test error", + }, + { + name: "test mix of true, errors and fail policy ignore", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + Error: errors.New("test error"), + ExpressionAccessor: &MatchCondition{}, + }, + { + EvalResult: celtypes.True, + ExpressionAccessor: &MatchCondition{}, + }, + { + Error: errors.New("test error"), + ExpressionAccessor: &MatchCondition{}, + }, + }, + failPolicy: &ignore, + shouldMatch: false, + throwError: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + m := NewMatcher(&fakeCelFilter{ + evaluations: tc.evaluations, + throwError: tc.throwError, + }, nil, tc.failPolicy, "test", "testhook") + ctx := context.TODO() + matchResult := m.Match(ctx, fakeVersionedAttr, nil) + + if matchResult.Error != nil { + if len(tc.expectError) == 0 { + t.Fatal(matchResult.Error) + } + if !strings.Contains(matchResult.Error.Error(), tc.expectError) { + t.Fatalf("expected error containing %q, got %s", tc.expectError, matchResult.Error.Error()) + } + return + } else if len(tc.expectError) > 0 { + t.Fatal("expected error but did not get one") + } + if len(tc.expectError) > 0 && matchResult.Error == nil { + t.Errorf("expected error thrown when filter errors") + } + + require.Equal(t, tc.shouldMatch, matchResult.Matches) + require.Equal(t, tc.returnedName, matchResult.FailedConditionName) + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go index ef46eafb5c5..c1d1ca6ff6b 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go @@ -26,14 +26,13 @@ import ( jsonpatch "github.com/evanphx/json-patch" "go.opentelemetry.io/otel/attribute" - apiequality "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/klog/v2" - admissionv1 "k8s.io/api/admission/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/json" utiljson "k8s.io/apimachinery/pkg/util/json" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -48,6 +47,7 @@ import ( webhookutil "k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/pkg/warning" "k8s.io/component-base/tracing" + "k8s.io/klog/v2" ) const ( @@ -75,6 +75,30 @@ func newMutatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generi } } +var _ generic.VersionedAttributeAccessor = &versionedAttributeAccessor{} + +type versionedAttributeAccessor struct { + versionedAttr *admission.VersionedAttributes + attr admission.Attributes + objectInterfaces admission.ObjectInterfaces +} + +func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) { + if v.versionedAttr == nil { + // First call, create versioned attributes + var err error + if v.versionedAttr, err = admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces); err != nil { + return nil, apierrors.NewInternalError(err) + } + } else { + // Subsequent call, convert existing versioned attributes to the requested version + if err := admission.ConvertVersionedAttributes(v.versionedAttr, gvk, v.objectInterfaces); err != nil { + return nil, apierrors.NewInternalError(err) + } + } + return v.versionedAttr, nil +} + var _ generic.Dispatcher = &mutatingDispatcher{} func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error { @@ -95,19 +119,24 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib defer func() { webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject()) }() - var versionedAttr *admission.VersionedAttributes + v := &versionedAttributeAccessor{ + attr: attr, + objectInterfaces: o, + } for i, hook := range hooks { attrForCheck := attr - if versionedAttr != nil { - attrForCheck = versionedAttr + if v.versionedAttr != nil { + attrForCheck = v.versionedAttr } - invocation, statusErr := a.plugin.ShouldCallHook(hook, attrForCheck, o) + + invocation, statusErr := a.plugin.ShouldCallHook(ctx, hook, attrForCheck, o, v) if statusErr != nil { return statusErr } if invocation == nil { continue } + hook, ok := invocation.Webhook.GetMutatingWebhook() if !ok { return fmt.Errorf("mutating webhook dispatch requires v1.MutatingWebhook, but got %T", hook) @@ -121,17 +150,9 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib continue } - if versionedAttr == nil { - // First webhook, create versioned attributes - var err error - if versionedAttr, err = admission.NewVersionedAttributes(attr, invocation.Kind, o); err != nil { - return apierrors.NewInternalError(err) - } - } else { - // Subsequent webhook, convert existing versioned attributes to this webhook's version - if err := admission.ConvertVersionedAttributes(versionedAttr, invocation.Kind, o); err != nil { - return apierrors.NewInternalError(err) - } + versionedAttr, err := v.VersionedAttribute(invocation.Kind) + if err != nil { + return apierrors.NewInternalError(err) } t := time.Now() @@ -203,8 +224,8 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib } // convert versionedAttr.VersionedObject to the internal version in the underlying admission.Attributes - if versionedAttr != nil && versionedAttr.VersionedObject != nil && versionedAttr.Dirty { - return o.GetObjectConvertor().Convert(versionedAttr.VersionedObject, versionedAttr.Attributes.GetObject(), nil) + if v.versionedAttr != nil && v.versionedAttr.VersionedObject != nil && v.versionedAttr.Dirty { + return o.GetObjectConvertor().Convert(v.versionedAttr.VersionedObject, v.versionedAttr.Attributes.GetObject(), nil) } return nil diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go index 19c6cdf14c4..14312fadd54 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go @@ -62,30 +62,51 @@ func newValidatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) gene } } +var _ generic.VersionedAttributeAccessor = &versionedAttributeAccessor{} + +type versionedAttributeAccessor struct { + versionedAttrs map[schema.GroupVersionKind]*admission.VersionedAttributes + attr admission.Attributes + objectInterfaces admission.ObjectInterfaces +} + +func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) { + if val, ok := v.versionedAttrs[gvk]; ok { + return val, nil + } + versionedAttr, err := admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces) + if err != nil { + return nil, err + } + v.versionedAttrs[gvk] = versionedAttr + return versionedAttr, nil +} + var _ generic.Dispatcher = &validatingDispatcher{} 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 - versionedAttrs := map[schema.GroupVersionKind]*admission.VersionedAttributes{} + versionedAttrAccessor := &versionedAttributeAccessor{ + versionedAttrs: map[schema.GroupVersionKind]*admission.VersionedAttributes{}, + attr: attr, + objectInterfaces: o, + } for _, hook := range hooks { - invocation, statusError := d.plugin.ShouldCallHook(hook, attr, o) + invocation, statusError := d.plugin.ShouldCallHook(ctx, hook, attr, o, versionedAttrAccessor) if statusError != nil { return statusError } if invocation == nil { continue } + relevantHooks = append(relevantHooks, invocation) - // If we already have this version, continue - if _, ok := versionedAttrs[invocation.Kind]; ok { - continue - } - versionedAttr, err := admission.NewVersionedAttributes(attr, invocation.Kind, o) + // VersionedAttr result will be cached and reused later during parallel webhook calls + _, err := versionedAttrAccessor.VersionedAttribute(invocation.Kind) if err != nil { return apierrors.NewInternalError(err) } - versionedAttrs[invocation.Kind] = versionedAttr } if len(relevantHooks) == 0 { @@ -108,7 +129,7 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr go func(invocation *generic.WebhookInvocation, idx int) { ignoreClientCallFailures := false hookName := "unknown" - versionedAttr := versionedAttrs[invocation.Kind] + versionedAttr := versionedAttrAccessor.versionedAttrs[invocation.Kind] // The ordering of these two defers is critical. The wg.Done will release the parent go func to close the errCh // that is used by the second defer to report errors. The recovery and error reporting must be done first. defer wg.Done() diff --git a/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go b/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go index d05821899de..319548cd537 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go @@ -25,6 +25,11 @@ const ( // current RuntimeCELCostBudget gives roughly 1 seconds for the validation RuntimeCELCostBudget = 10000000 + // RuntimeCELCostBudgetMatchConditions is the overall cost budget for runtime CEL validation cost on matchConditions per object with matchConditions + // this is per webhook for validatingwebhookconfigurations and mutatingwebhookconfigurations or per ValidatingAdmissionPolicyBinding + // current RuntimeCELCostBudgetMatchConditions gives roughly 1/4 seconds for the validation + RuntimeCELCostBudgetMatchConditions = 2500000 + // CheckFrequency configures the number of iterations within a comprehension to evaluate // before checking whether the function evaluation has been interrupted CheckFrequency = 100 diff --git a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go index 73351b585f2..44166447f30 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -35,6 +35,13 @@ const ( // of code conflicts because changes are more likely to be scattered // across the file. + // owner: @ivelichkovich, @tallclair + // alpha: v1.27 + // kep: https://kep.k8s.io/3716 + // + // Enables usage of MatchConditions fields to use CEL expressions for matching on admission webhooks + AdmissionWebhookMatchConditions featuregate.Feature = "AdmissionWebhookMatchConditions" + // owner: @jefftree @alexzielenski // alpha: v1.26 // @@ -223,8 +230,11 @@ func init() { // To add a new feature, define a key for it above and add it here. The features will be // available throughout Kubernetes binaries. var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ + AggregatedDiscoveryEndpoint: {Default: true, PreRelease: featuregate.Beta}, + AdmissionWebhookMatchConditions: {Default: false, PreRelease: featuregate.Alpha}, + APIListChunking: {Default: true, PreRelease: featuregate.Beta}, APIPriorityAndFairness: {Default: true, PreRelease: featuregate.Beta}, diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/matchcondition.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/matchcondition.go new file mode 100644 index 00000000000..ea1dc377b97 --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/matchcondition.go @@ -0,0 +1,48 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// MatchConditionApplyConfiguration represents an declarative configuration of the MatchCondition type for use +// with apply. +type MatchConditionApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Expression *string `json:"expression,omitempty"` +} + +// MatchConditionApplyConfiguration constructs an declarative configuration of the MatchCondition type for use with +// apply. +func MatchCondition() *MatchConditionApplyConfiguration { + return &MatchConditionApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *MatchConditionApplyConfiguration) WithName(value string) *MatchConditionApplyConfiguration { + b.Name = &value + return b +} + +// WithExpression sets the Expression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Expression field is set to the value of the last call. +func (b *MatchConditionApplyConfiguration) WithExpression(value string) *MatchConditionApplyConfiguration { + b.Expression = &value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/mutatingwebhook.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/mutatingwebhook.go index eba37bafdbd..faff51a0415 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/mutatingwebhook.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/mutatingwebhook.go @@ -37,6 +37,7 @@ type MutatingWebhookApplyConfiguration struct { TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` AdmissionReviewVersions []string `json:"admissionReviewVersions,omitempty"` ReinvocationPolicy *admissionregistrationv1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty"` + MatchConditions []MatchConditionApplyConfiguration `json:"matchConditions,omitempty"` } // MutatingWebhookApplyConfiguration constructs an declarative configuration of the MutatingWebhook type for use with @@ -139,3 +140,16 @@ func (b *MutatingWebhookApplyConfiguration) WithReinvocationPolicy(value admissi b.ReinvocationPolicy = &value return b } + +// WithMatchConditions adds the given value to the MatchConditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the MatchConditions field. +func (b *MutatingWebhookApplyConfiguration) WithMatchConditions(values ...*MatchConditionApplyConfiguration) *MutatingWebhookApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMatchConditions") + } + b.MatchConditions = append(b.MatchConditions, *values[i]) + } + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/validatingwebhook.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/validatingwebhook.go index d0691de107c..613856bac7f 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/validatingwebhook.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1/validatingwebhook.go @@ -36,6 +36,7 @@ type ValidatingWebhookApplyConfiguration struct { SideEffects *admissionregistrationv1.SideEffectClass `json:"sideEffects,omitempty"` TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` AdmissionReviewVersions []string `json:"admissionReviewVersions,omitempty"` + MatchConditions []MatchConditionApplyConfiguration `json:"matchConditions,omitempty"` } // ValidatingWebhookApplyConfiguration constructs an declarative configuration of the ValidatingWebhook type for use with @@ -130,3 +131,16 @@ func (b *ValidatingWebhookApplyConfiguration) WithAdmissionReviewVersions(values } return b } + +// WithMatchConditions adds the given value to the MatchConditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the MatchConditions field. +func (b *ValidatingWebhookApplyConfiguration) WithMatchConditions(values ...*MatchConditionApplyConfiguration) *ValidatingWebhookApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMatchConditions") + } + b.MatchConditions = append(b.MatchConditions, *values[i]) + } + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/matchcondition.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/matchcondition.go new file mode 100644 index 00000000000..d099b6b6eae --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/matchcondition.go @@ -0,0 +1,48 @@ +/* +Copyright 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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// MatchConditionApplyConfiguration represents an declarative configuration of the MatchCondition type for use +// with apply. +type MatchConditionApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Expression *string `json:"expression,omitempty"` +} + +// MatchConditionApplyConfiguration constructs an declarative configuration of the MatchCondition type for use with +// apply. +func MatchCondition() *MatchConditionApplyConfiguration { + return &MatchConditionApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *MatchConditionApplyConfiguration) WithName(value string) *MatchConditionApplyConfiguration { + b.Name = &value + return b +} + +// WithExpression sets the Expression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Expression field is set to the value of the last call. +func (b *MatchConditionApplyConfiguration) WithExpression(value string) *MatchConditionApplyConfiguration { + b.Expression = &value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/mutatingwebhook.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/mutatingwebhook.go index cc48d3b6f01..54845341f48 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/mutatingwebhook.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/mutatingwebhook.go @@ -38,6 +38,7 @@ type MutatingWebhookApplyConfiguration struct { TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` AdmissionReviewVersions []string `json:"admissionReviewVersions,omitempty"` ReinvocationPolicy *admissionregistrationv1beta1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty"` + MatchConditions []MatchConditionApplyConfiguration `json:"matchConditions,omitempty"` } // MutatingWebhookApplyConfiguration constructs an declarative configuration of the MutatingWebhook type for use with @@ -140,3 +141,16 @@ func (b *MutatingWebhookApplyConfiguration) WithReinvocationPolicy(value admissi b.ReinvocationPolicy = &value return b } + +// WithMatchConditions adds the given value to the MatchConditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the MatchConditions field. +func (b *MutatingWebhookApplyConfiguration) WithMatchConditions(values ...*MatchConditionApplyConfiguration) *MutatingWebhookApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMatchConditions") + } + b.MatchConditions = append(b.MatchConditions, *values[i]) + } + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/validatingwebhook.go b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/validatingwebhook.go index 84479b5db39..8c5c341bade 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/validatingwebhook.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1/validatingwebhook.go @@ -37,6 +37,7 @@ type ValidatingWebhookApplyConfiguration struct { SideEffects *admissionregistrationv1beta1.SideEffectClass `json:"sideEffects,omitempty"` TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` AdmissionReviewVersions []string `json:"admissionReviewVersions,omitempty"` + MatchConditions []MatchConditionApplyConfiguration `json:"matchConditions,omitempty"` } // ValidatingWebhookApplyConfiguration constructs an declarative configuration of the ValidatingWebhook type for use with @@ -131,3 +132,16 @@ func (b *ValidatingWebhookApplyConfiguration) WithAdmissionReviewVersions(values } return b } + +// WithMatchConditions adds the given value to the MatchConditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the MatchConditions field. +func (b *ValidatingWebhookApplyConfiguration) WithMatchConditions(values ...*MatchConditionApplyConfiguration) *ValidatingWebhookApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMatchConditions") + } + b.MatchConditions = append(b.MatchConditions, *values[i]) + } + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go b/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go index d1f2c651dbd..e490be8123e 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go @@ -39,6 +39,17 @@ func Parser() *typed.Parser { var parserOnce sync.Once var parser *typed.Parser var schemaYAML = typed.YAMLObject(`types: +- name: io.k8s.api.admissionregistration.v1.MatchCondition + map: + fields: + - name: expression + type: + scalar: string + default: "" + - name: name + type: + scalar: string + default: "" - name: io.k8s.api.admissionregistration.v1.MutatingWebhook map: fields: @@ -55,6 +66,14 @@ var schemaYAML = typed.YAMLObject(`types: - name: failurePolicy type: scalar: string + - name: matchConditions + type: + list: + elementType: + namedType: io.k8s.api.admissionregistration.v1.MatchCondition + elementRelationship: associative + keys: + - name - name: matchPolicy type: scalar: string @@ -167,6 +186,14 @@ var schemaYAML = typed.YAMLObject(`types: - name: failurePolicy type: scalar: string + - name: matchConditions + type: + list: + elementType: + namedType: io.k8s.api.admissionregistration.v1.MatchCondition + elementRelationship: associative + keys: + - name - name: matchPolicy type: scalar: string @@ -451,6 +478,17 @@ var schemaYAML = typed.YAMLObject(`types: - name: reason type: scalar: string +- name: io.k8s.api.admissionregistration.v1beta1.MatchCondition + map: + fields: + - name: expression + type: + scalar: string + default: "" + - name: name + type: + scalar: string + default: "" - name: io.k8s.api.admissionregistration.v1beta1.MutatingWebhook map: fields: @@ -467,6 +505,14 @@ var schemaYAML = typed.YAMLObject(`types: - name: failurePolicy type: scalar: string + - name: matchConditions + type: + list: + elementType: + namedType: io.k8s.api.admissionregistration.v1beta1.MatchCondition + elementRelationship: associative + keys: + - name - name: matchPolicy type: scalar: string @@ -549,6 +595,14 @@ var schemaYAML = typed.YAMLObject(`types: - name: failurePolicy type: scalar: string + - name: matchConditions + type: + list: + elementType: + namedType: io.k8s.api.admissionregistration.v1beta1.MatchCondition + elementRelationship: associative + keys: + - name - name: matchPolicy type: scalar: string diff --git a/staging/src/k8s.io/client-go/applyconfigurations/utils.go b/staging/src/k8s.io/client-go/applyconfigurations/utils.go index 98c743274e7..c9becae24c7 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/utils.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/utils.go @@ -121,6 +121,8 @@ import ( func ForKind(kind schema.GroupVersionKind) interface{} { switch kind { // Group=admissionregistration.k8s.io, Version=v1 + case v1.SchemeGroupVersion.WithKind("MatchCondition"): + return &admissionregistrationv1.MatchConditionApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("MutatingWebhook"): return &admissionregistrationv1.MutatingWebhookApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("MutatingWebhookConfiguration"): @@ -167,6 +169,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &admissionregistrationv1alpha1.ValidationApplyConfiguration{} // Group=admissionregistration.k8s.io, Version=v1beta1 + case v1beta1.SchemeGroupVersion.WithKind("MatchCondition"): + return &admissionregistrationv1beta1.MatchConditionApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("MutatingWebhook"): return &admissionregistrationv1beta1.MutatingWebhookApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("MutatingWebhookConfiguration"): diff --git a/test/integration/apiserver/admissionwebhook/match_conditions_test.go b/test/integration/apiserver/admissionwebhook/match_conditions_test.go new file mode 100644 index 00000000000..5baab1fe38a --- /dev/null +++ b/test/integration/apiserver/admissionwebhook/match_conditions_test.go @@ -0,0 +1,712 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admissionwebhook + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + clientset "k8s.io/client-go/kubernetes" + + admissionv1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" + apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/test/integration/framework" +) + +type admissionRecorder struct { + mu sync.Mutex + upCh chan struct{} + upOnce sync.Once + requests []*admissionv1.AdmissionRequest +} + +func (r *admissionRecorder) Record(req *admissionv1.AdmissionRequest) { + r.mu.Lock() + defer r.mu.Unlock() + r.requests = append(r.requests, req) +} + +func (r *admissionRecorder) MarkerReceived() { + r.mu.Lock() + defer r.mu.Unlock() + r.upOnce.Do(func() { + close(r.upCh) + }) +} + +func (r *admissionRecorder) Reset() chan struct{} { + r.mu.Lock() + defer r.mu.Unlock() + r.requests = []*admissionv1.AdmissionRequest{} + r.upCh = make(chan struct{}) + r.upOnce = sync.Once{} + return r.upCh +} + +func newMatchConditionHandler(recorder *admissionRecorder) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), 400) + } + review := admissionv1.AdmissionReview{} + if err := json.Unmarshal(data, &review); err != nil { + http.Error(w, err.Error(), 400) + } + + review.Response = &admissionv1.AdmissionResponse{ + Allowed: true, + UID: review.Request.UID, + Result: &metav1.Status{Message: "admitted"}, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(review); err != nil { + http.Error(w, err.Error(), 400) + return + } + + switch r.URL.Path { + case "/marker": + recorder.MarkerReceived() + return + } + + recorder.Record(review.Request) + }) +} + +// Test_MatchConditions tests ValidatingWebhookConfigurations and MutatingWebhookConfigurations that validates different cases of matchCondition fields +func Test_MatchConditions(t *testing.T) { + fail := admissionregistrationv1.Fail + ignore := admissionregistrationv1.Ignore + + testcases := []struct { + name string + matchConditions []admissionregistrationv1.MatchCondition + pods []*corev1.Pod + matchedPods []*corev1.Pod + expectErrorWH bool + expectErrorPod bool + failPolicy *admissionregistrationv1.FailurePolicyType + }{ + { + name: "pods in namespace kube-system is ignored", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "pods-in-kube-system-exempt.kubernetes.io", + Expression: "object.metadata.namespace != 'kube-system'", + }, + }, + pods: []*corev1.Pod{ + matchConditionsTestPod("test1", "kube-system"), + matchConditionsTestPod("test2", "default"), + }, + matchedPods: []*corev1.Pod{ + matchConditionsTestPod("test2", "default"), + }, + expectErrorWH: false, + }, + { + name: "matchConditions are ANDed together", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "pods-in-kube-system-exempt.kubernetes.io", + Expression: "object.metadata.namespace != 'kube-system'", + }, + { + Name: "pods-with-name-test1.kubernetes.io", + Expression: "object.metadata.name == 'test1'", + }, + }, + pods: []*corev1.Pod{ + matchConditionsTestPod("test1", "kube-system"), + matchConditionsTestPod("test1", "default"), + matchConditionsTestPod("test2", "default"), + }, + matchedPods: []*corev1.Pod{ + matchConditionsTestPod("test1", "default"), + }, + expectErrorWH: false, + }, + { + name: "mix of true, error and false should not match and not call webhook", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "test1", + Expression: "object.nonExistentProperty == 'someval'", + }, + { + Name: "test2", + Expression: "true", + }, + { + Name: "test3", + Expression: "false", + }, + { + Name: "test4", + Expression: "true", + }, + { + Name: "test5", + Expression: "object.nonExistentProperty == 'someval'", + }, + }, + pods: []*corev1.Pod{ + matchConditionsTestPod("test1", "kube-system"), + matchConditionsTestPod("test2", "default"), + }, + matchedPods: []*corev1.Pod{}, + expectErrorWH: false, + expectErrorPod: false, + }, + { + name: "mix of true and error should reject request without fail policy", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "test1", + Expression: "object.nonExistentProperty == 'someval'", + }, + { + Name: "test2", + Expression: "true", + }, + { + Name: "test4", + Expression: "true", + }, + { + Name: "test5", + Expression: "object.nonExistentProperty == 'someval'", + }, + }, + pods: []*corev1.Pod{ + matchConditionsTestPod("test1", "kube-system"), + matchConditionsTestPod("test2", "default"), + }, + matchedPods: []*corev1.Pod{}, + expectErrorWH: false, + expectErrorPod: true, + }, + { + name: "mix of true and error should reject request with fail policy fail", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "test1", + Expression: "object.nonExistentProperty == 'someval'", + }, + { + Name: "test2", + Expression: "true", + }, + { + Name: "test4", + Expression: "true", + }, + { + Name: "test5", + Expression: "object.nonExistentProperty == 'someval'", + }, + }, + pods: []*corev1.Pod{ + matchConditionsTestPod("test1", "kube-system"), + matchConditionsTestPod("test2", "default"), + }, + matchedPods: []*corev1.Pod{}, + failPolicy: &fail, + expectErrorWH: false, + expectErrorPod: true, + }, + { + name: "mix of true and error should match request and call webhook with fail policy ignore", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "tes1", + Expression: "object.nonExistentProperty == 'someval'", + }, + { + Name: "test2", + Expression: "true", + }, + { + Name: "test4", + Expression: "true", + }, + { + Name: "test5", + Expression: "object.nonExistentProperty == 'someval'", + }, + }, + pods: []*corev1.Pod{ + matchConditionsTestPod("test1", "kube-system"), + matchConditionsTestPod("test2", "default"), + }, + matchedPods: []*corev1.Pod{}, + failPolicy: &ignore, + expectErrorWH: false, + }, + { + name: "has access to oldObject", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "old-object-is-null.kubernetes.io", + Expression: "oldObject == null", + }, + }, + pods: []*corev1.Pod{ + matchConditionsTestPod("test2", "default"), + }, + matchedPods: []*corev1.Pod{ + matchConditionsTestPod("test2", "default"), + }, + expectErrorWH: false, + }, + { + name: "invalid field should error", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "old-object-is-null.kubernetes.io", + Expression: "imnotafield == null", + }, + }, + pods: []*corev1.Pod{}, + matchedPods: []*corev1.Pod{}, + expectErrorWH: true, + }, + { + name: "missing expression should error", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "old-object-is-null.kubernetes.io", + }, + }, + pods: []*corev1.Pod{}, + matchedPods: []*corev1.Pod{}, + expectErrorWH: true, + }, + { + name: "missing name should error", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Expression: "oldObject == null", + }, + }, + pods: []*corev1.Pod{}, + matchedPods: []*corev1.Pod{}, + expectErrorWH: true, + }, + { + name: "empty name should error", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "", + Expression: "oldObject == null", + }, + }, + pods: []*corev1.Pod{}, + matchedPods: []*corev1.Pod{}, + expectErrorWH: true, + }, + { + name: "empty expression should error", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "test-empty-expression.kubernetes.io", + Expression: "", + }, + }, + pods: []*corev1.Pod{}, + matchedPods: []*corev1.Pod{}, + expectErrorWH: true, + }, + { + name: "duplicate name should error", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "test1", + Expression: "oldObject == null", + }, + { + Name: "test1", + Expression: "oldObject == null", + }, + }, + pods: []*corev1.Pod{}, + matchedPods: []*corev1.Pod{}, + expectErrorWH: true, + }, + { + name: "name must be qualified name", + matchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: " test1", + Expression: "oldObject == null", + }, + }, + pods: []*corev1.Pod{}, + matchedPods: []*corev1.Pod{}, + expectErrorWH: true, + }, + } + + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(localhostCert) { + t.Fatal("Failed to append Cert from PEM") + } + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Failed to build cert with error: %+v", err) + } + + recorder := &admissionRecorder{requests: []*admissionv1.AdmissionRequest{}} + + webhookServer := httptest.NewUnstartedServer(newMatchConditionHandler(recorder)) + webhookServer.TLS = &tls.Config{ + RootCAs: roots, + Certificates: []tls.Certificate{cert}, + } + webhookServer.StartTLS() + defer webhookServer.Close() + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + upCh := recorder.Reset() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AdmissionWebhookMatchConditions, true)() + + server, err := apiservertesting.StartTestServer(t, nil, []string{ + "--disable-admission-plugins=ServiceAccount", + }, framework.SharedEtcd()) + if err != nil { + t.Fatal(err) + } + defer server.TearDownFn() + + config := server.ClientConfig + + client, err := clientset.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + // Write markers to a separate namespace to avoid cross-talk + markerNs := "marker" + _, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: markerNs}}, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + // Create a marker object to use to check for the webhook configurations to be ready. + marker, err := client.CoreV1().Pods(markerNs).Create(context.TODO(), newMarkerPod(markerNs), metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + endpoint := webhookServer.URL + markerEndpoint := webhookServer.URL + "/marker" + validatingwebhook := &admissionregistrationv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admission.integration.test", + }, + Webhooks: []admissionregistrationv1.ValidatingWebhook{ + { + Name: "admission.integration.test", + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"pods"}, + }, + }}, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: &endpoint, + CABundle: localhostCert, + }, + // ignore pods in the marker namespace + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: corev1.LabelMetadataName, + Operator: metav1.LabelSelectorOpNotIn, + Values: []string{"marker"}, + }, + }}, + FailurePolicy: testcase.failPolicy, + SideEffects: &noSideEffects, + AdmissionReviewVersions: []string{"v1"}, + MatchConditions: testcase.matchConditions, + }, + { + Name: "admission.integration.test.marker", + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, + Rule: admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}}, + }}, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: &markerEndpoint, + CABundle: localhostCert, + }, + NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{ + corev1.LabelMetadataName: "marker", + }}, + ObjectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}}, + FailurePolicy: testcase.failPolicy, + SideEffects: &noSideEffects, + AdmissionReviewVersions: []string{"v1"}, + }, + }, + } + + validatingcfg, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), validatingwebhook, metav1.CreateOptions{}) + if testcase.expectErrorWH == false && err != nil { + t.Fatal(err) + } else if testcase.expectErrorWH == true { + if err == nil { + t.Fatal("expected error creating ValidatingWebhookConfigurations") + } + return + } + vhwHasBeenCleanedUp := false + defer func() { + if !vhwHasBeenCleanedUp { + err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(context.TODO(), validatingcfg.GetName(), metav1.DeleteOptions{}) + if err != nil { + t.Fatal(err) + } + } + }() + + // wait until new webhook is called the first time + if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) { + _, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{}) + select { + case <-upCh: + return true, nil + default: + t.Logf("Waiting for webhook to become effective, getting marker object: %v", err) + return false, nil + } + }); err != nil { + t.Fatal(err) + } + + for _, pod := range testcase.pods { + _, err := client.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, metav1.CreateOptions{}) + if testcase.expectErrorPod == false && err != nil { + t.Fatalf("unexpected error creating test pod: %v", err) + } else if testcase.expectErrorWH == true { + if err == nil { + t.Fatal("expected error creating pods") + } + return + } + } + + if len(recorder.requests) != len(testcase.matchedPods) { + t.Errorf("unexpected requests %v, expected %v", recorder.requests, testcase.matchedPods) + } + + for i, request := range recorder.requests { + if request.Name != testcase.matchedPods[i].Name { + t.Errorf("unexpected pod name %v, expected %v", request.Name, testcase.matchedPods[i].Name) + } + if request.Namespace != testcase.matchedPods[i].Namespace { + t.Errorf("unexpected pod namespace %v, expected %v", request.Namespace, testcase.matchedPods[i].Namespace) + } + } + + //Reset and rerun against mutating webhook configuration + //TODO: private helper function for validation after creating vwh or mwh + upCh = recorder.Reset() + err = client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(context.TODO(), validatingcfg.GetName(), metav1.DeleteOptions{}) + if err != nil { + t.Fatal(err) + } else { + vhwHasBeenCleanedUp = true + } + + mutatingwebhook := &admissionregistrationv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admission.integration.test", + }, + Webhooks: []admissionregistrationv1.MutatingWebhook{ + { + Name: "admission.integration.test", + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"pods"}, + }, + }}, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: &endpoint, + CABundle: localhostCert, + }, + // ignore pods in the marker namespace + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: corev1.LabelMetadataName, + Operator: metav1.LabelSelectorOpNotIn, + Values: []string{"marker"}, + }, + }}, + FailurePolicy: testcase.failPolicy, + SideEffects: &noSideEffects, + AdmissionReviewVersions: []string{"v1"}, + MatchConditions: testcase.matchConditions, + }, + { + Name: "admission.integration.test.marker", + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, + Rule: admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}}, + }}, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: &markerEndpoint, + CABundle: localhostCert, + }, + NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{ + corev1.LabelMetadataName: "marker", + }}, + ObjectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}}, + FailurePolicy: testcase.failPolicy, + SideEffects: &noSideEffects, + AdmissionReviewVersions: []string{"v1"}, + }, + }, + } + + mutatingcfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), mutatingwebhook, metav1.CreateOptions{}) + if testcase.expectErrorWH == false && err != nil { + t.Fatal(err) + } else if testcase.expectErrorWH == true { + if err == nil { + t.Fatal("expected error creating MutatingWebhookConfiguration") + } + return + } + defer func() { + err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingcfg.GetName(), metav1.DeleteOptions{}) + if err != nil { + t.Fatal(err) + } + }() + + // wait until new webhook is called the first time + if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) { + _, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{}) + select { + case <-upCh: + return true, nil + default: + t.Logf("Waiting for webhook to become effective, getting marker object: %v", err) + return false, nil + } + }); err != nil { + t.Fatal(err) + } + + for _, pod := range testcase.pods { + if !testcase.expectErrorPod { + err := client.CoreV1().Pods(pod.Namespace).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{}) + //TODO: should probably confirm deleted + if err != nil { + t.Errorf("unexpected error deleting pods %v", err.Error()) + } + } + _, err = client.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, metav1.CreateOptions{}) + if testcase.expectErrorPod == false && err != nil { + t.Fatalf("unexpected error creating test pod: %v", err) + } else if testcase.expectErrorWH == true { + if err == nil { + t.Fatal("expected error creating pods") + } + return + } + } + + if len(recorder.requests) != len(testcase.matchedPods) { + t.Errorf("unexpected requests %v, expected %v", recorder.requests, testcase.matchedPods) + } + + for i, request := range recorder.requests { + if request.Name != testcase.matchedPods[i].Name { + t.Errorf("unexpected pod name %v, expected %v", request.Name, testcase.matchedPods[i].Name) + } + if request.Namespace != testcase.matchedPods[i].Namespace { + t.Errorf("unexpected pod namespace %v, expected %v", request.Namespace, testcase.matchedPods[i].Namespace) + } + } + }) + } +} + +func matchConditionsTestPod(name, ns string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test", + Image: "test", + }, + }, + }, + } +} + +func newMarkerPod(namespace string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "marker", + Labels: map[string]string{ + "marker": "true", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "fake-name", + Image: "fakeimage", + }}, + }, + } +} diff --git a/test/integration/apiserver/admissionwebhook/mutating_webhook_gvk_conversion_test.go b/test/integration/apiserver/admissionwebhook/mutating_webhook_gvk_conversion_test.go new file mode 100644 index 00000000000..f4008caaa56 --- /dev/null +++ b/test/integration/apiserver/admissionwebhook/mutating_webhook_gvk_conversion_test.go @@ -0,0 +1,400 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admissionwebhook + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + admissionv1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/test/integration/fixtures" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/client-go/dynamic" + clientset "k8s.io/client-go/kubernetes" + featuregatetesting "k8s.io/component-base/featuregate/testing" + apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/test/integration/etcd" + "k8s.io/kubernetes/test/integration/framework" +) + +var ( + runtimeSchemeGVKTest = runtime.NewScheme() + codecFactoryGVKTest = serializer.NewCodecFactory(runtimeSchemeGVKTest) + deserializerGVKTest = codecFactoryGVKTest.UniversalDeserializer() +) + +type admissionTypeChecker struct { + mu sync.Mutex + upCh chan struct{} + upOnce sync.Once + requests []*admissionv1.AdmissionRequest +} + +func (r *admissionTypeChecker) Reset() chan struct{} { + r.mu.Lock() + defer r.mu.Unlock() + r.upCh = make(chan struct{}) + r.upOnce = sync.Once{} + r.requests = []*admissionv1.AdmissionRequest{} + return r.upCh +} + +func (r *admissionTypeChecker) TypeCheck(req *admissionv1.AdmissionRequest, version string) *admissionv1.AdmissionResponse { + r.mu.Lock() + defer r.mu.Unlock() + r.requests = append(r.requests, req) + raw := req.Object.Raw + var into runtime.Object + if _, gvk, err := deserializerGVKTest.Decode(raw, nil, into); err != nil { + if gvk.Version != version { + return &admissionv1.AdmissionResponse{ + UID: req.UID, + Allowed: false, + } + } + } + + return &admissionv1.AdmissionResponse{ + UID: req.UID, + Allowed: true, + } +} + +func (r *admissionTypeChecker) MarkerReceived() { + r.mu.Lock() + defer r.mu.Unlock() + r.upOnce.Do(func() { + close(r.upCh) + }) +} + +func newAdmissionTypeCheckerHandler(recorder *admissionTypeChecker) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), 400) + } + review := admissionv1.AdmissionReview{} + if err := json.Unmarshal(data, &review); err != nil { + http.Error(w, err.Error(), 400) + } + + switch r.URL.Path { + case "/marker": + recorder.MarkerReceived() + return + case "/v1": + review.Response = recorder.TypeCheck(review.Request, "v1") + case "/v2": + review.Response = recorder.TypeCheck(review.Request, "v2") + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(review); err != nil { + http.Error(w, err.Error(), 400) + return + } + + }) +} + +// Test_MutatingWebhookConvertsGVKWithMatchPolicyEquivalent tests if a equivalent resource is properly converted between mutating webhooks +func Test_MutatingWebhookConvertsGVKWithMatchPolicyEquivalent(t *testing.T) { + + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(localhostCert) { + t.Fatal("Failed to append Cert from PEM") + } + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Failed to build cert with error: %+v", err) + } + + typeChecker := &admissionTypeChecker{} + + webhookServer := httptest.NewUnstartedServer(newAdmissionTypeCheckerHandler(typeChecker)) + webhookServer.TLS = &tls.Config{ + RootCAs: roots, + Certificates: []tls.Certificate{cert}, + } + webhookServer.StartTLS() + defer webhookServer.Close() + + upCh := typeChecker.Reset() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AdmissionWebhookMatchConditions, true)() + server, err := apiservertesting.StartTestServer(t, nil, []string{ + "--disable-admission-plugins=ServiceAccount", + }, framework.SharedEtcd()) + if err != nil { + t.Fatal(err) + } + defer server.TearDownFn() + + etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, versionedCustomResourceDefinition()) + if err != nil { + t.Fatal(err) + } + + config := server.ClientConfig + + client, err := clientset.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + // Write markers to a separate namespace to avoid cross-talk + markerNs := "marker" + _, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: markerNs}}, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + // Create a marker object to use to check for the webhook configurations to be ready. + marker, err := client.CoreV1().Pods(markerNs).Create(context.TODO(), newMarkerPodGVKConversion(markerNs), metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + equivalent := admissionregistrationv1.Equivalent + ignore := admissionregistrationv1.Ignore + + v1Endpoint := webhookServer.URL + "/v1" + markerEndpoint := webhookServer.URL + "/marker" + v2Endpoint := webhookServer.URL + "/v2" + mutatingWebhook := &admissionregistrationv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "admission.integration.test", + }, + Webhooks: []admissionregistrationv1.MutatingWebhook{ + { + Name: "admission.integration.test.v2", + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"awesome.example.com"}, + APIVersions: []string{"v2"}, + Resources: []string{"*/*"}, + }, + }}, + MatchPolicy: &equivalent, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: &v2Endpoint, + CABundle: localhostCert, + }, + FailurePolicy: &ignore, + SideEffects: &noSideEffects, + AdmissionReviewVersions: []string{"v1"}, + MatchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "test-v2", + Expression: "object.apiVersion == 'awesome.example.com/v2'", + }, + }, + }, + { + Name: "admission.integration.test", + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"awesome.example.com"}, + APIVersions: []string{"v1"}, + Resources: []string{"*/*"}, + }, + }}, + MatchPolicy: &equivalent, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: &v1Endpoint, + CABundle: localhostCert, + }, + SideEffects: &noSideEffects, + AdmissionReviewVersions: []string{"v1"}, + MatchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "test-v1", + Expression: "object.apiVersion == 'awesome.example.com/v1'", + }, + }, + }, + { + Name: "admission.integration.test.marker", + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, + Rule: admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}}, + }}, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: &markerEndpoint, + CABundle: localhostCert, + }, + NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{ + corev1.LabelMetadataName: "marker", + }}, + ObjectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}}, + SideEffects: &noSideEffects, + AdmissionReviewVersions: []string{"v1"}, + }, + }, + } + + mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), mutatingWebhook, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + defer func() { + err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{}) + if err != nil { + t.Fatal(err) + } + }() + + // wait until new webhook is called the first time + if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) { + _, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{}) + select { + case <-upCh: + return true, nil + default: + t.Logf("Waiting for webhook to become effective, getting marker object: %v", err) + return false, nil + } + }); err != nil { + t.Fatal(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + v1Resource := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "awesome.example.com" + "/" + "v1", + "kind": "Panda", + "metadata": map[string]interface{}{ + "name": "v1-bears", + }, + }, + } + + v2Resource := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "awesome.example.com" + "/" + "v2", + "kind": "Panda", + "metadata": map[string]interface{}{ + "name": "v2-bears", + }, + }, + } + + _, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.example.com", Version: "v1", Resource: "pandas"}).Create(context.TODO(), v1Resource, metav1.CreateOptions{}) + if err != nil { + t.Errorf("error1 %v", err.Error()) + } + + _, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.example.com", Version: "v2", Resource: "pandas"}).Create(context.TODO(), v2Resource, metav1.CreateOptions{}) + if err != nil { + t.Errorf("error2 %v", err.Error()) + } + + if len(typeChecker.requests) != 4 { + t.Errorf("expected 4 request got %v", len(typeChecker.requests)) + } +} + +func newMarkerPodGVKConversion(namespace string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "marker", + Labels: map[string]string{ + "marker": "true", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "fake-name", + Image: "fakeimage", + }}, + }, + } +} + +// Copied from etcd.GetCustomResourceDefinitionData +func versionedCustomResourceDefinition() *apiextensionsv1.CustomResourceDefinition { + return &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pandas.awesome.example.com", + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "awesome.example.com", + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1", + Served: true, + Storage: true, + Schema: fixtures.AllowAllSchema(), + Subresources: &apiextensionsv1.CustomResourceSubresources{ + Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, + Scale: &apiextensionsv1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: func() *string { path := ".status.selector"; return &path }(), + }, + }, + }, + { + Name: "v2", + Served: true, + Storage: false, + Schema: fixtures.AllowAllSchema(), + Subresources: &apiextensionsv1.CustomResourceSubresources{ + Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, + Scale: &apiextensionsv1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: func() *string { path := ".status.selector"; return &path }(), + }, + }, + }, + }, + Scope: apiextensionsv1.ClusterScoped, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "pandas", + Kind: "Panda", + }, + }, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e84d985d32c..00ef84b5ed9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1413,6 +1413,7 @@ k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alp k8s.io/apiserver/pkg/admission/plugin/webhook/errors k8s.io/apiserver/pkg/admission/plugin/webhook/generic k8s.io/apiserver/pkg/admission/plugin/webhook/initializer +k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions k8s.io/apiserver/pkg/admission/plugin/webhook/mutating k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object