Skip to content

Commit

Permalink
Merge pull request #15874 from hashicorp/serverless_app_repo
Browse files Browse the repository at this point in the history
Serverless Application Repository initial support
  • Loading branch information
gdavison authored Nov 25, 2020
2 parents 50133d8 + 19b88f7 commit 18279c6
Show file tree
Hide file tree
Showing 21 changed files with 1,389 additions and 18 deletions.
1 change: 1 addition & 0 deletions .semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
72 changes: 72 additions & 0 deletions aws/data_source_aws_serverlessapplicationrepository_application.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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"
}
`
28 changes: 24 additions & 4 deletions aws/internal/keyvaluetags/key_value_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
56 changes: 56 additions & 0 deletions aws/internal/service/cloudformation/finder/finder.go
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions aws/internal/service/cloudformation/waiter/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package waiter

import (
"fmt"
"log"
"strings"

"github.com/aws/aws-sdk-go/aws"
Expand All @@ -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{
Expand Down
29 changes: 29 additions & 0 deletions aws/internal/service/cloudformation/waiter/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 18279c6

Please sign in to comment.