diff --git a/.semgrep.yml b/.semgrep.yml index 9660007b6aa4..3520fd50fad2 100644 --- a/.semgrep.yml +++ b/.semgrep.yml @@ -8,6 +8,7 @@ rules: - aws/structure.go - aws/validators.go - aws/*wafregional*.go + - aws/resource_aws_serverlessapplicationrepository_cloudformation_stack.go - aws/*_test.go - aws/internal/keyvaluetags/ - aws/internal/service/wafregional/ diff --git a/aws/data_source_aws_serverlessapplicationrepository_application.go b/aws/data_source_aws_serverlessapplicationrepository_application.go new file mode 100644 index 000000000000..f1e3980a50b2 --- /dev/null +++ b/aws/data_source_aws_serverlessapplicationrepository_application.go @@ -0,0 +1,72 @@ +package aws + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/serverlessapplicationrepository/finder" +) + +func dataSourceAwsServerlessApplicationRepositoryApplication() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsServerlessRepositoryApplicationRead, + + Schema: map[string]*schema.Schema{ + "application_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "semantic_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "required_capabilities": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "source_code_url": { + Type: schema.TypeString, + Computed: true, + }, + "template_url": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsServerlessRepositoryApplicationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).serverlessapplicationrepositoryconn + + applicationID := d.Get("application_id").(string) + semanticVersion := d.Get("semantic_version").(string) + + output, err := finder.Application(conn, applicationID, semanticVersion) + if err != nil { + descriptor := applicationID + if semanticVersion != "" { + descriptor += fmt.Sprintf(", version %s", semanticVersion) + } + return fmt.Errorf("error getting Serverless Application Repository application (%s): %w", descriptor, err) + } + + d.SetId(applicationID) + d.Set("name", output.Name) + d.Set("semantic_version", output.Version.SemanticVersion) + d.Set("source_code_url", output.Version.SourceCodeUrl) + d.Set("template_url", output.Version.TemplateUrl) + if err = d.Set("required_capabilities", flattenStringSet(output.Version.RequiredCapabilities)); err != nil { + return fmt.Errorf("failed to set required_capabilities: %w", err) + } + + return nil +} diff --git a/aws/data_source_aws_serverlessapplicationrepository_application_test.go b/aws/data_source_aws_serverlessapplicationrepository_application_test.go new file mode 100644 index 000000000000..930545879f76 --- /dev/null +++ b/aws/data_source_aws_serverlessapplicationrepository_application_test.go @@ -0,0 +1,128 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfawsresource" +) + +func TestAccDataSourceAwsServerlessApplicationRepositoryApplication_Basic(t *testing.T) { + datasourceName := "data.aws_serverlessapplicationrepository_application.secrets_manager_postgres_single_user_rotator" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceID(datasourceName), + resource.TestCheckResourceAttr(datasourceName, "name", "SecretsManagerRDSPostgreSQLRotationSingleUser"), + resource.TestCheckResourceAttrSet(datasourceName, "semantic_version"), + resource.TestCheckResourceAttrSet(datasourceName, "source_code_url"), + resource.TestCheckResourceAttrSet(datasourceName, "template_url"), + resource.TestCheckResourceAttrSet(datasourceName, "required_capabilities.#"), + ), + }, + { + Config: testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig_NonExistent, + ExpectError: regexp.MustCompile(`error getting Serverless Application Repository application`), + }, + }, + }) +} +func TestAccDataSourceAwsServerlessApplicationRepositoryApplication_Versioned(t *testing.T) { + datasourceName := "data.aws_serverlessapplicationrepository_application.secrets_manager_postgres_single_user_rotator" + + const ( + version1 = "1.0.13" + version2 = "1.1.36" + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig_Versioned(version1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceID(datasourceName), + resource.TestCheckResourceAttr(datasourceName, "name", "SecretsManagerRDSPostgreSQLRotationSingleUser"), + resource.TestCheckResourceAttr(datasourceName, "semantic_version", version1), + resource.TestCheckResourceAttrSet(datasourceName, "source_code_url"), + resource.TestCheckResourceAttrSet(datasourceName, "template_url"), + resource.TestCheckResourceAttr(datasourceName, "required_capabilities.#", "0"), + ), + }, + { + Config: testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig_Versioned(version2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceID(datasourceName), + resource.TestCheckResourceAttr(datasourceName, "name", "SecretsManagerRDSPostgreSQLRotationSingleUser"), + resource.TestCheckResourceAttr(datasourceName, "semantic_version", version2), + resource.TestCheckResourceAttrSet(datasourceName, "source_code_url"), + resource.TestCheckResourceAttrSet(datasourceName, "template_url"), + resource.TestCheckResourceAttr(datasourceName, "required_capabilities.#", "2"), + tfawsresource.TestCheckTypeSetElemAttr(datasourceName, "required_capabilities.*", "CAPABILITY_IAM"), + tfawsresource.TestCheckTypeSetElemAttr(datasourceName, "required_capabilities.*", "CAPABILITY_RESOURCE_POLICY"), + ), + }, + { + Config: testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig_Versioned_NonExistent, + ExpectError: regexp.MustCompile(`error getting Serverless Application Repository application`), + }, + }, + }) +} + +func testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceID(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Can't find Serverless Repository Application data source: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("AMI data source ID not set") + } + return nil + } +} + +const testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig = testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication + ` +data "aws_serverlessapplicationrepository_application" "secrets_manager_postgres_single_user_rotator" { + application_id = local.postgres_single_user_rotator_arn +} +` + +const testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig_NonExistent = ` +data "aws_serverlessapplicationrepository_application" "no_such_function" { + application_id = "arn:${data.aws_partition.current.partition}:serverlessrepo:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:applications/ThisFunctionDoesNotExist" +} + +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} +data "aws_region" "current" {} +` + +func testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig_Versioned(version string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +data "aws_serverlessapplicationrepository_application" "secrets_manager_postgres_single_user_rotator" { + application_id = local.postgres_single_user_rotator_arn + semantic_version = "%[1]s" +} +`, version)) +} + +const testAccCheckAwsServerlessApplicationRepositoryApplicationDataSourceConfig_Versioned_NonExistent = testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication + ` +data "aws_serverlessapplicationrepository_application" "secrets_manager_postgres_single_user_rotator" { + application_id = local.postgres_single_user_rotator_arn + semantic_version = "42.13.7" +} +` diff --git a/aws/internal/keyvaluetags/key_value_tags.go b/aws/internal/keyvaluetags/key_value_tags.go index fd90b01ebe2a..00b80bc572ca 100644 --- a/aws/internal/keyvaluetags/key_value_tags.go +++ b/aws/internal/keyvaluetags/key_value_tags.go @@ -18,10 +18,11 @@ import ( ) const ( - AwsTagKeyPrefix = `aws:` - ElasticbeanstalkTagKeyPrefix = `elasticbeanstalk:` - NameTagKey = `Name` - RdsTagKeyPrefix = `rds:` + AwsTagKeyPrefix = `aws:` + ElasticbeanstalkTagKeyPrefix = `elasticbeanstalk:` + NameTagKey = `Name` + RdsTagKeyPrefix = `rds:` + ServerlessApplicationRepositoryTagKeyPrefix = `serverlessrepo:` ) // IgnoreConfig contains various options for removing resource tags. @@ -127,6 +128,25 @@ func (tags KeyValueTags) IgnoreRds() KeyValueTags { return result } +// IgnoreServerlessApplicationRepository returns non-AWS and non-ServerlessApplicationRepository tag keys. +func (tags KeyValueTags) IgnoreServerlessApplicationRepository() KeyValueTags { + result := make(KeyValueTags) + + for k, v := range tags { + if strings.HasPrefix(k, AwsTagKeyPrefix) { + continue + } + + if strings.HasPrefix(k, ServerlessApplicationRepositoryTagKeyPrefix) { + continue + } + + result[k] = v + } + + return result +} + // Ignore returns non-matching tag keys. func (tags KeyValueTags) Ignore(ignoreTags KeyValueTags) KeyValueTags { result := make(KeyValueTags) diff --git a/aws/internal/service/cloudformation/finder/finder.go b/aws/internal/service/cloudformation/finder/finder.go new file mode 100644 index 000000000000..88f58fed5bab --- /dev/null +++ b/aws/internal/service/cloudformation/finder/finder.go @@ -0,0 +1,56 @@ +package finder + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func Stack(conn *cloudformation.CloudFormation, stackID string) (*cloudformation.Stack, error) { + input := &cloudformation.DescribeStacksInput{ + StackName: aws.String(stackID), + } + log.Printf("[DEBUG] Querying CloudFormation Stack: %s", input) + resp, err := conn.DescribeStacks(input) + if tfawserr.ErrCodeEquals(err, "ValidationError") { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + LastResponse: resp, + } + } + if err != nil { + return nil, err + } + + if resp == nil { + return nil, &resource.NotFoundError{ + LastRequest: input, + LastResponse: resp, + Message: "returned empty response", + } + + } + stacks := resp.Stacks + if len(stacks) < 1 { + return nil, &resource.NotFoundError{ + LastRequest: input, + LastResponse: resp, + Message: "returned no results", + } + } + + stack := stacks[0] + if aws.StringValue(stack.StackStatus) == cloudformation.StackStatusDeleteComplete { + return nil, &resource.NotFoundError{ + LastRequest: input, + LastResponse: resp, + Message: "CloudFormation Stack deleted", + } + } + + return stack, nil +} diff --git a/aws/internal/service/cloudformation/waiter/status.go b/aws/internal/service/cloudformation/waiter/status.go index 567882b3085b..cdfa0fd07699 100644 --- a/aws/internal/service/cloudformation/waiter/status.go +++ b/aws/internal/service/cloudformation/waiter/status.go @@ -2,6 +2,7 @@ package waiter import ( "fmt" + "log" "strings" "github.com/aws/aws-sdk-go/aws" @@ -10,6 +11,28 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func ChangeSetStatus(conn *cloudformation.CloudFormation, stackID, changeSetName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeChangeSet(&cloudformation.DescribeChangeSetInput{ + ChangeSetName: aws.String(changeSetName), + StackName: aws.String(stackID), + }) + if err != nil { + log.Printf("[ERROR] Failed to describe CloudFormation change set: %s", err) + return nil, "", err + } + + if resp == nil { + log.Printf("[WARN] Describing CloudFormation change set returned no response") + return nil, "", nil + } + + status := aws.StringValue(resp.Status) + + return resp, status, err + } +} + func StackSetOperationStatus(conn *cloudformation.CloudFormation, stackSetName, operationID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { input := &cloudformation.DescribeStackSetOperationInput{ diff --git a/aws/internal/service/cloudformation/waiter/waiter.go b/aws/internal/service/cloudformation/waiter/waiter.go index 63da018f35ce..39d0aacaf237 100644 --- a/aws/internal/service/cloudformation/waiter/waiter.go +++ b/aws/internal/service/cloudformation/waiter/waiter.go @@ -12,6 +12,35 @@ import ( "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/lister" ) +const ( + // Maximum amount of time to wait for a Change Set to be Created + ChangeSetCreatedTimeout = 5 * time.Minute +) + +func ChangeSetCreated(conn *cloudformation.CloudFormation, stackID, changeSetName string) (*cloudformation.DescribeChangeSetOutput, error) { + stateConf := resource.StateChangeConf{ + Pending: []string{ + cloudformation.ChangeSetStatusCreatePending, + cloudformation.ChangeSetStatusCreateInProgress, + }, + Target: []string{ + cloudformation.ChangeSetStatusCreateComplete, + }, + Timeout: ChangeSetCreatedTimeout, + Refresh: ChangeSetStatus(conn, stackID, changeSetName), + } + outputRaw, err := stateConf.WaitForState() + if err != nil { + return nil, err + } + + changeSet, ok := outputRaw.(*cloudformation.DescribeChangeSetOutput) + if !ok { + return nil, err + } + return changeSet, err +} + const ( // Default maximum amount of time to wait for a StackSetInstance to be Created StackSetInstanceCreatedDefaultTimeout = 30 * time.Minute diff --git a/aws/internal/service/serverlessapplicationrepository/finder/finder.go b/aws/internal/service/serverlessapplicationrepository/finder/finder.go new file mode 100644 index 000000000000..e99f9db910b0 --- /dev/null +++ b/aws/internal/service/serverlessapplicationrepository/finder/finder.go @@ -0,0 +1,43 @@ +package finder + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + serverlessrepository "github.com/aws/aws-sdk-go/service/serverlessapplicationrepository" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func Application(conn *serverlessrepository.ServerlessApplicationRepository, applicationID, version string) (*serverlessrepository.GetApplicationOutput, error) { + input := &serverlessrepository.GetApplicationInput{ + ApplicationId: aws.String(applicationID), + } + if version != "" { + input.SemanticVersion = aws.String(version) + } + + log.Printf("[DEBUG] Getting Serverless Application Repository Application: %s", input) + resp, err := conn.GetApplication(input) + if tfawserr.ErrCodeEquals(err, serverlessrepository.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + LastResponse: resp, + } + } + if err != nil { + return nil, err + } + + if resp == nil { + return nil, &resource.NotFoundError{ + LastRequest: input, + LastResponse: resp, + Message: "returned empty response", + } + } + + return resp, nil + +} diff --git a/aws/internal/service/serverlessapplicationrepository/waiter/waiter.go b/aws/internal/service/serverlessapplicationrepository/waiter/waiter.go new file mode 100644 index 000000000000..f6a82e79792c --- /dev/null +++ b/aws/internal/service/serverlessapplicationrepository/waiter/waiter.go @@ -0,0 +1,16 @@ +package waiter + +import ( + "time" +) + +const ( + // Default maximum amount of time to wait for a Stack to be Created + CloudFormationStackCreatedDefaultTimeout = 30 * time.Minute + + // Default maximum amount of time to wait for a Stack to be Updated + CloudFormationStackUpdatedDefaultTimeout = 30 * time.Minute + + // Default maximum amount of time to wait for a Stack to be Deleted + CloudFormationStackDeletedDefaultTimeout = 30 * time.Minute +) diff --git a/aws/internal/tfresource/errors.go b/aws/internal/tfresource/errors.go index 13bcee15b3e2..ae1d006ff846 100644 --- a/aws/internal/tfresource/errors.go +++ b/aws/internal/tfresource/errors.go @@ -1,14 +1,17 @@ package tfresource import ( + "errors" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) // NotFound returns true if the error represents a "resource not found" condition. -// Specifically, NotFound returns true if the error is of type resource.NotFoundError. +// Specifically, NotFound returns true if the error or a wrapped error is of type +// resource.NotFoundError. func NotFound(err error) bool { - _, ok := err.(*resource.NotFoundError) - return ok + var e *resource.NotFoundError + return errors.As(err, &e) } // TimedOut returns true if the error represents a "wait timed out" condition. diff --git a/aws/internal/tfresource/errors_test.go b/aws/internal/tfresource/errors_test.go index e04c486c1d8d..175ef5150cd9 100644 --- a/aws/internal/tfresource/errors_test.go +++ b/aws/internal/tfresource/errors_test.go @@ -32,8 +32,9 @@ func TestNotFound(t *testing.T) { Err: fmt.Errorf("test: %w", errors.New("test")), }, { - Name: "wrapped not found error", - Err: fmt.Errorf("test: %w", &resource.NotFoundError{LastError: errors.New("test")}), + Name: "wrapped not found error", + Err: fmt.Errorf("test: %w", &resource.NotFoundError{LastError: errors.New("test")}), + Expected: true, }, } diff --git a/aws/provider.go b/aws/provider.go index 1b8bd8c38961..8c53f2590761 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -1041,6 +1041,11 @@ func Provider() *schema.Provider { }, } + // Avoid Go formatting churn and Git conflicts + // You probably should not do this + provider.DataSourcesMap["aws_serverlessapplicationrepository_application"] = dataSourceAwsServerlessApplicationRepositoryApplication() + provider.ResourcesMap["aws_serverlessapplicationrepository_cloudformation_stack"] = resourceAwsServerlessApplicationRepositoryCloudFormationStack() + provider.ConfigureFunc = func(d *schema.ResourceData) (interface{}, error) { terraformVersion := provider.TerraformVersion if terraformVersion == "" { diff --git a/aws/provider_test.go b/aws/provider_test.go index 1f0429cc8308..4f9c6de40cbd 100644 --- a/aws/provider_test.go +++ b/aws/provider_test.go @@ -343,7 +343,7 @@ func testAccMatchResourceAttrRegionalARN(resourceName, attributeName, arnService attributeMatch, err := regexp.Compile(arnRegexp) if err != nil { - return fmt.Errorf("Unable to compile ARN regexp (%s): %s", arnRegexp, err) + return fmt.Errorf("Unable to compile ARN regexp (%s): %w", arnRegexp, err) } return resource.TestMatchResourceAttr(resourceName, attributeName, attributeMatch)(s) @@ -384,7 +384,7 @@ func testAccMatchResourceAttrRegionalARNAccountID(resourceName, attributeName, a attributeMatch, err := regexp.Compile(arnRegexp) if err != nil { - return fmt.Errorf("Unable to compile ARN regexp (%s): %s", arnRegexp, err) + return fmt.Errorf("Unable to compile ARN regexp (%s): %w", arnRegexp, err) } return resource.TestMatchResourceAttr(resourceName, attributeName, attributeMatch)(s) @@ -399,7 +399,7 @@ func testAccMatchResourceAttrRegionalHostname(resourceName, attributeName, servi hostnameRegexp, err := regexp.Compile(hostnameRegexpPattern) if err != nil { - return fmt.Errorf("Unable to compile hostname regexp (%s): %s", hostnameRegexp, err) + return fmt.Errorf("Unable to compile hostname regexp (%s): %w", hostnameRegexp, err) } return resource.TestMatchResourceAttr(resourceName, attributeName, hostnameRegexp)(s) @@ -457,7 +457,28 @@ func testAccMatchResourceAttrGlobalARN(resourceName, attributeName, arnService s attributeMatch, err := regexp.Compile(arnRegexp) if err != nil { - return fmt.Errorf("Unable to compile ARN regexp (%s): %s", arnRegexp, err) + return fmt.Errorf("Unable to compile ARN regexp (%s): %w", arnRegexp, err) + } + + return resource.TestMatchResourceAttr(resourceName, attributeName, attributeMatch)(s) + } +} + +// testAccCheckResourceAttrRegionalARNIgnoreRegionAndAccount ensures the Terraform state exactly matches a formatted ARN with region without specifying the region or account +func testAccCheckResourceAttrRegionalARNIgnoreRegionAndAccount(resourceName, attributeName, arnService, arnResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + arnRegexp := arn.ARN{ + AccountID: awsAccountIDRegexpInternalPattern, + Partition: testAccGetPartition(), + Region: awsRegionRegexpInternalPattern, + Resource: arnResource, + Service: arnService, + }.String() + + attributeMatch, err := regexp.Compile(arnRegexp) + + if err != nil { + return fmt.Errorf("Unable to compile ARN regexp (%s): %w", arnRegexp, err) } return resource.TestMatchResourceAttr(resourceName, attributeName, attributeMatch)(s) diff --git a/aws/resource_aws_cloudformation_stack_test.go b/aws/resource_aws_cloudformation_stack_test.go index 0c34a757bd06..d94ae9406052 100644 --- a/aws/resource_aws_cloudformation_stack_test.go +++ b/aws/resource_aws_cloudformation_stack_test.go @@ -518,6 +518,16 @@ func testAccCheckAWSCloudFormationDestroy(s *terraform.State) error { return nil } +func testAccCheckCloudFormationStackNotRecreated(i, j *cloudformation.Stack) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.StringValue(i.StackId) != aws.StringValue(j.StackId) { + return fmt.Errorf("CloudFormation stack recreated") + } + + return nil + } +} + func testAccCheckCloudFormationStackDisappears(stack *cloudformation.Stack) resource.TestCheckFunc { return func(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).cfconn diff --git a/aws/resource_aws_serverlessapplicationrepository_cloudformation_stack.go b/aws/resource_aws_serverlessapplicationrepository_cloudformation_stack.go new file mode 100644 index 000000000000..998db6a1fa9a --- /dev/null +++ b/aws/resource_aws_serverlessapplicationrepository_cloudformation_stack.go @@ -0,0 +1,336 @@ +package aws + +import ( + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/cloudformation" + serverlessrepository "github.com/aws/aws-sdk-go/service/serverlessapplicationrepository" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + cffinder "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/finder" + cfwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/cloudformation/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/serverlessapplicationrepository/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/serverlessapplicationrepository/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +const ( + serverlessApplicationRepositoryCloudFormationStackNamePrefix = "serverlessrepo-" + + serverlessApplicationRepositoryCloudFormationStackTagApplicationID = "serverlessrepo:applicationId" + serverlessApplicationRepositoryCloudFormationStackTagSemanticVersion = "serverlessrepo:semanticVersion" +) + +func resourceAwsServerlessApplicationRepositoryCloudFormationStack() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsServerlessApplicationRepositoryCloudFormationStackCreate, + Read: resourceAwsServerlessApplicationRepositoryCloudFormationStackRead, + Update: resourceAwsServerlessApplicationRepositoryCloudFormationStackUpdate, + Delete: resourceAwsServerlessApplicationRepositoryCloudFormationStackDelete, + + Importer: &schema.ResourceImporter{ + State: resourceAwsServerlessApplicationRepositoryCloudFormationStackImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(waiter.CloudFormationStackCreatedDefaultTimeout), + Update: schema.DefaultTimeout(waiter.CloudFormationStackUpdatedDefaultTimeout), + Delete: schema.DefaultTimeout(waiter.CloudFormationStackDeletedDefaultTimeout), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "application_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "capabilities": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(serverlessrepository.Capability_Values(), false), + }, + Set: schema.HashString, + }, + "parameters": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "semantic_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "outputs": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "tags": tagsSchema(), + }, + } +} + +func resourceAwsServerlessApplicationRepositoryCloudFormationStackCreate(d *schema.ResourceData, meta interface{}) error { + cfConn := meta.(*AWSClient).cfconn + + changeSet, err := createServerlessApplicationRepositoryCloudFormationChangeSet(d, meta.(*AWSClient)) + if err != nil { + return fmt.Errorf("error creating Serverless Application Repository CloudFormation change set: %w", err) + } + + log.Printf("[INFO] Serverless Application Repository CloudFormation Stack (%s) change set created", d.Id()) + + d.SetId(aws.StringValue(changeSet.StackId)) + + requestToken := resource.UniqueId() + executeRequest := cloudformation.ExecuteChangeSetInput{ + ChangeSetName: changeSet.ChangeSetId, + ClientRequestToken: aws.String(requestToken), + } + log.Printf("[DEBUG] Executing Serverless Application Repository CloudFormation change set: %s", executeRequest) + _, err = cfConn.ExecuteChangeSet(&executeRequest) + if err != nil { + return fmt.Errorf("executing Serverless Application Repository CloudFormation Stack (%s) change set failed: %w", d.Id(), err) + } + + _, err = cfwaiter.StackCreated(cfConn, d.Id(), requestToken, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("error waiting for Serverless Application Repository CloudFormation Stack (%s) creation: %w", d.Id(), err) + } + + log.Printf("[INFO] Serverless Application Repository CloudFormation Stack (%s) created", d.Id()) + + return resourceAwsServerlessApplicationRepositoryCloudFormationStackRead(d, meta) +} + +func resourceAwsServerlessApplicationRepositoryCloudFormationStackRead(d *schema.ResourceData, meta interface{}) error { + serverlessConn := meta.(*AWSClient).serverlessapplicationrepositoryconn + cfConn := meta.(*AWSClient).cfconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + stack, err := cffinder.Stack(cfConn, d.Id()) + if tfresource.NotFound(err) { + log.Printf("[WARN] Serverless Application Repository CloudFormation Stack (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("error describing Serverless Application Repository CloudFormation Stack (%s): %w", d.Id(), err) + } + + // Serverless Application Repo prefixes the stack name with "serverlessrepo-", so remove it from the saved string + stackName := strings.TrimPrefix(aws.StringValue(stack.StackName), serverlessApplicationRepositoryCloudFormationStackNamePrefix) + d.Set("name", &stackName) + + tags := keyvaluetags.CloudformationKeyValueTags(stack.Tags) + var applicationID, semanticVersion string + if v, ok := tags[serverlessApplicationRepositoryCloudFormationStackTagApplicationID]; ok { + applicationID = aws.StringValue(v.Value) + d.Set("application_id", applicationID) + } else { + return fmt.Errorf("error describing Serverless Application Repository CloudFormation Stack (%s): missing required tag \"%s\"", d.Id(), serverlessApplicationRepositoryCloudFormationStackTagApplicationID) + } + if v, ok := tags[serverlessApplicationRepositoryCloudFormationStackTagSemanticVersion]; ok { + semanticVersion = aws.StringValue(v.Value) + d.Set("semantic_version", semanticVersion) + } else { + return fmt.Errorf("error describing Serverless Application Repository CloudFormation Stack (%s): missing required tag \"%s\"", d.Id(), serverlessApplicationRepositoryCloudFormationStackTagSemanticVersion) + } + + if err = d.Set("tags", tags.IgnoreServerlessApplicationRepository().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("failed to set tags: %w", err) + } + + if err = d.Set("outputs", flattenCloudFormationOutputs(stack.Outputs)); err != nil { + return fmt.Errorf("failed to set outputs: %w", err) + } + + getApplicationOutput, err := finder.Application(serverlessConn, applicationID, semanticVersion) + if err != nil { + return fmt.Errorf("error getting Serverless Application Repository application (%s, v%s): %w", applicationID, semanticVersion, err) + } + + if getApplicationOutput == nil || getApplicationOutput.Version == nil { + return fmt.Errorf("error getting Serverless Application Repository application (%s, v%s): empty response", applicationID, semanticVersion) + } + + version := getApplicationOutput.Version + + if err = d.Set("parameters", flattenNonDefaultServerlessApplicationCloudFormationParameters(stack.Parameters, version.ParameterDefinitions)); err != nil { + return fmt.Errorf("failed to set parameters: %w", err) + } + + if err = d.Set("capabilities", flattenServerlessRepositoryStackCapabilities(stack.Capabilities, version.RequiredCapabilities)); err != nil { + return fmt.Errorf("failed to set capabilities: %w", err) + } + + return nil +} + +func flattenNonDefaultServerlessApplicationCloudFormationParameters(cfParams []*cloudformation.Parameter, rawParameterDefinitions []*serverlessrepository.ParameterDefinition) map[string]interface{} { + parameterDefinitions := flattenServerlessRepositoryParameterDefinitions(rawParameterDefinitions) + params := make(map[string]interface{}, len(cfParams)) + for _, p := range cfParams { + key := aws.StringValue(p.ParameterKey) + value := aws.StringValue(p.ParameterValue) + if value != aws.StringValue(parameterDefinitions[key].DefaultValue) { + params[key] = value + } + } + return params +} + +func flattenServerlessRepositoryParameterDefinitions(parameterDefinitions []*serverlessrepository.ParameterDefinition) map[string]*serverlessrepository.ParameterDefinition { + result := make(map[string]*serverlessrepository.ParameterDefinition, len(parameterDefinitions)) + for _, p := range parameterDefinitions { + result[aws.StringValue(p.Name)] = p + } + return result +} + +func resourceAwsServerlessApplicationRepositoryCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error { + cfConn := meta.(*AWSClient).cfconn + + changeSet, err := createServerlessApplicationRepositoryCloudFormationChangeSet(d, meta.(*AWSClient)) + if err != nil { + return fmt.Errorf("error creating Serverless Application Repository CloudFormation Stack (%s) change set: %w", d.Id(), err) + } + + log.Printf("[INFO] Serverless Application Repository CloudFormation Stack (%s) change set created", d.Id()) + + requestToken := resource.UniqueId() + executeRequest := cloudformation.ExecuteChangeSetInput{ + ChangeSetName: changeSet.ChangeSetId, + ClientRequestToken: aws.String(requestToken), + } + log.Printf("[DEBUG] Executing Serverless Application Repository CloudFormation change set: %s", executeRequest) + _, err = cfConn.ExecuteChangeSet(&executeRequest) + if err != nil { + return fmt.Errorf("executing Serverless Application Repository CloudFormation change set failed: %w", err) + } + + _, err = cfwaiter.StackUpdated(cfConn, d.Id(), requestToken, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return fmt.Errorf("error waiting for Serverless Application Repository CloudFormation Stack (%s) update: %w", d.Id(), err) + } + + log.Printf("[INFO] Serverless Application Repository CloudFormation Stack (%s) updated", d.Id()) + + return resourceAwsServerlessApplicationRepositoryCloudFormationStackRead(d, meta) +} + +func resourceAwsServerlessApplicationRepositoryCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cfconn + + requestToken := resource.UniqueId() + input := &cloudformation.DeleteStackInput{ + StackName: aws.String(d.Id()), + ClientRequestToken: aws.String(requestToken), + } + log.Printf("[DEBUG] Deleting Serverless Application Repository CloudFormation stack %s", input) + _, err := conn.DeleteStack(input) + if tfawserr.ErrCodeEquals(err, "ValidationError") { + return nil + } + if err != nil { + return err + } + + _, err = cfwaiter.StackDeleted(conn, d.Id(), requestToken, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return fmt.Errorf("error waiting for Serverless Application Repository CloudFormation Stack deletion: %w", err) + } + + log.Printf("[INFO] Serverless Application Repository CloudFormation stack (%s) deleted", d.Id()) + + return nil +} + +func resourceAwsServerlessApplicationRepositoryCloudFormationStackImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + stackID := d.Id() + + // If this isn't an ARN, it's the stack name + if _, err := arn.Parse(stackID); err != nil { + if !strings.HasPrefix(stackID, serverlessApplicationRepositoryCloudFormationStackNamePrefix) { + stackID = serverlessApplicationRepositoryCloudFormationStackNamePrefix + stackID + } + } + + cfConn := meta.(*AWSClient).cfconn + stack, err := cffinder.Stack(cfConn, stackID) + if err != nil { + return nil, fmt.Errorf("error describing Serverless Application Repository CloudFormation Stack (%s): %w", stackID, err) + } + + d.SetId(aws.StringValue(stack.StackId)) + + return []*schema.ResourceData{d}, nil +} + +func createServerlessApplicationRepositoryCloudFormationChangeSet(d *schema.ResourceData, client *AWSClient) (*cloudformation.DescribeChangeSetOutput, error) { + serverlessConn := client.serverlessapplicationrepositoryconn + cfConn := client.cfconn + + stackName := d.Get("name").(string) + changeSetRequest := serverlessrepository.CreateCloudFormationChangeSetRequest{ + StackName: aws.String(stackName), + ApplicationId: aws.String(d.Get("application_id").(string)), + Capabilities: expandStringSet(d.Get("capabilities").(*schema.Set)), + Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreServerlessApplicationRepository().ServerlessapplicationrepositoryTags(), + } + if v, ok := d.GetOk("semantic_version"); ok { + changeSetRequest.SemanticVersion = aws.String(v.(string)) + } + if v, ok := d.GetOk("parameters"); ok { + changeSetRequest.ParameterOverrides = expandServerlessRepositoryCloudFormationChangeSetParameters(v.(map[string]interface{})) + } + + log.Printf("[DEBUG] Creating Serverless Application Repository CloudFormation change set: %s", changeSetRequest) + changeSetResponse, err := serverlessConn.CreateCloudFormationChangeSet(&changeSetRequest) + if err != nil { + return nil, err + } + + return cfwaiter.ChangeSetCreated(cfConn, aws.StringValue(changeSetResponse.StackId), aws.StringValue(changeSetResponse.ChangeSetId)) +} + +func expandServerlessRepositoryCloudFormationChangeSetParameters(params map[string]interface{}) []*serverlessrepository.ParameterValue { + var appParams []*serverlessrepository.ParameterValue + for k, v := range params { + appParams = append(appParams, &serverlessrepository.ParameterValue{ + Name: aws.String(k), + Value: aws.String(v.(string)), + }) + } + return appParams +} + +func flattenServerlessRepositoryStackCapabilities(stackCapabilities []*string, applicationRequiredCapabilities []*string) *schema.Set { + // We need to preserve "CAPABILITY_RESOURCE_POLICY" if it has been set. It is not + // returned by the CloudFormation APIs. + capabilities := flattenStringSet(stackCapabilities) + for _, capability := range applicationRequiredCapabilities { + if aws.StringValue(capability) == serverlessrepository.CapabilityCapabilityResourcePolicy { + capabilities.Add(serverlessrepository.CapabilityCapabilityResourcePolicy) + break + } + } + return capabilities +} diff --git a/aws/resource_aws_serverlessapplicationrepository_cloudformation_stack_test.go b/aws/resource_aws_serverlessapplicationrepository_cloudformation_stack_test.go new file mode 100644 index 000000000000..db48a36a51cc --- /dev/null +++ b/aws/resource_aws_serverlessapplicationrepository_cloudformation_stack_test.go @@ -0,0 +1,504 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfawsresource" +) + +// Since aws_serverlessapplicationrepository_cloudformation_stack creates CloudFormation stacks, +// the aws_cloudformation_stack sweeper will clean these up as well. + +func TestAccAwsServerlessApplicationRepositoryCloudFormationStack_basic(t *testing.T) { + var stack cloudformation.Stack + stackName := acctest.RandomWithPrefix("tf-acc-test") + + resourceName := "aws_serverlessapplicationrepository_cloudformation_stack.postgres-rotator" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudFormationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsServerlessApplicationRepositoryCloudFormationStackConfig(stackName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack), + resource.TestCheckResourceAttr(resourceName, "name", stackName), + testAccCheckResourceAttrRegionalARNIgnoreRegionAndAccount(resourceName, "application_id", "serverlessrepo", "applications/SecretsManagerRDSPostgreSQLRotationSingleUser"), + resource.TestCheckResourceAttrSet(resourceName, "semantic_version"), + resource.TestCheckResourceAttr(resourceName, "parameters.%", "2"), + resource.TestCheckResourceAttr(resourceName, "parameters.functionName", fmt.Sprintf("func-%s", stackName)), + testAccCheckResourceAttrRegionalHostnameService(resourceName, "parameters.endpoint", "secretsmanager"), + resource.TestCheckResourceAttr(resourceName, "outputs.%", "1"), + resource.TestCheckResourceAttrSet(resourceName, "outputs.RotationLambdaARN"), + resource.TestCheckResourceAttr(resourceName, "capabilities.#", "2"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "capabilities.*", "CAPABILITY_IAM"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "capabilities.*", "CAPABILITY_RESOURCE_POLICY"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: resourceName, + ImportStateIdFunc: testAccAwsServerlessApplicationRepositoryCloudFormationStackNameImportStateIdFunc(resourceName), + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: resourceName, + ImportStateIdFunc: testAccAwsServerlessApplicationRepositoryCloudFormationStackNameNoPrefixImportStateIdFunc(resourceName), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsServerlessApplicationRepositoryCloudFormationStack_disappears(t *testing.T) { + var stack cloudformation.Stack + stackName := acctest.RandomWithPrefix("tf-acc-test") + + resourceName := "aws_serverlessapplicationrepository_cloudformation_stack.postgres-rotator" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAmiDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsServerlessApplicationRepositoryCloudFormationStackConfig(stackName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack), + testAccCheckResourceDisappears(testAccProvider, resourceAwsServerlessApplicationRepositoryCloudFormationStack(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsServerlessApplicationRepositoryCloudFormationStack_versioned(t *testing.T) { + var stack1, stack2, stack3 cloudformation.Stack + stackName := acctest.RandomWithPrefix("tf-acc-test") + + const ( + version1 = "1.0.13" + version2 = "1.1.36" + ) + + resourceName := "aws_serverlessapplicationrepository_cloudformation_stack.postgres-rotator" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudFormationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_versioned(stackName, version1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack1), + resource.TestCheckResourceAttr(resourceName, "semantic_version", version1), + resource.TestCheckResourceAttr(resourceName, "capabilities.#", "1"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "capabilities.*", "CAPABILITY_IAM"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_versioned2(stackName, version2), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack2), + testAccCheckCloudFormationStackNotRecreated(&stack1, &stack2), + resource.TestCheckResourceAttr(resourceName, "semantic_version", version2), + resource.TestCheckResourceAttr(resourceName, "capabilities.#", "2"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "capabilities.*", "CAPABILITY_IAM"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "capabilities.*", "CAPABILITY_RESOURCE_POLICY"), + ), + }, + { + // Confirm removal of "CAPABILITY_RESOURCE_POLICY" is handled properly + Config: testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_versioned(stackName, version1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack3), + testAccCheckCloudFormationStackNotRecreated(&stack2, &stack3), + resource.TestCheckResourceAttr(resourceName, "semantic_version", version1), + resource.TestCheckResourceAttr(resourceName, "capabilities.#", "1"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "capabilities.*", "CAPABILITY_IAM"), + ), + }, + }, + }) +} + +func TestAccAwsServerlessApplicationRepositoryCloudFormationStack_paired(t *testing.T) { + var stack cloudformation.Stack + stackName := acctest.RandomWithPrefix("tf-acc-test") + + const version = "1.1.36" + + resourceName := "aws_serverlessapplicationrepository_cloudformation_stack.postgres-rotator" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudFormationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_versionedPaired(stackName, version), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack), + resource.TestCheckResourceAttr(resourceName, "semantic_version", version), + resource.TestCheckResourceAttr(resourceName, "capabilities.#", "2"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "capabilities.*", "CAPABILITY_IAM"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "capabilities.*", "CAPABILITY_RESOURCE_POLICY"), + ), + }, + }, + }) +} + +func TestAccAwsServerlessApplicationRepositoryCloudFormationStack_Tags(t *testing.T) { + var stack cloudformation.Stack + stackName := acctest.RandomWithPrefix("tf-acc-test") + + resourceName := "aws_serverlessapplicationrepository_cloudformation_stack.postgres-rotator" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudFormationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsServerlessApplicationRepositoryCloudFormationStackConfigTags1(stackName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsServerlessApplicationRepositoryCloudFormationStackConfigTags2(stackName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAwsServerlessApplicationRepositoryCloudFormationStackConfigTags1(stackName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccAwsServerlessApplicationRepositoryCloudFormationStack_update(t *testing.T) { + var stack cloudformation.Stack + stackName := acctest.RandomWithPrefix("tf-acc-test") + initialName := acctest.RandomWithPrefix("FuncName1") + updatedName := acctest.RandomWithPrefix("FuncName2") + + resourceName := "aws_serverlessapplicationrepository_cloudformation_stack.postgres-rotator" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudFormationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_updateInitial(stackName, initialName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack), + testAccCheckResourceAttrRegionalARNIgnoreRegionAndAccount(resourceName, "application_id", "serverlessrepo", "applications/SecretsManagerRDSPostgreSQLRotationSingleUser"), + resource.TestCheckResourceAttr(resourceName, "parameters.functionName", initialName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key", "value"), + ), + }, + { + Config: testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_updateUpdated(stackName, updatedName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(resourceName, &stack), + resource.TestCheckResourceAttr(resourceName, "parameters.functionName", updatedName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key", "value"), + ), + }, + }, + }) +} + +func testAccCheckServerlessApplicationRepositoryCloudFormationStackExists(n string, stack *cloudformation.Stack) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cfconn + params := &cloudformation.DescribeStacksInput{ + StackName: aws.String(rs.Primary.ID), + } + resp, err := conn.DescribeStacks(params) + if err != nil { + return err + } + if len(resp.Stacks) == 0 { + return fmt.Errorf("CloudFormation stack (%s) not found", rs.Primary.ID) + } + + *stack = *resp.Stacks[0] + + return nil + } +} + +func testAccAwsServerlessApplicationRepositoryCloudFormationStackNameImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s%s", serverlessApplicationRepositoryCloudFormationStackNamePrefix, rs.Primary.Attributes["name"]), nil + } +} + +func testAccAwsServerlessApplicationRepositoryCloudFormationStackNameNoPrefixImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return rs.Primary.Attributes["name"], nil + } +} + +func testAccAwsServerlessApplicationRepositoryCloudFormationStackConfig(stackName string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "%[1]s" + application_id = local.postgres_single_user_rotator_arn + capabilities = [ + "CAPABILITY_IAM", + "CAPABILITY_RESOURCE_POLICY", + ] + parameters = { + functionName = "func-%[1]s" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } +} + +data "aws_region" "current" {} +`, stackName)) +} + +func testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_updateInitial(stackName, functionName string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "%[1]s" + application_id = local.postgres_single_user_rotator_arn + capabilities = [ + "CAPABILITY_IAM", + "CAPABILITY_RESOURCE_POLICY", + ] + parameters = { + functionName = "%[2]s" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } + tags = { + key = "value" + } +} + +data "aws_region" "current" {} +`, stackName, functionName)) +} + +func testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_updateUpdated(stackName, functionName string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "%[1]s" + application_id = local.postgres_single_user_rotator_arn + capabilities = [ + "CAPABILITY_IAM", + "CAPABILITY_RESOURCE_POLICY", + ] + parameters = { + functionName = "%[2]s" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } + tags = { + key = "value" + } +} + +data "aws_region" "current" {} +`, stackName, functionName)) +} + +func testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_versioned(stackName, version string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "%[1]s" + application_id = local.postgres_single_user_rotator_arn + semantic_version = "%[2]s" + capabilities = [ + "CAPABILITY_IAM", + ] + parameters = { + functionName = "func-%[1]s" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } +} + +data "aws_region" "current" {} +`, stackName, version)) +} + +func testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_versioned2(stackName, version string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "%[1]s" + application_id = local.postgres_single_user_rotator_arn + capabilities = [ + "CAPABILITY_IAM", + "CAPABILITY_RESOURCE_POLICY", + ] + semantic_version = "%[2]s" + parameters = { + functionName = "func-%[1]s" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } +} + +data "aws_region" "current" {} +`, stackName, version)) +} + +func testAccAWSServerlessApplicationRepositoryCloudFormationStackConfig_versionedPaired(stackName, version string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "%[1]s" + application_id = data.aws_serverlessapplicationrepository_application.secrets_manager_postgres_single_user_rotator.application_id + semantic_version = data.aws_serverlessapplicationrepository_application.secrets_manager_postgres_single_user_rotator.semantic_version + capabilities = data.aws_serverlessapplicationrepository_application.secrets_manager_postgres_single_user_rotator.required_capabilities + parameters = { + functionName = "func-%[1]s" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } +} + +data "aws_serverlessapplicationrepository_application" "secrets_manager_postgres_single_user_rotator" { + application_id = local.postgres_single_user_rotator_arn + semantic_version = "%[2]s" +} + +data "aws_region" "current" {} +`, stackName, version)) +} + +func testAccAwsServerlessApplicationRepositoryCloudFormationStackConfigTags1(rName, tagKey1, tagValue1 string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "%[1]s" + application_id = local.postgres_single_user_rotator_arn + capabilities = [ + "CAPABILITY_IAM", + "CAPABILITY_RESOURCE_POLICY", + ] + parameters = { + functionName = "func-%[1]s" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } + tags = { + %[2]q = %[3]q + } +} + +data "aws_region" "current" {} +`, rName, tagKey1, tagValue1)) +} + +func testAccAwsServerlessApplicationRepositoryCloudFormationStackConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return composeConfig( + testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication, + fmt.Sprintf(` +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "%[1]s" + application_id = local.postgres_single_user_rotator_arn + capabilities = [ + "CAPABILITY_IAM", + "CAPABILITY_RESOURCE_POLICY", + ] + parameters = { + functionName = "func-%[1]s" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} + +data "aws_region" "current" {} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} + +const testAccCheckAwsServerlessApplicationRepositoryPostgresSingleUserRotatorApplication = ` +data "aws_partition" "current" {} + +locals { + postgres_single_user_rotator_arn = "arn:${data.aws_partition.current.partition}:serverlessrepo:${local.application_region}:${local.application_account}:applications/SecretsManagerRDSPostgreSQLRotationSingleUser" + + application_region = local.security_manager_regions[data.aws_partition.current.partition] + application_account = local.security_manager_accounts[data.aws_partition.current.partition] + + security_manager_regions = { + "aws" = "us-east-1", + "aws-us-gov" = "us-gov-west-1", + } + security_manager_accounts = { + "aws" = "297356227824", + "aws-us-gov" = "023102451235", + } +} +` diff --git a/aws/validators.go b/aws/validators.go index feb1038a95e3..526eb613492d 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -23,9 +23,15 @@ import ( ) const ( - awsAccountIDRegexpPattern = `^(aws|\d{12})$` - awsPartitionRegexpPattern = `^aws(-[a-z]+)*$` - awsRegionRegexpPattern = `^[a-z]{2}(-[a-z]+)+-\d$` + awsAccountIDRegexpInternalPattern = `(aws|\d{12})` + awsPartitionRegexpInternalPattern = `aws(-[a-z]+)*` + awsRegionRegexpInternalPattern = `[a-z]{2}(-[a-z]+)+-\d` +) + +const ( + awsAccountIDRegexpPattern = "^" + awsAccountIDRegexpInternalPattern + "$" + awsPartitionRegexpPattern = "^" + awsPartitionRegexpInternalPattern + "$" + awsRegionRegexpPattern = "^" + awsRegionRegexpInternalPattern + "$" ) var awsAccountIDRegexp = regexp.MustCompile(awsAccountIDRegexpPattern) diff --git a/docs/roadmaps/2020_August_to_October.md b/docs/roadmaps/2020_August_to_October.md index 37ba4933c57e..6ea8ac4413f9 100644 --- a/docs/roadmaps/2020_August_to_October.md +++ b/docs/roadmaps/2020_August_to_October.md @@ -67,11 +67,11 @@ Support for AWS Serverless Application Repository will include: New Resource(s): -- aws_serverlessrepository_stack +- aws_serverlessapplicationrepository_cloudformation_stack New Data Source(s): -- aws_serverlessrepository_application +- aws_serverlessapplicationrepository_application ## Issues and Enhancements diff --git a/website/allowed-subcategories.txt b/website/allowed-subcategories.txt index b0890e064840..ee701efc53ea 100644 --- a/website/allowed-subcategories.txt +++ b/website/allowed-subcategories.txt @@ -110,6 +110,7 @@ SWF Sagemaker Secrets Manager Security Hub +Serverless Application Repository Service Catalog Service Discovery Service Quotas diff --git a/website/docs/d/serverlessapplicationrepository_application.html.markdown b/website/docs/d/serverlessapplicationrepository_application.html.markdown new file mode 100644 index 000000000000..939c006e7813 --- /dev/null +++ b/website/docs/d/serverlessapplicationrepository_application.html.markdown @@ -0,0 +1,39 @@ +--- +subcategory: "Serverless Application Repository" +layout: "aws" +page_title: "AWS: aws_serverlessapplicationrepository_application" +description: |- + Get information on a AWS Serverless Application Repository application +--- + +# Data Source: aws_serverlessapplicationrepository_application + +Use this data source to get information about an AWS Serverless Application Repository application. For example, this can be used to determine the required `capabilities` for an application. + +## Example Usage + +```hcl +data "aws_serverlessapplicationrepository_application" "example" { + application_id = "arn:aws:serverlessrepo:us-east-1:123456789012:applications/ExampleApplication" +} + +resource "aws_serverlessapplicationrepository_cloudformation_stack" "example" { + name = "Example" + application_id = data.aws_serverlessapplicationrepository_application.example.application_id + semantic_version = data.aws_serverlessapplicationrepository_application.example.semantic_version + capabilities = data.aws_serverlessapplicationrepository_application.example.required_capabilities +} +``` + +## Argument Reference + +* `application_id` - (Required) The ARN of the application. +* `semantic_version` - (Optional) The requested version of the application. By default, retrieves the latest version. + +## Attributes Reference + +* `application_id` - The ARN of the application. +* `name` - The name of the application. +* `required_capabilities` - A list of capabilities describing the permissions needed to deploy the application. +* `source_code_url` - A URL pointing to the source code of the application version. +* `template_url` - A URL pointing to the Cloud Formation template for the application version. diff --git a/website/docs/r/serverlessapplicationrepository_cloudformation_stack.html.markdown b/website/docs/r/serverlessapplicationrepository_cloudformation_stack.html.markdown new file mode 100644 index 000000000000..ea5a0c644a3e --- /dev/null +++ b/website/docs/r/serverlessapplicationrepository_cloudformation_stack.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "Serverless Application Repository" +layout: "aws" +page_title: "AWS: aws_serverlessapplicationrepository_cloudformation_stack" +description: |- + Deploys an Application CloudFormation Stack from the Serverless Application Repository. +--- + +# Resource: aws_serverlessapplicationrepository_cloudformation_stack + +Deploys an Application CloudFormation Stack from the Serverless Application Repository. + +## Example Usage + +```hcl +resource "aws_serverlessapplicationrepository_cloudformation_stack" "postgres-rotator" { + name = "postgres-rotator" + application_id = "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSPostgreSQLRotationSingleUser" + capabilities = [ + "CAPABILITY_IAM", + "CAPABILITY_RESOURCE_POLICY", + ] + parameters = { + functionName = "func-postgres-rotator" + endpoint = "secretsmanager.${data.aws_region.current.name}.${data.aws_partition.current.dns_suffix}" + } +} + +data "aws_partition" "current" {} +data "aws_region" "current" {} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the stack to create. The resource deployed in AWS will be prefixed with `serverlessrepo-` +* `application_id` - (Required) The ARN of the application from the Serverless Application Repository. +* `capabilities` - (Required) A list of capabilities. Valid values are `CAPABILITY_IAM`, `CAPABILITY_NAMED_IAM`, `CAPABILITY_RESOURCE_POLICY`, or `CAPABILITY_AUTO_EXPAND` +* `parameters` - (Optional) A map of Parameter structures that specify input parameters for the stack. +* `semantic_version` - (Optional) The version of the application to deploy. If not supplied, deploys the latest version. +* `tags` - (Optional) A list of tags to associate with this stack. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - A unique identifier of the stack. +* `outputs` - A map of outputs from the stack. + +## Import + +Serverless Application Repository Stack can be imported using the CloudFormation Stack name (with or without the `serverlessrepo-` prefix) or the CloudFormation Stack ID, e.g. + +``` +$ terraform import aws_serverlessapplicationrepository_cloudformation_stack.example serverlessrepo-postgres-rotator +```