Browse Source

Implement Doppler Secret Push and Delete functions (#3200)

* Implement Doppler Secret Push and Delete functions

Signed-off-by: Carter Cook <carter.cook@filedeploy.com>

* Better error formatting (PR review #3200)

Signed-off-by: Carter Cook <carter.cook@filedeploy.com>

---------

Signed-off-by: Carter Cook <carter.cook@filedeploy.com>
filedeploy 2 years ago
parent
commit
1fbd7a01e1

+ 41 - 6
pkg/provider/doppler/client.go

@@ -35,8 +35,10 @@ import (
 const (
 	customBaseURLEnvVar                                = "DOPPLER_BASE_URL"
 	verifyTLSOverrideEnvVar                            = "DOPPLER_VERIFY_TLS"
-	errGetSecret                                       = "could not get secret %s: %s"
-	errGetSecrets                                      = "could not get secrets %s"
+	errGetSecret                                       = "could not get secret %s: %w"
+	errGetSecrets                                      = "could not get secrets %w"
+	errDeleteSecrets                                   = "could not delete secrets %s: %w"
+	errPushSecrets                                     = "could not push secrets %s: %w"
 	errUnmarshalSecretMap                              = "unable to unmarshal secret %s: %w"
 	secretsDownloadFileKey                             = "DOPPLER_SECRETS_FILE"
 	errDopplerTokenSecretName                          = "missing auth.secretRef.dopplerToken.name"
@@ -63,6 +65,7 @@ type SecretsClientInterface interface {
 	Authenticate() error
 	GetSecret(request dClient.SecretRequest) (*dClient.SecretResponse, error)
 	GetSecrets(request dClient.SecretsRequest) (*dClient.SecretsResponse, error)
+	UpdateSecrets(request dClient.UpdateSecretsRequest) error
 }
 
 func (c *Client) setAuth(ctx context.Context) error {
@@ -94,12 +97,44 @@ func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
 	return esv1beta1.ValidationResultReady, nil
 }
 
-func (c *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
-	return fmt.Errorf("not implemented")
+func (c *Client) DeleteSecret(_ context.Context, ref esv1beta1.PushSecretRemoteRef) error {
+	request := dClient.UpdateSecretsRequest{
+		ChangeRequests: []dClient.Change{
+			{
+				Name:         ref.GetRemoteKey(),
+				OriginalName: ref.GetRemoteKey(),
+				ShouldDelete: true,
+			},
+		},
+		Project: c.project,
+		Config:  c.config,
+	}
+
+	err := c.doppler.UpdateSecrets(request)
+	if err != nil {
+		return fmt.Errorf(errDeleteSecrets, ref.GetRemoteKey(), err)
+	}
+
+	return nil
 }
 
-func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
-	return fmt.Errorf("not implemented")
+func (c *Client) PushSecret(_ context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
+	value := secret.Data[data.GetSecretKey()]
+
+	request := dClient.UpdateSecretsRequest{
+		Secrets: dClient.Secrets{
+			data.GetRemoteKey(): string(value),
+		},
+		Project: c.project,
+		Config:  c.config,
+	}
+
+	err := c.doppler.UpdateSecrets(request)
+	if err != nil {
+		return fmt.Errorf(errPushSecrets, data.GetRemoteKey(), err)
+	}
+
+	return nil
 }
 
 func (c *Client) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {

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

@@ -41,7 +41,12 @@ type httpRequestBody []byte
 
 type Secrets map[string]string
 
-type RawSecrets map[string]*interface{}
+type Change struct {
+	Name         string  `json:"name"`
+	OriginalName string  `json:"originalName"`
+	Value        *string `json:"value"`
+	ShouldDelete bool    `json:"shouldDelete,omitempty"`
+}
 
 type APIError struct {
 	Err     error
@@ -74,9 +79,10 @@ type SecretsRequest struct {
 }
 
 type UpdateSecretsRequest struct {
-	Secrets RawSecrets `json:"secrets,omitempty"`
-	Project string     `json:"project,omitempty"`
-	Config  string     `json:"config,omitempty"`
+	Secrets        Secrets  `json:"secrets,omitempty"`
+	ChangeRequests []Change `json:"change_requests,omitempty"`
+	Project        string   `json:"project,omitempty"`
+	Config         string   `json:"config,omitempty"`
 }
 
 type secretResponseBody struct {

+ 172 - 1
pkg/provider/doppler/doppler_test.go

@@ -21,7 +21,9 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	corev1 "k8s.io/api/core/v1"
 
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
 	"github.com/external-secrets/external-secrets/pkg/provider/doppler/client"
@@ -31,11 +33,15 @@ import (
 const (
 	validSecretName   = "API_KEY"
 	validSecretValue  = "3a3ea4f5"
+	validRemoteKey    = "REMOTE_KEY"
 	dopplerProject    = "DOPPLER_PROJECT"
 	dopplerProjectVal = "auth-api"
 	missingSecret     = "INVALID_NAME"
 	invalidSecret     = "doppler_project"
+	invalidRemoteKey  = "INVALID_REMOTE_KEY"
 	missingSecretErr  = "could not get secret"
+	missingDeleteErr  = "could not delete secrets"
+	missingPushErr    = "could not push secrets"
 )
 
 type dopplerTestCase struct {
@@ -50,12 +56,43 @@ type dopplerTestCase struct {
 	expectedData   map[string][]byte
 }
 
+type updateSecretCase struct {
+	label       string
+	fakeClient  *fake.DopplerClient
+	request     client.UpdateSecretsRequest
+	remoteRef   *esv1alpha1.PushSecretRemoteRef
+	secret      corev1.Secret
+	secretData  esv1beta1.PushSecretData
+	apiErr      error
+	expectError string
+}
+
 func makeValidAPIRequest() client.SecretRequest {
 	return client.SecretRequest{
 		Name: validSecretName,
 	}
 }
 
+func makeValidPushRequest() client.UpdateSecretsRequest {
+	return client.UpdateSecretsRequest{
+		Secrets: client.Secrets{
+			validRemoteKey: validSecretValue,
+		},
+	}
+}
+
+func makeValidDeleteRequest() client.UpdateSecretsRequest {
+	return client.UpdateSecretsRequest{
+		ChangeRequests: []client.Change{
+			{
+				Name:         validRemoteKey,
+				OriginalName: validRemoteKey,
+				ShouldDelete: true,
+			},
+		},
+	}
+}
+
 func makeValidAPIOutput() *client.SecretResponse {
 	return &client.SecretResponse{
 		Name:  validSecretName,
@@ -69,6 +106,33 @@ func makeValidRemoteRef() *esv1beta1.ExternalSecretDataRemoteRef {
 	}
 }
 
+func makeValidPushRemoteRef() *esv1alpha1.PushSecretRemoteRef {
+	return &esv1alpha1.PushSecretRemoteRef{
+		RemoteKey: validRemoteKey,
+	}
+}
+
+func makeValidSecret() corev1.Secret {
+	return corev1.Secret{
+		Data: map[string][]byte{
+			validSecretName: []byte(validSecretValue),
+		},
+	}
+}
+
+func makeValidSecretData() esv1alpha1.PushSecretData {
+	return makeSecretData(validSecretName, *makeValidPushRemoteRef())
+}
+
+func makeSecretData(key string, ref esv1alpha1.PushSecretRemoteRef) esv1alpha1.PushSecretData {
+	return esv1alpha1.PushSecretData{
+		Match: esv1alpha1.PushSecretMatch{
+			SecretKey: key,
+			RemoteRef: ref,
+		},
+	}
+}
+
 func makeValidDopplerTestCase() *dopplerTestCase {
 	return &dopplerTestCase{
 		fakeClient:     &fake.DopplerClient{},
@@ -82,6 +146,17 @@ func makeValidDopplerTestCase() *dopplerTestCase {
 	}
 }
 
+func makeValidUpdateSecretTestCase() *updateSecretCase {
+	return &updateSecretCase{
+		fakeClient:  &fake.DopplerClient{},
+		remoteRef:   makeValidPushRemoteRef(),
+		secret:      makeValidSecret(),
+		secretData:  makeValidSecretData(),
+		apiErr:      nil,
+		expectError: "",
+	}
+}
+
 func makeValidDopplerTestCaseCustom(tweaks ...func(pstc *dopplerTestCase)) *dopplerTestCase {
 	pstc := makeValidDopplerTestCase()
 	for _, fn := range tweaks {
@@ -91,6 +166,15 @@ func makeValidDopplerTestCaseCustom(tweaks ...func(pstc *dopplerTestCase)) *dopp
 	return pstc
 }
 
+func makeValidUpdateSecretCaseCustom(tweaks ...func(pstc *updateSecretCase)) *updateSecretCase {
+	pstc := makeValidUpdateSecretTestCase()
+	for _, fn := range tweaks {
+		fn(pstc)
+	}
+	pstc.fakeClient.WithUpdateValue(pstc.request, pstc.apiErr)
+	return pstc
+}
+
 func TestGetSecret(t *testing.T) {
 	setSecret := func(pstc *dopplerTestCase) {
 		pstc.label = "set secret"
@@ -120,7 +204,7 @@ func TestGetSecret(t *testing.T) {
 	}
 
 	setClientError := func(pstc *dopplerTestCase) {
-		pstc.label = "invalid client error"
+		pstc.label = "invalid client error" //nolint:goconst
 		pstc.response = &client.SecretResponse{}
 		pstc.expectError = missingSecretErr
 		pstc.apiErr = fmt.Errorf("")
@@ -205,6 +289,93 @@ func ErrorContains(out error, want string) bool {
 	return strings.Contains(out.Error(), want)
 }
 
+func TestDeleteSecret(t *testing.T) {
+	deleteSecret := func(pstc *updateSecretCase) {
+		pstc.label = "delete secret"
+		pstc.request = makeValidDeleteRequest()
+	}
+
+	deleteMissingSecret := func(pstc *updateSecretCase) {
+		pstc.label = "delete missing secret"
+		pstc.request = makeValidDeleteRequest()
+		pstc.remoteRef.RemoteKey = invalidRemoteKey
+		pstc.expectError = missingDeleteErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	setClientError := func(pstc *updateSecretCase) {
+		pstc.label = "invalid client error"
+		pstc.request = makeValidDeleteRequest()
+		pstc.expectError = missingDeleteErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	testCases := []*updateSecretCase{
+		makeValidUpdateSecretCaseCustom(deleteSecret),
+		makeValidUpdateSecretCaseCustom(deleteMissingSecret),
+		makeValidUpdateSecretCaseCustom(setClientError),
+	}
+
+	c := Client{}
+	for k, tc := range testCases {
+		c.doppler = tc.fakeClient
+		err := c.DeleteSecret(context.Background(), tc.remoteRef)
+
+		if !ErrorContains(err, tc.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), tc.expectError)
+		}
+	}
+}
+
+func TestPushSecret(t *testing.T) {
+	pushSecret := func(pstc *updateSecretCase) {
+		pstc.label = "push secret"
+		pstc.request = makeValidPushRequest()
+	}
+
+	pushMissingSecretKey := func(pstc *updateSecretCase) {
+		pstc.label = "push missing secret key"
+		pstc.secretData = makeSecretData(invalidSecret, *makeValidPushRemoteRef())
+		pstc.expectError = missingPushErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	pushMissingRemoteSecret := func(pstc *updateSecretCase) {
+		pstc.label = "push missing remote secret"
+		pstc.secretData = makeSecretData(
+			validSecretName,
+			esv1alpha1.PushSecretRemoteRef{
+				RemoteKey: invalidRemoteKey,
+			},
+		)
+		pstc.expectError = missingPushErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	setClientError := func(pstc *updateSecretCase) {
+		pstc.label = "invalid client error"
+		pstc.expectError = missingPushErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	testCases := []*updateSecretCase{
+		makeValidUpdateSecretCaseCustom(pushSecret),
+		makeValidUpdateSecretCaseCustom(pushMissingSecretKey),
+		makeValidUpdateSecretCaseCustom(pushMissingRemoteSecret),
+		makeValidUpdateSecretCaseCustom(setClientError),
+	}
+
+	c := Client{}
+	for k, tc := range testCases {
+		c.doppler = tc.fakeClient
+		err := c.PushSecret(context.Background(), &tc.secret, tc.secretData)
+
+		if !ErrorContains(err, tc.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), tc.expectError)
+		}
+	}
+}
+
 type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore
 
 func makeSecretStore(fn ...storeModifier) *esv1beta1.SecretStore {

+ 17 - 1
pkg/provider/doppler/fake/fake.go

@@ -24,7 +24,8 @@ import (
 )
 
 type DopplerClient struct {
-	getSecret func(request client.SecretRequest) (*client.SecretResponse, error)
+	getSecret     func(request client.SecretRequest) (*client.SecretResponse, error)
+	updateSecrets func(request client.UpdateSecretsRequest) error
 }
 
 func (dc *DopplerClient) BaseURL() *url.URL {
@@ -44,6 +45,10 @@ func (dc *DopplerClient) GetSecrets(_ client.SecretsRequest) (*client.SecretsRes
 	return &client.SecretsResponse{}, nil
 }
 
+func (dc *DopplerClient) UpdateSecrets(request client.UpdateSecretsRequest) error {
+	return dc.updateSecrets(request)
+}
+
 func (dc *DopplerClient) WithValue(request client.SecretRequest, response *client.SecretResponse, err error) {
 	if dc != nil {
 		dc.getSecret = func(requestIn client.SecretRequest) (*client.SecretResponse, error) {
@@ -54,3 +59,14 @@ func (dc *DopplerClient) WithValue(request client.SecretRequest, response *clien
 		}
 	}
 }
+
+func (dc *DopplerClient) WithUpdateValue(request client.UpdateSecretsRequest, err error) {
+	if dc != nil {
+		dc.updateSecrets = func(requestIn client.UpdateSecretsRequest) error {
+			if !cmp.Equal(requestIn, request) {
+				return fmt.Errorf("unexpected test argument")
+			}
+			return err
+		}
+	}
+}