diff --git a/.changelog/20543.txt b/.changelog/20543.txt new file mode 100755 index 00000000000..bb75acf566f --- /dev/null +++ b/.changelog/20543.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_appstream_fleet +``` \ No newline at end of file diff --git a/aws/data_source_aws_iam_group_test.go b/aws/data_source_aws_iam_group_test.go index 1ac35b1bb51..6fb013ccbb0 100644 --- a/aws/data_source_aws_iam_group_test.go +++ b/aws/data_source_aws_iam_group_test.go @@ -18,7 +18,7 @@ func TestAccAWSDataSourceIAMGroup_basic(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAwsIAMGroupConfig(groupName), + Config: testAccAwsIAMGroupDataSourceConfig(groupName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("data.aws_iam_group.test", "group_id"), resource.TestCheckResourceAttr("data.aws_iam_group.test", "path", "/"), @@ -42,7 +42,7 @@ func TestAccAWSDataSourceIAMGroup_users(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAwsIAMGroupConfigWithUser(groupName, userName, groupMemberShipName, userCount), + Config: testAccAwsIAMGroupDataSourceConfigWithUser(groupName, userName, groupMemberShipName, userCount), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet("data.aws_iam_group.test", "group_id"), resource.TestCheckResourceAttr("data.aws_iam_group.test", "path", "/"), @@ -59,7 +59,7 @@ func TestAccAWSDataSourceIAMGroup_users(t *testing.T) { }) } -func testAccAwsIAMGroupConfig(name string) string { +func testAccAwsIAMGroupDataSourceConfig(name string) string { return fmt.Sprintf(` resource "aws_iam_group" "group" { name = "%s" @@ -72,7 +72,7 @@ data "aws_iam_group" "test" { `, name) } -func testAccAwsIAMGroupConfigWithUser(groupName, userName, membershipName string, userCount int) string { +func testAccAwsIAMGroupDataSourceConfigWithUser(groupName, userName, membershipName string, userCount int) string { return fmt.Sprintf(` resource "aws_iam_group" "group" { name = "%s" diff --git a/aws/data_source_aws_iam_role_test.go b/aws/data_source_aws_iam_role_test.go index b4525df232e..6d93568c040 100644 --- a/aws/data_source_aws_iam_role_test.go +++ b/aws/data_source_aws_iam_role_test.go @@ -20,7 +20,7 @@ func TestAccAWSDataSourceIAMRole_basic(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAwsIAMRoleConfig(roleName), + Config: testAccAwsIAMRoleDataSourceConfig(roleName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), resource.TestCheckResourceAttrPair(dataSourceName, "assume_role_policy", resourceName, "assume_role_policy"), @@ -48,7 +48,7 @@ func TestAccAWSDataSourceIAMRole_tags(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testAccAwsIAMRoleConfig_tags(roleName), + Config: testAccAwsIAMRoleDataSourceConfig_tags(roleName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), resource.TestCheckResourceAttrPair(dataSourceName, "assume_role_policy", resourceName, "assume_role_policy"), @@ -67,7 +67,7 @@ func TestAccAWSDataSourceIAMRole_tags(t *testing.T) { }) } -func testAccAwsIAMRoleConfig(roleName string) string { +func testAccAwsIAMRoleDataSourceConfig(roleName string) string { return fmt.Sprintf(` resource "aws_iam_role" "test" { name = %[1]q @@ -97,7 +97,7 @@ data "aws_iam_role" "test" { `, roleName) } -func testAccAwsIAMRoleConfig_tags(roleName string) string { +func testAccAwsIAMRoleDataSourceConfig_tags(roleName string) string { return fmt.Sprintf(` resource "aws_iam_role" "test" { name = %q diff --git a/aws/internal/service/appstream/finder/finder.go b/aws/internal/service/appstream/finder/finder.go index 8fce64010b9..3dea74dac63 100644 --- a/aws/internal/service/appstream/finder/finder.go +++ b/aws/internal/service/appstream/finder/finder.go @@ -16,12 +16,13 @@ func StackByName(ctx context.Context, conn *appstream.AppStream, name string) (* var stack *appstream.Stack resp, err := conn.DescribeStacksWithContext(ctx, input) + if err != nil { return nil, err } if len(resp.Stacks) > 1 { - return nil, fmt.Errorf("[ERROR] got more than one stack with the name %s", name) + return nil, fmt.Errorf("got more than one stack with the name %s", name) } if len(resp.Stacks) == 1 { @@ -30,3 +31,27 @@ func StackByName(ctx context.Context, conn *appstream.AppStream, name string) (* return stack, nil } + +// FleetByName Retrieve a appstream fleet by name +func FleetByName(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.Fleet, error) { + input := &appstream.DescribeFleetsInput{ + Names: []*string{aws.String(name)}, + } + + var fleet *appstream.Fleet + resp, err := conn.DescribeFleetsWithContext(ctx, input) + + if err != nil { + return nil, err + } + + if len(resp.Fleets) > 1 { + return nil, fmt.Errorf("got more than one fleet with the name %s", name) + } + + if len(resp.Fleets) == 1 { + fleet = resp.Fleets[0] + } + + return fleet, nil +} diff --git a/aws/internal/service/appstream/waiter/status.go b/aws/internal/service/appstream/waiter/status.go index 94428acc414..d720c5aaa69 100644 --- a/aws/internal/service/appstream/waiter/status.go +++ b/aws/internal/service/appstream/waiter/status.go @@ -3,6 +3,7 @@ package waiter import ( "context" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/appstream" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/finder" @@ -23,3 +24,20 @@ func StackState(ctx context.Context, conn *appstream.AppStream, name string) res return stack, "AVAILABLE", nil } } + +//FleetState fetches the fleet and its state +func FleetState(ctx context.Context, conn *appstream.AppStream, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + fleet, err := finder.FleetByName(ctx, conn, name) + + if err != nil { + return nil, "Unknown", err + } + + if fleet == nil { + return fleet, "NotFound", nil + } + + return fleet, aws.StringValue(fleet.State), nil + } +} diff --git a/aws/internal/service/appstream/waiter/waiter.go b/aws/internal/service/appstream/waiter/waiter.go index 765170f881b..c6f0db45833 100644 --- a/aws/internal/service/appstream/waiter/waiter.go +++ b/aws/internal/service/appstream/waiter/waiter.go @@ -11,6 +11,11 @@ import ( const ( // StackOperationTimeout Maximum amount of time to wait for Stack operation eventual consistency StackOperationTimeout = 4 * time.Minute + + // FleetStateTimeout Maximum amount of time to wait for the FleetState to be RUNNING or STOPPED + FleetStateTimeout = 180 * time.Minute + // FleetOperationTimeout Maximum amount of time to wait for Fleet operation eventual consistency + FleetOperationTimeout = 15 * time.Minute ) // StackStateDeleted waits for a deleted stack @@ -29,3 +34,39 @@ func StackStateDeleted(ctx context.Context, conn *appstream.AppStream, name stri return nil, err } + +// FleetStateRunning waits for a fleet running +func FleetStateRunning(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.Fleet, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{appstream.FleetStateStarting}, + Target: []string{appstream.FleetStateRunning}, + Refresh: FleetState(ctx, conn, name), + Timeout: FleetStateTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.Fleet); ok { + return output, err + } + + return nil, err +} + +// FleetStateStopped waits for a fleet stopped +func FleetStateStopped(ctx context.Context, conn *appstream.AppStream, name string) (*appstream.Fleet, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{appstream.FleetStateStopping}, + Target: []string{appstream.FleetStateStopped}, + Refresh: FleetState(ctx, conn, name), + Timeout: FleetStateTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*appstream.Fleet); ok { + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index d394cdfaee8..40a8d993ba9 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -537,6 +537,7 @@ func Provider() *schema.Provider { "aws_apprunner_custom_domain_association": resourceAwsAppRunnerCustomDomainAssociation(), "aws_apprunner_service": resourceAwsAppRunnerService(), "aws_appstream_stack": resourceAwsAppStreamStack(), + "aws_appstream_fleet": resourceAwsAppStreamFleet(), "aws_appsync_api_key": resourceAwsAppsyncApiKey(), "aws_appsync_datasource": resourceAwsAppsyncDatasource(), "aws_appsync_function": resourceAwsAppsyncFunction(), diff --git a/aws/resource_aws_appstream_fleet.go b/aws/resource_aws_appstream_fleet.go new file mode 100644 index 00000000000..fc5094be9b6 --- /dev/null +++ b/aws/resource_aws_appstream_fleet.go @@ -0,0 +1,595 @@ +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/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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/appstream/waiter" +) + +func resourceAwsAppStreamFleet() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceAwsAppStreamFleetCreate, + ReadWithoutTimeout: resourceAwsAppStreamFleetRead, + UpdateWithoutTimeout: resourceAwsAppStreamFleetUpdate, + DeleteWithoutTimeout: resourceAwsAppStreamFleetDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "compute_capacity": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "available": { + Type: schema.TypeInt, + Computed: true, + }, + "desired_instances": { + Type: schema.TypeInt, + Required: true, + }, + "in_use": { + Type: schema.TypeInt, + Computed: true, + }, + "running": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringLenBetween(0, 256), + }, + "disconnect_timeout_in_seconds": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntBetween(60, 360000), + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringLenBetween(0, 100), + }, + "domain_join_info": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: 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, + }, + "fleet_type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(appstream.FleetType_Values(), false), + }, + "iam_role_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateArn, + }, + "idle_disconnect_timeout_in_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + ValidateFunc: validation.IntBetween(60, 3600), + }, + "image_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "image_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "instance_type": { + Type: schema.TypeString, + Required: true, + }, + "max_user_duration_in_seconds": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntBetween(600, 360000), + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "stream_view": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(appstream.StreamView_Values(), false), + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "vpc_config": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "security_group_ids": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "subnet_ids": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + } +} + +func resourceAwsAppStreamFleetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + input := &appstream.CreateFleetInput{ + Name: aws.String(d.Get("name").(string)), + InstanceType: aws.String(d.Get("instance_type").(string)), + ComputeCapacity: expandComputeCapacity(d.Get("compute_capacity").([]interface{})), + } + + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("disconnect_timeout_in_seconds"); ok { + input.DisconnectTimeoutInSeconds = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("idle_disconnect_timeout_in_seconds"); ok { + input.IdleDisconnectTimeoutInSeconds = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("display_name"); ok { + input.DisplayName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("domain_join_info"); ok { + 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("fleet_type"); ok { + input.FleetType = aws.String(v.(string)) + } + + 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("max_user_duration_in_seconds"); ok { + input.MaxUserDurationInSeconds = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("vpc_config"); ok { + input.VpcConfig = expandVpcConfig(v.([]interface{})) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AppstreamTags() + } + + var err error + var output *appstream.CreateFleetOutput + err = resource.RetryContext(ctx, waiter.FleetOperationTimeout, func() *resource.RetryError { + output, err = conn.CreateFleetWithContext(ctx, input) + if err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + if isResourceTimeoutError(err) { + output, err = conn.CreateFleetWithContext(ctx, input) + } + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Appstream Fleet (%s): %w", d.Id(), err)) + } + + // Start fleet workflow + _, err = conn.StartFleetWithContext(ctx, &appstream.StartFleetInput{ + Name: output.Fleet.Name, + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error starting Appstream Fleet (%s): %w", d.Id(), err)) + } + + if _, err = waiter.FleetStateRunning(ctx, conn, aws.StringValue(output.Fleet.Name)); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream Fleet (%s) to be running: %w", d.Id(), err)) + } + + d.SetId(aws.StringValue(output.Fleet.Name)) + + return resourceAwsAppStreamFleetRead(ctx, d, meta) +} + +func resourceAwsAppStreamFleetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + resp, err := conn.DescribeFleetsWithContext(ctx, &appstream.DescribeFleetsInput{Names: []*string{aws.String(d.Id())}}) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Appstream Fleet (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error reading Appstream Fleet (%s): %w", d.Id(), err)) + } + + if len(resp.Fleets) == 0 { + return diag.FromErr(fmt.Errorf("error reading Appstream Fleet (%s): %s", d.Id(), "empty response")) + } + + if len(resp.Fleets) > 1 { + return diag.FromErr(fmt.Errorf("error reading Appstream Fleet (%s): %s", d.Id(), "multiple fleets found")) + } + + fleet := resp.Fleets[0] + + d.Set("arn", fleet.Arn) + + if err = d.Set("compute_capacity", flattenComputeCapacity(fleet.ComputeCapacityStatus)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Fleet (%s): %w", "compute_capacity", d.Id(), err)) + } + + d.Set("created_time", aws.TimeValue(fleet.CreatedTime).Format(time.RFC3339)) + d.Set("description", fleet.Description) + d.Set("display_name", fleet.DisplayName) + d.Set("disconnect_timeout_in_seconds", fleet.DisconnectTimeoutInSeconds) + + if err = d.Set("domain_join_info", flattenDomainInfo(fleet.DomainJoinInfo)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Fleet (%s): %w", "domain_join_info", d.Id(), err)) + } + + d.Set("idle_disconnect_timeout_in_seconds", fleet.IdleDisconnectTimeoutInSeconds) + d.Set("enable_default_internet_access", fleet.EnableDefaultInternetAccess) + d.Set("fleet_type", fleet.FleetType) + d.Set("iam_role_arn", fleet.IamRoleArn) + d.Set("image_name", fleet.ImageName) + d.Set("image_arn", fleet.ImageArn) + d.Set("instance_type", fleet.InstanceType) + d.Set("max_user_duration_in_seconds", fleet.MaxUserDurationInSeconds) + d.Set("name", fleet.Name) + d.Set("state", fleet.State) + d.Set("stream_view", fleet.StreamView) + + if err = d.Set("vpc_config", flattenVpcConfig(fleet.VpcConfig)); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Fleet (%s): %w", "vpc_config", d.Id(), err)) + } + + tg, err := conn.ListTagsForResource(&appstream.ListTagsForResourceInput{ + ResourceArn: fleet.Arn, + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("error listing stack tags for AppStream Stack (%s): %w", d.Id(), err)) + } + + if tg.Tags == nil { + log.Printf("[DEBUG] AppStream Stack tags (%s) not found", d.Id()) + return nil + } + + tags := keyvaluetags.AppstreamKeyValueTags(tg.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + if err = d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "tags", d.Id(), err)) + } + + if err = d.Set("tags_all", tags.Map()); err != nil { + return diag.FromErr(fmt.Errorf("error setting `%s` for AppStream Stack (%s): %w", "tags_all", d.Id(), err)) + } + + return nil +} + +func resourceAwsAppStreamFleetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + input := &appstream.UpdateFleetInput{ + Name: aws.String(d.Id()), + } + shouldStop := false + + if d.HasChanges("description", "domain_join_info", "enable_default_internet_access", "iam_role_arn", "instance_type", "max_user_duration_in_seconds", "stream_view", "vpc_config") { + shouldStop = true + } + + // Stop fleet workflow if needed + if shouldStop { + _, err := conn.StopFleetWithContext(ctx, &appstream.StopFleetInput{ + Name: aws.String(d.Id()), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error stopping Appstream Fleet (%s): %w", d.Id(), err)) + } + if _, err = waiter.FleetStateStopped(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream Fleet (%s) to be stopped: %w", d.Id(), err)) + } + } + + if d.HasChange("compute_capacity") { + input.ComputeCapacity = expandComputeCapacity(d.Get("compute_capacity").([]interface{})) + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("domain_join_info") { + input.DomainJoinInfo = expandDomainJoinInfo(d.Get("domain_join_info").([]interface{})) + } + + if d.HasChange("disconnect_timeout_in_seconds") { + input.DisconnectTimeoutInSeconds = aws.Int64(int64(d.Get("disconnect_timeout_in_seconds").(int))) + } + + if d.HasChange("enable_default_internet_access") { + input.EnableDefaultInternetAccess = aws.Bool(d.Get("enable_default_internet_access").(bool)) + } + + if d.HasChange("idle_disconnect_timeout_in_seconds") { + input.IdleDisconnectTimeoutInSeconds = aws.Int64(int64(d.Get("idle_disconnect_timeout_in_seconds").(int))) + } + + if d.HasChange("display_name") { + input.DisplayName = aws.String(d.Get("display_name").(string)) + } + + if d.HasChange("image_name") { + input.ImageName = aws.String(d.Get("image_name").(string)) + } + + if d.HasChange("image_arn") { + input.ImageArn = aws.String(d.Get("image_arn").(string)) + } + + if d.HasChange("iam_role_arn") { + input.IamRoleArn = aws.String(d.Get("iam_role_arn").(string)) + } + + if d.HasChange("stream_view") { + input.StreamView = aws.String(d.Get("stream_view").(string)) + } + + if d.HasChange("instance_type") { + input.InstanceType = aws.String(d.Get("instance_type").(string)) + } + + if d.HasChange("max_user_duration_in_seconds") { + input.MaxUserDurationInSeconds = aws.Int64(int64(d.Get("max_user_duration_in_seconds").(int))) + } + + if d.HasChange("vpc_config") { + input.VpcConfig = expandVpcConfig(d.Get("vpc_config").([]interface{})) + } + + resp, err := conn.UpdateFleetWithContext(ctx, input) + if err != nil { + return diag.FromErr(fmt.Errorf("error updating Appstream Fleet (%s): %w", d.Id(), err)) + } + + if d.HasChange("tags") { + arn := aws.StringValue(resp.Fleet.Arn) + + o, n := d.GetChange("tags") + if err := keyvaluetags.AppstreamUpdateTags(conn, arn, o, n); err != nil { + return diag.FromErr(fmt.Errorf("error updating Appstream Fleet tags (%s): %w", d.Id(), err)) + } + } + + // Start fleet workflow if stopped + if shouldStop { + _, err = conn.StartFleetWithContext(ctx, &appstream.StartFleetInput{ + Name: aws.String(d.Id()), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error starting Appstream Fleet (%s): %w", d.Id(), err)) + } + + if _, err = waiter.FleetStateRunning(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream Fleet (%s) to be running: %w", d.Id(), err)) + } + } + + return resourceAwsAppStreamFleetRead(ctx, d, meta) +} + +func resourceAwsAppStreamFleetDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).appstreamconn + + // Stop fleet workflow + _, err := conn.StopFleetWithContext(ctx, &appstream.StopFleetInput{ + Name: aws.String(d.Id()), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error stopping Appstream Fleet (%s): %w", d.Id(), err)) + } + + if _, err = waiter.FleetStateStopped(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting for Appstream Fleet (%s) to be stopped: %w", d.Id(), err)) + } + + _, err = conn.DeleteFleetWithContext(ctx, &appstream.DeleteFleetInput{ + Name: aws.String(d.Id()), + }) + + if err != nil { + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + return nil + } + return diag.FromErr(fmt.Errorf("error deleting Appstream Fleet (%s): %w", d.Id(), err)) + } + return nil +} + +func expandComputeCapacity(tfList []interface{}) *appstream.ComputeCapacity { + if len(tfList) == 0 { + return nil + } + + apiObject := &appstream.ComputeCapacity{} + + attr := tfList[0].(map[string]interface{}) + if v, ok := attr["desired_instances"]; ok { + apiObject.DesiredInstances = aws.Int64(int64(v.(int))) + } + + return apiObject +} + +func flattenComputeCapacity(apiObject *appstream.ComputeCapacityStatus) []interface{} { + if apiObject == nil { + return nil + } + + tfList := map[string]interface{}{} + tfList["desired_instances"] = aws.Int64Value(apiObject.Desired) + tfList["available"] = aws.Int64Value(apiObject.Available) + tfList["in_use"] = aws.Int64Value(apiObject.InUse) + tfList["running"] = aws.Int64Value(apiObject.Running) + + return []interface{}{tfList} +} + +func expandDomainJoinInfo(tfList []interface{}) *appstream.DomainJoinInfo { + if len(tfList) == 0 { + return nil + } + + apiObject := &appstream.DomainJoinInfo{} + + tfMap := tfList[0].(map[string]interface{}) + if v, ok := tfMap["directory_name"]; ok { + apiObject.DirectoryName = aws.String(v.(string)) + } + if v, ok := tfMap["organizational_unit_distinguished_name"]; ok { + apiObject.OrganizationalUnitDistinguishedName = aws.String(v.(string)) + } + + return apiObject +} + +func flattenDomainInfo(apiObject *appstream.DomainJoinInfo) []interface{} { + if apiObject == nil { + return nil + } + + tfList := map[string]interface{}{} + tfList["directory_name"] = aws.StringValue(apiObject.DirectoryName) + tfList["organizational_unit_distinguished_name"] = aws.StringValue(apiObject.OrganizationalUnitDistinguishedName) + + return []interface{}{tfList} +} + +func expandVpcConfig(tfList []interface{}) *appstream.VpcConfig { + if len(tfList) == 0 { + return nil + } + + apiObject := &appstream.VpcConfig{} + + tfMap := tfList[0].(map[string]interface{}) + if v, ok := tfMap["security_group_ids"]; ok { + apiObject.SecurityGroupIds = expandStringList(v.([]interface{})) + } + if v, ok := tfMap["subnet_ids"]; ok { + apiObject.SubnetIds = expandStringList(v.([]interface{})) + } + + return apiObject +} + +func flattenVpcConfig(apiObject *appstream.VpcConfig) []interface{} { + if apiObject == nil { + return nil + } + + tfList := map[string]interface{}{} + tfList["security_group_ids"] = aws.StringValueSlice(apiObject.SecurityGroupIds) + tfList["subnet_ids"] = aws.StringValueSlice(apiObject.SubnetIds) + + return []interface{}{tfList} +} diff --git a/aws/resource_aws_appstream_fleet_test.go b/aws/resource_aws_appstream_fleet_test.go new file mode 100644 index 00000000000..08b4ad32d62 --- /dev/null +++ b/aws/resource_aws_appstream_fleet_test.go @@ -0,0 +1,428 @@ +package aws + +import ( + "fmt" + "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/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() { + RegisterServiceErrorCheckFunc(appstream.EndpointsID, testAccErrorCheckSkipAppStream) +} + +// testAccErrorCheckSkipAppStream skips AppStream tests that have error messages indicating unsupported features +func testAccErrorCheckSkipAppStream(t *testing.T) resource.ErrorCheckFunc { + return testAccErrorCheckSkipMessagesContaining(t, + "ResourceNotFoundException: The image", + ) +} + +func TestAccAwsAppStreamFleet_basic(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + instanceType := "stream.standard.small" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfig(rName, instanceType), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppStreamFleet_disappears(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + instanceType := "stream.standard.small" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfig(rName, instanceType), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAppStreamFleet(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsAppStreamFleet_completeWithStop(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + descriptionUpdated := "Updated Description of a test" + fleetType := "ON_DEMAND" + instanceType := "stream.standard.small" + instanceTypeUpdate := "stream.standard.medium" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfigComplete(rName, description, fleetType, instanceType), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + Config: testAccAwsAppStreamFleetConfigComplete(rName, descriptionUpdated, fleetType, instanceTypeUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceTypeUpdate), + resource.TestCheckResourceAttr(resourceName, "description", descriptionUpdated), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppStreamFleet_completeWithoutStop(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + fleetType := "ON_DEMAND" + instanceType := "stream.standard.small" + displayName := "display name of a test" + displayNameUpdated := "display name of a test updated" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfigCompleteWithoutStopping(rName, description, fleetType, instanceType, displayName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "display_name", displayName), + ), + }, + { + Config: testAccAwsAppStreamFleetConfigCompleteWithoutStopping(rName, description, fleetType, instanceType, displayNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "display_name", displayNameUpdated), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppStreamFleet_withTags(t *testing.T) { + var fleetOutput appstream.Fleet + resourceName := "aws_appstream_fleet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + description := "Description of a test" + fleetType := "ON_DEMAND" + instanceType := "stream.standard.small" + displayName := "display name of a test" + displayNameUpdated := "display name of a test updated" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckHasIAMRole(t, "AmazonAppStreamServiceAccess") + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckAwsAppStreamFleetDestroy, + ErrorCheck: testAccErrorCheck(t, appstream.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccAwsAppStreamFleetConfigWithTags(rName, description, fleetType, instanceType, displayName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key", "value"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key", "value"), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + Config: testAccAwsAppStreamFleetConfigWithTags(rName, description, fleetType, instanceType, displayNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppStreamFleetExists(resourceName, &fleetOutput), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "state", appstream.FleetStateRunning), + resource.TestCheckResourceAttr(resourceName, "instance_type", instanceType), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key", "value"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key", "value"), + testAccCheckResourceAttrRfc3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsAppStreamFleetExists(resourceName string, appStreamFleet *appstream.Fleet) 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 + resp, err := conn.DescribeFleets(&appstream.DescribeFleetsInput{Names: []*string{aws.String(rs.Primary.ID)}}) + + if err != nil { + return err + } + + if resp == nil && len(resp.Fleets) == 0 { + return fmt.Errorf("appstream fleet %q does not exist", rs.Primary.ID) + } + + *appStreamFleet = *resp.Fleets[0] + + return nil + } +} + +func testAccCheckAwsAppStreamFleetDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).appstreamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appstream_fleet" { + continue + } + + resp, err := conn.DescribeFleets(&appstream.DescribeFleetsInput{Names: []*string{aws.String(rs.Primary.ID)}}) + + if tfawserr.ErrCodeEquals(err, appstream.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + + if resp != nil && len(resp.Fleets) > 0 { + return fmt.Errorf("appstream fleet %q still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccAwsAppStreamFleetConfig(name, instanceType string) string { + // "Amazon-AppStream2-Sample-Image-02-04-2019" is not available in GovCloud + return fmt.Sprintf(` +resource "aws_appstream_fleet" "test" { + name = %[1]q + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + instance_type = %[2]q + + compute_capacity { + desired_instances = 1 + } +} +`, name, instanceType) +} + +func testAccAwsAppStreamFleetConfigComplete(name, description, fleetType, instanceType string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "test" { + count = 2 + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = "10.0.${count.index}.0/24" + vpc_id = aws_vpc.test.id +} + +resource "aws_appstream_fleet" "test" { + name = %[1]q + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + + compute_capacity { + desired_instances = 1 + } + + description = %[2]q + idle_disconnect_timeout_in_seconds = 70 + enable_default_internet_access = false + fleet_type = %[3]q + instance_type = %[4]q + max_user_duration_in_seconds = 1000 + + vpc_config { + subnet_ids = aws_subnet.test.*.id + } +} +`, name, description, fleetType, instanceType)) +} + +func testAccAwsAppStreamFleetConfigCompleteWithoutStopping(name, description, fleetType, instanceType, displayName string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "test" { + count = 2 + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = "10.0.${count.index}.0/24" + vpc_id = aws_vpc.test.id +} + +resource "aws_appstream_fleet" "test" { + name = %[1]q + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + + compute_capacity { + desired_instances = 1 + } + + description = %[2]q + display_name = %[5]q + idle_disconnect_timeout_in_seconds = 70 + enable_default_internet_access = false + fleet_type = %[3]q + instance_type = %[4]q + max_user_duration_in_seconds = 1000 + + vpc_config { + subnet_ids = aws_subnet.test.*.id + } +} +`, name, description, fleetType, instanceType, displayName)) +} + +func testAccAwsAppStreamFleetConfigWithTags(name, description, fleetType, instanceType, displayName string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "test" { + count = 2 + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = "10.0.${count.index}.0/24" + vpc_id = aws_vpc.test.id +} + +resource "aws_appstream_fleet" "test" { + name = %[1]q + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + + compute_capacity { + desired_instances = 1 + } + + description = %[2]q + display_name = %[5]q + idle_disconnect_timeout_in_seconds = 70 + enable_default_internet_access = false + fleet_type = %[3]q + instance_type = %[4]q + max_user_duration_in_seconds = 1000 + + tags = { + Key = "value" + } + + vpc_config { + subnet_ids = aws_subnet.test.*.id + } +} +`, name, description, fleetType, instanceType, displayName)) +} diff --git a/aws/resource_aws_codestarconnections_host_test.go b/aws/resource_aws_codestarconnections_host_test.go index 923594de423..46ecb06b571 100644 --- a/aws/resource_aws_codestarconnections_host_test.go +++ b/aws/resource_aws_codestarconnections_host_test.go @@ -148,7 +148,7 @@ func testAccCheckAWSCodeStarConnectionsHostDestroy(s *terraform.State) error { return nil } -func testAccAWSCodeStarConnectionsHostVpcConfig(rName string) string { +func testAccAWSCodeStarConnectionsHostVpcBaseConfig(rName string) string { return fmt.Sprintf(` data "aws_availability_zones" "available" { state = "available" @@ -209,7 +209,9 @@ resource "aws_codestarconnections_host" "test" { } func testAccAWSCodeStarConnectionsHostConfigVpcConfig(rName string) string { - return testAccAWSCodeStarConnectionsHostVpcConfig(rName) + fmt.Sprintf(` + return composeConfig( + testAccAWSCodeStarConnectionsHostVpcBaseConfig(rName), + fmt.Sprintf(` resource "aws_codestarconnections_host" "test" { name = %[1]q provider_endpoint = "https://test.com" @@ -221,5 +223,5 @@ resource "aws_codestarconnections_host" "test" { vpc_id = aws_vpc.test.id } } -`, rName) +`, rName)) } diff --git a/aws/resource_aws_sqs_queue_policy_test.go b/aws/resource_aws_sqs_queue_policy_test.go index 1f24a156b26..45254ff0af0 100644 --- a/aws/resource_aws_sqs_queue_policy_test.go +++ b/aws/resource_aws_sqs_queue_policy_test.go @@ -22,7 +22,7 @@ func TestAccAWSSQSQueuePolicy_basic(t *testing.T) { CheckDestroy: testAccCheckAWSSQSQueueDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSQSPolicyConfig(rName), + Config: testAccAWSSQSQueuePolicyConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSQSQueueExists(queueResourceName, &queueAttributes), resource.TestCheckResourceAttrSet(resourceName, "policy"), @@ -34,7 +34,7 @@ func TestAccAWSSQSQueuePolicy_basic(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAWSSQSPolicyConfig(rName), + Config: testAccAWSSQSQueuePolicyConfig(rName), PlanOnly: true, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrPair(resourceName, "policy", queueResourceName, "policy"), @@ -57,7 +57,7 @@ func TestAccAWSSQSQueuePolicy_disappears(t *testing.T) { CheckDestroy: testAccCheckAWSSQSQueueDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSQSPolicyConfig(rName), + Config: testAccAWSSQSQueuePolicyConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSQSQueueExists(queueResourceName, &queueAttributes), testAccCheckResourceDisappears(testAccProvider, resourceAwsSqsQueuePolicy(), resourceName), @@ -80,7 +80,7 @@ func TestAccAWSSQSQueuePolicy_disappears_queue(t *testing.T) { CheckDestroy: testAccCheckAWSSQSQueueDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSQSPolicyConfig(rName), + Config: testAccAWSSQSQueuePolicyConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSQSQueueExists(queueResourceName, &queueAttributes), testAccCheckResourceDisappears(testAccProvider, resourceAwsSqsQueue(), queueResourceName), @@ -104,7 +104,7 @@ func TestAccAWSSQSQueuePolicy_Update(t *testing.T) { CheckDestroy: testAccCheckAWSSQSQueueDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSQSPolicyConfig(rName), + Config: testAccAWSSQSQueuePolicyConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSQSQueueExists(queueResourceName, &queueAttributes), resource.TestCheckResourceAttrSet(resourceName, "policy"), @@ -125,7 +125,7 @@ func TestAccAWSSQSQueuePolicy_Update(t *testing.T) { }) } -func testAccAWSSQSPolicyConfig(rName string) string { +func testAccAWSSQSQueuePolicyConfig(rName string) string { return fmt.Sprintf(` resource "aws_sqs_queue" "test" { name = %[1]q diff --git a/website/docs/r/appstream_fleet.html.markdown b/website/docs/r/appstream_fleet.html.markdown new file mode 100644 index 00000000000..1adcba1e4a0 --- /dev/null +++ b/website/docs/r/appstream_fleet.html.markdown @@ -0,0 +1,105 @@ +--- +subcategory: "AppStream" +layout: "aws" +page_title: "AWS: aws_appstream_fleet" +description: |- + Provides an AppStream fleet +--- + +# Resource: aws_appstream_fleet + +Provides an AppStream fleet. + +## Example Usage + +```terraform +resource "aws_appstream_fleet" "test_fleet" { + name = "test-fleet" + + compute_capacity { + desired_instances = 1 + } + + description = "test fleet" + idle_disconnect_timeout_in_seconds = 15 + display_name = "test-fleet" + enable_default_internet_access = false + fleet_type = "ON_DEMAND" + image_name = "Amazon-AppStream2-Sample-Image-02-04-2019" + instance_type = "stream.standard.large" + max_user_duration_in_seconds = 600 + + vpc_config { + subnet_ids = ["subnet-06e9b13400c225127"] + } + + tags = { + TagName = "tag-value" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `compute_capacity` - (Required) Configuration block for the desired capacity of the fleet. See below. +* `instance_type` - (Required) Instance type to use when launching fleet instances. +* `name` - (Required) Unique name for the fleet. + +The following arguments are optional: + +* `description` - (Optional) Description to display. +* `disconnect_timeout_in_seconds` - (Optional) Amount of time that a streaming session remains active after users disconnect. +* `display_name` - (Optional) Human-readable friendly name for the AppStream fleet. +* `domain_join_info` - (Optional) Configuration block for the name of the directory and organizational unit (OU) to use to join the fleet to a Microsoft Active Directory domain. See below. +* `enable_default_internet_access` - (Optional) Enables or disables default internet access for the fleet. +* `fleet_type` - (Optional) Fleet type. Valid values are: `ON_DEMAND`, `ALWAYS_ON` +* `iam_role_arn` - (Optional) ARN of the IAM role to apply to the fleet. +* `idle_disconnect_timeout_in_seconds` - (Optional) Amount of time that users can be idle (inactive) before they are disconnected from their streaming session and the `disconnect_timeout_in_seconds` time interval begins. +* `image_name` - (Optional) Name of the image used to create the fleet. +* `image_arn` - (Optional) ARN of the public, private, or shared image to use. +* `stream_view` - (Optional) AppStream 2.0 view that is displayed to your users when they stream from the fleet. When `APP` is specified, only the windows of applications opened by users display. When `DESKTOP` is specified, the standard desktop that is provided by the operating system displays. +* `max_user_duration_in_seconds` - (Optional) Maximum amount of time that a streaming session can remain active, in seconds. +* `vpc_config` - (Optional) Configuration block for the VPC configuration for the image builder. See below. +* `tags` - (Optional) Map of tags to attach to AppStream instances. + +### `compute_capacity` + +* `desired_instances` - (Required) Desired number of streaming instances. + +### `domain_join_info` + +* `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` + +* `security_group_ids` - Identifiers of the security groups for the fleet or image builder. +* `subnet_ids` - Identifiers of the subnets to which a network interface is attached from the fleet instance or image builder instance. + + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Unique identifier (ID) of the appstream fleet. +* `arn` - ARN of the appstream fleet. +* `state` - State of the fleet. Can be `STARTING`, `RUNNING`, `STOPPING` or `STOPPED` +* `created_time` - Date and time, in UTC and extended RFC 3339 format, when the fleet was created. +* `compute_capacity` - Describes the capacity status for a fleet. + +### `compute_capacity` + +* `available` - Number of currently available instances that can be used to stream sessions. +* `in_use` - Number of instances in use for streaming. +* `running` - Total number of simultaneous streaming instances that are running. + + +## Import + +`aws_appstream_fleet` can be imported using the id, e.g. + +``` +$ terraform import aws_appstream_fleet.example fleetNameExample +``` diff --git a/website/docs/r/secretsmanager_secret.html.markdown b/website/docs/r/secretsmanager_secret.html.markdown index 3b639f82754..e0d68242241 100644 --- a/website/docs/r/secretsmanager_secret.html.markdown +++ b/website/docs/r/secretsmanager_secret.html.markdown @@ -22,7 +22,7 @@ resource "aws_secretsmanager_secret" "example" { ### Rotation Configuration -To enable automatic secret rotation, the Secrets Manager service requires usage of a Lambda function. The [Rotate Secrets section in the Secrets Manager User Guide](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets.html) provides additional information about deploying a prebuilt Lambda functions for supported credential rotation (e.g. RDS) or deploying a custom Lambda function. +To enable automatic secret rotation, the Secrets Manager service requires usage of a Lambda function. The [Rotate Secrets section in the Secrets Manager User Guide](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets_strategies.html) provides additional information about deploying a prebuilt Lambda functions for supported credential rotation (e.g. RDS) or deploying a custom Lambda function. ~> **NOTE:** Configuring rotation causes the secret to rotate once as soon as you store the secret. Before you do this, you must ensure that all of your applications that use the credentials stored in the secret are updated to retrieve the secret from AWS Secrets Manager. The old credentials might no longer be usable after the initial rotation and any applications that you fail to update will break as soon as the old credentials are no longer valid. diff --git a/website/docs/r/secretsmanager_secret_rotation.html.markdown b/website/docs/r/secretsmanager_secret_rotation.html.markdown index 307966364dd..4b3bb054628 100644 --- a/website/docs/r/secretsmanager_secret_rotation.html.markdown +++ b/website/docs/r/secretsmanager_secret_rotation.html.markdown @@ -27,7 +27,7 @@ resource "aws_secretsmanager_secret_rotation" "example" { ### Rotation Configuration -To enable automatic secret rotation, the Secrets Manager service requires usage of a Lambda function. The [Rotate Secrets section in the Secrets Manager User Guide](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets.html) provides additional information about deploying a prebuilt Lambda functions for supported credential rotation (e.g. RDS) or deploying a custom Lambda function. +To enable automatic secret rotation, the Secrets Manager service requires usage of a Lambda function. The [Rotate Secrets section in the Secrets Manager User Guide](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets_strategies.html) provides additional information about deploying a prebuilt Lambda functions for supported credential rotation (e.g. RDS) or deploying a custom Lambda function. ~> **NOTE:** Configuring rotation causes the secret to rotate once as soon as you enable rotation. Before you do this, you must ensure that all of your applications that use the credentials stored in the secret are updated to retrieve the secret from AWS Secrets Manager. The old credentials might no longer be usable after the initial rotation and any applications that you fail to update will break as soon as the old credentials are no longer valid.