Skip to content

Commit

Permalink
Merge pull request #26857 from mtt88/f-ecr_most_recent_image_tag
Browse files Browse the repository at this point in the history
ECR - Add ability to query by and return most recent image tag(s)
  • Loading branch information
ewbankkit authored Feb 22, 2023
2 parents 0a850c5 + 7b6e01a commit 29a61c1
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 344 deletions.
7 changes: 7 additions & 0 deletions .changelog/26857.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
data-source/aws_ecr_image: Add `most_recent` argument to return the most recently pushed image
```

```release-note:enhancement
data-source/aws_ecr_repository: Add `most_recent_image_tags` attribute containing the most recently pushed image tag(s) in an ECR repository
```
149 changes: 98 additions & 51 deletions internal/service/ecr/image_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,29 @@ package ecr

import (
"context"
"log"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/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/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
"golang.org/x/exp/slices"
)

func DataSourceImage() *schema.Resource {
return &schema.Resource{
ReadWithoutTimeout: dataSourceImageRead,
Schema: map[string]*schema.Schema{
"registry_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.NoZeroValues,
},
"repository_name": {
Type: schema.TypeString,
Required: true,
},
"image_digest": {
Type: schema.TypeString,
Computed: true,
Optional: true,
},
"image_tag": {
Type: schema.TypeString,
Optional: true,
Type: schema.TypeString,
Computed: true,
Optional: true,
AtLeastOneOf: []string{"image_digest", "image_tag", "most_recent"},
ConflictsWith: []string{"most_recent"},
},
"image_pushed_at": {
Type: schema.TypeInt,
Expand All @@ -44,11 +34,33 @@ func DataSourceImage() *schema.Resource {
Type: schema.TypeInt,
Computed: true,
},
"image_tag": {
Type: schema.TypeString,
Optional: true,
AtLeastOneOf: []string{"image_digest", "image_tag", "most_recent"},
ConflictsWith: []string{"most_recent"},
},
"image_tags": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"most_recent": {
Type: schema.TypeBool,
Optional: true,
AtLeastOneOf: []string{"image_digest", "image_tag", "most_recent"},
ConflictsWith: []string{"image_digest", "image_tag"},
},
"registry_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.NoZeroValues,
},
"repository_name": {
Type: schema.TypeString,
Required: true,
},
},
}
}
Expand All @@ -57,58 +69,93 @@ func dataSourceImageRead(ctx context.Context, d *schema.ResourceData, meta inter
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).ECRConn()

params := &ecr.DescribeImagesInput{
input := &ecr.DescribeImagesInput{
RepositoryName: aws.String(d.Get("repository_name").(string)),
}

regId, ok := d.GetOk("registry_id")
if ok {
params.RegistryId = aws.String(regId.(string))
if v, ok := d.GetOk("image_digest"); ok {
input.ImageIds = []*ecr.ImageIdentifier{
{
ImageDigest: aws.String(v.(string)),
},
}
}

imgId := ecr.ImageIdentifier{}
digest, ok := d.GetOk("image_digest")
if ok {
imgId.ImageDigest = aws.String(digest.(string))
}
tag, ok := d.GetOk("image_tag")
if ok {
imgId.ImageTag = aws.String(tag.(string))
if v, ok := d.GetOk("image_tag"); ok {
if len(input.ImageIds) == 0 {
input.ImageIds = []*ecr.ImageIdentifier{
{
ImageTag: aws.String(v.(string)),
},
}
} else {
input.ImageIds[0].ImageTag = aws.String(v.(string))
}
}

if imgId.ImageDigest == nil && imgId.ImageTag == nil {
return sdkdiag.AppendErrorf(diags, "At least one of either image_digest or image_tag must be defined")
if v, ok := d.GetOk("registry_id"); ok {
input.RegistryId = aws.String(v.(string))
}

params.ImageIds = []*ecr.ImageIdentifier{&imgId}
imageDetails, err := FindImageDetails(ctx, conn, input)

var imageDetails []*ecr.ImageDetail
log.Printf("[DEBUG] Reading ECR Images: %s", params)
err := conn.DescribeImagesPagesWithContext(ctx, params, func(page *ecr.DescribeImagesOutput, lastPage bool) bool {
imageDetails = append(imageDetails, page.ImageDetails...)
return true
})
if err != nil {
return sdkdiag.AppendErrorf(diags, "describing ECR images: %s", err)
return sdkdiag.AppendErrorf(diags, "reading ECR Images: %s", err)
}

if len(imageDetails) == 0 {
return sdkdiag.AppendErrorf(diags, "No matching image found")
return sdkdiag.AppendErrorf(diags, "Your query returned no results. Please change your search criteria and try again.")
}

if len(imageDetails) > 1 {
return sdkdiag.AppendErrorf(diags, "More than one image found for tag/digest combination")
if !d.Get("most_recent").(bool) {
return sdkdiag.AppendErrorf(diags, "Your query returned more than one result. Please try a more specific search criteria, or set `most_recent` attribute to true.")
}

slices.SortFunc(imageDetails, func(a, b *ecr.ImageDetail) bool {
return aws.TimeValue(a.ImagePushedAt).After(aws.TimeValue(b.ImagePushedAt))
})
}

image := imageDetails[0]
imageDetail := imageDetails[0]
d.SetId(aws.StringValue(imageDetail.ImageDigest))
d.Set("image_digest", imageDetail.ImageDigest)
d.Set("image_pushed_at", imageDetail.ImagePushedAt.Unix())
d.Set("image_size_in_bytes", imageDetail.ImageSizeInBytes)
d.Set("image_tags", aws.StringValueSlice(imageDetail.ImageTags))
d.Set("registry_id", imageDetail.RegistryId)
d.Set("repository_name", imageDetail.RepositoryName)

return diags
}

func FindImageDetails(ctx context.Context, conn *ecr.ECR, input *ecr.DescribeImagesInput) ([]*ecr.ImageDetail, error) {
var output []*ecr.ImageDetail

d.SetId(aws.StringValue(image.ImageDigest))
d.Set("registry_id", image.RegistryId)
d.Set("image_digest", image.ImageDigest)
d.Set("image_pushed_at", image.ImagePushedAt.Unix())
d.Set("image_size_in_bytes", image.ImageSizeInBytes)
if err := d.Set("image_tags", aws.StringValueSlice(image.ImageTags)); err != nil {
return sdkdiag.AppendErrorf(diags, "to set image_tags: %s", err)
err := conn.DescribeImagesPagesWithContext(ctx, input, func(page *ecr.DescribeImagesOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, v := range page.ImageDetails {
if v != nil {
output = append(output, v)
}
}

return !lastPage
})

if tfawserr.ErrCodeEquals(err, ecr.ErrCodeRepositoryNotFoundException) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: input,
}
}

return diags
if err != nil {
return nil, err
}

return output, nil
}
48 changes: 16 additions & 32 deletions internal/service/ecr/image_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ package ecr_test

import (
"fmt"
"strconv"
"testing"

"github.com/aws/aws-sdk-go/service/ecr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
)

func TestAccECRImageDataSource_ecrImage(t *testing.T) {
func TestAccECRImageDataSource_basic(t *testing.T) {
registry, repo, tag := "137112412989", "amazonlinux", "latest"
resourceByTag := "data.aws_ecr_image.by_tag"
resourceByDigest := "data.aws_ecr_image.by_digest"
resourceByMostRecent := "data.aws_ecr_image.by_most_recent"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
Expand All @@ -27,10 +26,13 @@ func TestAccECRImageDataSource_ecrImage(t *testing.T) {
resource.TestCheckResourceAttrSet(resourceByTag, "image_digest"),
resource.TestCheckResourceAttrSet(resourceByTag, "image_pushed_at"),
resource.TestCheckResourceAttrSet(resourceByTag, "image_size_in_bytes"),
testCheckTagInImageTags(resourceByTag, tag),
resource.TestCheckTypeSetElemAttr(resourceByTag, "image_tags.*", tag),
resource.TestCheckResourceAttrSet(resourceByDigest, "image_pushed_at"),
resource.TestCheckResourceAttrSet(resourceByDigest, "image_size_in_bytes"),
testCheckTagInImageTags(resourceByDigest, tag),
resource.TestCheckTypeSetElemAttr(resourceByDigest, "image_tags.*", tag),
resource.TestCheckResourceAttrSet(resourceByMostRecent, "image_pushed_at"),
resource.TestCheckResourceAttrSet(resourceByMostRecent, "image_size_in_bytes"),
resource.TestCheckTypeSetElemAttr(resourceByMostRecent, "image_tags.*", tag),
),
},
},
Expand All @@ -40,39 +42,21 @@ func TestAccECRImageDataSource_ecrImage(t *testing.T) {
func testAccImageDataSourceConfig_basic(reg, repo, tag string) string {
return fmt.Sprintf(`
data "aws_ecr_image" "by_tag" {
registry_id = "%s"
repository_name = "%s"
image_tag = "%s"
registry_id = %[1]q
repository_name = %[2]q
image_tag = %[3]q
}
data "aws_ecr_image" "by_digest" {
registry_id = data.aws_ecr_image.by_tag.registry_id
repository_name = data.aws_ecr_image.by_tag.repository_name
image_digest = data.aws_ecr_image.by_tag.image_digest
}
`, reg, repo, tag)
}

func testCheckTagInImageTags(name, expectedTag string) resource.TestCheckFunc {
return func(s *terraform.State) error {
// Ensure we have enough information in state to look up in API
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Resource not found: %s", name)
}

tagsLenStr, ok := rs.Primary.Attributes["image_tags.#"]
if !ok {
return fmt.Errorf("No attribute 'image_tags' in resource: %s", name)
}
tagsLen, _ := strconv.Atoi(tagsLenStr)
for i := 0; i < tagsLen; i++ {
tag := rs.Primary.Attributes[fmt.Sprintf("image_tags.%d", i)]
if tag == expectedTag {
return nil
}
}
return fmt.Errorf("No tag '%s' in images_tags of resource %s", expectedTag, name)
}
data "aws_ecr_image" "by_most_recent" {
registry_id = data.aws_ecr_image.by_tag.registry_id
repository_name = data.aws_ecr_image.by_tag.repository_name
most_recent = true
}
`, reg, repo, tag)
}
Loading

0 comments on commit 29a61c1

Please sign in to comment.