Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/44621.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
data-source/aws_ecr_images: Add `tag_status`, `max_results`, and `describe_images` arguments with `image_details` attribute
```
94 changes: 90 additions & 4 deletions internal/service/ecr/images_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ type imagesDataSource struct {
func (d *imagesDataSource) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) {
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"image_ids": framework.DataSourceComputedListOfObjectAttribute[imagesIDsModel](ctx),
"describe_images": schema.BoolAttribute{
Optional: true,
Description: "Whether to call DescribeImages API to get detailed image information",
},
"image_details": framework.DataSourceComputedListOfObjectAttribute[imageDetailsModel](ctx),
"image_ids": framework.DataSourceComputedListOfObjectAttribute[imagesIDsModel](ctx),
"max_results": schema.Int64Attribute{
Optional: true,
Description: "Maximum number of images to return",
},
"registry_id": schema.StringAttribute{
Optional: true,
Description: "ID of the registry (AWS account ID)",
Expand All @@ -40,6 +49,10 @@ func (d *imagesDataSource) Schema(ctx context.Context, request datasource.Schema
Required: true,
Description: "Name of the repository",
},
"tag_status": schema.StringAttribute{
Optional: true,
Description: "Filter images by tag status. Valid values: TAGGED, UNTAGGED, ANY",
},
},
}
}
Expand All @@ -59,6 +72,20 @@ func (d *imagesDataSource) Read(ctx context.Context, req datasource.ReadRequest,
return
}

// Set tag status filter if provided
if !data.TagStatus.IsNull() && !data.TagStatus.IsUnknown() {
tagStatus := awstypes.TagStatus(data.TagStatus.ValueString())
input.Filter = &awstypes.ListImagesFilter{
TagStatus: tagStatus,
}
}

// Set max results if provided
if !data.MaxResults.IsNull() && !data.MaxResults.IsUnknown() {
maxResults := int32(data.MaxResults.ValueInt64())
input.MaxResults = &maxResults
}

output, err := findImages(ctx, conn, &input)
if err != nil {
resp.Diagnostics.AddError("reading ECR Images", err.Error())
Expand All @@ -70,9 +97,55 @@ func (d *imagesDataSource) Read(ctx context.Context, req datasource.ReadRequest,
return
}

// If describe_images is true, call DescribeImages API
if !data.DescribeImages.IsNull() && data.DescribeImages.ValueBool() {
registryID := ""
if !data.RegistryID.IsNull() {
registryID = data.RegistryID.ValueString()
}

imageDetails, err := findImagesDetails(ctx, conn, data.RepositoryName.ValueString(), registryID, output)
if err != nil {
resp.Diagnostics.AddError("describing ECR Images", err.Error())
return
}

resp.Diagnostics.Append(fwflex.Flatten(ctx, imageDetails, &data.ImageDetails)...)
if resp.Diagnostics.HasError() {
return
}
}

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

func findImagesDetails(ctx context.Context, conn *ecr.Client, repositoryName, registryID string, imageIds []awstypes.ImageIdentifier) ([]awstypes.ImageDetail, error) {
var output []awstypes.ImageDetail

// DescribeImages has a limit of 100 images per request
const batchSize = 100
for i := 0; i < len(imageIds); i += batchSize {
end := min(i+batchSize, len(imageIds))

input := &ecr.DescribeImagesInput{
RepositoryName: &repositoryName,
ImageIds: imageIds[i:end],
}
if registryID != "" {
input.RegistryId = &registryID
}

result, err := conn.DescribeImages(ctx, input)
if err != nil {
return nil, err
}

output = append(output, result.ImageDetails...)
}

return output, nil
}

func findImages(ctx context.Context, conn *ecr.Client, input *ecr.ListImagesInput) ([]awstypes.ImageIdentifier, error) {
var output []awstypes.ImageIdentifier

Expand All @@ -99,9 +172,22 @@ func findImages(ctx context.Context, conn *ecr.Client, input *ecr.ListImagesInpu

type imagesDataSourceModel struct {
framework.WithRegionModel
ImageIDs fwtypes.ListNestedObjectValueOf[imagesIDsModel] `tfsdk:"image_ids"`
RegistryID types.String `tfsdk:"registry_id"`
RepositoryName types.String `tfsdk:"repository_name"`
DescribeImages types.Bool `tfsdk:"describe_images"`
ImageDetails fwtypes.ListNestedObjectValueOf[imageDetailsModel] `tfsdk:"image_details"`
ImageIDs fwtypes.ListNestedObjectValueOf[imagesIDsModel] `tfsdk:"image_ids"`
MaxResults types.Int64 `tfsdk:"max_results"`
RegistryID types.String `tfsdk:"registry_id"`
RepositoryName types.String `tfsdk:"repository_name"`
TagStatus types.String `tfsdk:"tag_status"`
}

type imageDetailsModel struct {
ImageDigest types.String `tfsdk:"image_digest"`
ImagePushedAt types.String `tfsdk:"image_pushed_at"`
ImageSizeInBytes types.Int64 `tfsdk:"image_size_in_bytes"`
ImageTags fwtypes.ListValueOf[types.String] `tfsdk:"image_tags"`
RegistryID types.String `tfsdk:"registry_id"`
RepositoryName types.String `tfsdk:"repository_name"`
}

type imagesIDsModel struct {
Expand Down
137 changes: 137 additions & 0 deletions internal/service/ecr/images_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ package ecr_test

import (
"fmt"
"strconv"
"testing"

sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
"github.com/hashicorp/terraform-provider-aws/names"
)
Expand Down Expand Up @@ -102,6 +104,141 @@ data "aws_ecr_images" "test" {
`, rName)
}

