Browse Source

Add PushSecret UpdatePolicy (to replace PR #3100) (#3117)

* Add PushSecret UpdatePolicy

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>

* Adjust description of UpdatePolicy in PushSecret Spec

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>

* Restructure PushSecret Status

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>

* Refactor PushSecret controller method

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>

* Add missing methods for new providers

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>

* Add missing method to onboardbase client

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>

* Add docs on PushSecret UpdatePolicy

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>

* Use constant for error message

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>

---------

Signed-off-by: Carolin Dohmen <carodohmen@gmail.com>
Carolin Dohmen 2 years ago
parent
commit
29e5f71d8b
38 changed files with 731 additions and 106 deletions
  1. 15 1
      apis/externalsecrets/v1alpha1/pushsecret_types.go
  2. 3 0
      apis/externalsecrets/v1beta1/provider.go
  3. 5 0
      apis/externalsecrets/v1beta1/provider_schema_test.go
  4. 11 2
      config/crds/bases/external-secrets.io_pushsecrets.yaml
  5. 10 1
      deploy/crds/bundle.yaml
  6. 3 2
      docs/guides/pushsecrets.md
  7. 1 0
      docs/snippets/full-pushsecret.yaml
  8. 50 25
      pkg/controllers/pushsecret/pushsecret_controller.go
  9. 194 26
      pkg/controllers/pushsecret/pushsecret_controller_test.go
  10. 4 0
      pkg/controllers/secretstore/client_manager_test.go
  11. 8 3
      pkg/provider/akeyless/akeyless.go
  12. 8 3
      pkg/provider/alibaba/kms.go
  13. 4 0
      pkg/provider/aws/parameterstore/parameterstore.go
  14. 4 0
      pkg/provider/aws/secretsmanager/secretsmanager.go
  15. 4 0
      pkg/provider/azure/keyvault/keyvault.go
  16. 7 2
      pkg/provider/chef/chef.go
  17. 4 0
      pkg/provider/conjur/provider.go
  18. 4 0
      pkg/provider/delinea/client.go
  19. 4 0
      pkg/provider/doppler/client.go
  20. 5 0
      pkg/provider/fake/fake.go
  21. 57 0
      pkg/provider/fake/fake_test.go
  22. 4 0
      pkg/provider/fortanix/fortanix.go
  23. 4 0
      pkg/provider/gcp/secretmanager/client.go
  24. 7 2
      pkg/provider/gitlab/gitlab.go
  25. 8 3
      pkg/provider/ibm/provider.go
  26. 4 0
      pkg/provider/keepersecurity/client.go
  27. 4 0
      pkg/provider/kubernetes/client.go
  28. 5 0
      pkg/provider/onboardbase/client.go
  29. 4 0
      pkg/provider/onepassword/onepassword.go
  30. 4 0
      pkg/provider/oracle/oracle.go
  31. 4 0
      pkg/provider/pulumi/pulumi.go
  32. 4 0
      pkg/provider/scaleway/client.go
  33. 8 4
      pkg/provider/senhasegura/dsm/dsm.go
  34. 8 0
      pkg/provider/testing/fake/fake.go
  35. 48 26
      pkg/provider/vault/client_get.go
  36. 188 0
      pkg/provider/vault/client_get_test.go
  37. 11 3
      pkg/provider/webhook/webhook.go
  38. 11 3
      pkg/provider/yandex/common/secretsclient.go

+ 15 - 1
apis/externalsecrets/v1alpha1/pushsecret_types.go

@@ -41,6 +41,14 @@ type PushSecretStoreRef struct {
 	Kind string `json:"kind,omitempty"`
 }
 
+// +kubebuilder:validation:Enum=Replace;IfNotExists
+type PushSecretUpdatePolicy string
+
+const (
+	PushSecretUpdatePolicyReplace     PushSecretUpdatePolicy = "Replace"
+	PushSecretUpdatePolicyIfNotExists PushSecretUpdatePolicy = "IfNotExists"
+)
+
 // +kubebuilder:validation:Enum=Delete;None
 type PushSecretDeletionPolicy string
 
@@ -54,6 +62,10 @@ type PushSecretSpec struct {
 	// The Interval to which External Secrets will try to push a secret definition
 	RefreshInterval *metav1.Duration     `json:"refreshInterval,omitempty"`
 	SecretStoreRefs []PushSecretStoreRef `json:"secretStoreRefs"`
+	// UpdatePolicy to handle Secrets in the provider. Possible Values: "Replace/IfNotExists". Defaults to "Replace".
+	// +kubebuilder:default="Replace"
+	// +optional
+	UpdatePolicy PushSecretUpdatePolicy `json:"updatePolicy,omitempty"`
 	// Deletion Policy to handle Secrets in the provider. Possible Values: "Delete/None". Defaults to "None".
 	// +kubebuilder:default="None"
 	// +optional
@@ -148,6 +160,7 @@ type PushSecretStatusCondition struct {
 	// +optional
 	LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
 }
+
 type SyncedPushSecretsMap map[string]map[string]PushSecretData
 
 // PushSecretStatus indicates the history of the status of PushSecret.
@@ -159,7 +172,8 @@ type PushSecretStatus struct {
 
 	// SyncedResourceVersion keeps track of the last synced version.
 	SyncedResourceVersion string `json:"syncedResourceVersion,omitempty"`
-	// Synced Push Secrets for later deletion. Matches Secret Stores to PushSecretData that was stored to that secretStore.
+	// Synced PushSecrets, including secrets that already exist in provider.
+	// Matches secret stores to PushSecretData that was stored to that secret store.
 	// +optional
 	SyncedPushSecrets SyncedPushSecretsMap `json:"syncedPushSecrets,omitempty"`
 	// +optional

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

@@ -79,6 +79,9 @@ type SecretsClient interface {
 	// DeleteSecret will delete the secret from a provider
 	DeleteSecret(ctx context.Context, remoteRef PushSecretRemoteRef) error
 
+	// SecretExists checks if a secret is already present in the provider at the given location.
+	SecretExists(ctx context.Context, remoteRef PushSecretRemoteRef) (bool, error)
+
 	// Validate checks if the client is configured correctly
 	// and is able to retrieve secrets from the provider.
 	// If the validation result is unknown it will be ignored.

+ 5 - 0
apis/externalsecrets/v1beta1/provider_schema_test.go

@@ -47,6 +47,11 @@ func (p *PP) DeleteSecret(_ context.Context, _ PushSecretRemoteRef) error {
 	return nil
 }
 
+// Exists checks if a secret is already present in the provider at the given location.
+func (p *PP) SecretExists(_ context.Context, _ PushSecretRemoteRef) (bool, error) {
+	return false, nil
+}
+
 // GetSecret returns a single secret from the provider.
 func (p *PP) GetSecret(_ context.Context, _ ExternalSecretDataRemoteRef) ([]byte, error) {
 	return []byte("NOOP"), nil

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

@@ -266,6 +266,14 @@ spec:
                   type:
                     type: string
                 type: object
+              updatePolicy:
+                default: Replace
+                description: 'UpdatePolicy to handle Secrets in the provider. Possible
+                  Values: "Replace/IfNotExists". Defaults to "Replace".'
+                enum:
+                - Replace
+                - IfNotExists
+                type: string
             required:
             - secretStoreRefs
             - selector
@@ -339,8 +347,9 @@ spec:
                     - match
                     type: object
                   type: object
-                description: Synced Push Secrets for later deletion. Matches Secret
-                  Stores to PushSecretData that was stored to that secretStore.
+                description: |-
+                  Synced PushSecrets, including secrets that already exist in provider.
+                  Matches secret stores to PushSecretData that was stored to that secret store.
                 type: object
               syncedResourceVersion:
                 description: SyncedResourceVersion keeps track of the last synced

+ 10 - 1
deploy/crds/bundle.yaml

@@ -5668,6 +5668,13 @@ spec:
                     type:
                       type: string
                   type: object
+                updatePolicy:
+                  default: Replace
+                  description: 'UpdatePolicy to handle Secrets in the provider. Possible Values: "Replace/IfNotExists". Defaults to "Replace".'
+                  enum:
+                    - Replace
+                    - IfNotExists
+                  type: string
               required:
                 - secretStoreRefs
                 - selector
@@ -5737,7 +5744,9 @@ spec:
                         - match
                       type: object
                     type: object
-                  description: Synced Push Secrets for later deletion. Matches Secret Stores to PushSecretData that was stored to that secretStore.
+                  description: |-
+                    Synced PushSecrets, including secrets that already exist in provider.
+                    Matches secret stores to PushSecretData that was stored to that secret store.
                   type: object
                 syncedResourceVersion:
                   description: SyncedResourceVersion keeps track of the last synced version.

File diff suppressed because it is too large
+ 3 - 2
docs/guides/pushsecrets.md


+ 1 - 0
docs/snippets/full-pushsecret.yaml

@@ -5,6 +5,7 @@ metadata:
   name: pushsecret-example # Customisable
   namespace: default # Same of the SecretStores
 spec:
+  updatePolicy: Replace # Policy to overwrite existing secrets in the provider on sync
   deletionPolicy: Delete # the provider' secret will be deleted if the PushSecret is deleted
   refreshInterval: 10s # Refresh interval for which push secret will reconcile
   secretStoreRefs: # A list of secret stores to push secrets to

+ 50 - 25
pkg/controllers/pushsecret/pushsecret_controller.go

@@ -185,24 +185,27 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	return ctrl.Result{RequeueAfter: refreshInt}, nil
 }
 
-func (r *Reconciler) markAsFailed(msg string, ps *esapi.PushSecret, badSyncState esapi.SyncedPushSecretsMap) {
+func (r *Reconciler) markAsFailed(msg string, ps *esapi.PushSecret, syncState esapi.SyncedPushSecretsMap) {
 	cond := newPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonErrored, msg)
 	setPushSecretCondition(ps, *cond)
-	if badSyncState != nil {
-		r.setSyncedSecrets(ps, badSyncState)
+	if syncState != nil {
+		r.setSecrets(ps, syncState)
 	}
 	r.recorder.Event(ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
 }
 
-func (r *Reconciler) markAsDone(ps *esapi.PushSecret, syncedSecrets esapi.SyncedPushSecretsMap) {
+func (r *Reconciler) markAsDone(ps *esapi.PushSecret, secrets esapi.SyncedPushSecretsMap) {
 	msg := "PushSecret synced successfully"
+	if ps.Spec.UpdatePolicy == esapi.PushSecretUpdatePolicyIfNotExists {
+		msg += ". Existing secrets in providers unchanged."
+	}
 	cond := newPushSecretCondition(esapi.PushSecretReady, v1.ConditionTrue, esapi.ReasonSynced, msg)
 	setPushSecretCondition(ps, *cond)
-	r.setSyncedSecrets(ps, syncedSecrets)
+	r.setSecrets(ps, secrets)
 	r.recorder.Event(ps, v1.EventTypeNormal, esapi.ReasonSynced, msg)
 }
 
-func (r *Reconciler) setSyncedSecrets(ps *esapi.PushSecret, status esapi.SyncedPushSecretsMap) {
+func (r *Reconciler) setSecrets(ps *esapi.PushSecret, status esapi.SyncedPushSecretsMap) {
 	ps.Status.SyncedPushSecrets = status
 }
 
@@ -269,35 +272,57 @@ func (r *Reconciler) DeleteSecretFromStore(ctx context.Context, client v1beta1.S
 }
 
 func (r *Reconciler) PushSecretToProviders(ctx context.Context, stores map[esapi.PushSecretStoreRef]v1beta1.GenericStore, ps esapi.PushSecret, secret *v1.Secret, mgr *secretstore.Manager) (esapi.SyncedPushSecretsMap, error) {
-	out := esapi.SyncedPushSecretsMap{}
+	out := make(esapi.SyncedPushSecretsMap)
 	for ref, store := range stores {
-		storeKey := fmt.Sprintf("%v/%v", ref.Kind, store.GetName())
-		out[storeKey] = make(map[string]esapi.PushSecretData)
-		storeRef := v1beta1.SecretStoreRef{
-			Name: store.GetName(),
-			Kind: ref.Kind,
-		}
-		secretClient, err := mgr.Get(ctx, storeRef, ps.GetNamespace(), nil)
+		out, err := r.handlePushSecretDataForStore(ctx, ps, secret, out, mgr, store.GetName(), ref.Kind)
 		if err != nil {
-			return out, fmt.Errorf("could not get secrets client for store %v: %w", store.GetName(), err)
+			return out, err
 		}
-		for _, data := range ps.Spec.Data {
-			if data.Match.SecretKey != "" {
-				if _, ok := secret.Data[data.Match.SecretKey]; !ok {
-					return out, fmt.Errorf("secret key %v does not exist", data.Match.SecretKey)
-				}
-			}
+	}
+	return out, nil
+}
 
-			if err := secretClient.PushSecret(ctx, secret, data); err != nil {
-				return out, fmt.Errorf(errSetSecretFailed, data.Match.SecretKey, store.GetName(), err)
+func (r *Reconciler) handlePushSecretDataForStore(ctx context.Context, ps esapi.PushSecret, secret *v1.Secret, out esapi.SyncedPushSecretsMap, mgr *secretstore.Manager, storeName, refKind string) (esapi.SyncedPushSecretsMap, error) {
+	storeKey := fmt.Sprintf("%v/%v", refKind, storeName)
+	out[storeKey] = make(map[string]esapi.PushSecretData)
+	storeRef := v1beta1.SecretStoreRef{
+		Name: storeName,
+		Kind: refKind,
+	}
+	secretClient, err := mgr.Get(ctx, storeRef, ps.GetNamespace(), nil)
+	if err != nil {
+		return out, fmt.Errorf("could not get secrets client for store %v: %w", storeName, err)
+	}
+	for _, data := range ps.Spec.Data {
+		key := data.GetSecretKey()
+		if !secretKeyExists(key, secret) {
+			return out, fmt.Errorf("secret key %v does not exist", key)
+		}
+		switch ps.Spec.UpdatePolicy {
+		case esapi.PushSecretUpdatePolicyIfNotExists:
+			exists, err := secretClient.SecretExists(ctx, data.Match.RemoteRef)
+			if err != nil {
+				return out, fmt.Errorf("could not verify if secret exists in store: %w", err)
+			} else if exists {
+				out[storeKey][statusRef(data)] = data
+				continue
 			}
-
-			out[storeKey][statusRef(data)] = data
+		case esapi.PushSecretUpdatePolicyReplace:
+		default:
+		}
+		if err := secretClient.PushSecret(ctx, secret, data); err != nil {
+			return out, fmt.Errorf(errSetSecretFailed, key, storeName, err)
 		}
+		out[storeKey][statusRef(data)] = data
 	}
 	return out, nil
 }
 
+func secretKeyExists(key string, secret *v1.Secret) bool {
+	_, ok := secret.Data[key]
+	return key == "" || ok
+}
+
 func (r *Reconciler) GetSecret(ctx context.Context, ps esapi.PushSecret) (*v1.Secret, error) {
 	secretName := types.NamespacedName{Name: ps.Spec.Selector.Secret.Name, Namespace: ps.Namespace}
 	secret := &v1.Secret{}

+ 194 - 26
pkg/controllers/pushsecret/pushsecret_controller_test.go

@@ -128,6 +128,18 @@ var _ = Describe("ExternalSecret controller", func() {
 		})).To(Succeed())
 	})
 
+	const (
+		defaultKey          = "key"
+		defaultVal          = "value"
+		defaultPath         = "path/to/key"
+		otherKey            = "other-key"
+		otherVal            = "other-value"
+		otherPath           = "path/to/other-key"
+		newKey              = "new-key"
+		newVal              = "new-value"
+		storePrefixTemplate = "SecretStore/%v"
+	)
+
 	makeDefaultTestcase := func() *testCase {
 		return &testCase{
 			pushsecret: &v1alpha1.PushSecret{
@@ -150,9 +162,9 @@ var _ = Describe("ExternalSecret controller", func() {
 					Data: []v1alpha1.PushSecretData{
 						{
 							Match: v1alpha1.PushSecretMatch{
-								SecretKey: "key",
+								SecretKey: defaultKey,
 								RemoteRef: v1alpha1.PushSecretRemoteRef{
-									RemoteKey: "path/to/key",
+									RemoteKey: defaultPath,
 								},
 							},
 						},
@@ -165,7 +177,7 @@ var _ = Describe("ExternalSecret controller", func() {
 					Namespace: PushSecretNamespace,
 				},
 				Data: map[string][]byte{
-					"key": []byte("value"),
+					defaultKey: []byte(defaultVal),
 				},
 			},
 			store: &v1beta1.SecretStore{
@@ -195,7 +207,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
 			Eventually(func() bool {
 				By("checking if Provider value got updated")
-				secretValue := secret.Data["key"]
+				secretValue := secret.Data[defaultKey]
 				providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey]
 				if !ok {
 					return false
@@ -207,6 +219,157 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 	}
 
+	updateIfNotExists := func(tc *testCase) {
+		fakeProvider.SetSecretFn = func() error {
+			return nil
+		}
+		fakeProvider.SecretExistsFn = func(ctx context.Context, ref v1beta1.PushSecretRemoteRef) (bool, error) {
+			_, ok := fakeProvider.SetSecretArgs[ref.GetRemoteKey()]
+			return ok, nil
+		}
+		tc.pushsecret.Spec.UpdatePolicy = v1alpha1.PushSecretUpdatePolicyIfNotExists
+		initialValue := fakeProvider.SetSecretArgs[tc.pushsecret.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
+		tc.secret.Data[defaultKey] = []byte(newVal)
+
+		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
+			Eventually(func() bool {
+				By("checking if Provider value did not get updated")
+				Expect(k8sClient.Update(context.Background(), secret, &client.UpdateOptions{})).Should(Succeed())
+				providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey]
+				if !ok {
+					return false
+				}
+				got := providerValue.Value
+				return bytes.Equal(got, initialValue)
+			}, time.Second*10, time.Second).Should(BeTrue())
+			return true
+		}
+	}
+
+	updateIfNotExistsPartialSecrets := func(tc *testCase) {
+		fakeProvider.SetSecretFn = func() error {
+			return nil
+		}
+		fakeProvider.SecretExistsFn = func(ctx context.Context, ref v1beta1.PushSecretRemoteRef) (bool, error) {
+			_, ok := fakeProvider.SetSecretArgs[ref.GetRemoteKey()]
+			return ok, nil
+		}
+		tc.pushsecret.Spec.UpdatePolicy = v1alpha1.PushSecretUpdatePolicyIfNotExists
+		tc.pushsecret.Spec.Data = append(tc.pushsecret.Spec.Data, v1alpha1.PushSecretData{
+			Match: v1alpha1.PushSecretMatch{
+				SecretKey: otherKey,
+				RemoteRef: v1alpha1.PushSecretRemoteRef{
+					RemoteKey: otherPath,
+				},
+			},
+		})
+
+		initialValue := fakeProvider.SetSecretArgs[tc.pushsecret.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
+		tc.secret.Data[defaultKey] = []byte(newVal) // change initial value in secret
+		tc.secret.Data[otherKey] = []byte(otherVal)
+
+		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
+			Eventually(func() bool {
+				By("checking if only not existing Provider value got updated")
+				Expect(k8sClient.Update(context.Background(), secret, &client.UpdateOptions{})).Should(Succeed())
+				providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey]
+				if !ok {
+					return false
+				}
+				got := providerValue.Value
+				otherProviderValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[1].Match.RemoteRef.RemoteKey]
+				if !ok {
+					return false
+				}
+				gotOther := otherProviderValue.Value
+
+				return bytes.Equal(gotOther, tc.secret.Data[otherKey]) && bytes.Equal(got, initialValue)
+			}, time.Second*10, time.Second).Should(BeTrue())
+			return true
+		}
+	}
+
+	updateIfNotExistsSyncStatus := func(tc *testCase) {
+		fakeProvider.SetSecretFn = func() error {
+			return nil
+		}
+		fakeProvider.SecretExistsFn = func(ctx context.Context, ref v1beta1.PushSecretRemoteRef) (bool, error) {
+			_, ok := fakeProvider.SetSecretArgs[ref.GetRemoteKey()]
+			return ok, nil
+		}
+		tc.pushsecret.Spec.UpdatePolicy = v1alpha1.PushSecretUpdatePolicyIfNotExists
+		tc.pushsecret.Spec.Data = append(tc.pushsecret.Spec.Data, v1alpha1.PushSecretData{
+			Match: v1alpha1.PushSecretMatch{
+				SecretKey: otherKey,
+				RemoteRef: v1alpha1.PushSecretRemoteRef{
+					RemoteKey: otherPath,
+				},
+			},
+		})
+		tc.secret.Data[defaultKey] = []byte(newVal)
+		tc.secret.Data[otherKey] = []byte(otherVal)
+		updatedPS := &v1alpha1.PushSecret{}
+
+		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
+			Eventually(func() bool {
+				By("checking if PushSecret status gets updated correctly with UpdatePolicy=IfNotExists")
+				Expect(k8sClient.Update(context.Background(), secret, &client.UpdateOptions{})).Should(Succeed())
+				psKey := types.NamespacedName{Name: PushSecretName, Namespace: PushSecretNamespace}
+				err := k8sClient.Get(context.Background(), psKey, updatedPS)
+				if err != nil {
+					return false
+				}
+				_, ok := updatedPS.Status.SyncedPushSecrets[fmt.Sprintf(storePrefixTemplate, PushSecretStore)][defaultPath]
+				if !ok {
+					return false
+				}
+				_, ok = updatedPS.Status.SyncedPushSecrets[fmt.Sprintf(storePrefixTemplate, PushSecretStore)][otherPath]
+				if !ok {
+					return false
+				}
+				expected := v1alpha1.PushSecretStatusCondition{
+					Type:    v1alpha1.PushSecretReady,
+					Status:  v1.ConditionTrue,
+					Reason:  v1alpha1.ReasonSynced,
+					Message: "PushSecret synced successfully. Existing secrets in providers unchanged.",
+				}
+				return checkCondition(ps.Status, expected)
+			}, time.Second*10, time.Second).Should(BeTrue())
+			return true
+		}
+	}
+
+	updateIfNotExistsSyncFailed := func(tc *testCase) {
+		fakeProvider.SetSecretFn = func() error {
+			return nil
+		}
+		fakeProvider.SecretExistsFn = func(ctx context.Context, ref v1beta1.PushSecretRemoteRef) (bool, error) {
+			return false, fmt.Errorf("don't know")
+		}
+		tc.pushsecret.Spec.UpdatePolicy = v1alpha1.PushSecretUpdatePolicyIfNotExists
+		initialValue := fakeProvider.SetSecretArgs[tc.pushsecret.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
+		tc.secret.Data[defaultKey] = []byte(newVal)
+
+		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
+			Eventually(func() bool {
+				By("checking if sync failed if secret existence cannot be verified in Provider")
+				providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey]
+				if !ok {
+					return false
+				}
+				got := providerValue.Value
+				expected := v1alpha1.PushSecretStatusCondition{
+					Type:    v1alpha1.PushSecretReady,
+					Status:  v1.ConditionFalse,
+					Reason:  v1alpha1.ReasonErrored,
+					Message: "set secret failed: could not verify if secret exists in store: don't know",
+				}
+				return checkCondition(ps.Status, expected) && bytes.Equal(got, initialValue)
+			}, time.Second*10, time.Second).Should(BeTrue())
+			return true
+		}
+	}
+
 	// if target Secret name is not specified it should use the ExternalSecret name.
 	syncSuccessfullyWithTemplate := func(tc *testCase) {
 		fakeProvider.SetSecretFn = func() error {
@@ -232,9 +395,9 @@ var _ = Describe("ExternalSecret controller", func() {
 				Data: []v1alpha1.PushSecretData{
 					{
 						Match: v1alpha1.PushSecretMatch{
-							SecretKey: "key",
+							SecretKey: defaultKey,
 							RemoteRef: v1alpha1.PushSecretRemoteRef{
-								RemoteKey: "path/to/key",
+								RemoteKey: defaultPath,
 							},
 						},
 					},
@@ -251,7 +414,7 @@ var _ = Describe("ExternalSecret controller", func() {
 					Type:          v1.SecretTypeOpaque,
 					EngineVersion: v1beta1.TemplateEngineV2,
 					Data: map[string]string{
-						"key": "{{ .key | toString | upper }} was templated",
+						defaultKey: "{{ .key | toString | upper }} was templated",
 					},
 				},
 			},
@@ -269,6 +432,7 @@ var _ = Describe("ExternalSecret controller", func() {
 			return true
 		}
 	}
+
 	// if target Secret name is not specified it should use the ExternalSecret name.
 	syncAndDeleteSuccessfully := func(tc *testCase) {
 		fakeProvider.SetSecretFn = func() error {
@@ -295,9 +459,9 @@ var _ = Describe("ExternalSecret controller", func() {
 				Data: []v1alpha1.PushSecretData{
 					{
 						Match: v1alpha1.PushSecretMatch{
-							SecretKey: "key",
+							SecretKey: defaultKey,
 							RemoteRef: v1alpha1.PushSecretRemoteRef{
-								RemoteKey: "path/to/key",
+								RemoteKey: defaultPath,
 							},
 						},
 					},
@@ -305,7 +469,7 @@ var _ = Describe("ExternalSecret controller", func() {
 			},
 		}
 		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
-			ps.Spec.Data[0].Match.RemoteRef.RemoteKey = "different-key"
+			ps.Spec.Data[0].Match.RemoteRef.RemoteKey = newKey
 			updatedPS := &v1alpha1.PushSecret{}
 			Expect(k8sClient.Update(context.Background(), ps, &client.UpdateOptions{})).Should(Succeed())
 			Eventually(func() bool {
@@ -315,11 +479,11 @@ var _ = Describe("ExternalSecret controller", func() {
 				if err != nil {
 					return false
 				}
-				key, ok := updatedPS.Status.SyncedPushSecrets[fmt.Sprintf("SecretStore/%v", PushSecretStore)]["different-key"]
+				key, ok := updatedPS.Status.SyncedPushSecrets[fmt.Sprintf(storePrefixTemplate, PushSecretStore)][newKey]
 				if !ok {
 					return false
 				}
-				return key.Match.SecretKey == "key"
+				return key.Match.SecretKey == defaultKey
 			}, time.Second*10, time.Second).Should(BeTrue())
 			return true
 		}
@@ -352,9 +516,9 @@ var _ = Describe("ExternalSecret controller", func() {
 				Data: []v1alpha1.PushSecretData{
 					{
 						Match: v1alpha1.PushSecretMatch{
-							SecretKey: "key",
+							SecretKey: defaultKey,
 							RemoteRef: v1alpha1.PushSecretRemoteRef{
-								RemoteKey: "path/to/key",
+								RemoteKey: defaultPath,
 							},
 						},
 					},
@@ -362,7 +526,7 @@ var _ = Describe("ExternalSecret controller", func() {
 			},
 		}
 		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
-			ps.Spec.Data[0].Match.RemoteRef.RemoteKey = "different-key"
+			ps.Spec.Data[0].Match.RemoteRef.RemoteKey = newKey
 			updatedPS := &v1alpha1.PushSecret{}
 			Expect(k8sClient.Update(context.Background(), ps, &client.UpdateOptions{})).Should(Succeed())
 			Eventually(func() bool {
@@ -372,11 +536,11 @@ var _ = Describe("ExternalSecret controller", func() {
 				if err != nil {
 					return false
 				}
-				_, ok := updatedPS.Status.SyncedPushSecrets[fmt.Sprintf("SecretStore/%v", PushSecretStore)]["different-key"]
+				_, ok := updatedPS.Status.SyncedPushSecrets[fmt.Sprintf(storePrefixTemplate, PushSecretStore)][newKey]
 				if !ok {
 					return false
 				}
-				_, ok = updatedPS.Status.SyncedPushSecrets[fmt.Sprintf("SecretStore/%v", PushSecretStore)]["path/to/key"]
+				_, ok = updatedPS.Status.SyncedPushSecrets[fmt.Sprintf(storePrefixTemplate, PushSecretStore)][defaultPath]
 				return ok
 			}, time.Second*10, time.Second).Should(BeTrue())
 			return true
@@ -460,7 +624,7 @@ var _ = Describe("ExternalSecret controller", func() {
 				if err != nil {
 					return false
 				}
-				key, ok := updatedPS.Status.SyncedPushSecrets["SecretStore/new-store"]["path/to/key"]
+				key, ok := updatedPS.Status.SyncedPushSecrets["SecretStore/new-store"][defaultPath]
 				if !ok {
 					return false
 				}
@@ -468,7 +632,7 @@ var _ = Describe("ExternalSecret controller", func() {
 				if syncedLen != 1 {
 					return false
 				}
-				return key.Match.SecretKey == "key"
+				return key.Match.SecretKey == defaultKey
 			}, time.Second*10, time.Second).Should(BeTrue())
 			return true
 		}
@@ -505,9 +669,9 @@ var _ = Describe("ExternalSecret controller", func() {
 				Data: []v1alpha1.PushSecretData{
 					{
 						Match: v1alpha1.PushSecretMatch{
-							SecretKey: "key",
+							SecretKey: defaultKey,
 							RemoteRef: v1alpha1.PushSecretRemoteRef{
-								RemoteKey: "path/to/key",
+								RemoteKey: defaultPath,
 							},
 						},
 					},
@@ -534,7 +698,7 @@ var _ = Describe("ExternalSecret controller", func() {
 			},
 		}
 		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
-			secretValue := secret.Data["key"]
+			secretValue := secret.Data[defaultKey]
 			providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
 			expected := v1alpha1.PushSecretStatusCondition{
 				Type:    v1alpha1.PushSecretReady,
@@ -566,7 +730,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 		tc.pushsecret.Spec.SecretStoreRefs[0].Kind = "ClusterSecretStore"
 		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
-			secretValue := secret.Data["key"]
+			secretValue := secret.Data[defaultKey]
 			providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
 			expected := v1alpha1.PushSecretStatusCondition{
 				Type:    v1alpha1.PushSecretReady,
@@ -606,9 +770,9 @@ var _ = Describe("ExternalSecret controller", func() {
 				Data: []v1alpha1.PushSecretData{
 					{
 						Match: v1alpha1.PushSecretMatch{
-							SecretKey: "key",
+							SecretKey: defaultKey,
 							RemoteRef: v1alpha1.PushSecretRemoteRef{
-								RemoteKey: "path/to/key",
+								RemoteKey: defaultPath,
 							},
 						},
 					},
@@ -631,7 +795,7 @@ var _ = Describe("ExternalSecret controller", func() {
 			},
 		}
 		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
-			secretValue := secret.Data["key"]
+			secretValue := secret.Data[defaultKey]
 			providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
 			expected := v1alpha1.PushSecretStatusCondition{
 				Type:    v1alpha1.PushSecretReady,
@@ -768,6 +932,10 @@ var _ = Describe("ExternalSecret controller", func() {
 			// this must be optional so we can test faulty es configuration
 		},
 		Entry("should sync", syncSuccessfully),
+		Entry("should not update existing secret if UpdatePolicy=IfNotExists", updateIfNotExists),
+		Entry("should only update parts of secret that don't already exist if UpdatePolicy=IfNotExists", updateIfNotExistsPartialSecrets),
+		Entry("should update the PushSecret status correctly if UpdatePolicy=IfNotExists", updateIfNotExistsSyncStatus),
+		Entry("should fail if secret existence cannot be verified if UpdatePolicy=IfNotExists", updateIfNotExistsSyncFailed),
 		Entry("should sync with template", syncSuccessfullyWithTemplate),
 		Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
 		Entry("should track deletion tasks if Delete fails", failDelete),

+ 4 - 0
pkg/controllers/secretstore/client_manager_test.go

@@ -349,6 +349,10 @@ func (c *MockFakeClient) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretR
 	return nil
 }
 
+func (c *MockFakeClient) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, nil
+}
+
 func (c *MockFakeClient) GetSecret(_ context.Context, _ esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
 	return nil, nil
 }

+ 8 - 3
pkg/provider/akeyless/akeyless.go

@@ -44,7 +44,8 @@ import (
 )
 
 const (
-	defaultAPIUrl = "https://api.akeyless.io"
+	defaultAPIUrl     = "https://api.akeyless.io"
+	errNotImplemented = "not implemented"
 )
 
 // https://github.com/external-secrets/external-secrets/issues/644
@@ -236,11 +237,15 @@ func (a *Akeyless) Validate() (esv1beta1.ValidationResult, error) {
 }
 
 func (a *Akeyless) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
 }
 
 func (a *Akeyless) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (a *Akeyless) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf(errNotImplemented)
 }
 
 // Implements store.Client.GetSecret Interface.

+ 8 - 3
pkg/provider/alibaba/kms.go

@@ -39,6 +39,7 @@ const (
 	errUninitalizedAlibabaProvider = "provider Alibaba is not initialized"
 	errFetchAccessKeyID            = "could not fetch AccessKeyID secret: %w"
 	errFetchAccessKeySecret        = "could not fetch AccessKeySecret secret: %w"
+	errNotImplemented              = "not implemented"
 )
 
 // https://github.com/external-secrets/external-secrets/issues/644
@@ -56,17 +57,21 @@ type SMInterface interface {
 }
 
 func (kms *KeyManagementService) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
 }
 
 func (kms *KeyManagementService) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (kms *KeyManagementService) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf(errNotImplemented)
 }
 
 // Empty GetAllSecrets.
 func (kms *KeyManagementService) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	// TO be implemented
-	return nil, fmt.Errorf("GetAllSecrets not implemented")
+	return nil, fmt.Errorf(errNotImplemented)
 }
 
 // GetSecret returns a single secret from the provider.

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

@@ -133,6 +133,10 @@ func (pm *ParameterStore) DeleteSecret(ctx context.Context, remoteRef esv1beta1.
 	return nil
 }
 
+func (pm *ParameterStore) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 func (pm *ParameterStore) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
 	parameterType := "String"
 	overwrite := true

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

@@ -204,6 +204,10 @@ func (sm *SecretsManager) DeleteSecret(ctx context.Context, remoteRef esv1beta1.
 	return err
 }
 
+func (sm *SecretsManager) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 func (sm *SecretsManager) PushSecret(ctx context.Context, secret *corev1.Secret, psd esv1beta1.PushSecretData) error {
 	if psd.GetSecretKey() == "" {
 		return fmt.Errorf("pushing the whole secret is not yet implemented")

+ 4 - 0
pkg/provider/azure/keyvault/keyvault.go

@@ -310,6 +310,10 @@ func (a *Azure) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecret
 	}
 }
 
+func (a *Azure) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 func getCertificateFromValue(value []byte) (*x509.Certificate, error) {
 	// 1st: try decode pkcs12
 	_, localCert, err := pkcs12.Decode(value, "")

+ 7 - 2
pkg/provider/chef/chef.go

@@ -59,6 +59,7 @@ const (
 	errStoreValidateFailed                   = "unable to validate provided store. Check if username, serverUrl and privateKey are correct"
 	errServerURLNoEndSlash                   = "serverurl does not end with slash(/)"
 	errInvalidDataform                       = "invalid key format in dataForm section. Expected only 'databagName'"
+	errNotImplemented                        = "not implemented"
 
 	ProviderChef             = "Chef"
 	CallChefGetDataBagItem   = "GetDataBagItem"
@@ -329,12 +330,16 @@ func getChefProvider(store v1beta1.GenericStore) (*v1beta1.ChefProvider, error)
 
 // Not Implemented DeleteSecret.
 func (providerchef *Providerchef) DeleteSecret(_ context.Context, _ v1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
 }
 
 // Not Implemented PushSecret.
 func (providerchef *Providerchef) PushSecret(_ context.Context, _ *corev1.Secret, _ v1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (providerchef *Providerchef) SecretExists(_ context.Context, _ v1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf(errNotImplemented)
 }
 
 // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).

+ 4 - 0
pkg/provider/conjur/provider.go

@@ -193,6 +193,10 @@ func (p *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef
 	return nil
 }
 
+func (p *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 // GetSecretMap returns multiple k/v pairs from the provider.
 func (p *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	// Gets a secret as normal, expecting secret value to be a json object

+ 4 - 0
pkg/provider/delinea/client.go

@@ -71,6 +71,10 @@ func (c *client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef
 	return errors.New("deleting secrets is not supported by Delinea DevOps Secrets Vault")
 }
 
+func (c *client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, errors.New("not implemented")
+}
+
 func (c *client) Validate() (esv1beta1.ValidationResult, error) {
 	return esv1beta1.ValidationResultReady, nil
 }

+ 4 - 0
pkg/provider/doppler/client.go

@@ -118,6 +118,10 @@ func (c *Client) DeleteSecret(_ context.Context, ref esv1beta1.PushSecretRemoteR
 	return nil
 }
 
+func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 func (c *Client) PushSecret(_ context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
 	value := secret.Data[data.GetSecretKey()]
 

+ 5 - 0
pkg/provider/fake/fake.go

@@ -112,6 +112,11 @@ func (p *Provider) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteR
 	return nil
 }
 
+func (p *Provider) SecretExists(_ context.Context, ref esv1beta1.PushSecretRemoteRef) (bool, error) {
+	_, ok := p.config[ref.GetRemoteKey()]
+	return ok, nil
+}
+
 func (p *Provider) PushSecret(_ context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
 	value := secret.Data[data.GetSecretKey()]
 	currentData, ok := p.config[data.GetRemoteKey()]

+ 57 - 0
pkg/provider/fake/fake_test.go

@@ -27,6 +27,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
 
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	testingfake "github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
 )
@@ -393,6 +394,62 @@ func TestSetSecret(t *testing.T) {
 	}
 }
 
+type secretExistsTestCase struct {
+	name      string
+	input     []esv1beta1.FakeProviderData
+	request   esv1alpha1.PushSecretRemoteRef
+	expExists bool
+}
+
+func TestSecretExists(t *testing.T) {
+	gomega.RegisterTestingT(t)
+	p := &Provider{}
+	tbl := []secretExistsTestCase{
+		{
+			name:  "return false, nil if no existing secret",
+			input: []esv1beta1.FakeProviderData{},
+			request: esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: "/foo",
+			},
+			expExists: false,
+		},
+		{
+			name: "return true, nil if existing secret",
+			input: []esv1beta1.FakeProviderData{
+				{
+					Key:   "/foo",
+					Value: "bar",
+				},
+			},
+			request: esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: "/foo",
+			},
+			expExists: true,
+		},
+	}
+
+	for i, row := range tbl {
+		t.Run(row.name, func(t *testing.T) {
+			cl, err := p.NewClient(context.Background(), &esv1beta1.SecretStore{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: fmt.Sprintf("secret-store-%v", i),
+				},
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Fake: &esv1beta1.FakeProvider{
+							Data: row.input,
+						},
+					},
+				},
+			}, nil, "")
+			gomega.Expect(err).ToNot(gomega.HaveOccurred())
+			exists, err := cl.SecretExists(context.TODO(), row.request)
+			gomega.Expect(err).ToNot(gomega.HaveOccurred())
+			gomega.Expect(exists).To(gomega.Equal(row.expExists))
+		})
+	}
+}
+
 type testMapCase struct {
 	name     string
 	input    []esv1beta1.FakeProviderData

+ 4 - 0
pkg/provider/fortanix/fortanix.go

@@ -78,6 +78,10 @@ func (c *client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.Pus
 	return errors.New(errPushSecretsNotSupported)
 }
 
+func (c *client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, errors.New(errPushSecretsNotSupported)
+}
+
 func (c *client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
 	return errors.New(errDeleteSecretsNotSupported)
 }

+ 4 - 0
pkg/provider/gcp/secretmanager/client.go

@@ -129,6 +129,10 @@ func parseError(err error) error {
 	return err
 }
 
+func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 // PushSecret pushes a kubernetes secret key into gcp provider Secret.
 func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, pushSecretData esv1beta1.PushSecretData) error {
 	if pushSecretData.GetSecretKey() == "" {

+ 7 - 2
pkg/provider/gitlab/gitlab.go

@@ -49,6 +49,7 @@ const (
 	errTagsOnlyEnvironmentSupported           = "'find.tags' only supports 'environment_scope'"
 	errPathNotImplemented                     = "'find.path' is not implemented in the GitLab provider"
 	errJSONSecretUnmarshal                    = "unable to unmarshal secret: %w"
+	errNotImplemented                         = "not implemented"
 )
 
 // https://github.com/external-secrets/external-secrets/issues/644
@@ -88,11 +89,15 @@ func (g *gitlabBase) getAuth(ctx context.Context) (string, error) {
 }
 
 func (g *gitlabBase) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (g *gitlabBase) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf(errNotImplemented)
 }
 
 func (g *gitlabBase) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
 }
 
 // GetAllSecrets syncs all gitlab project and group variables into a single Kubernetes Secret.

+ 8 - 3
pkg/provider/ibm/provider.go

@@ -60,6 +60,7 @@ const (
 	errJSONSecretUnmarshal     = "unable to unmarshal secret: %w"
 	errJSONSecretMarshal       = "unable to marshal secret: %w"
 	errExtractingSecret        = "unable to extract the fetched secret %s of type %s while performing %s"
+	errNotImplemented          = "not implemented"
 )
 
 var contextTimeout = time.Minute * 2
@@ -97,18 +98,22 @@ func (c *client) setAuth(ctx context.Context) error {
 }
 
 func (ibm *providerIBM) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (ibm *providerIBM) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf(errNotImplemented)
 }
 
 // Not Implemented PushSecret.
 func (ibm *providerIBM) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
 }
 
 // Empty GetAllSecrets.
 func (ibm *providerIBM) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	// TO be implemented
-	return nil, fmt.Errorf("GetAllSecrets not implemented")
+	return nil, fmt.Errorf(errNotImplemented)
 }
 
 func (ibm *providerIBM) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {

+ 4 - 0
pkg/provider/keepersecurity/client.go

@@ -212,6 +212,10 @@ func (c *Client) DeleteSecret(_ context.Context, remoteRef esv1beta1.PushSecretR
 	return nil
 }
 
+func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 func (c *Client) buildSecretNameAndKey(remoteRef esv1beta1.PushSecretRemoteRef) ([]string, error) {
 	parts := strings.Split(remoteRef.GetRemoteKey(), "/")
 	if len(parts) != 2 {

+ 4 - 0
pkg/provider/kubernetes/client.go

@@ -100,6 +100,10 @@ func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecre
 	return c.fullDelete(ctx, remoteRef.GetRemoteKey())
 }
 
+func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 func (c *Client) PushSecret(ctx context.Context, secret *v1.Secret, data esv1beta1.PushSecretData) error {
 	if data.GetProperty() == "" && data.GetSecretKey() != "" {
 		return fmt.Errorf("requires property in RemoteRef to push secret value if secret key is defined")

+ 5 - 0
pkg/provider/onboardbase/client.go

@@ -126,6 +126,11 @@ func (c *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef
 	return nil
 }
 
+func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	// not implemented
+	return false, nil
+}
+
 func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
 	// not implemented
 	return nil

+ 4 - 0
pkg/provider/onepassword/onepassword.go

@@ -208,6 +208,10 @@ func (provider *ProviderOnePassword) DeleteSecret(_ context.Context, ref esv1bet
 	return nil
 }
 
+func (provider *ProviderOnePassword) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 const (
 	passwordLabel = "password"
 )

+ 4 - 0
pkg/provider/oracle/oracle.go

@@ -159,6 +159,10 @@ func (vms *VaultManagementService) DeleteSecret(ctx context.Context, remoteRef e
 	}
 }
 
+func (vms *VaultManagementService) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 func (vms *VaultManagementService) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	var page *string
 	var summaries []vault.SecretSummary

+ 4 - 0
pkg/provider/pulumi/pulumi.go

@@ -61,6 +61,10 @@ func (c *client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.Pus
 	return errors.New(errPushSecretsNotSupported)
 }
 
+func (c *client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, errors.New(errPushSecretsNotSupported)
+}
+
 func (c *client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
 	return errors.New(errDeleteSecretsNotSupported)
 }

+ 4 - 0
pkg/provider/scaleway/client.go

@@ -264,6 +264,10 @@ func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecre
 	return nil
 }
 
+func (c *client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("not implemented")
+}
+
 func (c *client) Validate() (esv1beta1.ValidationResult, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()

+ 8 - 4
pkg/provider/senhasegura/dsm/dsm.go

@@ -19,7 +19,6 @@ import (
 	"crypto/tls"
 	"encoding/json"
 	"errors"
-	"fmt"
 	"io"
 	"net/http"
 	"net/url"
@@ -80,6 +79,7 @@ var (
 	errInvalidResponseBody = errors.New("invalid HTTP response body received from senhasegura")
 	errInvalidHTTPCode     = errors.New("received invalid HTTP code from senhasegura")
 	errApplicationError    = errors.New("received application error from senhasegura")
+	errNotImplemented      = errors.New("not implemented")
 )
 
 /*
@@ -93,12 +93,16 @@ func New(isoSession *senhaseguraAuth.SenhaseguraIsoSession) (*DSM, error) {
 }
 
 func (dsm *DSM) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	return errNotImplemented
+}
+
+func (dsm *DSM) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, errNotImplemented
 }
 
 // Not Implemented PushSecret.
 func (dsm *DSM) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+	return errNotImplemented
 }
 
 /*
@@ -165,7 +169,7 @@ TODO: GetAllSecrets functionality is to get secrets from either regexp-matching
 https://github.com/external-secrets/external-secrets/pull/830#discussion_r858657107
 */
 func (dsm *DSM) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (secretData map[string][]byte, err error) {
-	return nil, fmt.Errorf("GetAllSecrets not implemented yet")
+	return nil, errNotImplemented
 }
 
 /*

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

@@ -38,6 +38,7 @@ type Client struct {
 	GetSecretFn     func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error)
 	GetSecretMapFn  func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
 	GetAllSecretsFn func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error)
+	SecretExistsFn  func(context.Context, esv1beta1.PushSecretRemoteRef) (bool, error)
 	SetSecretFn     func() error
 	DeleteSecretFn  func() error
 }
@@ -54,6 +55,9 @@ func New() *Client {
 		GetAllSecretsFn: func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 			return nil, nil
 		},
+		SecretExistsFn: func(context.Context, esv1beta1.PushSecretRemoteRef) (bool, error) {
+			return false, nil
+		},
 		SetSecretFn: func() error {
 			return nil
 		},
@@ -92,6 +96,10 @@ func (v *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef
 	return v.DeleteSecretFn()
 }
 
+func (v *Client) SecretExists(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return v.SecretExistsFn(ctx, ref)
+}
+
 // GetSecret implements the provider.Provider interface.
 func (v *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
 	return v.GetSecretFn(ctx, ref)

+ 48 - 26
pkg/provider/vault/client_get.go

@@ -70,32 +70,7 @@ func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 		}
 	}
 
-	// Return nil if secret value is null
-	if data == nil {
-		return nil, esv1beta1.NoSecretError{}
-	}
-	jsonStr, err := json.Marshal(data)
-	if err != nil {
-		return nil, err
-	}
-	// (1): return raw json if no property is defined
-	if ref.Property == "" {
-		return jsonStr, nil
-	}
-
-	// For backwards compatibility we want the
-	// actual keys to take precedence over gjson syntax
-	// (2): extract key from secret with property
-	if _, ok := data[ref.Property]; ok {
-		return utils.GetByteValueFromMap(data, ref.Property)
-	}
-
-	// (3): extract key from secret using gjson
-	val := gjson.Get(string(jsonStr), ref.Property)
-	if !val.Exists() {
-		return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
-	}
-	return []byte(val.String()), nil
+	return getSecretValue(data, ref.Property)
 }
 
 // GetSecretMap supports two modes of operation:
@@ -123,6 +98,25 @@ func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
 	return byteMap, nil
 }
 
+func (c *client) SecretExists(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (bool, error) {
+	path := c.buildPath(ref.GetRemoteKey())
+	data, err := c.readSecret(ctx, path, "")
+	if err != nil {
+		if errors.Is(err, esv1beta1.NoSecretError{}) {
+			return false, nil
+		}
+		return false, err
+	}
+	value, err := getSecretValue(data, ref.GetProperty())
+	if err != nil {
+		if errors.Is(err, esv1beta1.NoSecretError{}) || err.Error() == fmt.Sprintf(errSecretKeyFmt, ref.GetProperty()) {
+			return false, nil
+		}
+		return false, err
+	}
+	return value != nil, nil
+}
+
 func (c *client) readSecret(ctx context.Context, path, version string) (map[string]interface{}, error) {
 	dataPath := c.buildPath(path)
 
@@ -162,6 +156,34 @@ func (c *client) readSecret(ctx context.Context, path, version string) (map[stri
 	return secretData, nil
 }
 
+func getSecretValue(data map[string]interface{}, property string) ([]byte, error) {
+	if data == nil {
+		return nil, esv1beta1.NoSecretError{}
+	}
+	jsonStr, err := json.Marshal(data)
+	if err != nil {
+		return nil, err
+	}
+	// (1): return raw json if no property is defined
+	if property == "" {
+		return jsonStr, nil
+	}
+
+	// For backwards compatibility we want the
+	// actual keys to take precedence over gjson syntax
+	// (2): extract key from secret with property
+	if _, ok := data[property]; ok {
+		return utils.GetByteValueFromMap(data, property)
+	}
+
+	// (3): extract key from secret using gjson
+	val := gjson.Get(string(jsonStr), property)
+	if !val.Exists() {
+		return nil, fmt.Errorf(errSecretKeyFmt, property)
+	}
+	return []byte(val.String()), nil
+}
+
 func (c *client) readSecretMetadata(ctx context.Context, path string) (map[string]string, error) {
 	metadata := make(map[string]string)
 	url, err := c.buildMetadataPath(path)

+ 188 - 0
pkg/provider/vault/client_get_test.go

@@ -27,6 +27,7 @@ import (
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	testingfake "github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
 	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
 	"github.com/external-secrets/external-secrets/pkg/provider/vault/util"
 )
@@ -695,6 +696,193 @@ func TestGetSecretPath(t *testing.T) {
 	}
 }
 
+func TestSecretExists(t *testing.T) {
+	secret := map[string]interface{}{
+		"foo": "bar",
+	}
+	secretWithNil := map[string]interface{}{
+		"hi": nil,
+	}
+	errNope := errors.New("nope")
+	type args struct {
+		store   *esv1beta1.VaultProvider
+		vClient util.Logical
+	}
+	type want struct {
+		exists bool
+		err    error
+	}
+	tests := map[string]struct {
+		reason string
+		args   args
+		ref    *testingfake.PushSecretData
+		want   want
+	}{
+		"NoExistingSecretV1": {
+			reason: "Should return false, nil if secret does not exist in provider.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, esv1beta1.NoSecretError{}),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret"},
+			want: want{
+				exists: false,
+				err:    nil,
+			},
+		},
+		"NoExistingSecretV2": {
+			reason: "Should return false, nil if secret does not exist in provider.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, esv1beta1.NoSecretError{}),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret"},
+			want: want{
+				exists: false,
+				err:    nil,
+			},
+		},
+		"NoExistingSecretWithPropertyV2": {
+			reason: "Should return false, nil if secret with property does not exist in provider.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secret,
+					}, nil),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret", Property: "different"},
+			want: want{
+				exists: false,
+				err:    nil,
+			},
+		},
+		"NoExistingSecretWithPropertyV1": {
+			reason: "Should return false, nil if secret with property does not exist in provider.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secret, nil),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret", Property: "different"},
+			want: want{
+				exists: false,
+				err:    nil,
+			},
+		},
+		"ExistingSecretV1": {
+			reason: "Should return true, nil if secret exists in provider.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secret, nil),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret"},
+			want: want{
+				exists: true,
+				err:    nil,
+			},
+		},
+		"ExistingSecretV2": {
+			reason: "Should return true, nil if secret exists in provider.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secret,
+					}, nil),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret"},
+			want: want{
+				exists: true,
+				err:    nil,
+			},
+		},
+		"ExistingSecretWithNilV1": {
+			reason: "Should return false, nil if secret in provider has nil value.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(secretWithNil, nil),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret", Property: "hi"},
+			want: want{
+				exists: false,
+				err:    nil,
+			},
+		},
+		"ExistingSecretWithNilV2": {
+			reason: "Should return false, nil if secret in provider has nil value.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
+						"data": secretWithNil,
+					}, nil),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret", Property: "hi"},
+			want: want{
+				exists: false,
+				err:    nil,
+			},
+		},
+		"ErrorReadingSecretV1": {
+			reason: "Should return error if secret existence cannot be verified.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, errNope),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret"},
+			want: want{
+				exists: false,
+				err:    fmt.Errorf(errReadSecret, errNope),
+			},
+		},
+		"ErrorReadingSecretV2": {
+			reason: "Should return error if secret existence cannot be verified.",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, errNope),
+				},
+			},
+			ref: &testingfake.PushSecretData{RemoteKey: "secret"},
+			want: want{
+				exists: false,
+				err:    fmt.Errorf(errReadSecret, errNope),
+			},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			client := &client{
+				logical: tc.args.vClient,
+				store:   tc.args.store,
+			}
+			exists, err := client.SecretExists(context.Background(), tc.ref)
+			if diff := cmp.Diff(exists, tc.want.exists); diff != "" {
+				t.Errorf("\n%s\nvault.SecretExists(...): -want exists, +got exists:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nvault.GetSecret(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
 // EquateErrors returns true if the supplied errors are of the same type and
 // produce identical strings. This mirrors the error comparison behavior of
 // https://github.com/go-test/deep, which most Crossplane tests targeted before

+ 11 - 3
pkg/provider/webhook/webhook.go

@@ -31,6 +31,10 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
+const (
+	errNotImplemented = "not implemented"
+)
+
 // https://github.com/external-secrets/external-secrets/issues/644
 var _ esv1beta1.SecretsClient = &WebHook{}
 var _ esv1beta1.Provider = &Provider{}
@@ -101,18 +105,22 @@ func getProvider(store esv1beta1.GenericStore) (*webhook.Spec, error) {
 }
 
 func (w *WebHook) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (w *WebHook) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf(errNotImplemented)
 }
 
 // Not Implemented PushSecret.
 func (w *WebHook) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
 }
 
 // Empty GetAllSecrets.
 func (w *WebHook) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	// TO be implemented
-	return nil, fmt.Errorf("GetAllSecrets not implemented")
+	return nil, fmt.Errorf(errNotImplemented)
 }
 
 func (w *WebHook) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {

+ 11 - 3
pkg/provider/yandex/common/secretsclient.go

@@ -23,6 +23,10 @@ import (
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 
+const (
+	errNotImplemented = "not implemented"
+)
+
 // https://github.com/external-secrets/external-secrets/issues/644
 var _ esv1beta1.SecretsClient = &yandexCloudSecretsClient{}
 
@@ -38,11 +42,15 @@ func (c *yandexCloudSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.
 }
 
 func (c *yandexCloudSecretsClient) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (c *yandexCloudSecretsClient) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf(errNotImplemented)
 }
 
 func (c *yandexCloudSecretsClient) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+	return fmt.Errorf(errNotImplemented)
 }
 
 func (c *yandexCloudSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
@@ -55,7 +63,7 @@ func (c *yandexCloudSecretsClient) GetSecretMap(ctx context.Context, ref esv1bet
 
 func (c *yandexCloudSecretsClient) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	// TO be implemented
-	return nil, fmt.Errorf("GetAllSecrets not supported")
+	return nil, fmt.Errorf(errNotImplemented)
 }
 
 func (c *yandexCloudSecretsClient) Close(_ context.Context) error {