Skip to content

Commit

Permalink
Merge pull request #38225 from ThomasZalewski/f/appstream-image-describe
Browse files Browse the repository at this point in the history
[New Data Source]: aws_appstream_image
  • Loading branch information
ThomasZalewski authored Jul 2, 2024
2 parents 9ca5688 + e600a77 commit 8d7cddc
Show file tree
Hide file tree
Showing 6 changed files with 447 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .changelog/38225.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-data-source
aws_appstream_image
```
26 changes: 26 additions & 0 deletions internal/service/appstream/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,29 @@ func FindFleetStackAssociation(ctx context.Context, conn *appstream.Client, flee

return nil
}

// findImages finds all images from a describe images input
func findImages(ctx context.Context, conn *appstream.Client, input *appstream.DescribeImagesInput) ([]awstypes.Image, error) {
var output []awstypes.Image

pages := appstream.NewDescribeImagesPaginator(conn, input)

for pages.HasMorePages() {
page, err := pages.NextPage(ctx)

if errs.IsA[*awstypes.ResourceNotFoundException](err) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

output = append(output, page.Images...)
}

return output, nil
}
278 changes: 278 additions & 0 deletions internal/service/appstream/image_data_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package appstream

import (
"context"
"sort"
"time"

"github.com/YakDriver/regexache"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/appstream"
awstypes "github.com/aws/aws-sdk-go-v2/service/appstream/types"
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-aws/internal/create"
"github.com/hashicorp/terraform-provider-aws/internal/framework"
"github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @FrameworkDataSource(name="Image")
func newDataSourceImage(context.Context) (datasource.DataSourceWithConfigure, error) {
return &dataSourceImage{}, nil
}

const (
DSNameImage = "Image Data Source"
)

type dataSourceImage struct {
framework.DataSourceWithConfigure
}

func (d *dataSourceImage) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { // nosemgrep:ci.meta-in-func-name
resp.TypeName = "aws_appstream_image"
}

func (d *dataSourceImage) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
{
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{

names.AttrARN: schema.StringAttribute{
CustomType: fwtypes.ARNType,
Computed: true,
Optional: true,
Validators: []validator.String{
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot(names.AttrName),
}...),
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot("name_regex"),
}...),
},
},
"applications": schema.ListAttribute{
CustomType: fwtypes.NewListNestedObjectTypeOf[dsApplications](ctx),
Computed: true,
},
"appstream_agent_version": schema.StringAttribute{
Computed: true,
},
names.AttrMostRecent: schema.BoolAttribute{
Optional: true,
},
"base_image_arn": schema.StringAttribute{
Computed: true,
},
names.AttrCreatedTime: schema.StringAttribute{
CustomType: timetypes.RFC3339Type{},
Computed: true,
},
names.AttrDescription: schema.StringAttribute{
Computed: true,
},
names.AttrDisplayName: schema.StringAttribute{
Computed: true,
},
"image_builder_name": schema.StringAttribute{
Computed: true,
},
"image_builder_supported": schema.BoolAttribute{
Computed: true,
},
"image_permissions": schema.ListAttribute{
CustomType: fwtypes.NewListNestedObjectTypeOf[dsImagePermissions](ctx),
Computed: true,
},
names.AttrName: schema.StringAttribute{
Computed: true,
Optional: true,
Validators: []validator.String{
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot(names.AttrARN),
}...),
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot("name_regex"),
}...),
},
},
"name_regex": schema.StringAttribute{
CustomType: fwtypes.RegexpType,
Optional: true,
Validators: []validator.String{
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot(names.AttrName),
}...),
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot(names.AttrARN),
}...),
},
},

"platform": schema.StringAttribute{
Computed: true,
},
"public_base_image_released_date": schema.StringAttribute{
CustomType: timetypes.RFC3339Type{},
Computed: true,
},
names.AttrState: schema.StringAttribute{
Computed: true,
},
"state_change_reason": schema.ListAttribute{
CustomType: fwtypes.NewListNestedObjectTypeOf[dsStateChange](ctx),
Computed: true,
},
names.AttrType: schema.StringAttribute{
CustomType: fwtypes.StringEnumType[awstypes.VisibilityType](),
Optional: true,
},
},
}
}
}
func (d *dataSourceImage) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
conn := d.Meta().AppStreamClient(ctx)

var data dsImage
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
var describeImagesInput appstream.DescribeImagesInput
if !data.Name.IsNull() {
describeImagesInput.Names = []string{data.Name.ValueString()}
}
if !data.Arn.IsNull() {
describeImagesInput.Arns = []string{data.Arn.ValueString()}
}
images, err := findImages(ctx, conn, &describeImagesInput)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.AppStream, create.ErrActionReading, DSNameImage, data.Arn.String(), err),
err.Error(),
)
return
}

var filteredImages []awstypes.Image
if !data.NameRegex.IsNull() {
r := regexache.MustCompile(data.NameRegex.ValueString())
for _, img := range images {
name := aws.ToString(img.Name)

// Check for a very rare case where the response would include no
// image name. No name means nothing to attempt a match against,
// therefore we are skipping such image.
if name == "" {
continue
}

if r.MatchString(name) {
filteredImages = append(filteredImages, img)
}
}
} else {
filteredImages = images[:]
}

if len(filteredImages) < 1 {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.AppStream, create.ErrActionReading, DSNameImage, data.Arn.String(), err),
"Your query returned no results. Please change your search criteria and try again.",
)
return
}

if len(filteredImages) > 1 {
if !data.MostRecent.ValueBool() {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.AppStream, create.ErrActionReading, DSNameImage, data.Arn.String(), err),
"Your query returned more than one result. Please try a more specific search criteria, or set `most_recent` attribute to true.",
)
return
}
sort.Slice(filteredImages, func(i, j int) bool {
itime, _ := time.Parse(time.RFC3339, images[i].CreatedTime.Month().String())
jtime, _ := time.Parse(time.RFC3339, images[j].CreatedTime.Month().String())
return itime.Unix() > jtime.Unix()
})
}
image := filteredImages[0]

data.Type = fwtypes.StringEnumValue[awstypes.VisibilityType](image.Visibility)
resp.Diagnostics.Append(flex.Flatten(ctx, &image, &data)...)
if resp.Diagnostics.HasError() {
return
}

if image.PublicBaseImageReleasedDate != nil {
data.PubilcBaseImageReleasedDate = timetypes.NewRFC3339TimeValue(*image.PublicBaseImageReleasedDate)
}

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

type dsApplications struct {
AppBlockArn types.String `tfsdk:"app_block_arn"`
Arn fwtypes.ARN `tfsdk:"arn"`
CreatedTime timetypes.RFC3339 `tfsdk:"created_time"`
Description types.String `tfsdk:"description"`
DisplayName types.String `tfsdk:"display_name"`
Enabled types.Bool `tfsdk:"enabled"`
IconS3Location fwtypes.ListNestedObjectValueOf[dsIconS3] `tfsdk:"icon_s3_location"`
IconUrl types.String `tfsdk:"icon_url"`
InstanceFamilies fwtypes.ListValueOf[types.String] `tfsdk:"instance_families"`
LaunchParameters types.String `tfsdk:"launch_parameters"`
LaunchPath types.String `tfsdk:"launch_path"`
Metadata fwtypes.MapValueOf[types.String] `tfsdk:"metadata"`
Name types.String `tfsdk:"name"`
Platforms fwtypes.ListValueOf[types.String] `tfsdk:"platforms"`
WorkingDirectory types.String `tfsdk:"working_directory"`
}

type dsIconS3 struct {
S3Bucket types.String `tfsdk:"s3_bucket"`
S3Key types.String `tfsdk:"s3_key"`
}

type dsStateChange struct {
Code types.String `tfsdk:"code"`
Message types.String `tfsdk:"message"`
}

type dsImage struct {
Applications fwtypes.ListNestedObjectValueOf[dsApplications] `tfsdk:"applications"`
AppStreamAgentVersion types.String `tfsdk:"appstream_agent_version"`
Arn fwtypes.ARN `tfsdk:"arn"`
BaseImageArn types.String `tfsdk:"base_image_arn"`
CreatedTime timetypes.RFC3339 `tfsdk:"created_time"`
Description types.String `tfsdk:"description"`
DisplayName types.String `tfsdk:"display_name"`
ImageBuilderName types.String `tfsdk:"image_builder_name"`
ImageBuilderSupported types.Bool `tfsdk:"image_builder_supported"`
ImagePermissions fwtypes.ListNestedObjectValueOf[dsImagePermissions] `tfsdk:"image_permissions"`
MostRecent types.Bool `tfsdk:"most_recent"`
Name types.String `tfsdk:"name"`
NameRegex fwtypes.Regexp `tfsdk:"name_regex"`
Platform types.String `tfsdk:"platform"`
PubilcBaseImageReleasedDate timetypes.RFC3339 `tfsdk:"public_base_image_released_date"`
State types.String `tfsdk:"state"`
StateChangeReason fwtypes.ListNestedObjectValueOf[dsStateChange] `tfsdk:"state_change_reason"`
Type fwtypes.StringEnum[awstypes.VisibilityType] `tfsdk:"type"`
}

type dsImagePermissions struct {
AllowFleet types.Bool `tfsdk:"allow_fleet"`
AllowImageBuilder types.Bool `tfsdk:"allow_image_builder"`
}
60 changes: 60 additions & 0 deletions internal/service/appstream/image_data_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package appstream_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
"github.com/hashicorp/terraform-provider-aws/names"
)

func TestAccAppStreamImageDataSource_basic(t *testing.T) {
ctx := acctest.Context(t)
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}
dataSourceName := "data.aws_appstream_image.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(ctx, t)
acctest.PreCheckPartitionHasService(t, names.AppStreamEndpointID)
},
ErrorCheck: acctest.ErrorCheck(t, names.AppStreamServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccImageDataSourceConfig_basic(),
Check: resource.ComposeTestCheckFunc(

resource.TestCheckResourceAttrSet(dataSourceName, "applications.#"),
resource.TestCheckResourceAttrSet(dataSourceName, "appstream_agent_version"),
resource.TestCheckResourceAttrSet(dataSourceName, names.AttrARN),
resource.TestCheckResourceAttrSet(dataSourceName, names.AttrCreatedTime),
resource.TestCheckResourceAttrSet(dataSourceName, names.AttrDescription),
resource.TestCheckResourceAttrSet(dataSourceName, names.AttrDisplayName),
resource.TestCheckResourceAttrSet(dataSourceName, "image_builder_supported"),
resource.TestCheckResourceAttrSet(dataSourceName, names.AttrName),
resource.TestCheckResourceAttrSet(dataSourceName, "name_regex"),
resource.TestCheckResourceAttrSet(dataSourceName, "platform"),
resource.TestCheckResourceAttrSet(dataSourceName, "public_base_image_released_date"),
resource.TestCheckResourceAttrSet(dataSourceName, names.AttrType),
),
},
},
})
}

// name = "AppStream-WinServer2019-06-17-2024"
func testAccImageDataSourceConfig_basic() string {
return (`
data "aws_appstream_image" "test" {
name_regex = "^AppStream-WinServer.*$"
type = "PUBLIC"
most_recent = true
}
`)
}
7 changes: 6 additions & 1 deletion internal/service/appstream/service_package_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8d7cddc

Please sign in to comment.