Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emrserverless add image configuration #30398

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/30398.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_imagebuilder_container_recipe: Add `platform_override` field
```

```release-note:enhancement
resource/aws_emrserverless_application: Add `image_configuration` field
```
56 changes: 56 additions & 0 deletions internal/service/emrserverless/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ func ResourceApplication() *schema.Resource {
},
},
},
"image_configuration": {
Type: schema.TypeList,
Optional: true,
Computed: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"image_uri": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"initial_capacity": {
Type: schema.TypeSet,
Optional: true,
Expand Down Expand Up @@ -227,6 +241,10 @@ func resourceApplicationCreate(ctx context.Context, d *schema.ResourceData, meta
input.AutoStopConfiguration = expandAutoStopConfig(v.([]interface{})[0].(map[string]interface{}))
}

if v, ok := d.GetOk("image_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
input.ImageConfiguration = expandImageConfiguration(v.([]interface{})[0].(map[string]interface{}))
}

if v, ok := d.GetOk("initial_capacity"); ok && v.(*schema.Set).Len() > 0 {
input.InitialCapacity = expandInitialCapacity(v.(*schema.Set))
}
Expand Down Expand Up @@ -291,6 +309,10 @@ func resourceApplicationRead(ctx context.Context, d *schema.ResourceData, meta i
return sdkdiag.AppendErrorf(diags, "setting auto_stop_configuration: %s", err)
}

if err := d.Set("image_configuration", flattenImageConfiguration(application.ImageConfiguration)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting image_configuration: %s", err)
}

if err := d.Set("initial_capacity", flattenInitialCapacity(application.InitialCapacity)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting initial_capacity: %s", err)
}
Expand Down Expand Up @@ -339,6 +361,10 @@ func resourceApplicationUpdate(ctx context.Context, d *schema.ResourceData, meta
input.AutoStopConfiguration = expandAutoStopConfig(v.([]interface{})[0].(map[string]interface{}))
}

if v, ok := d.GetOk("image_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
input.ImageConfiguration = expandImageConfiguration(v.([]interface{})[0].(map[string]interface{}))
}

if v, ok := d.GetOk("initial_capacity"); ok && v.(*schema.Set).Len() > 0 {
input.InitialCapacity = expandInitialCapacity(v.(*schema.Set))
}
Expand Down Expand Up @@ -538,6 +564,36 @@ func flattenNetworkConfiguration(apiObject *emrserverless.NetworkConfiguration)
return tfMap
}

func expandImageConfiguration(tfMap map[string]interface{}) *emrserverless.ImageConfigurationInput_ {
if tfMap == nil {
return nil
}

apiObject := &emrserverless.ImageConfigurationInput_{}

if v, ok := tfMap["image_uri"].(string); ok && v != "" {
apiObject.ImageUri = aws.String(v)
}

return apiObject
}

func flattenImageConfiguration(apiObject *emrserverless.ImageConfiguration) []interface{} {
if apiObject == nil || apiObject.ImageUri == nil {
return nil
}

var tfList []interface{}

if v := apiObject.ImageUri; v != nil {
tfList = append(tfList, map[string]interface{}{
"image_uri": aws.StringValue(v),
})
}

return tfList
}

func expandInitialCapacity(tfMap *schema.Set) map[string]*emrserverless.InitialCapacityConfig {
if tfMap == nil {
return nil
Expand Down
235 changes: 235 additions & 0 deletions internal/service/emrserverless/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func TestAccEMRServerlessApplication_basic(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "auto_stop_configuration.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "auto_stop_configuration.0.idle_timeout_minutes", "15"),
resource.TestCheckResourceAttr(resourceName, "initial_capacity.#", "0"),
resource.TestCheckResourceAttr(resourceName, "image_configuration.#", "0"),
resource.TestCheckResourceAttr(resourceName, "tags.%", "0"),
),
},
Expand Down Expand Up @@ -137,6 +138,59 @@ func TestAccEMRServerlessApplication_initialCapacity(t *testing.T) {
})
}

func TestAccEMRServerlessApplication_imageConfiguration(t *testing.T) {
ctx := acctest.Context(t)
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}
var application emrserverless.Application
resourceName := "aws_emrserverless_application.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)

firstVersionRegex := regexp.MustCompile(`1\.0\.0`)
secondVersionRegex := regexp.MustCompile(`1\.0\.1`)

firstImageConfig, err := testAccApplicationConfig_imageConfiguration(rName, "1.0.0", "1.0.1", "1.0.0")
if err != nil {
t.Error(err)
}

secondImageConfig, err := testAccApplicationConfig_imageConfiguration(rName, "1.0.0", "1.0.1", "1.0.1")
if err != nil {
t.Error(err)
}

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, emrserverless.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckApplicationDestroy(ctx),
Steps: []resource.TestStep{
{
Config: firstImageConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckApplicationExists(ctx, resourceName, &application),
resource.TestCheckResourceAttr(resourceName, "image_configuration.#", "1"),
resource.TestMatchResourceAttr(resourceName, "image_configuration.0.image_uri", firstVersionRegex),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: secondImageConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckApplicationExists(ctx, resourceName, &application),
resource.TestCheckResourceAttr(resourceName, "image_configuration.#", "1"),
resource.TestMatchResourceAttr(resourceName, "image_configuration.0.image_uri", secondVersionRegex),
),
},
},
})
}

func TestAccEMRServerlessApplication_maxCapacity(t *testing.T) {
ctx := acctest.Context(t)
var application emrserverless.Application
Expand Down Expand Up @@ -445,3 +499,184 @@ resource "aws_emrserverless_application" "test" {
}
`, rName, arch)
}

