diff --git a/.changelog/38597.txt b/.changelog/38597.txt new file mode 100644 index 00000000000..4a02cbba9d9 --- /dev/null +++ b/.changelog/38597.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_ecr_repository_creation_template +``` + +```release-note:new-data-source +aws_ecr_repository_creation_template +``` \ No newline at end of file diff --git a/.ci/.semgrep-service-name1.yml b/.ci/.semgrep-service-name1.yml index 70475fde70b..15b21a7dbdf 100644 --- a/.ci/.semgrep-service-name1.yml +++ b/.ci/.semgrep-service-name1.yml @@ -1772,6 +1772,7 @@ rules: metavariable: $NAME patterns: - pattern-regex: "(?i)ECR" + - pattern-not-regex: "TemplateCreate" - focus-metavariable: $NAME - pattern-not: func $NAME($T *testing.T) severity: WARNING diff --git a/internal/generate/servicesemgrep/service.tmpl b/internal/generate/servicesemgrep/service.tmpl index 485a9565962..fc6b2e6aebd 100644 --- a/internal/generate/servicesemgrep/service.tmpl +++ b/internal/generate/servicesemgrep/service.tmpl @@ -29,6 +29,9 @@ {{- if eq .ServiceAlias "CloudTrail" }} - pattern-not-regex: ^testAccCloudTrailConfig_.* {{- end }} + {{- if eq .ServiceAlias "ECR" }} + - pattern-not-regex: "TemplateCreate" + {{- end }} {{- if eq .ServiceAlias "Pipes" }} - pattern-not-regex: ^.+Pipe[sS].*$ {{- end }} diff --git a/internal/service/ecr/exports_test.go b/internal/service/ecr/exports_test.go index 06efedfd5e2..84942e59e2a 100644 --- a/internal/service/ecr/exports_test.go +++ b/internal/service/ecr/exports_test.go @@ -11,13 +11,15 @@ var ( ResourceRegistryScanningConfiguration = resourceRegistryScanningConfiguration ResourceReplicationConfiguration = resourceReplicationConfiguration ResourceRepository = resourceRepository + ResourceRepositoryCreationTemplate = resourceRepositoryCreationTemplate ResourceRepositoryPolicy = resourceRepositoryPolicy - FindLifecyclePolicyByRepositoryName = findLifecyclePolicyByRepositoryName - FindPullThroughCacheRuleByRepositoryPrefix = findPullThroughCacheRuleByRepositoryPrefix - FindRegistryPolicy = findRegistryPolicy - FindRegistryScanningConfiguration = findRegistryScanningConfiguration - FindReplicationConfiguration = findReplicationConfiguration - FindRepositoryByName = findRepositoryByName - FindRepositoryPolicyByRepositoryName = findRepositoryPolicyByRepositoryName + FindLifecyclePolicyByRepositoryName = findLifecyclePolicyByRepositoryName + FindPullThroughCacheRuleByRepositoryPrefix = findPullThroughCacheRuleByRepositoryPrefix + FindRegistryPolicy = findRegistryPolicy + FindRegistryScanningConfiguration = findRegistryScanningConfiguration + FindReplicationConfiguration = findReplicationConfiguration + FindRepositoryByName = findRepositoryByName + FindRepositoryCreationTemplateByRepositoryPrefix = findRepositoryCreationTemplateByRepositoryPrefix + FindRepositoryPolicyByRepositoryName = findRepositoryPolicyByRepositoryName ) diff --git a/internal/service/ecr/repository_creation_template.go b/internal/service/ecr/repository_creation_template.go new file mode 100644 index 00000000000..6eddd12ff79 --- /dev/null +++ b/internal/service/ecr/repository_creation_template.go @@ -0,0 +1,402 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ecr + +import ( + "context" + "log" + + "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/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/structure" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKResource("aws_ecr_repository_creation_template", name="Repository Creation Template") +func resourceRepositoryCreationTemplate() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceRepositoryCreationTemplateCreate, + ReadWithoutTimeout: resourceRepositoryCreationTemplateRead, + UpdateWithoutTimeout: resourceRepositoryCreationTemplateUpdate, + DeleteWithoutTimeout: resourceRepositoryCreationTemplateDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "applied_for": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: enum.Validate[types.RCTAppliedFor](), + }, + }, + "custom_role_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, + }, + names.AttrDescription: { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 255), + }, + names.AttrEncryptionConfiguration: { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "encryption_type": { + Type: schema.TypeString, + Optional: true, + Default: types.EncryptionTypeAes256, + ValidateDiagFunc: enum.Validate[types.EncryptionType](), + }, + names.AttrKMSKey: { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, + }, + "image_tag_mutability": { + Type: schema.TypeString, + Optional: true, + Default: types.ImageTagMutabilityMutable, + ValidateDiagFunc: enum.Validate[types.ImageTagMutability](), + }, + "lifecycle_policy": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + equal, _ := equivalentLifecyclePolicyJSON(old, new) + return equal + }, + DiffSuppressOnRefresh: true, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, + }, + names.AttrPrefix: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(2, 30), + validation.StringMatch( + regexache.MustCompile(`(?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)*[a-z0-9]+(?:[._-][a-z0-9]+)*`), + "must only include alphanumeric, underscore, period, hyphen, or slash characters"), + ), + }, + "registry_id": { + Type: schema.TypeString, + Computed: true, + }, + "repository_policy": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: verify.SuppressEquivalentPolicyDiffs, + DiffSuppressOnRefresh: true, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, + }, + names.AttrResourceTags: tftags.TagsSchema(), + }, + } +} + +func resourceRepositoryCreationTemplateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ECRClient(ctx) + + prefix := d.Get(names.AttrPrefix).(string) + input := &ecr.CreateRepositoryCreationTemplateInput{ + EncryptionConfiguration: expandRepositoryEncryptionConfigurationForRepositoryCreationTemplate(d.Get(names.AttrEncryptionConfiguration).([]interface{})), + ImageTagMutability: types.ImageTagMutability((d.Get("image_tag_mutability").(string))), + Prefix: aws.String(prefix), + } + + if v, ok := d.GetOk("applied_for"); ok && v.(*schema.Set).Len() > 0 { + input.AppliedFor = flex.ExpandStringyValueSet[types.RCTAppliedFor](v.(*schema.Set)) + } + + if v, ok := d.GetOk("custom_role_arn"); ok { + input.CustomRoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk(names.AttrDescription); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("lifecycle_policy"); ok { + policy, err := structure.NormalizeJsonString(v.(string)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + input.LifecyclePolicy = aws.String(policy) + } + + if v, ok := d.GetOk("repository_policy"); ok { + policy, err := structure.NormalizeJsonString(v.(string)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + input.RepositoryPolicy = aws.String(policy) + } + + if v, ok := d.GetOk(names.AttrResourceTags); ok && len(v.(map[string]interface{})) > 0 { + input.ResourceTags = Tags(tftags.New(ctx, v.(map[string]interface{}))) + } + + output, err := conn.CreateRepositoryCreationTemplate(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating ECR Repository Creation Template (%s): %s", prefix, err) + } + + d.SetId(aws.ToString(output.RepositoryCreationTemplate.Prefix)) + + return append(diags, resourceRepositoryCreationTemplateRead(ctx, d, meta)...) +} + +func resourceRepositoryCreationTemplateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ECRClient(ctx) + + rct, registryID, err := findRepositoryCreationTemplateByRepositoryPrefix(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ECR Repository Creation Template (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading ECR Repository Creation Template (%s): %s", d.Id(), err) + } + + d.Set("applied_for", rct.AppliedFor) + d.Set("custom_role_arn", rct.CustomRoleArn) + d.Set(names.AttrDescription, rct.Description) + if err := d.Set(names.AttrEncryptionConfiguration, flattenRepositoryEncryptionConfigurationForRepositoryCreationTemplate(rct.EncryptionConfiguration)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting encryption_configuration: %s", err) + } + d.Set("image_tag_mutability", rct.ImageTagMutability) + + if _, err := equivalentLifecyclePolicyJSON(d.Get("lifecycle_policy").(string), aws.ToString(rct.LifecyclePolicy)); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + policyToSet, err := structure.NormalizeJsonString(aws.ToString(rct.LifecyclePolicy)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + d.Set("lifecycle_policy", policyToSet) + d.Set(names.AttrPrefix, rct.Prefix) + d.Set("registry_id", registryID) + + policyToSet, err = verify.SecondJSONUnlessEquivalent(d.Get("repository_policy").(string), aws.ToString(rct.RepositoryPolicy)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + policyToSet, err = structure.NormalizeJsonString(policyToSet) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + d.Set("repository_policy", policyToSet) + d.Set(names.AttrResourceTags, KeyValueTags(ctx, rct.ResourceTags).Map()) + + return diags +} + +func resourceRepositoryCreationTemplateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ECRClient(ctx) + + prefix := d.Get(names.AttrPrefix).(string) + input := &ecr.UpdateRepositoryCreationTemplateInput{ + Prefix: aws.String(prefix), + } + + if d.HasChange("applied_for") { + if v, ok := d.GetOk("applied_for"); ok && v.(*schema.Set).Len() > 0 { + input.AppliedFor = flex.ExpandStringyValueSet[types.RCTAppliedFor](v.(*schema.Set)) + } + } + + if d.HasChange("custom_role_arn") { + input.CustomRoleArn = aws.String(d.Get("custom_role_arn").(string)) + } + + if d.HasChange(names.AttrDescription) { + input.Description = aws.String(d.Get(names.AttrDescription).(string)) + } + + if d.HasChange(names.AttrEncryptionConfiguration) { + input.EncryptionConfiguration = expandRepositoryEncryptionConfigurationForRepositoryCreationTemplate(d.Get(names.AttrEncryptionConfiguration).([]interface{})) + } + + if d.HasChange("image_tag_mutability") { + input.ImageTagMutability = types.ImageTagMutability((d.Get("image_tag_mutability").(string))) + } + + if d.HasChange("lifecycle_policy") { + policy, err := structure.NormalizeJsonString(d.Get("lifecycle_policy").(string)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + input.LifecyclePolicy = aws.String(policy) + } + + if d.HasChange("repository_policy") { + policy, err := structure.NormalizeJsonString(d.Get("repository_policy").(string)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + input.RepositoryPolicy = aws.String(policy) + } + + if d.HasChange(names.AttrResourceTags) { + input.ResourceTags = Tags(tftags.New(ctx, d.Get(names.AttrResourceTags).(map[string]interface{}))) + } + + _, err := conn.UpdateRepositoryCreationTemplate(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating ECR Repository Creation Template (%s): %s", prefix, err) + } + + return append(diags, resourceRepositoryCreationTemplateRead(ctx, d, meta)...) +} + +func resourceRepositoryCreationTemplateDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ECRClient(ctx) + + log.Printf("[DEBUG] Deleting ECR Repository Creation Template: %s", d.Id()) + _, err := conn.DeleteRepositoryCreationTemplate(ctx, &ecr.DeleteRepositoryCreationTemplateInput{ + Prefix: aws.String(d.Id()), + }) + + if errs.IsA[*types.TemplateNotFoundException](err) { + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting ECR Repository Creation Template (%s): %s", d.Id(), err) + } + + return diags +} + +func findRepositoryCreationTemplateByRepositoryPrefix(ctx context.Context, conn *ecr.Client, repositoryPrefix string) (*types.RepositoryCreationTemplate, *string, error) { + input := &ecr.DescribeRepositoryCreationTemplatesInput{ + Prefixes: []string{repositoryPrefix}, + } + + return findRepositoryCreationTemplate(ctx, conn, input) +} + +func findRepositoryCreationTemplate(ctx context.Context, conn *ecr.Client, input *ecr.DescribeRepositoryCreationTemplatesInput) (*types.RepositoryCreationTemplate, *string, error) { + output, registryID, err := findRepositoryCreationTemplates(ctx, conn, input) + + if err != nil { + return nil, nil, err + } + + rct, err := tfresource.AssertSingleValueResult(output) + + return rct, registryID, err +} + +func findRepositoryCreationTemplates(ctx context.Context, conn *ecr.Client, input *ecr.DescribeRepositoryCreationTemplatesInput) ([]types.RepositoryCreationTemplate, *string, error) { + var ( + output []types.RepositoryCreationTemplate + registryID *string + ) + + pages := ecr.NewDescribeRepositoryCreationTemplatesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if errs.IsA[*types.TemplateNotFoundException](err) { + return nil, nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, nil, err + } + + output = append(output, page.RepositoryCreationTemplates...) + registryID = page.RegistryId + } + + return output, registryID, nil +} + +func expandRepositoryEncryptionConfigurationForRepositoryCreationTemplate(data []interface{}) *types.EncryptionConfigurationForRepositoryCreationTemplate { + if len(data) == 0 || data[0] == nil { + return nil + } + + ec := data[0].(map[string]interface{}) + config := &types.EncryptionConfigurationForRepositoryCreationTemplate{ + EncryptionType: types.EncryptionType((ec["encryption_type"].(string))), + } + if v, ok := ec[names.AttrKMSKey]; ok { + if s := v.(string); s != "" { + config.KmsKey = aws.String(v.(string)) + } + } + return config +} + +func flattenRepositoryEncryptionConfigurationForRepositoryCreationTemplate(ec *types.EncryptionConfigurationForRepositoryCreationTemplate) []map[string]interface{} { + if ec == nil { + return nil + } + + config := map[string]interface{}{ + "encryption_type": ec.EncryptionType, + names.AttrKMSKey: aws.ToString(ec.KmsKey), + } + + return []map[string]interface{}{ + config, + } +} diff --git a/internal/service/ecr/repository_creation_template_data_source.go b/internal/service/ecr/repository_creation_template_data_source.go new file mode 100644 index 00000000000..9512067ad5c --- /dev/null +++ b/internal/service/ecr/repository_creation_template_data_source.go @@ -0,0 +1,128 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ecr + +import ( + "context" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" + "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" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKDataSource("aws_ecr_repository_creation_template", name="Repository Creation Template") +func dataSourceRepositoryCreationTemplate() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceRepositoryCreationTemplateRead, + + Schema: map[string]*schema.Schema{ + "applied_for": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "custom_role_arn": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrDescription: { + Type: schema.TypeString, + Computed: true, + }, + names.AttrEncryptionConfiguration: { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "encryption_type": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrKMSKey: { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "image_tag_mutability": { + Type: schema.TypeString, + Computed: true, + }, + "lifecycle_policy": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrPrefix: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(2, 30), + validation.StringMatch( + regexache.MustCompile(`(?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)*[a-z0-9]+(?:[._-][a-z0-9]+)*`), + "must only include alphanumeric, underscore, period, hyphen, or slash characters"), + ), + }, + "registry_id": { + Type: schema.TypeString, + Computed: true, + }, + "repository_policy": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrResourceTags: tftags.TagsSchemaComputed(), + }, + } +} + +func dataSourceRepositoryCreationTemplateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ECRClient(ctx) + + prefix := d.Get(names.AttrPrefix).(string) + + rct, registryID, err := findRepositoryCreationTemplateByRepositoryPrefix(ctx, conn, prefix) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading ECR Repository Creation Template (%s): %s", prefix, err) + } + + d.SetId(aws.ToString(rct.Prefix)) + d.Set("applied_for", rct.AppliedFor) + d.Set("custom_role_arn", rct.CustomRoleArn) + d.Set(names.AttrDescription, rct.Description) + if err := d.Set(names.AttrEncryptionConfiguration, flattenRepositoryEncryptionConfigurationForRepositoryCreationTemplate(rct.EncryptionConfiguration)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting encryption_configuration: %s", err) + } + d.Set("image_tag_mutability", rct.ImageTagMutability) + + policy, err := structure.NormalizeJsonString(aws.ToString(rct.LifecyclePolicy)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + d.Set("lifecycle_policy", policy) + d.Set(names.AttrPrefix, rct.Prefix) + d.Set("registry_id", registryID) + + policy, err = structure.NormalizeJsonString(aws.ToString(rct.RepositoryPolicy)) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + d.Set("repository_policy", policy) + d.Set(names.AttrResourceTags, KeyValueTags(ctx, rct.ResourceTags).Map()) + + return diags +} diff --git a/internal/service/ecr/repository_creation_template_data_source_test.go b/internal/service/ecr/repository_creation_template_data_source_test.go new file mode 100644 index 00000000000..ee357c869ee --- /dev/null +++ b/internal/service/ecr/repository_creation_template_data_source_test.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ecr_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ecr/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccECRRepositoryCreationTemplateDataSource_basic(t *testing.T) { + ctx := acctest.Context(t) + repositoryPrefix := "tf-test-" + sdkacctest.RandString(8) + dataSource := "data.aws_ecr_repository_creation_template.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: testAccRepositoryCreationTemplateDataSourceConfig_basic(repositoryPrefix), + Check: resource.ComposeAggregateTestCheckFunc( + acctest.CheckResourceAttrAccountID(dataSource, "registry_id"), + resource.TestCheckResourceAttr(dataSource, "applied_for.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(dataSource, "applied_for.*", string(types.RCTAppliedForPullThroughCache)), + resource.TestCheckResourceAttr(dataSource, "custom_role_arn", ""), + resource.TestCheckResourceAttr(dataSource, names.AttrDescription, ""), + resource.TestCheckResourceAttr(dataSource, "encryption_configuration.#", acctest.Ct1), + resource.TestCheckResourceAttr(dataSource, "encryption_configuration.0.encryption_type", string(types.EncryptionTypeAes256)), + resource.TestCheckResourceAttr(dataSource, "encryption_configuration.0.kms_key", ""), + resource.TestCheckResourceAttr(dataSource, "image_tag_mutability", string(types.ImageTagMutabilityMutable)), + resource.TestCheckResourceAttr(dataSource, "lifecycle_policy", ""), + resource.TestCheckResourceAttr(dataSource, names.AttrPrefix, repositoryPrefix), + resource.TestCheckResourceAttr(dataSource, "repository_policy", ""), + resource.TestCheckResourceAttr(dataSource, "resource_tags.%", acctest.Ct1), + resource.TestCheckResourceAttr(dataSource, "resource_tags.Foo", "Bar"), + ), + }, + }, + }) +} + +func testAccRepositoryCreationTemplateDataSourceConfig_basic(repositoryPrefix string) string { + return fmt.Sprintf(` +resource "aws_ecr_repository_creation_template" "test" { + prefix = %[1]q + + applied_for = [ + "PULL_THROUGH_CACHE", + ] + + resource_tags = { + Foo = "Bar" + } +} + +data "aws_ecr_repository_creation_template" "test" { + prefix = aws_ecr_repository_creation_template.test.prefix +} +`, repositoryPrefix) +} diff --git a/internal/service/ecr/repository_creation_template_test.go b/internal/service/ecr/repository_creation_template_test.go new file mode 100644 index 00000000000..1ff2097956e --- /dev/null +++ b/internal/service/ecr/repository_creation_template_test.go @@ -0,0 +1,396 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ecr_test + +import ( + "context" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/service/ecr/types" + 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/internal/conns" + tfecr "github.com/hashicorp/terraform-provider-aws/internal/service/ecr" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccECRRepositoryCreationTemplate_basic(t *testing.T) { + ctx := acctest.Context(t) + repositoryPrefix := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_ecr_repository_creation_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRepositoryCreationTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRepositoryCreationTemplateConfig_basic(repositoryPrefix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckRepositoryCreationTemplateExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "applied_for.#", acctest.Ct2), + resource.TestCheckTypeSetElemAttr(resourceName, "applied_for.*", string(types.RCTAppliedForPullThroughCache)), + resource.TestCheckTypeSetElemAttr(resourceName, "applied_for.*", string(types.RCTAppliedForReplication)), + resource.TestCheckResourceAttr(resourceName, "custom_role_arn", ""), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, ""), + resource.TestCheckResourceAttr(resourceName, "encryption_configuration.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "encryption_configuration.0.encryption_type", string(types.EncryptionTypeAes256)), + resource.TestCheckResourceAttr(resourceName, "encryption_configuration.0.kms_key", ""), + resource.TestCheckResourceAttr(resourceName, "image_tag_mutability", string(types.ImageTagMutabilityMutable)), + resource.TestCheckResourceAttr(resourceName, "lifecycle_policy", ""), + resource.TestCheckResourceAttr(resourceName, names.AttrPrefix, repositoryPrefix), + resource.TestCheckResourceAttr(resourceName, "repository_policy", ""), + resource.TestCheckResourceAttr(resourceName, "resource_tags.%", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "resource_tags.Foo", "Bar"), + acctest.CheckResourceAttrAccountID(resourceName, "registry_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccECRRepositoryCreationTemplate_disappears(t *testing.T) { + ctx := acctest.Context(t) + repositoryPrefix := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_ecr_repository_creation_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRepositoryCreationTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRepositoryCreationTemplateConfig_basic(repositoryPrefix), + Check: resource.ComposeTestCheckFunc( + testAccCheckRepositoryCreationTemplateExists(ctx, resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfecr.ResourceRepositoryCreationTemplate(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccECRRepositoryCreationTemplate_failWhenAlreadyExists(t *testing.T) { + ctx := acctest.Context(t) + repositoryPrefix := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_ecr_repository_creation_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRepositoryCreationTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRepositoryCreationTemplateConfig_failWhenAlreadyExist(repositoryPrefix), + Check: resource.ComposeTestCheckFunc( + testAccCheckRepositoryCreationTemplateExists(ctx, resourceName), + ), + ExpectError: regexache.MustCompile(`TemplateAlreadyExistsException`), + }, + }, + }) +} + +func TestAccECRRepositoryCreationTemplate_ignoreEquivalentLifecycle(t *testing.T) { + ctx := acctest.Context(t) + repositoryPrefix := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_ecr_repository_creation_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRepositoryCreationTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRepositoryCreationTemplateConfig_lifecycleOrder(repositoryPrefix), + Check: resource.ComposeTestCheckFunc( + testAccCheckRepositoryCreationTemplateExists(ctx, resourceName), + ), + }, + { + Config: testAccRepositoryCreationTemplateConfig_lifecycleNewOrder(repositoryPrefix), + PlanOnly: true, + }, + }, + }) +} + +func TestAccECRRepositoryCreationTemplate_repository(t *testing.T) { + ctx := acctest.Context(t) + repositoryPrefix := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_ecr_repository_creation_template.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECRServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRepositoryCreationTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRepositoryCreationTemplateConfig_repositoryInitial(repositoryPrefix), + Check: resource.ComposeTestCheckFunc( + testAccCheckRepositoryCreationTemplateExists(ctx, resourceName), + resource.TestMatchResourceAttr(resourceName, "repository_policy", regexache.MustCompile(repositoryPrefix)), + acctest.CheckResourceAttrAccountID(resourceName, "registry_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccRepositoryCreationTemplateConfig_repositoryUpdated(repositoryPrefix), + Check: resource.ComposeTestCheckFunc( + testAccCheckRepositoryCreationTemplateExists(ctx, resourceName), + resource.TestMatchResourceAttr(resourceName, "repository_policy", regexache.MustCompile(repositoryPrefix)), + resource.TestMatchResourceAttr(resourceName, "repository_policy", regexache.MustCompile("ecr:DescribeImages")), + acctest.CheckResourceAttrAccountID(resourceName, "registry_id"), + ), + }, + }, + }) +} + +func testAccCheckRepositoryCreationTemplateDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ECRClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ecr_repository_creation_template" { + continue + } + + _, _, err := tfecr.FindRepositoryCreationTemplateByRepositoryPrefix(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("ECR Repository Creation Template %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckRepositoryCreationTemplateExists(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ECRClient(ctx) + + _, _, err := tfecr.FindRepositoryCreationTemplateByRepositoryPrefix(ctx, conn, rs.Primary.ID) + + return err + } +} + +func testAccRepositoryCreationTemplateConfig_basic(repositoryPrefix string) string { + return fmt.Sprintf(` +resource "aws_ecr_repository_creation_template" "test" { + prefix = %[1]q + + applied_for = [ + "PULL_THROUGH_CACHE", + "REPLICATION", + ] + + resource_tags = { + Foo = "Bar" + } +} +`, repositoryPrefix) +} + +func testAccRepositoryCreationTemplateConfig_failWhenAlreadyExist(repositoryPrefix string) string { + return fmt.Sprintf(` +resource "aws_ecr_repository_creation_template" "test" { + prefix = %[1]q + + applied_for = [ + "PULL_THROUGH_CACHE", + "REPLICATION", + ] +} + +resource "aws_ecr_repository_creation_template" "duplicate" { + prefix = %[1]q + + applied_for = [ + "PULL_THROUGH_CACHE", + "REPLICATION", + ] + + depends_on = [ + aws_ecr_repository_creation_template.test, + ] +} +`, repositoryPrefix) +} + +func testAccRepositoryCreationTemplateConfig_lifecycleOrder(repositoryPrefix string) string { + return fmt.Sprintf(` +resource "aws_ecr_repository_creation_template" "test" { + prefix = %[1]q + + applied_for = [ + "PULL_THROUGH_CACHE", + ] + + lifecycle_policy = jsonencode({ + rules = [ + { + rulePriority = 1 + description = "Expire images older than 14 days" + selection = { + tagStatus = "untagged" + countType = "sinceImagePushed" + countUnit = "days" + countNumber = 14 + } + action = { + type = "expire" + } + }, + { + rulePriority = 2 + description = "Expire tagged images older than 14 days" + selection = { + tagStatus = "tagged" + tagPrefixList = [ + "first", + "second", + "third", + ] + countType = "sinceImagePushed" + countUnit = "days" + countNumber = 14 + } + action = { + type = "expire" + } + }, + ] + }) +} +`, repositoryPrefix) +} + +func testAccRepositoryCreationTemplateConfig_lifecycleNewOrder(repositoryPrefix string) string { + return fmt.Sprintf(` +resource "aws_ecr_repository_creation_template" "test" { + prefix = %[1]q + + applied_for = [ + "PULL_THROUGH_CACHE", + ] + + lifecycle_policy = jsonencode({ + rules = [ + { + rulePriority = 2 + description = "Expire tagged images older than 14 days" + selection = { + tagStatus = "tagged" + tagPrefixList = [ + "third", + "second", + "first", + ] + countType = "sinceImagePushed" + countUnit = "days" + countNumber = 14 + } + action = { + type = "expire" + } + }, + { + rulePriority = 1 + description = "Expire images older than 14 days" + selection = { + tagStatus = "untagged" + countType = "sinceImagePushed" + countUnit = "days" + countNumber = 14 + } + action = { + type = "expire" + } + }, + ] + }) +} +`, repositoryPrefix) +} + +func testAccRepositoryCreationTemplateConfig_repositoryInitial(repositoryPrefix string) string { + return fmt.Sprintf(` +resource "aws_ecr_repository_creation_template" "test" { + prefix = %[1]q + + applied_for = [ + "REPLICATION", + ] + + repository_policy = jsonencode({ + Version = "2008-10-17" + Statement = [{ + Sid = %[1]q + Effect = "Allow" + Principal = "*" + Action = "ecr:ListImages" + }] + }) +} +`, repositoryPrefix) +} + +func testAccRepositoryCreationTemplateConfig_repositoryUpdated(repositoryPrefix string) string { + return fmt.Sprintf(` +resource "aws_ecr_repository_creation_template" "test" { + prefix = %[1]q + + applied_for = [ + "REPLICATION", + ] + + repository_policy = jsonencode({ + Version = "2008-10-17" + Statement = [{ + Sid = %[1]q + Effect = "Allow" + Principal = "*" + Action = [ + "ecr:ListImages", + "ecr:DescribeImages", + ] + }] + }) +} +`, repositoryPrefix) +} diff --git a/internal/service/ecr/service_package_gen.go b/internal/service/ecr/service_package_gen.go index 200916df88b..9a1f8e3ee2f 100644 --- a/internal/service/ecr/service_package_gen.go +++ b/internal/service/ecr/service_package_gen.go @@ -56,6 +56,11 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac IdentifierAttribute: names.AttrARN, }, }, + { + Factory: dataSourceRepositoryCreationTemplate, + TypeName: "aws_ecr_repository_creation_template", + Name: "Repository Creation Template", + }, } } @@ -94,6 +99,11 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka IdentifierAttribute: names.AttrARN, }, }, + { + Factory: resourceRepositoryCreationTemplate, + TypeName: "aws_ecr_repository_creation_template", + Name: "Repository Creation Template", + }, { Factory: resourceRepositoryPolicy, TypeName: "aws_ecr_repository_policy", diff --git a/website/docs/d/ecr_repository_creation_template.html.markdown b/website/docs/d/ecr_repository_creation_template.html.markdown new file mode 100644 index 00000000000..5044924aa13 --- /dev/null +++ b/website/docs/d/ecr_repository_creation_template.html.markdown @@ -0,0 +1,44 @@ +--- +subcategory: "ECR (Elastic Container Registry)" +layout: "aws" +page_title: "AWS: aws_ecr_repository_creation_template" +description: |- + Provides details about an ECR Repository Creation Template +--- + +# Data Source: aws_ecr_repository_creation_template + +The ECR Repository Creation Template data source allows the template details to be retrieved for a Repository Creation Template. + +## Example Usage + +```terraform +data "aws_ecr_repository_creation_template" "example" { + prefix = "example" +} +``` + +## Argument Reference + +This data source supports the following arguments: + +* `prefix` - (Required) The repository name prefix that the template matches against. + +## Attribute Reference + +This data source exports the following attributes in addition to the arguments above: + +* `applied_for` - Which features this template applies to. Contains one or more of `PULL_THROUGH_CACHE` or `REPLICATION`. +* `custom_role_arn` - The ARN of the custom role used for repository creation. +* `description` - The description for this template. +* `encryption_configuration` - Encryption configuration for any created repositories. See [Encryption Configuration](#encryption-configuration) below. +* `image_tag_mutability` - The tag mutability setting for any created repositories. +* `lifecycle_policy` - The lifecycle policy document to apply to any created repositories. +* `registry_id` - The registry ID the repository creation template applies to. +* `repository_policy` - The registry policy document to apply to any created repositories. +* `resource_tags` - A map of tags to assign to any created repositories. + +### Encryption Configuration + +* `encryption_type` - Encryption type to use for any created repositories, either `AES256` or `KMS`. +* `kms_key` - If `encryption_type` is `KMS`, the ARN of the KMS key used. diff --git a/website/docs/r/ecr_repository_creation_template.html.markdown b/website/docs/r/ecr_repository_creation_template.html.markdown new file mode 100644 index 00000000000..1766f18cba6 --- /dev/null +++ b/website/docs/r/ecr_repository_creation_template.html.markdown @@ -0,0 +1,127 @@ +--- +subcategory: "ECR (Elastic Container Registry)" +layout: "aws" +page_title: "AWS: aws_ecr_repository_creation_template" +description: |- + Provides an Elastic Container Registry Repository Creation Template. +--- + +# Resource: aws_ecr_repository_creation_template + +Provides an Elastic Container Registry Repository Creation Template. + +## Example Usage + +```terraform +data "aws_iam_policy_document" "example" { + statement { + sid = "new policy" + effect = "Allow" + + principals { + type = "AWS" + identifiers = ["123456789012"] + } + + actions = [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:DescribeRepositories", + "ecr:GetRepositoryPolicy", + "ecr:ListImages", + "ecr:DeleteRepository", + "ecr:BatchDeleteImage", + "ecr:SetRepositoryPolicy", + "ecr:DeleteRepositoryPolicy", + ] + } +} + +resource "aws_ecr_repository_creation_template" "example" { + prefix = "example" + description = "An example template" + image_tag_mutability = "IMMUTABLE" + custom_role_arn = "arn:aws:iam::123456789012:role/example" + + applied_for = [ + "PULL_THROUGH_CACHE", + ] + + encryption_configuration { + encryption_type = "AES256" + } + + repository_policy = data.aws_iam_policy_document.example.json + + lifecycle_policy = <