github.com/thiagoyeds/go-cloud@v0.26.0/runtimevar/awssecretsmanager/awssecretsmanager_test.go (about)

     1  // Copyright 2020 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package awssecretsmanager
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"crypto/sha1"
    21  	"errors"
    22  	"fmt"
    23  	"os"
    24  	"testing"
    25  	"time"
    26  
    27  	awsv2 "github.com/aws/aws-sdk-go-v2/aws"
    28  	secretsmanagerv2 "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
    29  	"github.com/aws/aws-sdk-go/aws"
    30  	"github.com/aws/aws-sdk-go/aws/awserr"
    31  	"github.com/aws/aws-sdk-go/aws/client"
    32  	"github.com/aws/aws-sdk-go/aws/session"
    33  	"github.com/aws/aws-sdk-go/service/secretsmanager"
    34  	"github.com/aws/smithy-go"
    35  	"github.com/googleapis/gax-go/v2"
    36  	"gocloud.dev/gcerrors"
    37  	"gocloud.dev/internal/gcerr"
    38  	"gocloud.dev/internal/retry"
    39  	"gocloud.dev/internal/testing/setup"
    40  	"gocloud.dev/runtimevar"
    41  	"gocloud.dev/runtimevar/driver"
    42  	"gocloud.dev/runtimevar/drivertest"
    43  )
    44  
    45  // This constant records the region used for the last --record.
    46  // If you want to use --record mode,
    47  // 1. Update this constant to your AWS region.
    48  // TODO(issue #300): Use Terraform to get this.
    49  const region = "us-east-2"
    50  
    51  type harness struct {
    52  	useV2    bool
    53  	session  client.ConfigProvider
    54  	clientV2 *secretsmanagerv2.Client
    55  	closer   func()
    56  }
    57  
    58  // waitForMutation uses check to wait until a mutation has taken effect.
    59  // The check function should return nil to indicate success (the mutation has
    60  // taken effect), an error with gcerrors.ErrorCode == NotFound to trigger
    61  // a retry, or any other error to signal permanent failure.
    62  func waitForMutation(ctx context.Context, check func() error) error {
    63  	backoff := gax.Backoff{Multiplier: 1.0}
    64  	var initial time.Duration
    65  	if *setup.Record {
    66  		// When recording, wait 15 seconds and then poll every 5s.
    67  		initial = 15 * time.Second
    68  		backoff.Initial = 5 * time.Second
    69  	} else {
    70  		// During replay, we don't wait at all.
    71  		// The recorded file may have retries, but we don't need to actually wait between them.
    72  		backoff.Initial = 1 * time.Millisecond
    73  	}
    74  	backoff.Max = backoff.Initial
    75  
    76  	// Sleep before the check, since we know it doesn't take effect right away.
    77  	time.Sleep(initial)
    78  
    79  	// retryIfNotFound returns true if err is NotFound.
    80  	var retryIfNotFound = func(err error) bool { return gcerrors.Code(err) == gcerrors.NotFound }
    81  
    82  	// Poll until the mtuation is seen.
    83  	return retry.Call(ctx, backoff, retryIfNotFound, check)
    84  }
    85  
    86  // AWS Secrets Manager requires unique token for Create and Update requests to ensure idempotency.
    87  // From the other side, request data must be deterministic in order to make tests reproducible.
    88  // generateClientRequestToken generates token which is unique per test session but deterministic.
    89  func generateClientRequestToken(name string, data []byte) string {
    90  	const maxClientRequestTokenLen = 64
    91  	h := sha1.New()
    92  	_, _ = h.Write(data)
    93  
    94  	token := fmt.Sprintf("%s-%x", name, h.Sum(nil))
    95  
    96  	// Token must have length less than or equal to 64
    97  	if len(token) > maxClientRequestTokenLen {
    98  		token = token[:maxClientRequestTokenLen]
    99  	}
   100  
   101  	return token
   102  }
   103  
   104  func newHarness(t *testing.T) (drivertest.Harness, error) {
   105  	sess, _, done, _ := setup.NewAWSSession(context.Background(), t, region)
   106  
   107  	return &harness{
   108  		useV2:   false,
   109  		session: sess,
   110  		closer:  done,
   111  	}, nil
   112  }
   113  
   114  func newHarnessV2(t *testing.T) (drivertest.Harness, error) {
   115  	cfg, _, done, _ := setup.NewAWSv2Config(context.Background(), t, region)
   116  	return &harness{
   117  		useV2:    true,
   118  		clientV2: secretsmanagerv2.NewFromConfig(cfg),
   119  		closer:   done,
   120  	}, nil
   121  }
   122  
   123  func (h *harness) MakeWatcher(_ context.Context, name string, decoder *runtimevar.Decoder) (driver.Watcher, error) {
   124  	return newWatcher(h.useV2, h.session, h.clientV2, name, decoder, nil), nil
   125  }
   126  
   127  func (h *harness) CreateVariable(ctx context.Context, name string, val []byte) error {
   128  	var err error
   129  	var svc *secretsmanager.SecretsManager
   130  	if h.useV2 {
   131  		_, err = h.clientV2.CreateSecret(ctx, &secretsmanagerv2.CreateSecretInput{
   132  			Name:               awsv2.String(name),
   133  			ClientRequestToken: awsv2.String(generateClientRequestToken(name, val)),
   134  			SecretBinary:       val,
   135  		})
   136  	} else {
   137  		svc = secretsmanager.New(h.session)
   138  		_, err = svc.CreateSecretWithContext(ctx, &secretsmanager.CreateSecretInput{
   139  			Name:               aws.String(name),
   140  			ClientRequestToken: aws.String(generateClientRequestToken(name, val)),
   141  			SecretBinary:       val,
   142  		})
   143  	}
   144  	if err != nil {
   145  		return err
   146  	}
   147  	// Secret Manager is only eventually consistent, so we retry until we've
   148  	// verified that the mutation was applied. This is still not a guarantee
   149  	// but in practice seems to work well enough to make tests repeatable.
   150  	return waitForMutation(ctx, func() error {
   151  		var err error
   152  		if h.useV2 {
   153  			_, err = h.clientV2.GetSecretValue(ctx, &secretsmanagerv2.GetSecretValueInput{SecretId: awsv2.String(name)})
   154  		} else {
   155  			_, err = svc.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{SecretId: aws.String(name)})
   156  		}
   157  		if err == nil {
   158  			// Create was seen.
   159  			return nil
   160  		}
   161  		// Failure; we'll retry if it's a NotFound.
   162  		w := &watcher{}
   163  		return gcerr.New(w.ErrorCode(err), err, 1, "runtimevar")
   164  	})
   165  }
   166  
   167  func (h *harness) UpdateVariable(ctx context.Context, name string, val []byte) error {
   168  	var svc *secretsmanager.SecretsManager
   169  	var err error
   170  	if h.useV2 {
   171  		_, err = h.clientV2.PutSecretValue(ctx, &secretsmanagerv2.PutSecretValueInput{
   172  			ClientRequestToken: awsv2.String(generateClientRequestToken(name, val)),
   173  			SecretBinary:       val,
   174  			SecretId:           awsv2.String(name),
   175  		})
   176  	} else {
   177  		svc = secretsmanager.New(h.session)
   178  		_, err = svc.PutSecretValueWithContext(ctx, &secretsmanager.PutSecretValueInput{
   179  			ClientRequestToken: aws.String(generateClientRequestToken(name, val)),
   180  			SecretBinary:       val,
   181  			SecretId:           aws.String(name),
   182  		})
   183  	}
   184  	if err != nil {
   185  		return err
   186  	}
   187  	// Secret Manager is only eventually consistent, so we retry until we've
   188  	// verified that the mutation was applied. This is still not a guarantee
   189  	// but in practice seems to work well enough to make tests repeatable.
   190  	return waitForMutation(ctx, func() error {
   191  		var err error
   192  		var bb []byte
   193  		if h.useV2 {
   194  			var getResp *secretsmanagerv2.GetSecretValueOutput
   195  			getResp, err = h.clientV2.GetSecretValue(ctx, &secretsmanagerv2.GetSecretValueInput{SecretId: awsv2.String(name)})
   196  			if err == nil {
   197  				bb = getResp.SecretBinary
   198  			}
   199  		} else {
   200  			var getResp *secretsmanager.GetSecretValueOutput
   201  			getResp, err = svc.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{SecretId: aws.String(name)})
   202  			if err == nil {
   203  				bb = getResp.SecretBinary
   204  			}
   205  		}
   206  		if err != nil {
   207  			// Failure; we'll retry if it's a NotFound, but that's not
   208  			// really expected for an Update.
   209  			w := &watcher{}
   210  			return gcerr.New(w.ErrorCode(err), err, 1, "runtimevar")
   211  		}
   212  		if !bytes.Equal(bb, val) {
   213  			// Value hasn't been updated yet, return a NotFound to
   214  			// trigger retry.
   215  			return gcerr.Newf(gcerr.NotFound, nil, "updated value not seen yet")
   216  		}
   217  		// Update was seen.
   218  		return nil
   219  	})
   220  }
   221  
   222  func (h *harness) DeleteVariable(ctx context.Context, name string) error {
   223  	var svc *secretsmanager.SecretsManager
   224  	var err error
   225  	if h.useV2 {
   226  		_, err = h.clientV2.DeleteSecret(ctx, &secretsmanagerv2.DeleteSecretInput{
   227  			ForceDeleteWithoutRecovery: true,
   228  			SecretId:                   awsv2.String(name),
   229  		})
   230  	} else {
   231  		svc = secretsmanager.New(h.session)
   232  		_, err = svc.DeleteSecretWithContext(ctx, &secretsmanager.DeleteSecretInput{
   233  			ForceDeleteWithoutRecovery: aws.Bool(true),
   234  			SecretId:                   aws.String(name),
   235  		})
   236  	}
   237  	if err != nil {
   238  		return err
   239  	}
   240  	// Secret Manager is only eventually consistent, so we retry until we've
   241  	// verified that the mutation was applied. This is still not a guarantee
   242  	// but in practice seems to work well enough to make tests repeatable.
   243  	// Note that "success" after a delete is a NotFound error, so we massage
   244  	// the err returned from DescribeSecret to reflect that.
   245  	return waitForMutation(ctx, func() error {
   246  		var err error
   247  		if h.useV2 {
   248  			_, err = h.clientV2.DescribeSecret(ctx, &secretsmanagerv2.DescribeSecretInput{SecretId: awsv2.String(name)})
   249  		} else {
   250  			_, err = svc.DescribeSecretWithContext(ctx, &secretsmanager.DescribeSecretInput{SecretId: aws.String(name)})
   251  		}
   252  		if err == nil {
   253  			// Secret still exists, return a NotFound to trigger a retry.
   254  			return gcerr.Newf(gcerr.NotFound, nil, "delete not seen yet")
   255  		}
   256  		w := &watcher{useV2: h.useV2}
   257  		if w.ErrorCode(err) == gcerrors.NotFound {
   258  			// Delete was seen.
   259  			return nil
   260  		}
   261  		// Other errors are not retryable.
   262  		return gcerr.New(w.ErrorCode(err), err, 1, "runtimevar")
   263  	})
   264  }
   265  
   266  func (h *harness) Close() {
   267  	h.closer()
   268  }
   269  
   270  func (h *harness) Mutable() bool { return true }
   271  
   272  func TestConformance(t *testing.T) {
   273  	drivertest.RunConformanceTests(t, newHarness, []drivertest.AsTest{verifyAs{useV2: false}})
   274  }
   275  
   276  func TestConformanceV2(t *testing.T) {
   277  	drivertest.RunConformanceTests(t, newHarnessV2, []drivertest.AsTest{verifyAs{useV2: true}})
   278  }
   279  
   280  type verifyAs struct {
   281  	useV2 bool
   282  }
   283  
   284  func (verifyAs) Name() string {
   285  	return "verify As"
   286  }
   287  
   288  func (v verifyAs) SnapshotCheck(s *runtimevar.Snapshot) error {
   289  	if v.useV2 {
   290  		var getParam *secretsmanagerv2.GetSecretValueOutput
   291  		if !s.As(&getParam) {
   292  			return errors.New("Snapshot.As failed for GetSecretValueOutput")
   293  		}
   294  		var descParam *secretsmanagerv2.DescribeSecretOutput
   295  		if !s.As(&descParam) {
   296  			return errors.New("Snapshot.As failed for DescribeSecretOutput")
   297  		}
   298  		return nil
   299  	}
   300  	var getParam *secretsmanager.GetSecretValueOutput
   301  	if !s.As(&getParam) {
   302  		return errors.New("Snapshot.As failed for GetSecretValueOutput")
   303  	}
   304  	var descParam *secretsmanager.DescribeSecretOutput
   305  	if !s.As(&descParam) {
   306  		return errors.New("Snapshot.As failed for DescribeSecretOutput")
   307  	}
   308  	return nil
   309  }
   310  
   311  func (va verifyAs) ErrorCheck(v *runtimevar.Variable, err error) error {
   312  	if va.useV2 {
   313  		var e smithy.APIError
   314  		if !v.ErrorAs(err, &e) {
   315  			return errors.New("Keeper.ErrorAs failed")
   316  		}
   317  		return nil
   318  	}
   319  	var e awserr.Error
   320  	if !v.ErrorAs(err, &e) {
   321  		return errors.New("runtimevar.ErrorAs failed")
   322  	}
   323  	return nil
   324  }
   325  
   326  // Secrets Manager-specific tests.
   327  
   328  func TestEquivalentError(t *testing.T) {
   329  	tests := []struct {
   330  		Err1, Err2 error
   331  		Want       bool
   332  	}{
   333  		{Err1: errors.New("not aws"), Err2: errors.New("not aws"), Want: true},
   334  		{Err1: errors.New("not aws"), Err2: errors.New("not aws but different")},
   335  		{Err1: errors.New("not aws"), Err2: awserr.New("code1", "fail", nil)},
   336  		{Err1: awserr.New("code1", "fail", nil), Err2: awserr.New("code2", "fail", nil)},
   337  		{Err1: awserr.New("code1", "fail", nil), Err2: awserr.New("code1", "fail", nil), Want: true},
   338  	}
   339  
   340  	for _, test := range tests {
   341  		got := equivalentError(test.Err1, test.Err2)
   342  		if got != test.Want {
   343  			t.Errorf("%v vs %v: got %v want %v", test.Err1, test.Err2, got, test.Want)
   344  		}
   345  	}
   346  }
   347  
   348  func TestNoConnectionError(t *testing.T) {
   349  	prevAccessKey := os.Getenv("AWS_ACCESS_KEY")
   350  	prevSecretKey := os.Getenv("AWS_SECRET_KEY")
   351  	prevRegion := os.Getenv("AWS_REGION")
   352  	os.Setenv("AWS_ACCESS_KEY", "myaccesskey")
   353  	os.Setenv("AWS_SECRET_KEY", "mysecretkey")
   354  	os.Setenv("AWS_REGION", "us-east-1")
   355  	defer func() {
   356  		os.Setenv("AWS_ACCESS_KEY", prevAccessKey)
   357  		os.Setenv("AWS_SECRET_KEY", prevSecretKey)
   358  		os.Setenv("AWS_REGION", prevRegion)
   359  	}()
   360  	sess, err := session.NewSession()
   361  	if err != nil {
   362  		t.Fatal(err)
   363  	}
   364  
   365  	v, err := OpenVariable(sess, "variable-name", nil, nil)
   366  	if err != nil {
   367  		t.Fatal(err)
   368  	}
   369  	defer v.Close()
   370  	_, err = v.Watch(context.Background())
   371  	if err == nil {
   372  		t.Error("got nil want error")
   373  	}
   374  }
   375  
   376  func TestOpenVariable(t *testing.T) {
   377  	tests := []struct {
   378  		URL     string
   379  		WantErr bool
   380  	}{
   381  		// OK.
   382  		{"awssecretsmanager://myvar", false},
   383  		// OK, setting region.
   384  		{"awssecretsmanager://myvar?region=us-west-1", false},
   385  		// OK, setting decoder.
   386  		{"awssecretsmanager://myvar?decoder=string", false},
   387  		// Invalid decoder.
   388  		{"awssecretsmanager://myvar?decoder=notadecoder", true},
   389  		// Invalid parameter.
   390  		{"awssecretsmanager://myvar?param=value", true},
   391  		// OK, using SDK V2.
   392  		{"awssecretsmanager://myvar?decoder=string&awssdk=v2", false},
   393  	}
   394  
   395  	ctx := context.Background()
   396  	for _, test := range tests {
   397  		v, err := runtimevar.OpenVariable(ctx, test.URL)
   398  		if (err != nil) != test.WantErr {
   399  			t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr)
   400  		}
   401  		if err == nil {
   402  			v.Close()
   403  		}
   404  	}
   405  }