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 }