diff --git a/.changelog/19670.txt b/.changelog/19670.txt new file mode 100644 index 00000000000..aaa44144676 --- /dev/null +++ b/.changelog/19670.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_ecs_task_definition: Add plan time validation for `family` and `requires_compatibilities`. +``` + +```release-note:enhancement +resource/aws_ecs_task_definition: Add support for `fsx_windows_file_server_volume_configuration`. +``` \ No newline at end of file diff --git a/aws/resource_aws_ecs_task_definition.go b/aws/resource_aws_ecs_task_definition.go index df7e4f04967..243d5d249e3 100644 --- a/aws/resource_aws_ecs_task_definition.go +++ b/aws/resource_aws_ecs_task_definition.go @@ -2,12 +2,14 @@ package aws import ( "bytes" + "encoding/json" "fmt" "log" "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/private/protocol/json/jsonutil" "github.com/aws/aws-sdk-go/service/ecs" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" @@ -61,9 +63,10 @@ func resourceAwsEcsTaskDefinition() *schema.Resource { }, "family": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 255), }, "revision": { @@ -115,16 +118,11 @@ func resourceAwsEcsTaskDefinition() *schema.Resource { }, "network_mode": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - ecs.NetworkModeBridge, - ecs.NetworkModeHost, - ecs.NetworkModeAwsvpc, - ecs.NetworkModeNone, - }, false), + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ecs.NetworkMode_Values(), false), }, "volume": { @@ -153,14 +151,11 @@ func resourceAwsEcsTaskDefinition() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "scope": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - ecs.ScopeShared, - ecs.ScopeTask, - }, false), + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ecs.Scope_Values(), false), }, "autoprovision": { Type: schema.TypeBool, @@ -207,13 +202,10 @@ func resourceAwsEcsTaskDefinition() *schema.Resource { Default: "/", }, "transit_encryption": { - Type: schema.TypeString, - ForceNew: true, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - ecs.EFSTransitEncryptionEnabled, - ecs.EFSTransitEncryptionDisabled, - }, false), + Type: schema.TypeString, + ForceNew: true, + Optional: true, + ValidateFunc: validation.StringInSlice(ecs.EFSTransitEncryption_Values(), false), }, "transit_encryption_port": { Type: schema.TypeInt, @@ -234,13 +226,51 @@ func resourceAwsEcsTaskDefinition() *schema.Resource { Optional: true, }, "iam": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + ValidateFunc: validation.StringInSlice(ecs.EFSAuthorizationConfigIAM_Values(), false), + }, + }, + }, + }, + }, + }, + }, + "fsx_windows_file_server_volume_configuration": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "file_system_id": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + "root_directory": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + "authorization_config": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "credentials_parameter": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validateArn, + }, + "domain": { Type: schema.TypeString, ForceNew: true, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - ecs.EFSAuthorizationConfigIAMEnabled, - ecs.EFSAuthorizationConfigIAMDisabled, - }, false), + Required: true, }, }, }, @@ -261,12 +291,10 @@ func resourceAwsEcsTaskDefinition() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "type": { - Type: schema.TypeString, - ForceNew: true, - Required: true, - ValidateFunc: validation.StringInSlice([]string{ - ecs.TaskDefinitionPlacementConstraintTypeMemberOf, - }, false), + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validation.StringInSlice(ecs.TaskDefinitionPlacementConstraintType_Values(), false), }, "expression": { Type: schema.TypeString, @@ -281,28 +309,28 @@ func resourceAwsEcsTaskDefinition() *schema.Resource { Type: schema.TypeSet, Optional: true, ForceNew: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "EC2", + "FARGATE", + "EXTERNAL", + }, false), + }, }, "ipc_mode": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - ecs.IpcModeHost, - ecs.IpcModeNone, - ecs.IpcModeTask, - }, false), + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ecs.IpcMode_Values(), false), }, "pid_mode": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - ecs.PidModeHost, - ecs.PidModeTask, - }, false), + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ecs.PidMode_Values(), false), }, "proxy_configuration": { @@ -324,13 +352,11 @@ func resourceAwsEcsTaskDefinition() *schema.Resource { ForceNew: true, }, "type": { - Type: schema.TypeString, - Default: ecs.ProxyConfigurationTypeAppmesh, - Optional: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - ecs.ProxyConfigurationTypeAppmesh, - }, false), + Type: schema.TypeString, + Default: ecs.ProxyConfigurationTypeAppmesh, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ecs.ProxyConfigurationType_Values(), false), }, }, }, @@ -430,20 +456,11 @@ func resourceAwsEcsTaskDefinitionCreate(d *schema.ResourceData, meta interface{} constraints := d.Get("placement_constraints").(*schema.Set).List() if len(constraints) > 0 { - var pc []*ecs.TaskDefinitionPlacementConstraint - for _, raw := range constraints { - p := raw.(map[string]interface{}) - t := p["type"].(string) - e := p["expression"].(string) - if err := validateAwsEcsPlacementConstraint(t, e); err != nil { - return err - } - pc = append(pc, &ecs.TaskDefinitionPlacementConstraint{ - Type: aws.String(t), - Expression: aws.String(e), - }) + cons, err := expandEcsTaskDefinitionPlacementConstraints(constraints) + if err != nil { + return err } - input.PlacementConstraints = pc + input.PlacementConstraints = cons } if v, ok := d.GetOk("requires_compatibilities"); ok && v.(*schema.Set).Len() > 0 { @@ -452,30 +469,7 @@ func resourceAwsEcsTaskDefinitionCreate(d *schema.ResourceData, meta interface{} proxyConfigs := d.Get("proxy_configuration").([]interface{}) if len(proxyConfigs) > 0 { - proxyConfig := proxyConfigs[0] - configMap := proxyConfig.(map[string]interface{}) - - containerName := configMap["container_name"].(string) - proxyType := configMap["type"].(string) - - rawProperties := configMap["properties"].(map[string]interface{}) - - properties := make([]*ecs.KeyValuePair, len(rawProperties)) - i := 0 - for name, value := range rawProperties { - properties[i] = &ecs.KeyValuePair{ - Name: aws.String(name), - Value: aws.String(value.(string)), - } - i++ - } - - var ecsProxyConfig ecs.ProxyConfiguration - ecsProxyConfig.ContainerName = aws.String(containerName) - ecsProxyConfig.Type = aws.String(proxyType) - ecsProxyConfig.Properties = properties - - input.ProxyConfiguration = &ecsProxyConfig + input.ProxyConfiguration = expandEcsTaskDefinitionProxyConfiguration(proxyConfigs) } log.Printf("[DEBUG] Registering ECS task definition: %s", input) @@ -558,23 +552,23 @@ func resourceAwsEcsTaskDefinitionRead(d *schema.ResourceData, meta interface{}) } if err := d.Set("volume", flattenEcsVolumes(taskDefinition.Volumes)); err != nil { - return fmt.Errorf("error setting volume: %s", err) + return fmt.Errorf("error setting volume: %w", err) } if err := d.Set("inference_accelerator", flattenEcsInferenceAccelerators(taskDefinition.InferenceAccelerators)); err != nil { - return fmt.Errorf("error setting inference accelerators: %s", err) + return fmt.Errorf("error setting inference accelerators: %w", err) } if err := d.Set("placement_constraints", flattenPlacementConstraints(taskDefinition.PlacementConstraints)); err != nil { - log.Printf("[ERR] Error setting placement_constraints for (%s): %s", d.Id(), err) + return fmt.Errorf("error setting placement_constraints: %w", err) } if err := d.Set("requires_compatibilities", flattenStringList(taskDefinition.RequiresCompatibilities)); err != nil { - return fmt.Errorf("error setting requires_compatibilities: %s", err) + return fmt.Errorf("error setting requires_compatibilities: %w", err) } if err := d.Set("proxy_configuration", flattenProxyConfiguration(taskDefinition.ProxyConfiguration)); err != nil { - return fmt.Errorf("error setting proxy_configuration: %s", err) + return fmt.Errorf("error setting proxy_configuration: %w", err) } return nil @@ -587,8 +581,8 @@ func flattenPlacementConstraints(pcs []*ecs.TaskDefinitionPlacementConstraint) [ results := make([]map[string]interface{}, 0) for _, pc := range pcs { c := make(map[string]interface{}) - c["type"] = *pc.Type - c["expression"] = *pc.Expression + c["type"] = aws.StringValue(pc.Type) + c["expression"] = aws.StringValue(pc.Expression) results = append(results, c) } return results @@ -677,7 +671,28 @@ func resourceAwsEcsTaskDefinitionVolumeHash(v interface{}) int { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } } + } + + if v, ok := m["fsx_windows_file_server_volume_configuration"]; ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + m := v.([]interface{})[0].(map[string]interface{}) + + if v, ok := m["file_system_id"]; ok && v.(string) != "" { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + if v, ok := m["root_directory"]; ok && v.(string) != "" { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := m["authorization_config"]; ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + m := v.([]interface{})[0].(map[string]interface{}) + if v, ok := m["credentials_parameter"]; ok && v.(string) != "" { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + if v, ok := m["domain"]; ok && v.(string) != "" { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + } } return hashcode.String(buf.String()) @@ -687,8 +702,8 @@ func flattenEcsInferenceAccelerators(list []*ecs.InferenceAccelerator) []map[str result := make([]map[string]interface{}, 0, len(list)) for _, iAcc := range list { l := map[string]interface{}{ - "device_name": *iAcc.DeviceName, - "device_type": *iAcc.DeviceType, + "device_name": aws.StringValue(iAcc.DeviceName), + "device_type": aws.StringValue(iAcc.DeviceType), } result = append(result, l) @@ -709,3 +724,345 @@ func expandEcsInferenceAccelerators(configured []interface{}) []*ecs.InferenceAc return iAccs } + +func expandEcsTaskDefinitionPlacementConstraints(constraints []interface{}) ([]*ecs.TaskDefinitionPlacementConstraint, error) { + var pc []*ecs.TaskDefinitionPlacementConstraint + for _, raw := range constraints { + p := raw.(map[string]interface{}) + t := p["type"].(string) + e := p["expression"].(string) + if err := validateAwsEcsPlacementConstraint(t, e); err != nil { + return nil, err + } + pc = append(pc, &ecs.TaskDefinitionPlacementConstraint{ + Type: aws.String(t), + Expression: aws.String(e), + }) + } + + return pc, nil +} + +func expandEcsTaskDefinitionProxyConfiguration(proxyConfigs []interface{}) *ecs.ProxyConfiguration { + proxyConfig := proxyConfigs[0] + configMap := proxyConfig.(map[string]interface{}) + + rawProperties := configMap["properties"].(map[string]interface{}) + + properties := make([]*ecs.KeyValuePair, len(rawProperties)) + i := 0 + for name, value := range rawProperties { + properties[i] = &ecs.KeyValuePair{ + Name: aws.String(name), + Value: aws.String(value.(string)), + } + i++ + } + + ecsProxyConfig := &ecs.ProxyConfiguration{ + ContainerName: aws.String(configMap["container_name"].(string)), + Type: aws.String(configMap["type"].(string)), + Properties: properties, + } + + return ecsProxyConfig +} + +func expandEcsVolumes(configured []interface{}) []*ecs.Volume { + volumes := make([]*ecs.Volume, 0, len(configured)) + + // Loop over our configured volumes and create + // an array of aws-sdk-go compatible objects + for _, lRaw := range configured { + data := lRaw.(map[string]interface{}) + + l := &ecs.Volume{ + Name: aws.String(data["name"].(string)), + } + + hostPath := data["host_path"].(string) + if hostPath != "" { + l.Host = &ecs.HostVolumeProperties{ + SourcePath: aws.String(hostPath), + } + } + + if v, ok := data["docker_volume_configuration"].([]interface{}); ok && len(v) > 0 { + l.DockerVolumeConfiguration = expandEcsVolumesDockerVolume(v) + } + + if v, ok := data["efs_volume_configuration"].([]interface{}); ok && len(v) > 0 { + l.EfsVolumeConfiguration = expandEcsVolumesEFSVolume(v) + } + + if v, ok := data["fsx_windows_file_server_volume_configuration"].([]interface{}); ok && len(v) > 0 { + l.FsxWindowsFileServerVolumeConfiguration = expandEcsVolumesFsxWinVolume(v) + } + + volumes = append(volumes, l) + } + + return volumes +} + +func expandEcsVolumesDockerVolume(configList []interface{}) *ecs.DockerVolumeConfiguration { + config := configList[0].(map[string]interface{}) + dockerVol := &ecs.DockerVolumeConfiguration{} + + if v, ok := config["scope"].(string); ok && v != "" { + dockerVol.Scope = aws.String(v) + } + + if v, ok := config["autoprovision"]; ok && v != "" { + scope := dockerVol.Scope + if scope == nil || *scope != ecs.ScopeTask || v.(bool) { + dockerVol.Autoprovision = aws.Bool(v.(bool)) + } + } + + if v, ok := config["driver"].(string); ok && v != "" { + dockerVol.Driver = aws.String(v) + } + + if v, ok := config["driver_opts"].(map[string]interface{}); ok && len(v) > 0 { + dockerVol.DriverOpts = expandStringMap(v) + } + + if v, ok := config["labels"].(map[string]interface{}); ok && len(v) > 0 { + dockerVol.Labels = expandStringMap(v) + } + + return dockerVol +} + +func expandEcsVolumesEFSVolume(efsConfig []interface{}) *ecs.EFSVolumeConfiguration { + config := efsConfig[0].(map[string]interface{}) + efsVol := &ecs.EFSVolumeConfiguration{} + + if v, ok := config["file_system_id"].(string); ok && v != "" { + efsVol.FileSystemId = aws.String(v) + } + + if v, ok := config["root_directory"].(string); ok && v != "" { + efsVol.RootDirectory = aws.String(v) + } + if v, ok := config["transit_encryption"].(string); ok && v != "" { + efsVol.TransitEncryption = aws.String(v) + } + + if v, ok := config["transit_encryption_port"].(int); ok && v > 0 { + efsVol.TransitEncryptionPort = aws.Int64(int64(v)) + } + if v, ok := config["authorization_config"].([]interface{}); ok && len(v) > 0 { + efsVol.RootDirectory = nil + efsVol.AuthorizationConfig = expandEcsVolumesEFSVolumeAuthorizationConfig(v) + } + + return efsVol +} + +func expandEcsVolumesEFSVolumeAuthorizationConfig(efsConfig []interface{}) *ecs.EFSAuthorizationConfig { + authconfig := efsConfig[0].(map[string]interface{}) + auth := &ecs.EFSAuthorizationConfig{} + + if v, ok := authconfig["access_point_id"].(string); ok && v != "" { + auth.AccessPointId = aws.String(v) + } + + if v, ok := authconfig["iam"].(string); ok && v != "" { + auth.Iam = aws.String(v) + } + + return auth +} + +func expandEcsVolumesFsxWinVolume(fsxWinConfig []interface{}) *ecs.FSxWindowsFileServerVolumeConfiguration { + config := fsxWinConfig[0].(map[string]interface{}) + fsxVol := &ecs.FSxWindowsFileServerVolumeConfiguration{} + + if v, ok := config["file_system_id"].(string); ok && v != "" { + fsxVol.FileSystemId = aws.String(v) + } + + if v, ok := config["root_directory"].(string); ok && v != "" { + fsxVol.RootDirectory = aws.String(v) + } + + if v, ok := config["authorization_config"].([]interface{}); ok && len(v) > 0 { + fsxVol.AuthorizationConfig = expandEcsVolumesFsxWinVolumeAuthorizationConfig(v) + } + + return fsxVol +} + +func expandEcsVolumesFsxWinVolumeAuthorizationConfig(config []interface{}) *ecs.FSxWindowsFileServerAuthorizationConfig { + authconfig := config[0].(map[string]interface{}) + auth := &ecs.FSxWindowsFileServerAuthorizationConfig{} + + if v, ok := authconfig["credentials_parameter"].(string); ok && v != "" { + auth.CredentialsParameter = aws.String(v) + } + + if v, ok := authconfig["domain"].(string); ok && v != "" { + auth.Domain = aws.String(v) + } + + return auth +} + +func flattenEcsVolumes(list []*ecs.Volume) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(list)) + for _, volume := range list { + l := map[string]interface{}{ + "name": aws.StringValue(volume.Name), + } + + if volume.Host != nil && volume.Host.SourcePath != nil { + l["host_path"] = aws.StringValue(volume.Host.SourcePath) + } + + if volume.DockerVolumeConfiguration != nil { + l["docker_volume_configuration"] = flattenDockerVolumeConfiguration(volume.DockerVolumeConfiguration) + } + + if volume.EfsVolumeConfiguration != nil { + l["efs_volume_configuration"] = flattenEFSVolumeConfiguration(volume.EfsVolumeConfiguration) + } + + if volume.FsxWindowsFileServerVolumeConfiguration != nil { + l["fsx_windows_file_server_volume_configuration"] = flattenFsxWinVolumeConfiguration(volume.FsxWindowsFileServerVolumeConfiguration) + } + + result = append(result, l) + } + return result +} + +func flattenDockerVolumeConfiguration(config *ecs.DockerVolumeConfiguration) []interface{} { + var items []interface{} + m := make(map[string]interface{}) + + if v := config.Scope; v != nil { + m["scope"] = aws.StringValue(v) + } + + if v := config.Autoprovision; v != nil { + m["autoprovision"] = aws.BoolValue(v) + } + + if v := config.Driver; v != nil { + m["driver"] = aws.StringValue(v) + } + + if config.DriverOpts != nil { + m["driver_opts"] = pointersMapToStringList(config.DriverOpts) + } + + if v := config.Labels; v != nil { + m["labels"] = pointersMapToStringList(v) + } + + items = append(items, m) + return items +} + +func flattenEFSVolumeConfiguration(config *ecs.EFSVolumeConfiguration) []interface{} { + var items []interface{} + m := make(map[string]interface{}) + if config != nil { + if v := config.FileSystemId; v != nil { + m["file_system_id"] = aws.StringValue(v) + } + + if v := config.RootDirectory; v != nil { + m["root_directory"] = aws.StringValue(v) + } + if v := config.TransitEncryption; v != nil { + m["transit_encryption"] = aws.StringValue(v) + } + + if v := config.TransitEncryptionPort; v != nil { + m["transit_encryption_port"] = int(aws.Int64Value(v)) + } + + if v := config.AuthorizationConfig; v != nil { + m["authorization_config"] = flattenEFSVolumeAuthorizationConfig(v) + } + } + + items = append(items, m) + return items +} + +func flattenEFSVolumeAuthorizationConfig(config *ecs.EFSAuthorizationConfig) []interface{} { + var items []interface{} + m := make(map[string]interface{}) + if config != nil { + if v := config.AccessPointId; v != nil { + m["access_point_id"] = aws.StringValue(v) + } + if v := config.Iam; v != nil { + m["iam"] = aws.StringValue(v) + } + } + + items = append(items, m) + return items +} + +func flattenFsxWinVolumeConfiguration(config *ecs.FSxWindowsFileServerVolumeConfiguration) []interface{} { + var items []interface{} + m := make(map[string]interface{}) + if config != nil { + if v := config.FileSystemId; v != nil { + m["file_system_id"] = aws.StringValue(v) + } + + if v := config.RootDirectory; v != nil { + m["root_directory"] = aws.StringValue(v) + } + + if v := config.AuthorizationConfig; v != nil { + m["authorization_config"] = flattenFsxWinVolumeAuthorizationConfig(v) + } + } + + items = append(items, m) + return items +} + +func flattenFsxWinVolumeAuthorizationConfig(config *ecs.FSxWindowsFileServerAuthorizationConfig) []interface{} { + var items []interface{} + m := make(map[string]interface{}) + if config != nil { + if v := config.CredentialsParameter; v != nil { + m["credentials_parameter"] = aws.StringValue(v) + } + if v := config.Domain; v != nil { + m["domain"] = aws.StringValue(v) + } + } + + items = append(items, m) + return items +} + +func flattenEcsContainerDefinitions(definitions []*ecs.ContainerDefinition) (string, error) { + b, err := jsonutil.BuildJSON(definitions) + if err != nil { + return "", err + } + + return string(b), nil +} + +func expandEcsContainerDefinitions(rawDefinitions string) ([]*ecs.ContainerDefinition, error) { + var definitions []*ecs.ContainerDefinition + + err := json.Unmarshal([]byte(rawDefinitions), &definitions) + if err != nil { + return nil, fmt.Errorf("Error decoding JSON: %s", err) + } + + return definitions, nil +} diff --git a/aws/resource_aws_ecs_task_definition_test.go b/aws/resource_aws_ecs_task_definition_test.go index b6206f7d76a..bf015941135 100644 --- a/aws/resource_aws_ecs_task_definition_test.go +++ b/aws/resource_aws_ecs_task_definition_test.go @@ -15,6 +15,8 @@ import ( ) func init() { + RegisterServiceErrorCheckFunc(ecs.EndpointsID, testAccErrorCheckSkipECS) + resource.AddTestSweepers("aws_ecs_task_definition", &resource.Sweeper{ Name: "aws_ecs_task_definition", F: testSweepEcsTaskDefinitions, @@ -65,6 +67,12 @@ func testSweepEcsTaskDefinitions(region string) error { return sweeperErrs.ErrorOrNil() } +func testAccErrorCheckSkipECS(t *testing.T) resource.ErrorCheckFunc { + return testAccErrorCheckSkipMessagesContaining(t, + "Unsupported field 'inferenceAccelerators'", + ) +} + func TestAccAWSEcsTaskDefinition_basic(t *testing.T) { var def ecs.TaskDefinition @@ -147,6 +155,19 @@ func TestAccAWSEcsTaskDefinition_withDockerVolume(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsTaskDefinitionExists(resourceName, &def), resource.TestCheckResourceAttr(resourceName, "volume.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "volume.*", map[string]string{ + "name": tdName, + "docker_volume_configuration.#": "1", + "docker_volume_configuration.0.driver": "local", + "docker_volume_configuration.0.scope": "shared", + "docker_volume_configuration.0.autoprovision": "true", + "docker_volume_configuration.0.driver_opts.%": "2", + "docker_volume_configuration.0.driver_opts.device": "tmpfs", + "docker_volume_configuration.0.driver_opts.uid": "1000", + "docker_volume_configuration.0.labels.%": "2", + "docker_volume_configuration.0.labels.environment": "test", + "docker_volume_configuration.0.labels.stack": "april", + }), ), }, { @@ -176,6 +197,11 @@ func TestAccAWSEcsTaskDefinition_withDockerVolumeMinimalConfig(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsTaskDefinitionExists(resourceName, &def), resource.TestCheckResourceAttr(resourceName, "volume.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "volume.*", map[string]string{ + "name": tdName, + "docker_volume_configuration.#": "1", + "docker_volume_configuration.0.autoprovision": "true", + }), ), }, { @@ -205,6 +231,11 @@ func TestAccAWSEcsTaskDefinition_withEFSVolumeMinimal(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsTaskDefinitionExists(resourceName, &def), resource.TestCheckResourceAttr(resourceName, "volume.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "volume.*", map[string]string{ + "name": tdName, + "efs_volume_configuration.#": "1", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "volume.*.efs_volume_configuration.0.file_system_id", "aws_efs_file_system.test", "id"), ), }, { @@ -234,6 +265,12 @@ func TestAccAWSEcsTaskDefinition_withEFSVolume(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsTaskDefinitionExists(resourceName, &def), resource.TestCheckResourceAttr(resourceName, "volume.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "volume.*", map[string]string{ + "name": tdName, + "efs_volume_configuration.#": "1", + "efs_volume_configuration.0.root_directory": "/home/test", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "volume.*.efs_volume_configuration.0.file_system_id", "aws_efs_file_system.test", "id"), ), }, { @@ -262,6 +299,14 @@ func TestAccAWSEcsTaskDefinition_withTransitEncryptionEFSVolume(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsTaskDefinitionExists(resourceName, &def), resource.TestCheckResourceAttr(resourceName, "volume.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "volume.*", map[string]string{ + "name": tdName, + "efs_volume_configuration.#": "1", + "efs_volume_configuration.0.root_directory": "/home/test", + "efs_volume_configuration.0.transit_encryption": "ENABLED", + "efs_volume_configuration.0.transit_encryption_port": "2999", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "volume.*.efs_volume_configuration.0.file_system_id", "aws_efs_file_system.test", "id"), ), }, { @@ -291,6 +336,59 @@ func TestAccAWSEcsTaskDefinition_withEFSAccessPoint(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsTaskDefinitionExists(resourceName, &def), resource.TestCheckResourceAttr(resourceName, "volume.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "volume.*", map[string]string{ + "name": tdName, + "efs_volume_configuration.#": "1", + "efs_volume_configuration.0.root_directory": "/", + "efs_volume_configuration.0.transit_encryption": "ENABLED", + "efs_volume_configuration.0.transit_encryption_port": "2999", + "efs_volume_configuration.0.authorization_config.#": "1", + "efs_volume_configuration.0.authorization_config.0.iam": "DISABLED", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "volume.*.efs_volume_configuration.0.file_system_id", "aws_efs_file_system.test", "id"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "volume.*.efs_volume_configuration.0.authorization_config.0.access_point_id", "aws_efs_access_point.test", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSEcsTaskDefinitionImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSEcsTaskDefinition_withFsxWinFileSystem(t *testing.T) { + var def ecs.TaskDefinition + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_ecs_task_definition.test" + + if testAccGetPartition() == "aws-us-gov" { + t.Skip("Amazon FSx for Windows File Server volumes for ECS tasks are not supported in GovCloud partition") + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ecs.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEcsTaskDefinitionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEcsTaskDefinitionWithFsxVolume(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsTaskDefinitionExists(resourceName, &def), + resource.TestCheckResourceAttr(resourceName, "volume.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "volume.*", map[string]string{ + "name": rName, + "fsx_windows_file_server_volume_configuration.#": "1", + "fsx_windows_file_server_volume_configuration.0.root_directory": "\\data", + "fsx_windows_file_server_volume_configuration.0.authorization_config.#": "1", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "volume.*.fsx_windows_file_server_volume_configuration.0.file_system_id", "aws_fsx_windows_file_system.test", "id"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "volume.*.fsx_windows_file_server_volume_configuration.0.authorization_config.0.credentials_parameter", "aws_secretsmanager_secret_version.test", "arn"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "volume.*.fsx_windows_file_server_volume_configuration.0.authorization_config.0.domain", "aws_directory_service_directory.test", "name"), ), }, { @@ -2204,6 +2302,94 @@ TASK_DEFINITION `, tdName) } +func testAccAWSEcsTaskDefinitionWithFsxVolume(tdName string) string { + return testAccAwsFsxWindowsFileSystemConfigSubnetIds1() + fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_secretsmanager_secret" "test" { + name = %[1]q + recovery_window_in_days = 0 +} + +resource "aws_secretsmanager_secret_version" "test" { + secret_id = aws_secretsmanager_secret.test.id + secret_string = jsonencode({ username : "admin", password : aws_directory_service_directory.test.password }) +} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : "sts:AssumeRole", + "Principal" : { + "Service" : "ecs-tasks.${data.aws_partition.current.dns_suffix}" + }, + "Effect" : "Allow", + "Sid" : "" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "test" { + role = aws_iam_role.test.name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role_policy_attachment" "test2" { + role = aws_iam_role.test.name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/SecretsManagerReadWrite" +} + +resource "aws_iam_role_policy_attachment" "test3" { + role = aws_iam_role.test.name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonFSxReadOnlyAccess" +} + +resource "aws_ecs_task_definition" "test" { + family = %[1]q + execution_role_arn = aws_iam_role.test.arn + + container_definitions = < 0 { - config := configList[0].(map[string]interface{}) - l.DockerVolumeConfiguration = &ecs.DockerVolumeConfiguration{} - - if v, ok := config["scope"].(string); ok && v != "" { - l.DockerVolumeConfiguration.Scope = aws.String(v) - } - - if v, ok := config["autoprovision"]; ok && v != "" { - scope := l.DockerVolumeConfiguration.Scope - if scope == nil || *scope != ecs.ScopeTask || v.(bool) { - l.DockerVolumeConfiguration.Autoprovision = aws.Bool(v.(bool)) - } - } - - if v, ok := config["driver"].(string); ok && v != "" { - l.DockerVolumeConfiguration.Driver = aws.String(v) - } - - if v, ok := config["driver_opts"].(map[string]interface{}); ok && len(v) > 0 { - l.DockerVolumeConfiguration.DriverOpts = expandStringMap(v) - } - - if v, ok := config["labels"].(map[string]interface{}); ok && len(v) > 0 { - l.DockerVolumeConfiguration.Labels = expandStringMap(v) - } - } - - efsConfig, ok := data["efs_volume_configuration"].([]interface{}) - if ok && len(efsConfig) > 0 { - config := efsConfig[0].(map[string]interface{}) - l.EfsVolumeConfiguration = &ecs.EFSVolumeConfiguration{} - - if v, ok := config["file_system_id"].(string); ok && v != "" { - l.EfsVolumeConfiguration.FileSystemId = aws.String(v) - } - - if v, ok := config["root_directory"].(string); ok && v != "" { - l.EfsVolumeConfiguration.RootDirectory = aws.String(v) - } - if v, ok := config["transit_encryption"].(string); ok && v != "" { - l.EfsVolumeConfiguration.TransitEncryption = aws.String(v) - } - - if v, ok := config["transit_encryption_port"].(int); ok && v > 0 { - l.EfsVolumeConfiguration.TransitEncryptionPort = aws.Int64(int64(v)) - } - authConfig, ok := config["authorization_config"].([]interface{}) - if ok && len(authConfig) > 0 { - authconfig := authConfig[0].(map[string]interface{}) - l.EfsVolumeConfiguration.RootDirectory = nil - l.EfsVolumeConfiguration.AuthorizationConfig = &ecs.EFSAuthorizationConfig{} - - if v, ok := authconfig["access_point_id"].(string); ok && v != "" { - l.EfsVolumeConfiguration.AuthorizationConfig.AccessPointId = aws.String(v) - } - - if v, ok := authconfig["iam"].(string); ok && v != "" { - l.EfsVolumeConfiguration.AuthorizationConfig.Iam = aws.String(v) - } - } - } - - volumes = append(volumes, l) - } - - return volumes -} - -// Takes JSON in a string. Decodes JSON into -// an array of ecs.ContainerDefinition compatible objects -func expandEcsContainerDefinitions(rawDefinitions string) ([]*ecs.ContainerDefinition, error) { - var definitions []*ecs.ContainerDefinition - - err := json.Unmarshal([]byte(rawDefinitions), &definitions) - if err != nil { - return nil, fmt.Errorf("Error decoding JSON: %s", err) - } - - return definitions, nil -} - // Takes the result of flatmap. Expand for an array of load balancers and // returns ecs.LoadBalancer compatible objects func expandEcsLoadBalancers(configured []interface{}) []*ecs.LoadBalancer { @@ -682,103 +577,6 @@ func flattenListeners(list []*elb.ListenerDescription) []map[string]interface{} return result } -// Flattens an array of Volumes into a []map[string]interface{} -func flattenEcsVolumes(list []*ecs.Volume) []map[string]interface{} { - result := make([]map[string]interface{}, 0, len(list)) - for _, volume := range list { - l := map[string]interface{}{ - "name": *volume.Name, - } - - if volume.Host != nil && volume.Host.SourcePath != nil { - l["host_path"] = *volume.Host.SourcePath - } - - if volume.DockerVolumeConfiguration != nil { - l["docker_volume_configuration"] = flattenDockerVolumeConfiguration(volume.DockerVolumeConfiguration) - } - - if volume.EfsVolumeConfiguration != nil { - l["efs_volume_configuration"] = flattenEFSVolumeConfiguration(volume.EfsVolumeConfiguration) - } - - result = append(result, l) - } - return result -} - -func flattenDockerVolumeConfiguration(config *ecs.DockerVolumeConfiguration) []interface{} { - var items []interface{} - m := make(map[string]interface{}) - - if v := config.Scope; v != nil { - m["scope"] = aws.StringValue(v) - } - - if v := config.Autoprovision; v != nil { - m["autoprovision"] = aws.BoolValue(v) - } - - if v := config.Driver; v != nil { - m["driver"] = aws.StringValue(v) - } - - if config.DriverOpts != nil { - m["driver_opts"] = pointersMapToStringList(config.DriverOpts) - } - - if v := config.Labels; v != nil { - m["labels"] = pointersMapToStringList(v) - } - - items = append(items, m) - return items -} - -func flattenEFSVolumeConfiguration(config *ecs.EFSVolumeConfiguration) []interface{} { - var items []interface{} - m := make(map[string]interface{}) - if config != nil { - if v := config.FileSystemId; v != nil { - m["file_system_id"] = aws.StringValue(v) - } - - if v := config.RootDirectory; v != nil { - m["root_directory"] = aws.StringValue(v) - } - if v := config.TransitEncryption; v != nil { - m["transit_encryption"] = aws.StringValue(v) - } - - if v := config.TransitEncryptionPort; v != nil { - m["transit_encryption_port"] = int(aws.Int64Value(v)) - } - - if v := config.AuthorizationConfig; v != nil { - m["authorization_config"] = flattenEFSVolumeAuthorizationConfig(v) - } - } - - items = append(items, m) - return items -} - -func flattenEFSVolumeAuthorizationConfig(config *ecs.EFSAuthorizationConfig) []interface{} { - var items []interface{} - m := make(map[string]interface{}) - if config != nil { - if v := config.AccessPointId; v != nil { - m["access_point_id"] = aws.StringValue(v) - } - if v := config.Iam; v != nil { - m["iam"] = aws.StringValue(v) - } - } - - items = append(items, m) - return items -} - // Flattens an array of ECS LoadBalancers into a []map[string]interface{} func flattenEcsLoadBalancers(list []*ecs.LoadBalancer) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(list)) @@ -801,16 +599,6 @@ func flattenEcsLoadBalancers(list []*ecs.LoadBalancer) []map[string]interface{} return result } -// Encodes an array of ecs.ContainerDefinitions into a JSON string -func flattenEcsContainerDefinitions(definitions []*ecs.ContainerDefinition) (string, error) { - b, err := jsonutil.BuildJSON(definitions) - if err != nil { - return "", err - } - - return string(b), nil -} - // Flattens an array of Options into a []map[string]interface{} func flattenOptions(apiOptions []*rds.Option, optionConfigurations []*rds.OptionConfiguration) []map[string]interface{} { result := make([]map[string]interface{}, 0) diff --git a/website/docs/r/ecs_task_definition.html.markdown b/website/docs/r/ecs_task_definition.html.markdown index 0aa9f1f2caa..f1d885c5553 100644 --- a/website/docs/r/ecs_task_definition.html.markdown +++ b/website/docs/r/ecs_task_definition.html.markdown @@ -128,6 +128,34 @@ resource "aws_ecs_task_definition" "service" { } ``` +### Example Using `fsx_windows_file_server_volume_configuration` + +```terraform +resource "aws_ecs_task_definition" "service" { + family = "service" + container_definitions = file("task-definitions/service.json") + + volume { + name = "service-storage" + + fsx_windows_file_server_volume_configuration { + file_system_id = aws_fsx_windows_file_system.test.id + root_directory = "\\data" + + authorization_config { + credentials_parameter = aws_secretsmanager_secret_version.test.arn + domain = aws_directory_service_directory.test.name + } + } + } +} + +resource "aws_secretsmanager_secret_version" "test" { + secret_id = aws_secretsmanager_secret.test.id + secret_string = jsonencode({ username : "admin", password : aws_directory_service_directory.test.password }) +} +``` + ### Example Using `container_definitions` and `inference_accelerator` ```terraform @@ -198,6 +226,7 @@ The following arguments are optional: * `docker_volume_configuration` - (Optional) Configuration block to configure a [docker volume](#docker_volume_configuration). Detailed below. * `efs_volume_configuration` - (Optional) Configuration block for an [EFS volume](#efs_volume_configuration). Detailed below. +* `fsx_windows_file_server_volume_configuration` - (Optional) Configuration block for an [FSX Windows File Server volume](#fsx_windows_file_server_volume_configuration). Detailed below. * `host_path` - (Optional) Path on the host container instance that is presented to the container. If not set, ECS will create a nonpersistent data volume that starts empty and is deleted after the task has finished. * `name` - (Required) Name of the volume. This name is referenced in the `sourceVolume` parameter of container definition in the `mountPoints` section. @@ -222,11 +251,24 @@ For more information, see [Specifying an EFS volume in your Task Definition Deve * `transit_encryption_port` - (Optional) Port to use for transit encryption. If you do not specify a transit encryption port, it will use the port selection strategy that the Amazon EFS mount helper uses. * `authorization_config` - (Optional) Configuration block for [authorization](#authorization_config) for the Amazon EFS file system. Detailed below. -### authorization_config +#### authorization_config * `access_point_id` - (Optional) Access point ID to use. If an access point is specified, the root directory value will be relative to the directory set for the access point. If specified, transit encryption must be enabled in the EFSVolumeConfiguration. * `iam` - (Optional) Whether or not to use the Amazon ECS task IAM role defined in a task definition when mounting the Amazon EFS file system. If enabled, transit encryption must be enabled in the EFSVolumeConfiguration. Valid values: `ENABLED`, `DISABLED`. If this parameter is omitted, the default value of `DISABLED` is used. +### fsx_windows_file_server_volume_configuration + +For more information, see [Specifying an FSX Windows File Server volume in your Task Definition Developer Guide](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/tutorial-wfsx-volumes.html) + +* `file_system_id` - (Required) The Amazon FSx for Windows File Server file system ID to use. +* `root_directory` - (Required) The directory within the Amazon FSx for Windows File Server file system to mount as the root directory inside the host. +* `authorization_config` - (Required) Configuration block for [authorization](#authorization_config) for the Amazon FSx for Windows File Server file system detailed below. + +#### authorization_config + +* `credentials_parameter` - (Required) The authorization credential option to use. The authorization credential options can be provided using either the Amazon Resource Name (ARN) of an AWS Secrets Manager secret or AWS Systems Manager Parameter Store parameter. The ARNs refer to the stored credentials. +* `domain` - (Required) A fully qualified domain name hosted by an AWS Directory Service Managed Microsoft AD (Active Directory) or self-hosted AD on Amazon EC2. + ### placement_constraints * `expression` - (Optional) Cluster Query Language expression to apply to the constraint. For more information, see [Cluster Query Language in the Amazon EC2 Container Service Developer Guide](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-query-language.html).