Repository: GoogleContainerTools/kpt-config-sync
Pull request #1639: refactor: DeletionPropagationPolicy helpers
Author: login: karlkfi
Labels: name: size/L
name: lgtm
name: approved
Created: 2025-04-10T19:50:00Z | Merged: 2025-04-10T21:02:06Z

- Add helpers to the metadata package, remove from e2e
- Add similar helpers to the ManagementMode enum


Diff:
diff --git a/e2e/nomostest/clean.go b/e2e/nomostest/clean.go
index 3ecaa2ef32..ce5af4d793 100644
--- a/e2e/nomostest/clean.go
+++ b/e2e/nomostest/clean.go
@@ -644,11 +644,11 @@ func disableRepoSyncDeletionPropagation(nt *NT) error {
 		return err
 	}
 	for _, rs := range rsList.Items {
-		if IsDeletionPropagationEnabled(&rs) {
+		if metadata.HasDeletionPropagationPolicy(&rs, metadata.DeletionPropagationPolicyForeground) {
 			rsCopy := rs.DeepCopy()
 			// Disable deletion-propagation and delete finalizers
 			// This should unblock RepoSync & Namespace deletion.
-			RemoveDeletionPropagationPolicy(rsCopy)
+			metadata.RemoveDeletionPropagationPolicy(rsCopy)
 			rsCopy.Finalizers = nil
 			if err := nt.KubeClient.Update(rsCopy); err != nil {
 				return err
@@ -669,11 +669,11 @@ func disableRootSyncDeletionPropagation(nt *NT) error {
 		return err
 	}
 	for _, rs := range rsList.Items {
-		if IsDeletionPropagationEnabled(&rs) {
+		if metadata.HasDeletionPropagationPolicy(&rs, metadata.DeletionPropagationPolicyForeground) {
 			rsCopy := rs.DeepCopy()
 			// Disable deletion-propagation and delete finalizers
 			// This should unblock RootSync & `config-management-system` Namespace deletion.
-			RemoveDeletionPropagationPolicy(rsCopy)
+			metadata.RemoveDeletionPropagationPolicy(rsCopy)
 			rsCopy.Finalizers = nil
 			if err := nt.KubeClient.Update(rsCopy); err != nil {
 				return err
diff --git a/e2e/nomostest/config_sync.go b/e2e/nomostest/config_sync.go
index a480e6ae8b..44f6d03e1d 100644
--- a/e2e/nomostest/config_sync.go
+++ b/e2e/nomostest/config_sync.go
@@ -746,9 +746,9 @@ func SetRSyncTestDefaults(nt *NT, obj client.Object) {
 	// Enable automatic deletion of managed objects by default.
 	// This helps ensure that test artifacts are cleaned up.
 	if nt.deletionPropagationPolicy == nil {
-		RemoveDeletionPropagationPolicy(obj)
+		metadata.RemoveDeletionPropagationPolicy(obj)
 	} else {
-		SetDeletionPropagationPolicy(obj, *nt.deletionPropagationPolicy)
+		metadata.SetDeletionPropagationPolicy(obj, *nt.deletionPropagationPolicy)
 	}
 }
 
diff --git a/e2e/nomostest/deletion_propagation.go b/e2e/nomostest/deletion_propagation.go
deleted file mode 100644
index d3367527d8..0000000000
--- a/e2e/nomostest/deletion_propagation.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package nomostest
-
-import (
-	"kpt.dev/configsync/pkg/core"
-	"kpt.dev/configsync/pkg/metadata"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-// EnableDeletionPropagation enables foreground deletion propagation on a
-// RootSync or RepoSync. The object is annotated locally, but not applied.
-// Returns true if a change was made, false if already enabled.
-func EnableDeletionPropagation(rs client.Object) bool {
-	return SetDeletionPropagationPolicy(rs, metadata.DeletionPropagationPolicyForeground)
-}
-
-// DisableDeletionPropagation disables foreground deletion propagation on a
-// RootSync or RepoSync. The object annotated is removed locally, but not applied.
-// Returns true if a change was made, false if already enabled.
-func DisableDeletionPropagation(rs client.Object) bool {
-	return RemoveDeletionPropagationPolicy(rs)
-}
-
-// IsDeletionPropagationEnabled returns true if deletion propagation annotation
-// is set to Foreground.
-func IsDeletionPropagationEnabled(rs client.Object) bool {
-	return HasDeletionPropagationPolicy(rs, metadata.DeletionPropagationPolicyForeground)
-}
-
-// HasDeletionPropagationPolicy returns true if deletion propagation annotation
-// is set to the specified policy. Returns false if not set.
-func HasDeletionPropagationPolicy(obj client.Object, policy metadata.DeletionPropagationPolicy) bool {
-	annotations := obj.GetAnnotations()
-	// don't panic if nil
-	if len(annotations) == 0 {
-		return false
-	}
-	foundPolicy, found := annotations[metadata.DeletionPropagationPolicyAnnotationKey]
-	return found && foundPolicy == string(policy)
-}
-
-// SetDeletionPropagationPolicy sets the value of the deletion propagation
-// annotation locally (does not apply). Returns true if the object was modified.
-func SetDeletionPropagationPolicy(obj client.Object, policy metadata.DeletionPropagationPolicy) bool {
-	return core.SetAnnotation(obj, metadata.DeletionPropagationPolicyAnnotationKey, string(policy))
-}
-
-// RemoveDeletionPropagationPolicy removes the deletion propagation annotation
-// locally (does not apply). Returns true if the object was modified.
-func RemoveDeletionPropagationPolicy(obj client.Object) bool {
-	return core.RemoveAnnotations(obj, metadata.DeletionPropagationPolicyAnnotationKey)
-}
diff --git a/e2e/nomostest/reset.go b/e2e/nomostest/reset.go
index f5247635c8..5c0933db20 100644
--- a/e2e/nomostest/reset.go
+++ b/e2e/nomostest/reset.go
@@ -157,7 +157,7 @@ func ResetRootSyncs(nt *NT, rsList []v1beta1.RootSync) error {
 
 		if manager, found := rs.GetAnnotations()[string(metadata.ResourceManagerKey)]; found {
 			nt.T.Logf("[RESET] RootSync %s managed by %q", rsNN, manager)
-			if !IsDeletionPropagationEnabled(rs) {
+			if !metadata.HasDeletionPropagationPolicy(rs, metadata.DeletionPropagationPolicyForeground) {
 				// If you go this error, make sure your test cleanup ensures
 				// that the managed RootSync has deletion propagation enabled.
 				return fmt.Errorf("RootSync %s managed by %q does NOT have deletion propagation enabled: test reset incomplete", rsNN, manager)
@@ -166,7 +166,7 @@ func ResetRootSyncs(nt *NT, rsList []v1beta1.RootSync) error {
 		}
 
 		// Enable deletion propagation, if not enabled
-		if EnableDeletionPropagation(rs) {
+		if metadata.SetDeletionPropagationPolicy(rs, metadata.DeletionPropagationPolicyForeground) {
 			nt.T.Logf("[RESET] Enabling deletion propagation on RootSync %s", rsNN)
 			// Use MergePatch instead of Update in case modified since list
 			patch := fmt.Sprintf(`{"metadata": {"annotations": {"%s": "%s"}}}`,
@@ -232,7 +232,7 @@ func ResetRepoSyncs(nt *NT, rsList []v1beta1.RepoSync) error {
 		// If managed, skip direct deletion
 		if manager, found := rs.GetAnnotations()[string(metadata.ResourceManagerKey)]; found {
 			nt.T.Logf("[RESET] RepoSync %s managed by %q", rsNN, manager)
-			if !IsDeletionPropagationEnabled(rs) {
+			if !metadata.HasDeletionPropagationPolicy(rs, metadata.DeletionPropagationPolicyForeground) {
 				// If you go this error, make sure your test cleanup ensures
 				// that the managed RepoSync has deletion propagation enabled.
 				return fmt.Errorf("RepoSync %s managed by %q does NOT have deletion propagation enabled: test reset incomplete", rsNN, manager)
@@ -241,7 +241,7 @@ func ResetRepoSyncs(nt *NT, rsList []v1beta1.RepoSync) error {
 		}
 
 		// Enable deletion propagation, if not enabled
-		if EnableDeletionPropagation(rs) {
+		if metadata.SetDeletionPropagationPolicy(rs, metadata.DeletionPropagationPolicyForeground) {
 			nt.T.Logf("[RESET] Enabling deletion propagation on RepoSync %s", rsNN)
 			// Use MergePatch instead of Update in case modified since list
 			patch := fmt.Sprintf(`{"metadata": {"annotations": {"%s": "%s"}}}`,
diff --git a/e2e/testcases/reconciler_finalizer_test.go b/e2e/testcases/reconciler_finalizer_test.go
index d7b8b5ea23..c45abaaa7f 100644
--- a/e2e/testcases/reconciler_finalizer_test.go
+++ b/e2e/testcases/reconciler_finalizer_test.go
@@ -95,7 +95,7 @@ func TestReconcilerFinalizer_Orphan(t *testing.T) {
 	rootSync := &v1beta1.RootSync{}
 	nt.Must(nt.KubeClient.Get(rootSyncID.Name, rootSyncID.Namespace, rootSync))
 
-	if nomostest.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyOrphan) {
+	if metadata.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyOrphan) {
 		nt.Must(nt.KubeClient.Update(rootSync))
 	}
 	nt.Must(nt.Watcher.WatchObject(kinds.RootSyncV1Beta1(), rootSync.GetName(), rootSync.GetNamespace(),
@@ -103,7 +103,7 @@ func TestReconcilerFinalizer_Orphan(t *testing.T) {
 			testpredicates.StatusEquals(nt.Scheme, kstatus.CurrentStatus),
 			testpredicates.HasFinalizer(metadata.ReconcilerFinalizer),
 			testpredicates.HasAnnotation(metadata.DeletionPropagationPolicyAnnotationKey,
-				string(metadata.DeletionPropagationPolicyOrphan)),
+				metadata.DeletionPropagationPolicyOrphan.String()),
 		)))
 
 	// Delete the RootSync
@@ -138,7 +138,7 @@ func TestReconcilerFinalizer_Orphan(t *testing.T) {
 
 	// Recreate the RootSync
 	rootSync = nomostest.RootSyncObjectV1Beta1FromRootRepo(nt, rootSyncKey.Name)
-	nomostest.RemoveDeletionPropagationPolicy(rootSync)
+	metadata.RemoveDeletionPropagationPolicy(rootSync)
 	nt.Must(nt.KubeClient.Create(rootSync))
 	nt.Must(nt.WatchForAllSyncs())
 
@@ -196,7 +196,7 @@ func TestReconcilerFinalizer_Foreground(t *testing.T) {
 	if err != nil {
 		nt.T.Fatal(err)
 	}
-	if nomostest.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyForeground) {
+	if metadata.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyForeground) {
 		err = nt.KubeClient.Update(rootSync)
 		if err != nil {
 			nt.T.Fatal(err)
@@ -306,7 +306,7 @@ func TestReconcilerFinalizer_MultiLevelForeground(t *testing.T) {
 	if err != nil {
 		nt.T.Fatal(err)
 	}
-	if nomostest.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyForeground) {
+	if metadata.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyForeground) {
 		err = nt.KubeClient.Update(rootSync)
 		if err != nil {
 			nt.T.Fatal(err)
@@ -320,7 +320,7 @@ func TestReconcilerFinalizer_MultiLevelForeground(t *testing.T) {
 
 	nt.T.Log("Enabling RepoSync deletion propagation")
 	repoSync := rootSyncGitRepo.MustGet(nt.T, repoSyncPath)
-	if nomostest.SetDeletionPropagationPolicy(repoSync, metadata.DeletionPropagationPolicyForeground) {
+	if metadata.SetDeletionPropagationPolicy(repoSync, metadata.DeletionPropagationPolicyForeground) {
 		nt.Must(rootSyncGitRepo.Add(repoSyncPath, repoSync))
 		nt.Must(rootSyncGitRepo.CommitAndPush("Enabling RepoSync deletion propagation"))
 	}
@@ -423,7 +423,7 @@ func TestReconcilerFinalizer_MultiLevelMixed(t *testing.T) {
 	nt.T.Log("Enabling RootSync deletion propagation")
 	rootSync := &v1beta1.RootSync{}
 	nt.Must(nt.KubeClient.Get(rootSyncID.Name, rootSyncID.Namespace, rootSync))
-	if nomostest.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyForeground) {
+	if metadata.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyForeground) {
 		nt.Must(nt.KubeClient.Update(rootSync))
 	}
 	nt.Must(nt.Watcher.WatchObject(kinds.RootSyncV1Beta1(), rootSync.GetName(), rootSync.GetNamespace(),
@@ -434,7 +434,7 @@ func TestReconcilerFinalizer_MultiLevelMixed(t *testing.T) {
 
 	nt.T.Log("Disabling RepoSync deletion propagation")
 	repoSync := rootSyncGitRepo.MustGet(nt.T, repoSyncPath)
-	if nomostest.RemoveDeletionPropagationPolicy(repoSync) {
+	if metadata.RemoveDeletionPropagationPolicy(repoSync) {
 		nt.Must(rootSyncGitRepo.Add(repoSyncPath, repoSync))
 		nt.Must(rootSyncGitRepo.CommitAndPush("Disabling RepoSync deletion propagation"))
 		nt.Must(nt.WatchForAllSyncs())
@@ -722,7 +722,7 @@ func deleteSyncWithOrphanPolicy(nt *nomostest.NT, obj client.Object) error {
 	}
 
 	nt.T.Log("Removing deletion propagation annotation")
-	if nomostest.RemoveDeletionPropagationPolicy(obj) {
+	if metadata.RemoveDeletionPropagationPolicy(obj) {
 		err = nt.KubeClient.Update(obj)
 		if err != nil {
 			return err
diff --git a/e2e/testcases/reconciler_manager_test.go b/e2e/testcases/reconciler_manager_test.go
index 0e7eb4cda4..9ae6886f2d 100644
--- a/e2e/testcases/reconciler_manager_test.go
+++ b/e2e/testcases/reconciler_manager_test.go
@@ -1201,7 +1201,7 @@ func TestReconcilerManagerRootSyncCRDMissing(t *testing.T) {
 	// Enable RootSync deletion propagation, if not enabled
 	rootSync := &v1beta1.RootSync{}
 	nt.Must(nt.KubeClient.Get(rootSyncKey.Name, rootSyncKey.Namespace, rootSync))
-	if nomostest.EnableDeletionPropagation(rootSync) {
+	if metadata.SetDeletionPropagationPolicy(rootSync, metadata.DeletionPropagationPolicyForeground) {
 		nt.Must(nt.KubeClient.Update(rootSync))
 		nt.Must(nt.Watcher.WatchObject(kinds.RootSyncV1Beta1(), rootSync.Name, rootSync.Namespace,
 			testwatcher.WatchPredicates(
diff --git a/pkg/metadata/annotation_deletion_propagation_policy.go b/pkg/metadata/annotation_deletion_propagation_policy.go
new file mode 100644
index 0000000000..549284e1ee
--- /dev/null
+++ b/pkg/metadata/annotation_deletion_propagation_policy.go
@@ -0,0 +1,100 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadata
+
+import (
+	"kpt.dev/configsync/pkg/api/configsync"
+	"kpt.dev/configsync/pkg/core"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// DeletionPropagationPolicy is the type used to identify value enums to use
+// with the deletion-propagation-policy annotation.
+type DeletionPropagationPolicy string
+
+// String returns the string value of the DeletionPropagationPolicy.
+// Implements the Stringer interface.
+func (p DeletionPropagationPolicy) String() string {
+	return string(p)
+}
+
+const (
+	// DeletionPropagationPolicyAnnotationKey is the annotation key set on
+	// RootSync/RepoSync objects to indicate what do do with the managed
+	// resources when the RootSync/RepoSync object is deleted.
+	DeletionPropagationPolicyAnnotationKey = configsync.ConfigSyncPrefix + "deletion-propagation-policy"
+	// DeletionPropagationPolicyForeground indicates that the managed resources
+	// should all be deleted/pruned before the RootSync/RepoSync object is deleted.
+	// This will block deletion of the RootSync/RepoSync using a finalizer.
+	DeletionPropagationPolicyForeground DeletionPropagationPolicy = "Foreground"
+	// DeletionPropagationPolicyOrphan indicates that the managed resources
+	// should all be orphaned (unmanaged but not deleted) when the
+	// RootSync/RepoSync object is deleted.
+	// This is the default behavior if the annotation is not specified.
+	DeletionPropagationPolicyOrphan DeletionPropagationPolicy = "Orphan"
+)
+
+// HasDeletionPropagationPolicy returns true if deletion propagation annotation
+// is set to the specified policy. Returns false if not set.
+func HasDeletionPropagationPolicy(obj client.Object, policy DeletionPropagationPolicy) bool {
+	return core.GetAnnotation(obj, DeletionPropagationPolicyAnnotationKey) == policy.String()
+}
+
+// SetDeletionPropagationPolicy sets the value of the deletion propagation
+// annotation locally (does not apply). Returns true if the object was modified.
+func SetDeletionPropagationPolicy(obj client.Object, policy DeletionPropagationPolicy) bool {
+	return core.SetAnnotation(obj, DeletionPropagationPolicyAnnotationKey, policy.String())
+}
+
+// RemoveDeletionPropagationPolicy removes the deletion propagation annotation
+// locally (does not apply). Returns true if the object was modified.
+func RemoveDeletionPropagationPolicy(obj client.Object) bool {
+	return core.RemoveAnnotations(obj, DeletionPropagationPolicyAnnotationKey)
+}
+
+// WithDeletionPropagationPolicy returns a MetaMutator that sets the
+// DeletionPropagationPolicy annotation on an Object.
+func WithDeletionPropagationPolicy(mode DeletionPropagationPolicy) core.MetaMutator {
+	return core.Annotation(DeletionPropagationPolicyAnnotationKey, mode.String())
+}
+
+// WithoutDeletionPropagationPolicy returns a MetaMutator that removes the
+// DeletionPropagationPolicy annotation on an Object.
+func WithoutDeletionPropagationPolicy() core.MetaMutator {
+	return core.WithoutAnnotation(DeletionPropagationPolicyAnnotationKey)
+}
+
+// IsDeletionPropagationForeground returns true if the object has the annotation
+// `configsync.gke.io/deletion-propagation-policy: Foreground`.
+func IsDeletionPropagationForeground(obj client.Object) bool {
+	return core.GetAnnotation(obj, DeletionPropagationPolicyAnnotationKey) == DeletionPropagationPolicyForeground.String()
+}
+
+// IsDeletionPropagationOrphan returns true if the object has the annotation
+// `configsync.gke.io/deletion-propagation-policy: Orphan`.
+func IsDeletionPropagationOrphan(obj client.Object) bool {
+	return core.GetAnnotation(obj, DeletionPropagationPolicyAnnotationKey) == DeletionPropagationPolicyOrphan.String()
+}
+
+// IsDeletionPropagationUnspecified returns true if the object does NOT have an
+// annotation with the key `configsync.gke.io/deletion-propagation-policy`.
+func IsDeletionPropagationUnspecified(obj client.Object) bool {
+	annotations := obj.GetAnnotations()
+	if annotations == nil {
+		return true
+	}
+	_, found := annotations[DeletionPropagationPolicyAnnotationKey]
+	return !found
+}
diff --git a/pkg/metadata/annotations.go b/pkg/metadata/annotations.go
index 33d1d30d16..dfa190a0fd 100644
--- a/pkg/metadata/annotations.go
+++ b/pkg/metadata/annotations.go
@@ -111,11 +111,6 @@ const (
 	// to indicate that the scope of a resource is unknown.
 	UnknownScopeAnnotationValue = "true"
 
-	// DeletionPropagationPolicyAnnotationKey is the annotation key set on
-	// RootSync/RepoSync objects to indicate what do do with the managed
-	// resources when the RootSync/RepoSync object is deleted.
-	DeletionPropagationPolicyAnnotationKey = configsync.ConfigSyncPrefix + "deletion-propagation-policy"
-
 	// RequiresRenderingAnnotationKey is the annotation key set on
 	// RootSync/RepoSync objects to indicate whether the source of truth
 	// requires last mile hydration. The reconciler writes the value of this
@@ -206,33 +201,6 @@ const KustomizeOrigin = "config.kubernetes.io/origin"
 // FleetWorkloadIdentityCredentials is the key for the credentials file of the Fleet Workload Identity.
 const FleetWorkloadIdentityCredentials = "config.kubernetes.io/fleet-workload-identity"
 
-// DeletionPropagationPolicy is the type used to identify value enums to use
-// with the deletion-propagation-policy annotation.
-type DeletionPropagationPolicy string
-
-// String returns the string value of the DeletionPropagationPolicy.
-// Implements the Stringer interface.
-func (p DeletionPropagationPolicy) String() string {
-	return string(p)
-}
-
-const (
-	// DeletionPropagationPolicyForeground indicates that the managed resources
-	// should all be deleted/pruned before the RootSync/RepoSync object is deleted.
-	// This will block deletion of the RootSync/RepoSync using a finalizer.
-	DeletionPropagationPolicyForeground = DeletionPropagationPolicy("Foreground")
-
-	// DeletionPropagationPolicyOrphan indicates that the managed resources
-	// should all be orphanned (not deleted) when the RootSync/RepoSync object
-	// is deleted.
-	// This will NOT block deletion of the RootSync/RepoSync AND will not
-	// remove or modify any config sync managed annotations.
-	// This allows the RootSync/RepoSync to be deleted and re-created without
-	// affecting the managed resources.
-	// This is the default behavior if the annotation is not specified.
-	DeletionPropagationPolicyOrphan = DeletionPropagationPolicy("Orphan")
-)
-
 // StatusMode is the type used to identify value enums to use with the
 // `configsync.gke.io/status` annotation.
 type StatusMode string
diff --git a/pkg/reconciler/finalizer/controller.go b/pkg/reconciler/finalizer/controller.go
index 8da9a75a53..9c6ad09178 100644
--- a/pkg/reconciler/finalizer/controller.go
+++ b/pkg/reconciler/finalizer/controller.go
@@ -139,25 +139,11 @@ func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (reco
 	return result, nil
 }
 
-// reconcileFinalizer adds or removes the `configsync.gke.io/reconciler`
-// finalizer, depending on the existence and value of the
-// `configsync.gke.io/deletion-propagation-policy` annotation.
+// reconcileFinalizer adds the `configsync.gke.io/reconciler` finalizer.
+// Now that both Foreground and Orphan policies use the finalizer, always add it.
 func (c *Controller) reconcileFinalizer(ctx context.Context, obj client.Object) error {
-	policyStr, found := obj.GetAnnotations()[metadata.DeletionPropagationPolicyAnnotationKey]
-	policy := metadata.DeletionPropagationPolicy(policyStr)
-	if !found {
-		// Orphan is the default policy
-		policy = metadata.DeletionPropagationPolicyOrphan
-	}
-	switch policy {
-	case metadata.DeletionPropagationPolicyForeground, metadata.DeletionPropagationPolicyOrphan:
-		if _, err := c.Finalizer.AddFinalizer(ctx, obj); err != nil {
-			return err
-		}
-	default:
-		klog.Warningf("%T %s has an invalid value for the annotation %q: %q",
-			obj, client.ObjectKeyFromObject(obj), metadata.DeletionPropagationPolicyAnnotationKey, policy)
-		// User error. Retry won't help, so don't return the error.
+	if _, err := c.Finalizer.AddFinalizer(ctx, obj); err != nil {
+		return err
 	}
 	return nil
 }
diff --git a/pkg/reconciler/finalizer/reposync_finalizer_test.go b/pkg/reconciler/finalizer/reposync_finalizer_test.go
index 668184e282..1127a062c2 100644
--- a/pkg/reconciler/finalizer/reposync_finalizer_test.go
+++ b/pkg/reconciler/finalizer/reposync_finalizer_test.go
@@ -449,9 +449,7 @@ func TestRepoSyncFinalize(t *testing.T) {
 					},
 				},
 			}
-			core.SetAnnotation(tc.rsync,
-				metadata.DeletionPropagationPolicyAnnotationKey,
-				tc.deletionPolicy.String())
+			metadata.SetDeletionPropagationPolicy(tc.rsync, tc.deletionPolicy)
 			fakeClient := fake.NewClient(t, scheme, tc.rsync, cm, rg)
 			ctx := context.Background()
 
@@ -467,9 +465,7 @@ func TestRepoSyncFinalize(t *testing.T) {
 				rsync := &v1beta1.RepoSync{}
 				err := fakeClient.Get(context.Background(), key, rsync)
 				require.NoError(t, err)
-				core.SetAnnotation(tc.expectedRsyncBeforeDestroy,
-					metadata.DeletionPropagationPolicyAnnotationKey,
-					tc.deletionPolicy.String())
+				metadata.SetDeletionPropagationPolicy(tc.expectedRsyncBeforeDestroy, tc.deletionPolicy)
 				asserter.Equal(t, tc.expectedRsyncBeforeDestroy, rsync)
 				// Return errors, if any
 				return tc.destroyErrs
@@ -496,9 +492,7 @@ func TestRepoSyncFinalize(t *testing.T) {
 			assert.Equal(t, tc.expectedStopped, stopped)
 			expectedObjs := []client.Object{rg, wantCM}
 			if tc.expectedRsyncAfterFinalize != nil {
-				core.SetAnnotation(tc.expectedRsyncAfterFinalize,
-					metadata.DeletionPropagationPolicyAnnotationKey,
-					tc.deletionPolicy.String())
+				metadata.SetDeletionPropagationPolicy(tc.expectedRsyncAfterFinalize, tc.deletionPolicy)
 				expectedObjs = append(expectedObjs, tc.expectedRsyncAfterFinalize)
 			}
 			fakeClient.Check(t, expectedObjs...)
diff --git a/pkg/reconciler/finalizer/rootsync_finalizer_test.go b/pkg/reconciler/finalizer/rootsync_finalizer_test.go
index db339e5754..a3fcd7895c 100644
--- a/pkg/reconciler/finalizer/rootsync_finalizer_test.go
+++ b/pkg/reconciler/finalizer/rootsync_finalizer_test.go
@@ -66,9 +66,7 @@ spec:
 
 func TestRootSyncFinalize(t *testing.T) {
 	rootSync1 := yamlToTypedObject(t, rootSync1Yaml).(*v1beta1.RootSync)
-	core.SetAnnotation(rootSync1,
-		metadata.DeletionPropagationPolicyAnnotationKey,
-		string(metadata.DeletionPropagationPolicyForeground))
+	metadata.SetDeletionPropagationPolicy(rootSync1, metadata.DeletionPropagationPolicyForeground)
 	rootSync1.SetFinalizers([]string{
 		metadata.ReconcilerFinalizer,
 	})
@@ -458,9 +456,7 @@ func TestRootSyncFinalize(t *testing.T) {
 					},
 				},
 			}
-			core.SetAnnotation(tc.rsync,
-				metadata.DeletionPropagationPolicyAnnotationKey,
-				tc.deletionPolicy.String())
+			metadata.SetDeletionPropagationPolicy(tc.rsync, tc.deletionPolicy)
 
 			fakeClient := fake.NewClient(t, scheme, tc.rsync, cm, rg)
 			ctx := context.Background()
@@ -477,9 +473,7 @@ func TestRootSyncFinalize(t *testing.T) {
 				rsync := &v1beta1.RootSync{}
 				err := fakeClient.Get(context.Background(), key, rsync)
 				require.NoError(t, err)
-				core.SetAnnotation(tc.expectedRsyncBeforeDestroy,
-					metadata.DeletionPropagationPolicyAnnotationKey,
-					tc.deletionPolicy.String())
+				metadata.SetDeletionPropagationPolicy(tc.expectedRsyncBeforeDestroy, tc.deletionPolicy)
 				asserter.Equal(t, tc.expectedRsyncBeforeDestroy, rsync)
 				// Return errors, if any
 				return tc.destroyErrs
@@ -506,9 +500,7 @@ func TestRootSyncFinalize(t *testing.T) {
 			assert.Equal(t, tc.expectedStopped, stopped)
 			expectedObjs := []client.Object{rg, wantCM}
 			if tc.expectedRsyncAfterFinalize != nil {
-				core.SetAnnotation(tc.expectedRsyncAfterFinalize,
-					metadata.DeletionPropagationPolicyAnnotationKey,
-					tc.deletionPolicy.String())
+				metadata.SetDeletionPropagationPolicy(tc.expectedRsyncAfterFinalize, tc.deletionPolicy)
 				expectedObjs = append(expectedObjs, tc.expectedRsyncAfterFinalize)
 			}
 			fakeClient.Check(t, expectedObjs...)


Reviews:
user: login: sdowell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:00:14Z
user: login: karlkfi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:22:12Z
user: login: sdowell
body: /lgtm
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:49:22Z

Review comments:
diff_hunk: @@ -0,0 +1,104 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadata
+
+import (
+	"kpt.dev/configsync/pkg/api/configsync"
+	"kpt.dev/configsync/pkg/core"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// DeletionPropagationPolicy is the type used to identify value enums to use
+// with the deletion-propagation-policy annotation.
+type DeletionPropagationPolicy string
+
+// String returns the string value of the DeletionPropagationPolicy.
+// Implements the Stringer interface.
+func (p DeletionPropagationPolicy) String() string {
+	return string(p)
+}
+
+const (
+	// DeletionPropagationPolicyAnnotationKey is the annotation key set on
+	// RootSync/RepoSync objects to indicate what do do with the managed
+	// resources when the RootSync/RepoSync object is deleted.
+	DeletionPropagationPolicyAnnotationKey = configsync.ConfigSyncPrefix + "deletion-propagation-policy"
+	// DeletionPropagationPolicyForeground indicates that the managed resources
+	// should all be deleted/pruned before the RootSync/RepoSync object is deleted.
+	// This will block deletion of the RootSync/RepoSync using a finalizer.
+	DeletionPropagationPolicyForeground DeletionPropagationPolicy = "Foreground"
+	// DeletionPropagationPolicyOrphan indicates that the managed resources
+	// should all be orphanned (not deleted) when the RootSync/RepoSync object
+	// is deleted.
+	// This will NOT block deletion of the RootSync/RepoSync AND will not
+	// remove or modify any config sync managed annotations.
path: pkg/metadata/annotation_deletion_propagation_policy.go
user: login: sdowell
body: nit: this comment can be updated as this now removes CS metadata
created_at: 2025-04-10T20:00:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metadata
+
+import (
+	"kpt.dev/configsync/pkg/api/configsync"
+	"kpt.dev/configsync/pkg/core"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// DeletionPropagationPolicy is the type used to identify value enums to use
+// with the deletion-propagation-policy annotation.
+type DeletionPropagationPolicy string
+
+// String returns the string value of the DeletionPropagationPolicy.
+// Implements the Stringer interface.
+func (p DeletionPropagationPolicy) String() string {
+	return string(p)
+}
+
+const (
+	// DeletionPropagationPolicyAnnotationKey is the annotation key set on
+	// RootSync/RepoSync objects to indicate what do do with the managed
+	// resources when the RootSync/RepoSync object is deleted.
+	DeletionPropagationPolicyAnnotationKey = configsync.ConfigSyncPrefix + "deletion-propagation-policy"
+	// DeletionPropagationPolicyForeground indicates that the managed resources
+	// should all be deleted/pruned before the RootSync/RepoSync object is deleted.
+	// This will block deletion of the RootSync/RepoSync using a finalizer.
+	DeletionPropagationPolicyForeground DeletionPropagationPolicy = "Foreground"
+	// DeletionPropagationPolicyOrphan indicates that the managed resources
+	// should all be orphanned (not deleted) when the RootSync/RepoSync object
+	// is deleted.
+	// This will NOT block deletion of the RootSync/RepoSync AND will not
+	// remove or modify any config sync managed annotations.
path: pkg/metadata/annotation_deletion_propagation_policy.go
user: login: karlkfi
body: Done
created_at: 2025-04-10T20:22:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: google-oss-prow[bot]
created_at: 2025-04-10T20:49:28Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/GoogleContainerTools/kpt-config-sync/pull/1639#pullrequestreview-2758363272" title="LGTM">sdowell</a>*

The full list of commands accepted by this bot can be found [here](https://oss.gprow.dev/command-help?repo=GoogleContainerTools%2Fkpt-config-sync).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/GoogleContainerTools/kpt-config-sync/blob/main/OWNERS)~~ [sdowell]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE