Repository: external-secrets/external-secrets (Go)

Issue:
Implement Deletion Policies across providers
`DeletionPolicy` is a feature that depends on the provider to return a `SecretNotFound` error whenever the operation failed because the secret didn't yet exist.

This needs to be implemented for the following providers:

- [x] #1868
- [x] #1869
- [x] #1870
- [x] #1871
Implement SecretNotFound returns for GCP Secret Manager
Whenever the client fails to get a secret because it was not found, a specific error `SecretNotFound` should be output. This allows `deletionPolicy` compatibility for GCP Secrets Manager.
Implement SecretNotFound returns for Azure Key vault
Whenever the client fails to get a secret because it was not found, a specific error `SecretNotFound` should be output. This allows `deletionPolicy` compatibility for Azure Keyvault.
Implement SecretNotFound returns for Kubernetes
Whenever the client fails to get a secret because it was not found, a specific error `SecretNotFound` should be output. This allows `deletionPolicy` compatibility for Kubernetes.


Discussion:

Patch:
diff --git a/docs/introduction/stability-support.md b/docs/introduction/stability-support.md
index 243a1163cfe..08a1a84cf85 100644
--- a/docs/introduction/stability-support.md
+++ b/docs/introduction/stability-support.md
@@ -44,24 +44,24 @@ The following table describes the stability level of each provider and who's res
 
 The following table show the support for features across different providers.
 
-| Provider                  | find by name | find by tags | metadataPolicy Fetch | referent authentication | store validation | push secret |
-|---------------------------|:------------:|:------------:| :------------------: | :---------------------: | :--------------: | :---------: |
-| AWS Secrets Manager       |      x       |      x       |                      |            x            |        x         |             |
-| AWS Parameter Store       |      x       |      x       |                      |            x            |        x         |             |
-| Hashicorp Vault           |      x       |      x       |                      |            x            |        x         |             |
-| GCP Secret Manager        |      x       |      x       |                      |            x            |        x         |             |
-| Azure Keyvault            |      x       |      x       |          x           |            x            |        x         |      x      |
-| Kubernetes                |      x       |      x       |                      |            x            |        x         |             |
-| IBM Cloud Secrets Manager |              |              |                      |                         |        x         |             |
-| Yandex Lockbox            |              |              |                      |                         |        x         |             |
-| Gitlab Variables          |      x       |      x       |                      |                         |        x         |             |
-| Alibaba Cloud KMS         |              |              |                      |                         |        x         |             |
-| Oracle Vault              |              |              |                      |                         |        x         |             |
-| Akeyless                  |              |              |                      |                         |        x         |             |
-| 1Password                 |      x       |              |                      |                         |        x         |             |
-| Generic Webhook           |              |              |                      |                         |                  |             |
-| senhasegura DSM           |              |              |                      |                         |        x         |             |
-| Doppler                   |      x       |              |                      |                         |        x         |             |
+| Provider                  | find by name | find by tags | metadataPolicy Fetch | referent authentication | store validation | push secret | DeletionPolicy Merge/Delete |
+|---------------------------|:------------:|:------------:| :------------------: | :---------------------: | :--------------: | :---------: | :-------------------------: 
+| AWS Secrets Manager       |      x       |      x       |                      |            x            |        x         |     x       |            x                |
+| AWS Parameter Store       |      x       |      x       |                      |            x            |        x         |     x       |            x                |
+| Hashicorp Vault           |      x       |      x       |                      |            x            |        x         |     x       |            x                |
+| GCP Secret Manager        |      x       |      x       |                      |            x            |        x         |     x       |            x                |
+| Azure Keyvault            |      x       |      x       |          x           |            x            |        x         |     x       |            x                |
+| Kubernetes                |      x       |      x       |                      |            x            |        x         |     x       |            x                |
+| IBM Cloud Secrets Manager |              |              |                      |                         |        x         |             |                             |
+| Yandex Lockbox            |              |              |                      |                         |        x         |             |                             |
+| Gitlab Variables          |      x       |      x       |                      |                         |        x         |             |                             |
+| Alibaba Cloud KMS         |              |              |                      |                         |        x         |             |                             |
+| Oracle Vault              |              |              |                      |                         |        x         |             |                             |
+| Akeyless                  |              |              |                      |                         |        x         |             |                             |
+| 1Password                 |      x       |              |                      |                         |        x         |             |                             |
+| Generic Webhook           |              |              |                      |                         |                  |             |                             |
+| senhasegura DSM           |              |              |                      |                         |        x         |             |                             |
+| Doppler                   |      x       |              |                      |                         |        x         |             |                             |
 
 
 ## Support Policy
