diff --git a/.github/workflows/test-aws.yaml b/.github/workflows/test-aws.yaml index 5d39f95..9e0f3e0 100644 --- a/.github/workflows/test-aws.yaml +++ b/.github/workflows/test-aws.yaml @@ -16,6 +16,10 @@ on: env: testdir : ./aws +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: strategy: @@ -25,30 +29,46 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 5 steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Install Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Docker + - name: Cache Go modules + id: cache-go + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Download Go modules working-directory: ${{ env.testdir }} - run: docker-compose up -d + shell: bash + if: ${{ steps.cache-go.outputs.cache-hit != 'true' }} + run: go mod download - - name: Go Module Download + - name: Setup Docker working-directory: ${{ env.testdir }} + env: + DOCKER_BUILDKIT: 1 run: | - go install gotest.tools/gotestsum@latest - go mod download + # Create the directory for the volume of dynamodb in advance, otherwise permission error will occur. + # https://stackoverflow.com/questions/45850688/unable-to-open-local-dynamodb-database-file-after-power-outage + mkdir -p ./docker/dynamodb/data + sudo chmod 777 ./docker/dynamodb/data + docker compose up -d - name: Test working-directory: ${{ env.testdir }} - timeout-minutes: 3 run: | # shellcheck disable=SC2046 - gotestsum --junitfile unit-tests.xml -- -v ./... -race -coverprofile="coverage.txt" -covermode=atomic -coverpkg=./... + go test -p 4 -parallel 4 -v ./... -race -coverprofile="coverage.txt" -covermode=atomic -coverpkg=./... - uses: codecov/codecov-action@v3 with: diff --git a/aws/.gitignore b/aws/.gitignore index b3b1b44..fe3a8fb 100644 --- a/aws/.gitignore +++ b/aws/.gitignore @@ -4,3 +4,5 @@ # Minio Data docker/minio/config docker/minio/data +# DynamoDB Data +docker/dynamodb/data diff --git a/aws/awsdynamo/awsdynamo.go b/aws/awsdynamo/awsdynamo.go new file mode 100644 index 0000000..4b6f624 --- /dev/null +++ b/aws/awsdynamo/awsdynamo.go @@ -0,0 +1,250 @@ +// nolint:typecheck +package awsdynamo + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + awstime "github.com/aws/smithy-go/time" + + "github.com/88labs/go-utils/aws/awsconfig" + "github.com/88labs/go-utils/aws/awsdynamo/dynamooptions" +) + +var ( + ErrNotFound = errors.New("record not found") +) + +// PutItem Put the item in DynamoDB Upsert if it does not exist +func PutItem(ctx context.Context, region awsconfig.Region, tableName string, item any, opts ...dynamooptions.OptionDynamo) error { + c := dynamooptions.GetDynamoConf(opts...) + client, err := GetClient(ctx, region, c.MaxAttempts, c.MaxBackoffDelay) + if err != nil { + return err + } + putItem, err := attributevalue.MarshalMap(item) + if err != nil { + return err + } + putItemInput := &dynamodb.PutItemInput{ + Item: putItem, + TableName: aws.String(tableName), + } + if _, err := client.PutItem(ctx, putItemInput); err != nil { + return err + } + return nil +} + +// UpdateItem Update the attributes of the item in DynamoDB Upsert if it does not exist +// expression: https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/expression/#example_Builder_WithUpdate +func UpdateItem( + ctx context.Context, + region awsconfig.Region, + tableName, keyFieldName, key string, + update expression.UpdateBuilder, + out any, + opts ...dynamooptions.OptionDynamo, +) error { + c := dynamooptions.GetDynamoConf(opts...) + client, err := GetClient(ctx, region, c.MaxAttempts, c.MaxBackoffDelay) + if err != nil { + return err + } + expr, err := expression.NewBuilder().WithUpdate(update).Build() + if err != nil { + return err + } + putItemInput := &dynamodb.UpdateItemInput{ + Key: map[string]types.AttributeValue{keyFieldName: &types.AttributeValueMemberS{Value: key}}, + TableName: aws.String(tableName), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + ReturnConsumedCapacity: types.ReturnConsumedCapacityNone, + ReturnItemCollectionMetrics: types.ReturnItemCollectionMetricsNone, + ReturnValues: types.ReturnValueAllNew, + UpdateExpression: expr.Update(), + } + updatedItem, err := client.UpdateItem(ctx, putItemInput) + if err != nil { + return err + } + if updatedItem.Attributes == nil { + return ErrNotFound + } + if out != nil { + if err := attributevalue.UnmarshalMap(updatedItem.Attributes, &out); err != nil { + return err + } + } + return nil +} + +// DeleteItem Delete DynamoDB item +// expression: https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/expression/#example_Builder_WithUpdate +// Mapping the retrieved item to `out`, must be a pointer to the `out`. +func DeleteItem(ctx context.Context, region awsconfig.Region, tableName, keyFieldName, key string, out any, opts ...dynamooptions.OptionDynamo) error { + c := dynamooptions.GetDynamoConf(opts...) + client, err := GetClient(ctx, region, c.MaxAttempts, c.MaxBackoffDelay) + if err != nil { + return err + } + deleteItemInput := &dynamodb.DeleteItemInput{ + Key: map[string]types.AttributeValue{keyFieldName: &types.AttributeValueMemberS{Value: key}}, + TableName: aws.String(tableName), + ReturnConsumedCapacity: types.ReturnConsumedCapacityTotal, + ReturnItemCollectionMetrics: types.ReturnItemCollectionMetricsSize, + ReturnValues: types.ReturnValueAllOld, + } + deletedItem, err := client.DeleteItem(ctx, deleteItemInput) + if err != nil { + return err + } + if deletedItem.Attributes == nil { + return ErrNotFound + } + if out != nil { + if err := attributevalue.UnmarshalMap(deletedItem.Attributes, &out); err != nil { + return err + } + } + return nil +} + +// GetItem Get the item in DynamoDB +// Mapping the retrieved item to `out`, must be a pointer to the `out`. +func GetItem(ctx context.Context, region awsconfig.Region, tableName, keyFieldName, key string, out any, opts ...dynamooptions.OptionDynamo) error { + c := dynamooptions.GetDynamoConf(opts...) + client, err := GetClient(ctx, region, c.MaxAttempts, c.MaxBackoffDelay) + if err != nil { + return err + } + getItemInput := &dynamodb.GetItemInput{ + Key: map[string]types.AttributeValue{ + keyFieldName: &types.AttributeValueMemberS{Value: key}, + }, + TableName: aws.String(tableName), + // https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html + ConsistentRead: aws.Bool(true), + } + getItem, err := client.GetItem(ctx, getItemInput) + if err != nil { + return err + } + if getItem.Item == nil { + return ErrNotFound + } + if err := attributevalue.UnmarshalMap(getItem.Item, &out); err != nil { + return err + } + return nil +} + +// BatchGetItem Retrieve Dynamodb items in a batch process +// Return the retrieved item as a slice of type `T`. +// Note that the order of retrieval is not the order in which the keys are specified. +func BatchGetItem[T any, Key ~string](ctx context.Context, region awsconfig.Region, tableName, keyFieldName string, keys []Key, _ T, opts ...dynamooptions.OptionDynamo) ([]T, error) { + // DynamoDB allows a maximum batch size of 100 items. + // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html + const MaxBatchSize = 100 + + c := dynamooptions.GetDynamoConf(opts...) + client, err := GetClient(ctx, region, c.MaxAttempts, c.MaxBackoffDelay) + if err != nil { + return nil, err + } + + reqKeys := make([]map[string]types.AttributeValue, len(keys)) + for i, key := range keys { + reqKeys[i] = map[string]types.AttributeValue{ + keyFieldName: &types.AttributeValueMemberS{Value: string(key)}, + } + } + + resultItems := make([]T, 0, len(keys)) + + start := 0 + end := start + MaxBatchSize + for start < len(reqKeys) { + getReqs := make([]map[string]types.AttributeValue, 0, MaxBatchSize) + if end > len(reqKeys) { + end = len(reqKeys) + } + for _, v := range reqKeys[start:end] { + getReqs = append(getReqs, v) + } + getItems, err := client.BatchGetItem(ctx, &dynamodb.BatchGetItemInput{ + RequestItems: map[string]types.KeysAndAttributes{ + tableName: {Keys: getReqs}, + }, + }) + if err != nil { + return nil, fmt.Errorf("received batch error %+#v for batch getting. %v\n", getItems, err) + } + + for _, v := range getItems.Responses[tableName] { + var ret T + if err := attributevalue.UnmarshalMap(v, &ret); err != nil { + return nil, fmt.Errorf("Couldn't unmarshal item %+#v for batch getting. %v\n", v, err) + } + resultItems = append(resultItems, ret) + } + start = end + end += MaxBatchSize + } + + return resultItems, nil +} + +// BatchWriteItem Write Dynamodb items in a batch process +func BatchWriteItem[T any](ctx context.Context, region awsconfig.Region, tableName string, items []T, opts ...dynamooptions.OptionDynamo) error { + // DynamoDB allows a maximum batch size of 25 items. + // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + const MaxBatchSize = 25 + + c := dynamooptions.GetDynamoConf(opts...) + client, err := GetClient(ctx, region, c.MaxAttempts, c.MaxBackoffDelay) + if err != nil { + return err + } + + start := 0 + end := start + MaxBatchSize + for start < len(items) { + writeReqs := make([]types.WriteRequest, 0, MaxBatchSize) + if end > len(items) { + end = len(items) + } + for _, v := range items[start:end] { + item, err := attributevalue.MarshalMap(v) + if err != nil { + return fmt.Errorf("Couldn't marshal item %+#v for batch writing. %v\n", v, err) + } else { + writeReqs = append( + writeReqs, + types.WriteRequest{PutRequest: &types.PutRequest{Item: item}}, + ) + } + } + if _, err := client.BatchWriteItem(ctx, &dynamodb.BatchWriteItemInput{ + RequestItems: map[string][]types.WriteRequest{tableName: writeReqs}, + }, + ); err != nil { + return fmt.Errorf("received batch error %+#v for batch writing. %v\n", writeReqs, err) + } + if err := awstime.SleepWithContext(ctx, 10*time.Millisecond); err != nil { + return err + } + start = end + end += MaxBatchSize + } + + return err +} diff --git a/aws/awsdynamo/awsdynamo_test.go b/aws/awsdynamo/awsdynamo_test.go new file mode 100644 index 0000000..c827485 --- /dev/null +++ b/aws/awsdynamo/awsdynamo_test.go @@ -0,0 +1,302 @@ +package awsdynamo_test + +import ( + "context" + "sort" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/bxcodec/faker/v3" + "github.com/stretchr/testify/assert" + + "github.com/88labs/go-utils/aws/awsconfig" + "github.com/88labs/go-utils/aws/awsdynamo" + "github.com/88labs/go-utils/aws/ctxawslocal" + "github.com/88labs/go-utils/ulid" +) + +const ( + TestTable = "test" + TestDynamoEndpoint = "http://127.0.0.1:28002" // use local dynamo + TestRegion = awsconfig.RegionTokyo + TestAccessKey = "DUMMYACCESSKEYEXAMPLE" + TestSecretAccessKey = "DUMMYSECRETKEYEXAMPLE" +) + +type Test struct { + ID string `json:"id" dynamodbav:"id"` + Name string `json:"name" dynamodbav:"name"` + CreatedAt attributevalue.UnixTime `json:"created_at" dynamodbav:"created_at"` +} + +func TestPutItem(t *testing.T) { + t.Parallel() + ctx := ctxawslocal.WithContext( + context.Background(), + ctxawslocal.WithDynamoEndpoint(TestDynamoEndpoint), + ctxawslocal.WithAccessKey(TestAccessKey), + ctxawslocal.WithSecretAccessKey(TestSecretAccessKey), + ) + + t.Run("New", func(t *testing.T) { + t.Parallel() + item := Test{ + ID: ulid.MustNew().String(), + Name: faker.Name(), + CreatedAt: attributevalue.UnixTime(time.Now()), + } + err := awsdynamo.PutItem(ctx, TestRegion, TestTable, item) + assert.NoError(t, err) + }) + t.Run("New/Update", func(t *testing.T) { + t.Parallel() + item := Test{ + ID: ulid.MustNew().String(), + Name: faker.Name(), + CreatedAt: attributevalue.UnixTime(time.Now()), + } + err := awsdynamo.PutItem(ctx, TestRegion, TestTable, item) + assert.NoError(t, err) + item.Name = faker.Name() + err = awsdynamo.PutItem(ctx, TestRegion, TestTable, item) + assert.NoError(t, err) + }) +} + +func TestGetItem(t *testing.T) { + t.Parallel() + ctx := ctxawslocal.WithContext( + context.Background(), + ctxawslocal.WithDynamoEndpoint(TestDynamoEndpoint), + ctxawslocal.WithAccessKey(TestAccessKey), + ctxawslocal.WithSecretAccessKey(TestSecretAccessKey), + ) + testItem := Test{ + ID: ulid.MustNew().String(), + Name: faker.Name(), + CreatedAt: attributevalue.UnixTime(time.Now()), + } + err := awsdynamo.PutItem(ctx, TestRegion, TestTable, testItem) + assert.NoError(t, err) + + t.Run("Get", func(t *testing.T) { + t.Parallel() + var out Test + err := awsdynamo.GetItem(ctx, TestRegion, TestTable, "id", testItem.ID, &out) + assert.NoError(t, err) + assert.Equal(t, testItem.ID, out.ID) + assert.Equal(t, testItem.Name, out.Name) + expectedCreatedAt, err := testItem.CreatedAt.MarshalDynamoDBAttributeValue() + assert.NoError(t, err) + actualCreatedAt, err := out.CreatedAt.MarshalDynamoDBAttributeValue() + assert.NoError(t, err) + assert.Equal(t, expectedCreatedAt, actualCreatedAt) + }) + + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + var out Test + err := awsdynamo.GetItem(ctx, TestRegion, TestTable, "id", "NOT_FOUND", &out) + assert.Error(t, err) + assert.ErrorIs(t, awsdynamo.ErrNotFound, err) + }) +} + +func TestDeleteItem(t *testing.T) { + t.Parallel() + ctx := ctxawslocal.WithContext( + context.Background(), + ctxawslocal.WithDynamoEndpoint(TestDynamoEndpoint), + ctxawslocal.WithAccessKey(TestAccessKey), + ctxawslocal.WithSecretAccessKey(TestSecretAccessKey), + ) + testItem := Test{ + ID: ulid.MustNew().String(), + Name: faker.Name(), + CreatedAt: attributevalue.UnixTime(time.Now()), + } + + t.Run("Delete", func(t *testing.T) { + t.Parallel() + err := awsdynamo.PutItem(ctx, TestRegion, TestTable, testItem) + assert.NoError(t, err) + + var out Test + err = awsdynamo.DeleteItem(ctx, TestRegion, TestTable, "id", testItem.ID, &out) + assert.NoError(t, err) + assert.Equal(t, testItem.ID, out.ID) + assert.Equal(t, testItem.Name, out.Name) + expectedCreatedAt, err := testItem.CreatedAt.MarshalDynamoDBAttributeValue() + assert.NoError(t, err) + actualCreatedAt, err := out.CreatedAt.MarshalDynamoDBAttributeValue() + assert.NoError(t, err) + assert.Equal(t, expectedCreatedAt, actualCreatedAt) + }) + + t.Run("Delete out param nil", func(t *testing.T) { + t.Parallel() + err := awsdynamo.PutItem(ctx, TestRegion, TestTable, testItem) + assert.NoError(t, err) + + err = awsdynamo.DeleteItem(ctx, TestRegion, TestTable, "id", testItem.ID, nil) + assert.NoError(t, err) + }) + + t.Run("Delete NotFound", func(t *testing.T) { + t.Parallel() + err := awsdynamo.DeleteItem(ctx, TestRegion, TestTable, "id", "NOT_FOUND", nil) + assert.Error(t, err) + assert.ErrorIs(t, awsdynamo.ErrNotFound, err) + }) +} + +func TestUpdateItem(t *testing.T) { + t.Parallel() + ctx := ctxawslocal.WithContext( + context.Background(), + ctxawslocal.WithDynamoEndpoint(TestDynamoEndpoint), + ctxawslocal.WithAccessKey(TestAccessKey), + ctxawslocal.WithSecretAccessKey(TestSecretAccessKey), + ) + + t.Run("Update", func(t *testing.T) { + t.Parallel() + testItem := Test{ + ID: ulid.MustNew().String(), + Name: faker.Name(), + CreatedAt: attributevalue.UnixTime(time.Now()), + } + err := awsdynamo.PutItem(ctx, TestRegion, TestTable, testItem) + assert.NoError(t, err) + + var out Test + updateName := faker.Name() + update := expression.Set( + expression.Name("name"), + expression.Value(updateName), + ) + err = awsdynamo.UpdateItem(ctx, TestRegion, TestTable, "id", testItem.ID, update, &out) + assert.NoError(t, err) + assert.Equal(t, testItem.ID, out.ID) + assert.Equal(t, updateName, out.Name) + expectedCreatedAt, err := testItem.CreatedAt.MarshalDynamoDBAttributeValue() + assert.NoError(t, err) + actualCreatedAt, err := out.CreatedAt.MarshalDynamoDBAttributeValue() + assert.NoError(t, err) + assert.Equal(t, expectedCreatedAt, actualCreatedAt) + }) + + t.Run("Update out param nil", func(t *testing.T) { + t.Parallel() + testItem := Test{ + ID: ulid.MustNew().String(), + Name: faker.Name(), + CreatedAt: attributevalue.UnixTime(time.Now()), + } + err := awsdynamo.PutItem(ctx, TestRegion, TestTable, testItem) + assert.NoError(t, err) + + updateName := faker.Name() + update := expression.Set( + expression.Name("name"), + expression.Value(updateName), + ) + err = awsdynamo.UpdateItem(ctx, TestRegion, TestTable, "id", testItem.ID, update, nil) + assert.NoError(t, err) + }) +} + +func TestBatchGetItem(t *testing.T) { + t.Parallel() + ctx := ctxawslocal.WithContext( + context.Background(), + ctxawslocal.WithDynamoEndpoint(TestDynamoEndpoint), + ctxawslocal.WithAccessKey(TestAccessKey), + ctxawslocal.WithSecretAccessKey(TestSecretAccessKey), + ) + + makeItems := func(size int) ([]string, []Test) { + ids := make([]string, 0, size) + testItems := make([]Test, 0, size) + for i := 0; i < size; i++ { + item := Test{ + ID: ulid.MustNew().String(), + Name: faker.Name(), + CreatedAt: attributevalue.UnixTime(time.Now()), + } + err := awsdynamo.PutItem(ctx, TestRegion, TestTable, item) + assert.NoError(t, err) + testItems = append(testItems, item) + ids = append(ids, item.ID) + } + return ids, testItems + } + + t.Run("Get 101 items", func(t *testing.T) { + t.Parallel() + ids, testItems := makeItems(101) + out, err := awsdynamo.BatchGetItem(ctx, TestRegion, TestTable, "id", ids, Test{}) + assert.NoError(t, err) + sort.Slice(testItems, func(i, j int) bool { + return testItems[i].ID < testItems[j].ID + }) + sort.Slice(out, func(i, j int) bool { + return out[i].ID < out[j].ID + }) + for i, testItem := range testItems { + assert.Equal(t, testItem.ID, out[i].ID) + assert.Equal(t, testItem.Name, out[i].Name) + + expectedCreatedAt, err := testItem.CreatedAt.MarshalDynamoDBAttributeValue() + assert.NoError(t, err) + actualCreatedAt, err := out[i].CreatedAt.MarshalDynamoDBAttributeValue() + assert.NoError(t, err) + assert.Equal(t, expectedCreatedAt, actualCreatedAt) + } + }) + + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + out, err := awsdynamo.BatchGetItem(ctx, TestRegion, TestTable, "id", []string{"NOT_FOUND"}, Test{}) + assert.NoError(t, err) + assert.Len(t, out, 0) + }) +} + +func TestBatchWriteItem(t *testing.T) { + t.Parallel() + ctx := ctxawslocal.WithContext( + context.Background(), + ctxawslocal.WithDynamoEndpoint(TestDynamoEndpoint), + ctxawslocal.WithAccessKey(TestAccessKey), + ctxawslocal.WithSecretAccessKey(TestSecretAccessKey), + ) + + makeItems := func(size int) ([]string, []Test) { + ids := make([]string, 0, size) + testItems := make([]Test, 0, size) + for i := 0; i < size; i++ { + item := Test{ + ID: ulid.MustNew().String(), + Name: faker.Name(), + CreatedAt: attributevalue.UnixTime(time.Now()), + } + testItems = append(testItems, item) + ids = append(ids, item.ID) + } + return ids, testItems + } + + t.Run("Write 26 items", func(t *testing.T) { + t.Parallel() + ids, testItems := makeItems(26) + err := awsdynamo.BatchWriteItem(ctx, TestRegion, TestTable, testItems) + assert.NoError(t, err) + + out, err := awsdynamo.BatchGetItem(ctx, TestRegion, TestTable, "id", ids, Test{}) + assert.NoError(t, err) + assert.Equal(t, len(testItems), len(out)) + }) +} diff --git a/aws/awsdynamo/client.go b/aws/awsdynamo/client.go new file mode 100644 index 0000000..9239106 --- /dev/null +++ b/aws/awsdynamo/client.go @@ -0,0 +1,107 @@ +package awsdynamo + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/88labs/go-utils/aws/ctxawslocal" + + "github.com/88labs/go-utils/aws/awsconfig" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/retry" + awsConfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +var dynamoDBClient *dynamodb.Client +var once sync.Once + +func GetClient(ctx context.Context, region awsconfig.Region, limitAttempts int, limitBackOffDelay time.Duration) (*dynamodb.Client, error) { + if localProfile, ok := getLocalEndpoint(ctx); ok { + return getClientLocal(ctx, *localProfile) + } + var responseError error + once.Do(func() { + // S3 Client + awsCfg, err := awsConfig.LoadDefaultConfig(ctx, awsConfig.WithRegion(region.String()), + awsConfig.WithRetryer(func() aws.Retryer { + r := retry.AddWithMaxAttempts(retry.NewStandard(), limitAttempts) + r = retry.AddWithMaxBackoffDelay(r, limitBackOffDelay) + r = retry.AddWithErrorCodes(r, + string(types.BatchStatementErrorCodeEnumItemCollectionSizeLimitExceeded), + string(types.BatchStatementErrorCodeEnumRequestLimitExceeded), + string(types.BatchStatementErrorCodeEnumProvisionedThroughputExceeded), + string(types.BatchStatementErrorCodeEnumInternalServerError), + string(types.BatchStatementErrorCodeEnumThrottlingError), + ) + return r + }), + ) + if err != nil { + responseError = fmt.Errorf("unable to load SDK config, %w", err) + } else { + responseError = nil + } + dynamoDBClient = dynamodb.NewFromConfig(awsCfg) + }) + return dynamoDBClient, responseError +} + +func getClientLocal(ctx context.Context, localProfile LocalProfile) (*dynamodb.Client, error) { + var responseError error + once.Do(func() { + // https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/endpoints/ + customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + if service == dynamodb.ServiceID { + return aws.Endpoint{ + PartitionID: "aws", + URL: localProfile.Endpoint, + SigningRegion: region, + HostnameImmutable: true, + }, nil + } + // returning EndpointNotFoundError will allow the service to fallback to it's default resolution + return aws.Endpoint{}, &aws.EndpointNotFoundError{} + }) + awsCfg, err := awsConfig.LoadDefaultConfig(ctx, + awsConfig.WithEndpointResolverWithOptions(customResolver), + awsConfig.WithCredentialsProvider(credentials.StaticCredentialsProvider{ + Value: aws.Credentials{ + AccessKeyID: localProfile.AccessKey, + SecretAccessKey: localProfile.SecretAccessKey, + }, + }), + ) + if err != nil { + responseError = fmt.Errorf("unable to load SDK config, %w", err) + return + } else { + responseError = nil + } + dynamoDBClient = dynamodb.NewFromConfig(awsCfg) + }) + return dynamoDBClient, responseError +} + +type LocalProfile struct { + Endpoint string + AccessKey string + SecretAccessKey string + SessionToken string +} + +func getLocalEndpoint(ctx context.Context) (*LocalProfile, bool) { + if c, ok := ctxawslocal.GetConf(ctx); ok { + p := new(LocalProfile) + p.Endpoint = c.DynamoEndpoint + p.AccessKey = c.AccessKey + p.SecretAccessKey = c.SecretAccessKey + p.SessionToken = c.SessionToken + return p, true + } + return nil, false +} diff --git a/aws/awsdynamo/dynamooptions/options.go b/aws/awsdynamo/dynamooptions/options.go new file mode 100644 index 0000000..c44c967 --- /dev/null +++ b/aws/awsdynamo/dynamooptions/options.go @@ -0,0 +1,46 @@ +package dynamooptions + +import "time" + +type OptionDynamo interface { + Apply(*confDynamo) +} + +type confDynamo struct { + MaxAttempts int + MaxBackoffDelay time.Duration +} + +type OptionMaxAttempts int + +func (o OptionMaxAttempts) Apply(c *confDynamo) { + c.MaxAttempts = int(o) +} + +func WithMaxAttempts(maxAttempts int) OptionMaxAttempts { + return OptionMaxAttempts(maxAttempts) +} + +type OptionMaxBackoffDelay time.Duration + +func (o OptionMaxBackoffDelay) Apply(c *confDynamo) { + c.MaxBackoffDelay = time.Duration(o) +} + +func WithMaxBackoffDelay(maxBackoffDelay time.Duration) OptionMaxBackoffDelay { + return OptionMaxBackoffDelay(maxBackoffDelay) +} + +// nolint:revive +func GetDynamoConf(opts ...OptionDynamo) confDynamo { + // default + // https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/retries-timeouts/#standard-retryer + c := confDynamo{ + MaxAttempts: 3, + MaxBackoffDelay: 20 * time.Second, + } + for _, opt := range opts { + opt.Apply(&c) + } + return c +} diff --git a/aws/awss3/awss3_test.go b/aws/awss3/awss3_test.go index cd86a51..79c6865 100644 --- a/aws/awss3/awss3_test.go +++ b/aws/awss3/awss3_test.go @@ -44,6 +44,7 @@ const ( ) func TestHeadObject(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -69,18 +70,21 @@ func TestHeadObject(t *testing.T) { } t.Run("exists object", func(t *testing.T) { + t.Parallel() key := createFixture(100) res, err := awss3.HeadObject(ctx, TestRegion, TestBucket, key) assert.NoError(t, err) assert.Equal(t, int64(100), res.ContentLength) }) t.Run("not exists object", func(t *testing.T) { + t.Parallel() _, err := awss3.HeadObject(ctx, TestRegion, TestBucket, "NOT_FOUND") if assert.Error(t, err) { assert.ErrorIs(t, awss3.ErrNotFound, err) } }) t.Run("exists object use Waiter", func(t *testing.T) { + t.Parallel() key := createFixture(100) res, err := awss3.HeadObject(ctx, TestRegion, TestBucket, key, s3head.WithTimeout(5*time.Second), @@ -89,6 +93,7 @@ func TestHeadObject(t *testing.T) { assert.Equal(t, int64(100), res.ContentLength) }) t.Run("not exists object use Waiter", func(t *testing.T) { + t.Parallel() _, err := awss3.HeadObject(ctx, TestRegion, TestBucket, "NOT_FOUND", s3head.WithTimeout(5*time.Second), ) @@ -99,6 +104,7 @@ func TestHeadObject(t *testing.T) { } func TestListObjects(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -124,6 +130,7 @@ func TestListObjects(t *testing.T) { } t.Run("ListObjects", func(t *testing.T) { + t.Parallel() key1 := createFixture("hoge") key2 := createFixture("hoge") key3 := createFixture("hoge") @@ -141,6 +148,7 @@ func TestListObjects(t *testing.T) { }) t.Run("ListObjects OptionsPrefix", func(t *testing.T) { + t.Parallel() key1 := createFixture("hoge") key2 := createFixture("hoge") key3 := createFixture("fuga") @@ -158,6 +166,7 @@ func TestListObjects(t *testing.T) { }) t.Run("ListObjects 1001 objects", func(t *testing.T) { + t.Parallel() keys := make([]awss3.Key, 1001) for i := 0; i < 1001; i++ { keys[i] = createFixture("piyo") @@ -173,6 +182,7 @@ func TestListObjects(t *testing.T) { } func TestGetObject(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -198,6 +208,7 @@ func TestGetObject(t *testing.T) { } t.Run("GetObjectWriter", func(t *testing.T) { + t.Parallel() key := createFixture() var buf bytes.Buffer err := awss3.GetObjectWriter(ctx, TestRegion, TestBucket, key, &buf) @@ -206,6 +217,7 @@ func TestGetObject(t *testing.T) { }) t.Run("GetObjectWriter NotFound", func(t *testing.T) { + t.Parallel() var buf bytes.Buffer err := awss3.GetObjectWriter(ctx, TestRegion, TestBucket, "NOT_FOUND", &buf) if assert.Error(t, err) { @@ -215,6 +227,7 @@ func TestGetObject(t *testing.T) { } func TestDeleteObject(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -240,6 +253,7 @@ func TestDeleteObject(t *testing.T) { } t.Run("DeleteObject", func(t *testing.T) { + t.Parallel() key := createFixture() _, err := awss3.DeleteObject(ctx, TestRegion, TestBucket, key) assert.NoError(t, err) @@ -248,6 +262,7 @@ func TestDeleteObject(t *testing.T) { }) t.Run("DeleteObject NotFound", func(t *testing.T) { + t.Parallel() _, err := awss3.DeleteObject(ctx, TestRegion, TestBucket, "NOT_FOUND") if assert.Error(t, err) { assert.ErrorIs(t, awss3.ErrNotFound, err) @@ -256,6 +271,7 @@ func TestDeleteObject(t *testing.T) { } func TestDownloadFiles(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -283,6 +299,7 @@ func TestDownloadFiles(t *testing.T) { } t.Run("no option", func(t *testing.T) { + t.Parallel() filePaths, err := awss3.DownloadFiles(ctx, TestRegion, TestBucket, keys, t.TempDir()) assert.NoError(t, err) if assert.Len(t, filePaths, len(keys)) { @@ -295,6 +312,7 @@ func TestDownloadFiles(t *testing.T) { } }) t.Run("FileNameReplacer:not duplicate", func(t *testing.T) { + t.Parallel() filePaths, err := awss3.DownloadFiles(ctx, TestRegion, TestBucket, keys, t.TempDir(), s3download.WithFileNameReplacerFunc(func(S3Key, baseFileName string) string { return "add_" + baseFileName @@ -311,6 +329,7 @@ func TestDownloadFiles(t *testing.T) { } }) t.Run("FileNameReplacer:duplicate", func(t *testing.T) { + t.Parallel() filePaths, err := awss3.DownloadFiles(ctx, TestRegion, TestBucket, keys, t.TempDir(), s3download.WithFileNameReplacerFunc(func(S3Key, baseFileName string) string { return "fixname.txt" @@ -333,6 +352,7 @@ func TestDownloadFiles(t *testing.T) { } func TestPutObject(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -341,6 +361,7 @@ func TestPutObject(t *testing.T) { ) t.Run("PutObject", func(t *testing.T) { + t.Parallel() key := fmt.Sprintf("awstest/%s.txt", ulid.MustNew()) body := faker.Sentence() _, err := awss3.PutObject(ctx, TestRegion, TestBucket, awss3.Key(key), strings.NewReader(body)) @@ -355,6 +376,7 @@ func TestPutObject(t *testing.T) { } func TestUploadManager(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -363,6 +385,7 @@ func TestUploadManager(t *testing.T) { ) t.Run("UploadManager", func(t *testing.T) { + t.Parallel() key := fmt.Sprintf("awstest/%s.txt", ulid.MustNew()) body := faker.Sentence() _, err := awss3.UploadManager(ctx, TestRegion, TestBucket, awss3.Key(key), strings.NewReader(body)) @@ -378,6 +401,7 @@ func TestUploadManager(t *testing.T) { } func TestPresign(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -424,18 +448,21 @@ func TestPresign(t *testing.T) { } t.Run("Presign", func(t *testing.T) { + t.Parallel() key := uploadText() presign, err := awss3.Presign(ctx, TestRegion, TestBucket, key) assert.NoError(t, err) assert.NotEmpty(t, presign) }) t.Run("Presign PDF", func(t *testing.T) { + t.Parallel() key := uploadPDF() presign, err := awss3.Presign(ctx, TestRegion, TestBucket, key) assert.NoError(t, err) assert.NotEmpty(t, presign) }) t.Run("Presign NotFound", func(t *testing.T) { + t.Parallel() _, err := awss3.Presign(ctx, TestRegion, TestBucket, "NOT_FOUND") if assert.Error(t, err) { assert.ErrorIs(t, awss3.ErrNotFound, err) @@ -444,14 +471,17 @@ func TestPresign(t *testing.T) { } func TestResponseContentDisposition(t *testing.T) { + t.Parallel() const fileName = ",あいうえお 牡蠣喰家来 サシスセソ@+$_-^|+{}" t.Run("success", func(t *testing.T) { + t.Parallel() actual := awss3.ResponseContentDisposition(s3presigned.ContentDispositionTypeAttachment, fileName) assert.NotEmpty(t, actual) }) } func TestCopy(t *testing.T) { + t.Parallel() createFixture := func(ctx context.Context) awss3.Key { s3Client, err := awss3.GetClient(ctx, TestRegion) if err != nil { @@ -481,6 +511,7 @@ func TestCopy(t *testing.T) { } t.Run("Copy:Same Bucket and Other Key", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -492,6 +523,7 @@ func TestCopy(t *testing.T) { assert.NoError(t, awss3.Copy(ctx, TestRegion, TestBucket, key, key2)) }) t.Run("Copy:Same Item", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -503,6 +535,7 @@ func TestCopy(t *testing.T) { }) t.Run("Copy:NotFound", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -517,6 +550,7 @@ func TestCopy(t *testing.T) { } func TestSelectCSVAll(t *testing.T) { + t.Parallel() type TestCSV string const ( TestCSVHeader TestCSV = `id,name,detail @@ -580,6 +614,7 @@ func TestSelectCSVAll(t *testing.T) { } t.Run("CSV With Header", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -599,6 +634,7 @@ func TestSelectCSVAll(t *testing.T) { assert.Equal(t, WantCSV, records) }) t.Run("CSV With Header", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -618,6 +654,7 @@ func TestSelectCSVAll(t *testing.T) { assert.Equal(t, WantCSV, records) }) t.Run("CSV With LineFeed File:LF, Field:LF", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -639,6 +676,7 @@ func TestSelectCSVAll(t *testing.T) { assert.Equal(t, WantCSVWithLineFeedLF, records) }) t.Run("CSV With LineFeed File:CRLF, Field:LF", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -660,6 +698,7 @@ func TestSelectCSVAll(t *testing.T) { assert.Equal(t, WantCSVWithLineFeedLF, records) }) t.Run("CSV With LineFeed File:LF, Field:CRLF", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -681,6 +720,7 @@ func TestSelectCSVAll(t *testing.T) { assert.Equal(t, WantCSVWithLineFeedLF, records) }) t.Run("CSV With LineFeed File:CRLF, Field:CRLF", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -703,6 +743,7 @@ func TestSelectCSVAll(t *testing.T) { assert.Equal(t, WantCSVWithLineFeedLF, records) }) t.Run("CSV No Header", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -724,6 +765,7 @@ func TestSelectCSVAll(t *testing.T) { assert.Equal(t, WantNoHeaderCSV, records) }) t.Run("CSV With UTF-8 BOM", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -743,6 +785,7 @@ func TestSelectCSVAll(t *testing.T) { assert.Equal(t, WantCSV, records) }) t.Run("CSV 300000 records", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -767,6 +810,7 @@ func TestSelectCSVAll(t *testing.T) { } func TestSelectCSVHeaders(t *testing.T) { + t.Parallel() type TestCSV string const ( TestCSVHeader TestCSV = `id,name,detail @@ -806,6 +850,7 @@ func TestSelectCSVHeaders(t *testing.T) { } t.Run("CSV With Header", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio @@ -821,6 +866,7 @@ func TestSelectCSVHeaders(t *testing.T) { assert.Equal(t, WantCSVHeaders, got) }) t.Run("Empty CSV", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext( context.Background(), ctxawslocal.WithS3Endpoint("http://127.0.0.1:29000"), // use Minio diff --git a/aws/ctxawslocal/ctxawslocal_test.go b/aws/ctxawslocal/ctxawslocal_test.go index d8a2e71..b999d20 100644 --- a/aws/ctxawslocal/ctxawslocal_test.go +++ b/aws/ctxawslocal/ctxawslocal_test.go @@ -10,23 +10,29 @@ import ( ) func TestIsLocal(t *testing.T) { + t.Parallel() t.Run("context.Value not exists", func(t *testing.T) { + t.Parallel() ctx := context.Background() assert.False(t, ctxawslocal.IsLocal(ctx)) }) t.Run("context.Value exists", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext(context.Background()) assert.True(t, ctxawslocal.IsLocal(ctx)) }) } func TestGetConf(t *testing.T) { + t.Parallel() t.Run("context.Value not exists", func(t *testing.T) { + t.Parallel() ctx := context.Background() _, ok := ctxawslocal.GetConf(ctx) assert.False(t, ok) }) t.Run("unspecified config", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext(context.Background()) c, ok := ctxawslocal.GetConf(ctx) assert.True(t, ok) @@ -37,9 +43,11 @@ func TestGetConf(t *testing.T) { S3Endpoint: "http://127.0.0.1:4566", // localstack default endpoint SQSEndpoint: "http://127.0.0.1:4566", // localstack default endpoint CognitoEndpoint: "", + DynamoEndpoint: "http://127.0.0.1:4566", // localstack default endpoint }, c) }) t.Run("set config", func(t *testing.T) { + t.Parallel() ctx := ctxawslocal.WithContext(context.Background(), ctxawslocal.WithAccessKey("DUMMYACCESSKEYEXAMPLE"), ctxawslocal.WithSecretAccessKey("DUMMYACCESSKEYEXAMPLE"), @@ -47,6 +55,7 @@ func TestGetConf(t *testing.T) { ctxawslocal.WithS3Endpoint("http://localhost:14572"), ctxawslocal.WithSQSEndpoint("http://localhost:24572"), ctxawslocal.WithCognitoEndpoint("http://localhost:34572"), + ctxawslocal.WithDynamoEndpoint("http://localhost:44572"), ) c, ok := ctxawslocal.GetConf(ctx) assert.True(t, ok) @@ -57,6 +66,7 @@ func TestGetConf(t *testing.T) { S3Endpoint: "http://localhost:14572", SQSEndpoint: "http://localhost:24572", CognitoEndpoint: "http://localhost:34572", + DynamoEndpoint: "http://localhost:44572", }, c) }) } diff --git a/aws/ctxawslocal/options.go b/aws/ctxawslocal/options.go index 2db5058..45f6257 100644 --- a/aws/ctxawslocal/options.go +++ b/aws/ctxawslocal/options.go @@ -11,6 +11,7 @@ type ConfMock struct { S3Endpoint string SQSEndpoint string CognitoEndpoint string + DynamoEndpoint string } // nolint:revive @@ -22,6 +23,7 @@ func getConf(opts ...OptionMock) ConfMock { SessionToken: "", S3Endpoint: "http://127.0.0.1:4566", // localstack default endpoint SQSEndpoint: "http://127.0.0.1:4566", // localstack default endpoint + DynamoEndpoint: "http://127.0.0.1:4566", // localstack default endpoint } for _, opt := range opts { opt.Apply(&c) @@ -88,3 +90,13 @@ func (o OptionCognitoEndpoint) Apply(c *ConfMock) { func WithCognitoEndpoint(endpoint string) OptionCognitoEndpoint { return OptionCognitoEndpoint(endpoint) } + +type OptionDynamoEndpoint string + +func (o OptionDynamoEndpoint) Apply(c *ConfMock) { + c.DynamoEndpoint = string(o) +} + +func WithDynamoEndpoint(endpoint string) OptionDynamoEndpoint { + return OptionDynamoEndpoint(endpoint) +} diff --git a/aws/docker-compose.yaml b/aws/docker-compose.yaml index 52a6db1..fb5ccc1 100644 --- a/aws/docker-compose.yaml +++ b/aws/docker-compose.yaml @@ -38,3 +38,29 @@ services: " volumes: - ./docker/minio/policies:/policies + dynamodb: + command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data -port 28002" + image: "amazon/dynamodb-local:latest" + container_name: go_utils_dynamodb_ci + ports: + - "28002:28002" + volumes: + - "./docker/dynamodb/data:/home/dynamodblocal/data" + working_dir: /home/dynamodblocal + dynamodbcreatetable: + image: "amazon/aws-cli" + container_name: go_utils_dynamodb_ci_init + environment: + - AWS_ACCESS_KEY_ID=DUMMYACCESSKEYEXAMPLE + - AWS_SECRET_ACCESS_KEY=DUMMYSECRETKEYEXAMPLE + - AWS_DEFAULT_REGION=ap-northeast-1 + depends_on: + - dynamodb + entrypoint: > + /bin/sh -c ' + until (aws dynamodb list-tables --endpoint-url http://dynamodb:28002 --output text) do echo '...waiting...' && sleep 1; done; + aws dynamodb create-table --cli-input-json file:///test_tables/table_test.json --endpoint-url http://dynamodb:28002; + until (aws dynamodb describe-table --table-name test --endpoint-url http://dynamodb:28002 --output text) do echo '...waiting...' && sleep 1; done; + ' + volumes: + - ./docker/dynamodb:/test_tables diff --git a/aws/docker/dynamodb/table_test.json b/aws/docker/dynamodb/table_test.json new file mode 100644 index 0000000..9127b5f --- /dev/null +++ b/aws/docker/dynamodb/table_test.json @@ -0,0 +1,19 @@ +{ + "TableName": "test", + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 10, + "WriteCapacityUnits": 10 + } +} diff --git a/aws/go.mod b/aws/go.mod index 0177c62..daf5116 100644 --- a/aws/go.mod +++ b/aws/go.mod @@ -8,8 +8,11 @@ require ( github.com/aws/aws-sdk-go-v2 v1.19.0 github.com/aws/aws-sdk-go-v2/config v1.18.28 github.com/aws/aws-sdk-go-v2/credentials v1.13.27 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.31 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.58 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.72 github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.15.13 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.20.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.37.0 github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2 github.com/aws/smithy-go v1.13.5 @@ -27,8 +30,10 @@ require ( github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.27 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.29 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect diff --git a/aws/go.sum b/aws/go.sum index 531c75f..f9682e3 100644 --- a/aws/go.sum +++ b/aws/go.sum @@ -11,6 +11,10 @@ github.com/aws/aws-sdk-go-v2/config v1.18.28 h1:TINEaKyh1Td64tqFvn09iYpKiWjmHYrG github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A= github.com/aws/aws-sdk-go-v2/credentials v1.13.27 h1:dz0yr/yR1jweAnsCx+BmjerUILVPQ6FS5AwF/OyG1kA= github.com/aws/aws-sdk-go-v2/credentials v1.13.27/go.mod h1:syOqAek45ZXZp29HlnRS/BNgMIW6uiRmeuQsz4Qh2UE= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.31 h1:E8dD+ybAgXQDoXzFdosX8nKBG78yZLZLY83JGtuvyx8= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.31/go.mod h1:KBfz/i2tcY+0H3IOOOMVk9Olw8GRmgFPlKamh4Ro0ts= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.58 h1:GJHzGBrUmQevWqa0CTxIRQd4qu8DkNcDUKRzcDy1Qzg= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.58/go.mod h1:xkwd24omkXMpLJZeceDFcEL/eL0w2rsnz6Wp08yl9PI= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 h1:kP3Me6Fy3vdi+9uHd7YLr6ewPxRL+PU6y15urfTaamU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.72 h1:m0MmP89v1B0t3b8W8rtATU76KNsodak69QtiokHyEvo= @@ -27,10 +31,16 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.27 h1:cZG7psLfqpkB6H+fIrgUDWmlzM4 github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.27/go.mod h1:ZdjYvJpDlefgh8/hWelJhqgqJeodxu4SmbVsSdBlL7E= github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.15.13 h1:rMea2eUnnuDpwGeWTiYxZjCNOvKResVnW0KTYF1n2hI= github.com/aws/aws-sdk-go-v2/service/cognitoidentity v1.15.13/go.mod h1:6ysb13E+Ah8lESQ5Ajb2N7U9hPFU4AphflgTwhpBt3I= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.20.1 h1:gknY3OHEGXaLamootb1VaJSohtHwcIMGvm23VnZVIzE= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.20.1/go.mod h1:iA/evsHrPWhDyMj6cuMa6qlFTqSqYXoKs8LSvIFauTA= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.15 h1:yonnEISVD77M77F813Va41d8wl3A1W6HhfEmrVOcqfM= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.15/go.mod h1:3zUTVwCixtSfFyNFK0P0x92IMkfTZQpuXH7Lk/WbW9g= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.30 h1:Bje8Xkh2OWpjBdNfXLrnn8eZg569dUQmhgtydxAYyP0= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.30/go.mod h1:qQtIBl5OVMfmeQkz8HaVyh5DzFmmFXyvK27UgIgOr4c= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.29 h1:gajv/wALzb2KgK9YKq1jW+y2ZgL5o4A+UZmFfZi8lSY= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.29/go.mod h1:SYEgYIjFeLoPSOCIqdFr44QiBwGlnsUIHqMD5OZnsgg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 h1:IiDolu/eLmuB18DRZibj77n1hHQT7z12jnGO7Ze3pLc= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.4 h1:hx4WksB0NRQ9utR+2c3gEGzl6uKj3eM6PMQ6tN3lgXs=