From fd67707c007e1aa71dd3e8bfb61bb26d3bbad0de Mon Sep 17 00:00:00 2001
From: Mike Bryant <mike.bryant@mettle.co.uk>
Date: Wed, 2 Aug 2023 18:59:37 +0100
Subject: [PATCH] feat: Add support for server-side-apply in generate rules
 (#7705)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: Add support for server-side-apply in generate rules

Signed-off-by: Mike Bryant <mike@mikebryant.me.uk>

* chore: run make codegen-all

Signed-off-by: Mike Bryant <mike.bryant@mettle.co.uk>

* chore: Remove unnecessary file I got from copy/paste

Signed-off-by: Mike Bryant <mike.bryant@mettle.co.uk>

---------

Signed-off-by: Mike Bryant <mike@mikebryant.me.uk>
Signed-off-by: Mike Bryant <mike.bryant@mettle.co.uk>
Co-authored-by: Chip Zoller <chipzoller@gmail.com>
Co-authored-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
Co-authored-by: Jim Bugwadia <jim@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
---
 api/kyverno/v1/spec_types.go                  |  6 ++
 api/kyverno/v2beta1/spec_types.go             |  6 ++
 charts/kyverno/templates/crds/crds.yaml       | 24 ++++++
 config/crds/kyverno.io_clusterpolicies.yaml   | 12 +++
 config/crds/kyverno.io_policies.yaml          | 12 +++
 config/install-latest-testing.yaml            | 24 ++++++
 docs/user/crd/index.html                      | 84 +++++++++++++++++++
 pkg/background/generate/clone.go              | 24 ++++--
 pkg/background/generate/generate.go           | 18 +++-
 .../applyconfigurations/kyverno/v1/spec.go    |  9 ++
 .../kyverno/v2beta1/spec.go                   |  9 ++
 pkg/clients/dclient/client.go                 | 34 ++++++++
 .../00-sleep.yaml                             |  5 ++
 .../01-policy.yaml                            |  6 ++
 .../02-resource.yaml                          |  6 ++
 .../03-modifydownstream.yaml                  |  6 ++
 .../README.md                                 | 11 +++
 .../cloned.yaml                               |  8 ++
 .../editeddownstream.yaml                     |  9 ++
 .../finalsecret.yaml                          |  9 ++
 .../ns.yaml                                   |  4 +
 .../policy-ready.yaml                         |  9 ++
 .../policy.yaml                               | 32 +++++++
 23 files changed, 357 insertions(+), 10 deletions(-)
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/00-sleep.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/01-policy.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/02-resource.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/03-modifydownstream.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/README.md
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/cloned.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/editeddownstream.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/finalsecret.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/ns.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/policy-ready.yaml
 create mode 100644 test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/policy.yaml

diff --git a/api/kyverno/v1/spec_types.go b/api/kyverno/v1/spec_types.go
index 973f50c336..f6afe5b475 100644
--- a/api/kyverno/v1/spec_types.go
+++ b/api/kyverno/v1/spec_types.go
@@ -117,6 +117,12 @@ type Spec struct {
 	// Defaults to "false" if not specified.
 	// +optional
 	GenerateExisting bool `json:"generateExisting,omitempty" yaml:"generateExisting,omitempty"`
+
+	// UseServerSideApply controls whether to use server-side apply for generate rules
+	// If is set to "true" create & update for generate rules will use apply instead of create/update.
+	// Defaults to "false" if not specified.
+	// +optional
+	UseServerSideApply bool `json:"useServerSideApply,omitempty" yaml:"useServerSideApply,omitempty"`
 }
 
 func (s *Spec) SetRules(rules []Rule) {
diff --git a/api/kyverno/v2beta1/spec_types.go b/api/kyverno/v2beta1/spec_types.go
index 6478b75382..4aaf716c3c 100644
--- a/api/kyverno/v2beta1/spec_types.go
+++ b/api/kyverno/v2beta1/spec_types.go
@@ -78,6 +78,12 @@ type Spec struct {
 	// Defaults to "false" if not specified.
 	// +optional
 	GenerateExisting bool `json:"generateExisting,omitempty" yaml:"generateExisting,omitempty"`
+
+	// UseServerSideApply controls whether to use server-side apply for generate rules
+	// If is set to "true" create & update for generate rules will use apply instead of create/update.
+	// Defaults to "false" if not specified.
+	// +optional
+	UseServerSideApply bool `json:"useServerSideApply,omitempty" yaml:"useServerSideApply,omitempty"`
 }
 
 func (s *Spec) SetRules(rules []Rule) {
diff --git a/charts/kyverno/templates/crds/crds.yaml b/charts/kyverno/templates/crds/crds.yaml
index b3dca560e9..c0b7ac3247 100644
--- a/charts/kyverno/templates/crds/crds.yaml
+++ b/charts/kyverno/templates/crds/crds.yaml
@@ -7764,6 +7764,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
@@ -15606,6 +15612,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
@@ -23759,6 +23771,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
@@ -31603,6 +31621,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
diff --git a/config/crds/kyverno.io_clusterpolicies.yaml b/config/crds/kyverno.io_clusterpolicies.yaml
index ef214515d8..638a993e7f 100644
--- a/config/crds/kyverno.io_clusterpolicies.yaml
+++ b/config/crds/kyverno.io_clusterpolicies.yaml
@@ -3947,6 +3947,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
@@ -11789,6 +11795,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
diff --git a/config/crds/kyverno.io_policies.yaml b/config/crds/kyverno.io_policies.yaml
index 9fecd89954..fb9642fef2 100644
--- a/config/crds/kyverno.io_policies.yaml
+++ b/config/crds/kyverno.io_policies.yaml
@@ -3948,6 +3948,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
@@ -11792,6 +11798,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
diff --git a/config/install-latest-testing.yaml b/config/install-latest-testing.yaml
index 2d73aeeee8..446cdf3669 100644
--- a/config/install-latest-testing.yaml
+++ b/config/install-latest-testing.yaml
@@ -7967,6 +7967,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
@@ -15809,6 +15815,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
@@ -23962,6 +23974,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
@@ -31806,6 +31824,12 @@ spec:
                   as well as patched resources. Optional. The default value is set
                   to "true", it must be set to "false" to disable the validation checks.
                 type: boolean
+              useServerSideApply:
+                description: UseServerSideApply controls whether to use server-side
+                  apply for generate rules If is set to "true" create & update for
+                  generate rules will use apply instead of create/update. Defaults
+                  to "false" if not specified.
+                type: boolean
               validationFailureAction:
                 default: Audit
                 description: ValidationFailureAction defines if a validation policy
diff --git a/docs/user/crd/index.html b/docs/user/crd/index.html
index b65ac614da..871580b995 100644
--- a/docs/user/crd/index.html
+++ b/docs/user/crd/index.html
@@ -275,6 +275,20 @@ If is set to &ldquo;true&rdquo; generate rule will be triggered and applied to e
 Defaults to &ldquo;false&rdquo; if not specified.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>useServerSideApply</code><br/>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>UseServerSideApply controls whether to use server-side apply for generate rules
+If is set to &ldquo;true&rdquo; create &amp; update for generate rules will use apply instead of create/update.
+Defaults to &ldquo;false&rdquo; if not specified.</p>
+</td>
+</tr>
 </table>
 </td>
 </tr>
@@ -526,6 +540,20 @@ If is set to &ldquo;true&rdquo; generate rule will be triggered and applied to e
 Defaults to &ldquo;false&rdquo; if not specified.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>useServerSideApply</code><br/>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>UseServerSideApply controls whether to use server-side apply for generate rules
+If is set to &ldquo;true&rdquo; create &amp; update for generate rules will use apply instead of create/update.
+Defaults to &ldquo;false&rdquo; if not specified.</p>
+</td>
+</tr>
 </table>
 </td>
 </tr>
@@ -3679,6 +3707,20 @@ If is set to &ldquo;true&rdquo; generate rule will be triggered and applied to e
 Defaults to &ldquo;false&rdquo; if not specified.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>useServerSideApply</code><br/>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>UseServerSideApply controls whether to use server-side apply for generate rules
+If is set to &ldquo;true&rdquo; create &amp; update for generate rules will use apply instead of create/update.
+Defaults to &ldquo;false&rdquo; if not specified.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <hr />
@@ -6070,6 +6112,20 @@ If is set to &ldquo;true&rdquo; generate rule will be triggered and applied to e
 Defaults to &ldquo;false&rdquo; if not specified.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>useServerSideApply</code><br/>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>UseServerSideApply controls whether to use server-side apply for generate rules
+If is set to &ldquo;true&rdquo; create &amp; update for generate rules will use apply instead of create/update.
+Defaults to &ldquo;false&rdquo; if not specified.</p>
+</td>
+</tr>
 </table>
 </td>
 </tr>
@@ -6320,6 +6376,20 @@ If is set to &ldquo;true&rdquo; generate rule will be triggered and applied to e
 Defaults to &ldquo;false&rdquo; if not specified.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>useServerSideApply</code><br/>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>UseServerSideApply controls whether to use server-side apply for generate rules
+If is set to &ldquo;true&rdquo; create &amp; update for generate rules will use apply instead of create/update.
+Defaults to &ldquo;false&rdquo; if not specified.</p>
+</td>
+</tr>
 </table>
 </td>
 </tr>
@@ -7242,6 +7312,20 @@ If is set to &ldquo;true&rdquo; generate rule will be triggered and applied to e
 Defaults to &ldquo;false&rdquo; if not specified.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>useServerSideApply</code><br/>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>UseServerSideApply controls whether to use server-side apply for generate rules
+If is set to &ldquo;true&rdquo; create &amp; update for generate rules will use apply instead of create/update.
+Defaults to &ldquo;false&rdquo; if not specified.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <hr />
diff --git a/pkg/background/generate/clone.go b/pkg/background/generate/clone.go
index 489702bf29..e871503be8 100644
--- a/pkg/background/generate/clone.go
+++ b/pkg/background/generate/clone.go
@@ -11,6 +11,7 @@ import (
 	datautils "github.com/kyverno/kyverno/pkg/utils/data"
 	kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func manageClone(log logr.Logger, target, sourceSpec kyvernov1.ResourceSpec, policy kyvernov1.PolicyInterface, ur kyvernov1beta1.UpdateRequest, rule kyvernov1.Rule, client dclient.Interface) generateResponse {
@@ -50,6 +51,13 @@ func manageClone(log logr.Logger, target, sourceSpec kyvernov1.ResourceSpec, pol
 	if sourceObjCopy.GetNamespace() != target.GetNamespace() && sourceObjCopy.GetOwnerReferences() != nil {
 		sourceObjCopy.SetOwnerReferences(nil)
 	}
+	// Clean up parameters that shouldn't be copied
+	sourceObjCopy.SetUID("")
+	sourceObjCopy.SetSelfLink("")
+	var emptyTime metav1.Time
+	sourceObjCopy.SetCreationTimestamp(emptyTime)
+	sourceObjCopy.SetManagedFields(nil)
+	sourceObjCopy.SetResourceVersion("")
 
 	targetObj, err := client.GetResource(context.TODO(), target.GetAPIVersion(), target.GetKind(), target.GetNamespace(), target.GetName())
 	if err != nil {
@@ -64,13 +72,15 @@ func manageClone(log logr.Logger, target, sourceSpec kyvernov1.ResourceSpec, pol
 	}
 
 	if targetObj != nil {
-		sourceObjCopy.SetUID(targetObj.GetUID())
-		sourceObjCopy.SetSelfLink(targetObj.GetSelfLink())
-		sourceObjCopy.SetCreationTimestamp(targetObj.GetCreationTimestamp())
-		sourceObjCopy.SetManagedFields(targetObj.GetManagedFields())
-		sourceObjCopy.SetResourceVersion(targetObj.GetResourceVersion())
-		if datautils.DeepEqual(sourceObjCopy, targetObj) {
-			return newSkipGenerateResponse(nil, target, nil)
+		if !policy.GetSpec().UseServerSideApply {
+			sourceObjCopy.SetUID(targetObj.GetUID())
+			sourceObjCopy.SetSelfLink(targetObj.GetSelfLink())
+			sourceObjCopy.SetCreationTimestamp(targetObj.GetCreationTimestamp())
+			sourceObjCopy.SetManagedFields(targetObj.GetManagedFields())
+			sourceObjCopy.SetResourceVersion(targetObj.GetResourceVersion())
+			if datautils.DeepEqual(sourceObjCopy, targetObj) {
+				return newSkipGenerateResponse(nil, target, nil)
+			}
 		}
 		return newUpdateGenerateResponse(sourceObjCopy.UnstructuredContent(), target, nil)
 	}
diff --git a/pkg/background/generate/generate.go b/pkg/background/generate/generate.go
index cf48f41347..f68e06ad5d 100644
--- a/pkg/background/generate/generate.go
+++ b/pkg/background/generate/generate.go
@@ -403,7 +403,11 @@ func applyRule(log logr.Logger, client dclient.Interface, rule kyvernov1.Rule, t
 		common.ManageLabels(newResource, trigger, policy, rule.Name)
 		if response.GetAction() == Create {
 			newResource.SetResourceVersion("")
-			_, err = client.CreateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
+			if policy.GetSpec().UseServerSideApply {
+				_, err = client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
+			} else {
+				_, err = client.CreateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
+			}
 			if err != nil {
 				if !apierrors.IsAlreadyExists(err) {
 					return newGenResources, err
@@ -415,7 +419,11 @@ func applyRule(log logr.Logger, client dclient.Interface, rule kyvernov1.Rule, t
 			generatedObj, err := client.GetResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName())
 			if err != nil {
 				logger.V(2).Info("target resource not found, creating new target")
-				_, err = client.CreateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
+				if policy.GetSpec().UseServerSideApply {
+					_, err = client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
+				} else {
+					_, err = client.CreateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
+				}
 				if err != nil {
 					return newGenResources, err
 				}
@@ -433,7 +441,11 @@ func applyRule(log logr.Logger, client dclient.Interface, rule kyvernov1.Rule, t
 					newResource.SetNamespace("default")
 				}
 
-				_, err = client.UpdateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
+				if policy.GetSpec().UseServerSideApply {
+					_, err = client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
+				} else {
+					_, err = client.UpdateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
+				}
 				if err != nil {
 					logger.Error(err, "failed to update resource")
 					return newGenResources, err
diff --git a/pkg/client/applyconfigurations/kyverno/v1/spec.go b/pkg/client/applyconfigurations/kyverno/v1/spec.go
index af5215b950..1ed14a4fc8 100644
--- a/pkg/client/applyconfigurations/kyverno/v1/spec.go
+++ b/pkg/client/applyconfigurations/kyverno/v1/spec.go
@@ -37,6 +37,7 @@ type SpecApplyConfiguration struct {
 	MutateExistingOnPolicyUpdate     *bool                                               `json:"mutateExistingOnPolicyUpdate,omitempty"`
 	GenerateExistingOnPolicyUpdate   *bool                                               `json:"generateExistingOnPolicyUpdate,omitempty"`
 	GenerateExisting                 *bool                                               `json:"generateExisting,omitempty"`
+	UseServerSideApply               *bool                                               `json:"useServerSideApply,omitempty"`
 }
 
 // SpecApplyConfiguration constructs an declarative configuration of the Spec type for use with
@@ -150,3 +151,11 @@ func (b *SpecApplyConfiguration) WithGenerateExisting(value bool) *SpecApplyConf
 	b.GenerateExisting = &value
 	return b
 }
+
+// WithUseServerSideApply sets the UseServerSideApply field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UseServerSideApply field is set to the value of the last call.
+func (b *SpecApplyConfiguration) WithUseServerSideApply(value bool) *SpecApplyConfiguration {
+	b.UseServerSideApply = &value
+	return b
+}
diff --git a/pkg/client/applyconfigurations/kyverno/v2beta1/spec.go b/pkg/client/applyconfigurations/kyverno/v2beta1/spec.go
index 244f9b383c..427aeef855 100644
--- a/pkg/client/applyconfigurations/kyverno/v2beta1/spec.go
+++ b/pkg/client/applyconfigurations/kyverno/v2beta1/spec.go
@@ -38,6 +38,7 @@ type SpecApplyConfiguration struct {
 	MutateExistingOnPolicyUpdate     *bool                                                         `json:"mutateExistingOnPolicyUpdate,omitempty"`
 	GenerateExistingOnPolicyUpdate   *bool                                                         `json:"generateExistingOnPolicyUpdate,omitempty"`
 	GenerateExisting                 *bool                                                         `json:"generateExisting,omitempty"`
+	UseServerSideApply               *bool                                                         `json:"useServerSideApply,omitempty"`
 }
 
 // SpecApplyConfiguration constructs an declarative configuration of the Spec type for use with
@@ -151,3 +152,11 @@ func (b *SpecApplyConfiguration) WithGenerateExisting(value bool) *SpecApplyConf
 	b.GenerateExisting = &value
 	return b
 }
+
+// WithUseServerSideApply sets the UseServerSideApply field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UseServerSideApply field is set to the value of the last call.
+func (b *SpecApplyConfiguration) WithUseServerSideApply(value bool) *SpecApplyConfiguration {
+	b.UseServerSideApply = &value
+	return b
+}
diff --git a/pkg/clients/dclient/client.go b/pkg/clients/dclient/client.go
index a809a704c0..818bb8000d 100644
--- a/pkg/clients/dclient/client.go
+++ b/pkg/clients/dclient/client.go
@@ -48,6 +48,10 @@ type Interface interface {
 	UpdateResource(ctx context.Context, apiVersion string, kind string, namespace string, obj interface{}, dryRun bool, subresources ...string) (*unstructured.Unstructured, error)
 	// UpdateStatusResource updates the resource "status" subresource
 	UpdateStatusResource(ctx context.Context, apiVersion string, kind string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error)
+	// ApplyResource applies object for the specified resource/namespace using server-side apply
+	ApplyResource(ctx context.Context, apiVersion string, kind string, namespace string, name string, obj interface{}, dryRun bool, fieldManager string, subresources ...string) (*unstructured.Unstructured, error)
+	// ApplyStatusResource applies the resource "status" subresource using server-side apply
+	ApplyStatusResource(ctx context.Context, apiVersion string, kind string, namespace string, name string, obj interface{}, dryRun bool, fieldManager string) (*unstructured.Unstructured, error)
 }
 
 // Client enables interaction with k8 resource
@@ -226,6 +230,36 @@ func (c *client) UpdateStatusResource(ctx context.Context, apiVersion string, ki
 	return nil, fmt.Errorf("unable to update resource ")
 }
 
+// ApplyResource updates object for the specified resource/namespace
+func (c *client) ApplyResource(ctx context.Context, apiVersion string, kind string, namespace string, name string, obj interface{}, dryRun bool, fieldManager string, subresources ...string) (*unstructured.Unstructured, error) {
+	// We have a different field manager for different situations, so a generated object that then goes through admission control
+	// won't have the changes wiped out by any use of server-side apply in the mutation path
+	options := metav1.ApplyOptions{FieldManager: "kyverno-" + fieldManager, Force: true}
+	if dryRun {
+		options.DryRun = []string{metav1.DryRunAll}
+	}
+	// convert typed to unstructured obj
+	if unstructuredObj, err := kubeutils.ObjToUnstructured(obj); err == nil && unstructuredObj != nil {
+		return c.getResourceInterface(apiVersion, kind, namespace).Apply(ctx, name, unstructuredObj, options, subresources...)
+	}
+	return nil, fmt.Errorf("unable to apply resource ")
+}
+
+// ApplyStatusResource updates the resource "status" subresource
+func (c *client) ApplyStatusResource(ctx context.Context, apiVersion string, kind string, namespace string, name string, obj interface{}, dryRun bool, fieldManager string) (*unstructured.Unstructured, error) {
+	// We have a different field manager for different situations, so a generated object that then goes through admission control
+	// won't have the changes wiped out by any use of server-side apply in the mutation path
+	options := metav1.ApplyOptions{FieldManager: "kyverno-" + fieldManager, Force: true}
+	if dryRun {
+		options.DryRun = []string{metav1.DryRunAll}
+	}
+	// convert typed to unstructured obj
+	if unstructuredObj, err := kubeutils.ObjToUnstructured(obj); err == nil && unstructuredObj != nil {
+		return c.getResourceInterface(apiVersion, kind, namespace).ApplyStatus(ctx, name, unstructuredObj, options)
+	}
+	return nil, fmt.Errorf("unable to apply resource ")
+}
+
 // Discovery return the discovery client implementation
 func (c *client) Discovery() IDiscovery {
 	return c.disco
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/00-sleep.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/00-sleep.yaml
new file mode 100644
index 0000000000..a8f497427f
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/00-sleep.yaml
@@ -0,0 +1,5 @@
+# A pre-test sleep is needed here due to https://github.com/kudobuilder/kuttl/pull/422
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+  - command: sleep 3
\ No newline at end of file
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/01-policy.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/01-policy.yaml
new file mode 100644
index 0000000000..f3857739b0
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/01-policy.yaml
@@ -0,0 +1,6 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+apply:
+- policy.yaml
+assert:
+- policy-ready.yaml
\ No newline at end of file
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/02-resource.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/02-resource.yaml
new file mode 100644
index 0000000000..90ff828793
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/02-resource.yaml
@@ -0,0 +1,6 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+apply:
+- ns.yaml
+assert:
+- cloned.yaml
\ No newline at end of file
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/03-modifydownstream.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/03-modifydownstream.yaml
new file mode 100644
index 0000000000..e1eae46abf
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/03-modifydownstream.yaml
@@ -0,0 +1,6 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+apply:
+- editeddownstream.yaml
+assert:
+- finalsecret.yaml
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/README.md b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/README.md
new file mode 100644
index 0000000000..56b2c21bc4
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/README.md
@@ -0,0 +1,11 @@
+## Description
+
+This test ensures that modification of the downstream (cloned/generated) resource used by a ClusterPolicy `generate` rule with sync enabled using a clone declaration and server-side apply causes those changes to be merged from the state of the upstream/source.
+
+## Expected Behavior
+
+After the downstream resource is modified, the changes should be merged with the clone after synchronization occurs. If the downstream resource is synced with the state of the source resource, and also respects the modifications to other fields, the test passes. If the downstream resource doesn't retain the cloned fields and the directly modified fields, the test fails.
+
+## Reference Issue(s)
+
+N/A
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/cloned.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/cloned.yaml
new file mode 100644
index 0000000000..61ff2f02d2
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/cloned.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+data:
+  foo: YmFy
+kind: Secret
+metadata:
+  name: regcred
+  namespace: myfoons
+type: Opaque
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/editeddownstream.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/editeddownstream.yaml
new file mode 100644
index 0000000000..60c3c479ac
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/editeddownstream.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+data:
+  foo: bm90YmFjaGhlcmU=
+  foo2: bm90YmFjaGhlcmU=
+kind: Secret
+metadata:
+  name: regcred
+  namespace: myfoons
+type: Opaque
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/finalsecret.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/finalsecret.yaml
new file mode 100644
index 0000000000..f2a80beb17
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/finalsecret.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+data:
+  foo: YmFy
+  foo2: bm90YmFjaGhlcmU=
+kind: Secret
+metadata:
+  name: regcred
+  namespace: myfoons
+type: Opaque
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/ns.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/ns.yaml
new file mode 100644
index 0000000000..f5d7996929
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/ns.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: myfoons
\ No newline at end of file
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/policy-ready.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/policy-ready.yaml
new file mode 100644
index 0000000000..2603a65a7e
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/policy-ready.yaml
@@ -0,0 +1,9 @@
+apiVersion: kyverno.io/v2beta1
+kind: ClusterPolicy
+metadata:
+  name: cpol-clone-sync-modify-downstream-apply
+status:
+  conditions:
+  - reason: Succeeded
+    status: "True"
+    type: Ready
diff --git a/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/policy.yaml b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/policy.yaml
new file mode 100644
index 0000000000..6154b8639f
--- /dev/null
+++ b/test/conformance/kuttl/generate/clusterpolicy/standard/clone/sync/cpol-clone-sync-modify-downstream-apply/policy.yaml
@@ -0,0 +1,32 @@
+apiVersion: v1
+data:
+  foo: YmFy
+kind: Secret
+metadata:
+  name: regcred
+  namespace: default
+type: Opaque
+---
+apiVersion: kyverno.io/v2beta1
+kind: ClusterPolicy
+metadata:
+  name: cpol-clone-sync-modify-downstream-apply
+spec:
+  rules:
+  - name: cpol-clone-sync-modify-downstream-secret
+    match:
+      any:
+      - resources:
+          kinds:
+          - Namespace
+    generate:
+      apiVersion: v1
+      kind: Secret
+      name: regcred
+      namespace: "{{request.object.metadata.name}}"
+      synchronize: true
+      clone:
+        namespace: default
+        name: regcred
+  useServerSideApply: true
+---