Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serverless Application Repository initial support #15874

Merged
merged 46 commits into from
Nov 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
fab4ce2
Created happy path latest version Serverless Repo data source
Aug 1, 2018
9d762eb
Handle non-existent applications in the repo
Aug 1, 2018
294e1e8
Added version parameter for Serverless Application Repo application d…
Aug 1, 2018
b2cd9c3
Added documentation
Aug 1, 2018
649d557
Added basic creation, reading, and deletion of a Serveless Repo Appli…
Sep 7, 2018
13eedc6
Added version parameter as output and optional input
Sep 22, 2018
69b0823
Added test for changing application version
Oct 17, 2018
d6a90ec
Added parameter tests for basic case
Oct 23, 2018
48a747a
Formatting changes
Oct 23, 2018
14c40a2
Added outputs to serverless repo application
Oct 23, 2018
8ad2111
Added CloudFormation `capabilities` field as output; needed for update
Nov 11, 2018
41c3a7f
Factored waiting for creation of CloudFormation change set to separat…
Nov 13, 2018
8b6e857
Moved status check into function as well
Nov 13, 2018
6f28051
Factored waiting for execution of CloudFormation change set to separa…
Nov 13, 2018
0b42e3e
Revert "Factored waiting for execution of CloudFormation change set t…
Nov 14, 2018
8f715b7
Added Update to serverless application
Nov 14, 2018
753fa55
Added tags to Read operation
Nov 21, 2018
76a3de2
Add a test for reading tags
Sep 5, 2019
8344eb4
Fixes linting error
Sep 9, 2019
cc1f10e
Creates and updates tags
Sep 10, 2019
3c5419f
Updates AWS region used to the default `us-west-2`
Sep 10, 2019
70fd994
Renames the resource to `aws_serverlessrepository_stack`
Sep 10, 2019
69701fa
Adds documentation for `aws_serverlessrepository_stack`
Sep 10, 2019
062f836
Renames tests for `aws_serverlessrepository_stack`
Sep 10, 2019
507fbfb
Fixes typo in function name
Sep 10, 2019
18132b1
Merge branch 'master' into serverless_app_repo
gdavison Oct 26, 2020
0a5feb8
Fixes documentation errors
gdavison Oct 27, 2020
ef3f4a7
Adds `required_capabilities` to Serverless Application Repository app…
gdavison Oct 30, 2020
ae96e4f
Adds support for CAPABILITY_RESOURCE_POLICY capability
gdavison Nov 2, 2020
dc58f23
Adds disappears test
gdavison Nov 12, 2020
6d59d77
Create timeout constants
gdavison Nov 12, 2020
110c46f
Test stack name
gdavison Nov 12, 2020
f84d85b
Updates documentation
gdavison Nov 13, 2020
a7679dc
Tweaks
gdavison Nov 13, 2020
c5c4b99
Merge branch 'master' into serverless_app_repo
gdavison Nov 20, 2020
1ad9806
Adds exclusion to multiple AWS service rule
gdavison Nov 20, 2020
56a5fd4
Adds import functionality
gdavison Nov 24, 2020
f3aabe1
Renaming to match package name
gdavison Nov 24, 2020
66bccf1
Merge branch 'master' into serverless_app_repo
gdavison Nov 24, 2020
49c7ded
Renames file
gdavison Nov 24, 2020
a70a12d
Merge branch 'master' into serverless_app_repo
gdavison Nov 24, 2020
0908954
More renaming
gdavison Nov 24, 2020
124075f
Removes unneeded ForceNew from semantic_version attribute
gdavison Nov 25, 2020
69a0677
Updates tfresource.NotFound() to support wrapped errors
gdavison Nov 25, 2020
167791b
Reorganizes import and read code
gdavison Nov 25, 2020
19b88f7
Avoids Go formatting churn and Git conflict
gdavison Nov 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
bflad marked this conversation as resolved.
Show resolved Hide resolved

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