func TestAccECRImagesDataSource_describeImages(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
dataSourceName := "data.aws_ecr_images.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccImagesDataSourceConfig_describeImages(rName, true),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, names.AttrRepositoryName, rName),
resource.TestCheckResourceAttr(dataSourceName, "describe_images", acctest.CtTrue),
),
},
},
})
}

func TestAccECRImagesDataSource_maxResults(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
dataSourceName := "data.aws_ecr_images.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccImagesDataSourceConfig_maxResults(rName, 5),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, names.AttrRepositoryName, rName),
resource.TestCheckResourceAttr(dataSourceName, "max_results", "5"),
),
},
},
})
}

func TestAccECRImagesDataSource_tagStatus(t *testing.T) {
ctx := acctest.Context(t)
registryID := "137112412989"
repositoryName := "amazonlinux"
dataSourceName := "data.aws_ecr_images.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccImagesDataSourceConfig_tagStatusPublic(registryID, repositoryName, "TAGGED"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, names.AttrRepositoryName, repositoryName),
resource.TestCheckResourceAttr(dataSourceName, "tag_status", "TAGGED"),
resource.TestCheckResourceAttrSet(dataSourceName, "image_ids.#"),
// Verify all returned images have tags
testAccCheckImagesAllHaveTags(dataSourceName),
),
},
{
Config: testAccImagesDataSourceConfig_tagStatusPublic(registryID, repositoryName, "ANY"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, names.AttrRepositoryName, repositoryName),
resource.TestCheckResourceAttr(dataSourceName, "tag_status", "ANY"),
resource.TestCheckResourceAttrSet(dataSourceName, "image_ids.#"),
),
},
},
})
}

func testAccImagesDataSourceConfig_describeImages(rName string, describeImages bool) string {
return fmt.Sprintf(`
resource "aws_ecr_repository" "test" {
name = %[1]q
}

data "aws_ecr_images" "test" {
repository_name = aws_ecr_repository.test.name
describe_images = %[2]t
}
`, rName, describeImages)
}

func testAccImagesDataSourceConfig_maxResults(rName string, maxResults int) string {
return fmt.Sprintf(`
resource "aws_ecr_repository" "test" {
name = %[1]q
}

data "aws_ecr_images" "test" {
repository_name = aws_ecr_repository.test.name
max_results = %[2]d
}
`, rName, maxResults)
}

func testAccCheckImagesAllHaveTags(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)
}

imageCount := rs.Primary.Attributes["image_ids.#"]
count, err := strconv.Atoi(imageCount)
if err != nil {
return err
}

for i := range count {
tagKey := fmt.Sprintf("image_ids.%d.image_tag", i)
if tag := rs.Primary.Attributes[tagKey]; tag == "" {
return fmt.Errorf("Image at index %d has no tag when TAGGED filter was used", i)
}
}

return nil
}
}

func testAccImagesDataSourceConfig_tagStatusPublic(registryID, repositoryName, tagStatus string) string {
return fmt.Sprintf(`
data "aws_ecr_images" "test" {
registry_id = %[1]q
repository_name = %[2]q
tag_status = %[3]q
}
`, registryID, repositoryName, tagStatus)
}

func testAccImagesDataSourceConfig_registryID(registryID, repositoryName string) string {
return fmt.Sprintf(`
data "aws_ecr_images" "test" {
Expand Down
41 changes: 41 additions & 0 deletions website/docs/d/ecr_images.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,47 @@ output "image_tags" {
}
```

### Filter by Tag Status

```terraform
data "aws_ecr_images" "tagged_only" {
repository_name = "my-repository"
tag_status = "TAGGED"
}
```

### Limit Results

```terraform
data "aws_ecr_images" "limited" {
repository_name = "my-repository"
max_results = 10
}
```

### Get Detailed Image Information

```terraform
data "aws_ecr_images" "detailed" {
repository_name = "my-repository"
describe_images = true
}

output "image_details" {
value = data.aws_ecr_images.detailed.image_details
}
```

## Argument Reference

This data source supports the following arguments:

* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference).
* `registry_id` - (Optional) ID of the Registry where the repository resides.
* `repository_name` - (Required) Name of the ECR Repository.
* `tag_status` - (Optional) Filter images by tag status. Valid values: `TAGGED`, `UNTAGGED`, `ANY`.
* `max_results` - (Optional) Maximum number of images to return.
* `describe_images` - (Optional) Whether to call DescribeImages API to get detailed image information. Defaults to `false`.

## Attribute Reference

Expand All @@ -41,3 +75,10 @@ This data source exports the following attributes in addition to the arguments a
* `image_ids` - List of image objects containing image digest and tags. Each object has the following attributes:
* `image_digest` - The sha256 digest of the image manifest.
* `image_tag` - The tag associated with the image.
* `image_details` - List of detailed image information (only populated when `describe_images` is `true`). Each object has the following attributes:
* `image_digest` - The sha256 digest of the image manifest.
* `image_pushed_at` - The date and time when the image was pushed to the repository.
* `image_size_in_bytes` - The size of the image in bytes.
* `image_tags` - List of tags associated with the image.
* `registry_id` - The AWS account ID associated with the registry.
* `repository_name` - The name of the repository.
Loading