From 9efea4e878e6cef17f2319ba8c87fae883f488e3 Mon Sep 17 00:00:00 2001 From: Ricardo Ferraz Leal <56277891+ricleal-fugue@users.noreply.github.com> Date: Thu, 21 Apr 2022 16:08:34 -0400 Subject: [PATCH] [cloud 139] new resources (#105) * added aws_ecs_task * added aws_iam_credential_report * add resources to provider list Co-authored-by: Gil Browdy --- internal/service/ecs/service_package_gen.go | 8 + internal/service/ecs/task.go | 230 ++++++++++++++++ internal/service/iam/credential_report.go | 274 ++++++++++++++++++++ internal/service/iam/service_package_gen.go | 5 + 4 files changed, 517 insertions(+) create mode 100644 internal/service/ecs/task.go create mode 100644 internal/service/iam/credential_report.go diff --git a/internal/service/ecs/service_package_gen.go b/internal/service/ecs/service_package_gen.go index d9434c75f6c..f45f8646f4e 100644 --- a/internal/service/ecs/service_package_gen.go +++ b/internal/service/ecs/service_package_gen.go @@ -39,6 +39,14 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac Factory: DataSourceService, TypeName: "aws_ecs_service", }, + { + Factory: ResourceTask, + TypeName: "aws_ecs_task", + Name: "Task", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, { Factory: DataSourceTaskDefinition, TypeName: "aws_ecs_task_definition", diff --git a/internal/service/ecs/task.go b/internal/service/ecs/task.go new file mode 100644 index 00000000000..00af2618e0e --- /dev/null +++ b/internal/service/ecs/task.go @@ -0,0 +1,230 @@ +package ecs + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecs" + "github.com/aws/aws-sdk-go-v2/service/ecs/types" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" +) + +// @SDKDataSource("aws_ecs_task", name="Task") +// @Tags(identifierAttribute="arn") +func ResourceTask() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsEcsTaskUpdate, + ReadWithoutTimeout: resourceAwsEcsTaskRead, + UpdateWithoutTimeout: resourceAwsEcsTaskUpdate, + DeleteWithoutTimeout: resourceAwsEcsTaskDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "cluster_arn": { + Type: schema.TypeString, + Computed: true, + }, + + "task_definition_arn": { + Type: schema.TypeString, + Computed: true, + }, + "group": { + Type: schema.TypeString, + Computed: true, + }, + "launch_type": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "containers": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "container_arn": { + Type: schema.TypeString, + Computed: true, + }, + "task_arn": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "network_interfaces": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "attachment_id": { + Type: schema.TypeString, + Computed: true, + }, + "private_ipv4_address": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "attachments": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "details": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "health_status": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceAwsEcsTaskRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + client := meta.(*conns.AWSClient).ECSClient(ctx) + + taskArn := d.Get("arn").(string) + clusterArn := d.Get("cluster_arn").(string) + log.Printf("[DEBUG] Reading task %s", d.Id()) + out, err := client.DescribeTasks(ctx, &ecs.DescribeTasksInput{ + Tasks: []string{taskArn}, + Cluster: aws.String(clusterArn), + }) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading ECS Task (%s): %s", d.Id(), err) + } + + task := out.Tasks[0] // DescribeTasks returns a list but we're only passing in a single task ARN + + d.SetId(taskArn) + d.Set("arn", &taskArn) + d.Set("cluster_arn", task.ClusterArn) + d.Set("task_definition_arn", task.TaskDefinitionArn) + d.Set("group", task.Group) + d.Set("launch_type", task.LaunchType) + d.Set("health_status", task.HealthStatus) + + if err := d.Set("containers", flattenEcsTaskContainers(task.Containers)); err != nil { + return sdkdiag.AppendErrorf(diags, "reading ECS Task (%s): failed to set flattened ECS task containers: %s", d.Id(), err) + } + + if err := d.Set("attachments", flattenEcsTaskAttachments(task.Attachments)); err != nil { + return sdkdiag.AppendErrorf(diags, "reading ECS Task (%s): failed to set flattened ECS task attachments: %s", d.Id(), err) + } + + return nil +} + +func resourceAwsEcsTaskUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceAwsEcsTaskDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func flattenEcsTaskContainers(config []types.Container) []map[string]interface{} { + containers := make([]map[string]interface{}, 0, len(config)) + + for _, raw := range config { + item := make(map[string]interface{}) + item["container_arn"] = *raw.ContainerArn + item["task_arn"] = *raw.TaskArn + item["network_interfaces"] = flattenEcsTaskContainerNetworkInterfaces(raw.NetworkInterfaces) + + containers = append(containers, item) + } + + return containers +} + +func flattenEcsTaskContainerNetworkInterfaces(config []types.NetworkInterface) []map[string]interface{} { + networkInterfaces := make([]map[string]interface{}, 0, len(config)) + + for _, raw := range config { + item := make(map[string]interface{}) + item["attachment_id"] = *raw.AttachmentId + item["private_ipv4_address"] = *raw.PrivateIpv4Address + + networkInterfaces = append(networkInterfaces, item) + } + + return networkInterfaces +} + +func flattenEcsTaskAttachments(config []types.Attachment) []map[string]interface{} { + attachments := make([]map[string]interface{}, 0, len(config)) + + for _, raw := range config { + item := make(map[string]interface{}) + item["id"] = *raw.Id + item["type"] = *raw.Type + item["details"] = flattenEcsTaskAttachmentDetails(raw.Details) + + attachments = append(attachments, item) + } + + return attachments +} + +func flattenEcsTaskAttachmentDetails(config []types.KeyValuePair) []map[string]interface{} { + details := make([]map[string]interface{}, 0, len(config)) + + for _, raw := range config { + item := make(map[string]interface{}) + item["name"] = *raw.Name + item["value"] = *raw.Value + + details = append(details, item) + } + + return details +} diff --git a/internal/service/iam/credential_report.go b/internal/service/iam/credential_report.go new file mode 100644 index 00000000000..666dbb2fdea --- /dev/null +++ b/internal/service/iam/credential_report.go @@ -0,0 +1,274 @@ +// This is a Fugue-specific read-only resource type that just grabs all +// information from the AWS IAM Credential Report. + +package iam + +import ( + "bytes" + "context" + "log" + "regexp" + "time" + + "encoding/csv" + + "github.com/aws/aws-sdk-go/aws/awserr" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" +) + +// @SDKDataSource("aws_iam_credential_report", name="Credential Report") +func ResourceCredentialReport() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsIamCredentialReportUpdate, + ReadWithoutTimeout: resourceAwsIamCredentialReportRead, + UpdateWithoutTimeout: resourceAwsIamCredentialReportUpdate, + DeleteWithoutTimeout: resourceAwsIamCredentialReportDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "report": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "user": { + Type: schema.TypeString, + Computed: true, + }, + "password_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "password_last_used": { + Type: schema.TypeString, + Computed: true, + }, + "password_last_changed": { + Type: schema.TypeString, + Computed: true, + }, + "mfa_active": { + Type: schema.TypeBool, + Computed: true, + }, + "mfa_virtual": { + Type: schema.TypeBool, + Computed: true, + }, + "access_keys": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "active": { + Type: schema.TypeBool, + Computed: true, + }, + "last_used_date": { + Type: schema.TypeString, + Computed: true, + }, + "last_rotated": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func resourceAwsIamCredentialReportUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // RM-2550: (eric-luminal) Fugue sets the ID of this resource as something + // more human readable + d.SetId("CredentialReport") + return resourceAwsIamCredentialReportRead(ctx, d, meta) +} + +func resourceAwsIamCredentialReportRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + iamclient := meta.(*conns.AWSClient).IAMClient(ctx) + + // Send a request to generate a credential report. + if _, err := iamclient.GenerateCredentialReport(ctx, nil); err != nil { + return sdkdiag.AppendErrorf(diags, "reading ECS credential report (%s): %s", d.Id(), err) + } + + err := retry.RetryContext(ctx, time.Duration(3)*time.Minute, func() *retry.RetryError { + // Prepare a request to actually get the credential report. + getReportOutput, err := iamclient.GetCredentialReport(ctx, nil) + if err != nil { + if awserr, ok := err.(awserr.Error); ok { + switch awserr.Code() { + // Retry if it is still being generated. + case "ReportInProgress": + return resource.RetryableError(awserr) + } + } + return resource.NonRetryableError(err) + } + + // Parse report. + log.Printf("[INFO]: Credential Report Content: %s", string(getReportOutput.Content)) + report, err := parseCsvCredentialReport(getReportOutput.Content) + if err != nil { + return resource.NonRetryableError(err) + } + + // Retrieve info about virtual MFA devices. + listMfaOutput, err := iamclient.ListVirtualMFADevices(ctx, nil) + if err != nil { + return resource.NonRetryableError(err) + } + + // Run through the virtual MFA devices to create a set of users that + // have them enabled. The user names are constructed to match those in + // the credential report. + accountsWithVirtualMfa := map[string]bool{} + serial, _ := regexp.Compile("^arn:aws:iam::[0-9]+:mfa/(.*)$") + for _, virtualMfa := range listMfaOutput.VirtualMFADevices { + match := serial.FindStringSubmatch(*virtualMfa.SerialNumber) + if match != nil && len(match) > 1 { + accountName := match[1] + if accountName == "root-account-mfa-device" { + accountName = "" + } + + accountsWithVirtualMfa[accountName] = true + } + } + + // Extend the report with the virtual MFA info. + for _, row := range report { + if _, ok := accountsWithVirtualMfa[row.User]; ok { + row.MfaVirtual = true + } + } + + // Store report in the resource state. + d.Set("report", flattenCredentialReport(report)) + + return nil + }) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading ECS credential report details (%s): %s", d.Id(), err) + } + + return nil +} + +func resourceAwsIamCredentialReportDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +type CredentialReport = []*ReportRow + +type ReportRow struct { + User string + PasswordEnabled bool + PasswordLastUsed string + PasswordLastChanged string + MfaActive bool + MfaVirtual bool + AccessKeys []AccessKey +} + +type AccessKey struct { + Active bool + LastUsedDate string + LastRotated string +} + +func parseCsvCredentialReport(content []byte) (CredentialReport, error) { + reader := csv.NewReader(bytes.NewReader(content)) + + // Parse header. + header := map[string]int{} + headerLine, err := reader.Read() + if err != nil { + return nil, err + } + for i, k := range headerLine { + header[k] = i + } + + // Parse rows into CSV. + lines, err := reader.ReadAll() + if err != nil { + return nil, err + } + + // Copy rows into the datatype. + rows := make([]*ReportRow, len(lines)) + for i, line := range lines { + rows[i] = &ReportRow{ + User: line[header["user"]], + PasswordEnabled: parseCsvBool(line[header["password_enabled"]]), + PasswordLastUsed: line[header["password_last_used"]], + PasswordLastChanged: line[header["password_last_changed"]], + MfaActive: parseCsvBool(line[header["mfa_active"]]), + AccessKeys: []AccessKey{ + { + Active: parseCsvBool(line[header["access_key_1_active"]]), + LastUsedDate: line[header["access_key_1_last_used_date"]], + LastRotated: line[header["access_key_1_last_rotated"]], + }, + { + Active: parseCsvBool(line[header["access_key_2_active"]]), + LastUsedDate: line[header["access_key_2_last_used_date"]], + LastRotated: line[header["access_key_2_last_rotated"]], + }, + }, + } + } + + return rows, nil +} + +func parseCsvBool(csv string) bool { + return csv == "true" +} + +func flattenCredentialReport(report CredentialReport) []map[string]interface{} { + out := make([]map[string]interface{}, 0) + for _, row := range report { + m := map[string]interface{}{ + "user": row.User, + "password_enabled": row.PasswordEnabled, + "password_last_used": row.PasswordLastUsed, + "password_last_changed": row.PasswordLastChanged, + "mfa_active": row.MfaActive, + "mfa_virtual": row.MfaVirtual, + "access_keys": flattenCredentialReportAccessKeys(row.AccessKeys), + } + out = append(out, m) + } + return out +} + +func flattenCredentialReportAccessKeys(accessKeys []AccessKey) []map[string]interface{} { + out := make([]map[string]interface{}, 0) + for _, accessKey := range accessKeys { + m := map[string]interface{}{ + "active": accessKey.Active, + "last_used_date": accessKey.LastUsedDate, + "last_rotated": accessKey.LastRotated, + } + out = append(out, m) + } + return out +} diff --git a/internal/service/iam/service_package_gen.go b/internal/service/iam/service_package_gen.go index 1e19e3281c0..1e58ead3b25 100644 --- a/internal/service/iam/service_package_gen.go +++ b/internal/service/iam/service_package_gen.go @@ -34,6 +34,11 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac TypeName: "aws_iam_account_alias", Name: "Account Alias", }, + { + Factory: ResourceCredentialReport, + TypeName: "aws_iam_credential_report", + Name: "Credential Report", + }, { Factory: dataSourceGroup, TypeName: "aws_iam_group",