diff --git a/aws/data_source_aws_imagebuilder_image_recipe.go b/aws/data_source_aws_imagebuilder_image_recipe.go new file mode 100644 index 00000000000..0fee2445097 --- /dev/null +++ b/aws/data_source_aws_imagebuilder_image_recipe.go @@ -0,0 +1,159 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/imagebuilder" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func dataSourceAwsImageBuilderImageRecipe() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsImageBuilderImageRecipeRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "block_device_mapping": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": { + Type: schema.TypeString, + Computed: true, + }, + "ebs": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": { + Type: schema.TypeBool, + Computed: true, + }, + "encrypted": { + Type: schema.TypeBool, + Computed: true, + }, + "iops": { + Type: schema.TypeInt, + Computed: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Computed: true, + }, + "snapshot_id": { + Type: schema.TypeString, + Computed: true, + }, + "volume_size": { + Type: schema.TypeInt, + Computed: true, + }, + "volume_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "no_device": { + Type: schema.TypeString, + Computed: true, + }, + "virtual_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "component": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "component_arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "date_created": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "parent_image": { + Type: schema.TypeString, + Computed: true, + }, + "platform": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + "version": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsImageBuilderImageRecipeRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).imagebuilderconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + input := &imagebuilder.GetImageRecipeInput{} + + if v, ok := d.GetOk("arn"); ok { + input.ImageRecipeArn = aws.String(v.(string)) + } + + output, err := conn.GetImageRecipe(input) + + if err != nil { + return fmt.Errorf("error reading Image Builder Image Recipe (%s): %w", aws.StringValue(input.ImageRecipeArn), err) + } + + if output == nil || output.ImageRecipe == nil { + return fmt.Errorf("error reading Image Builder Image Recipe (%s): empty response", aws.StringValue(input.ImageRecipeArn)) + } + + imageRecipe := output.ImageRecipe + + d.SetId(aws.StringValue(imageRecipe.Arn)) + d.Set("arn", imageRecipe.Arn) + d.Set("block_device_mapping", flattenImageBuilderInstanceBlockDeviceMappings(imageRecipe.BlockDeviceMappings)) + d.Set("component", flattenImageBuilderComponentConfigurations(imageRecipe.Components)) + d.Set("date_created", imageRecipe.DateCreated) + d.Set("description", imageRecipe.Description) + d.Set("name", imageRecipe.Name) + d.Set("owner", imageRecipe.Owner) + d.Set("parent_image", imageRecipe.ParentImage) + d.Set("platform", imageRecipe.Platform) + d.Set("tags", keyvaluetags.ImagebuilderKeyValueTags(imageRecipe.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()) + d.Set("version", imageRecipe.Version) + + return nil +} diff --git a/aws/data_source_aws_imagebuilder_image_recipe_test.go b/aws/data_source_aws_imagebuilder_image_recipe_test.go new file mode 100644 index 00000000000..80ef8941dcc --- /dev/null +++ b/aws/data_source_aws_imagebuilder_image_recipe_test.go @@ -0,0 +1,81 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccAwsImageBuilderImageRecipeDataSource_Arn(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.aws_imagebuilder_image_recipe.test" + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeDataSourceConfigArn(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "block_device_mapping.#", resourceName, "block_device_mapping.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "component.#", resourceName, "component.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "date_created", resourceName, "date_created"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "owner", resourceName, "owner"), + resource.TestCheckResourceAttrPair(dataSourceName, "parent_image", resourceName, "parent_image"), + resource.TestCheckResourceAttrPair(dataSourceName, "platform", resourceName, "platform"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(dataSourceName, "version", resourceName, "version"), + ), + }, + }, + }) +} + +func testAccAwsImageBuilderImageRecipeDataSourceConfigArn(rName string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +data "aws_partition" "current" {} + +resource "aws_imagebuilder_component" "test" { + data = yamlencode({ + phases = [{ + name = "build" + steps = [{ + action = "ExecuteBash" + inputs = { + commands = ["echo 'hello world'"] + } + name = "example" + onFailure = "Continue" + }] + }] + schemaVersion = 1.0 + }) + name = %[1]q + platform = "Linux" + version = "1.0.0" +} + +resource "aws_imagebuilder_image_recipe" "test" { + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} + +data "aws_imagebuilder_image_recipe" "test" { + arn = aws_imagebuilder_image_recipe.test.arn +} +`, rName) +} diff --git a/aws/provider.go b/aws/provider.go index 977e63f7cbe..80199d84940 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -268,6 +268,7 @@ func Provider() *schema.Provider { "aws_iam_user": dataSourceAwsIAMUser(), "aws_imagebuilder_component": dataSourceAwsImageBuilderComponent(), "aws_imagebuilder_distribution_configuration": datasourceAwsImageBuilderDistributionConfiguration(), + "aws_imagebuilder_image_recipe": dataSourceAwsImageBuilderImageRecipe(), "aws_imagebuilder_infrastructure_configuration": datasourceAwsImageBuilderInfrastructureConfiguration(), "aws_internet_gateway": dataSourceAwsInternetGateway(), "aws_iot_endpoint": dataSourceAwsIotEndpoint(), @@ -706,6 +707,7 @@ func Provider() *schema.Provider { "aws_iam_user_login_profile": resourceAwsIamUserLoginProfile(), "aws_imagebuilder_component": resourceAwsImageBuilderComponent(), "aws_imagebuilder_distribution_configuration": resourceAwsImageBuilderDistributionConfiguration(), + "aws_imagebuilder_image_recipe": resourceAwsImageBuilderImageRecipe(), "aws_imagebuilder_infrastructure_configuration": resourceAwsImageBuilderInfrastructureConfiguration(), "aws_inspector_assessment_target": resourceAWSInspectorAssessmentTarget(), "aws_inspector_assessment_template": resourceAWSInspectorAssessmentTemplate(), diff --git a/aws/resource_aws_imagebuilder_image_recipe.go b/aws/resource_aws_imagebuilder_image_recipe.go new file mode 100644 index 00000000000..9cbab5cc790 --- /dev/null +++ b/aws/resource_aws_imagebuilder_image_recipe.go @@ -0,0 +1,546 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/imagebuilder" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func resourceAwsImageBuilderImageRecipe() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsImageBuilderImageRecipeCreate, + Read: resourceAwsImageBuilderImageRecipeRead, + Update: resourceAwsImageBuilderImageRecipeUpdate, + Delete: resourceAwsImageBuilderImageRecipeDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "block_device_mapping": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 1024), + }, + "ebs": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": { + // Use TypeString to allow an "unspecified" value, + // since TypeBool only has true/false with false default. + // The conversion from bare true/false values in + // configurations to TypeString value is currently safe. + Type: schema.TypeString, + Optional: true, + ForceNew: true, + DiffSuppressFunc: suppressEquivalentTypeStringBoolean, + ValidateFunc: validateTypeStringNullableBoolean, + }, + "encrypted": { + // Use TypeString to allow an "unspecified" value, + // since TypeBool only has true/false with false default. + // The conversion from bare true/false values in + // configurations to TypeString value is currently safe. + Type: schema.TypeString, + Optional: true, + ForceNew: true, + DiffSuppressFunc: suppressEquivalentTypeStringBoolean, + ValidateFunc: validateTypeStringNullableBoolean, + }, + "iops": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntBetween(100, 10000), + }, + "kms_key_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "snapshot_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "volume_size": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 16000), + }, + "volume_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(imagebuilder.EbsVolumeType_Values(), false), + }, + }, + }, + }, + "no_device": { + // Use TypeBool to allow an "unspecified" value of false, + // since the API uses an empty string ("") as true and + // this is not compatible with TypeString's zero value. + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "virtual_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 1024), + }, + }, + }, + }, + "component": { + Type: schema.TypeSet, + Required: true, + ForceNew: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "component_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + }, + }, + }, + "date_created": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1024), + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 126), + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "parent_image": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 126), + }, + "platform": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + "version": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + }, + } +} + +func resourceAwsImageBuilderImageRecipeCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).imagebuilderconn + + input := &imagebuilder.CreateImageRecipeInput{ + ClientToken: aws.String(resource.UniqueId()), + } + + if v, ok := d.GetOk("block_device_mapping"); ok && v.(*schema.Set).Len() > 0 { + input.BlockDeviceMappings = expandImageBuilderInstanceBlockDeviceMappings(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("component"); ok && v.(*schema.Set).Len() > 0 { + input.Components = expandImageBuilderComponentConfigurations(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("name"); ok { + input.Name = aws.String(v.(string)) + } + + if v, ok := d.GetOk("parent_image"); ok { + input.ParentImage = aws.String(v.(string)) + } + + if v, ok := d.GetOk("tags"); ok { + input.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().ImagebuilderTags() + } + + if v, ok := d.GetOk("version"); ok { + input.SemanticVersion = aws.String(v.(string)) + } + + output, err := conn.CreateImageRecipe(input) + + if err != nil { + return fmt.Errorf("error creating Image Builder Image Recipe: %w", err) + } + + if output == nil { + return fmt.Errorf("error creating Image Builder Image Recipe: empty response") + } + + d.SetId(aws.StringValue(output.ImageRecipeArn)) + + return resourceAwsImageBuilderImageRecipeRead(d, meta) +} + +func resourceAwsImageBuilderImageRecipeRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).imagebuilderconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + input := &imagebuilder.GetImageRecipeInput{ + ImageRecipeArn: aws.String(d.Id()), + } + + output, err := conn.GetImageRecipe(input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, imagebuilder.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Image Builder Image Recipe (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error getting Image Builder Image Recipe (%s): %w", d.Id(), err) + } + + if output == nil || output.ImageRecipe == nil { + return fmt.Errorf("error getting Image Builder Image Recipe (%s): empty response", d.Id()) + } + + imageRecipe := output.ImageRecipe + + d.Set("arn", imageRecipe.Arn) + d.Set("block_device_mapping", flattenImageBuilderInstanceBlockDeviceMappings(imageRecipe.BlockDeviceMappings)) + d.Set("component", flattenImageBuilderComponentConfigurations(imageRecipe.Components)) + d.Set("date_created", imageRecipe.DateCreated) + d.Set("description", imageRecipe.Description) + d.Set("name", imageRecipe.Name) + d.Set("owner", imageRecipe.Owner) + d.Set("parent_image", imageRecipe.ParentImage) + d.Set("platform", imageRecipe.Platform) + d.Set("tags", keyvaluetags.ImagebuilderKeyValueTags(imageRecipe.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()) + d.Set("version", imageRecipe.Version) + + return nil +} + +func resourceAwsImageBuilderImageRecipeUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).imagebuilderconn + + if d.HasChange("tags") { + o, n := d.GetChange("tags") + + if err := keyvaluetags.ImagebuilderUpdateTags(conn, d.Id(), o, n); err != nil { + return fmt.Errorf("error updating tags for Image Builder Image Recipe (%s): %w", d.Id(), err) + } + } + + return resourceAwsImageBuilderImageRecipeRead(d, meta) +} + +func resourceAwsImageBuilderImageRecipeDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).imagebuilderconn + + input := &imagebuilder.DeleteImageRecipeInput{ + ImageRecipeArn: aws.String(d.Id()), + } + + _, err := conn.DeleteImageRecipe(input) + + if tfawserr.ErrCodeEquals(err, imagebuilder.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Image Builder Image Recipe (%s): %w", d.Id(), err) + } + + return nil +} + +func expandImageBuilderComponentConfiguration(tfMap map[string]interface{}) *imagebuilder.ComponentConfiguration { + if tfMap == nil { + return nil + } + + apiObject := &imagebuilder.ComponentConfiguration{} + + if v, ok := tfMap["component_arn"].(string); ok && v != "" { + apiObject.ComponentArn = aws.String(v) + } + + return apiObject +} + +func expandImageBuilderComponentConfigurations(tfList []interface{}) []*imagebuilder.ComponentConfiguration { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*imagebuilder.ComponentConfiguration + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandImageBuilderComponentConfiguration(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func expandImageBuilderEbsInstanceBlockDeviceSpecification(tfMap map[string]interface{}) *imagebuilder.EbsInstanceBlockDeviceSpecification { + if tfMap == nil { + return nil + } + + apiObject := &imagebuilder.EbsInstanceBlockDeviceSpecification{} + + if v, ok := tfMap["delete_on_termination"].(string); ok && v != "" { + vBool, _ := strconv.ParseBool(v) // ignore error as previously validatated + apiObject.DeleteOnTermination = aws.Bool(vBool) + } + + if v, ok := tfMap["encrypted"].(string); ok && v != "" { + vBool, _ := strconv.ParseBool(v) // ignore error as previously validatated + apiObject.Encrypted = aws.Bool(vBool) + } + + if v, ok := tfMap["iops"].(int); ok && v != 0 { + apiObject.Iops = aws.Int64(int64(v)) + } + + if v, ok := tfMap["kms_key_id"].(string); ok && v != "" { + apiObject.KmsKeyId = aws.String(v) + } + + if v, ok := tfMap["snapshot_id"].(string); ok && v != "" { + apiObject.SnapshotId = aws.String(v) + } + + if v, ok := tfMap["volume_size"].(int); ok && v != 0 { + apiObject.VolumeSize = aws.Int64(int64(v)) + } + + if v, ok := tfMap["volume_type"].(string); ok && v != "" { + apiObject.VolumeType = aws.String(v) + } + + return apiObject +} + +func expandImageBuilderInstanceBlockDeviceMapping(tfMap map[string]interface{}) *imagebuilder.InstanceBlockDeviceMapping { + if tfMap == nil { + return nil + } + + apiObject := &imagebuilder.InstanceBlockDeviceMapping{} + + if v, ok := tfMap["device_name"].(string); ok && v != "" { + apiObject.DeviceName = aws.String(v) + } + + if v, ok := tfMap["ebs"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + apiObject.Ebs = expandImageBuilderEbsInstanceBlockDeviceSpecification(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["no_device"].(bool); ok && v { + apiObject.NoDevice = aws.String("") + } + + if v, ok := tfMap["virtual_name"].(string); ok && v != "" { + apiObject.VirtualName = aws.String(v) + } + + return apiObject +} + +func expandImageBuilderInstanceBlockDeviceMappings(tfList []interface{}) []*imagebuilder.InstanceBlockDeviceMapping { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*imagebuilder.InstanceBlockDeviceMapping + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandImageBuilderInstanceBlockDeviceMapping(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenImageBuilderComponentConfiguration(apiObject *imagebuilder.ComponentConfiguration) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.ComponentArn; v != nil { + tfMap["component_arn"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenImageBuilderComponentConfigurations(apiObjects []*imagebuilder.ComponentConfiguration) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenImageBuilderComponentConfiguration(apiObject)) + } + + return tfList +} + +func flattenImageBuilderEbsInstanceBlockDeviceSpecification(apiObject *imagebuilder.EbsInstanceBlockDeviceSpecification) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.DeleteOnTermination; v != nil { + tfMap["delete_on_termination"] = strconv.FormatBool(aws.BoolValue(v)) + } + + if v := apiObject.Encrypted; v != nil { + tfMap["encrypted"] = strconv.FormatBool(aws.BoolValue(v)) + } + + if v := apiObject.Iops; v != nil { + tfMap["iops"] = aws.Int64Value(v) + } + + if v := apiObject.KmsKeyId; v != nil { + tfMap["kms_key_id"] = aws.StringValue(v) + } + + if v := apiObject.SnapshotId; v != nil { + tfMap["snapshot_id"] = aws.StringValue(v) + } + + if v := apiObject.VolumeSize; v != nil { + tfMap["volume_size"] = aws.Int64Value(v) + } + + if v := apiObject.VolumeType; v != nil { + tfMap["volume_type"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenImageBuilderInstanceBlockDeviceMapping(apiObject *imagebuilder.InstanceBlockDeviceMapping) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.DeviceName; v != nil { + tfMap["device_name"] = aws.StringValue(v) + } + + if v := apiObject.Ebs; v != nil { + tfMap["ebs"] = []interface{}{flattenImageBuilderEbsInstanceBlockDeviceSpecification(v)} + } + + if v := apiObject.NoDevice; v != nil { + tfMap["no_device"] = true + } + + if v := apiObject.VirtualName; v != nil { + tfMap["virtual_name"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenImageBuilderInstanceBlockDeviceMappings(apiObjects []*imagebuilder.InstanceBlockDeviceMapping) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenImageBuilderInstanceBlockDeviceMapping(apiObject)) + } + + return tfList +} diff --git a/aws/resource_aws_imagebuilder_image_recipe_test.go b/aws/resource_aws_imagebuilder_image_recipe_test.go new file mode 100644 index 00000000000..1c0ad2f6e53 --- /dev/null +++ b/aws/resource_aws_imagebuilder_image_recipe_test.go @@ -0,0 +1,862 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/imagebuilder" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func init() { + resource.AddTestSweepers("aws_imagebuilder_image_recipe", &resource.Sweeper{ + Name: "aws_imagebuilder_image_recipe", + F: testSweepImageBuilderImageRecipes, + }) +} + +func testSweepImageBuilderImageRecipes(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*AWSClient).imagebuilderconn + + var sweeperErrs *multierror.Error + + input := &imagebuilder.ListImageRecipesInput{ + Owner: aws.String(imagebuilder.OwnershipSelf), + } + + err = conn.ListImageRecipesPages(input, func(page *imagebuilder.ListImageRecipesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, imageRecipeSummary := range page.ImageRecipeSummaryList { + if imageRecipeSummary == nil { + continue + } + + arn := aws.StringValue(imageRecipeSummary.Arn) + + r := resourceAwsImageBuilderImageRecipe() + d := r.Data(nil) + d.SetId(arn) + + err := r.Delete(d, client) + + if err != nil { + sweeperErr := fmt.Errorf("error deleting Image Builder Image Recipe (%s): %w", arn, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + continue + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Image Builder Image Recipe sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Image Builder Image Recipes: %w", err)) + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAwsImageBuilderImageRecipe_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "imagebuilder", regexp.MustCompile(fmt.Sprintf("image-recipe/%s/1.0.0", rName))), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "0"), + resource.TestCheckResourceAttr(resourceName, "component.#", "1"), + testAccCheckResourceAttrRfc3339(resourceName, "date_created"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrAccountID(resourceName, "owner"), + testAccCheckResourceAttrRegionalARNAccountID(resourceName, "parent_image", "imagebuilder", "aws", "image/amazon-linux-2-x86/x.x.x"), + resource.TestCheckResourceAttr(resourceName, "platform", imagebuilder.PlatformLinux), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "version", "1.0.0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsImageBuilderImageRecipe(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_DeviceName(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingDeviceName(rName, "/dev/xvdb"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "block_device_mapping.*", map[string]string{ + "device_name": "/dev/xvdb", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_Ebs_DeleteOnTermination(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsDeleteOnTermination(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "block_device_mapping.*", map[string]string{ + "ebs.0.delete_on_termination": "true", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_Ebs_Encrypted(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsEncrypted(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "block_device_mapping.*", map[string]string{ + "ebs.0.encrypted": "true", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_Ebs_Iops(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsIops(rName, 100), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "block_device_mapping.*", map[string]string{ + "ebs.0.iops": "100", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_Ebs_KmsKeyId(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + kmsKeyResourceName := "aws_kms_key.test" + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsKmsKeyId(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "block_device_mapping.*.ebs.0.kms_key_id", kmsKeyResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_Ebs_SnapshotId(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + ebsSnapshotResourceName := "aws_ebs_snapshot.test" + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsSnapshotId(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "block_device_mapping.*.ebs.0.snapshot_id", ebsSnapshotResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_Ebs_VolumeSize(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsVolumeSize(rName, 20), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "block_device_mapping.*", map[string]string{ + "ebs.0.volume_size": "20", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_Ebs_VolumeType(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsVolumeType(rName, imagebuilder.EbsVolumeTypeGp2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "block_device_mapping.*", map[string]string{ + "ebs.0.volume_type": imagebuilder.EbsVolumeTypeGp2, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_NoDevice(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingNoDevice(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "block_device_mapping.*", map[string]string{ + "no_device": "true", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_BlockDeviceMapping_VirtualName(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingVirtualName(rName, "ephemeral0"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "block_device_mapping.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "block_device_mapping.*", map[string]string{ + "virtual_name": "ephemeral0", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_Description(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigDescription(rName, "description1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", "description1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsImageBuilderImageRecipe_Tags(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_imagebuilder_image_recipe.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsImageBuilderImageRecipeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsImageBuilderImageRecipeConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsImageBuilderImageRecipeConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAwsImageBuilderImageRecipeConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsImageBuilderImageRecipeExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAwsImageBuilderImageRecipeDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).imagebuilderconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_imagebuilder_image_recipe" { + continue + } + + input := &imagebuilder.GetImageRecipeInput{ + ImageRecipeArn: aws.String(rs.Primary.ID), + } + + output, err := conn.GetImageRecipe(input) + + if tfawserr.ErrCodeEquals(err, imagebuilder.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return fmt.Errorf("error getting Image Builder Image Recipe (%s): %w", rs.Primary.ID, err) + } + + if output != nil { + return fmt.Errorf("Image Builder Image Recipe (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckAwsImageBuilderImageRecipeExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).imagebuilderconn + + input := &imagebuilder.GetImageRecipeInput{ + ImageRecipeArn: aws.String(rs.Primary.ID), + } + + _, err := conn.GetImageRecipe(input) + + if err != nil { + return fmt.Errorf("error getting Image Builder Image Recipe (%s): %w", rs.Primary.ID, err) + } + + return nil + } +} + +func testAccAwsImageBuilderImageRecipeConfigBase(rName string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +data "aws_partition" "current" {} + +resource "aws_imagebuilder_component" "test" { + data = yamlencode({ + phases = [{ + name = "build" + steps = [{ + action = "ExecuteBash" + inputs = { + commands = ["echo 'hello world'"] + } + name = "example" + onFailure = "Continue" + }] + }] + schemaVersion = 1.0 + }) + name = %[1]q + platform = "Linux" + version = "1.0.0" +} +`, rName) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingDeviceName(rName string, deviceName string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + device_name = %[2]q + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, deviceName)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsDeleteOnTermination(rName string, deleteOnTermination bool) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + ebs { + delete_on_termination = %[2]t + } + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, deleteOnTermination)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsEncrypted(rName string, encrypted bool) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + ebs { + encrypted = %[2]t + } + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, encrypted)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsIops(rName string, iops int) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + ebs { + iops = %[2]d + } + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, iops)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsKmsKeyId(rName string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_kms_key" "test" { + deletion_window_in_days = 7 +} + +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + ebs { + kms_key_id = aws_kms_key.test.arn + } + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsSnapshotId(rName string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_ebs_volume" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + size = 1 +} + +resource "aws_ebs_snapshot" "test" { + volume_id = aws_ebs_volume.test.id +} + +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + ebs { + snapshot_id = aws_ebs_snapshot.test.id + } + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsVolumeSize(rName string, volumeSize int) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + ebs { + volume_size = %[2]d + } + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, volumeSize)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingEbsVolumeType(rName string, volumeType string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + ebs { + volume_type = %[2]q + } + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, volumeType)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingNoDevice(rName string, noDevice bool) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + no_device = %[2]t + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, noDevice)) +} + +func testAccAwsImageBuilderImageRecipeConfigBlockDeviceMappingVirtualName(rName string, virtualName string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + block_device_mapping { + virtual_name = %[2]q + } + + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, virtualName)) +} + +func testAccAwsImageBuilderImageRecipeConfigDescription(rName string, description string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + component { + component_arn = aws_imagebuilder_component.test.arn + } + + description = %[2]q + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName, description)) +} + +func testAccAwsImageBuilderImageRecipeConfigName(rName string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +`, rName)) +} + +func testAccAwsImageBuilderImageRecipeConfigTags1(rName string, tagKey1 string, tagValue1 string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1)) +} + +func testAccAwsImageBuilderImageRecipeConfigTags2(rName string, tagKey1 string, tagValue1 string, tagKey2 string, tagValue2 string) string { + return composeConfig( + testAccAwsImageBuilderImageRecipeConfigBase(rName), + fmt.Sprintf(` +resource "aws_imagebuilder_image_recipe" "test" { + component { + component_arn = aws_imagebuilder_component.test.arn + } + + name = %[1]q + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} diff --git a/website/docs/d/imagebuilder_image_recipe.html.markdown b/website/docs/d/imagebuilder_image_recipe.html.markdown new file mode 100644 index 00000000000..bf072a68e71 --- /dev/null +++ b/website/docs/d/imagebuilder_image_recipe.html.markdown @@ -0,0 +1,52 @@ +--- +subcategory: "Image Builder" +layout: "aws" +page_title: "AWS: aws_imagebuilder_image_recipe" +description: |- + Provides details about an Image Builder Image Recipe +--- + +# Data Source: aws_imagebuilder_image_recipe + +Provides details about an Image Builder Image Recipe. + +## Example Usage + +```hcl +data "aws_imagebuilder_image_recipe" "example" { + arn = "arn:aws:imagebuilder:us-east-1:aws:image-recipe/example/1.0.0" +} +``` + +## Argument Reference + +The following arguments are required: + +* `arn` - (Required) Amazon Resource Name (ARN) of the image recipe. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `block_device_mapping` - Set of objects with block device mappings for the the image recipe. + * `device_name` - Name of the device. For example, `/dev/sda` or `/dev/xvdb`. + * `ebs` - Single list of object with Elastic Block Storage (EBS) block device mapping settings. + * `delete_on_termination` - Whether to delete the volume on termination. Defaults to unset, which is the value inherited from the parent image. + * `encrypted` - Whether to encrypt the volume. Defaults to unset, which is the value inherited from the parent image. + * `iops` - Number of Input/Output (I/O) operations per second to provision for an `io1` or `io2` volume. + * `kms_key_id` - Amazon Resource Name (ARN) of the Key Management Service (KMS) Key for encryption. + * `snapshot_id` - Identifier of the EC2 Volume Snapshot. + * `volume_size` - Size of the volume, in GiB. + * `volume_type` - Type of the volume. For example, `gp2` or `io2`. + * `no_device` - Whether to remove a mapping from the parent image. + * `virtual_name` - Virtual device name. For example, `ephemeral0`. Instance store volumes are numbered starting from 0. +* `component` - Set of objects with components for the image recipe. + * `component_arn` - Amazon Resource Name (ARN) of the Image Builder Component. +* `date_created` - Date the image recipe was created. +* `description` - Description of the image recipe. +* `name` - Name of the image recipe. +* `owner` - Owner of the image recipe. +* `parent_image` - Platform of the image recipe. +* `platform` - Platform of the image recipe. +* `tags` - (Optional) Key-value map of resource tags for the image recipe. +* `version` - Version of the image recipe. diff --git a/website/docs/r/imagebuilder_image_recipe.html.markdown b/website/docs/r/imagebuilder_image_recipe.html.markdown new file mode 100644 index 00000000000..fed1b559df8 --- /dev/null +++ b/website/docs/r/imagebuilder_image_recipe.html.markdown @@ -0,0 +1,94 @@ +--- +subcategory: "Image Builder" +layout: "aws" +page_title: "AWS: aws_imagebuilder_image_recipe" +description: |- + Manage an Image Builder Image Recipe +--- + +# Resource: aws_imagebuilder_image_recipe + +Manages an Image Builder Image Recipe. + +## Example Usage + +```hcl +resource "aws_imagebuilder_image_recipe" "example" { + block_device_mapping { + device_name = "/dev/xvdb" + + ebs { + delete_on_termination = true + volume_size = 100 + volume_type = "gp2" + } + } + + component { + component_arn = aws_imagebuilder_component.example.arn + } + + name = "example" + parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x" + version = "1.0.0" +} +``` + +## Argument Reference + +The following arguments are required: + +* `component` - (Required) Configuration block(s) with components for the image recipe. Detailed below. +* `name` - (Required) Name of the image recipe. +* `parent_image` - (Required) Platform of the image recipe. +* `version` - (Required) Version of the image recipe. + +The following attributes are optional: + +* `block_device_mapping` - (Optional) Configuration block(s) with block device mappings for the the image recipe. Detailed below. +* `description` - (Optional) Description of the image recipe. +* `tags` - (Optional) Key-value map of resource tags for the image recipe. + +### block_device_mapping + +The following arguments are optional: + +* `device_name` - (Optional) Name of the device. For example, `/dev/sda` or `/dev/xvdb`. +* `ebs` - (Optional) Configuration block with Elastic Block Storage (EBS) block device mapping settings. Detailed below. +* `no_device` - (Optional) Set to `true` to remove a mapping from the parent image. +* `virtual_name` - (Optional) Virtual device name. For example, `ephemeral0`. Instance store volumes are numbered starting from 0. + +#### ebs + +The following arguments are optional: + +* `delete_on_termination` - (Optional) Whether to delete the volume on termination. Defaults to unset, which is the value inherited from the parent image. +* `encrypted` - (Optional) Whether to encrypt the volume. Defaults to unset, which is the value inherited from the parent image. +* `iops` - (Optional) Number of Input/Output (I/O) operations per second to provision for an `io1` or `io2` volume. +* `kms_key_id` - (Optional) Amazon Resource Name (ARN) of the Key Management Service (KMS) Key for encryption. +* `snapshot_id` - (Optional) Identifier of the EC2 Volume Snapshot. +* `volume_size` - (Optional) Size of the volume, in GiB. +* `volume_type` - (Optional) Type of the volume. For example, `gp2` or `io2`. + +### component + +The following arguments are required: + +* `component_arn` - (Required) Amazon Resource Name (ARN) of the Image Builder Component to associate. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - (Required) Amazon Resource Name (ARN) of the image recipe. +* `date_created` - Date the image recipe was created. +* `owner` - Owner of the image recipe. +* `platform` - Platform of the image recipe. + +## Import + +`aws_imagebuilder_image_recipe` resources can be imported by using the Amazon Resource Name (ARN), e.g. + +``` +$ terraform import aws_imagebuilder_image_recipe.example arn:aws:imagebuilder:us-east-1:123456789012:image-recipe/example/1.0.0 +```