diff --git a/pkg/background/mutate/mutate.go b/pkg/background/mutate/mutate.go index f08e047929..1a5d8fe392 100644 --- a/pkg/background/mutate/mutate.go +++ b/pkg/background/mutate/mutate.go @@ -18,6 +18,7 @@ import ( engineutils "github.com/kyverno/kyverno/pkg/utils/engine" "go.uber.org/multierr" yamlv2 "gopkg.in/yaml.v2" + admissionv1 "k8s.io/api/admission/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" corev1listers "k8s.io/client-go/listers/core/v1" @@ -84,11 +85,46 @@ func (c *MutateExistingController) ProcessUR(ur *kyvernov1beta1.UpdateRequest) e continue } - trigger, err := common.GetResource(c.client, ur.Spec, c.log) - if err != nil || trigger == nil { - logger.WithName(rule.Name).Error(err, "failed to get trigger resource") - errs = append(errs, err) - continue + var trigger *unstructured.Unstructured + admissionRequest := ur.Spec.Context.AdmissionRequestInfo.AdmissionRequest + if admissionRequest == nil { + trigger, err = common.GetResource(c.client, ur.Spec, c.log) + if err != nil || trigger == nil { + logger.WithName(rule.Name).Error(err, "failed to get trigger resource") + errs = append(errs, err) + continue + } + } else { + if admissionRequest.Operation == admissionv1.Create { + trigger, err = common.GetResource(c.client, ur.Spec, c.log) + if err != nil || trigger == nil { + if admissionRequest.SubResource == "" { + logger.WithName(rule.Name).Error(err, "failed to get trigger resource") + errs = append(errs, err) + continue + } else { + logger.WithName(rule.Name).Info("trigger resource not found for subresource, reverting to resource in AdmissionReviewRequest", "subresource", admissionRequest.SubResource) + triggerBytes := admissionRequest.Object.Raw + trigger = &unstructured.Unstructured{} + if err := trigger.UnmarshalJSON(triggerBytes); err != nil { + logger.WithName(rule.Name).Error(err, "failed to convert trigger resource") + errs = append(errs, err) + continue + } + } + } + } else { + triggerBytes := admissionRequest.Object.Raw + if triggerBytes == nil { + triggerBytes = admissionRequest.OldObject.Raw + } + trigger = &unstructured.Unstructured{} + if err := trigger.UnmarshalJSON(triggerBytes); err != nil { + logger.WithName(rule.Name).Error(err, "failed to convert trigger resource") + errs = append(errs, err) + continue + } + } } namespaceLabels := engineutils.GetNamespaceSelectorsFromNamespaceLister(trigger.GetKind(), trigger.GetNamespace(), c.nsLister, logger) @@ -98,6 +134,16 @@ func (c *MutateExistingController) ProcessUR(ur *kyvernov1beta1.UpdateRequest) e errs = append(errs, err) continue } + if admissionRequest != nil { + var gvk schema.GroupVersionKind + gvk, err = c.client.Discovery().GetGVKFromGVR(schema.GroupVersionResource(admissionRequest.Resource)) + if err != nil { + logger.WithName(rule.Name).Error(err, "failed to get GVK from GVR", "GVR", admissionRequest.Resource) + errs = append(errs, err) + continue + } + policyContext = policyContext.WithResourceKind(gvk, admissionRequest.SubResource) + } er := c.engine.Mutate(context.TODO(), policyContext) for _, r := range er.PolicyResponse.Rules { diff --git a/pkg/config/config.go b/pkg/config/config.go index a0b75f12b7..5734f590ff 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -96,7 +96,7 @@ var ( // kyvernoConfigMapName is the Kyverno configmap name kyvernoConfigMapName = osutils.GetEnvWithFallback("INIT_CONFIG", "kyverno") // defaultExcludedUsernames are the usernames excluded by default when matching an incoming admission request - defaultExcludedUsernames []string = []string{"system:kube-scheduler"} + defaultExcludedUsernames []string // defaultExcludedGroups are the groups excluded by default when matching an incoming admission request defaultExcludedGroups []string = []string{"system:serviceaccounts:kube-system", "system:nodes"} // kyvernoDryRunNamespace is the namespace for DryRun option of YAML verification diff --git a/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/01-assert.yaml b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/01-assert.yaml new file mode 100644 index 0000000000..32127417a5 --- /dev/null +++ b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/01-assert.yaml @@ -0,0 +1,16 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: mutate-pod-on-binding-request +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-ns +status: + phase: Active diff --git a/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/01-manifests.yaml b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/01-manifests.yaml new file mode 100644 index 0000000000..69d7889431 --- /dev/null +++ b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/01-manifests.yaml @@ -0,0 +1,66 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: test-ns +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: mutate-pod-on-binding-request +spec: + background: false + rules: + - name: mutate-pod-on-binding-request + match: + any: + - resources: + kinds: + - Pod/binding + names: + - nginx-pod + preconditions: + all: + - key: "{{node}}" + operator: NotEquals + value: "" + - key: "{{ request.operation }}" + operator: AnyIn + value: + - CREATE + - UPDATE + context: + - name: node + variable: + jmesPath: request.object.target.name + default: '' + - name: foolabel + apiCall: + urlPath: "/api/v1/nodes/{{node}}" + jmesPath: metadata.labels.foo || 'empty' + mutate: + targets: + - apiVersion: v1 + kind: Pod + name: "{{ request.name }}" + namespace: "{{ request.namespace}}" + patchStrategicMerge: + metadata: + labels: + foo: "{{ foolabel }}" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: admission-controller + app.kubernetes.io/instance: kyverno + app.kubernetes.io/part-of: kyverno + name: kyverno:modify-pods +rules: + - apiGroups: + - "" + resources: + - pods + verbs: + - update + - patch diff --git a/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/02-assert.yaml b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/02-assert.yaml new file mode 100644 index 0000000000..fc4c2a292c --- /dev/null +++ b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/02-assert.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + foo: empty + name: nginx-pod + namespace: test-ns diff --git a/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/02-script.yaml b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/02-script.yaml new file mode 100644 index 0000000000..ad08caea21 --- /dev/null +++ b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/02-script.yaml @@ -0,0 +1,5 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: ./modify-resource-filters.sh removeBinding + - command: kubectl run nginx-pod --image=nginx -n test-ns diff --git a/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/99-cleanup.yaml b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/99-cleanup.yaml new file mode 100644 index 0000000000..570e88b42c --- /dev/null +++ b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/99-cleanup.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl delete pod nginx-pod -n test-ns --force --wait=true + - command: kubectl delete -f 01-manifests.yaml --force --wait=true + - script: ./modify-resource-filters.sh addBinding diff --git a/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/README.md b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/README.md new file mode 100644 index 0000000000..693727892b --- /dev/null +++ b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/README.md @@ -0,0 +1,22 @@ +## Description + +This test validates that an incoming request to `Pod/binding` subresource can act as a trigger for mutation of an existing `Pod` object. + +## Expected Behavior + +The `Pod` `nginx-pod` is labelled with `foo: empty` label. + +## Steps + +### Test Steps + +1. Create a `ClusterPolicy` that matches on `Pod/binding` and mutates `Pod` object. +2. Create `ClusterRole` for allowing modifications to `Pod` resource. +3. Modify kyverno `resourceFilters` to allow mutating incoming requests to `Pod/binding` subresource. +4. Modify kyverno `resourceFilters` to allow mutating incoming requests from `kube-system` namespace. +5. Create a `Pod` object. +6. Verify that the `Pod` object is labelled with `foo: empty` label. + +## Reference Issue(s) + +https://github.com/kyverno/kyverno/issues/6503 \ No newline at end of file diff --git a/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/modify-resource-filters.sh b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/modify-resource-filters.sh new file mode 100755 index 0000000000..e4ecbb1f64 --- /dev/null +++ b/test/conformance/kuttl/mutate/clusterpolicy/standard/existing/mutate-pod-on-binding-request/modify-resource-filters.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -eu + +if [ $# -ne 1 ]; then + echo "Usage: $0 [addBinding|removeBinding]" + exit 1 +fi + +if [ "$1" = "removeBinding" ]; then + resource_filters=$(kubectl get ConfigMap kyverno -n kyverno -o json | jq .data.resourceFilters) + resource_filters="${resource_filters//\[Binding,\*,\*\]/}" + + kubectl patch ConfigMap kyverno -n kyverno --type='json' -p="[{\"op\": \"replace\", \"path\": \"/data/resourceFilters\", \"value\":""$resource_filters""}]" +fi + +if [ "$1" = "addBinding" ]; then + resource_filters=$(kubectl get ConfigMap kyverno -n kyverno -o json | jq .data.resourceFilters) + resource_filters="${resource_filters%?}" + + resource_filters="${resource_filters}""[Binding,*,*]\"" + kubectl patch ConfigMap kyverno -n kyverno --type='json' -p="[{\"op\": \"replace\", \"path\": \"/data/resourceFilters\", \"value\":""$resource_filters""}]" +fi