diff --git a/pkg/engine/wildcards/wildcards.go b/pkg/engine/wildcards/wildcards.go index 11cca2a029..5a671896a8 100644 --- a/pkg/engine/wildcards/wildcards.go +++ b/pkg/engine/wildcards/wildcards.go @@ -1,10 +1,11 @@ package wildcards import ( + "strings" + commonAnchor "github.com/kyverno/kyverno/pkg/engine/anchor/common" "github.com/minio/minio/pkg/wildcard" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "strings" ) // ReplaceInSelector replaces label selector keys and values containing diff --git a/pkg/webhooks/generation.go b/pkg/webhooks/generation.go index b1655d8a0a..f07b8d317d 100644 --- a/pkg/webhooks/generation.go +++ b/pkg/webhooks/generation.go @@ -17,6 +17,7 @@ import ( "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/response" enginutils "github.com/kyverno/kyverno/pkg/engine/utils" + "github.com/kyverno/kyverno/pkg/engine/validate" "github.com/kyverno/kyverno/pkg/event" kyvernoutils "github.com/kyverno/kyverno/pkg/utils" "github.com/kyverno/kyverno/pkg/webhooks/generate" @@ -84,12 +85,12 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic } if request.Operation == v1beta1.Update { - ws.handleUpdate(request) + ws.handleUpdate(request, policies) } } -//HandleUpdate handles admission-requests for update -func (ws *WebhookServer) handleUpdate(request *v1beta1.AdmissionRequest) { +//handleUpdate handles admission-requests for update +func (ws *WebhookServer) handleUpdate(request *v1beta1.AdmissionRequest, policies []*kyverno.ClusterPolicy) { logger := ws.log.WithValues("action", "generation", "uid", request.UID, "kind", request.Kind, "namespace", request.Namespace, "name", request.Name, "operation", request.Operation) resource, err := enginutils.ConvertToUnstructured(request.OldObject.Raw) if err != nil { @@ -98,22 +99,140 @@ func (ws *WebhookServer) handleUpdate(request *v1beta1.AdmissionRequest) { resLabels := resource.GetLabels() if resLabels["generate.kyverno.io/clone-policy-name"] != "" { - policyNames := strings.Split(resLabels["generate.kyverno.io/clone-policy-name"], ",") - for _, policyName := range policyNames { - selector := labels.SelectorFromSet(labels.Set(map[string]string{ - "generate.kyverno.io/policy-name": policyName, - })) + ws.handleUpdateCloneSourceResource(resLabels, logger) + } - grList, err := ws.grLister.List(selector) - if err != nil { - logger.Error(err, "failed to get generate request for the resource", "label", "generate.kyverno.io/policy-name") + if resLabels["app.kubernetes.io/managed-by"] == "kyverno" && resLabels["policy.kyverno.io/synchronize"] == "enable" && request.Operation == v1beta1.Update { + ws.handleUpdateTargetResource(request, policies, resLabels, logger) + } +} - } - for _, gr := range grList { - ws.grController.EnqueueGenerateRequestFromWebhook(gr) +//handleUpdateCloneSourceResource - handles updation of clone source for generate policy +func (ws *WebhookServer) handleUpdateCloneSourceResource(resLabels map[string]string, logger logr.Logger) { + policyNames := strings.Split(resLabels["generate.kyverno.io/clone-policy-name"], ",") + for _, policyName := range policyNames { + selector := labels.SelectorFromSet(labels.Set(map[string]string{ + "generate.kyverno.io/policy-name": policyName, + })) + + grList, err := ws.grLister.List(selector) + if err != nil { + logger.Error(err, "failed to get generate request for the resource", "label", "generate.kyverno.io/policy-name") + + } + for _, gr := range grList { + ws.grController.EnqueueGenerateRequestFromWebhook(gr) + } + } +} + +//handleUpdateTargetResource - handles updation of target resource for generate policy +func (ws *WebhookServer) handleUpdateTargetResource(request *v1beta1.AdmissionRequest, policies []*v1.ClusterPolicy, resLabels map[string]string, logger logr.Logger) { + enqueueBool := false + newRes, err := enginutils.ConvertToUnstructured(request.Object.Raw) + if err != nil { + logger.Error(err, "failed to convert object resource to unstructured format") + } + + policyName := resLabels["policy.kyverno.io/policy-name"] + targetSourceName := newRes.GetName() + targetSourceKind := newRes.GetKind() + + for _, policy := range policies { + if policy.GetName() == policyName { + for _, rule := range policy.Spec.Rules { + if rule.Generation.Kind == targetSourceKind && rule.Generation.Name == targetSourceName { + data := rule.Generation.DeepCopy().Data + if data != nil { + if _, err := validate.ValidateResourceWithPattern(logger, newRes.Object, data); err != nil { + enqueueBool = true + break + } + } + + cloneName := rule.Generation.Clone.Name + if cloneName != "" { + obj, err := ws.client.GetResource("", rule.Generation.Kind, rule.Generation.Clone.Namespace, rule.Generation.Clone.Name) + if err != nil { + logger.Error(err, fmt.Sprintf("source resource %s/%s/%s not found.", rule.Generation.Kind, rule.Generation.Clone.Namespace, rule.Generation.Clone.Name)) + continue + } + + sourceObj, newResObj := stripNonPolicyFields(obj.Object, newRes.Object, logger) + + if _, err := validate.ValidateResourceWithPattern(logger, newResObj, sourceObj); err != nil { + enqueueBool = true + break + } + } + } } } } + + if enqueueBool { + grName := resLabels["policy.kyverno.io/gr-name"] + gr, err := ws.grLister.Get(grName) + if err != nil { + logger.Error(err, "failed to get generate request", "name", grName) + } + ws.grController.EnqueueGenerateRequestFromWebhook(gr) + } +} + +//stripNonPolicyFields - remove feilds which get updated with each request by kyverno and are non policy fields +func stripNonPolicyFields(obj, newRes map[string]interface{}, logger logr.Logger) (map[string]interface{}, map[string]interface{}) { + + delete(obj["metadata"].(map[string]interface{})["annotations"].(map[string]interface{}), "kubectl.kubernetes.io/last-applied-configuration") + delete(obj["metadata"].(map[string]interface{})["labels"].(map[string]interface{}), "generate.kyverno.io/clone-policy-name") + + requiredMetadataInObj := make(map[string]interface{}) + requiredMetadataInObj["annotations"] = obj["metadata"].(map[string]interface{})["annotations"] + requiredMetadataInObj["labels"] = obj["metadata"].(map[string]interface{})["labels"] + obj["metadata"] = requiredMetadataInObj + + requiredMetadataInNewRes := make(map[string]interface{}) + if _, found := newRes["metadata"].(map[string]interface{})["annotations"]; found { + requiredMetadataInNewRes["annotations"] = newRes["metadata"].(map[string]interface{})["annotations"] + } + if _, found := newRes["metadata"].(map[string]interface{})["labels"]; found { + requiredMetadataInNewRes["labels"] = newRes["metadata"].(map[string]interface{})["labels"] + } + newRes["metadata"] = requiredMetadataInNewRes + + if _, found := obj["status"]; found { + delete(obj, "status") + } + + if _, found := obj["spec"]; found { + delete(obj["spec"].(map[string]interface{}), "tolerations") + } + + if dataMap, found := obj["data"]; found { + keyInData := make([]string, 0) + switch dataMap.(type) { + case map[string]interface{}: + for k, _ := range dataMap.(map[string]interface{}) { + keyInData = append(keyInData, k) + } + } + + if len(keyInData) > 0 { + for _, dataKey := range keyInData { + originalResourceData := dataMap.(map[string]interface{})[dataKey] + replaceData := strings.Replace(originalResourceData.(string), "\n", "", -1) + dataMap.(map[string]interface{})[dataKey] = replaceData + + newResourceData := newRes["data"].(map[string]interface{})[dataKey] + replacenewResourceData := strings.Replace(newResourceData.(string), "\n", "", -1) + newRes["data"].(map[string]interface{})[dataKey] = replacenewResourceData + } + } else { + logger.V(4).Info("data is not of type map[string]interface{}") + } + } + + return obj, newRes } //HandleDelete handles admission-requests for delete diff --git a/pkg/webhooks/generation_test.go b/pkg/webhooks/generation_test.go new file mode 100644 index 0000000000..409c66b7d6 --- /dev/null +++ b/pkg/webhooks/generation_test.go @@ -0,0 +1,273 @@ +package webhooks + +import ( + "reflect" + "testing" + + "gotest.tools/assert" +) + +func Test_updateFeildsInSourceAndUpdatedResource(t *testing.T) { + type TestCase struct { + obj map[string]interface{} + newRes map[string]interface{} + expectedObj map[string]interface{} + expectedNewRes map[string]interface{} + } + + testcases := []TestCase{ + TestCase{ + obj: map[string]interface{}{ + "apiVersion": "v1", + "data": map[string]interface{}{ + "ca": "-----BEGIN CERTIFICATE-----\nMIID5zCCAs+gAwIBAgIUCl6BKlpe2QiS5IQby6QOW7vexMwwDQYJKoZIhvcNAQEL\nBQAwgYIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTENMAsGA1UEBwwEVG93bjEQ\n-----END CERTIFICATE-----", + }, + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "imageregistry": "https://hub.docker.com/", + "kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","data":{"ca":"-----BEGIN CERTIFICATE-----\nMIID5zCCAs+gAwIBAgIUCl6BKlpe2QiS5IQby6QOW7vexMwwDQYJKoZIhvcNAQEL\nBQAwgYIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTENMAsGA1UEBwwEVG93bjEQ\n-----END CERTIFICATE-----"},"kind":"ConfigMap","metadata":{"annotations":{"imageregistry":"https://hub.docker.com/"},"name":"corp-ca-cert","namespace":"default"}}`, + }, + "creationTimestamp": "2021-01-09T12:37:26Z", + "labels": map[string]interface{}{"generate.kyverno.io/clone-policy-name": "generate-policy"}, + "managedFields": map[string]interface{}{ + "apiVersion": "v1", + "fieldsType": "FieldsV1", + }, + }, + }, + + newRes: map[string]interface{}{ + "apiVersion": "v1", + "data": map[string]interface{}{ + "ca": "-----BEGIN CERTIFICATE-----\nMIID5zCCAs+gAwIBAgIUCl6BKlpe2QiS5IQby6QOW7vexMwwDQYJKoZIhvcNAQEL\nBQAwgYIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTENMAsGA1UEBwwEVG93bjEQ\n-----END CERTIFICATE-----", + }, + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "creationTimestamp": "2021-01-09T12:37:26Z", + "managedFields": map[string]interface{}{ + "apiVersion": "v1", + "fieldsType": "FieldsV1", + }, + }, + }, + + expectedObj: map[string]interface{}{ + "apiVersion": "v1", + "data": map[string]interface{}{ + "ca": "-----BEGIN CERTIFICATE-----MIID5zCCAs+gAwIBAgIUCl6BKlpe2QiS5IQby6QOW7vexMwwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTENMAsGA1UEBwwEVG93bjEQ-----END CERTIFICATE-----", + }, + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "imageregistry": "https://hub.docker.com/", + }, + "labels": map[string]interface{}{}, + }, + }, + + expectedNewRes: map[string]interface{}{ + "apiVersion": "v1", + "data": map[string]interface{}{ + "ca": "-----BEGIN CERTIFICATE-----MIID5zCCAs+gAwIBAgIUCl6BKlpe2QiS5IQby6QOW7vexMwwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTENMAsGA1UEBwwEVG93bjEQ-----END CERTIFICATE-----", + }, + "kind": "ConfigMap", + "metadata": map[string]interface{}{}, + }, + }, + TestCase{ + obj: map[string]interface{}{ + "apiVersion": "v1", + "data": map[string]interface{}{ + "tls.crt": "MIIC2DCCAcCgAwIBAgIBATANBgkqh", + "tls.key": "MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ", + }, + "kind": "Secret", + "type": "kubernetes.io/tls", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","data":{"tls.crt":"MIIC2DCCAcCgAwIBAgIBATANBgkqh","tls.key": "MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ"},"type": "kubernetes.io/tls","kind":"Secret"}`, + }, + "creationTimestamp": "2021-01-09T12:37:26Z", + "labels": map[string]interface{}{"generate.kyverno.io/clone-policy-name": "generate-policy"}, + "managedFields": map[string]interface{}{ + "apiVersion": "v1", + "fieldsType": "FieldsV1", + }, + }, + }, + + newRes: map[string]interface{}{ + "apiVersion": "v1", + "data": map[string]interface{}{ + "tls.crt": "MIIC2DCCAcCgAwIBAgIBATANBgkqh", + "tls.key": "MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ", + }, + "kind": "Secret", + "type": "kubernetes.io/tls", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{}, + "creationTimestamp": "2021-01-09T12:37:26Z", + "labels": map[string]interface{}{ + "policy.kyverno.io/gr-name": "gr-qmjr9", + "policy.kyverno.io/policy-name": "generate-policy", + }, + "managedFields": map[string]interface{}{ + "apiVersion": "v1", + "fieldsType": "FieldsV1", + }, + }, + }, + + expectedObj: map[string]interface{}{ + "apiVersion": "v1", + "data": map[string]interface{}{ + "tls.crt": "MIIC2DCCAcCgAwIBAgIBATANBgkqh", + "tls.key": "MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ", + }, + "kind": "Secret", + "type": "kubernetes.io/tls", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{}, + "labels": map[string]interface{}{}, + }, + }, + + expectedNewRes: map[string]interface{}{ + "apiVersion": "v1", + "data": map[string]interface{}{ + "tls.crt": "MIIC2DCCAcCgAwIBAgIBATANBgkqh", + "tls.key": "MIIEpgIBAAKCAQEA7yn3bRHQ5FHMQ", + }, + "kind": "Secret", + "type": "kubernetes.io/tls", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{}, + "labels": map[string]interface{}{ + "policy.kyverno.io/gr-name": "gr-qmjr9", + "policy.kyverno.io/policy-name": "generate-policy", + }, + }, + }, + }, + TestCase{ + obj: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","kind":"Pod", ...}`, + }, + "creationTimestamp": "2021-01-09T12:37:26Z", + "labels": map[string]interface{}{"generate.kyverno.io/clone-policy-name": "generate-policy"}, + "managedFields": map[string]interface{}{ + "apiVersion": "v1", + "fieldsType": "FieldsV1", + }, + }, + "spec": map[string]interface{}{ + "containers": map[string]interface{}{ + "image": "redis:5.0.4", + "imagePullPolicy": "IfNotPresent", + "name": "redis", + }, + }, + "status": map[string]interface{}{ + "conditions": map[string]interface{}{ + "lastProbeTime": "null", + "lastTransitionTime": "2021-01-19T13:09:14Z", + "status": "True", + "type": "Initialized", + }, + "containerStatuses": map[string]interface{}{ + "containerID": `docker://55ad0787835e874b6762ad650af3d36c1`, + "image": "redis:5.0.4", + }, + }, + }, + + newRes: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{}, + "creationTimestamp": "2021-01-09T12:37:26Z", + "labels": map[string]interface{}{}, + "managedFields": map[string]interface{}{ + "apiVersion": "v1", + "fieldsType": "FieldsV1", + }, + }, + "spec": map[string]interface{}{ + "containers": map[string]interface{}{ + "image": "redis:5.0.4", + "imagePullPolicy": "IfNotPresent", + "name": "redis", + }, + }, + "status": map[string]interface{}{ + "conditions": map[string]interface{}{ + "lastProbeTime": "null", + "lastTransitionTime": "2021-01-19T13:09:14Z", + "status": "True", + "type": "Initialized", + }, + "containerStatuses": map[string]interface{}{ + "containerID": `docker://55ad0787835e874b6762ad650af3d36c1`, + "image": "redis:5.0.4", + }, + }, + }, + + expectedObj: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{}, + "labels": map[string]interface{}{}, + }, + "spec": map[string]interface{}{ + "containers": map[string]interface{}{ + "image": "redis:5.0.4", + "imagePullPolicy": "IfNotPresent", + "name": "redis", + }, + }, + }, + + expectedNewRes: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{}, + "labels": map[string]interface{}{}, + }, + "spec": map[string]interface{}{ + "containers": map[string]interface{}{ + "image": "redis:5.0.4", + "imagePullPolicy": "IfNotPresent", + "name": "redis", + }, + }, + "status": map[string]interface{}{ + "conditions": map[string]interface{}{ + "lastProbeTime": "null", + "lastTransitionTime": "2021-01-19T13:09:14Z", + "status": "True", + "type": "Initialized", + }, + "containerStatuses": map[string]interface{}{ + "containerID": `docker://55ad0787835e874b6762ad650af3d36c1`, + "image": "redis:5.0.4", + }, + }, + }, + }, + } + + for _, tc := range testcases { + o, n := stripNonPolicyFields(tc.obj, tc.newRes, nil) + assert.Assert(t, reflect.DeepEqual(tc.expectedObj, o)) + assert.Assert(t, reflect.DeepEqual(tc.expectedNewRes, n)) + } +}