diff --git a/internal/mackerel/aws_integration.go b/internal/mackerel/aws_integration.go new file mode 100644 index 0000000..df7ec81 --- /dev/null +++ b/internal/mackerel/aws_integration.go @@ -0,0 +1,321 @@ +package mackerel + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/mackerelio/mackerel-client-go" +) + +type AWSIntegrationModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Memo types.String `tfsdk:"memo"` + Key types.String `tfsdk:"key"` + SecretKey types.String `tfsdk:"secret_key"` + RoleARN types.String `tfsdk:"role_arn"` + ExternalID types.String `tfsdk:"external_id"` + Region types.String `tfsdk:"region"` + IncludedTags types.String `tfsdk:"included_tags"` + ExcludedTags types.String `tfsdk:"excluded_tags"` + + EC2 AWSIntegrationServiceWithRetireAutomaticallyOpt `tfsdk:"ec2"` + ELB AWSIntegrationServiceOpt `tfsdk:"elb"` + ALB AWSIntegrationServiceOpt `tfsdk:"alb"` + NLB AWSIntegrationServiceOpt `tfsdk:"nlb"` + RDS AWSIntegrationServiceWithRetireAutomaticallyOpt `tfsdk:"rds"` + Redshift AWSIntegrationServiceOpt `tfsdk:"redshift"` + ElastiCache AWSIntegrationServiceWithRetireAutomaticallyOpt `tfsdk:"elasticache"` + SQS AWSIntegrationServiceOpt `tfsdk:"sqs"` + Lambda AWSIntegrationServiceOpt `tfsdk:"lambda"` + DynamoDB AWSIntegrationServiceOpt `tfsdk:"dynamodb"` + CloudFront AWSIntegrationServiceOpt `tfsdk:"cloudfront"` + APIGateway AWSIntegrationServiceOpt `tfsdk:"api_gateway"` + Kinesis AWSIntegrationServiceOpt `tfsdk:"kinesis"` + S3 AWSIntegrationServiceOpt `tfsdk:"s3"` + ES AWSIntegrationServiceOpt `tfsdk:"es"` + ECSCluster AWSIntegrationServiceOpt `tfsdk:"ecs_cluster"` + SES AWSIntegrationServiceOpt `tfsdk:"ses"` + States AWSIntegrationServiceOpt `tfsdk:"states"` + EFS AWSIntegrationServiceOpt `tfsdk:"efs"` + Firehose AWSIntegrationServiceOpt `tfsdk:"firehose"` + Batch AWSIntegrationServiceOpt `tfsdk:"batch"` + WAF AWSIntegrationServiceOpt `tfsdk:"waf"` + Billing AWSIntegrationServiceOpt `tfsdk:"billing"` + Route53 AWSIntegrationServiceOpt `tfsdk:"route53"` + Connect AWSIntegrationServiceOpt `tfsdk:"connect"` + DocDB AWSIntegrationServiceOpt `tfsdk:"docdb"` + CodeBuild AWSIntegrationServiceOpt `tfsdk:"codebuild"` +} + +type AWSIntegrationService struct { + Enable types.Bool `tfsdk:"enable"` + Role types.String `tfsdk:"role"` + ExcludedMetrics []string `tfsdk:"excluded_metrics"` + RetireAutomatically types.Bool `tfsdk:"-"` +} + +type AWSIntegrationServiceOpt []AWSIntegrationService // length <= 1 + +type AWSIntegrationServiceWithRetireAutomatically struct { + Enable types.Bool `tfsdk:"enable"` + Role types.String `tfsdk:"role"` + ExcludedMetrics []string `tfsdk:"excluded_metrics"` + RetireAutomatically types.Bool `tfsdk:"retire_automatically"` +} + +type AWSIntegrationServiceWithRetireAutomaticallyOpt []AWSIntegrationServiceWithRetireAutomatically // length <= 1 + +func readAWSIntegration(client *Client, id string) (*AWSIntegrationModel, error) { + mackerelAWSIntegration, err := client.FindAWSIntegration(id) + if err != nil { + return nil, err + } + return newAWSIntegrationModel(*mackerelAWSIntegration) +} + +func (m *AWSIntegrationModel) Create(_ context.Context, client *Client) error { + newIntegration, err := client.CreateAWSIntegration(m.createParam()) + if err != nil { + return err + } + + m.ID = types.StringValue(newIntegration.ID) + return nil +} + +func (m *AWSIntegrationModel) Read(_ context.Context, client *Client) error { + integration, err := readAWSIntegration(client, m.ID.ValueString()) + if err != nil { + return err + } + + m.merge(*integration) + return nil +} + +func (m *AWSIntegrationModel) Update(_ context.Context, client *Client) error { + if _, err := client.UpdateAWSIntegration(m.ID.ValueString(), m.updateParam()); err != nil { + return err + } + return nil +} + +func (m *AWSIntegrationModel) Delete(_ context.Context, client *Client) error { + if _, err := client.DeleteAWSIntegration(m.ID.ValueString()); err != nil { + return err + } + return nil +} + +func newAWSIntegrationModel(aws mackerel.AWSIntegration) (*AWSIntegrationModel, error) { + model := &AWSIntegrationModel{ + ID: types.StringValue(aws.ID), + Name: types.StringValue(aws.Name), + Memo: types.StringValue(aws.Memo), + Key: types.StringValue(aws.Key), + RoleARN: types.StringValue(aws.RoleArn), + ExternalID: types.StringValue(aws.ExternalID), + Region: types.StringValue(aws.Region), + IncludedTags: types.StringValue(aws.IncludedTags), + ExcludedTags: types.StringValue(aws.ExcludedTags), + } + + svcs := make(map[string]AWSIntegrationService, len(aws.Services)) + for name, awsService := range aws.Services { + if /* nil */ !awsService.Enable && + awsService.Role == nil && + len(awsService.ExcludedMetrics) == 0 && + len(awsService.IncludedMetrics) == 0 && + !awsService.RetireAutomatically { + continue + } + if len(awsService.IncludedMetrics) != 0 { + return nil, fmt.Errorf("%s: IncludedMetrics is not supported.", name) + } + + svcs[name] = AWSIntegrationService{ + Enable: types.BoolValue(awsService.Enable), + Role: types.StringPointerValue(awsService.Role), + ExcludedMetrics: awsService.ExcludedMetrics, + RetireAutomatically: types.BoolValue(awsService.RetireAutomatically), + } + } + model.each(func(name string, _ *AWSIntegrationService) *AWSIntegrationService { + svc, ok := svcs[name] + if ok { + delete(svcs, name) + return &svc + } else { + return nil + } + }) + if len(svcs) != 0 { + unsupportedServiceNames := make([]string, 0, len(svcs)) + for name := range svcs { + unsupportedServiceNames = append(unsupportedServiceNames, name) + } + slices.SortStableFunc(unsupportedServiceNames, strings.Compare) + return nil, fmt.Errorf("unsupported AWS integration service(s): %s", + strings.Join(unsupportedServiceNames, ",")) + } + + return model, nil +} + +func (m *AWSIntegrationModel) createParam() *mackerel.CreateAWSIntegrationParam { + mackerelServices := make(map[string]*mackerel.AWSIntegrationService) + m.each(func(name string, service *AWSIntegrationService) *AWSIntegrationService { + var mackerelService mackerel.AWSIntegrationService + if service != nil { + mackerelService = mackerel.AWSIntegrationService{ + Enable: service.Enable.ValueBool(), + Role: nil, + ExcludedMetrics: nil, + IncludedMetrics: nil, + RetireAutomatically: service.RetireAutomatically.ValueBool(), + } + if role := service.Role.ValueString(); role != "" { + mackerelService.Role = &role + } + if service.ExcludedMetrics != nil { + mackerelService.ExcludedMetrics = service.ExcludedMetrics + } else { + mackerelService.ExcludedMetrics = []string{} + } + } else { + mackerelService = mackerel.AWSIntegrationService{ + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + IncludedMetrics: nil, + RetireAutomatically: false, + } + } + mackerelServices[name] = &mackerelService + return service + }) + + return &mackerel.CreateAWSIntegrationParam{ + Name: m.Name.ValueString(), + Memo: m.Memo.ValueString(), + Key: m.Key.ValueString(), + SecretKey: m.SecretKey.ValueString(), + RoleArn: m.RoleARN.ValueString(), + ExternalID: m.ExternalID.ValueString(), + Region: m.Region.ValueString(), + IncludedTags: m.IncludedTags.ValueString(), + ExcludedTags: m.ExcludedTags.ValueString(), + Services: mackerelServices, + } +} + +func (m *AWSIntegrationModel) updateParam() *mackerel.UpdateAWSIntegrationParam { + return (*mackerel.UpdateAWSIntegrationParam)(m.createParam()) +} + +func (m *AWSIntegrationModel) merge(newModel AWSIntegrationModel) { + oldServices := make(map[string]AWSIntegrationService) + m.each(func(name string, service *AWSIntegrationService) *AWSIntegrationService { + if service != nil { + oldServices[name] = *service + } + return service + }) + + newModel.SecretKey = m.SecretKey + newModel.each(func(name string, service *AWSIntegrationService) *AWSIntegrationService { + oldService, ok := oldServices[name] + if !ok { + return service + } + + // If new == nil && old == zero, use old one. + if service == nil { + if !oldService.Enable.ValueBool() && + len(oldService.ExcludedMetrics) == 0 && + oldService.Role.IsNull() && + !oldService.RetireAutomatically.ValueBool() { + return &oldService + } else { + return nil + } + } + + if service.Role.ValueString() == "" && oldService.Role.ValueString() == "" { + service.Role = oldService.Role + } + if len(service.ExcludedMetrics) == 0 && len(oldService.ExcludedMetrics) == 0 { + service.ExcludedMetrics = oldService.ExcludedMetrics + } + return service + }) + *m = newModel +} + +type awsServiceEachFunc func(name string, service *AWSIntegrationService) *AWSIntegrationService + +// Iterates and updates over services by name +func (m *AWSIntegrationModel) each(fn awsServiceEachFunc) { + m.EC2.each("EC2", fn) + m.ELB.each("ELB", fn) + m.ALB.each("ALB", fn) + m.NLB.each("NLB", fn) + m.RDS.each("RDS", fn) + m.Redshift.each("Redshift", fn) + m.ElastiCache.each("ElastiCache", fn) + m.SQS.each("SQS", fn) + m.Lambda.each("Lambda", fn) + m.DynamoDB.each("DynamoDB", fn) + m.CloudFront.each("CloudFront", fn) + m.APIGateway.each("APIGateway", fn) + m.Kinesis.each("Kinesis", fn) + m.S3.each("S3", fn) + m.ES.each("ES", fn) + m.ECSCluster.each("ECSCluster", fn) + m.SES.each("SES", fn) + m.States.each("States", fn) + m.EFS.each("EFS", fn) + m.Firehose.each("Firehose", fn) + m.Batch.each("Batch", fn) + m.WAF.each("WAF", fn) + m.Billing.each("Billing", fn) + m.Route53.each("Route53", fn) + m.Connect.each("Connect", fn) + m.DocDB.each("DocDB", fn) + m.CodeBuild.each("CodeBuild", fn) +} + +func (s *AWSIntegrationServiceOpt) each(name string, fn awsServiceEachFunc) { + var svc *AWSIntegrationService + if len(*s) != 0 { + svc = &(*s)[0] + } + + newSvc := fn(name, svc) + if newSvc != nil { + *s = []AWSIntegrationService{*newSvc} + } else { + *s = AWSIntegrationServiceOpt{} + } +} + +func (s *AWSIntegrationServiceWithRetireAutomaticallyOpt) each(name string, fn awsServiceEachFunc) { + var svc *AWSIntegrationService + if len(*s) != 0 { + baseSvc := AWSIntegrationService((*s)[0]) + svc = &baseSvc + } + + newSvc := fn(name, svc) + if newSvc != nil { + *s = []AWSIntegrationServiceWithRetireAutomatically{ + AWSIntegrationServiceWithRetireAutomatically(*newSvc), + } + } else { + *s = AWSIntegrationServiceWithRetireAutomaticallyOpt{} + } +} diff --git a/internal/mackerel/aws_integration_test.go b/internal/mackerel/aws_integration_test.go new file mode 100644 index 0000000..b7e0d6d --- /dev/null +++ b/internal/mackerel/aws_integration_test.go @@ -0,0 +1,411 @@ +package mackerel + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/mackerelio/mackerel-client-go" +) + +func Test_AWSIntegration_fromAPI(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + api mackerel.AWSIntegration + model AWSIntegrationModel + wantErr bool + }{ + "basic": { + api: mackerel.AWSIntegration{ + ID: "aaaabbbb", + Name: "aws-integration", + Memo: "This resource is managed by Terraform.", + Key: "", + RoleArn: "arn:aws:iam::11111111:role/MackerelAWSIntegrationRole", + ExternalID: "ccccddddd", + Region: "ap-northeast-1", + IncludedTags: "Name:production-server,Environment:production", + ExcludedTags: "Name:staging-server,Environment:staging", + Services: map[string]*mackerel.AWSIntegrationService{ + "EC2": { + Enable: true, + Role: nil, + RetireAutomatically: true, + ExcludedMetrics: []string{}, + }, + "ELB": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "ALB": { + Enable: true, + Role: ptr("service: role"), + ExcludedMetrics: []string{"alb.request.count", "alb.bytes.processed"}, + }, + "RDS": { + Enable: true, + Role: ptr("service: role"), + ExcludedMetrics: []string{"rds.cpu.used"}, + RetireAutomatically: false, + }, + "Redshift": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "ElastiCache": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + RetireAutomatically: false, + }, + "SQS": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "Lambda": { + Enable: true, + Role: nil, + ExcludedMetrics: []string{}, + }, + "NLB": { + Enable: true, + Role: nil, + ExcludedMetrics: []string{}, + }, + "DynamoDB": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "CloudFront": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "APIGateway": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "Kinesis": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "S3": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "ES": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "ECSCluster": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "SES": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "States": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "EFS": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "Firehose": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "Batch": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "WAF": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "Billing": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "Route53": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "Connect": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "DocDB": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + "CodeBuild": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + // AWS Integration supports Athena, but the terraform provider does not. + "Athena": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + // Unsupported services are ignored when they are empty. + "SomeNewService": { + Enable: false, + Role: nil, + ExcludedMetrics: []string{}, + }, + }, + }, + model: AWSIntegrationModel{ + ID: types.StringValue("aaaabbbb"), + Name: types.StringValue("aws-integration"), + Memo: types.StringValue("This resource is managed by Terraform."), + Key: types.StringValue(""), + SecretKey: types.StringNull(), + RoleARN: types.StringValue("arn:aws:iam::11111111:role/MackerelAWSIntegrationRole"), + ExternalID: types.StringValue("ccccddddd"), + Region: types.StringValue("ap-northeast-1"), + IncludedTags: types.StringValue("Name:production-server,Environment:production"), + ExcludedTags: types.StringValue("Name:staging-server,Environment:staging"), + + EC2: []AWSIntegrationServiceWithRetireAutomatically{{ + Enable: types.BoolValue(true), + ExcludedMetrics: []string{}, + RetireAutomatically: types.BoolValue(true), + }}, + ELB: []AWSIntegrationService{}, + ALB: []AWSIntegrationService{{ + Enable: types.BoolValue(true), + Role: types.StringValue("service: role"), + ExcludedMetrics: []string{"alb.request.count", "alb.bytes.processed"}, + RetireAutomatically: types.BoolValue(false), + }}, + NLB: []AWSIntegrationService{{ + Enable: types.BoolValue(true), + Role: types.StringNull(), + ExcludedMetrics: []string{}, + RetireAutomatically: types.BoolValue(false), + }}, + RDS: []AWSIntegrationServiceWithRetireAutomatically{{ + Enable: types.BoolValue(true), + Role: types.StringValue("service: role"), + ExcludedMetrics: []string{"rds.cpu.used"}, + RetireAutomatically: types.BoolValue(false), + }}, + Redshift: []AWSIntegrationService{}, + ElastiCache: []AWSIntegrationServiceWithRetireAutomatically{}, + SQS: []AWSIntegrationService{}, + Lambda: []AWSIntegrationService{{ + Enable: types.BoolValue(true), + Role: types.StringNull(), + ExcludedMetrics: []string{}, + RetireAutomatically: types.BoolValue(false), + }}, + DynamoDB: []AWSIntegrationService{}, + CloudFront: []AWSIntegrationService{}, + APIGateway: []AWSIntegrationService{}, + Kinesis: []AWSIntegrationService{}, + S3: []AWSIntegrationService{}, + ES: []AWSIntegrationService{}, + ECSCluster: []AWSIntegrationService{}, + SES: []AWSIntegrationService{}, + States: []AWSIntegrationService{}, + EFS: []AWSIntegrationService{}, + Firehose: []AWSIntegrationService{}, + Batch: []AWSIntegrationService{}, + WAF: []AWSIntegrationService{}, + Billing: []AWSIntegrationService{}, + Route53: []AWSIntegrationService{}, + Connect: []AWSIntegrationService{}, + DocDB: []AWSIntegrationService{}, + CodeBuild: []AWSIntegrationService{}, + }, + }, + } + + for name, tt := range cases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + model, err := newAWSIntegrationModel(tt.api) + if (err != nil) != tt.wantErr { + t.Errorf("unexpected error: %+v", err) + } + if err != nil { + return + } + if diff := cmp.Diff(model, &tt.model); diff != "" { + t.Error(diff) + } + }) + } +} + +func Test_AWSIntegration_toAPI(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + model AWSIntegrationModel + param mackerel.CreateAWSIntegrationParam + }{ + "basic": { + model: AWSIntegrationModel{ + ID: types.StringValue("aaaabbbb"), + Name: types.StringValue("aws-integration"), + Memo: types.StringValue("This resource is managed by Terraform."), + Key: types.StringValue(""), + SecretKey: types.StringNull(), + RoleARN: types.StringValue("arn:aws:iam::11111111:role/MackerelAWSIntegrationRole"), + ExternalID: types.StringValue("ccccddddd"), + Region: types.StringValue("ap-northeast-1"), + IncludedTags: types.StringValue("Name:production-server,Environment:production"), + ExcludedTags: types.StringValue("Name:staging-server,Environment:staging"), + + EC2: []AWSIntegrationServiceWithRetireAutomatically{{ + Enable: types.BoolValue(true), + ExcludedMetrics: []string{}, + RetireAutomatically: types.BoolValue(true), + }}, + ELB: []AWSIntegrationService{}, + ALB: []AWSIntegrationService{{ + Enable: types.BoolValue(true), + Role: types.StringValue("service: role"), + ExcludedMetrics: []string{"alb.request.count", "alb.bytes.processed"}, + RetireAutomatically: types.BoolValue(false), + }}, + NLB: []AWSIntegrationService{{ + Enable: types.BoolValue(true), + Role: types.StringNull(), + ExcludedMetrics: []string{}, + RetireAutomatically: types.BoolValue(false), + }}, + RDS: []AWSIntegrationServiceWithRetireAutomatically{{ + Enable: types.BoolValue(true), + Role: types.StringValue("service: role"), + ExcludedMetrics: []string{"rds.cpu.used"}, + RetireAutomatically: types.BoolValue(false), + }}, + Redshift: []AWSIntegrationService{}, + ElastiCache: []AWSIntegrationServiceWithRetireAutomatically{}, + SQS: []AWSIntegrationService{}, + Lambda: []AWSIntegrationService{{ + Enable: types.BoolValue(true), + Role: types.StringNull(), + ExcludedMetrics: []string{}, + RetireAutomatically: types.BoolValue(false), + }}, + DynamoDB: []AWSIntegrationService{}, + CloudFront: []AWSIntegrationService{}, + APIGateway: []AWSIntegrationService{}, + Kinesis: []AWSIntegrationService{}, + S3: []AWSIntegrationService{}, + ES: []AWSIntegrationService{}, + ECSCluster: []AWSIntegrationService{}, + SES: []AWSIntegrationService{}, + States: []AWSIntegrationService{}, + EFS: []AWSIntegrationService{}, + Firehose: []AWSIntegrationService{}, + Batch: []AWSIntegrationService{}, + WAF: []AWSIntegrationService{}, + Billing: []AWSIntegrationService{}, + Route53: []AWSIntegrationService{}, + Connect: []AWSIntegrationService{}, + DocDB: []AWSIntegrationService{}, + CodeBuild: []AWSIntegrationService{}, + }, + param: mackerel.CreateAWSIntegrationParam{ + Name: "aws-integration", + Memo: "This resource is managed by Terraform.", + Key: "", + SecretKey: "", + RoleArn: "arn:aws:iam::11111111:role/MackerelAWSIntegrationRole", + ExternalID: "ccccddddd", + Region: "ap-northeast-1", + IncludedTags: "Name:production-server,Environment:production", + ExcludedTags: "Name:staging-server,Environment:staging", + Services: map[string]*mackerel.AWSIntegrationService{ + "EC2": { + Enable: true, + ExcludedMetrics: []string{}, + RetireAutomatically: true, + }, + "ELB": {ExcludedMetrics: []string{}}, + "ALB": { + Enable: true, + Role: ptr("service: role"), + ExcludedMetrics: []string{"alb.request.count", "alb.bytes.processed"}, + }, + "NLB": { + Enable: true, + ExcludedMetrics: []string{}, + }, + "RDS": { + Enable: true, + Role: ptr("service: role"), + ExcludedMetrics: []string{"rds.cpu.used"}, + }, + "Redshift": {ExcludedMetrics: []string{}}, + "ElastiCache": {ExcludedMetrics: []string{}}, + "SQS": {ExcludedMetrics: []string{}}, + "Lambda": { + Enable: true, + ExcludedMetrics: []string{}, + }, + "DynamoDB": {ExcludedMetrics: []string{}}, + "CloudFront": {ExcludedMetrics: []string{}}, + "APIGateway": {ExcludedMetrics: []string{}}, + "Kinesis": {ExcludedMetrics: []string{}}, + "S3": {ExcludedMetrics: []string{}}, + "ES": {ExcludedMetrics: []string{}}, + "ECSCluster": {ExcludedMetrics: []string{}}, + "SES": {ExcludedMetrics: []string{}}, + "States": {ExcludedMetrics: []string{}}, + "EFS": {ExcludedMetrics: []string{}}, + "Firehose": {ExcludedMetrics: []string{}}, + "Batch": {ExcludedMetrics: []string{}}, + "WAF": {ExcludedMetrics: []string{}}, + "Billing": {ExcludedMetrics: []string{}}, + "Route53": {ExcludedMetrics: []string{}}, + "Connect": {ExcludedMetrics: []string{}}, + "DocDB": {ExcludedMetrics: []string{}}, + "CodeBuild": {ExcludedMetrics: []string{}}, + }, + }, + }, + } + + for name, tt := range cases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + param := tt.model.createParam() + if diff := cmp.Diff(param, &tt.param); diff != "" { + t.Error(diff) + } + }) + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 95f709d..7f7cbb9 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -84,6 +84,7 @@ func (m *mackerelProvider) Configure(ctx context.Context, req provider.Configure func (m *mackerelProvider) Resources(context.Context) []func() resource.Resource { return []func() resource.Resource{ NewMackerelAlertGroupSettingResource, + NewMackerelAWSIntegrationResource, NewMackerelChannelResource, NewMackerelDashboardResource, NewMackerelDowntimeResource, diff --git a/internal/provider/resource_mackerel_aws_integration.go b/internal/provider/resource_mackerel_aws_integration.go new file mode 100644 index 0000000..ebda102 --- /dev/null +++ b/internal/provider/resource_mackerel_aws_integration.go @@ -0,0 +1,273 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/mackerelio-labs/terraform-provider-mackerel/internal/mackerel" +) + +var ( + _ resource.Resource = (*mackerelAWSIntegrationResource)(nil) + _ resource.ResourceWithConfigure = (*mackerelAWSIntegrationResource)(nil) + _ resource.ResourceWithImportState = (*mackerelAWSIntegrationResource)(nil) +) + +func NewMackerelAWSIntegrationResource() resource.Resource { + return &mackerelAWSIntegrationResource{} +} + +type mackerelAWSIntegrationResource struct { + Client *mackerel.Client +} + +func (_ *mackerelAWSIntegrationResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_aws_integration" +} + +func (_ *mackerelAWSIntegrationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemaAWSIntegrationResource() +} + +func (r *mackerelAWSIntegrationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := retrieveClient(ctx, req.ProviderData) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + r.Client = client +} + +func (r *mackerelAWSIntegrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data mackerel.AWSIntegrationModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := data.Create(ctx, r.Client); err != nil { + resp.Diagnostics.AddError( + "Unable to create aws integration settings", + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *mackerelAWSIntegrationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data mackerel.AWSIntegrationModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := data.Read(ctx, r.Client); err != nil { + resp.Diagnostics.AddError( + "Unable to read aws integration settings", + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *mackerelAWSIntegrationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data mackerel.AWSIntegrationModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := data.Update(ctx, r.Client); err != nil { + resp.Diagnostics.AddError( + "Unable to update aws integration settings", + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *mackerelAWSIntegrationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data mackerel.AWSIntegrationModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := data.Delete(ctx, r.Client); err != nil { + resp.Diagnostics.AddError( + "Unable to delete aws integration settings", + err.Error(), + ) + return + } +} + +func (_ *mackerelAWSIntegrationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func schemaAWSIntegrationResource() schema.Schema { + serviceSchema := schema.SetNestedBlock{ + Validators: []validator.Set{ + setvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "enable": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "role": schema.StringAttribute{ + Optional: true, + }, + "excluded_metrics": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + }, + }, + }, + } + serviceSchemaWithRetireAutomatically := schema.SetNestedBlock{ + Validators: []validator.Set{ + setvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "enable": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "role": schema.StringAttribute{ + Optional: true, + }, + "excluded_metrics": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + }, + "retire_automatically": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + } + + services := map[string]schema.Block{ + "ec2": serviceSchemaWithRetireAutomatically, + "elb": serviceSchema, + "alb": serviceSchema, + "nlb": serviceSchema, + "rds": serviceSchemaWithRetireAutomatically, + "redshift": serviceSchema, + "elasticache": serviceSchemaWithRetireAutomatically, + "sqs": serviceSchema, + "lambda": serviceSchema, + "dynamodb": serviceSchema, + "cloudfront": serviceSchema, + "api_gateway": serviceSchema, + "kinesis": serviceSchema, + "s3": serviceSchema, + "es": serviceSchema, + "ecs_cluster": serviceSchema, + "ses": serviceSchema, + "states": serviceSchema, + "efs": serviceSchema, + "firehose": serviceSchema, + "batch": serviceSchema, + "waf": serviceSchema, + "billing": serviceSchema, + "route53": serviceSchema, + "connect": serviceSchema, + "docdb": serviceSchema, + "codebuild": serviceSchema, + } + + schema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), // immutable + }, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "memo": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "key": schema.StringAttribute{ + Optional: true, + Sensitive: true, + Computed: true, + Default: stringdefault.StaticString(""), + Validators: []validator.String{ + // With Access Key, secret access key is need too. + stringvalidator.AlsoRequires(path.MatchRoot("secret_key")), + }, + }, + "secret_key": schema.StringAttribute{ + Optional: true, + Sensitive: true, + Computed: true, + Default: stringdefault.StaticString(""), + Validators: []validator.String{ + // Secret access key cannot be set alone + stringvalidator.AlsoRequires(path.MatchRoot("key")), + }, + }, + "role_arn": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "external_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + Validators: []validator.String{ + // External ID cannot be set alone + stringvalidator.AlsoRequires(path.MatchRoot("role_arn")), + }, + }, + "region": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "included_tags": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "excluded_tags": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + }, + Blocks: services, + } + return schema +} diff --git a/mackerel/provider.go b/mackerel/provider.go index 5e39dc5..3e8fe42 100644 --- a/mackerel/provider.go +++ b/mackerel/provider.go @@ -83,6 +83,7 @@ func protoV5ProviderServer(provider *schema.Provider) tfprotov5.ProviderServer { // Resources delete(provider.ResourcesMap, "mackerel_alert_group_setting") + delete(provider.ResourcesMap, "mackerel_aws_integration") delete(provider.ResourcesMap, "mackerel_channel") delete(provider.ResourcesMap, "mackerel_dashboard") delete(provider.ResourcesMap, "mackerel_downtime") diff --git a/mackerel/resource_mackerel_aws_integration_test.go b/mackerel/resource_mackerel_aws_integration_test.go index 60adfd7..7d4c280 100644 --- a/mackerel/resource_mackerel_aws_integration_test.go +++ b/mackerel/resource_mackerel_aws_integration_test.go @@ -303,7 +303,6 @@ resource "mackerel_aws_integration" "foo" { ec2 { enable = true - role = "" excluded_metrics = [] retire_automatically = true } @@ -357,7 +356,6 @@ resource "mackerel_aws_integration" "foo" { ec2 { enable = true - role = "" excluded_metrics = [] retire_automatically = true }