diff --git a/.changelog/21036.txt b/.changelog/21036.txt new file mode 100644 index 00000000000..fbdbdace43f --- /dev/null +++ b/.changelog/21036.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appstream_image_builder +``` \ No newline at end of file diff --git a/aws/internal/service/appstream/finder/finder.go b/aws/internal/service/appstream/finder/finder.go index 3dea74dac63..4cc7aa90b0b 100644 --- a/aws/internal/service/appstream/finder/finder.go +++ b/aws/internal/service/appstream/finder/finder.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/appstream" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/lister" ) // StackByName Retrieve a appstream stack by name @@ -55,3 +56,40 @@ func FleetByName(ctx context.Context, conn *appstream.AppStream, name string) (* return fleet, nil } + +// ImageBuilderByName Retrieve a appstream ImageBuilder by name +func ImageBuilderByName(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.ImageBuilder, error) { + input := &appstream.DescribeImageBuildersInput{ + Names: []*string{aws.String(name)}, + } + + var result *appstream.ImageBuilder + + err := lister.DescribeImageBuildersPagesWithContext(ctx, conn, input, func(page *appstream.DescribeImageBuildersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, imageBuilder := range page.ImageBuilders { + if imageBuilder == nil { + continue + } + if aws.StringValue(imageBuilder.Name) == name { + result = imageBuilder + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if result == nil { + return nil, nil + } + + return result, nil +} diff --git a/aws/internal/service/appstream/lister/list.go b/aws/internal/service/appstream/lister/list.go new file mode 100644 index 00000000000..1f9ea59c7f7 --- /dev/null +++ b/aws/internal/service/appstream/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=DescribeImageBuilders github.com/aws/aws-sdk-go/service/appstream + +package lister diff --git a/aws/internal/service/appstream/lister/list_pages_gen.go b/aws/internal/service/appstream/lister/list_pages_gen.go new file mode 100644 index 00000000000..cfc91fd780b --- /dev/null +++ b/aws/internal/service/appstream/lister/list_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=DescribeImageBuilders github.com/aws/aws-sdk-go/service/appstream"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" +) + +func DescribeImageBuildersPages(conn *appstream.AppStream, input *appstream.DescribeImageBuildersInput, fn func(*appstream.DescribeImageBuildersOutput, bool) bool) error { + return DescribeImageBuildersPagesWithContext(context.Background(), conn, input, fn) +} + +func DescribeImageBuildersPagesWithContext(ctx context.Context, conn *appstream.AppStream, input *appstream.DescribeImageBuildersInput, fn func(*appstream.DescribeImageBuildersOutput, bool) bool) error { + for { + output, err := conn.DescribeImageBuildersWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/internal/service/appstream/waiter/status.go b/aws/internal/service/appstream/waiter/status.go index d720c5aaa69..2d6ee9365dc 100644 --- a/aws/internal/service/appstream/waiter/status.go +++ b/aws/internal/service/appstream/waiter/status.go @@ -41,3 +41,20 @@ func FleetState(ctx context.Context, conn *appstream.AppStream, name string) res return fleet, aws.StringValue(fleet.State), nil } } + +//ImageBuilderState fetches the ImageBuilder and its state +func ImageBuilderState(ctx context.Context, conn *appstream.AppStream, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + imageBuilder, err := finder.ImageBuilderByName(ctx, conn, name) + + if err != nil { + return nil, "", err + } + + if imageBuilder == nil { + return nil, "", nil + } + + return imageBuilder, aws.StringValue(imageBuilder.State), nil + } +} diff --git a/aws/internal/service/appstream/waiter/waiter.go b/aws/internal/service/appstream/waiter/waiter.go index c6f0db45833..234ef7b3091 100644 --- a/aws/internal/service/appstream/waiter/waiter.go +++ b/aws/internal/service/appstream/waiter/waiter.go @@ -2,10 +2,14 @@ package waiter import ( "context" + "fmt" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -16,6 +20,9 @@ const ( FleetStateTimeout = 180 * time.Minute // FleetOperationTimeout Maximum amount of time to wait for Fleet operation eventual consistency FleetOperationTimeout = 15 * time.Minute + // ImageBuilderStateTimeout Maximum amount of time to wait for the ImageBuilderState to be RUNNING + // or for the ImageBuilder to be deleted + ImageBuilderStateTimeout = 60 * time.Minute ) // StackStateDeleted waits for a deleted stack @@ -70,3 +77,59 @@ func FleetStateStopped(ctx context.Context, conn *appstream.AppStream, name stri return nil, err } + +// ImageBuilderStateRunning waits for a ImageBuilder running +func ImageBuilderStateRunning(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.ImageBuilder, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{appstream.ImageBuilderStatePending}, + Target: []string{appstream.ImageBuilderStateRunning}, + Refresh: ImageBuilderState(ctx, conn, name), + Timeout: ImageBuilderStateTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.ImageBuilder); ok { + if state, errors := aws.StringValue(output.State), output.ImageBuilderErrors; state == appstream.ImageBuilderStateFailed && len(errors) > 0 { + var errs *multierror.Error + + for _, err := range errors { + errs = multierror.Append(errs, fmt.Errorf("%s: %s", aws.StringValue(err.ErrorCode), aws.StringValue(err.ErrorMessage))) + } + + tfresource.SetLastError(err, errs.ErrorOrNil()) + } + + return output, err + } + + return nil, err +} + +// ImageBuilderStateDeleted waits for a ImageBuilder deleted +func ImageBuilderStateDeleted(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.ImageBuilder, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{appstream.ImageBuilderStatePending, appstream.ImageBuilderStateDeleting}, + Target: []string{}, + Refresh: ImageBuilderState(ctx, conn, name), + Timeout: ImageBuilderStateTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.ImageBuilder); ok { + if state, errors := aws.StringValue(output.State), output.ImageBuilderErrors; state == appstream.ImageBuilderStateFailed && len(errors) > 0 { + var errs *multierror.Error + + for _, err := range errors { + errs = multierror.Append(errs, fmt.Errorf("%s: %s", aws.StringValue(err.ErrorCode), aws.StringValue(err.ErrorMessage))) + } + + tfresource.SetLastError(err, errs.ErrorOrNil()) + } + + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index 4e26b991cd9..b93b568d848 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -543,6 +543,7 @@ func Provider() *schema.Provider { "aws_apprunner_service": resourceAwsAppRunnerService(), "aws_appstream_stack": resourceAwsAppStreamStack(), "aws_appstream_fleet": resourceAwsAppStreamFleet(), + "aws_appstream_image_builder": resourceAwsAppStreamImageBuilder(), "aws_appsync_api_key": resourceAwsAppsyncApiKey(), "aws_appsync_datasource": resourceAwsAppsyncDatasource(), "aws_appsync_function": resourceAwsAppsyncFunction(), diff --git a/aws/resource_aws_appstream_image_builder.go b/aws/resource_aws_appstream_image_builder.go new file mode 100644 index 00000000000..0c5c06530a1 --- /dev/null +++ b/aws/resource_aws_appstream_image_builder.go @@ -0,0 +1,364 @@ +package aws + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/waiter" +) + +func resourceAwsAppStreamImageBuilder() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsAppStreamImageBuilderCreate, + ReadWithoutTimeout: resourceAwsAppStreamImageBuilderRead, + UpdateWithoutTimeout: resourceAwsAppStreamImageBuilderUpdate, + DeleteWithoutTimeout: resourceAwsAppStreamImageBuilderDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "access_endpoint": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + MinItems: 1, + MaxItems: 4, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(appstream.AccessEndpointType_Values(), false), + }, + "vpce_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "appstream_agent_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 100), + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 256), + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 100), + }, + "domain_join_info": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "directory_name": { + Type: schema.TypeString, + Optional: true, + }, + "organizational_unit_distinguished_name": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "enable_default_internet_access": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + ForceNew: true, + }, + "iam_role_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "image_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ExactlyOneOf: []string{"image_arn", "image_name"}, + }, + "image_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ExactlyOneOf: []string{"image_name", "image_arn"}, + }, + "instance_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "vpc_config": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "security_group_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "subnet_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + CustomizeDiff: SetTagsDiff, + } +} + +func resourceAwsAppStreamImageBuilderCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + name := d.Get("name").(string) + + input := &appstream.CreateImageBuilderInput{ + Name: aws.String(name), + InstanceType: aws.String(d.Get("instance_type").(string)), + } + + if v, ok := d.GetOk("access_endpoint"); ok && v.(*schema.Set).Len() > 0 { + input.AccessEndpoints = expandAccessEndpoints(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("appstream_agent_version"); ok { + input.AppstreamAgentVersion = aws.String(v.(string)) + } + + if v, ok := d.GetOk("display_name"); ok { + input.DisplayName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("domain_join_info"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DomainJoinInfo = expandDomainJoinInfo(v.([]interface{})) + } + + if v, ok := d.GetOk("enable_default_internet_access"); ok { + input.EnableDefaultInternetAccess = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("image_name"); ok { + input.ImageName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("iam_role_arn"); ok { + input.IamRoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("vpc_config"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.VpcConfig = expandAppStreamImageBuilderVpcConfig(v.([]interface{})) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AppstreamTags() + } + + output, err := conn.CreateImageBuilderWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Appstream ImageBuilder (%s): %w", name, err)) + } + + d.SetId(aws.StringValue(output.ImageBuilder.Name)) + + if _, err = waiter.ImageBuilderStateRunning(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream ImageBuilder (%s) to be running: %w", d.Id(), err)) + } + + return resourceAwsAppStreamImageBuilderRead(ctx, d, meta) +} + +func resourceAwsAppStreamImageBuilderRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + imageBuilder, err := finder.ImageBuilderByName(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Appstream ImageBuilder (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Appstream ImageBuilder (%s): %w", d.Id(), err)) + } + + if imageBuilder == nil { + return diag.FromErr(fmt.Errorf("error reading Appstream ImageBuilder (%s): not found after creation", d.Id())) + } + + arn := aws.StringValue(imageBuilder.Arn) + + d.Set("appstream_agent_version", imageBuilder.AppstreamAgentVersion) + d.Set("arn", arn) + d.Set("created_time", aws.TimeValue(imageBuilder.CreatedTime).Format(time.RFC3339)) + d.Set("description", imageBuilder.Description) + d.Set("display_name", imageBuilder.DisplayName) + d.Set("enable_default_internet_access", imageBuilder.EnableDefaultInternetAccess) + d.Set("image_arn", imageBuilder.ImageArn) + d.Set("iam_role_arn", imageBuilder.IamRoleArn) + d.Set("instance_type", imageBuilder.InstanceType) + + if err = d.Set("access_endpoint", flattenAccessEndpoints(imageBuilder.AccessEndpoints)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream ImageBuilder (%s): %w", "access_endpoints", d.Id(), err)) + } + if err = d.Set("domain_join_info", flattenDomainInfo(imageBuilder.DomainJoinInfo)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream ImageBuilder (%s): %w", "domain_join_info", d.Id(), err)) + } + + if err = d.Set("vpc_config", flattenVpcConfig(imageBuilder.VpcConfig)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream ImageBuilder (%s): %w", "vpc_config", d.Id(), err)) + } + + d.Set("name", imageBuilder.Name) + d.Set("state", imageBuilder.State) + + tags, err := keyvaluetags.AppstreamListTags(conn, arn) + if err != nil { + return diag.FromErr(fmt.Errorf("error listing tags for AppStream ImageBuilder (%s): %w", arn, err)) + } + + tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags: %w", err)) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting tags_all: %w", err)) + } + + return nil +} + +func resourceAwsAppStreamImageBuilderUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + if d.HasChange("tags_all") { + conn := meta.(*AWSClient).appstreamconn + + o, n := d.GetChange("tags_all") + + if err := keyvaluetags.AppstreamUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.FromErr(fmt.Errorf("error updating tags for AppStream ImageBuilder (%s): %w", d.Id(), err)) + } + } + + return resourceAwsAppStreamImageBuilderRead(ctx, d, meta) +} + +func resourceAwsAppStreamImageBuilderDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + _, err := conn.DeleteImageBuilderWithContext(ctx, &appstream.DeleteImageBuilderInput{ + Name: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting Appstream ImageBuilder (%s): %w", d.Id(), err)) + } + + if _, err = waiter.ImageBuilderStateDeleted(ctx, conn, d.Id()); err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return nil + } + return diag.FromErr(fmt.Errorf("error waiting for Appstream ImageBuilder (%s) to be deleted: %w", d.Id(), err)) + } + + return nil +} + +func expandAppStreamImageBuilderVpcConfig(tfList []interface{}) *appstream.VpcConfig { + if len(tfList) == 0 { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + + if !ok { + return nil + } + + apiObject := &appstream.VpcConfig{} + + if v, ok := tfMap["security_group_ids"].(*schema.Set); ok && v.Len() > 0 { + apiObject.SecurityGroupIds = expandStringSet(v) + } + if v, ok := tfMap["subnet_ids"].(*schema.Set); ok && v.Len() > 0 { + apiObject.SubnetIds = expandStringSet(v) + } + + return apiObject +} diff --git a/aws/resource_aws_appstream_image_builder_test.go b/aws/resource_aws_appstream_image_builder_test.go new file mode 100644 index 00000000000..fd5de3f34a1 --- /dev/null +++ b/aws/resource_aws_appstream_image_builder_test.go @@ -0,0 +1,345 @@ +package aws + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appstream" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/lister" +) + +func init() { + resource.AddTestSweepers("aws_appstream_image_builder", &resource.Sweeper{ + Name: "aws_appstream_image_builder", + F: testSweepAppStreamImageBuilder, + Dependencies: []string{ + "aws_vpc", + "aws_subnet", + }, + }) +} + +func testSweepAppStreamImageBuilder(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + conn := client.(*AWSClient).appstreamconn + sweepResources := make([]*testSweepResource, 0) + var errs *multierror.Error + + input := &appstream.DescribeImageBuildersInput{} + + err = lister.DescribeImageBuildersPagesWithContext(context.TODO(), conn, input, func(page *appstream.DescribeImageBuildersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, imageBuilder := range page.ImageBuilders { + if imageBuilder == nil { + continue + } + + id := aws.StringValue(imageBuilder.Name) + + r := resourceAwsAppStreamImageBuilder() + d := r.Data(nil) + d.SetId(id) + + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error listing AppStream Image Builders: %w", err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping AppStream Image Builders for %s: %w", region, err)) + } + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping AppStream Image Builders sweep for %s: %s", region, err) + return nil // In case we have completed some pages, but had errors + } + + return errs.ErrorOrNil() +} + +func TestAccAwsAppStreamImageBuilder_basic(t *testing.T) { + resourceName := "aws_appstream_image_builder.test" + instanceType := "stream.standard.small" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamImageBuilderDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamImageBuilderConfig(instanceType, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "state", appstream.ImageBuilderStateRunning), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_name"}, + }, + }, + }) +} + +func TestAccAwsAppStreamImageBuilder_disappears(t *testing.T) { + resourceName := "aws_appstream_image_builder.test" + instanceType := "stream.standard.medium" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamImageBuilderDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamImageBuilderConfig(instanceType, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppStreamImageBuilder(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsAppStreamImageBuilder_complete(t *testing.T) { + resourceName := "aws_appstream_image_builder.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + descriptionUpdated := "Updated Description of a test" + instanceType := "stream.standard.small" + instanceTypeUpdate := "stream.standard.medium" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamImageBuilderDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamImageBuilderConfigComplete(rName, description, instanceType), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.ImageBuilderStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_name"}, + }, + { + Config: testAccAwsAppStreamImageBuilderConfigComplete(rName, descriptionUpdated, instanceTypeUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.ImageBuilderStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceTypeUpdate), + resource.TestCheckResourceAttr(resourceName, "description", descriptionUpdated), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_name"}, + }, + }, + }) +} + +func TestAccAwsAppStreamImageBuilder_Tags(t *testing.T) { + resourceName := "aws_appstream_image_builder.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + instanceType := "stream.standard.small" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamImageBuilderDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamImageBuilderConfigTags1(instanceType, rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"image_name"}, + }, + { + Config: testAccAwsAppStreamImageBuilderConfigTags2(instanceType, rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAwsAppStreamImageBuilderConfigTags1(instanceType, rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamImageBuilderExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAwsAppStreamImageBuilderExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + + imageBuilder, err := finder.ImageBuilderByName(context.Background(), conn, rs.Primary.ID) + + if err != nil { + return err + } + + if imageBuilder == nil { + return fmt.Errorf("appstream imageBuilder %q does not exist", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckAwsAppStreamImageBuilderDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appstream_image_builder" { + continue + } + + imageBuilder, err := finder.ImageBuilderByName(context.Background(), conn, rs.Primary.ID) + + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + + if imageBuilder != nil { + return fmt.Errorf("appstream imageBuilder %q still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccAwsAppStreamImageBuilderConfig(instanceType, name string) string { + return fmt.Sprintf(` +resource "aws_appstream_image_builder" "test" { + image_name = "AppStream-WinServer2012R2-07-19-2021" + instance_type = %[1]q + name = %[2]q +} +`, instanceType, name) +} + +func testAccAwsAppStreamImageBuilderConfigComplete(name, description, instanceType string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_subnet" "test" { + availability_zone = data.aws_availability_zones.available.names[1] + cidr_block = "10.1.0.0/24" + vpc_id = aws_vpc.test.id +} + +resource "aws_appstream_image_builder" "test" { + image_name = "AppStream-WinServer2012R2-07-19-2021" + name = %[1]q + description = %[2]q + enable_default_internet_access = false + instance_type = %[3]q + vpc_config { + subnet_ids = [aws_subnet.test.id] + } +} +`, name, description, instanceType)) +} + +func testAccAwsAppStreamImageBuilderConfigTags1(instanceType, name, key, value string) string { + return fmt.Sprintf(` +resource "aws_appstream_image_builder" "test" { + image_name = "AppStream-WinServer2012R2-07-19-2021" + instance_type = %[1]q + name = %[2]q + + tags = { + %[3]q = %[4]q + } +} +`, instanceType, name, key, value) +} + +func testAccAwsAppStreamImageBuilderConfigTags2(instanceType, name, key1, value1, key2, value2 string) string { + return fmt.Sprintf(` +resource "aws_appstream_image_builder" "test" { + image_name = "AppStream-WinServer2012R2-07-19-2021" + instance_type = %[1]q + name = %[2]q + + tags = { + %[3]q = %[4]q + %[5]q = %[6]q + } +} +`, instanceType, name, key1, value1, key2, value2) +} diff --git a/website/docs/r/appstream_image_builder.html.markdown b/website/docs/r/appstream_image_builder.html.markdown new file mode 100644 index 00000000000..c98ecce87d4 --- /dev/null +++ b/website/docs/r/appstream_image_builder.html.markdown @@ -0,0 +1,92 @@ +--- +subcategory: "AppStream" +layout: "aws" +page_title: "AWS: aws_appstream_image_builder" +description: |- + Provides an AppStream image builder +--- + +# Resource: aws_appstream_image_builder + +Provides an AppStream image builder. + +## Example Usage + +```terraform +resource "aws_appstream_image_builder" "test_fleet" { + name = "Image Builder Name" + description = "Description of a ImageBuilder" + display_name = "Display name of a ImageBuilder" + enable_default_internet_access = false + image_name = "AppStream-WinServer2012R2-07-19-2021" + instance_type = "stream.standard.large" + + vpc_config { + subnet_ids = [aws_subnet.example.id] + } + + tags = { + Name = "Example Image Builder" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `instance_type` - (Required) The instance type to use when launching the image builder. +* `name` - (Required) Unique name for the image builder. + +The following arguments are optional: + +* `access_endpoint` - (Optional) Set of interface VPC endpoint (interface endpoint) objects. Maximum of 4. See below. +* `appstream_agent_version` - (Optional) The version of the AppStream 2.0 agent to use for this image builder. +* `description` - (Optional) Description to display. +* `display_name` - (Optional) Human-readable friendly name for the AppStream image builder. +* `domain_join_info` - (Optional) Configuration block for the name of the directory and organizational unit (OU) to use to join the image builder to a Microsoft Active Directory domain. See below. +* `enable_default_internet_access` - (Optional) Enables or disables default internet access for the image builder. +* `iam_role_arn` - (Optional) ARN of the IAM role to apply to the image builder. +* `image_arn` - (Optional, Required if `image_name` not provided) ARN of the public, private, or shared image to use. +* `image_name` - (Optional, Required if `image_arn` not provided) Name of the image used to create the image builder. +* `vpc_config` - (Optional) Configuration block for the VPC configuration for the image builder. See below. +* `tags` - (Optional) A map of tags to assign to the instance. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +### `access_endpoint` + +The `access_endpoint` block supports the following arguments: + +* `endpoint_type` - (Required) Type of interface endpoint. +* `vpce_id` - (Optional) Identifier (ID) of the VPC in which the interface endpoint is used. + +### `domain_join_info` + +The `domain_join_info` block supports the following arguments: + +* `directory_name` - (Optional) Fully qualified name of the directory (for example, corp.example.com). +* `organizational_unit_distinguished_name` - (Optional) Distinguished name of the organizational unit for computer accounts. + +### `vpc_config` + +The `vpc_config` block supports the following arguments: + +* `security_group_ids` - (Optional) Identifiers of the security groups for the image builder or image builder. +* `subnet_ids` - (Optional) Identifiers of the subnets to which a network interface is attached from the image builder instance or image builder instance. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN of the appstream image builder. +* `created_time` - Date and time, in UTC and extended RFC 3339 format, when the image builder was created. +* `id` - The name of the image builder. +* `state` - State of the image builder. Can be: `PENDING`, `UPDATING_AGENT`, `RUNNING`, `STOPPING`, `STOPPED`, `REBOOTING`, `SNAPSHOTTING`, `DELETING`, `FAILED`, `UPDATING`, `PENDING_QUALIFICATION` +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Import + +`aws_appstream_image_builder` can be imported using the `name`, e.g. + +``` +$ terraform import aws_appstream_image_builder.example imageBuilderExample +```