Просмотр исходного кода

feat: implement deletionPolicy (#900)

* feat: implement deletionPolicy

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Gustavo Fernandes de Carvalho <gustavo.carvalho@container-solutions.com>
Moritz Johner 4 лет назад
Родитель
Сommit
c2bcceb057
28 измененных файлов с 628 добавлено и 50 удалено
  1. 1 1
      apis/externalsecrets/v1alpha1/externalsecret_conversion.go
  2. 1 1
      apis/externalsecrets/v1alpha1/externalsecret_conversion_test.go
  3. 28 13
      apis/externalsecrets/v1beta1/externalsecret_types.go
  4. 52 0
      apis/externalsecrets/v1beta1/externalsecret_validator.go
  5. 1 0
      apis/externalsecrets/v1beta1/externalsecret_webhook.go
  6. 12 0
      apis/externalsecrets/v1beta1/provider.go
  7. 30 0
      apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
  8. 11 2
      config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml
  9. 11 2
      config/crds/bases/external-secrets.io_externalsecrets.yaml
  10. 25 0
      deploy/charts/external-secrets/templates/validatingwebhook.yaml
  11. 22 4
      deploy/crds/bundle.yaml
  12. 50 0
      docs/guides-ownership-deletion-policy.md
  13. 8 3
      docs/snippets/full-external-secret.yaml
  14. 1 1
      e2e/framework/log/log.go
  15. 4 0
      e2e/framework/testcase.go
  16. 1 0
      e2e/suite/aws/parameterstore/parameterstore.go
  17. 5 0
      e2e/suite/aws/parameterstore/provider.go
  18. 5 0
      e2e/suite/aws/secretsmanager/provider.go
  19. 1 0
      e2e/suite/aws/secretsmanager/secretsmanager.go
  20. 49 0
      e2e/suite/common/common.go
  21. 1 0
      hack/api-docs/mkdocs.yml
  22. 117 12
      pkg/controllers/externalsecret/externalsecret_controller.go
  23. 169 6
      pkg/controllers/externalsecret/externalsecret_controller_test.go
  24. 5 0
      pkg/provider/aws/parameterstore/parameterstore.go
  25. 7 0
      pkg/provider/aws/secretsmanager/secretsmanager.go
  26. 2 3
      pkg/provider/fake/fake.go
  27. 2 2
      pkg/provider/fake/fake_test.go
  28. 7 0
      pkg/provider/testing/fake/fake.go

+ 1 - 1
apis/externalsecrets/v1alpha1/externalsecret_conversion.go

@@ -24,7 +24,7 @@ import (
 
 
 func (alpha *ExternalSecret) ConvertTo(betaRaw conversion.Hub) error {
 func (alpha *ExternalSecret) ConvertTo(betaRaw conversion.Hub) error {
 	beta := betaRaw.(*esv1beta1.ExternalSecret)
 	beta := betaRaw.(*esv1beta1.ExternalSecret)
-	// Actual converted code thatn eeds to be like this
+	// Actual converted code that needs to be like this
 	v1beta1DataFrom := make([]esv1beta1.ExternalSecretDataFromRemoteRef, 0)
 	v1beta1DataFrom := make([]esv1beta1.ExternalSecretDataFromRemoteRef, 0)
 	for _, v1alpha1RemoteRef := range alpha.Spec.DataFrom {
 	for _, v1alpha1RemoteRef := range alpha.Spec.DataFrom {
 		v1beta1RemoteRef := esv1beta1.ExternalSecretDataFromRemoteRef{
 		v1beta1RemoteRef := esv1beta1.ExternalSecretDataFromRemoteRef{

+ 1 - 1
apis/externalsecrets/v1alpha1/externalsecret_conversion_test.go

@@ -134,7 +134,7 @@ func newExternalSecretV1Beta1() *esv1beta1.ExternalSecret {
 			},
 			},
 			Target: esv1beta1.ExternalSecretTarget{
 			Target: esv1beta1.ExternalSecretTarget{
 				Name:           "test-target",
 				Name:           "test-target",
-				CreationPolicy: esv1beta1.Owner,
+				CreationPolicy: esv1beta1.CreatePolicyOwner,
 				Immutable:      false,
 				Immutable:      false,
 				Template: &esv1beta1.ExternalSecretTemplate{
 				Template: &esv1beta1.ExternalSecretTemplate{
 					Type: corev1.SecretTypeOpaque,
 					Type: corev1.SecretTypeOpaque,

+ 28 - 13
apis/externalsecrets/v1beta1/externalsecret_types.go

@@ -31,31 +31,45 @@ type SecretStoreRef struct {
 }
 }
 
 
 // ExternalSecretCreationPolicy defines rules on how to create the resulting Secret.
 // ExternalSecretCreationPolicy defines rules on how to create the resulting Secret.
+// +kubebuilder:validation:Enum=Owner;Orphan;Merge;None
 type ExternalSecretCreationPolicy string
 type ExternalSecretCreationPolicy string
 
 
 const (
 const (
 	// Owner creates the Secret and sets .metadata.ownerReferences to the ExternalSecret resource.
 	// Owner creates the Secret and sets .metadata.ownerReferences to the ExternalSecret resource.
-	Owner ExternalSecretCreationPolicy = "Owner"
+	CreatePolicyOwner ExternalSecretCreationPolicy = "Owner"
+
+	// Orphan creates the Secret and does not set the ownerReference.
+	// I.e. it will be orphaned after the deletion of the ExternalSecret.
+	CreatePolicyOrphan ExternalSecretCreationPolicy = "Orphan"
 
 
 	// Merge does not create the Secret, but merges the data fields to the Secret.
 	// Merge does not create the Secret, but merges the data fields to the Secret.
-	Merge ExternalSecretCreationPolicy = "Merge"
+	CreatePolicyMerge ExternalSecretCreationPolicy = "Merge"
 
 
 	// None does not create a Secret (future use with injector).
 	// None does not create a Secret (future use with injector).
-	None ExternalSecretCreationPolicy = "None"
+	CreatePolicyNone ExternalSecretCreationPolicy = "None"
 )
 )
 
 
 // ExternalSecretDeletionPolicy defines rules on how to delete the resulting Secret.
 // ExternalSecretDeletionPolicy defines rules on how to delete the resulting Secret.
+// +kubebuilder:validation:Enum=Delete;Merge;Retain
 type ExternalSecretDeletionPolicy string
 type ExternalSecretDeletionPolicy string
 
 
 const (
 const (
-	// Owner creates the Secret and sets .metadata.ownerReferences to the ExternalSecret resource.
-	DeletionOwner ExternalSecretDeletionPolicy = "Owner"
-
-	// Merge does not create the Secret, but merges the data fields to the Secret.
-	DeletionMerge ExternalSecretDeletionPolicy = "Merge"
-
-	// None does not create a Secret (future use with injector).
-	DeletionNone ExternalSecretDeletionPolicy = "None"
+	// Delete deletes the secret if all provider secrets are deleted.
+	// If a secret gets deleted on the provider side and is not accessible
+	// anymore this is not considered an error and the ExternalSecret
+	// does not go into SecretSyncedError status.
+	DeletionPolicyDelete ExternalSecretDeletionPolicy = "Delete"
+
+	// Merge removes keys in the secret, but not the secret itself.
+	// If a secret gets deleted on the provider side and is not accessible
+	// anymore this is not considered an error and the ExternalSecret
+	// does not go into SecretSyncedError status.
+	DeletionPolicyMerge ExternalSecretDeletionPolicy = "Merge"
+
+	// Retain will retain the secret if all provider secrets have been deleted.
+	// If a provider secret does not exist the ExternalSecret gets into the
+	// SecretSyncedError status.
+	DeletionPolicyRetain ExternalSecretDeletionPolicy = "Retain"
 )
 )
 
 
 // ExternalSecretTemplateMetadata defines metadata fields for the Secret blueprint.
 // ExternalSecretTemplateMetadata defines metadata fields for the Secret blueprint.
@@ -127,9 +141,9 @@ type ExternalSecretTarget struct {
 	// +kubebuilder:default="Owner"
 	// +kubebuilder:default="Owner"
 	CreationPolicy ExternalSecretCreationPolicy `json:"creationPolicy,omitempty"`
 	CreationPolicy ExternalSecretCreationPolicy `json:"creationPolicy,omitempty"`
 	// DeletionPolicy defines rules on how to delete the resulting Secret
 	// DeletionPolicy defines rules on how to delete the resulting Secret
-	// Defaults to 'None'
+	// Defaults to 'Retain'
 	// +optional
 	// +optional
-	// +kubebuilder:default="None"
+	// +kubebuilder:default="Retain"
 	DeletionPolicy ExternalSecretDeletionPolicy `json:"deletionPolicy,omitempty"`
 	DeletionPolicy ExternalSecretDeletionPolicy `json:"deletionPolicy,omitempty"`
 	// Template defines a blueprint for the created Secret resource.
 	// Template defines a blueprint for the created Secret resource.
 	// +optional
 	// +optional
@@ -262,6 +276,7 @@ const (
 	ReasonProviderClientConfig = "InvalidProviderClientConfig"
 	ReasonProviderClientConfig = "InvalidProviderClientConfig"
 	ReasonUpdateFailed         = "UpdateFailed"
 	ReasonUpdateFailed         = "UpdateFailed"
 	ReasonUpdated              = "Updated"
 	ReasonUpdated              = "Updated"
+	ReasonDeleted              = "Deleted"
 )
 )
 
 
 type ExternalSecretStatus struct {
 type ExternalSecretStatus struct {

+ 52 - 0
apis/externalsecrets/v1beta1/externalsecret_validator.go

@@ -0,0 +1,52 @@
+/*
+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 v1beta1
+
+import (
+	"context"
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+type ExternalSecretValidator struct{}
+
+func (esv *ExternalSecretValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error {
+	return validateExternalSecret(obj)
+}
+
+func (esv *ExternalSecretValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
+	return validateExternalSecret(newObj)
+}
+
+func (esv *ExternalSecretValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error {
+	return nil
+}
+
+func validateExternalSecret(obj runtime.Object) error {
+	es, ok := obj.(*ExternalSecret)
+	if !ok {
+		return fmt.Errorf("unexpected type")
+	}
+
+	if (es.Spec.Target.DeletionPolicy == DeletionPolicyDelete && es.Spec.Target.CreationPolicy == CreatePolicyMerge) ||
+		(es.Spec.Target.DeletionPolicy == DeletionPolicyDelete && es.Spec.Target.CreationPolicy == CreatePolicyNone) {
+		return fmt.Errorf("deletionPolicy=Delete must not be used when the controller doesn't own the secret. Please set creationPolcy=Owner")
+	}
+
+	if es.Spec.Target.DeletionPolicy == DeletionPolicyMerge && es.Spec.Target.CreationPolicy == CreatePolicyNone {
+		return fmt.Errorf("deletionPolicy=Merge must not be used with creationPolcy=None. There is no Secret to merge with")
+	}
+	return nil
+}

+ 1 - 0
apis/externalsecrets/v1beta1/externalsecret_webhook.go

@@ -21,5 +21,6 @@ import (
 func (r *ExternalSecret) SetupWebhookWithManager(mgr ctrl.Manager) error {
 func (r *ExternalSecret) SetupWebhookWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewWebhookManagedBy(mgr).
 	return ctrl.NewWebhookManagedBy(mgr).
 		For(r).
 		For(r).
+		WithValidator(&ExternalSecretValidator{}).
 		Complete()
 		Complete()
 }
 }

+ 12 - 0
apis/externalsecrets/v1beta1/provider.go

@@ -42,6 +42,8 @@ type Provider interface {
 // SecretsClient provides access to secrets.
 // SecretsClient provides access to secrets.
 type SecretsClient interface {
 type SecretsClient interface {
 	// GetSecret returns a single secret from the provider
 	// GetSecret returns a single secret from the provider
+	// if GetSecret returns an error with type NoSecretError
+	// then the secret entry will be deleted depending on the deletionPolicy.
 	GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error)
 	GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error)
 
 
 	// Validate checks if the client is configured correctly
 	// Validate checks if the client is configured correctly
@@ -56,3 +58,13 @@ type SecretsClient interface {
 
 
 	Close(ctx context.Context) error
 	Close(ctx context.Context) error
 }
 }
+
+var NoSecretErr = NoSecretError{}
+
+// NoSecretError shall be returned when a GetSecret can not find the
+// desired secret. This is used for deletionPolicy.
+type NoSecretError struct{}
+
+func (NoSecretError) Error() string {
+	return "Secret does not exist"
+}

+ 30 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -821,6 +821,21 @@ func (in *ExternalSecretTemplateMetadata) DeepCopy() *ExternalSecretTemplateMeta
 	return out
 	return out
 }
 }
 
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalSecretValidator) DeepCopyInto(out *ExternalSecretValidator) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretValidator.
+func (in *ExternalSecretValidator) DeepCopy() *ExternalSecretValidator {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalSecretValidator)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *FakeProvider) DeepCopyInto(out *FakeProvider) {
 func (in *FakeProvider) DeepCopyInto(out *FakeProvider) {
 	*out = *in
 	*out = *in
@@ -1141,6 +1156,21 @@ func (in *KubernetesServer) DeepCopy() *KubernetesServer {
 	return out
 	return out
 }
 }
 
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NoSecretError) DeepCopyInto(out *NoSecretError) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NoSecretError.
+func (in *NoSecretError) DeepCopy() *NoSecretError {
+	if in == nil {
+		return nil
+	}
+	out := new(NoSecretError)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OracleAuth) DeepCopyInto(out *OracleAuth) {
 func (in *OracleAuth) DeepCopyInto(out *OracleAuth) {
 	*out = *in
 	*out = *in

+ 11 - 2
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -168,11 +168,20 @@ spec:
                         default: Owner
                         default: Owner
                         description: CreationPolicy defines rules on how to create
                         description: CreationPolicy defines rules on how to create
                           the resulting Secret Defaults to 'Owner'
                           the resulting Secret Defaults to 'Owner'
+                        enum:
+                        - Owner
+                        - Orphan
+                        - Merge
+                        - None
                         type: string
                         type: string
                       deletionPolicy:
                       deletionPolicy:
-                        default: None
+                        default: Retain
                         description: DeletionPolicy defines rules on how to delete
                         description: DeletionPolicy defines rules on how to delete
-                          the resulting Secret Defaults to 'None'
+                          the resulting Secret Defaults to 'Retain'
+                        enum:
+                        - Delete
+                        - Merge
+                        - Retain
                         type: string
                         type: string
                       immutable:
                       immutable:
                         description: Immutable defines if the final secret will be
                         description: Immutable defines if the final secret will be

+ 11 - 2
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -412,11 +412,20 @@ spec:
                     default: Owner
                     default: Owner
                     description: CreationPolicy defines rules on how to create the
                     description: CreationPolicy defines rules on how to create the
                       resulting Secret Defaults to 'Owner'
                       resulting Secret Defaults to 'Owner'
+                    enum:
+                    - Owner
+                    - Orphan
+                    - Merge
+                    - None
                     type: string
                     type: string
                   deletionPolicy:
                   deletionPolicy:
-                    default: None
+                    default: Retain
                     description: DeletionPolicy defines rules on how to delete the
                     description: DeletionPolicy defines rules on how to delete the
-                      resulting Secret Defaults to 'None'
+                      resulting Secret Defaults to 'Retain'
+                    enum:
+                    - Delete
+                    - Merge
+                    - Retain
                     type: string
                     type: string
                   immutable:
                   immutable:
                     description: Immutable defines if the final secret will be immutable
                     description: Immutable defines if the final secret will be immutable

+ 25 - 0
deploy/charts/external-secrets/templates/validatingwebhook.yaml

@@ -40,4 +40,29 @@ webhooks:
   admissionReviewVersions: ["v1", "v1beta1"]
   admissionReviewVersions: ["v1", "v1beta1"]
   sideEffects: None
   sideEffects: None
   timeoutSeconds: 5
   timeoutSeconds: 5
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+  name: externalsecret-validate
+  labels:
+    external-secrets.io/component: webhook
+webhooks:
+- name: "validate.externalsecret.external-secrets.io"
+  rules:
+  - apiGroups:   ["external-secrets.io"]
+    apiVersions: ["v1beta1"]
+    operations:  ["CREATE", "UPDATE", "DELETE"]
+    resources:   ["externalsecrets"]
+    scope:       "Namespaced"
+  clientConfig:
+    service:
+      namespace: {{ .Release.Namespace | quote }}
+      name: {{ include "external-secrets.fullname" . }}-webhook
+      path: /validate-external-secrets-io-v1beta1-externalsecret
+    # will be set by controller
+    caBundle: Cg==
+  admissionReviewVersions: ["v1", "v1beta1"]
+  sideEffects: None
+  timeoutSeconds: 5
 {{- end }}
 {{- end }}

+ 22 - 4
deploy/crds/bundle.yaml

@@ -143,10 +143,19 @@ spec:
                         creationPolicy:
                         creationPolicy:
                           default: Owner
                           default: Owner
                           description: CreationPolicy defines rules on how to create the resulting Secret Defaults to 'Owner'
                           description: CreationPolicy defines rules on how to create the resulting Secret Defaults to 'Owner'
+                          enum:
+                            - Owner
+                            - Orphan
+                            - Merge
+                            - None
                           type: string
                           type: string
                         deletionPolicy:
                         deletionPolicy:
-                          default: None
-                          description: DeletionPolicy defines rules on how to delete the resulting Secret Defaults to 'None'
+                          default: Retain
+                          description: DeletionPolicy defines rules on how to delete the resulting Secret Defaults to 'Retain'
+                          enum:
+                            - Delete
+                            - Merge
+                            - Retain
                           type: string
                           type: string
                         immutable:
                         immutable:
                           description: Immutable defines if the final secret will be immutable
                           description: Immutable defines if the final secret will be immutable
@@ -2730,10 +2739,19 @@ spec:
                     creationPolicy:
                     creationPolicy:
                       default: Owner
                       default: Owner
                       description: CreationPolicy defines rules on how to create the resulting Secret Defaults to 'Owner'
                       description: CreationPolicy defines rules on how to create the resulting Secret Defaults to 'Owner'
+                      enum:
+                        - Owner
+                        - Orphan
+                        - Merge
+                        - None
                       type: string
                       type: string
                     deletionPolicy:
                     deletionPolicy:
-                      default: None
-                      description: DeletionPolicy defines rules on how to delete the resulting Secret Defaults to 'None'
+                      default: Retain
+                      description: DeletionPolicy defines rules on how to delete the resulting Secret Defaults to 'Retain'
+                      enum:
+                        - Delete
+                        - Merge
+                        - Retain
                       type: string
                       type: string
                     immutable:
                     immutable:
                       description: Immutable defines if the final secret will be immutable
                       description: Immutable defines if the final secret will be immutable

+ 50 - 0
docs/guides-ownership-deletion-policy.md

@@ -0,0 +1,50 @@
+# Lifecycle
+The External Secrets Operator manages the lifecycle of secrets in Kubernetes. With `creationPolicy` and `deletionPolicy` you get fine-grained control of its lifecycle.
+
+!!! note "Creation/Deletion Policy Combinations"
+    Some combinations of creationPolicy/deletionPolicy are not allowed as they would delete existing secrets:
+    <br/>- `deletionPolicy=Delete` & `creationPolicy=Merge`
+    <br/>- `deletionPolicy=Delete` & `creationPolicy=None`
+    <br/>- `deletionPolicy=Merge` & `creationPolicy=None`
+
+## Creation Policy
+The field `spec.creationPolicy` defines how the operator creates the a secret.
+
+### Owner (default)
+The External Secret Operator creates secret and sets the `ownerReference` field on the Secret. This secret is subject to [garbage collection](https://kubernetes.io/docs/concepts/architecture/garbage-collection/) if the initial `ExternalSecret` is absent. If a secret with the same name already exists that is not owned by the controller it will result in a conflict. The operator will just error out, not claiming the ownership.
+
+### Orphan
+The operator creates ths secret but does not set the `ownerReference` on the Secret. That means the Secret will not be subject to garbage collection. If a secret with the same name already exists it will be updated.
+
+### Merge
+The operator does not create a secret. Instead, it expects the secret to already exist. Values from the secret provider will be merged into the existing secret. Note: the controller takes ownership of a field even if it is owned by a different entity. Multiple ExternalSecrets can use `creationPolicy=Merge` with a single secret as long as the fields don't collide - otherwise you end up in an oscillating state.
+
+### None
+The operator does not create or update the secret, this is basically a no-op.
+
+## Deletion Policy
+DeletionPolicy defines what should happen if a given secret gets deleted **from the provider**.
+
+DeletionPolicy is only supported on the following providers. Please feel free to contribute more:
+* AWS Secrets Manager
+* AWS Parameter Store
+
+### Retain (default)
+Retain will retain the secret if all provider secrets have been deleted.
+If a provider secret does not exist the ExternalSecret gets into the
+SecretSyncedError status.
+
+### Delete
+Delete deletes the secret if all provider secrets are deleted.
+If a secret gets deleted on the provider side and is not accessible
+anymore this is not considered an error and the ExternalSecret
+does not go into SecretSyncedError status. This is also true for new
+ExternalSecrets mapping to non-existing secrets in the provider.
+
+### Merge
+Merge removes keys in the secret, but not the secret itself.
+If a secret gets deleted on the provider side and is not accessible
+anymore this is not considered an error and the ExternalSecret
+does not go into SecretSyncedError status.
+
+

+ 8 - 3
docs/snippets/full-external-secret.yaml

@@ -39,6 +39,11 @@ spec:
     # None does not create a secret (future use with injector)
     # None does not create a secret (future use with injector)
     creationPolicy: 'Merge'
     creationPolicy: 'Merge'
 
 
+    # DeletionPolicy defines how/when to delete the Secret in Kubernetes
+    # if the provider secret gets deleted.
+    # Valid values are Delete, Merge, Retain
+    deletionPolicy: "Retain"
+
     # Specify a blueprint for the resulting Kind=Secret
     # Specify a blueprint for the resulting Kind=Secret
     template:
     template:
       type: kubernetes.io/dockerconfigjson # or TLS...
       type: kubernetes.io/dockerconfigjson # or TLS...
@@ -79,10 +84,10 @@ spec:
       property: provider-key-property
       property: provider-key-property
       conversionStrategy: Default
       conversionStrategy: Default
   - find:
   - find:
-      path: path-to-filter 
-      name: 
+      path: path-to-filter
+      name:
         regexp: ".*foobar.*"
         regexp: ".*foobar.*"
-      tags: 
+      tags:
         foo: bar
         foo: bar
       conversionStrategy: Unicode
       conversionStrategy: Unicode
 
 

+ 1 - 1
e2e/framework/log/log.go

@@ -19,5 +19,5 @@ import (
 
 
 // Logf logs the format string to ginkgo stdout.
 // Logf logs the format string to ginkgo stdout.
 func Logf(format string, args ...interface{}) {
 func Logf(format string, args ...interface{}) {
-	ginkgo.GinkgoWriter.Printf(format, args)
+	ginkgo.GinkgoWriter.Printf(format, args...)
 }
 }

+ 4 - 0
e2e/framework/testcase.go

@@ -35,6 +35,7 @@ type TestCase struct {
 	ExternalSecretV1Alpha1 *esv1alpha1.ExternalSecret
 	ExternalSecretV1Alpha1 *esv1alpha1.ExternalSecret
 	Secrets                map[string]SecretEntry
 	Secrets                map[string]SecretEntry
 	ExpectedSecret         *v1.Secret
 	ExpectedSecret         *v1.Secret
+	AfterSync              func(SecretStoreProvider, *v1.Secret)
 }
 }
 
 
 type SecretEntry struct {
 type SecretEntry struct {
@@ -92,11 +93,14 @@ func TableFunc(f *Framework, prov SecretStoreProvider) func(...func(*TestCase))
 		}
 		}
 
 
 		Expect(err).ToNot(HaveOccurred())
 		Expect(err).ToNot(HaveOccurred())
+
+		tc.AfterSync(prov, secret)
 	}
 	}
 }
 }
 
 
 func makeDefaultTestCase(f *Framework) *TestCase {
 func makeDefaultTestCase(f *Framework) *TestCase {
 	return &TestCase{
 	return &TestCase{
+		AfterSync: func(ssp SecretStoreProvider, s *v1.Secret) {},
 		Framework: f,
 		Framework: f,
 		ExternalSecret: &esv1beta1.ExternalSecret{
 		ExternalSecret: &esv1beta1.ExternalSecret{
 			ObjectMeta: metav1.ObjectMeta{
 			ObjectMeta: metav1.ObjectMeta{

+ 1 - 0
e2e/suite/aws/parameterstore/parameterstore.go

@@ -42,6 +42,7 @@ var _ = Describe("[aws] ", Label("aws", "parameterstore"), func() {
 		Entry(common.SyncWithoutTargetName(f)),
 		Entry(common.SyncWithoutTargetName(f)),
 		Entry(common.JSONDataWithoutTargetName(f)),
 		Entry(common.JSONDataWithoutTargetName(f)),
 		Entry(common.SyncV1Alpha1(f)),
 		Entry(common.SyncV1Alpha1(f)),
+		Entry(common.DeletionPolicyDelete(f)),
 
 
 		// These are specific to parameterstore
 		// These are specific to parameterstore
 		Entry(FindByName(f)),
 		Entry(FindByName(f)),

+ 5 - 0
e2e/suite/aws/parameterstore/provider.go

@@ -16,6 +16,7 @@ package aws
 
 
 import (
 import (
 	"context"
 	"context"
+	"errors"
 	"os"
 	"os"
 
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws"
@@ -118,6 +119,10 @@ func (s *Provider) DeleteSecret(key string) {
 	_, err := s.client.DeleteParameter(&ssm.DeleteParameterInput{
 	_, err := s.client.DeleteParameter(&ssm.DeleteParameterInput{
 		Name: aws.String(key),
 		Name: aws.String(key),
 	})
 	})
+	var nf *ssm.ParameterNotFound
+	if errors.As(err, &nf) {
+		return
+	}
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 }
 }
 
 

+ 5 - 0
e2e/suite/aws/secretsmanager/provider.go

@@ -16,6 +16,7 @@ package aws
 
 
 import (
 import (
 	"context"
 	"context"
+	"errors"
 	"os"
 	"os"
 	"time"
 	"time"
 
 
@@ -136,6 +137,10 @@ func (s *Provider) DeleteSecret(key string) {
 		SecretId:                   aws.String(key),
 		SecretId:                   aws.String(key),
 		ForceDeleteWithoutRecovery: aws.Bool(true),
 		ForceDeleteWithoutRecovery: aws.Bool(true),
 	})
 	})
+	var nf *secretsmanager.ResourceNotFoundException
+	if errors.As(err, &nf) {
+		return
+	}
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())
 }
 }
 
 

+ 1 - 0
e2e/suite/aws/secretsmanager/secretsmanager.go

@@ -46,5 +46,6 @@ var _ = Describe("[aws] ", Label("aws", "secretsmanager"), func() {
 		Entry(common.FindByTag(f)),
 		Entry(common.FindByTag(f)),
 		Entry(common.FindByTagWithPath(f)),
 		Entry(common.FindByTagWithPath(f)),
 		Entry(common.SyncV1Alpha1(f)),
 		Entry(common.SyncV1Alpha1(f)),
+		Entry(common.DeletionPolicyDelete(f)),
 	)
 	)
 })
 })

+ 49 - 0
e2e/suite/common/common.go

@@ -13,9 +13,13 @@ limitations under the License.
 package common
 package common
 
 
 import (
 import (
+	"context"
 	"fmt"
 	"fmt"
+	"time"
 
 
+	"github.com/onsi/gomega"
 	v1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@@ -556,3 +560,48 @@ func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.Tes
 		}
 		}
 	}
 	}
 }
 }
+
+func DeletionPolicyDelete(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should delete secret when provider secret was deleted using .data[]", func(tc *framework.TestCase) {
+		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
+		secretValue := "bazz"
+		tc.Secrets = map[string]framework.SecretEntry{
+			secretKey1: {Value: secretValue},
+			secretKey2: {Value: secretValue},
+		}
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				secretKey1: []byte(secretValue),
+				secretKey2: []byte(secretValue),
+			},
+		}
+
+		tc.ExternalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second * 5}
+		tc.ExternalSecret.Spec.Target.DeletionPolicy = esv1beta1.DeletionPolicyDelete
+		tc.ExternalSecret.Spec.Data = []esv1beta1.ExternalSecretData{
+			{
+				SecretKey: secretKey1,
+				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
+					Key: secretKey1,
+				},
+			},
+			{
+				SecretKey: secretKey2,
+				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
+					Key: secretKey2,
+				},
+			},
+		}
+		tc.AfterSync = func(prov framework.SecretStoreProvider, secret *v1.Secret) {
+			prov.DeleteSecret(secretKey1)
+			prov.DeleteSecret(secretKey2)
+
+			gomega.Eventually(func() bool {
+				_, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Get(context.Background(), secret.Name, metav1.GetOptions{})
+				return errors.IsNotFound(err)
+			}, time.Minute, time.Second*5).Should(gomega.BeTrue())
+		}
+	}
+}

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -41,6 +41,7 @@ nav:
     - All keys, One secret: guides-all-keys-one-secret.md
     - All keys, One secret: guides-all-keys-one-secret.md
     - Common K8S Secret Types: guides-common-k8s-secret-types.md
     - Common K8S Secret Types: guides-common-k8s-secret-types.md
     - Controller Classes: guides-controller-class.md
     - Controller Classes: guides-controller-class.md
+    - "Lifecycle: ownership & deletion": guides-ownership-deletion-policy.md
     - Getting Multiple Secrets: guides-getallsecrets.md
     - Getting Multiple Secrets: guides-getallsecrets.md
     - Multi Tenancy: guides-multi-tenancy.md
     - Multi Tenancy: guides-multi-tenancy.md
     - Metrics: guides-metrics.md
     - Metrics: guides-metrics.md

+ 117 - 12
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -16,7 +16,10 @@ package externalsecret
 
 
 import (
 import (
 	"context"
 	"context"
+	"encoding/json"
+	"errors"
 	"fmt"
 	"fmt"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/go-logr/logr"
 	"github.com/go-logr/logr"
@@ -45,6 +48,8 @@ import (
 const (
 const (
 	requeueAfter = time.Second * 30
 	requeueAfter = time.Second * 30
 
 
+	fieldOwner = "external-secrets"
+
 	errGetES                 = "could not get ExternalSecret"
 	errGetES                 = "could not get ExternalSecret"
 	errConvert               = "could not apply conversion strategy to keys: %v"
 	errConvert               = "could not apply conversion strategy to keys: %v"
 	errUpdateSecret          = "could not update Secret"
 	errUpdateSecret          = "could not update Secret"
@@ -58,9 +63,11 @@ const (
 	errCloseStoreClient      = "could not close provider client"
 	errCloseStoreClient      = "could not close provider client"
 	errSetCtrlReference      = "could not set ExternalSecret controller reference: %w"
 	errSetCtrlReference      = "could not set ExternalSecret controller reference: %w"
 	errFetchTplFrom          = "error fetching templateFrom data: %w"
 	errFetchTplFrom          = "error fetching templateFrom data: %w"
-	errGetSecretData         = "could not get secret data from provider: %w"
+	errGetSecretData         = "could not get secret data from provider"
+	errDeleteSecret          = "could not delete secret"
 	errApplyTemplate         = "could not apply template: %w"
 	errApplyTemplate         = "could not apply template: %w"
 	errExecTpl               = "could not execute template: %w"
 	errExecTpl               = "could not execute template: %w"
+	errInvalidCreatePolicy   = "invalid creationPolicy=%s. Can not delete secret i do not own"
 	errPolicyMergeNotFound   = "the desired secret %s was not found. With creationPolicy=Merge the secret won't be created"
 	errPolicyMergeNotFound   = "the desired secret %s was not found. With creationPolicy=Merge the secret won't be created"
 	errPolicyMergeGetSecret  = "unable to get secret %s: %w"
 	errPolicyMergeGetSecret  = "unable to get secret %s: %w"
 	errPolicyMergeMutate     = "unable to mutate secret %s: %w"
 	errPolicyMergeMutate     = "unable to mutate secret %s: %w"
@@ -209,32 +216,89 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 		Data:      make(map[string][]byte),
 		Data:      make(map[string][]byte),
 	}
 	}
 
 
+	dataMap, err := r.getProviderSecretData(ctx, secretClient, &externalSecret)
+	if err != nil {
+		log.Error(err, errGetSecretData)
+		r.recorder.Event(&externalSecret, v1.EventTypeWarning, esv1beta1.ReasonUpdateFailed, err.Error())
+		conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionFalse, esv1beta1.ConditionReasonSecretSyncedError, errGetSecretData)
+		SetExternalSecretCondition(&externalSecret, *conditionSynced)
+		syncCallsError.With(syncCallsMetricLabels).Inc()
+		return ctrl.Result{RequeueAfter: requeueAfter}, nil
+	}
+
+	// if no data was found we can delete the secret if needed.
+	if len(dataMap) == 0 {
+		switch externalSecret.Spec.Target.DeletionPolicy {
+		// delete secret and return early.
+		case esv1beta1.DeletionPolicyDelete:
+			// safeguard that we only can delete secrets we own
+			// this is also implemented in the es validation webhook
+			if externalSecret.Spec.Target.CreationPolicy != esv1beta1.CreatePolicyOwner {
+				err := fmt.Errorf(errInvalidCreatePolicy, externalSecret.Spec.Target.CreationPolicy)
+				log.Error(err, errDeleteSecret)
+				r.recorder.Event(&externalSecret, v1.EventTypeWarning, esv1beta1.ReasonUpdateFailed, err.Error())
+				conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionFalse, esv1beta1.ConditionReasonSecretSyncedError, errDeleteSecret)
+				SetExternalSecretCondition(&externalSecret, *conditionSynced)
+				syncCallsError.With(syncCallsMetricLabels).Inc()
+				return ctrl.Result{RequeueAfter: requeueAfter}, nil
+			}
+			err = r.Delete(ctx, secret)
+			if err != nil && !apierrors.IsNotFound(err) {
+				log.Error(err, errDeleteSecret)
+				r.recorder.Event(&externalSecret, v1.EventTypeWarning, esv1beta1.ReasonUpdateFailed, err.Error())
+				conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionFalse, esv1beta1.ConditionReasonSecretSyncedError, errDeleteSecret)
+				SetExternalSecretCondition(&externalSecret, *conditionSynced)
+				syncCallsError.With(syncCallsMetricLabels).Inc()
+			}
+
+			conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionTrue, esv1beta1.ConditionReasonSecretDeleted, "secret deleted due to DeletionPolicy")
+			SetExternalSecretCondition(&externalSecret, *conditionSynced)
+			return ctrl.Result{RequeueAfter: requeueAfter}, nil
+
+		case esv1beta1.DeletionPolicyMerge:
+			// noop, handled below
+
+		// In case provider secrets don't exist the kubernetes secret will be kept as-is.
+		case esv1beta1.DeletionPolicyRetain:
+			return ctrl.Result{RequeueAfter: requeueAfter}, nil
+		}
+	}
+
 	mutationFunc := func() error {
 	mutationFunc := func() error {
-		if externalSecret.Spec.Target.CreationPolicy == esv1beta1.Owner {
+		if externalSecret.Spec.Target.CreationPolicy == esv1beta1.CreatePolicyOwner {
 			err = controllerutil.SetControllerReference(&externalSecret, &secret.ObjectMeta, r.Scheme)
 			err = controllerutil.SetControllerReference(&externalSecret, &secret.ObjectMeta, r.Scheme)
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf(errSetCtrlReference, err)
 				return fmt.Errorf(errSetCtrlReference, err)
 			}
 			}
 		}
 		}
-
-		dataMap, err := r.getProviderSecretData(ctx, secretClient, &externalSecret)
-		if err != nil {
-			return fmt.Errorf(errGetSecretData, err)
+		if secret.Data == nil {
+			secret.Data = make(map[string][]byte)
 		}
 		}
-
 		err = r.applyTemplate(ctx, &externalSecret, secret, dataMap)
 		err = r.applyTemplate(ctx, &externalSecret, secret, dataMap)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf(errApplyTemplate, err)
 			return fmt.Errorf(errApplyTemplate, err)
 		}
 		}
 
 
+		// diff existing keys
+		if externalSecret.Spec.Target.DeletionPolicy == esv1beta1.DeletionPolicyMerge {
+			keys, err := getManagedKeys(&existingSecret)
+			if err != nil {
+				return err
+			}
+			for _, key := range keys {
+				if dataMap[key] == nil {
+					secret.Data[key] = nil
+				}
+			}
+		}
 		return nil
 		return nil
 	}
 	}
 
 
 	// nolint
 	// nolint
 	switch externalSecret.Spec.Target.CreationPolicy {
 	switch externalSecret.Spec.Target.CreationPolicy {
-	case esv1beta1.Merge:
+	case esv1beta1.CreatePolicyMerge:
 		err = patchSecret(ctx, r.Client, r.Scheme, secret, mutationFunc)
 		err = patchSecret(ctx, r.Client, r.Scheme, secret, mutationFunc)
-	case esv1beta1.None:
+	case esv1beta1.CreatePolicyNone:
 		log.V(1).Info("secret creation skipped due to creationPolicy=None")
 		log.V(1).Info("secret creation skipped due to creationPolicy=None")
 		err = nil
 		err = nil
 	default:
 	default:
@@ -302,13 +366,42 @@ func patchSecret(ctx context.Context, c client.Client, scheme *runtime.Scheme, s
 
 
 	// we're not able to resolve conflicts so we force ownership
 	// we're not able to resolve conflicts so we force ownership
 	// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#using-server-side-apply-in-a-controller
 	// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#using-server-side-apply-in-a-controller
-	err = c.Patch(ctx, secret, client.Apply, client.FieldOwner("external-secrets"), client.ForceOwnership)
+	err = c.Patch(ctx, secret, client.Apply, client.FieldOwner(fieldOwner), client.ForceOwnership)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf(errPolicyMergePatch, secret.Name, err)
 		return fmt.Errorf(errPolicyMergePatch, secret.Name, err)
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
+func getManagedKeys(secret *v1.Secret) ([]string, error) {
+	var keys []string
+	for _, v := range secret.ObjectMeta.ManagedFields {
+		if v.Manager != fieldOwner {
+			continue
+		}
+		fields := make(map[string]interface{})
+		err := json.Unmarshal(v.FieldsV1.Raw, &fields)
+		if err != nil {
+			return nil, fmt.Errorf("error unmarshaling managed fields: %w", err)
+		}
+		dataFields := fields["f:data"]
+		if dataFields == nil {
+			continue
+		}
+		df, ok := dataFields.(map[string]string)
+		if !ok {
+			continue
+		}
+		for k := range df {
+			if k == "." {
+				continue
+			}
+			keys = append(keys, strings.TrimPrefix(k, "f:"))
+		}
+	}
+	return keys, nil
+}
+
 func getResourceVersion(es esv1beta1.ExternalSecret) string {
 func getResourceVersion(es esv1beta1.ExternalSecret) string {
 	return fmt.Sprintf("%d-%s", es.ObjectMeta.GetGeneration(), hashMeta(es.ObjectMeta))
 	return fmt.Sprintf("%d-%s", es.ObjectMeta.GetGeneration(), hashMeta(es.ObjectMeta))
 }
 }
@@ -404,11 +497,15 @@ func (r *Reconciler) getStore(ctx context.Context, externalSecret *esv1beta1.Ext
 func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient esv1beta1.SecretsClient, externalSecret *esv1beta1.ExternalSecret) (map[string][]byte, error) {
 func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient esv1beta1.SecretsClient, externalSecret *esv1beta1.ExternalSecret) (map[string][]byte, error) {
 	providerData := make(map[string][]byte)
 	providerData := make(map[string][]byte)
 
 
-	for _, remoteRef := range externalSecret.Spec.DataFrom {
+	for i, remoteRef := range externalSecret.Spec.DataFrom {
 		var secretMap map[string][]byte
 		var secretMap map[string][]byte
 		var err error
 		var err error
 		if remoteRef.Find != nil {
 		if remoteRef.Find != nil {
 			secretMap, err = providerClient.GetAllSecrets(ctx, *remoteRef.Find)
 			secretMap, err = providerClient.GetAllSecrets(ctx, *remoteRef.Find)
+			if errors.Is(err, esv1beta1.NoSecretErr) && externalSecret.Spec.Target.DeletionPolicy != esv1beta1.DeletionPolicyRetain {
+				r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1beta1.ReasonDeleted, fmt.Sprintf("secret does not exist at provider using .dataFrom[%d]", i))
+				continue
+			}
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
@@ -418,6 +515,10 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient e
 			}
 			}
 		} else if remoteRef.Extract != nil {
 		} else if remoteRef.Extract != nil {
 			secretMap, err = providerClient.GetSecretMap(ctx, *remoteRef.Extract)
 			secretMap, err = providerClient.GetSecretMap(ctx, *remoteRef.Extract)
+			if errors.Is(err, esv1beta1.NoSecretErr) && externalSecret.Spec.Target.DeletionPolicy != esv1beta1.DeletionPolicyRetain {
+				r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1beta1.ReasonDeleted, fmt.Sprintf("secret does not exist at provider using .dataFrom[%d]", i))
+				continue
+			}
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
@@ -430,8 +531,12 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient e
 		providerData = utils.MergeByteMap(providerData, secretMap)
 		providerData = utils.MergeByteMap(providerData, secretMap)
 	}
 	}
 
 
-	for _, secretRef := range externalSecret.Spec.Data {
+	for i, secretRef := range externalSecret.Spec.Data {
 		secretData, err := providerClient.GetSecret(ctx, secretRef.RemoteRef)
 		secretData, err := providerClient.GetSecret(ctx, secretRef.RemoteRef)
+		if errors.Is(err, esv1beta1.NoSecretErr) && externalSecret.Spec.Target.DeletionPolicy != esv1beta1.DeletionPolicyRetain {
+			r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1beta1.ReasonDeleted, fmt.Sprintf("secret does not exist at provider using .data[%d] key=%s", i, secretRef.RemoteRef.Key))
+			continue
+		}
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}

+ 169 - 6
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -14,6 +14,7 @@ limitations under the License.
 package externalsecret
 package externalsecret
 
 
 import (
 import (
+	"bytes"
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
@@ -24,6 +25,7 @@ import (
 	. "github.com/onsi/gomega"
 	. "github.com/onsi/gomega"
 	dto "github.com/prometheus/client_model/go"
 	dto "github.com/prometheus/client_model/go"
 	v1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -155,6 +157,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		syncCallsTotal.Reset()
 		syncCallsTotal.Reset()
 		syncCallsError.Reset()
 		syncCallsError.Reset()
 		externalSecretCondition.Reset()
 		externalSecretCondition.Reset()
+		fakeProvider.Reset()
 	})
 	})
 
 
 	AfterEach(func() {
 	AfterEach(func() {
@@ -162,13 +165,13 @@ var _ = Describe("ExternalSecret controller", func() {
 			ObjectMeta: metav1.ObjectMeta{
 			ObjectMeta: metav1.ObjectMeta{
 				Name: ExternalSecretNamespace,
 				Name: ExternalSecretNamespace,
 			},
 			},
-		}, client.PropagationPolicy(metav1.DeletePropagationBackground)), client.GracePeriodSeconds(0)).To(Succeed())
+		})).To(Succeed())
 		Expect(k8sClient.Delete(context.Background(), &esv1beta1.SecretStore{
 		Expect(k8sClient.Delete(context.Background(), &esv1beta1.SecretStore{
 			ObjectMeta: metav1.ObjectMeta{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      ExternalSecretStore,
 				Name:      ExternalSecretStore,
 				Namespace: ExternalSecretNamespace,
 				Namespace: ExternalSecretNamespace,
 			},
 			},
-		}, client.PropagationPolicy(metav1.DeletePropagationBackground)), client.GracePeriodSeconds(0)).To(Succeed())
+		})).To(Succeed())
 	})
 	})
 
 
 	const targetProp = "targetProperty"
 	const targetProp = "targetProperty"
@@ -281,7 +284,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		const secretVal = "someValue"
 		const secretVal = "someValue"
 		const existingKey = "pre-existing-key"
 		const existingKey = "pre-existing-key"
 		existingVal := "pre-existing-value"
 		existingVal := "pre-existing-value"
-		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.Merge
+		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 
 
 		// create secret beforehand
 		// create secret beforehand
 		Expect(k8sClient.Create(context.Background(), &v1.Secret{
 		Expect(k8sClient.Create(context.Background(), &v1.Secret{
@@ -320,7 +323,7 @@ var _ = Describe("ExternalSecret controller", func() {
 	mergeWithSecretNoChange := func(tc *testCase) {
 	mergeWithSecretNoChange := func(tc *testCase) {
 		const existingKey = "pre-existing-key"
 		const existingKey = "pre-existing-key"
 		existingVal := "someValue"
 		existingVal := "someValue"
-		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.Merge
+		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 
 
 		// create secret beforehand
 		// create secret beforehand
 		Expect(k8sClient.Create(context.Background(), &v1.Secret{
 		Expect(k8sClient.Create(context.Background(), &v1.Secret{
@@ -360,7 +363,7 @@ var _ = Describe("ExternalSecret controller", func() {
 	// should not merge with secret if it doesn't exist
 	// should not merge with secret if it doesn't exist
 	mergeWithSecretErr := func(tc *testCase) {
 	mergeWithSecretErr := func(tc *testCase) {
 		const secretVal = "someValue"
 		const secretVal = "someValue"
-		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.Merge
+		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 
 
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		fakeProvider.WithGetSecret([]byte(secretVal), nil)
 		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
 		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
@@ -386,7 +389,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		// this should confict
 		// this should confict
 		const existingKey = targetProp
 		const existingKey = targetProp
 		existingVal := "pre-existing-value"
 		existingVal := "pre-existing-value"
-		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.Merge
+		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
 
 
 		// create secret beforehand
 		// create secret beforehand
 		Expect(k8sClient.Create(context.Background(), &v1.Secret{
 		Expect(k8sClient.Create(context.Background(), &v1.Secret{
@@ -785,6 +788,162 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 		}
 	}
 	}
 
 
+	deleteSecretPolicy := func(tc *testCase) {
+		expVal := []byte("1234")
+		// set initial value
+		fakeProvider.WithGetAllSecrets(map[string][]byte{
+			"foo": expVal,
+			"bar": expVal,
+		}, nil)
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Find: &esv1beta1.ExternalSecretFind{
+					Tags: map[string]string{},
+				},
+			},
+		}
+		tc.externalSecret.Spec.Target.DeletionPolicy = esv1beta1.DeletionPolicyDelete
+		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			Expect(secret.Data["foo"]).To(Equal(expVal))
+
+			// update provider secret
+			fakeProvider.WithGetAllSecrets(map[string][]byte{
+				"foo": expVal,
+			}, nil)
+			sec := &v1.Secret{}
+			secretLookupKey := types.NamespacedName{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace,
+			}
+			Eventually(func() bool {
+				By("checking secret value for foo=1234 and bar=nil")
+				err := k8sClient.Get(context.Background(), secretLookupKey, sec)
+				if err != nil {
+					return false
+				}
+				return bytes.Equal(sec.Data["foo"], expVal) && sec.Data["bar"] == nil
+			}, time.Second*10, time.Second).Should(BeTrue())
+
+			// return specific delete err to indicate deletion
+			fakeProvider.WithGetAllSecrets(map[string][]byte{}, esv1beta1.NoSecretErr)
+			Eventually(func() bool {
+				By("checking that secret has been deleted")
+				err := k8sClient.Get(context.Background(), secretLookupKey, sec)
+				return apierrors.IsNotFound(err)
+			}, time.Second*10, time.Second).Should(BeTrue())
+		}
+	}
+
+	deleteSecretPolicyRetain := func(tc *testCase) {
+		expVal := []byte("1234")
+		// set initial value
+		fakeProvider.WithGetAllSecrets(map[string][]byte{
+			"foo": expVal,
+			"bar": expVal,
+		}, nil)
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Find: &esv1beta1.ExternalSecretFind{
+					Tags: map[string]string{},
+				},
+			},
+		}
+		tc.externalSecret.Spec.Target.DeletionPolicy = esv1beta1.DeletionPolicyRetain
+		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			Expect(secret.Data["foo"]).To(Equal(expVal))
+
+			sec := &v1.Secret{}
+			secretLookupKey := types.NamespacedName{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace,
+			}
+			// return specific delete err to indicate deletion
+			// however this should not trigger a delete
+			fakeProvider.WithGetAllSecrets(map[string][]byte{}, esv1beta1.NoSecretErr)
+			Consistently(func() bool {
+				By("checking that secret has not been deleted")
+				err := k8sClient.Get(context.Background(), secretLookupKey, sec)
+				return apierrors.IsNotFound(err) && bytes.Equal(sec.Data["foo"], expVal)
+			}, time.Second*10, time.Second).Should(BeFalse())
+		}
+	}
+
+	// merge with existing secret using creationPolicy=Merge
+	// if provider secret gets deleted only the managed field should get deleted
+	deleteSecretPolicyMerge := func(tc *testCase) {
+		const secretVal = "someValue"
+		const existingKey = "some-existing-key"
+		existingVal := "some-existing-value"
+		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
+		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyMerge
+		tc.externalSecret.Spec.Target.DeletionPolicy = esv1beta1.DeletionPolicyMerge
+
+		// create secret beforehand
+		Expect(k8sClient.Create(context.Background(), &v1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace,
+			},
+			Data: map[string][]byte{
+				existingKey: []byte(existingVal),
+			},
+		}, client.FieldOwner(FakeManager))).To(Succeed())
+
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check value
+			Expect(string(secret.Data[existingKey])).To(Equal(existingVal))
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+
+			sec := &v1.Secret{}
+			secretLookupKey := types.NamespacedName{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace,
+			}
+			// return specific delete err to indicate deletion
+			// however this should not trigger a delete
+			// instead expect that only the pre-existing value exists
+			fakeProvider.WithGetSecret(nil, esv1beta1.NoSecretErr)
+			Eventually(func() bool {
+				By("checking that secret has not been deleted and pre-existing key exists")
+				err := k8sClient.Get(context.Background(), secretLookupKey, sec)
+				return !apierrors.IsNotFound(err) &&
+					len(sec.Data) == 1 &&
+					bytes.Equal(sec.Data[existingKey], []byte(existingVal))
+			}, time.Second*30, time.Second).Should(BeTrue())
+
+		}
+	}
+
+	// orphan the secret after the external secret has been deleted
+	createSecretPolicyOrphan := func(tc *testCase) {
+		const secretVal = "someValue"
+		tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second}
+		tc.externalSecret.Spec.Target.CreationPolicy = esv1beta1.CreatePolicyOrphan
+
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check value
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+
+			sec := &v1.Secret{}
+			secretLookupKey := types.NamespacedName{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace,
+			}
+			err := k8sClient.Delete(context.Background(), tc.externalSecret)
+			Expect(err).ToNot(HaveOccurred())
+			Consistently(func() bool {
+				By("checking that secret has not been deleted")
+				err := k8sClient.Get(context.Background(), secretLookupKey, sec)
+				return !apierrors.IsNotFound(err)
+			}, time.Second*15, time.Second).Should(BeTrue())
+		}
+	}
+
 	// with dataFrom all properties from the specified secret
 	// with dataFrom all properties from the specified secret
 	// should be put into the secret
 	// should be put into the secret
 	syncWithDataFrom := func(tc *testCase) {
 	syncWithDataFrom := func(tc *testCase) {
@@ -1113,6 +1272,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		Entry("should error if secret doesn't exist when using creationPolicy=Merge", mergeWithSecretErr),
 		Entry("should error if secret doesn't exist when using creationPolicy=Merge", mergeWithSecretErr),
 		Entry("should not resolve conflicts with creationPolicy=Merge", mergeWithConflict),
 		Entry("should not resolve conflicts with creationPolicy=Merge", mergeWithConflict),
 		Entry("should not update unchanged secret using creationPolicy=Merge", mergeWithSecretNoChange),
 		Entry("should not update unchanged secret using creationPolicy=Merge", mergeWithSecretNoChange),
+		Entry("should not delete pre-existing secret with creationPolicy=Orphan", createSecretPolicyOrphan),
 		Entry("should sync with template", syncWithTemplate),
 		Entry("should sync with template", syncWithTemplate),
 		Entry("should sync with template engine v2", syncWithTemplateV2),
 		Entry("should sync with template engine v2", syncWithTemplateV2),
 		Entry("should sync template with correct value precedence", syncWithTemplatePrecedence),
 		Entry("should sync template with correct value precedence", syncWithTemplatePrecedence),
@@ -1130,6 +1290,9 @@ var _ = Describe("ExternalSecret controller", func() {
 		Entry("should set an error condition when store provider constructor fails", storeConstructErrCondition),
 		Entry("should set an error condition when store provider constructor fails", storeConstructErrCondition),
 		Entry("should not process store with mismatching controller field", ignoreMismatchController),
 		Entry("should not process store with mismatching controller field", ignoreMismatchController),
 		Entry("should not process cluster secret store when it is disabled", ignoreClusterSecretStoreWhenDisabled),
 		Entry("should not process cluster secret store when it is disabled", ignoreClusterSecretStoreWhenDisabled),
+		Entry("should eventually delete target secret with deletionPolicy=Delete", deleteSecretPolicy),
+		Entry("should not delete target secret with deletionPolicy=Retain", deleteSecretPolicyRetain),
+		Entry("should not delete pre-existing secret with deletionPolicy=Merge", deleteSecretPolicyMerge),
 	)
 	)
 })
 })
 
 

+ 5 - 0
pkg/provider/aws/parameterstore/parameterstore.go

@@ -170,6 +170,11 @@ func (pm *ParameterStore) GetSecret(ctx context.Context, ref esv1beta1.ExternalS
 		Name:           &ref.Key,
 		Name:           &ref.Key,
 		WithDecryption: aws.Bool(true),
 		WithDecryption: aws.Bool(true),
 	})
 	})
+
+	var nf *ssm.ParameterNotFound
+	if errors.As(err, &nf) {
+		return nil, esv1beta1.NoSecretErr
+	}
 	if err != nil {
 	if err != nil {
 		return nil, util.SanitizeErr(err)
 		return nil, util.SanitizeErr(err)
 	}
 	}

+ 7 - 0
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -77,6 +77,10 @@ func (sm *SecretsManager) fetch(_ context.Context, ref esv1beta1.ExternalSecretD
 		SecretId:     &ref.Key,
 		SecretId:     &ref.Key,
 		VersionStage: &ver,
 		VersionStage: &ver,
 	})
 	})
+	var nf *awssm.ResourceNotFoundException
+	if errors.As(err, &nf) {
+		return nil, esv1beta1.NoSecretErr
+	}
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -212,6 +216,9 @@ func (sm *SecretsManager) fetchAndSet(ctx context.Context, data map[string][]byt
 // GetSecret returns a single secret from the provider.
 // GetSecret returns a single secret from the provider.
 func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
 func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
 	secretOut, err := sm.fetch(ctx, ref)
 	secretOut, err := sm.fetch(ctx, ref)
+	if errors.Is(err, esv1beta1.NoSecretErr) {
+		return nil, err
+	}
 	if err != nil {
 	if err != nil {
 		return nil, util.SanitizeErr(err)
 		return nil, util.SanitizeErr(err)
 	}
 	}

+ 2 - 3
pkg/provider/fake/fake.go

@@ -24,7 +24,6 @@ import (
 )
 )
 
 
 var (
 var (
-	errNotFound            = fmt.Errorf("secret value not found")
 	errMissingStore        = fmt.Errorf("missing store provider")
 	errMissingStore        = fmt.Errorf("missing store provider")
 	errMissingFakeProvider = fmt.Errorf("missing store provider fake")
 	errMissingFakeProvider = fmt.Errorf("missing store provider fake")
 	errMissingKeyField     = "key must be set in data %v"
 	errMissingKeyField     = "key must be set in data %v"
@@ -69,7 +68,7 @@ func (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDa
 			return []byte(data.Value), nil
 			return []byte(data.Value), nil
 		}
 		}
 	}
 	}
-	return nil, errNotFound
+	return nil, esv1beta1.NoSecretErr
 }
 }
 
 
 // GetSecretMap returns multiple k/v pairs from the provider.
 // GetSecretMap returns multiple k/v pairs from the provider.
@@ -80,7 +79,7 @@ func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecre
 		}
 		}
 		return convertMap(data.ValueMap), nil
 		return convertMap(data.ValueMap), nil
 	}
 	}
-	return nil, errNotFound
+	return nil, esv1beta1.NoSecretErr
 }
 }
 
 
 func convertMap(in map[string]string) map[string][]byte {
 func convertMap(in map[string]string) map[string][]byte {

+ 2 - 2
pkg/provider/fake/fake_test.go

@@ -95,7 +95,7 @@ func TestGetSecret(t *testing.T) {
 				Key:     "/foo",
 				Key:     "/foo",
 				Version: "v2",
 				Version: "v2",
 			},
 			},
-			expErr: "secret value not found",
+			expErr: esv1beta1.NoSecretErr.Error(),
 		},
 		},
 		{
 		{
 			name: "get correct value from multiple versions",
 			name: "get correct value from multiple versions",
@@ -165,7 +165,7 @@ func TestGetSecretMap(t *testing.T) {
 				Key:     "/foo",
 				Key:     "/foo",
 				Version: "v2",
 				Version: "v2",
 			},
 			},
-			expErr: "secret value not found",
+			expErr: esv1beta1.NoSecretErr.Error(),
 		},
 		},
 		{
 		{
 			name: "get correct value from multiple versions",
 			name: "get correct value from multiple versions",

+ 7 - 0
pkg/provider/testing/fake/fake.go

@@ -125,3 +125,10 @@ func (v *Client) NewClient(ctx context.Context, store esv1beta1.GenericStore, ku
 	}
 	}
 	return c, nil
 	return c, nil
 }
 }
+
+func (v *Client) Reset() {
+	v.WithNew(func(context.Context, esv1beta1.GenericStore, client.Client,
+		string) (esv1beta1.SecretsClient, error) {
+		return v, nil
+	})
+}