diff --git a/.changelog/11937.txt b/.changelog/11937.txt index 106a416f8af..9da502923d7 100644 --- a/.changelog/11937.txt +++ b/.changelog/11937.txt @@ -3,5 +3,5 @@ aws_amplify_branch ``` ```release-note:bug -resource/aws_amplify_app: Mark the `enable_performance_mode` argumnet in the `auto_branch_creation_config` configuration block as `ForceNew` +resource/aws_amplify_app: Mark the `enable_performance_mode` argument in the `auto_branch_creation_config` configuration block as `ForceNew` ``` \ No newline at end of file diff --git a/.changelog/11939.txt b/.changelog/11939.txt new file mode 100644 index 00000000000..7555c387a8c --- /dev/null +++ b/.changelog/11939.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_amplify_webhook +``` \ No newline at end of file diff --git a/aws/internal/service/amplify/finder/finder.go b/aws/internal/service/amplify/finder/finder.go index 7074fabb4c2..ec468523b9f 100644 --- a/aws/internal/service/amplify/finder/finder.go +++ b/aws/internal/service/amplify/finder/finder.go @@ -92,3 +92,31 @@ func BranchByAppIDAndBranchName(conn *amplify.Amplify, appID, branchName string) return output.Branch, nil } + +func WebhookByID(conn *amplify.Amplify, id string) (*amplify.Webhook, error) { + input := &lify.GetWebhookInput{ + WebhookId: aws.String(id), + } + + output, err := conn.GetWebhook(input) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Webhook == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Webhook, nil +} diff --git a/aws/provider.go b/aws/provider.go index 4fd4105c563..74fd8c21cb9 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -456,6 +456,7 @@ func Provider() *schema.Provider { "aws_amplify_app": resourceAwsAmplifyApp(), "aws_amplify_backend_environment": resourceAwsAmplifyBackendEnvironment(), "aws_amplify_branch": resourceAwsAmplifyBranch(), + "aws_amplify_webhook": resourceAwsAmplifyWebhook(), "aws_api_gateway_account": resourceAwsApiGatewayAccount(), "aws_api_gateway_api_key": resourceAwsApiGatewayApiKey(), "aws_api_gateway_authorizer": resourceAwsApiGatewayAuthorizer(), diff --git a/aws/resource_aws_amplify_test.go b/aws/resource_aws_amplify_test.go index 03e39da1c05..c238c783c87 100644 --- a/aws/resource_aws_amplify_test.go +++ b/aws/resource_aws_amplify_test.go @@ -35,6 +35,11 @@ func TestAccAWSAmplify_serial(t *testing.T) { "EnvironmentVariables": testAccAWSAmplifyBranch_EnvironmentVariables, "OptionalArguments": testAccAWSAmplifyBranch_OptionalArguments, }, + "Webhook": { + "basic": testAccAWSAmplifyWebhook_basic, + "disappears": testAccAWSAmplifyWebhook_disappears, + "update": testAccAWSAmplifyWebhook_update, + }, } for group, m := range testCases { diff --git a/aws/resource_aws_amplify_webhook.go b/aws/resource_aws_amplify_webhook.go new file mode 100644 index 00000000000..fe66597b64d --- /dev/null +++ b/aws/resource_aws_amplify_webhook.go @@ -0,0 +1,164 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAmplifyWebhook() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAmplifyWebhookCreate, + Read: resourceAwsAmplifyWebhookRead, + Update: resourceAwsAmplifyWebhookUpdate, + Delete: resourceAwsAmplifyWebhookDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "app_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "branch_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[0-9A-Za-z/_.-]{1,255}$`), "should be not be more than 255 letters, numbers, and the symbols /_.-"), + }, + + "description": { + Type: schema.TypeString, + Optional: true, + }, + + "url": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsAmplifyWebhookCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + input := &lify.CreateWebhookInput{ + AppId: aws.String(d.Get("app_id").(string)), + BranchName: aws.String(d.Get("branch_name").(string)), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating Amplify Webhook: %s", input) + output, err := conn.CreateWebhook(input) + + if err != nil { + return fmt.Errorf("error creating Amplify Webhook: %w", err) + } + + d.SetId(aws.StringValue(output.Webhook.WebhookId)) + + return resourceAwsAmplifyWebhookRead(d, meta) +} + +func resourceAwsAmplifyWebhookRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + webhook, err := finder.WebhookByID(conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Amplify Webhook (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Amplify Webhook (%s): %w", d.Id(), err) + } + + webhookArn := aws.StringValue(webhook.WebhookArn) + arn, err := arn.Parse(webhookArn) + + if err != nil { + return fmt.Errorf("error parsing %q: %w", webhookArn, err) + } + + // arn:${Partition}:amplify:${Region}:${Account}:apps/${AppId}/webhooks/${WebhookId} + parts := strings.Split(arn.Resource, "/") + + if len(parts) != 4 { + return fmt.Errorf("unexpected format for ARN resource (%s)", arn.Resource) + } + + d.Set("app_id", parts[1]) + d.Set("arn", webhookArn) + d.Set("branch_name", webhook.BranchName) + d.Set("description", webhook.Description) + d.Set("url", webhook.WebhookUrl) + + return nil +} + +func resourceAwsAmplifyWebhookUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + input := &lify.UpdateWebhookInput{ + WebhookId: aws.String(d.Id()), + } + + if d.HasChange("branch_name") { + input.BranchName = aws.String(d.Get("branch_name").(string)) + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + log.Printf("[DEBUG] Updating Amplify Webhook: %s", input) + _, err := conn.UpdateWebhook(input) + + if err != nil { + return fmt.Errorf("error updating Amplify Webhook (%s): %w", d.Id(), err) + } + + return resourceAwsAmplifyWebhookRead(d, meta) +} + +func resourceAwsAmplifyWebhookDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + log.Printf("[DEBUG] Deleting Amplify Webhook: %s", d.Id()) + _, err := conn.DeleteWebhook(&lify.DeleteWebhookInput{ + WebhookId: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Amplify Webhook (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_amplify_webhook_test.go b/aws/resource_aws_amplify_webhook_test.go new file mode 100644 index 00000000000..18beddc6fb4 --- /dev/null +++ b/aws/resource_aws_amplify_webhook_test.go @@ -0,0 +1,218 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/amplify" + "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/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func testAccAWSAmplifyWebhook_basic(t *testing.T) { + var webhook amplify.Webhook + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_webhook.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyWebhookDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyWebhookConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyWebhookExists(resourceName, &webhook), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+/webhooks/.+`)), + resource.TestCheckResourceAttr(resourceName, "branch_name", rName), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestMatchResourceAttr(resourceName, "url", regexp.MustCompile(fmt.Sprintf(`^https://webhooks.amplify.%s.%s/.+$`, testAccGetRegion(), testAccGetPartitionDNSSuffix()))), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSAmplifyWebhook_disappears(t *testing.T) { + var webhook amplify.Webhook + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_webhook.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyWebhookDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyWebhookConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyWebhookExists(resourceName, &webhook), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAmplifyWebhook(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSAmplifyWebhook_update(t *testing.T) { + var webhook amplify.Webhook + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_webhook.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyWebhookDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyWebhookConfigDescription(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyWebhookExists(resourceName, &webhook), + resource.TestCheckResourceAttr(resourceName, "branch_name", fmt.Sprintf("%s-1", rName)), + resource.TestCheckResourceAttr(resourceName, "description", "testdescription1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyWebhookConfigDescriptionUpdated(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyWebhookExists(resourceName, &webhook), + resource.TestCheckResourceAttr(resourceName, "branch_name", fmt.Sprintf("%s-2", rName)), + resource.TestCheckResourceAttr(resourceName, "description", "testdescription2"), + ), + }, + }, + }) +} + +func testAccCheckAWSAmplifyWebhookExists(resourceName string, v *amplify.Webhook) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Amplify Webhook ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + webhook, err := finder.WebhookByID(conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *webhook + + return nil + } +} + +func testAccCheckAWSAmplifyWebhookDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_amplify_webhook" { + continue + } + + _, err := finder.WebhookByID(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Amplify Webhook %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAWSAmplifyWebhookConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q +} + +resource "aws_amplify_webhook" "test" { + app_id = aws_amplify_app.test.id + branch_name = aws_amplify_branch.test.branch_name +} +`, rName) +} + +func testAccAWSAmplifyWebhookConfigDescription(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test1" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-1" +} + +resource "aws_amplify_branch" "test2" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-2" +} + +resource "aws_amplify_webhook" "test" { + app_id = aws_amplify_app.test.id + branch_name = aws_amplify_branch.test1.branch_name + description = "testdescription1" +} +`, rName) +} + +func testAccAWSAmplifyWebhookConfigDescriptionUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test1" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-1" +} + +resource "aws_amplify_branch" "test2" { + app_id = aws_amplify_app.test.id + branch_name = "%[1]s-2" +} + +resource "aws_amplify_webhook" "test" { + app_id = aws_amplify_app.test.id + branch_name = aws_amplify_branch.test2.branch_name + description = "testdescription2" +} +`, rName) +} diff --git a/website/docs/r/amplify_webhook.html.markdown b/website/docs/r/amplify_webhook.html.markdown new file mode 100644 index 00000000000..2a0ea85bb96 --- /dev/null +++ b/website/docs/r/amplify_webhook.html.markdown @@ -0,0 +1,53 @@ +--- +subcategory: "Amplify Console" +layout: "aws" +page_title: "AWS: aws_amplify_webhook" +description: |- + Provides an Amplify Webhook resource. +--- + +# Resource: aws_amplify_webhook + +Provides an Amplify Webhook resource. + +## Example Usage + +```terraform +resource "aws_amplify_app" "example" { + name = "app" +} + +resource "aws_amplify_branch" "master" { + app_id = aws_amplify_app.example.id + branch_name = "master" +} + +resource "aws_amplify_webhook" "master" { + app_id = aws_amplify_app.example.id + branch_name = aws_amplify_branch.master.branch_name + description = "triggermaster" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `app_id` - (Required) The unique ID for an Amplify app. +* `branch_name` - (Required) The name for a branch that is part of the Amplify app. +* `description` - (Optional) The description for a webhook. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - The Amazon Resource Name (ARN) for the webhook. +* `url` - The URL of the webhook. + +## Import + +Amplify webhook can be imported using a webhook ID, e.g. + +``` +$ terraform import aws_amplify_webhook.master a26b22a0-748b-4b57-b9a0-ae7e601fe4b1 +```