Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changelog/43642.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_ecr_repository: Add `image_tag_mutability_exclusion_filter` argument
```

```release-note:enhancement
resource/aws_ecr_repository: Support `IMMUTABLE_WITH_EXCLUSION` and `MUTABLE_WITH_EXCLUSION` as valid values for `image_tag_mutability`
```
110 changes: 109 additions & 1 deletion internal/service/ecr/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ package ecr

import (
"context"
"fmt"
"log"
"strings"
"time"

"github.com/YakDriver/regexache"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"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/enum"
"github.com/hashicorp/terraform-provider-aws/internal/errs"
Expand Down Expand Up @@ -41,6 +46,8 @@ func resourceRepository() *schema.Resource {
Delete: schema.DefaultTimeout(20 * time.Minute),
},

CustomizeDiff: validateImageTagMutabilityExclusionFilterUsage,

Schema: map[string]*schema.Schema{
names.AttrARN: {
Type: schema.TypeString,
Expand Down Expand Up @@ -93,6 +100,32 @@ func resourceRepository() *schema.Resource {
Default: types.ImageTagMutabilityMutable,
ValidateDiagFunc: enum.Validate[types.ImageTagMutability](),
},
"image_tag_mutability_exclusion_filter": {
Type: schema.TypeList,
Optional: true,
MaxItems: 5,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
names.AttrFilter: {
Type: schema.TypeString,
Required: true,
ValidateDiagFunc: validation.AllDiag(
validation.ToDiagFunc(validation.StringLenBetween(1, 128)),
validation.ToDiagFunc(validation.StringMatch(
regexache.MustCompile(`^[a-zA-Z0-9._*-]+$`),
"must contain only letters, numbers, and special characters (._*-)",
)),
validateImageTagMutabilityExclusionFilter(),
),
},
"filter_type": {
Type: schema.TypeString,
Required: true,
ValidateDiagFunc: enum.Validate[types.ImageTagMutabilityExclusionFilterType](),
},
},
},
},
names.AttrName: {
Type: schema.TypeString,
Required: true,
Expand Down Expand Up @@ -131,6 +164,10 @@ func resourceRepositoryCreate(ctx context.Context, d *schema.ResourceData, meta
}
}

if v, ok := d.GetOk("image_tag_mutability_exclusion_filter"); ok && len(v.([]any)) > 0 {
input.ImageTagMutabilityExclusionFilters = expandImageTagMutabilityExclusionFilters(v.([]any))
}

output, err := conn.CreateRepository(ctx, input)

// Some partitions (e.g. ISO) may not support tag-on-create.
Expand Down Expand Up @@ -189,6 +226,9 @@ func resourceRepositoryRead(ctx context.Context, d *schema.ResourceData, meta an
return sdkdiag.AppendErrorf(diags, "setting image_scanning_configuration: %s", err)
}
d.Set("image_tag_mutability", repository.ImageTagMutability)
if err := d.Set("image_tag_mutability_exclusion_filter", flattenImageTagMutabilityExclusionFilters(repository.ImageTagMutabilityExclusionFilters)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting image_tag_mutability_exclusion_filter: %s", err)
}
d.Set(names.AttrName, repository.RepositoryName)
d.Set("registry_id", repository.RegistryId)
d.Set("repository_url", repository.RepositoryUri)
Expand All @@ -200,13 +240,17 @@ func resourceRepositoryUpdate(ctx context.Context, d *schema.ResourceData, meta
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).ECRClient(ctx)

if d.HasChange("image_tag_mutability") {
if d.HasChanges("image_tag_mutability", "image_tag_mutability_exclusion_filter") {
input := &ecr.PutImageTagMutabilityInput{
ImageTagMutability: types.ImageTagMutability((d.Get("image_tag_mutability").(string))),
RegistryId: aws.String(d.Get("registry_id").(string)),
RepositoryName: aws.String(d.Id()),
}

if v, ok := d.GetOk("image_tag_mutability_exclusion_filter"); ok && len(v.([]any)) > 0 {
input.ImageTagMutabilityExclusionFilters = expandImageTagMutabilityExclusionFilters(v.([]any))
}

_, err := conn.PutImageTagMutability(ctx, input)

if err != nil {
Expand Down Expand Up @@ -343,3 +387,67 @@ func flattenRepositoryEncryptionConfiguration(ec *types.EncryptionConfiguration)
config,
}
}

func expandImageTagMutabilityExclusionFilters(data []any) []types.ImageTagMutabilityExclusionFilter {
if len(data) == 0 {
return nil
}

var filters []types.ImageTagMutabilityExclusionFilter
for _, v := range data {
tfMap := v.(map[string]any)
filter := types.ImageTagMutabilityExclusionFilter{
Filter: aws.String(tfMap[names.AttrFilter].(string)),
FilterType: types.ImageTagMutabilityExclusionFilterType(tfMap["filter_type"].(string)),
}
filters = append(filters, filter)
}

return filters
}

func flattenImageTagMutabilityExclusionFilters(filters []types.ImageTagMutabilityExclusionFilter) []any {
if len(filters) == 0 {
return nil
}

var tfList []any
for _, filter := range filters {
tfMap := map[string]any{
names.AttrFilter: aws.ToString(filter.Filter),
"filter_type": string(filter.FilterType),
}
tfList = append(tfList, tfMap)
}

return tfList
}

func validateImageTagMutabilityExclusionFilter() schema.SchemaValidateDiagFunc {
return func(v any, path cty.Path) diag.Diagnostics {
var diags diag.Diagnostics
value := v.(string)

wildcardCount := strings.Count(value, "*")
if wildcardCount > 2 {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Invalid filter pattern",
Detail: "Image tag mutability exclusion filter can contain a maximum of 2 wildcards (*)",
})
}

return diags
}
}

func validateImageTagMutabilityExclusionFilterUsage(_ context.Context, d *schema.ResourceDiff, meta any) error {
mutability := d.Get("image_tag_mutability").(string)
filters := d.Get("image_tag_mutability_exclusion_filter").([]any)

if len(filters) > 0 && mutability != string(types.ImageTagMutabilityImmutableWithExclusion) && mutability != string(types.ImageTagMutabilityMutableWithExclusion) {
return fmt.Errorf("image_tag_mutability_exclusion_filter can only be used when image_tag_mutability is set to IMMUTABLE_WITH_EXCLUSION or MUTABLE_WITH_EXCLUSION")
}

return nil
}
165 changes: 165 additions & 0 deletions internal/service/ecr/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package ecr_test
import (
"context"
"fmt"
"strings"
"testing"

"github.com/YakDriver/regexache"
Expand Down Expand Up @@ -156,6 +157,128 @@ func TestAccECRRepository_immutability(t *testing.T) {
})
}

func TestAccECRRepository_immutabilityWithExclusion(t *testing.T) {
ctx := acctest.Context(t)
var v types.Repository
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_ecr_repository.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckRepositoryDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccRepositoryConfig_immutabilityWithExclusion(rName, "latest*"),
Check: resource.ComposeTestCheckFunc(
testAccCheckRepositoryExists(ctx, resourceName, &v),
resource.TestCheckResourceAttr(resourceName, names.AttrName, rName),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability", string(types.ImageTagMutabilityImmutableWithExclusion)),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability_exclusion_filter.#", "1"),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability_exclusion_filter.0.filter", "latest*"),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability_exclusion_filter.0.filter_type", string(types.ImageTagMutabilityExclusionFilterTypeWildcard)),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: testAccRepositoryConfig_immutabilityWithExclusion(rName, "dev-*"),
Check: resource.ComposeTestCheckFunc(
testAccCheckRepositoryExists(ctx, resourceName, &v),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability_exclusion_filter.0.filter", "dev-*"),
),
},
},
})
}

func TestAccECRRepository_mutabilityWithExclusion(t *testing.T) {
ctx := acctest.Context(t)
var v types.Repository
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_ecr_repository.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckRepositoryDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccRepositoryConfig_mutabilityWithExclusion(rName, "prod-*"),
Check: resource.ComposeTestCheckFunc(
testAccCheckRepositoryExists(ctx, resourceName, &v),
resource.TestCheckResourceAttr(resourceName, names.AttrName, rName),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability", string(types.ImageTagMutabilityMutableWithExclusion)),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability_exclusion_filter.#", "1"),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability_exclusion_filter.0.filter", "prod-*"),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability_exclusion_filter.0.filter_type", string(types.ImageTagMutabilityExclusionFilterTypeWildcard)),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: testAccRepositoryConfig_mutabilityWithExclusion(rName, "release-*"),
Check: resource.ComposeTestCheckFunc(
testAccCheckRepositoryExists(ctx, resourceName, &v),
resource.TestCheckResourceAttr(resourceName, "image_tag_mutability_exclusion_filter.0.filter", "release-*"),
),
},
},
})
}

func TestAccECRRepository_immutabilityWithExclusion_validation(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckRepositoryDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccRepositoryConfig_immutabilityWithExclusion(rName, "invalid!@#$"),
ExpectError: regexache.MustCompile(`must contain only letters, numbers, and special characters`),
},
{
Config: testAccRepositoryConfig_immutabilityWithExclusion(rName, "a*b*c*d"),
ExpectError: regexache.MustCompile(`Image tag mutability exclusion filter can contain a maximum of 2 wildcards`),
},
{
Config: testAccRepositoryConfig_immutabilityWithExclusion(rName, strings.Repeat("a", 129)),
ExpectError: regexache.MustCompile(`expected length of.*to be in the range.*128`),
},
},
})
}

func TestAccECRRepository_immutabilityWithExclusion_crossValidation(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckRepositoryDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccRepositoryConfig_immutabilityWithExclusionInvalid(rName),
ExpectError: regexache.MustCompile(`image_tag_mutability_exclusion_filter can only be used when image_tag_mutability is set to IMMUTABLE_WITH_EXCLUSION`),
},
},
})
}

func TestAccECRRepository_Image_scanning(t *testing.T) {
ctx := acctest.Context(t)
var v1, v2 types.Repository
Expand Down Expand Up @@ -448,6 +571,48 @@ resource "aws_ecr_repository" "test" {
`, rName)
}