diff --git a/pkg/provider/azure/keyvault/keyvault.go b/pkg/provider/azure/keyvault/keyvault.go
index d0aa7890191..30d08500e72 100644
--- a/pkg/provider/azure/keyvault/keyvault.go
+++ b/pkg/provider/azure/keyvault/keyvault.go
@@ -489,6 +489,7 @@ func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretF
 	checkName := ref.Name != nil && len(ref.Name.RegExp) > 0
 
 	secretListIter, err := basicClient.GetSecretsComplete(context.Background(), *a.provider.VaultURL, nil)
+	err = parseError(err)
 	if err != nil {
 		return nil, err
 	}
@@ -502,6 +503,7 @@ func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretF
 			}
 
 			secretResp, err := basicClient.GetSecret(context.Background(), *a.provider.VaultURL, secretName, "")
+			err = parseError(err)
 			if err != nil {
 				return nil, err
 			}
@@ -568,6 +570,14 @@ func getProperty(secret, property, key string) ([]byte, error) {
 	return []byte(res.String()), nil
 }
 
+func parseError(err error) error {
+	aerr := autorest.DetailedError{}
+	if errors.As(err, &aerr) && aerr.StatusCode == 404 {
+		return esv1beta1.NoSecretError{}
+	}
+	return err
+}
+
 // Implements store.Client.GetSecret Interface.
 // Retrieves a secret/Key/Certificate/Tag with the secret name defined in ref.Name
 // The Object Type is defined as a prefix in the ref.Name , if no prefix is defined , we assume a secret is required.
@@ -579,6 +589,7 @@ func (a *Azure) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataR
 		// returns a SecretBundle with the secret value
 		// https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#SecretBundle
 		secretResp, err := a.baseClient.GetSecret(context.Background(), *a.provider.VaultURL, secretName, ref.Version)
+		err = parseError(err)
 		if err != nil {
 			return nil, err
 		}
@@ -590,6 +601,7 @@ func (a *Azure) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataR
 		// returns a CertBundle. We return CER contents of x509 certificate
 		// see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#CertificateBundle
 		certResp, err := a.baseClient.GetCertificate(context.Background(), *a.provider.VaultURL, secretName, ref.Version)
+		err = parseError(err)
 		if err != nil {
 			return nil, err
 		}
@@ -602,6 +614,7 @@ func (a *Azure) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataR
 		// azure kv returns only public keys
 		// see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#KeyBundle
 		keyResp, err := a.baseClient.GetKey(context.Background(), *a.provider.VaultURL, secretName, ref.Version)
+		err = parseError(err)
 		if err != nil {
 			return nil, err
 		}
@@ -618,7 +631,7 @@ func (a *Azure) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataR
 func (a *Azure) getSecretTags(ref esv1beta1.ExternalSecretDataRemoteRef) (map[string]*string, error) {
 	_, secretName := getObjType(ref)
 	secretResp, err := a.baseClient.GetSecret(context.Background(), *a.provider.VaultURL, secretName, ref.Version)
-
+	err = parseError(err)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/provider/gcp/secretmanager/client.go b/pkg/provider/gcp/secretmanager/client.go
index 83f5e2e8d66..73754ea377f 100644
--- a/pkg/provider/gcp/secretmanager/client.go
+++ b/pkg/provider/gcp/secretmanager/client.go
@@ -114,6 +114,14 @@ func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemot
 	return c.smClient.DeleteSecret(ctx, deleteSecretVersionReq)
 }
 
