diff --git a/api/policies/v1/admissionpolicy_types.go b/api/policies/v1/admissionpolicy_types.go
index d285987a..97df5b1a 100644
--- a/api/policies/v1/admissionpolicy_types.go
+++ b/api/policies/v1/admissionpolicy_types.go
@@ -122,6 +122,10 @@ func (r *AdmissionPolicy) GetMatchPolicy() *admissionregistrationv1.MatchPolicyT
return r.Spec.MatchPolicy
}
+func (r *AdmissionPolicy) GetMatchConditions() []admissionregistrationv1.MatchCondition {
+ return r.Spec.MatchConditions
+}
+
// GetNamespaceSelector returns the namespace of the AdmissionPolicy since it is the only namespace we want the policy to be applied to.
func (r *AdmissionPolicy) GetUpdatedNamespaceSelector(string) *metav1.LabelSelector {
return &metav1.LabelSelector{
diff --git a/api/policies/v1/clusteradmissionpolicy_types.go b/api/policies/v1/clusteradmissionpolicy_types.go
index ed7a106f..f89fd23b 100644
--- a/api/policies/v1/clusteradmissionpolicy_types.go
+++ b/api/policies/v1/clusteradmissionpolicy_types.go
@@ -176,6 +176,10 @@ func (r *ClusterAdmissionPolicy) GetRules() []admissionregistrationv1.RuleWithOp
return r.Spec.Rules
}
+func (r *ClusterAdmissionPolicy) GetMatchConditions() []admissionregistrationv1.MatchCondition {
+ return r.Spec.MatchConditions
+}
+
func (r *ClusterAdmissionPolicy) GetUpdatedNamespaceSelector(deploymentNamespace string) *metav1.LabelSelector {
// exclude namespace where kubewarden was deployed
if r.Spec.NamespaceSelector != nil {
diff --git a/api/policies/v1/policy.go b/api/policies/v1/policy.go
index 8d2e4613..c107a067 100644
--- a/api/policies/v1/policy.go
+++ b/api/policies/v1/policy.go
@@ -98,6 +98,7 @@ type Policy interface {
GetRules() []admissionregistrationv1.RuleWithOperations
GetFailurePolicy() *admissionregistrationv1.FailurePolicyType
GetMatchPolicy() *admissionregistrationv1.MatchPolicyType
+ GetMatchConditions() []admissionregistrationv1.MatchCondition
GetUpdatedNamespaceSelector(deploymentNamespace string) *metav1.LabelSelector
GetObjectSelector() *metav1.LabelSelector
GetTimeoutSeconds() *int32
diff --git a/api/policies/v1/policy_types.go b/api/policies/v1/policy_types.go
index 8c702b4b..442be7b6 100644
--- a/api/policies/v1/policy_types.go
+++ b/api/policies/v1/policy_types.go
@@ -104,6 +104,21 @@ type PolicySpec struct {
// +optional
MatchPolicy *admissionregistrationv1.MatchPolicyType `json:"matchPolicy,omitempty"`
+ // MatchConditions is a list of conditions that must be met for a request to be
+ // validated. 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. If a parameter object is provided, it can be accessed via
+ // the `params` handle in the same manner as validation expressions. The exact
+ // matching logic is (in order): 1. If ANY matchCondition evaluates to FALSE,
+ // the policy is skipped. 2. If ALL matchConditions evaluate to TRUE, the policy
+ // is evaluated. 3. If any matchCondition evaluates to an error (but none are
+ // FALSE): - If failurePolicy=Fail, reject the request - If
+ // failurePolicy=Ignore, the policy is skipped
+ // Only available if the feature gate AdmissionWebhookMatchConditions is enabled.
+ // +optional
+ MatchConditions []admissionregistrationv1.MatchCondition `json:"matchConditions,omitempty"`
+
// ObjectSelector decides whether to run the webhook based on if the
// object has matching labels. objectSelector is evaluated against both
// the oldObject and newObject that would be sent to the webhook, and
diff --git a/api/policies/v1/zz_generated.deepcopy.go b/api/policies/v1/zz_generated.deepcopy.go
index f5ae0a76..275e4684 100644
--- a/api/policies/v1/zz_generated.deepcopy.go
+++ b/api/policies/v1/zz_generated.deepcopy.go
@@ -413,6 +413,11 @@ func (in *PolicySpec) DeepCopyInto(out *PolicySpec) {
*out = new(admissionregistrationv1.MatchPolicyType)
**out = **in
}
+ if in.MatchConditions != nil {
+ in, out := &in.MatchConditions, &out.MatchConditions
+ *out = make([]admissionregistrationv1.MatchCondition, len(*in))
+ copy(*out, *in)
+ }
if in.ObjectSelector != nil {
in, out := &in.ObjectSelector, &out.ObjectSelector
*out = new(metav1.LabelSelector)
diff --git a/cmd/main.go b/cmd/main.go
index dd0373a4..2f71b738 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -45,6 +45,7 @@ import (
"github.com/kubewarden/kubewarden-controller/api/policies/v1alpha2"
"github.com/kubewarden/kubewarden-controller/internal/constants"
"github.com/kubewarden/kubewarden-controller/internal/controller"
+ "github.com/kubewarden/kubewarden-controller/internal/featuregates"
"github.com/kubewarden/kubewarden-controller/internal/metrics"
//+kubebuilder:scaffold:imports
)
@@ -129,7 +130,14 @@ func main() {
return
}
- if err = setupReconcilers(mgr, deploymentsNamespace, enableMetrics, enableTracing, alwaysAcceptAdmissionReviewsOnDeploymentsNamespace); err != nil {
+ featureGateAdmissionWebhookMatchConditions, err := featuregates.CheckAdmissionWebhookMatchConditions(ctrl.GetConfigOrDie())
+ if err != nil {
+ setupLog.Error(err, "unable to check for feature gate AdmissionWebhookMatchConditions")
+ retcode = 1
+ return
+ }
+
+ if err = setupReconcilers(mgr, deploymentsNamespace, enableMetrics, enableTracing, alwaysAcceptAdmissionReviewsOnDeploymentsNamespace, featureGateAdmissionWebhookMatchConditions); err != nil {
setupLog.Error(err, "unable to create controllers")
retcode = 1
return
@@ -217,7 +225,7 @@ func setupProbes(mgr ctrl.Manager) error {
return nil
}
-func setupReconcilers(mgr ctrl.Manager, deploymentsNamespace string, enableMetrics, enableTracing, alwaysAcceptAdmissionReviewsOnDeploymentsNamespace bool) error {
+func setupReconcilers(mgr ctrl.Manager, deploymentsNamespace string, enableMetrics, enableTracing, alwaysAcceptAdmissionReviewsOnDeploymentsNamespace bool, featureGateAdmissionWebhookMatchConditions bool) error {
if err := (&controller.PolicyServerReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
@@ -235,6 +243,7 @@ func setupReconcilers(mgr ctrl.Manager, deploymentsNamespace string, enableMetri
Scheme: mgr.GetScheme(),
Log: ctrl.Log.WithName("admission-policy-reconciler"),
DeploymentsNamespace: deploymentsNamespace,
+ FeatureGateAdmissionWebhookMatchConditions: featureGateAdmissionWebhookMatchConditions,
}).SetupWithManager(mgr); err != nil {
return errors.Join(errors.New("unable to create AdmissionPolicy controller"), err)
}
@@ -244,6 +253,7 @@ func setupReconcilers(mgr ctrl.Manager, deploymentsNamespace string, enableMetri
Scheme: mgr.GetScheme(),
Log: ctrl.Log.WithName("cluster-admission-policy-reconciler"),
DeploymentsNamespace: deploymentsNamespace,
+ FeatureGateAdmissionWebhookMatchConditions: featureGateAdmissionWebhookMatchConditions,
}).SetupWithManager(mgr); err != nil {
return errors.Join(errors.New("unable to create ClusterAdmissionPolicy controller"), err)
}
diff --git a/config/crd/bases/policies.kubewarden.io_admissionpolicies.yaml b/config/crd/bases/policies.kubewarden.io_admissionpolicies.yaml
index 592c1f71..ffb021fa 100644
--- a/config/crd/bases/policies.kubewarden.io_admissionpolicies.yaml
+++ b/config/crd/bases/policies.kubewarden.io_admissionpolicies.yaml
@@ -93,6 +93,60 @@ spec:
fail and the API request to be rejected.
The default behaviour is "Fail"
type: string
+ matchConditions:
+ description: |-
+ MatchConditions is a list of conditions that must be met for a request to be
+ validated. 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. If a parameter object is provided, it can be accessed via
+ the `params` handle in the same manner as validation expressions. The exact
+ matching logic is (in order): 1. If ANY matchCondition evaluates to FALSE,
+ the policy is skipped. 2. If ALL matchConditions evaluate to TRUE, the policy
+ is evaluated. 3. If any matchCondition evaluates to an error (but none are
+ FALSE): - If failurePolicy=Fail, reject the request - If
+ failurePolicy=Ignore, the policy is skipped
+ Only available if the feature gate AdmissionWebhookMatchConditions is enabled.
+ items:
+ 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:
+
+
+ '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.
+ 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')
+
+
+ Required.
+ type: string
+ required:
+ - expression
+ - name
+ type: object
+ type: array
matchPolicy:
description: |-
matchPolicy defines how the "rules" list is used to match incoming requests.
diff --git a/config/crd/bases/policies.kubewarden.io_clusteradmissionpolicies.yaml b/config/crd/bases/policies.kubewarden.io_clusteradmissionpolicies.yaml
index 148d3fe5..93422a53 100644
--- a/config/crd/bases/policies.kubewarden.io_clusteradmissionpolicies.yaml
+++ b/config/crd/bases/policies.kubewarden.io_clusteradmissionpolicies.yaml
@@ -114,6 +114,60 @@ spec:
fail and the API request to be rejected.
The default behaviour is "Fail"
type: string
+ matchConditions:
+ description: |-
+ MatchConditions is a list of conditions that must be met for a request to be
+ validated. 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. If a parameter object is provided, it can be accessed via
+ the `params` handle in the same manner as validation expressions. The exact
+ matching logic is (in order): 1. If ANY matchCondition evaluates to FALSE,
+ the policy is skipped. 2. If ALL matchConditions evaluate to TRUE, the policy
+ is evaluated. 3. If any matchCondition evaluates to an error (but none are
+ FALSE): - If failurePolicy=Fail, reject the request - If
+ failurePolicy=Ignore, the policy is skipped
+ Only available if the feature gate AdmissionWebhookMatchConditions is enabled.
+ items:
+ 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:
+
+
+ '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.
+ 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')
+
+
+ Required.
+ type: string
+ required:
+ - expression
+ - name
+ type: object
+ type: array
matchPolicy:
description: |-
matchPolicy defines how the "rules" list is used to match incoming requests.
diff --git a/docs/crds/CRD-docs-for-docs-repo.md b/docs/crds/CRD-docs-for-docs-repo.md
index 64747cea..16c69a24 100644
--- a/docs/crds/CRD-docs-for-docs-repo.md
+++ b/docs/crds/CRD-docs-for-docs-repo.md
@@ -74,6 +74,7 @@ _Appears in:_
| `mutating` _boolean_ | Mutating indicates whether a policy has the ability to mutate incoming requests or not. |
| `backgroundAudit` _boolean_ | BackgroundAudit indicates whether a policy should be used or skipped when performing audit checks. If false, the policy cannot produce meaningful evaluation results during audit checks and will be skipped. The default is "true". |
| `matchPolicy` _[MatchPolicyType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchpolicytype-v1-admissionregistration)_ | matchPolicy defines how the "rules" list is used to match incoming requests. Allowed values are "Exact" or "Equivalent".
- Exact: match a request only if it exactly matches a specified rule. For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the webhook.
- Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the webhook.
Defaults to "Equivalent" |
+| `matchConditions` _[MatchCondition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchcondition-v1-admissionregistration) array_ | MatchConditions is a list of conditions that must be met for a request to be validated. 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. If a parameter object is provided, it can be accessed via the `params` handle in the same manner as validation expressions. The exact matching logic is (in order): 1. If ANY matchCondition evaluates to FALSE, the policy is skipped. 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated. 3. If any matchCondition evaluates to an error (but none are FALSE): - If failurePolicy=Fail, reject the request - If failurePolicy=Ignore, the policy is skipped Only available if the feature gate AdmissionWebhookMatchConditions is enabled. |
| `objectSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#labelselector-v1-meta)_ | ObjectSelector decides whether to run the webhook based on if the object has matching labels. objectSelector is evaluated against both the oldObject and newObject that would be sent to the webhook, and is considered to match if either object matches the selector. A null object (oldObject in the case of create, or newObject in the case of delete) or an object that cannot have labels (like a DeploymentRollback or a PodProxyOptions object) is not considered to match. Use the object selector only if the webhook is opt-in, because end users may skip the admission webhook by setting the labels. Default to the empty LabelSelector, which matches everything. |
| `sideEffects` _[SideEffectClass](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#sideeffectclass-v1-admissionregistration)_ | 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 change and the side effects therefore need to be undone. Requests with the dryRun attribute will be auto-rejected if they match a webhook with sideEffects == Unknown or Some. |
| `timeoutSeconds` _integer_ | 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. |
@@ -132,6 +133,7 @@ _Appears in:_
| `mutating` _boolean_ | Mutating indicates whether a policy has the ability to mutate incoming requests or not. |
| `backgroundAudit` _boolean_ | BackgroundAudit indicates whether a policy should be used or skipped when performing audit checks. If false, the policy cannot produce meaningful evaluation results during audit checks and will be skipped. The default is "true". |
| `matchPolicy` _[MatchPolicyType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchpolicytype-v1-admissionregistration)_ | matchPolicy defines how the "rules" list is used to match incoming requests. Allowed values are "Exact" or "Equivalent". - Exact: match a request only if it exactly matches a specified rule. For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the webhook.
- Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the webhook.
Defaults to "Equivalent" |
+| `matchConditions` _[MatchCondition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchcondition-v1-admissionregistration) array_ | MatchConditions is a list of conditions that must be met for a request to be validated. 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. If a parameter object is provided, it can be accessed via the `params` handle in the same manner as validation expressions. The exact matching logic is (in order): 1. If ANY matchCondition evaluates to FALSE, the policy is skipped. 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated. 3. If any matchCondition evaluates to an error (but none are FALSE): - If failurePolicy=Fail, reject the request - If failurePolicy=Ignore, the policy is skipped Only available if the feature gate AdmissionWebhookMatchConditions is enabled. |
| `objectSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#labelselector-v1-meta)_ | ObjectSelector decides whether to run the webhook based on if the object has matching labels. objectSelector is evaluated against both the oldObject and newObject that would be sent to the webhook, and is considered to match if either object matches the selector. A null object (oldObject in the case of create, or newObject in the case of delete) or an object that cannot have labels (like a DeploymentRollback or a PodProxyOptions object) is not considered to match. Use the object selector only if the webhook is opt-in, because end users may skip the admission webhook by setting the labels. Default to the empty LabelSelector, which matches everything. |
| `sideEffects` _[SideEffectClass](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#sideeffectclass-v1-admissionregistration)_ | 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 change and the side effects therefore need to be undone. Requests with the dryRun attribute will be auto-rejected if they match a webhook with sideEffects == Unknown or Some. |
| `timeoutSeconds` _integer_ | 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. |
@@ -241,8 +243,8 @@ _Appears in:_
| --- | --- |
| `image` _string_ | Docker image name. |
| `replicas` _integer_ | Replicas is the number of desired replicas. |
-| `minAvailable` _IntOrString_ | Number of policy server replicas that must be still available after the eviction |
-| `maxUnavailable` _IntOrString_ | Number of policy server replicas that can be unavailable after the eviction |
+| `minAvailable` _IntOrString_ | Number of policy server replicas that must be still available after the eviction. The value can be an absolute number or a percentage. Only one of MinAvailable or Max MaxUnavailable can be set. |
+| `maxUnavailable` _IntOrString_ | Number of policy server replicas that can be unavailable after the eviction. The value can be an absolute number or a percentage. Only one of MinAvailable or Max MaxUnavailable can be set. |
| `annotations` _object (keys:string, values:string)_ | Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations |
| `env` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#envvar-v1-core) array_ | List of environment variables to set in the container. |
| `serviceAccountName` _string_ | Name of the service account associated with the policy server. Namespace service account will be used if not specified. |
@@ -254,6 +256,7 @@ _Appears in:_
| `affinity` _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#affinity-v1-core)_ | Affinity rules for the associated Policy Server pods. |
| `limits` _object (keys:[ResourceName](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#resourcename-v1-core), values:Quantity)_ | Limits describes the maximum amount of compute resources allowed. |
| `requests` _object (keys:[ResourceName](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#resourcename-v1-core), values:Quantity)_ | Requests describes the minimum amount of compute resources required. If Request is omitted for, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value |
+| `tolerations` _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#toleration-v1-core) array_ | Tolerations describes the policy server pod's tolerations. It can be user to ensure that the policy server pod is not scheduled onto a node with a taint. |
@@ -279,6 +282,7 @@ _Appears in:_
| `mutating` _boolean_ | Mutating indicates whether a policy has the ability to mutate incoming requests or not. |
| `backgroundAudit` _boolean_ | BackgroundAudit indicates whether a policy should be used or skipped when performing audit checks. If false, the policy cannot produce meaningful evaluation results during audit checks and will be skipped. The default is "true". |
| `matchPolicy` _[MatchPolicyType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchpolicytype-v1-admissionregistration)_ | matchPolicy defines how the "rules" list is used to match incoming requests. Allowed values are "Exact" or "Equivalent". - Exact: match a request only if it exactly matches a specified rule. For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, but "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, a request to apps/v1beta1 or extensions/v1beta1 would not be sent to the webhook.
- Equivalent: match a request if modifies a resource listed in rules, even via another API group or version. For example, if deployments can be modified via apps/v1, apps/v1beta1, and extensions/v1beta1, and "rules" only included `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]`, a request to apps/v1beta1 or extensions/v1beta1 would be converted to apps/v1 and sent to the webhook.
Defaults to "Equivalent" |
+| `matchConditions` _[MatchCondition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchcondition-v1-admissionregistration) array_ | MatchConditions is a list of conditions that must be met for a request to be validated. 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. If a parameter object is provided, it can be accessed via the `params` handle in the same manner as validation expressions. The exact matching logic is (in order): 1. If ANY matchCondition evaluates to FALSE, the policy is skipped. 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated. 3. If any matchCondition evaluates to an error (but none are FALSE): - If failurePolicy=Fail, reject the request - If failurePolicy=Ignore, the policy is skipped Only available if the feature gate AdmissionWebhookMatchConditions is enabled. |
| `objectSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#labelselector-v1-meta)_ | ObjectSelector decides whether to run the webhook based on if the object has matching labels. objectSelector is evaluated against both the oldObject and newObject that would be sent to the webhook, and is considered to match if either object matches the selector. A null object (oldObject in the case of create, or newObject in the case of delete) or an object that cannot have labels (like a DeploymentRollback or a PodProxyOptions object) is not considered to match. Use the object selector only if the webhook is opt-in, because end users may skip the admission webhook by setting the labels. Default to the empty LabelSelector, which matches everything. |
| `sideEffects` _[SideEffectClass](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#sideeffectclass-v1-admissionregistration)_ | 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 change and the side effects therefore need to be undone. Requests with the dryRun attribute will be auto-rejected if they match a webhook with sideEffects == Unknown or Some. |
| `timeoutSeconds` _integer_ | 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. |
diff --git a/internal/controller/admissionpolicy_controller.go b/internal/controller/admissionpolicy_controller.go
index e2159eeb..a0b7b24a 100644
--- a/internal/controller/admissionpolicy_controller.go
+++ b/internal/controller/admissionpolicy_controller.go
@@ -50,10 +50,11 @@ import (
// AdmissionPolicyReconciler reconciles an AdmissionPolicy object.
type AdmissionPolicyReconciler struct {
client.Client
- Log logr.Logger
- Scheme *runtime.Scheme
- DeploymentsNamespace string
- policySubReconciler *policySubReconciler
+ Log logr.Logger
+ Scheme *runtime.Scheme
+ DeploymentsNamespace string
+ FeatureGateAdmissionWebhookMatchConditions bool
+ policySubReconciler *policySubReconciler
}
// Reconcile reconciles admission policies.
@@ -70,7 +71,9 @@ func (r *AdmissionPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Requ
func (r *AdmissionPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.policySubReconciler = &policySubReconciler{
r.Client,
+ r.Log,
r.DeploymentsNamespace,
+ r.FeatureGateAdmissionWebhookMatchConditions,
}
err := ctrl.NewControllerManagedBy(mgr).
diff --git a/internal/controller/admissionpolicy_controller_test.go b/internal/controller/admissionpolicy_controller_test.go
index 89f7f19c..6359f08b 100644
--- a/internal/controller/admissionpolicy_controller_test.go
+++ b/internal/controller/admissionpolicy_controller_test.go
@@ -88,6 +88,7 @@ var _ = Describe("AdmissionPolicy controller", Label("real-cluster"), func() {
Expect(validatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNamespaceAnnotationKey]).To(Equal(policyNamespace))
Expect(validatingWebhookConfiguration.Webhooks).To(HaveLen(1))
Expect(validatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Name).To(Equal("policy-server-" + policyServerName))
+ Expect(validatingWebhookConfiguration.Webhooks[0].MatchConditions).To(HaveLen(1))
caSecret, err := getTestCASecret(ctx)
Expect(err).ToNot(HaveOccurred())
@@ -199,6 +200,7 @@ var _ = Describe("AdmissionPolicy controller", Label("real-cluster"), func() {
Expect(mutatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNamespaceAnnotationKey]).To(Equal(policyNamespace))
Expect(mutatingWebhookConfiguration.Webhooks).To(HaveLen(1))
Expect(mutatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Name).To(Equal("policy-server-" + policyServerName))
+ Expect(mutatingWebhookConfiguration.Webhooks[0].MatchConditions).To(HaveLen(1))
caSecret, err := getTestCASecret(ctx)
Expect(err).ToNot(HaveOccurred())
diff --git a/internal/controller/clusteradmissionpolicy_controller.go b/internal/controller/clusteradmissionpolicy_controller.go
index 0e5851d2..b503db74 100644
--- a/internal/controller/clusteradmissionpolicy_controller.go
+++ b/internal/controller/clusteradmissionpolicy_controller.go
@@ -50,10 +50,11 @@ import (
// ClusterAdmissionPolicyReconciler reconciles a ClusterAdmissionPolicy object.
type ClusterAdmissionPolicyReconciler struct {
client.Client
- Log logr.Logger
- Scheme *runtime.Scheme
- DeploymentsNamespace string
- policySubReconciler *policySubReconciler
+ Log logr.Logger
+ Scheme *runtime.Scheme
+ DeploymentsNamespace string
+ FeatureGateAdmissionWebhookMatchConditions bool
+ policySubReconciler *policySubReconciler
}
// Reconcile reconciles admission policies.
@@ -70,7 +71,9 @@ func (r *ClusterAdmissionPolicyReconciler) Reconcile(ctx context.Context, req ct
func (r *ClusterAdmissionPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.policySubReconciler = &policySubReconciler{
r.Client,
+ r.Log,
r.DeploymentsNamespace,
+ r.FeatureGateAdmissionWebhookMatchConditions,
}
err := ctrl.NewControllerManagedBy(mgr).
diff --git a/internal/controller/clusteradmissionpolicy_controller_test.go b/internal/controller/clusteradmissionpolicy_controller_test.go
index 5585addc..ed6ddfe1 100644
--- a/internal/controller/clusteradmissionpolicy_controller_test.go
+++ b/internal/controller/clusteradmissionpolicy_controller_test.go
@@ -74,6 +74,7 @@ var _ = Describe("ClusterAdmissionPolicy controller", Label("real-cluster"), fun
Expect(validatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNamespaceAnnotationKey]).To(BeEmpty())
Expect(validatingWebhookConfiguration.Webhooks).To(HaveLen(1))
Expect(validatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Name).To(Equal("policy-server-" + policyServerName))
+ Expect(validatingWebhookConfiguration.Webhooks[0].MatchConditions).To(HaveLen(1))
caSecret, err := getTestCASecret(ctx)
Expect(err).ToNot(HaveOccurred())
@@ -162,6 +163,7 @@ var _ = Describe("ClusterAdmissionPolicy controller", Label("real-cluster"), fun
Expect(mutatingWebhookConfiguration.Annotations[constants.WebhookConfigurationPolicyNamespaceAnnotationKey]).To(BeEmpty())
Expect(mutatingWebhookConfiguration.Webhooks).To(HaveLen(1))
Expect(mutatingWebhookConfiguration.Webhooks[0].ClientConfig.Service.Name).To(Equal("policy-server-" + policyServerName))
+ Expect(mutatingWebhookConfiguration.Webhooks[0].MatchConditions).To(HaveLen(1))
caSecret, err := getTestCASecret(ctx)
Expect(err).ToNot(HaveOccurred())
diff --git a/internal/controller/policy_subreconciler.go b/internal/controller/policy_subreconciler.go
index 8fb8bfce..39063f20 100644
--- a/internal/controller/policy_subreconciler.go
+++ b/internal/controller/policy_subreconciler.go
@@ -32,6 +32,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "github.com/go-logr/logr"
policiesv1 "github.com/kubewarden/kubewarden-controller/api/policies/v1"
"github.com/kubewarden/kubewarden-controller/internal/constants"
"github.com/kubewarden/kubewarden-controller/internal/metrics"
@@ -39,7 +40,9 @@ import (
type policySubReconciler struct {
client.Client
- deploymentsNamespace string
+ Log logr.Logger
+ deploymentsNamespace string
+ featureGateAdmissionWebhookMatchConditions bool
}
func (r *policySubReconciler) reconcile(ctx context.Context, policy policiesv1.Policy) (ctrl.Result, error) {
@@ -83,7 +86,7 @@ func (r *policySubReconciler) reconcilePolicy(ctx context.Context, policy polici
policyServer, err := r.getPolicyServer(ctx, policy)
if err != nil {
policy.SetStatus(policiesv1.PolicyStatusScheduled)
- //lint:ignore nilerr set status to scheduled if policyServer can't be retrieved, and stop reconciling
+ //nolint:nilerr // set status to scheduled if policyServer can't be retrieved, and stop reconciling
return ctrl.Result{}, nil
}
if policy.GetStatus().PolicyStatus != policiesv1.PolicyStatusActive {
diff --git a/internal/controller/policy_subreconciler_webhook.go b/internal/controller/policy_subreconciler_webhook.go
index 1444ac5a..9f04624f 100644
--- a/internal/controller/policy_subreconciler_webhook.go
+++ b/internal/controller/policy_subreconciler_webhook.go
@@ -77,6 +77,12 @@ func (r *policySubReconciler) reconcileValidatingWebhookConfiguration(
AdmissionReviewVersions: []string{"v1"},
},
}
+ if r.featureGateAdmissionWebhookMatchConditions {
+ webhook.Webhooks[0].MatchConditions = policy.GetMatchConditions()
+ } else if len(policy.GetMatchConditions()) > 0 {
+ r.Log.Info("Skipping matchConditions for policy as the feature gate AdmissionWebhookMatchConditions is disabled",
+ "policy", policy.GetName())
+ }
return nil
})
if err != nil {
@@ -162,6 +168,12 @@ func (r *policySubReconciler) reconcileMutatingWebhookConfiguration(
AdmissionReviewVersions: []string{"v1"},
},
}
+ if r.featureGateAdmissionWebhookMatchConditions {
+ webhook.Webhooks[0].MatchConditions = policy.GetMatchConditions()
+ } else if len(policy.GetMatchConditions()) > 0 {
+ r.Log.Info("Skipping matchConditions for policy as the feature gate AdmissionWebhookMatchConditions is disabled",
+ "policy", policy.GetName())
+ }
return nil
})
if err != nil {
diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go
index 8c34ff67..df402cb6 100644
--- a/internal/controller/suite_test.go
+++ b/internal/controller/suite_test.go
@@ -117,6 +117,7 @@ var _ = SynchronizedBeforeSuite(func() []byte {
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
DeploymentsNamespace: deploymentsNamespace,
+ FeatureGateAdmissionWebhookMatchConditions: true,
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())
@@ -124,6 +125,7 @@ var _ = SynchronizedBeforeSuite(func() []byte {
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
DeploymentsNamespace: deploymentsNamespace,
+ FeatureGateAdmissionWebhookMatchConditions: true,
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())
diff --git a/internal/controller/utils_test.go b/internal/controller/utils_test.go
index 38e04f0d..452d0451 100644
--- a/internal/controller/utils_test.go
+++ b/internal/controller/utils_test.go
@@ -97,6 +97,12 @@ func admissionPolicyFactory(name, policyNamespace, policyServerName string, muta
admissionPolicy.Namespace = policyNamespace
admissionPolicy.Spec.PolicyServer = policyServerName
admissionPolicy.Spec.PolicySpec.Mutating = mutating
+ admissionPolicy.Spec.PolicySpec.MatchConditions = []admissionregistrationv1.MatchCondition{
+ {
+ Name: "noop",
+ Expression: "true",
+ },
+ }
admissionPolicy.Finalizers = []string{
// On a real cluster the Kubewarden finalizer is added by our mutating
// webhook. This is not running now, hence we have to manually add the finalizer
@@ -114,6 +120,12 @@ func clusterAdmissionPolicyFactory(name, policyServerName string, mutating bool)
clusterAdmissionPolicy.Name = name
clusterAdmissionPolicy.Spec.PolicyServer = policyServerName
clusterAdmissionPolicy.Spec.PolicySpec.Mutating = mutating
+ clusterAdmissionPolicy.Spec.PolicySpec.MatchConditions = []admissionregistrationv1.MatchCondition{
+ {
+ Name: "noop",
+ Expression: "true",
+ },
+ }
clusterAdmissionPolicy.Finalizers = []string{
// On a real cluster the Kubewarden finalizer is added by our mutating
// webhook. This is not running now, hence we have to manually add the finalizer
diff --git a/internal/featuregates/featuregates.go b/internal/featuregates/featuregates.go
new file mode 100644
index 00000000..2f7d63b7
--- /dev/null
+++ b/internal/featuregates/featuregates.go
@@ -0,0 +1,46 @@
+package featuregates
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "k8s.io/client-go/discovery"
+ "k8s.io/client-go/rest"
+
+ "k8s.io/apimachinery/pkg/runtime"
+)
+
+// CheckAdmissionWebhookMatchConditions returns true if the feature gate
+// AdmissionWebhookMatchConditions is activated. It does this by fetching the
+// OpenAPIV3 schema from the discovery client and checking for the feature
+// gate. This feature is stable since Kubernetes v1.30.
+func CheckAdmissionWebhookMatchConditions(config *rest.Config) (bool, error) {
+ // Obtain openAPIV3 client from discoveryClient
+ apiClient := discovery.NewDiscoveryClientForConfigOrDie(config).OpenAPIV3()
+ paths, err := apiClient.Paths()
+ if err != nil {
+ return false, fmt.Errorf("failed to fetch OpenAPI spec: %w", err)
+ }
+
+ // Check for the feature gate AdmissionWebhookMatchConditions by looking at
+ // the path `apis/admissionregistration.k8s.io/v1`, under
+ // `components.schemas.io.k8s.api.admissionregistration.v1.ValidatingWebhook`.
+ resourcePath := "apis/admissionregistration.k8s.io/v1"
+ groupVersion, exists := paths[resourcePath]
+ if !exists {
+ return false, fmt.Errorf("couldn't find resource for \"%v\"", resourcePath)
+ }
+ openAPISchemaBytes, err := groupVersion.Schema(runtime.ContentTypeJSON)
+ if err != nil {
+ return false, fmt.Errorf("failed to fetch openapi schema for %s: %w", resourcePath, err)
+ }
+ var parsedV3Schema map[string]interface{}
+ if err = json.Unmarshal(openAPISchemaBytes, &parsedV3Schema); err != nil {
+ return false, fmt.Errorf("failed to unmarshal openapi schema for %s: %w", resourcePath, err)
+ }
+ schemas := parsedV3Schema["components"].(map[string]interface{})["schemas"]
+ validatingWebhook := schemas.(map[string]interface{})["io.k8s.api.admissionregistration.v1.ValidatingWebhook"]
+ _, exists = validatingWebhook.(map[string]interface{})["properties"].(map[string]interface{})["matchConditions"]
+
+ return exists, nil
+}