From 02a4bad83e6605a2c5faff62075403161f21008b Mon Sep 17 00:00:00 2001 From: Shaharia Azam Date: Wed, 16 Aug 2023 02:36:26 +0200 Subject: [PATCH] Added fields option for AWS ECR (#45) - Added fields option for AWS ECR - Re-factored field mapper for sources to reduce code duplication --- pkg/cmd/testdata/valid_config.yaml | 17 ++- pkg/config/schema.json | 60 +++++--- pkg/config/schema_validator_test.go | 6 + pkg/config/testdata/valid_config.yaml | 6 + pkg/source/scanner/aws_ec2.go | 91 +++--------- pkg/source/scanner/aws_ecr.go | 78 +++++----- pkg/source/scanner/aws_ecr_test.go | 166 ++++++++-------------- pkg/source/scanner/github_repo_scanner.go | 66 ++++----- pkg/source/scanner/scanner.go | 71 +++++++++ pkg/source/scanner/scanner_test.go | 65 +++++++++ pkg/source/source.go | 1 + pkg/source/source_test.go | 1 + 12 files changed, 354 insertions(+), 274 deletions(-) create mode 100644 pkg/source/scanner/scanner_test.go diff --git a/pkg/cmd/testdata/valid_config.yaml b/pkg/cmd/testdata/valid_config.yaml index f3025ded..0e1a2734 100644 --- a/pkg/cmd/testdata/valid_config.yaml +++ b/pkg/cmd/testdata/valid_config.yaml @@ -55,7 +55,7 @@ source: - topics aws_ec2_one: type: aws_ec2 - configuration: + configuration: &aws_conf access_key: "xxxx" secret_key: "xxxx" session_token: "xxxx" @@ -71,12 +71,15 @@ source: - instance_state - vpc_id - tags - #two: - # type: kubernetes - # configuration: - # kube_config_file_path: "another_path" - # depends_on: - # - "one" + aws_ecr_one: + type: aws_ecr + configuration: *aws_conf + fields: + - repository_name + - repository_uri + - registry_id + - arn + - tags relations: criteria: - name: "file-system-rule1" diff --git a/pkg/config/schema.json b/pkg/config/schema.json index cf6c1322..14398121 100644 --- a/pkg/config/schema.json +++ b/pkg/config/schema.json @@ -235,7 +235,7 @@ "$ref": "#/definitions/source.github_repository.fields" } }, - "required": ["type", "configuration"] + "required": ["type", "configuration", "fields"] }, "source.github_repository.configuration": { "type": "object", @@ -310,7 +310,26 @@ "$ref": "#/definitions/source.aws_ec2.fields" } }, - "required": ["type", "configuration"] + "required": ["type", "configuration", "fields"] + }, + "source.aws_ec2.fields": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "instance_id", + "image_id", + "private_dns_name", + "instance_type", + "architecture", + "instance_lifecycle", + "instance_state", + "vpc_id", + "tags" + ] + }, + "uniqueItems": true, + "additionalItems": false }, "source.aws_ecr": { "type": "object", @@ -321,10 +340,28 @@ }, "configuration": { "$ref": "#/definitions/source.aws_common.configuration" + }, + "fields": { + "$ref": "#/definitions/source.aws_ecr.fields" } }, "required": ["type", "configuration"] }, + "source.aws_ecr.fields": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "repository_name", + "arn", + "registry_id", + "repository_uri", + "tags" + ] + }, + "uniqueItems": true, + "additionalItems": false + }, "source.aws_common.configuration": { "type": "object", "properties": { @@ -351,25 +388,6 @@ }, "required": ["access_key", "secret_key", "session_token", "region", "account_id"] }, - "source.aws_ec2.fields": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "instance_id", - "image_id", - "private_dns_name", - "instance_type", - "architecture", - "instance_lifecycle", - "instance_state", - "vpc_id", - "tags" - ] - }, - "uniqueItems": true, - "additionalItems": false - }, "storage.postgresql": { "type": "object", "properties": { diff --git a/pkg/config/schema_validator_test.go b/pkg/config/schema_validator_test.go index 48629122..91f63751 100644 --- a/pkg/config/schema_validator_test.go +++ b/pkg/config/schema_validator_test.go @@ -96,6 +96,12 @@ source: aws_ecr_example: type: aws_ecr configuration: *aws_conf + fields: + - repository_name + - repository_uri + - registry_id + - arn + - tags relations: criteria: - name: "file-system-rule1" diff --git a/pkg/config/testdata/valid_config.yaml b/pkg/config/testdata/valid_config.yaml index 666bf27c..4516afbf 100644 --- a/pkg/config/testdata/valid_config.yaml +++ b/pkg/config/testdata/valid_config.yaml @@ -71,6 +71,12 @@ source: aws_ecr_example: type: aws_ecr configuration: *aws_conf + fields: + - repository_name + - repository_uri + - registry_id + - arn + - tags relations: criteria: - name: "file-system-rule1" diff --git a/pkg/source/scanner/aws_ec2.go b/pkg/source/scanner/aws_ec2.go index ae577a32..b7320581 100644 --- a/pkg/source/scanner/aws_ec2.go +++ b/pkg/source/scanner/aws_ec2.go @@ -3,12 +3,9 @@ package scanner import ( "context" - "fmt" - "strings" "github.com/shaharia-lab/teredix/pkg" "github.com/shaharia-lab/teredix/pkg/resource" - "github.com/shaharia-lab/teredix/pkg/util" "github.com/aws/aws-sdk-go-v2/service/ec2/types" @@ -69,7 +66,13 @@ func (a *AWSEC2) Scan(resourceChannel chan resource.Resource) error { // Loop through instances and their tags for _, reservation := range resp.Reservations { for _, instance := range reservation.Instances { - resourceChannel <- a.mapToResource(instance) + resourceChannel <- resource.Resource{ + Name: *instance.InstanceId, + Kind: pkg.ResourceKindAWSEC2, + UUID: *instance.InstanceId, + ExternalID: *instance.InstanceId, + MetaData: a.getMetaData(instance), + } } } @@ -83,6 +86,22 @@ func (a *AWSEC2) Scan(resourceChannel chan resource.Resource) error { return nil } +func (a *AWSEC2) getMetaData(instance types.Instance) []resource.MetaData { + mappings := map[string]func() string{ + fieldInstanceID: func() string { return safeDereference(instance.InstanceId) }, + fieldImageID: func() string { return safeDereference(instance.ImageId) }, + fieldPrivateDNSName: func() string { return safeDereference(instance.PrivateDnsName) }, + fieldInstanceType: func() string { return stringValueOrDefault(string(instance.InstanceType)) }, + fieldArchitecture: func() string { return stringValueOrDefault(string(instance.Architecture)) }, + fieldInstanceLifecycle: func() string { return stringValueOrDefault(string(instance.InstanceLifecycle)) }, + fieldInstanceState: func() string { return stringValueOrDefault(string(instance.State.Name)) }, + fieldVpcID: func() string { return safeDereference(instance.VpcId) }, + } + return NewFieldMapper(mappings, func() []types.Tag { + return instance.Tags + }, a.Fields).getResourceMetaData() +} + func (a *AWSEC2) makeAPICallToAWS(nextToken string) (*ec2.DescribeInstancesOutput, error) { // Describe instances for current page params := &ec2.DescribeInstancesInput{ @@ -112,67 +131,3 @@ func (a *AWSEC2) makeAPICallToAWS(nextToken string) (*ec2.DescribeInstancesOutpu } return resp, nil } - -func (a *AWSEC2) mapToResource(instance types.Instance) resource.Resource { - var repoMeta []resource.MetaData - for _, mapper := range a.fieldMapper(instance) { - if util.IsFieldExistsInConfig(mapper.field, a.Fields) || strings.Contains(mapper.field, "tag_") { - val := mapper.value() - if val != "" { - repoMeta = append(repoMeta, resource.MetaData{Key: mapper.field, Value: val}) - } - } - } - - return resource.Resource{ - Name: *instance.InstanceId, - Kind: pkg.ResourceKindAWSEC2, - UUID: *instance.InstanceId, - ExternalID: *instance.InstanceId, - MetaData: repoMeta, - } -} - -func safeDereference(s *string) string { - if s != nil { - return *s - } - return "" -} - -func stringValueOrDefault(s string) string { - if s != "" { - return s - } - return "" -} - -func (a *AWSEC2) fieldMapper(instance types.Instance) []MetaDataMapper { - var fieldMapper []MetaDataMapper - - mappings := map[string]func() string{ - fieldInstanceID: func() string { return safeDereference(instance.InstanceId) }, - fieldImageID: func() string { return safeDereference(instance.ImageId) }, - fieldPrivateDNSName: func() string { return safeDereference(instance.PrivateDnsName) }, - fieldInstanceType: func() string { return stringValueOrDefault(string(instance.InstanceType)) }, - fieldArchitecture: func() string { return stringValueOrDefault(string(instance.Architecture)) }, - fieldInstanceLifecycle: func() string { return stringValueOrDefault(string(instance.InstanceLifecycle)) }, - fieldInstanceState: func() string { return stringValueOrDefault(string(instance.State.Name)) }, - fieldVpcID: func() string { return safeDereference(instance.VpcId) }, - } - - for field, fn := range mappings { - fieldMapper = append(fieldMapper, MetaDataMapper{field: field, value: fn}) - } - - if util.IsFieldExistsInConfig(fieldTags, a.Fields) { - for _, tag := range instance.Tags { - fieldMapper = append(fieldMapper, MetaDataMapper{ - field: fmt.Sprintf("tag_%s", safeDereference(tag.Key)), - value: func() string { return safeDereference(tag.Value) }, - }) - } - } - - return fieldMapper -} diff --git a/pkg/source/scanner/aws_ecr.go b/pkg/source/scanner/aws_ecr.go index 441971d8..b524d3bc 100644 --- a/pkg/source/scanner/aws_ecr.go +++ b/pkg/source/scanner/aws_ecr.go @@ -3,8 +3,9 @@ package scanner import ( "context" - "fmt" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types" "github.com/shaharia-lab/teredix/pkg" "github.com/shaharia-lab/teredix/pkg/resource" "github.com/shaharia-lab/teredix/pkg/util" @@ -13,6 +14,14 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecr" ) +const ( + ecrFieldRepositoryName = "repositoryName" + ecrFieldArn = "repositoryArn" + ecrFieldRegistryID = "registryID" + ecrFieldRepositoryURI = "repositoryURI" + ecrFieldTags = "tags" +) + // EcrClient build aws client type EcrClient interface { DescribeRepositories(context.Context, *ecr.DescribeRepositoriesInput, ...func(options *ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) @@ -27,16 +36,18 @@ type AWSECR struct { Region string AccountID string ResourceTaggingService util.ResourceTaggingServiceClient + Fields []string } // NewAWSECR construct AWS ECR source -func NewAWSECR(sourceName string, region string, accountID string, ecrClient EcrClient, resourceTaggingService util.ResourceTaggingServiceClient) *AWSECR { +func NewAWSECR(sourceName string, region string, accountID string, ecrClient EcrClient, resourceTaggingService util.ResourceTaggingServiceClient, fields []string) *AWSECR { return &AWSECR{ SourceName: sourceName, ECRClient: ecrClient, Region: region, AccountID: accountID, ResourceTaggingService: resourceTaggingService, + Fields: fields, } } @@ -68,40 +79,7 @@ func (a *AWSECR) Scan(resourceChannel chan resource.Resource) error { Kind: pkg.ResourceKindAWSECR, UUID: util.GenerateUUID(), ExternalID: *repository.RepositoryArn, - MetaData: []resource.MetaData{ - { - Key: "AWS-ECR-Repository-Name", - Value: *repository.RepositoryName, - }, - { - Key: "AWS-ECR-Repository-Arn", - Value: *repository.RepositoryArn, - }, - { - Key: "AWS-ECR-Registry-Id", - Value: *repository.RegistryId, - }, - { - Key: "AWS-ECR-Repository-URI", - Value: *repository.RepositoryUri, - }, - { - Key: pkg.MetaKeyScannerLabel, - Value: a.SourceName, - }, - }, - } - - tags, err := util.GetAWSResourceTagByARN(context.Background(), a.ResourceTaggingService, *repository.RepositoryArn) - if err != nil { - return err - } - - for tagKey, tagValue := range tags { - res.MetaData = append(res.MetaData, resource.MetaData{ - Key: fmt.Sprintf("AWS-ECR-%s", tagKey), - Value: tagValue, - }) + MetaData: a.getMetaData(repository), } resourceChannel <- res @@ -117,3 +95,31 @@ func (a *AWSECR) Scan(resourceChannel chan resource.Resource) error { return nil } + +func (a *AWSECR) getMetaData(repository ecrTypes.Repository) []resource.MetaData { + mappings := map[string]func() string{ + ecrFieldRepositoryName: func() string { return stringValueOrDefault(*repository.RepositoryName) }, + ecrFieldArn: func() string { return stringValueOrDefault(*repository.RepositoryArn) }, + ecrFieldRegistryID: func() string { return stringValueOrDefault(*repository.RegistryId) }, + ecrFieldRepositoryURI: func() string { return stringValueOrDefault(*repository.RepositoryUri) }, + } + + getTags := func() []types.Tag { + tags, err := util.GetAWSResourceTagByARN(context.Background(), a.ResourceTaggingService, *repository.RepositoryArn) + if err != nil { + return []types.Tag{} + } + + var tt []types.Tag + for key, val := range tags { + tt = append(tt, types.Tag{ + Key: aws.String(key), + Value: aws.String(val), + }) + } + + return tt + } + + return NewFieldMapper(mappings, getTags, a.Fields).getResourceMetaData() +} diff --git a/pkg/source/scanner/aws_ecr_test.go b/pkg/source/scanner/aws_ecr_test.go index c44943c9..66491351 100644 --- a/pkg/source/scanner/aws_ecr_test.go +++ b/pkg/source/scanner/aws_ecr_test.go @@ -4,10 +4,8 @@ import ( "context" "testing" - "github.com/shaharia-lab/teredix/pkg" - "github.com/shaharia-lab/teredix/pkg/resource" - "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi" + "github.com/shaharia-lab/teredix/pkg/util" "github.com/aws/aws-sdk-go/aws" @@ -150,117 +148,75 @@ func (_m *ResourceTaggingServiceClientMock) GetResources(_a0 context.Context, _a } func TestAWSECR_Scan(t *testing.T) { - // create mock ECR client - mockEcrClient := new(EcrClientMock) - - // Create a mock client - mockSvc := new(ResourceTaggingServiceClientMock) - - expectedOutput := &resourcegroupstaggingapi.GetResourcesOutput{ - ResourceTagMappingList: []types.ResourceTagMapping{ - { - Tags: []types.Tag{ - { - Key: aws.String("Environment"), - Value: aws.String("prod"), - }, - { - Key: aws.String("Owner"), - Value: aws.String("john@example.com"), - }, - }, + testCases := []struct { + name string + sourceFields []string + awsECRRepositories []ecrTypes.Repository + awsECRTags []types.Tag + expectedTotalResource int + expectedMetaDataKeys []string + }{ + { + name: "returns resources", + sourceFields: []string{ + ecrFieldRepositoryName, + ecrFieldRepositoryURI, + ecrFieldArn, + ecrFieldRegistryID, + ecrFieldTags, }, - }, - } - - mockSvc.On("GetResources", mock.Anything, mock.Anything, mock.Anything).Return(expectedOutput, nil) - - // create an instance of AWSECR that uses the mock ECR client - awsecr := NewAWSECR("test-source", "us-west-2", "xxx", mockEcrClient, mockSvc) - - // Define mock output - mockOutput := &ecr.DescribeRepositoriesOutput{ - Repositories: []ecrTypes.Repository{ - { - RepositoryUri: aws.String("something"), - RepositoryName: &[]string{"test-repo"}[0], - RegistryId: &[]string{"1234567890"}[0], - RepositoryArn: &[]string{"arn:aws:ecr:us-west-2:1234567890:repository/test-repo"}[0], - }, - }, - } - mockEcrClient.On("DescribeRepositories", mock.Anything, mock.Anything).Return(mockOutput, nil) - - // set expectations on the mock ECR client's DescribeImages method - mockEcrClient.On("DescribeImages", - mock.Anything, - mock.Anything, - mock.Anything, - ).Return( - &ecr.DescribeImagesOutput{ - ImageDetails: []ecrTypes.ImageDetail{ + awsECRRepositories: []ecrTypes.Repository{ { - ImageDigest: aws.String("sha256:1234567890"), - ImageTags: []string{"tag1", "tag2", "tag3"}, + RepositoryUri: aws.String("something"), + RepositoryName: &[]string{"test-repo"}[0], + RegistryId: &[]string{"1234567890"}[0], + RepositoryArn: &[]string{"arn:aws:ecr:us-west-2:1234567890:repository/test-repo"}[0], }, }, - }, nil, - ) - - // create channel for resource output - resourceChannel := make(chan resource.Resource, 1) - - // run Scan method - err := awsecr.Scan(resourceChannel) - - // assert no errors occurred - assert.Nil(t, err) - - // assert expected resource was sent to resource channel - expectedResource := resource.Resource{ - Name: "test-repo", - Kind: pkg.ResourceKindAWSECR, - UUID: "arn:aws:ecr:us-west-2:1234567890:repository/test-repo", - ExternalID: "arn:aws:ecr:us-west-2:1234567890:repository/test-repo", - MetaData: []resource.MetaData{ - { - Key: "AWS-ECR-Repository-Name", - Value: "test-repo", - }, - { - Key: "AWS-ECR-Image-Digest", - Value: "sha256:1234567890", - }, - { - Key: "AWS-ECR-Image-Tag", - Value: "tag1", - }, - { - Key: "AWS-ECR-Image-Tag", - Value: "tag1", - }, - { - Key: "AWS-ECR-Image-Tag", - Value: "tag2", - }, - { - Key: "AWS-ECR-Image-Tag", - Value: "tag3", + awsECRTags: []types.Tag{ + { + Key: aws.String("Environment"), + Value: aws.String("prod"), + }, }, - { - Key: pkg.MetaKeyScannerLabel, - Value: "tag3", + expectedTotalResource: 1, + expectedMetaDataKeys: []string{ + ecrFieldRepositoryName, + ecrFieldRepositoryURI, + ecrFieldArn, + ecrFieldRegistryID, + "tag_Environment", }, }, } - receivedResource := <-resourceChannel + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // create mock ECR client + mockEcrClient := new(EcrClientMock) + // Define mock output + mockOutput := &ecr.DescribeRepositoriesOutput{ + Repositories: tc.awsECRRepositories, + } + mockEcrClient.On("DescribeRepositories", mock.Anything, mock.Anything).Return(mockOutput, nil) + + // Create a mock client + mockSvc := new(ResourceTaggingServiceClientMock) + + expectedOutput := &resourcegroupstaggingapi.GetResourcesOutput{ + ResourceTagMappingList: []types.ResourceTagMapping{ + { + Tags: tc.awsECRTags, + }, + }, + } - assert.Equal(t, len(expectedResource.MetaData), len(receivedResource.MetaData)) - assert.Equal(t, expectedResource.Kind, receivedResource.Kind) - assert.Equal(t, expectedResource.Name, receivedResource.Name) - assert.Equal(t, expectedResource.ExternalID, receivedResource.ExternalID) + mockSvc.On("GetResources", mock.Anything, mock.Anything, mock.Anything).Return(expectedOutput, nil) + // create an instance of AWSECR that uses the mock ECR client + res := RunScannerForTests(NewAWSECR("test-source", "us-west-2", "xxx", mockEcrClient, mockSvc, tc.sourceFields)) + assert.Equal(t, tc.expectedTotalResource, len(res)) + util.CheckIfMetaKeysExistsInResources(t, res, tc.expectedMetaDataKeys) + }) - // assert that all expected calls to mock ECR client's methods were made - //mockEcrClient.AssertExpectations(t) + } } diff --git a/pkg/source/scanner/github_repo_scanner.go b/pkg/source/scanner/github_repo_scanner.go index 9c2e6a31..869ff353 100644 --- a/pkg/source/scanner/github_repo_scanner.go +++ b/pkg/source/scanner/github_repo_scanner.go @@ -7,6 +7,7 @@ import ( "fmt" "strconv" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/shaharia-lab/teredix/pkg" "github.com/shaharia-lab/teredix/pkg/resource" "github.com/shaharia-lab/teredix/pkg/util" @@ -90,69 +91,60 @@ func (r *GitHubRepositoryScanner) Scan(resourceChannel chan resource.Resource) e } for _, repo := range repos { - resourceChannel <- r.mapToResource(repo) - } - - return nil -} - -func (r *GitHubRepositoryScanner) mapToResource(repo *github.Repository) resource.Resource { - var repoMeta []resource.MetaData - for _, mapper := range r.fieldMapper(repo) { - if util.IsFieldExistsInConfig(mapper.field, r.fields) { - val := mapper.value() - if val != "" { - repoMeta = append(repoMeta, resource.MetaData{Key: mapper.field, Value: val}) - } + resourceChannel <- resource.Resource{ + Kind: pkg.ResourceKindGitHubRepository, + UUID: util.GenerateUUID(), + Name: repo.GetFullName(), + ExternalID: repo.GetFullName(), + MetaData: r.getMetaData(repo), } } - return resource.Resource{ - Kind: pkg.ResourceKindGitHubRepository, - UUID: util.GenerateUUID(), - Name: repo.GetFullName(), - ExternalID: repo.GetFullName(), - MetaData: repoMeta, - } + return nil } -func (r *GitHubRepositoryScanner) fieldMapper(repo *github.Repository) []MetaDataMapper { - return []MetaDataMapper{ - {fieldCompany, func() string { +func (r *GitHubRepositoryScanner) getMetaData(repo *github.Repository) []resource.MetaData { + mappings := map[string]func() string{ + fieldCompany: func() string { if repo.GetOwner() != nil { return repo.GetOwner().GetCompany() } return "" - }}, - {fieldLanguage, repo.GetLanguage}, - {fieldHomepage, repo.GetHomepage}, - {fieldOrg, func() string { + }, + fieldLanguage: repo.GetLanguage, + fieldHomepage: repo.GetHomepage, + fieldOrg: func() string { if repo.GetOrganization() != nil { return repo.GetOrganization().GetName() } return "" - }}, - {fieldStars, func() string { return strconv.Itoa(repo.GetStargazersCount()) }}, - {fieldGitURL, repo.GetGitURL}, - {fieldOwnerName, func() string { + }, + fieldStars: func() string { return strconv.Itoa(repo.GetStargazersCount()) }, + fieldGitURL: repo.GetGitURL, + fieldOwnerName: func() string { if repo.GetOwner() != nil { return repo.GetOwner().GetName() } return "" - }}, - {fieldOwnerLogin, func() string { + }, + fieldOwnerLogin: func() string { if repo.GetOwner() != nil { return repo.GetOwner().GetLogin() } return "" - }}, - {fieldTopics, func() string { + }, + fieldTopics: func() string { topics, err := json.Marshal(repo.Topics) if err == nil { return string(topics) } return "" - }}, + }, } + + fm := NewFieldMapper(mappings, func() []types.Tag { + return []types.Tag{} + }, r.fields) + return fm.getResourceMetaData() } diff --git a/pkg/source/scanner/scanner.go b/pkg/source/scanner/scanner.go index 83cd74cd..5d156131 100644 --- a/pkg/source/scanner/scanner.go +++ b/pkg/source/scanner/scanner.go @@ -2,7 +2,12 @@ package scanner import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/shaharia-lab/teredix/pkg/resource" + "github.com/shaharia-lab/teredix/pkg/util" ) // Scanner interface to build different scanner @@ -35,3 +40,69 @@ func RunScannerForTests(scanner Scanner) []resource.Resource { } return res } + +func safeDereference(s *string) string { + if s != nil { + return *s + } + return "" +} + +func stringValueOrDefault(s string) string { + if s != "" { + return s + } + return "" +} + +// FieldMapper is a structure that helps in mapping various fields +// and tags to resource.MetaData structures. +type FieldMapper struct { + mappings map[string]func() string // Map of field names to their corresponding value functions. + tags func() []types.Tag // Function that retrieves a list of tags. + fields []string // List of fields to consider during the mapping. +} + +// NewFieldMapper initializes and returns a new instance of FieldMapper. +func NewFieldMapper(mappings map[string]func() string, tags func() []types.Tag, fields []string) *FieldMapper { + return &FieldMapper{ + mappings: mappings, + tags: tags, + fields: fields, + } +} + +// getResourceMetaData constructs and returns a list of resource.MetaData based on +// the FieldMapper's mappings and tags. Only fields specified in the FieldMapper's +// 'fields' slice or having the "tag_" prefix are considered. +// +// For each field in mappings, the associated function is called to retrieve its value. +// Additionally, if tags are specified in the configuration, they are appended with +// the "tag_" prefix and included in the final resource.MetaData list. +func (f *FieldMapper) getResourceMetaData() []resource.MetaData { + var fieldMapper []MetaDataMapper + for field, fn := range f.mappings { + fieldMapper = append(fieldMapper, MetaDataMapper{field: field, value: fn}) + } + + if util.IsFieldExistsInConfig(fieldTags, f.fields) { + for _, tag := range f.tags() { + fieldMapper = append(fieldMapper, MetaDataMapper{ + field: fmt.Sprintf("tag_%s", *tag.Key), + value: func() string { return stringValueOrDefault(*tag.Value) }, + }) + } + } + + var resMeta []resource.MetaData + for _, mapper := range fieldMapper { + if util.IsFieldExistsInConfig(mapper.field, f.fields) || strings.Contains(mapper.field, "tag_") { + val := mapper.value() + if val != "" { + resMeta = append(resMeta, resource.MetaData{Key: mapper.field, Value: val}) + } + } + } + + return resMeta +} diff --git a/pkg/source/scanner/scanner_test.go b/pkg/source/scanner/scanner_test.go new file mode 100644 index 00000000..0fb60fbb --- /dev/null +++ b/pkg/source/scanner/scanner_test.go @@ -0,0 +1,65 @@ +package scanner + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/shaharia-lab/teredix/pkg/resource" +) + +// Data provider structure +type getResourceMetaDataTestCase struct { + name string + inputMapper *FieldMapper + expectedOutput []resource.MetaData +} + +func TestGetResourceMetaData(t *testing.T) { + // Mocked functions for demonstration purposes + mockMappingFunc := func() string { + return "value" + } + mockTagsFunc := func() []types.Tag { + return []types.Tag{{Key: aws.String("tagKey"), Value: aws.String("tagValue")}} + } + + // Your data provider test cases + testCases := []getResourceMetaDataTestCase{ + { + name: "Basic Case", + inputMapper: NewFieldMapper( + map[string]func() string{"field1": mockMappingFunc}, + mockTagsFunc, + []string{"field1", fieldTags}, + ), + expectedOutput: []resource.MetaData{ + {Key: "field1", Value: "value"}, + {Key: "tag_tagKey", Value: "tagValue"}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actualOutput := testCase.inputMapper.getResourceMetaData() + + if !reflect.DeepEqual(actualOutput, testCase.expectedOutput) { + t.Errorf("Expected %v, but got %v", testCase.expectedOutput, actualOutput) + } + }) + } +} + +// MockScanner is a mock implementation of the Scanner interface +type MockScanner struct { + resources []resource.Resource +} + +// Scan is a mock method that sends resources to the given channel +func (m *MockScanner) Scan(ch chan<- resource.Resource) { + for _, r := range m.resources { + ch <- r + } +} diff --git a/pkg/source/source.go b/pkg/source/source.go index c967a9b8..747c8fad 100644 --- a/pkg/source/source.go +++ b/pkg/source/source.go @@ -105,6 +105,7 @@ func BuildSources(appConfig *config.AppConfig) []Source { s.Configuration["account_id"], ecr.NewFromConfig(buildAWSConfig(s)), resourcegroupstaggingapi.NewFromConfig(buildAWSConfig(s)), + s.Fields, ), }) } diff --git a/pkg/source/source_test.go b/pkg/source/source_test.go index 78ad0236..af99ef7a 100644 --- a/pkg/source/source_test.go +++ b/pkg/source/source_test.go @@ -70,6 +70,7 @@ func TestBuildSources(t *testing.T) { "xxxx", ecr.NewFromConfig(awsConfig), resourcegroupstaggingapi.NewFromConfig(awsConfig), + []string{}, ) expectedSources := []Source{