+func parseError(err error) error {
+	var gerr *apierror.APIError
+	if errors.As(err, &gerr) && gerr.GRPCStatus().Code() == codes.NotFound {
+		return esv1beta1.NoSecretError{}
+	}
+	return err
+}
+
 // PushSecret pushes a kubernetes secret key into gcp provider Secret.
 func (c *Client) PushSecret(ctx context.Context, payload []byte, remoteRef esv1beta1.PushRemoteRef) error {
 	createSecretReq := &secretmanagerpb.CreateSecretRequest{
@@ -325,6 +333,7 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
 		Name: fmt.Sprintf("projects/%s/secrets/%s/versions/%s", c.store.ProjectID, ref.Key, version),
 	}
 	result, err := c.smClient.AccessSecretVersion(ctx, req)
+	err = parseError(err)
 	if err != nil {
 		return nil, fmt.Errorf(errClientGetSecretAccess, err)
 	}
diff --git a/pkg/provider/kubernetes/client.go b/pkg/provider/kubernetes/client.go
index 30705b2543a..9ac593fb84b 100644
--- a/pkg/provider/kubernetes/client.go
+++ b/pkg/provider/kubernetes/client.go
@@ -18,6 +18,7 @@ import (
 	"encoding/json"
 	"fmt"
 
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	labels "k8s.io/apimachinery/pkg/labels"
 
@@ -60,6 +61,9 @@ func (c *Client) PushSecret(ctx context.Context, value []byte, remoteRef esv1bet
 
 func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	secret, err := c.userSecretClient.Get(ctx, ref.Key, metav1.GetOptions{})
+	if apierrors.IsNotFound(err) {
+		return nil, esv1beta1.NoSecretError{}
+	}
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/pkg/provider/azure/keyvault/keyvault_test.go b/pkg/provider/azure/keyvault/keyvault_test.go
index 2ab00c8e33d..53452c50110 100644
--- a/pkg/provider/azure/keyvault/keyvault_test.go
+++ b/pkg/provider/azure/keyvault/keyvault_test.go
@@ -763,6 +763,26 @@ func TestAzureKeyVaultSecretManagerGetSecret(t *testing.T) {
 			Value: &secretString,
 		}
 	}
+	// good case
+	secretNotFound := func(smtc *secretManagerTestCase) {
+		smtc.expectedSecret = ""
+		smtc.apiErr = autorest.DetailedError{StatusCode: 404}
+		smtc.expectError = esv1beta1.NoSecretError{}.Error()
+	}
+
+	certNotFound := func(smtc *secretManagerTestCase) {
+		smtc.expectedSecret = ""
+		smtc.secretName = certName
+		smtc.apiErr = autorest.DetailedError{StatusCode: 404}
+		smtc.expectError = esv1beta1.NoSecretError{}.Error()
+	}
+
+	keyNotFound := func(smtc *secretManagerTestCase) {
+		smtc.expectedSecret = ""
+		smtc.secretName = keyName
+		smtc.apiErr = autorest.DetailedError{StatusCode: 404}
+		smtc.expectError = esv1beta1.NoSecretError{}.Error()
+	}
 
 	setSecretStringWithVersion := func(smtc *secretManagerTestCase) {
 		smtc.expectedSecret = secretString
@@ -1062,6 +1082,9 @@ func TestAzureKeyVaultSecretManagerGetSecret(t *testing.T) {
 		makeValidSecretManagerTestCaseCustom(badSecretWithProperty),
 		makeValidSecretManagerTestCaseCustom(setPubRSAKey),
 		makeValidSecretManagerTestCaseCustom(setPubECKey),
+		makeValidSecretManagerTestCaseCustom(secretNotFound),
+		makeValidSecretManagerTestCaseCustom(certNotFound),
+		makeValidSecretManagerTestCaseCustom(keyNotFound),
 		makeValidSecretManagerTestCaseCustom(setCertificate),
 		makeValidSecretManagerTestCaseCustom(badSecretType),
 		makeValidSecretManagerTestCaseCustom(setSecretWithTag),
diff --git a/pkg/provider/gcp/secretmanager/client_test.go b/pkg/provider/gcp/secretmanager/client_test.go
index 659438e51c0..9c524e74010 100644
--- a/pkg/provider/gcp/secretmanager/client_test.go
+++ b/pkg/provider/gcp/secretmanager/client_test.go
@@ -113,6 +113,13 @@ func TestSecretManagerGetSecret(t *testing.T) {
 		smtc.apiOutput.Payload.Data = []byte("testtesttest")
 		smtc.expectedSecret = "testtesttest"
 	}
+	secretNotFound := func(smtc *secretManagerTestCase) {
+		fErr := status.Error(codes.NotFound, "failed")
+		notFoundError, _ := apierror.FromError(fErr)
+		smtc.apiErr = notFoundError
+		smtc.expectedSecret = ""
+		smtc.expectError = esv1beta1.NoSecretErr.Error()
+	}
 	// good case: with a dot in the key name
 	setDotRef := func(smtc *secretManagerTestCase) {
 		smtc.ref = &esv1beta1.ExternalSecretDataRemoteRef{
@@ -164,6 +171,7 @@ func TestSecretManagerGetSecret(t *testing.T) {
 	successCases := []*secretManagerTestCase{
 		makeValidSecretManagerTestCase(),
 		makeValidSecretManagerTestCaseCustom(setSecretString),
+		makeValidSecretManagerTestCaseCustom(secretNotFound),
 		makeValidSecretManagerTestCaseCustom(setCustomVersion),
 		makeValidSecretManagerTestCaseCustom(setAPIErr),
 		makeValidSecretManagerTestCaseCustom(setCustomRef),
diff --git a/pkg/provider/kubernetes/client_test.go b/pkg/provider/kubernetes/client_test.go
index 856e20e3dc0..0e200957fed 100644
--- a/pkg/provider/kubernetes/client_test.go
+++ b/pkg/provider/kubernetes/client_test.go
@@ -21,7 +21,9 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
@@ -34,6 +36,7 @@ type fakeClient struct {
 	t                   *testing.T
 	secretMap           map[string]corev1.Secret
 	expectedListOptions metav1.ListOptions
+	err                 error
 }
 
 func (fk fakeClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.Secret, error) {
@@ -42,7 +45,7 @@ func (fk fakeClient) Get(ctx context.Context, name string, opts metav1.GetOption
 	if !ok {
 		return nil, errors.New(errSomethingWentWrong)
 	}
-	return &secret, nil
+	return &secret, fk.err
 }
 
 func (fk fakeClient) List(ctx context.Context, opts metav1.ListOptions) (*corev1.SecretList, error) {
@@ -68,6 +71,28 @@ func TestGetSecret(t *testing.T) {
 		want    []byte
 		wantErr bool
 	}{
+		{
+			name: "secretNotFound",
+			fields: fields{
+				Client: fakeClient{
+					t: t,
+					secretMap: map[string]corev1.Secret{
+						"mysec": {
+							Data: map[string][]byte{
+								"token": []byte(`foobar`),
+							},
+						},
+					},
+					err: apierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "Secret"}, "secret"),
+				},
+				Namespace: "default",
+			},
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "mysec",
+				Property: "token",
+			},
+			wantErr: true,
+		},
 		{
 			name: "err GetSecretMap",
 			fields: fields{