func testAccRepositoryConfig_immutabilityWithExclusion(rName, filter string) string {
return fmt.Sprintf(`
resource "aws_ecr_repository" "test" {
name = %[1]q
image_tag_mutability = "IMMUTABLE_WITH_EXCLUSION"

image_tag_mutability_exclusion_filter {
filter = %[2]q
filter_type = "WILDCARD"
}
}
`, rName, filter)
}

func testAccRepositoryConfig_mutabilityWithExclusion(rName, filter string) string {
return fmt.Sprintf(`
resource "aws_ecr_repository" "test" {
name = %[1]q
image_tag_mutability = "MUTABLE_WITH_EXCLUSION"

image_tag_mutability_exclusion_filter {
filter = %[2]q
filter_type = "WILDCARD"
}
}
`, rName, filter)
}

func testAccRepositoryConfig_immutabilityWithExclusionInvalid(rName string) string {
return fmt.Sprintf(`
resource "aws_ecr_repository" "test" {
name = %[1]q
image_tag_mutability = "MUTABLE"

image_tag_mutability_exclusion_filter {
filter = "latest*"
filter_type = "WILDCARD"
}
}
`, rName)
}

func testAccRepositoryConfig_imageScanningConfiguration(rName string, scanOnPush bool) string {
return fmt.Sprintf(`
resource "aws_ecr_repository" "test" {
Expand Down
Loading
Loading