From 8005d2018d866c233b88b1326d3a6ff400459584 Mon Sep 17 00:00:00 2001 From: sakisv Date: Wed, 27 May 2026 18:16:08 +0300 Subject: [PATCH 1/2] Get dynamodb tables --- aws/dynamodb.go | 69 +++++++++++++++++++++++++++++++++ aws/types.go | 4 ++ common/awsservicename_string.go | 5 ++- common/resourcetype_string.go | 5 ++- common/types.go | 2 + go.mod | 4 +- go.sum | 8 +++- 7 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 aws/dynamodb.go diff --git a/aws/dynamodb.go b/aws/dynamodb.go new file mode 100644 index 0000000..ce43638 --- /dev/null +++ b/aws/dynamodb.go @@ -0,0 +1,69 @@ +package aws + +import ( + "context" + "fmt" + "log" + + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/noclickops/common" +) + +type DynamodbClient interface { + ListTables(ctx context.Context, params *dynamodb.ListTablesInput, optFns ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) +} + +type NoclickopsDynamodbClient struct { + Client DynamodbClient + ClientMeta +} + +type NoclickopsDynamodbService struct { + Clients []NoclickopsDynamodbClient + common.ServiceMeta +} + +func NewDynamodbServiceFromConfigs(cfg []awssdk.Config, meta common.ServiceMeta) NoclickopsDynamodbService { + service := NoclickopsDynamodbService{ServiceMeta: meta} + for _, c := range cfg { + service.Clients = append(service.Clients, NoclickopsDynamodbClient{ + Client: dynamodb.NewFromConfig(c), + ClientMeta: ClientMeta{Region: c.Region}, + }) + } + return service +} + +func (s *NoclickopsDynamodbService) GetAllResources() []common.Resource { + return s.GetTables() +} + +func (s *NoclickopsDynamodbService) GetTables() []common.Resource { + var resources []common.Resource + for _, rc := range s.Clients { + var exclusiveStartTableName *string = nil + for { + res, err := rc.Client.ListTables(context.TODO(), &dynamodb.ListTablesInput{ + ExclusiveStartTableName: exclusiveStartTableName, + }) + if err != nil { + log.Printf("warning: %v", err) + break + } + + for _, table := range res.TableNames { + // dynamodb table ARN format: + // arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName} + arn := fmt.Sprintf("arn:aws:dynamodb:%v:%v:table/%v", rc.Region, s.AccountId, table) + resources = append(resources, common.Resource{Arn: arn, TerraformID: table, ResourceType: common.Dynamodb_table, Region: rc.Region}) + } + + if res.LastEvaluatedTableName == nil { + break + } + exclusiveStartTableName = res.LastEvaluatedTableName + } + } + return resources +} diff --git a/aws/types.go b/aws/types.go index 70d85e6..914109a 100644 --- a/aws/types.go +++ b/aws/types.go @@ -27,6 +27,7 @@ var SERVICES = map[common.AWSServiceName]common.ServiceMeta{ common.ASG: {Global: false, ServiceName: "autoscaling"}, common.Lambda: {Global: false, ServiceName: "lambda"}, common.CloudwatchLogs: {Global: false, ServiceName: "cloudwatchlogs"}, + common.Dynamodb: {Global: false, ServiceName: "dynamodb"}, // not included in the switch/case in `NewclickopsServiceFromConfigs` // because it doesn't follow the same invocation pattern. @@ -92,6 +93,9 @@ func NewNoclickopsServiceFromConfigs(service common.AWSServiceName, configs []aw case common.CloudwatchLogs: c := NewCloudwatchLogsServiceFromConfigs(configs, meta) return &c + case common.Dynamodb: + c := NewDynamodbServiceFromConfigs(configs, meta) + return &c case common.Lambda: c := NewLambdaServiceFromConfigs(configs, meta) return &c diff --git a/common/awsservicename_string.go b/common/awsservicename_string.go index b1cf44c..db460b0 100644 --- a/common/awsservicename_string.go +++ b/common/awsservicename_string.go @@ -25,11 +25,12 @@ func _() { _ = x[Lambda-14] _ = x[ResourceGroupsTaggingAPI-15] _ = x[CloudwatchLogs-16] + _ = x[Dynamodb-17] } -const _AWSServiceName_name = "Route53IAMEKSSSMEC2RDSSSOAdminIdentityStoreSNSS3CloudFrontELBELBV2ASGLambdaResourceGroupsTaggingAPICloudwatchLogs" +const _AWSServiceName_name = "Route53IAMEKSSSMEC2RDSSSOAdminIdentityStoreSNSS3CloudFrontELBELBV2ASGLambdaResourceGroupsTaggingAPICloudwatchLogsDynamodb" -var _AWSServiceName_index = [...]uint8{0, 7, 10, 13, 16, 19, 22, 30, 43, 46, 48, 58, 61, 66, 69, 75, 99, 113} +var _AWSServiceName_index = [...]uint8{0, 7, 10, 13, 16, 19, 22, 30, 43, 46, 48, 58, 61, 66, 69, 75, 99, 113, 121} func (i AWSServiceName) String() string { idx := int(i) - 0 diff --git a/common/resourcetype_string.go b/common/resourcetype_string.go index 5faff51..09614ff 100644 --- a/common/resourcetype_string.go +++ b/common/resourcetype_string.go @@ -39,11 +39,12 @@ func _() { _ = x[Lambda_function-28] _ = x[Autoscaling_group-29] _ = x[Cloudwatchlogs_log_group-30] + _ = x[Dynamodb_table-31] } -const _ResourceType_name = "Route53_zoneRoute53_recordIAM_policyIAM_userIAM_groupSSM_parameterSecurity_groupSecurity_group_ruleEKS_clusterEKS_node_groupSSOAdmin_permission_setIdentitystore_userIdentitystore_groupInstanceEipDB_instanceRDS_clusterSNS_topicSNS_subscriptionS3_bucketCloudFront_distributionELB_load_balancerELBV2_load_balancerVPCInternet_gatewayNAT_gatewaySubnetVPC_endpointLambda_functionAutoscaling_groupCloudwatchlogs_log_group" +const _ResourceType_name = "Route53_zoneRoute53_recordIAM_policyIAM_userIAM_groupSSM_parameterSecurity_groupSecurity_group_ruleEKS_clusterEKS_node_groupSSOAdmin_permission_setIdentitystore_userIdentitystore_groupInstanceEipDB_instanceRDS_clusterSNS_topicSNS_subscriptionS3_bucketCloudFront_distributionELB_load_balancerELBV2_load_balancerVPCInternet_gatewayNAT_gatewaySubnetVPC_endpointLambda_functionAutoscaling_groupCloudwatchlogs_log_groupDynamodb_table" -var _ResourceType_index = [...]uint16{0, 12, 26, 36, 44, 53, 66, 80, 99, 110, 124, 147, 165, 184, 192, 195, 206, 217, 226, 242, 251, 274, 291, 310, 313, 329, 340, 346, 358, 373, 390, 414} +var _ResourceType_index = [...]uint16{0, 12, 26, 36, 44, 53, 66, 80, 99, 110, 124, 147, 165, 184, 192, 195, 206, 217, 226, 242, 251, 274, 291, 310, 313, 329, 340, 346, 358, 373, 390, 414, 428} func (i ResourceType) String() string { idx := int(i) - 0 diff --git a/common/types.go b/common/types.go index 94c829d..13182ac 100644 --- a/common/types.go +++ b/common/types.go @@ -37,6 +37,7 @@ const ( Lambda_function Autoscaling_group Cloudwatchlogs_log_group + Dynamodb_table ) func (r ResourceType) MarshalJSON() ([]byte, error) { @@ -64,6 +65,7 @@ const ( Lambda ResourceGroupsTaggingAPI CloudwatchLogs + Dynamodb ) type Resource struct { diff --git a/go.mod b/go.mod index 5c60851..b294bac 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/autoscaling v1.66.1 github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1 github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.74.0 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.4 github.com/aws/aws-sdk-go-v2/service/ec2 v1.299.0 github.com/aws/aws-sdk-go-v2/service/eks v1.82.1 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.24 @@ -35,8 +36,9 @@ require ( github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.12.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect diff --git a/go.sum b/go.sum index c38b80c..4e70a25 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1 h1:LSv6jOIn/yEsGLeL4TLgg github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1/go.mod h1:XUduecWr236DyG8nZwJMewFbS4QcL8NZHxohdYDoPhM= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.74.0 h1:6TqDeYdvJJEIJGg5ICy7nzC7/UuHk2Eg3wrpb5bWKPM= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.74.0/go.mod h1:MLJu3PUd8fp5Qvj4CiLvyY5H8y7kxHKlTp060Wsd+Vc= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.4 h1:0E3bfw1Va3vfCrmtATvKRnGojY4oIlLl0u0xRDDUgfY= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.4/go.mod h1:dFPU89qDDGgQbXyzQ5ZY6zcjjKPVW+1M63axOw887JE= github.com/aws/aws-sdk-go-v2/service/ec2 v1.299.0 h1:qTozRFl2YFFU2HJGl7ZAywlRQvBnAN591gbAFT5bE0s= github.com/aws/aws-sdk-go-v2/service/ec2 v1.299.0/go.mod h1:E1pnYwWFZ8N3REmeN9Fe/Zipbpps4HJj8DQGNnLUMYc= github.com/aws/aws-sdk-go-v2/service/eks v1.82.1 h1:xTzXiQ8Q6U4ACdMNSCm72zd4Ds7QxhgVLqt5x8GXLBM= @@ -34,10 +36,12 @@ github.com/aws/aws-sdk-go-v2/service/iam v1.53.8 h1:p0oB4eZfBfBAOasnKvHJOlNcuHVE github.com/aws/aws-sdk-go-v2/service/iam v1.53.8/go.mod h1:epCaPnGVdiX5ra1lHPfRkVuiQGxrdY8bRI2FBJU+6ok= github.com/aws/aws-sdk-go-v2/service/identitystore v1.36.6 h1:BZuH7nZjB+3HghoE9dLMhGkBuB0iZC/oz7GoO9nhxT4= github.com/aws/aws-sdk-go-v2/service/identitystore v1.36.6/go.mod h1:d1oJcGnJe++u//4bp58J5gwQyE10wPObgWAvlfGjy/4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcbVcGABLOVuPYaIihj6IlkqubBwFj10K5fxRek= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 h1:xnvDEnw+pnj5mctWiYuFbigrEzSm35x7k4KS/ZkCANg= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14/go.mod h1:yS5rNogD8e0Wu9+l3MUwr6eENBzEeGejvINpN5PAYfY= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.12.0 h1:iNQlIMVathbcvo6USGjFFO8SgANIBg5hsoIv/QAiYK4= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.12.0/go.mod h1:3oh+5xGSd1iuxonVb3Qbm+WJYlbhczT9kbzr6doJLzY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 h1:SE+aQ4DEqG53RRCAIHlCf//B2ycxGH7jFkpnAh/kKPM= From e1ce345da3b3df0fa8742c9ffd5d39babbe4d8de Mon Sep 17 00:00:00 2001 From: sakisv Date: Wed, 27 May 2026 18:18:51 +0300 Subject: [PATCH 2/2] Add tests for dynamodb tables --- aws/dynamodb_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++ aws/helpers_test.go | 9 ++++ 2 files changed, 111 insertions(+) create mode 100644 aws/dynamodb_test.go diff --git a/aws/dynamodb_test.go b/aws/dynamodb_test.go new file mode 100644 index 0000000..aaa3eab --- /dev/null +++ b/aws/dynamodb_test.go @@ -0,0 +1,102 @@ +package aws_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/google/go-cmp/cmp" + "github.com/noclickops/aws" + "github.com/noclickops/common" +) + +func getMockedDynamodbService(mock *mockDynamodbClient) aws.NoclickopsDynamodbService { + return aws.NoclickopsDynamodbService{ + Clients: []aws.NoclickopsDynamodbClient{ + { + Client: mock, + ClientMeta: aws.ClientMeta{Region: "eu-west-1"}, + }, + }, + ServiceMeta: common.ServiceMeta{Global: false, ServiceName: "dynamodb", AccountId: "123456789012"}, + } +} + +func TestGetDynamodbTables_BasicCase(t *testing.T) { + mock := &mockDynamodbClient{ + listTablesFn: func(_ context.Context, _ *dynamodb.ListTablesInput, _ ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) { + return &dynamodb.ListTablesOutput{ + TableNames: []string{"table-1", "table-2"}, + }, nil + }, + } + svc := getMockedDynamodbService(mock) + got := svc.GetTables() + expected := []common.Resource{ + {Arn: "arn:aws:dynamodb:eu-west-1:123456789012:table/table-1", TerraformID: "table-1", ResourceType: common.Dynamodb_table, Region: "eu-west-1"}, + {Arn: "arn:aws:dynamodb:eu-west-1:123456789012:table/table-2", TerraformID: "table-2", ResourceType: common.Dynamodb_table, Region: "eu-west-1"}, + } + if diff := cmp.Diff(got, expected); diff != "" { + t.Errorf("mismatch (-got +want):\n%s", diff) + } +} + +func TestGetDynamodbTables_NoTables(t *testing.T) { + mock := &mockDynamodbClient{ + listTablesFn: func(_ context.Context, _ *dynamodb.ListTablesInput, _ ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) { + return &dynamodb.ListTablesOutput{}, nil + }, + } + svc := getMockedDynamodbService(mock) + got := svc.GetTables() + if len(got) != 0 { + t.Errorf("expected empty result, got %v", got) + } +} + +func TestGetDynamodbTables_PaginationFollowed(t *testing.T) { + callCount := 0 + mock := &mockDynamodbClient{ + listTablesFn: func(_ context.Context, params *dynamodb.ListTablesInput, _ ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) { + callCount++ + if callCount == 1 { + return &dynamodb.ListTablesOutput{ + TableNames: []string{"table-1"}, + LastEvaluatedTableName: ptr("table-1"), + }, nil + } + if params.ExclusiveStartTableName == nil || *params.ExclusiveStartTableName != "table-1" { + return nil, fmt.Errorf("wrong ExclusiveStartTableName: expected 'table-1', got %v", params.ExclusiveStartTableName) + } + return &dynamodb.ListTablesOutput{ + TableNames: []string{"table-2"}, + }, nil + }, + } + svc := getMockedDynamodbService(mock) + got := svc.GetTables() + expected := []common.Resource{ + {Arn: "arn:aws:dynamodb:eu-west-1:123456789012:table/table-1", TerraformID: "table-1", ResourceType: common.Dynamodb_table, Region: "eu-west-1"}, + {Arn: "arn:aws:dynamodb:eu-west-1:123456789012:table/table-2", TerraformID: "table-2", ResourceType: common.Dynamodb_table, Region: "eu-west-1"}, + } + if diff := cmp.Diff(got, expected); diff != "" { + t.Errorf("mismatch (-got +want):\n%s", diff) + } + if callCount != 2 { + t.Errorf("expected 2 calls to ListTables, got %d", callCount) + } +} + +func TestGetDynamodbTables_ErrorIsSkipped(t *testing.T) { + mock := &mockDynamodbClient{ + listTablesFn: func(_ context.Context, _ *dynamodb.ListTablesInput, _ ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) { + return nil, fmt.Errorf("access denied") + }, + } + svc := getMockedDynamodbService(mock) + got := svc.GetTables() + if len(got) != 0 { + t.Errorf("expected empty result on error, got %v", got) + } +} diff --git a/aws/helpers_test.go b/aws/helpers_test.go index 06d9f26..000af64 100644 --- a/aws/helpers_test.go +++ b/aws/helpers_test.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/autoscaling" "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" @@ -205,3 +206,11 @@ type mockCloudwatchLogsClient struct { func (m *mockCloudwatchLogsClient) ListLogGroups(ctx context.Context, params *cloudwatchlogs.ListLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListLogGroupsOutput, error) { return m.listLogGroupsFn(ctx, params, optFns...) } + +type mockDynamodbClient struct { + listTablesFn func(ctx context.Context, params *dynamodb.ListTablesInput, optFns ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) +} + +func (m *mockDynamodbClient) ListTables(ctx context.Context, params *dynamodb.ListTablesInput, optFns ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) { + return m.listTablesFn(ctx, params, optFns...) +}