From 41d96c75b0c728754d7a2cb0a6f52775f62c85eb Mon Sep 17 00:00:00 2001 From: sakisv Date: Wed, 27 May 2026 17:52:18 +0300 Subject: [PATCH 1/2] Fetch cloudwatch log groups --- aws/cloudwatchlogs.go | 66 +++++++++++++++++++++++++++++++++ aws/types.go | 28 ++++++++------ common/awsservicename_string.go | 5 ++- common/resourcetype_string.go | 5 ++- common/types.go | 2 + go.mod | 1 + go.sum | 2 + 7 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 aws/cloudwatchlogs.go diff --git a/aws/cloudwatchlogs.go b/aws/cloudwatchlogs.go new file mode 100644 index 0000000..07cf913 --- /dev/null +++ b/aws/cloudwatchlogs.go @@ -0,0 +1,66 @@ +package aws + +import ( + "context" + "log" + + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + "github.com/noclickops/common" +) + +type CloudwatchLogsClient interface { + ListLogGroups(ctx context.Context, params *cloudwatchlogs.ListLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListLogGroupsOutput, error) +} + +type NoclickopsCloudwatchLogsClient struct { + Client CloudwatchLogsClient + ClientMeta +} + +type NoclickopsCloudwatchLogsService struct { + Clients []NoclickopsCloudwatchLogsClient + common.ServiceMeta +} + +func NewCloudwatchLogsServiceFromConfigs(cfg []awssdk.Config, meta common.ServiceMeta) NoclickopsCloudwatchLogsService { + service := NoclickopsCloudwatchLogsService{ServiceMeta: meta} + for _, c := range cfg { + service.Clients = append(service.Clients, NoclickopsCloudwatchLogsClient{ + Client: cloudwatchlogs.NewFromConfig(c), + ClientMeta: ClientMeta{Region: c.Region}, + }) + } + return service +} + +func (s *NoclickopsCloudwatchLogsService) GetAllResources() []common.Resource { + return s.GetLogGroups() +} + +func (s *NoclickopsCloudwatchLogsService) GetLogGroups() []common.Resource { + var resources []common.Resource + for _, rc := range s.Clients { + var nextToken *string = nil + for { + res, err := rc.Client.ListLogGroups(context.TODO(), &cloudwatchlogs.ListLogGroupsInput{ + NextToken: nextToken, + }) + if err != nil { + log.Printf("warning: %v", err) + break + } + + for _, loggroup := range res.LogGroups { + resources = append(resources, common.Resource{Arn: *loggroup.LogGroupArn, TerraformID: *loggroup.LogGroupName, ResourceType: common.Cloudwatchlogs_log_group, Region: rc.Region}) + } + + + if res.NextToken == nil { + break + } + nextToken = res.NextToken + } + } + return resources +} diff --git a/aws/types.go b/aws/types.go index 8fc5649..70d85e6 100644 --- a/aws/types.go +++ b/aws/types.go @@ -14,18 +14,19 @@ var SERVICES = map[common.AWSServiceName]common.ServiceMeta{ common.Route53: {Global: true, ServiceName: "route53"}, common.CloudFront: {Global: true, ServiceName: "cloudfront"}, - common.EKS: {Global: false, ServiceName: "eks"}, - common.IdentityStore: {Global: false, ServiceName: "identitystore"}, - common.SSM: {Global: false, ServiceName: "ssm"}, - common.SSOAdmin: {Global: false, ServiceName: "ssoadmin"}, - common.EC2: {Global: false, ServiceName: "ec2"}, - common.RDS: {Global: false, ServiceName: "rds"}, - common.SNS: {Global: false, ServiceName: "sns"}, - common.S3: {Global: false, ServiceName: "s3"}, - common.ELB: {Global: false, ServiceName: "elb"}, - common.ELBV2: {Global: false, ServiceName: "elbv2"}, - common.ASG: {Global: false, ServiceName: "autoscaling"}, - common.Lambda: {Global: false, ServiceName: "lambda"}, + common.EKS: {Global: false, ServiceName: "eks"}, + common.IdentityStore: {Global: false, ServiceName: "identitystore"}, + common.SSM: {Global: false, ServiceName: "ssm"}, + common.SSOAdmin: {Global: false, ServiceName: "ssoadmin"}, + common.EC2: {Global: false, ServiceName: "ec2"}, + common.RDS: {Global: false, ServiceName: "rds"}, + common.SNS: {Global: false, ServiceName: "sns"}, + common.S3: {Global: false, ServiceName: "s3"}, + common.ELB: {Global: false, ServiceName: "elb"}, + common.ELBV2: {Global: false, ServiceName: "elbv2"}, + common.ASG: {Global: false, ServiceName: "autoscaling"}, + common.Lambda: {Global: false, ServiceName: "lambda"}, + common.CloudwatchLogs: {Global: false, ServiceName: "cloudwatchlogs"}, // not included in the switch/case in `NewclickopsServiceFromConfigs` // because it doesn't follow the same invocation pattern. @@ -88,6 +89,9 @@ func NewNoclickopsServiceFromConfigs(service common.AWSServiceName, configs []aw case common.ASG: c := NewAutoscalingServiceFromConfigs(configs, meta) return &c + case common.CloudwatchLogs: + c := NewCloudwatchLogsServiceFromConfigs(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 3e302f3..b1cf44c 100644 --- a/common/awsservicename_string.go +++ b/common/awsservicename_string.go @@ -24,11 +24,12 @@ func _() { _ = x[ASG-13] _ = x[Lambda-14] _ = x[ResourceGroupsTaggingAPI-15] + _ = x[CloudwatchLogs-16] } -const _AWSServiceName_name = "Route53IAMEKSSSMEC2RDSSSOAdminIdentityStoreSNSS3CloudFrontELBELBV2ASGLambdaResourceGroupsTaggingAPI" +const _AWSServiceName_name = "Route53IAMEKSSSMEC2RDSSSOAdminIdentityStoreSNSS3CloudFrontELBELBV2ASGLambdaResourceGroupsTaggingAPICloudwatchLogs" -var _AWSServiceName_index = [...]uint8{0, 7, 10, 13, 16, 19, 22, 30, 43, 46, 48, 58, 61, 66, 69, 75, 99} +var _AWSServiceName_index = [...]uint8{0, 7, 10, 13, 16, 19, 22, 30, 43, 46, 48, 58, 61, 66, 69, 75, 99, 113} func (i AWSServiceName) String() string { idx := int(i) - 0 diff --git a/common/resourcetype_string.go b/common/resourcetype_string.go index 0d1a6d7..5faff51 100644 --- a/common/resourcetype_string.go +++ b/common/resourcetype_string.go @@ -38,11 +38,12 @@ func _() { _ = x[VPC_endpoint-27] _ = x[Lambda_function-28] _ = x[Autoscaling_group-29] + _ = x[Cloudwatchlogs_log_group-30] } -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_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_group" -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} +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} func (i ResourceType) String() string { idx := int(i) - 0 diff --git a/common/types.go b/common/types.go index c5d5485..94c829d 100644 --- a/common/types.go +++ b/common/types.go @@ -36,6 +36,7 @@ const ( VPC_endpoint Lambda_function Autoscaling_group + Cloudwatchlogs_log_group ) func (r ResourceType) MarshalJSON() ([]byte, error) { @@ -62,6 +63,7 @@ const ( ASG Lambda ResourceGroupsTaggingAPI + CloudwatchLogs ) type Resource struct { diff --git a/go.mod b/go.mod index f02850a..5c60851 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/account v1.30.6 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/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 diff --git a/go.sum b/go.sum index d3519da..c38b80c 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/aws/aws-sdk-go-v2/service/autoscaling v1.66.1 h1:kGlbhb5GMfkP/bcqcbt3 github.com/aws/aws-sdk-go-v2/service/autoscaling v1.66.1/go.mod h1:z45kurrOonQepd3SN5LIgropAn1NGHwBn1yOMF+QVFU= github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1 h1:LSv6jOIn/yEsGLeL4TLggsLA+I+XbuZ8sKmUIEWKrzI= 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/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= From a2ad30cc3fc769a0fb0c6cf55e03b32f3e147ff5 Mon Sep 17 00:00:00 2001 From: sakisv Date: Wed, 27 May 2026 17:55:53 +0300 Subject: [PATCH 2/2] Add tests for cloudwatchlogs --- aws/cloudwatchlogs_test.go | 110 +++++++++++++++++++++++++++++++++++++ aws/helpers_test.go | 9 +++ 2 files changed, 119 insertions(+) create mode 100644 aws/cloudwatchlogs_test.go diff --git a/aws/cloudwatchlogs_test.go b/aws/cloudwatchlogs_test.go new file mode 100644 index 0000000..273a89a --- /dev/null +++ b/aws/cloudwatchlogs_test.go @@ -0,0 +1,110 @@ +package aws_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/google/go-cmp/cmp" + "github.com/noclickops/aws" + "github.com/noclickops/common" +) + +func getMockedCloudwatchLogsService(mock *mockCloudwatchLogsClient) aws.NoclickopsCloudwatchLogsService { + return aws.NoclickopsCloudwatchLogsService{ + Clients: []aws.NoclickopsCloudwatchLogsClient{ + { + Client: mock, + ClientMeta: aws.ClientMeta{Region: "eu-west-1"}, + }, + }, + ServiceMeta: common.ServiceMeta{Global: false, ServiceName: "cloudwatchlogs"}, + } +} + +func TestGetLogGroups_BasicCase(t *testing.T) { + mock := &mockCloudwatchLogsClient{ + listLogGroupsFn: func(_ context.Context, _ *cloudwatchlogs.ListLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListLogGroupsOutput, error) { + return &cloudwatchlogs.ListLogGroupsOutput{ + LogGroups: []types.LogGroupSummary{ + {LogGroupArn: ptr("arn:aws:logs:eu-west-1:123456789012:log-group:group-1:*"), LogGroupName: ptr("group-1")}, + {LogGroupArn: ptr("arn:aws:logs:eu-west-1:123456789012:log-group:group-2:*"), LogGroupName: ptr("group-2")}, + }, + }, nil + }, + } + svc := getMockedCloudwatchLogsService(mock) + got := svc.GetLogGroups() + expected := []common.Resource{ + {Arn: "arn:aws:logs:eu-west-1:123456789012:log-group:group-1:*", TerraformID: "group-1", ResourceType: common.Cloudwatchlogs_log_group, Region: "eu-west-1"}, + {Arn: "arn:aws:logs:eu-west-1:123456789012:log-group:group-2:*", TerraformID: "group-2", ResourceType: common.Cloudwatchlogs_log_group, Region: "eu-west-1"}, + } + if diff := cmp.Diff(got, expected); diff != "" { + t.Errorf("mismatch (-got +want):\n%s", diff) + } +} + +func TestGetLogGroups_NoLogGroups(t *testing.T) { + mock := &mockCloudwatchLogsClient{ + listLogGroupsFn: func(_ context.Context, _ *cloudwatchlogs.ListLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListLogGroupsOutput, error) { + return &cloudwatchlogs.ListLogGroupsOutput{}, nil + }, + } + svc := getMockedCloudwatchLogsService(mock) + got := svc.GetLogGroups() + if len(got) != 0 { + t.Errorf("expected empty result, got %v", got) + } +} + +func TestGetLogGroups_PaginationFollowed(t *testing.T) { + callCount := 0 + mock := &mockCloudwatchLogsClient{ + listLogGroupsFn: func(_ context.Context, params *cloudwatchlogs.ListLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListLogGroupsOutput, error) { + callCount++ + if callCount == 1 { + return &cloudwatchlogs.ListLogGroupsOutput{ + LogGroups: []types.LogGroupSummary{ + {LogGroupArn: ptr("arn:aws:logs:eu-west-1:123456789012:log-group:group-1:*"), LogGroupName: ptr("group-1")}, + }, + NextToken: ptr("next-page-token"), + }, nil + } + if params.NextToken == nil || *params.NextToken != "next-page-token" { + return nil, fmt.Errorf("wrong NextToken: expected 'next-page-token', got %v", params.NextToken) + } + return &cloudwatchlogs.ListLogGroupsOutput{ + LogGroups: []types.LogGroupSummary{ + {LogGroupArn: ptr("arn:aws:logs:eu-west-1:123456789012:log-group:group-2:*"), LogGroupName: ptr("group-2")}, + }, + }, nil + }, + } + svc := getMockedCloudwatchLogsService(mock) + got := svc.GetLogGroups() + expected := []common.Resource{ + {Arn: "arn:aws:logs:eu-west-1:123456789012:log-group:group-1:*", TerraformID: "group-1", ResourceType: common.Cloudwatchlogs_log_group, Region: "eu-west-1"}, + {Arn: "arn:aws:logs:eu-west-1:123456789012:log-group:group-2:*", TerraformID: "group-2", ResourceType: common.Cloudwatchlogs_log_group, 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 ListLogGroups, got %d", callCount) + } +} + +func TestGetLogGroups_ErrorIsSkipped(t *testing.T) { + mock := &mockCloudwatchLogsClient{ + listLogGroupsFn: func(_ context.Context, _ *cloudwatchlogs.ListLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListLogGroupsOutput, error) { + return nil, fmt.Errorf("access denied") + }, + } + svc := getMockedCloudwatchLogsService(mock) + got := svc.GetLogGroups() + 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 3b227b7..06d9f26 100644 --- a/aws/helpers_test.go +++ b/aws/helpers_test.go @@ -5,6 +5,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/ec2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" @@ -196,3 +197,11 @@ type mockLambdaClient struct { func (m *mockLambdaClient) ListFunctions(ctx context.Context, params *lambda.ListFunctionsInput, optFns ...func(*lambda.Options)) (*lambda.ListFunctionsOutput, error) { return m.listFunctionsFn(ctx, params, optFns...) } + +type mockCloudwatchLogsClient struct { + listLogGroupsFn func(ctx context.Context, params *cloudwatchlogs.ListLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListLogGroupsOutput, error) +} + +func (m *mockCloudwatchLogsClient) ListLogGroups(ctx context.Context, params *cloudwatchlogs.ListLogGroupsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.ListLogGroupsOutput, error) { + return m.listLogGroupsFn(ctx, params, optFns...) +}