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". 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". 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". 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 +}