diff --git a/chart/crds/crds.yaml b/chart/crds/crds.yaml index e27d9eba..3f4ef07f 100644 --- a/chart/crds/crds.yaml +++ b/chart/crds/crds.yaml @@ -208,6 +208,13 @@ spec: description: OperationType specifies an operation for a request. type: string type: array + reinvocationPolicy: + description: To allow mutating admission plugins to observe changes + made by other plugins, built-in mutating admission plugins are re-run + if a mutating webhook modifies an object, and mutating webhooks + can specify a reinvocationPolicy to control whether they are reinvoked + as well. + type: string resources: description: "Resources is a list of resources this rule applies to. \n For example: 'pods' means pods. 'pods/log' means the log subresource diff --git a/docs/pages/getting-started/work-with-policies.mdx b/docs/pages/getting-started/work-with-policies.mdx index c00674a1..8cec8b8f 100644 --- a/docs/pages/getting-started/work-with-policies.mdx +++ b/docs/pages/getting-started/work-with-policies.mdx @@ -65,6 +65,7 @@ The following options may be configured to specify when a particular policy shou Within the `spec` of a `JsPolicy` object, you can also define certain settings that are relevant during the execution of a policy: - `violationPolicy`: `deny` (default) or `warn` (for testing) when the policy logic calls the `deny()` function - `failurePolicy`: `Fail` (default) or `Ignore` when jsPolicy fails to execute the policy or it aborts with a runtime error +- `reinvocationPolicy`: [Reinvocation Policy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) defines whether JSPolicy is called again as part of the admission evaluation if the object being admitted is modified by other admission plugins after the initial webhook call (`IfNeeded`) or not (`Never`, *default*) - `auditPolicy`: `Log` (default) or `Skip` logging any policy violations (requests that lead to `deny()`) in the status of this policy - `auditLogSize`: Maximum number of violations that should be stored in the status of this policy (default: `10` violations) - `timeoutSeconds`: Maximum number of seconds that the execution of the policy logic may take before jsPolicy aborts the policy execution (default: `10` seconds, maximum is `30`) diff --git a/docs/pages/reference/policy-crd.mdx b/docs/pages/reference/policy-crd.mdx index 20bef339..117ea12f 100644 --- a/docs/pages/reference/policy-crd.mdx +++ b/docs/pages/reference/policy-crd.mdx @@ -20,9 +20,10 @@ spec: matchPolicy: "Equivalent" violationPolicy: "deny" failurePolicy: "" + reinvocationPolicy: "Never" auditPolicy: "" auditLogSize: 10 - dependencies: + dependencies: "@jspolicy/package-1": "^1.0.0" "@jspolicy/package-2": "~2.0.0" javascript: "/* JS CODE HERE */" diff --git a/docs/pages/using-policies/policy-types.mdx b/docs/pages/using-policies/policy-types.mdx index ceb0407d..ee69834f 100644 --- a/docs/pages/using-policies/policy-types.mdx +++ b/docs/pages/using-policies/policy-types.mdx @@ -20,6 +20,13 @@ Possible examples for mutating policies would be: Since mutating policies change the request's object, Kubernetes executes them sequentially, so that they do not interfere with each other. +Policy can specify a [Reinvocation Policy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) to allow JSPolicy admission plugin to observe changes made by other admission plugins after the initial mutating webhook call. + +`reinvocationPolicy` in JSPolicy specification may be set to `Never` or `IfNeeded`. It defaults to `Never`. + +- `Never`: the policy webhook must not be called more than once in a single admission evaluation. +- `IfNeeded`: the policy webhook may be called again as part of the admission evaluation if the object being admitted is modified by other admission plugins after the initial webhook call. + ## `Validating` Validating policies are executed as part of `kubectl` requests after the execution of mutating policies. The objective of validating policies is to inspect the request and then to either deny or allow it. diff --git a/docs/pages/writing-policies/configuration.mdx b/docs/pages/writing-policies/configuration.mdx index 101c9cbb..044019c5 100644 --- a/docs/pages/writing-policies/configuration.mdx +++ b/docs/pages/writing-policies/configuration.mdx @@ -30,7 +30,7 @@ kind: JsPolicy metadata: name: "policy-name.company.tld" spec: - type: Validating # other options: Mutating | Controller + type: Validating # other options: Mutating | Controller ``` ### Policy Trigger @@ -75,6 +75,7 @@ spec: Within the `spec` of a `JsPolicy` object, you can also define certain settings that are relevant during the execution of a policy: - `violationPolicy`: `deny` (default) or `warn` (for testing) when the policy logic calls the `deny()` function - `failurePolicy`: `Fail` (default) or `Ignore` when jsPolicy fails to execute the policy or it aborts with a runtime error +- `reinvocationPolicy`: [Reinvocation Policy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) defines whether JSPolicy is called again as part of the admission evaluation if the object being admitted is modified by other admission plugins after the initial webhook call (`IfNeeded`) or not (`Never`, *default*) - `auditPolicy`: `Log` (default) or `Skip` logging any policy violations (requests that lead to `deny()`) in the status of this policy - `auditLogSize`: Maximum number of violations that should be stored in the status of this policy (default: `10` violations) - `timeoutSeconds`: Maximum number of seconds that the execution of the policy logic may take before jsPolicy aborts the policy execution (default: `10` seconds, maximum is `30`) @@ -89,6 +90,7 @@ spec: resources: ["pods", "deployments"] violationPolicy: warn failurePolicy: Ignore + reinvocationPolicy: IfNeeded auditPolicy: Skip timeoutSeconds: 30 ``` diff --git a/pkg/apis/policy/v1beta1/jspolicy_types.go b/pkg/apis/policy/v1beta1/jspolicy_types.go index 3c0a8b81..d543f817 100644 --- a/pkg/apis/policy/v1beta1/jspolicy_types.go +++ b/pkg/apis/policy/v1beta1/jspolicy_types.go @@ -66,6 +66,17 @@ type JsPolicySpec struct { // +optional Scope *admissionregistrationv1.ScopeType `json:"scope,omitempty" protobuf:"bytes,4,rep,name=scope"` + // To allow mutating admission plugins to observe changes made by other + // plugins, built-in mutating admission plugins are re-run if a mutating webhook + // modifies an object, and mutating webhooks can specify a + // reinvocationPolicy to control whether they are reinvoked as well. + // Allowed values are IfNeeded or Never. Defaults to Never. + // See + // https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy + // for more detailed information. + // +optional + ReinvocationPolicy *admissionregistrationv1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty" protobuf:"bytes,4,opt,name=reinvocationPolicy,casttype=reinvocationPolicyType"` + // FailurePolicy defines how unrecognized errors from the admission endpoint are handled - // allowed values are Ignore or Fail. Defaults to Fail. // +optional diff --git a/pkg/apis/policy/v1beta1/zz_generated.deepcopy.go b/pkg/apis/policy/v1beta1/zz_generated.deepcopy.go index bd29726c..ffc8a2d2 100644 --- a/pkg/apis/policy/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/policy/v1beta1/zz_generated.deepcopy.go @@ -253,6 +253,11 @@ func (in *JsPolicySpec) DeepCopyInto(out *JsPolicySpec) { *out = new(v1.ScopeType) **out = **in } + if in.ReinvocationPolicy != nil { + in, out := &in.ReinvocationPolicy, &out.ReinvocationPolicy + *out = new(v1.ReinvocationPolicyType) + **out = **in + } if in.FailurePolicy != nil { in, out := &in.FailurePolicy, &out.FailurePolicy *out = new(v1.FailurePolicyType) diff --git a/pkg/controllers/jspolicy.go b/pkg/controllers/jspolicy.go index 798e0f37..d7e61512 100644 --- a/pkg/controllers/jspolicy.go +++ b/pkg/controllers/jspolicy.go @@ -591,6 +591,7 @@ func (r *JsPolicyReconciler) syncMutatingWebhookConfiguration(ctx context.Contex if webhook.Webhooks[0].SideEffects == nil { webhook.Webhooks[0].SideEffects = &none } + webhook.Webhooks[0].ReinvocationPolicy = jsPolicy.Spec.ReinvocationPolicy if webhook.Webhooks[0].ReinvocationPolicy == nil { webhook.Webhooks[0].ReinvocationPolicy = &never } diff --git a/pkg/controllers/jspolicy_test.go b/pkg/controllers/jspolicy_test.go index a657cd9a..e0a836fa 100644 --- a/pkg/controllers/jspolicy_test.go +++ b/pkg/controllers/jspolicy_test.go @@ -38,6 +38,8 @@ var ( Spec: policyv1beta1.JsPolicyBundleSpec{}, } + ifNeeded = admissionregistrationv1.IfNeededReinvocationPolicy + mutatingTestPolicy = &policyv1beta1.JsPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "test.test.com", @@ -51,6 +53,7 @@ var ( "test": "test", }, }, + ReinvocationPolicy: &ifNeeded, }, } ) diff --git a/pkg/webhook/validation/generic.go b/pkg/webhook/validation/generic.go index d9a5ef47..53ef0587 100644 --- a/pkg/webhook/validation/generic.go +++ b/pkg/webhook/validation/generic.go @@ -158,6 +158,9 @@ func validateValidatingWebhook(name string, hook *policyv1beta1.JsPolicySpec, ol if hook.FailurePolicy != nil && !supportedFailurePolicies.Has(string(*hook.FailurePolicy)) { allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List())) } + if hook.ReinvocationPolicy != nil && !supportedReinvocationPolicies.Has(string(*hook.ReinvocationPolicy)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("reinvocationPolicy"), *hook.ReinvocationPolicy, supportedReinvocationPolicies.List())) + } if hook.MatchPolicy != nil && !supportedMatchPolicies.Has(string(*hook.MatchPolicy)) { allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *hook.MatchPolicy, supportedMatchPolicies.List())) } @@ -250,6 +253,11 @@ var supportedOperations = sets.NewString( string(admissionregistrationv1.Connect), ) +var supportedReinvocationPolicies = sets.NewString( + string(admissionregistrationv1.NeverReinvocationPolicy), + string(admissionregistrationv1.IfNeededReinvocationPolicy), +) + func hasWildcardOperation(operations []admissionregistrationv1.OperationType) bool { for _, o := range operations { if o == admissionregistrationv1.OperationAll {