// At the time of writing, the AWS EMR Serverless API returns a 500 error if you try to create an EMR Serverless
// application with an image from a public emr repo, and so we need to build an image and put it in a temporary
// repo in order to run the test
func testAccApplicationConfig_imageConfiguration(rName, firstImageVersion, secondImageVersion, selectedImageVersion string) (string, error) {
if firstImageVersion == secondImageVersion {
return "", fmt.Errorf("firstImageVersion and secondImageVersion cannot be equal. Was given %[1]q for both", firstImageVersion)
}

if selectedImageVersion != firstImageVersion && selectedImageVersion != secondImageVersion {
return "", fmt.Errorf("selectedImageVersion must be equal to firstImageVersion or secondImageVersion (%[1]q or %[2]q). Was given %[3]q", firstImageVersion, secondImageVersion, selectedImageVersion)
}

selectedVersionResourceName := "test_version1"
if selectedImageVersion != firstImageVersion {
selectedVersionResourceName = "test_version2"
}

return fmt.Sprintf(`
data "aws_region" "current" {}

data "aws_partition" "current" {}

resource "aws_ecr_repository" "test" {
name = %[1]q
force_delete = true
}

resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}

resource "aws_default_route_table" "test" {
default_route_table_id = aws_vpc.test.default_route_table_id

route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.test.id
}
}

resource "aws_internet_gateway" "test" {
vpc_id = aws_vpc.test.id
}

resource "aws_subnet" "test" {
cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 0)
map_public_ip_on_launch = true
vpc_id = aws_vpc.test.id
}

resource "aws_default_security_group" "test" {
vpc_id = aws_vpc.test.id

egress {
cidr_blocks = ["0.0.0.0/0"]
from_port = 0
protocol = "-1"
to_port = 0
}

ingress {
from_port = 0
protocol = -1
self = true
to_port = 0
}
}

resource "aws_iam_role" "test" {
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.${data.aws_partition.current.dns_suffix}"
}
Sid = ""
}]
})
name = %[1]q
}

resource "aws_iam_role_policy_attachment" "AmazonSSMManagedInstanceCore" {
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.test.name
}

resource "aws_iam_role_policy_attachment" "EC2InstanceProfileForImageBuilder" {
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/EC2InstanceProfileForImageBuilder"
role = aws_iam_role.test.name
}

resource "aws_iam_role_policy_attachment" "EC2InstanceProfileForImageBuilderECRContainerBuilds" {
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/EC2InstanceProfileForImageBuilderECRContainerBuilds"
role = aws_iam_role.test.name
}

resource "aws_iam_instance_profile" "test" {
name = aws_iam_role.test.name
role = aws_iam_role.test.name

depends_on = [
aws_iam_role_policy_attachment.AmazonSSMManagedInstanceCore,
aws_iam_role_policy_attachment.EC2InstanceProfileForImageBuilderECRContainerBuilds
]
}

resource "aws_imagebuilder_container_recipe" "test_version1" {
name = "%[1]s_version1"
container_type = "DOCKER"
parent_image = "public.ecr.aws/emr-serverless/hive/emr-6.9.0"
version = %[3]q
platform_override = "Linux"

component {
component_arn = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:component/hello-world-linux/x.x.x"
}

dockerfile_template_data = <<EOF
FROM {{{ imagebuilder:parentImage }}}
EOF

target_repository {
repository_name = aws_ecr_repository.test.name
service = "ECR"
}
}

resource "aws_imagebuilder_container_recipe" "test_version2" {
name = "%[1]s_version2"
container_type = "DOCKER"
parent_image = "public.ecr.aws/emr-serverless/hive/emr-6.9.0"
version = %[4]q
platform_override = "Linux"

component {
component_arn = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:component/hello-world-linux/x.x.x"
}

dockerfile_template_data = <<EOF
FROM {{{ imagebuilder:parentImage }}}
EOF

target_repository {
repository_name = aws_ecr_repository.test.name
service = "ECR"
}
}

resource "aws_imagebuilder_infrastructure_configuration" "test" {
instance_profile_name = aws_iam_instance_profile.test.name
name = %[1]q
security_group_ids = [aws_default_security_group.test.id]
subnet_id = aws_subnet.test.id

depends_on = [aws_default_route_table.test]
}

resource "aws_imagebuilder_image" "test_version1" {
container_recipe_arn = aws_imagebuilder_container_recipe.test_version1.arn
infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.test.arn
}

resource "aws_imagebuilder_image" "test_version2" {
container_recipe_arn = aws_imagebuilder_container_recipe.test_version2.arn
infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.test.arn
}

resource "aws_emrserverless_application" "test" {
name = %[1]q
release_label = "emr-6.9.0"
type = "hive"

image_configuration {
image_uri = "${aws_ecr_repository.test.repository_url}:${replace(aws_imagebuilder_image.%[2]s.version, "/", "-")}"
}
}
`, rName, selectedVersionResourceName, firstImageVersion, secondImageVersion), nil
}
10 changes: 10 additions & 0 deletions internal/service/imagebuilder/container_recipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ func ResourceContainerRecipe() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"platform_override": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"Linux", "Windows"}, false),
},
"tags": tftags.TagsSchema(),
"tags_all": tftags.TagsSchemaComputed(),
"target_repository": {
Expand Down Expand Up @@ -318,6 +324,10 @@ func resourceContainerRecipeCreate(ctx context.Context, d *schema.ResourceData,
input.ParentImage = aws.String(v.(string))
}

if v, ok := d.GetOk("platform_override"); ok {
input.PlatformOverride = aws.String(v.(string))
}

if len(tags) > 0 {
input.Tags = Tags(tags.IgnoreAWS())
}
Expand Down
Loading