diff --git a/aws/data_source_aws_cloudformation_stack.go b/aws/data_source_aws_cloudformation_stack.go index 4e67bb9de34..f84b8eccb20 100644 --- a/aws/data_source_aws_cloudformation_stack.go +++ b/aws/data_source_aws_cloudformation_stack.go @@ -113,7 +113,7 @@ func dataSourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface return err } - template, err := normalizeCloudFormationTemplate(*tOut.TemplateBody) + template, err := normalizeJsonOrYamlString(*tOut.TemplateBody) if err != nil { return fmt.Errorf("template body contains an invalid JSON or YAML: %s", err) } diff --git a/aws/diff_suppress_funcs.go b/aws/diff_suppress_funcs.go index b192d208f8a..15e2e2e6011 100644 --- a/aws/diff_suppress_funcs.go +++ b/aws/diff_suppress_funcs.go @@ -97,15 +97,15 @@ func suppressOpenIdURL(k, old, new string, d *schema.ResourceData) bool { return oldUrl.String() == newUrl.String() } -func suppressCloudFormationTemplateBodyDiffs(k, old, new string, d *schema.ResourceData) bool { - normalizedOld, err := normalizeCloudFormationTemplate(old) +func suppressEquivalentJsonOrYamlDiffs(k, old, new string, d *schema.ResourceData) bool { + normalizedOld, err := normalizeJsonOrYamlString(old) if err != nil { log.Printf("[WARN] Unable to normalize Terraform state CloudFormation template body: %s", err) return false } - normalizedNew, err := normalizeCloudFormationTemplate(new) + normalizedNew, err := normalizeJsonOrYamlString(new) if err != nil { log.Printf("[WARN] Unable to normalize Terraform configuration CloudFormation template body: %s", err) diff --git a/aws/diff_suppress_funcs_test.go b/aws/diff_suppress_funcs_test.go index 0b406d6438d..3096217fd9d 100644 --- a/aws/diff_suppress_funcs_test.go +++ b/aws/diff_suppress_funcs_test.go @@ -71,7 +71,7 @@ func TestSuppressEquivalentTypeStringBoolean(t *testing.T) { } } -func TestSuppressCloudFormationTemplateBodyDiffs(t *testing.T) { +func TestSuppressEquivalentJsonOrYamlDiffs(t *testing.T) { testCases := []struct { description string equivalent bool @@ -253,7 +253,7 @@ Outputs: } for _, tc := range testCases { - value := suppressCloudFormationTemplateBodyDiffs("test_property", tc.old, tc.new, nil) + value := suppressEquivalentJsonOrYamlDiffs("test_property", tc.old, tc.new, nil) if tc.equivalent && !value { t.Fatalf("expected test case (%s) to be equivalent", tc.description) diff --git a/aws/resource_aws_apigatewayv2_api.go b/aws/resource_aws_apigatewayv2_api.go index 63048054e34..b0a455fa54b 100644 --- a/aws/resource_aws_apigatewayv2_api.go +++ b/aws/resource_aws_apigatewayv2_api.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "reflect" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" @@ -101,6 +102,12 @@ func resourceAwsApiGatewayV2Api() *schema.Resource { Required: true, ValidateFunc: validation.StringLenBetween(1, 128), }, + "body": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppressEquivalentJsonOrYamlDiffs, + ValidateFunc: validateStringIsJsonOrYaml, + }, "protocol_type": { Type: schema.TypeString, Required: true, @@ -135,6 +142,64 @@ func resourceAwsApiGatewayV2Api() *schema.Resource { } } +func resourceAwsAPIGatewayV2ImportOpenAPI(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).apigatewayv2conn + + if body, ok := d.GetOk("body"); ok { + revertReq := &apigatewayv2.UpdateApiInput{ + ApiId: aws.String(d.Id()), + Name: aws.String(d.Get("name").(string)), + Description: aws.String(d.Get("description").(string)), + Version: aws.String(d.Get("version").(string)), + } + + log.Printf("[DEBUG] Updating API Gateway from OpenAPI spec %s", d.Id()) + importReq := &apigatewayv2.ReimportApiInput{ + ApiId: aws.String(d.Id()), + Body: aws.String(body.(string)), + } + + _, err := conn.ReimportApi(importReq) + + if err != nil { + return fmt.Errorf("error importing API Gateway v2 API (%s) OpenAPI specification: %s", d.Id(), err) + } + + tags := d.Get("tags") + corsConfiguration := d.Get("cors_configuration") + + if err := resourceAwsApiGatewayV2ApiRead(d, meta); err != nil { + return err + } + + if !reflect.DeepEqual(corsConfiguration, d.Get("cors_configuration")) { + if len(corsConfiguration.([]interface{})) == 0 { + log.Printf("[DEBUG] Deleting CORS configuration for API Gateway v2 API (%s)", d.Id()) + _, err := conn.DeleteCorsConfiguration(&apigatewayv2.DeleteCorsConfigurationInput{ + ApiId: aws.String(d.Id()), + }) + if err != nil { + return fmt.Errorf("error deleting CORS configuration for API Gateway v2 API (%s): %s", d.Id(), err) + } + } else { + revertReq.CorsConfiguration = expandApiGateway2CorsConfiguration(corsConfiguration.([]interface{})) + } + } + + if err := keyvaluetags.Apigatewayv2UpdateTags(conn, d.Get("arn").(string), d.Get("tags"), tags); err != nil { + return fmt.Errorf("error updating API Gateway v2 API (%s) tags: %s", d.Id(), err) + } + + log.Printf("[DEBUG] Reverting API Gateway v2 API: %s", revertReq) + _, err = conn.UpdateApi(revertReq) + if err != nil { + return fmt.Errorf("error updating API Gateway v2 API (%s): %s", d.Id(), err) + } + } + + return nil +} + func resourceAwsApiGatewayV2ApiCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).apigatewayv2conn @@ -177,6 +242,11 @@ func resourceAwsApiGatewayV2ApiCreate(d *schema.ResourceData, meta interface{}) d.SetId(aws.StringValue(resp.ApiId)) + err = resourceAwsAPIGatewayV2ImportOpenAPI(d, meta) + if err != nil { + return err + } + return resourceAwsApiGatewayV2ApiRead(d, meta) } @@ -286,6 +356,13 @@ func resourceAwsApiGatewayV2ApiUpdate(d *schema.ResourceData, meta interface{}) } } + if d.HasChange("body") { + err := resourceAwsAPIGatewayV2ImportOpenAPI(d, meta) + if err != nil { + return err + } + } + return resourceAwsApiGatewayV2ApiRead(d, meta) } diff --git a/aws/resource_aws_apigatewayv2_api_test.go b/aws/resource_aws_apigatewayv2_api_test.go index 8b25d0e72a3..ad1d5ecbcfe 100644 --- a/aws/resource_aws_apigatewayv2_api_test.go +++ b/aws/resource_aws_apigatewayv2_api_test.go @@ -317,6 +317,231 @@ func TestAccAWSAPIGatewayV2Api_AllAttributesHttp(t *testing.T) { }) } +func TestAccAWSAPIGatewayV2Api_Openapi(t *testing.T) { + var v apigatewayv2.GetApiOutput + resourceName := "aws_apigatewayv2_api.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApiConfig_OpenAPI(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttrSet(resourceName, "api_endpoint"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "version", ""), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "apigateway", regexp.MustCompile(`/apis/.+`)), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /test"}), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"body"}, + }, + { + Config: testAccAWSAPIGatewayV2ApiConfig_UpdatedOpenAPIYaml(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "version", ""), + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /update"}), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayV2Api_Openapi_WithTags(t *testing.T) { + var v apigatewayv2.GetApiOutput + resourceName := "aws_apigatewayv2_api.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApiConfig_OpenAPIYaml_tags(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttrSet(resourceName, "api_endpoint"), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "apigateway", regexp.MustCompile(`/apis/.+`)), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /test"}), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "Value1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "Value2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"body"}, + }, + { + Config: testAccAWSAPIGatewayV2ApiConfig_OpenAPIYaml_tagsUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /update"}), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "Value1U"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "Value2U"), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayV2Api_Openapi_WithCorsConfiguration(t *testing.T) { + var v apigatewayv2.GetApiOutput + resourceName := "aws_apigatewayv2_api.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApiConfig_OpenAPIYaml_corsConfiguration(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttrSet(resourceName, "api_endpoint"), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "apigateway", regexp.MustCompile(`/apis/.+`)), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + resource.TestCheckResourceAttr(resourceName, "cors_configuration.0.allow_methods.#", "1"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "cors_configuration.0.allow_methods.*", "delete"), + resource.TestCheckResourceAttr(resourceName, "cors_configuration.0.allow_origins.#", "1"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "cors_configuration.0.allow_origins.*", "https://www.google.de"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"body"}, + }, + { + Config: testAccAWSAPIGatewayV2ApiConfig_OpenAPIYaml_corsConfigurationUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /update"}), + resource.TestCheckResourceAttr(resourceName, "cors_configuration.#", "0"), + ), + }, + { + Config: testAccAWSAPIGatewayV2ApiConfig_OpenAPIYaml_corsConfigurationUpdated2(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /update"}), + resource.TestCheckResourceAttr(resourceName, "cors_configuration.0.allow_methods.#", "2"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "cors_configuration.0.allow_methods.*", "get"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "cors_configuration.0.allow_methods.*", "put"), + resource.TestCheckResourceAttr(resourceName, "cors_configuration.0.allow_origins.#", "2"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "cors_configuration.0.allow_origins.*", "https://www.example.com"), + tfawsresource.TestCheckTypeSetElemAttr(resourceName, "cors_configuration.0.allow_origins.*", "https://www.google.de"), + ), + }, + }, + }) +} + +func TestAccAWSAPIGatewayV2Api_OpenapiWithMoreFields(t *testing.T) { + var v apigatewayv2.GetApiOutput + resourceName := "aws_apigatewayv2_api.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAPIGatewayV2ApiDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAPIGatewayV2ApiConfig_OpenAPIYaml(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttrSet(resourceName, "api_endpoint"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "version", ""), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "apigateway", regexp.MustCompile(`/apis/.+`)), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /test"}), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"body"}, + }, + { + Config: testAccAWSAPIGatewayV2ApiConfig_UpdatedOpenAPI2(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "description", "description test"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "version", "2017-04-21T04:08:08Z"), + testAccCheckAWSAPIGatewayV2ApiExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "protocol_type", apigatewayv2.ProtocolTypeHttp), + testAccCheckAWSAPIGatewayV2ApiRoutes(&v, []string{"GET /update"}), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"body"}, + }, + }, + }) +} + +func testAccCheckAWSAPIGatewayV2ApiRoutes(v *apigatewayv2.GetApiOutput, routes []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).apigatewayv2conn + + resp, err := conn.GetRoutes(&apigatewayv2.GetRoutesInput{ + ApiId: v.ApiId, + }) + if err != nil { + return err + } + + actualRoutePaths := map[string]bool{} + for _, route := range resp.Items { + actualRoutePaths[*route.RouteKey] = true + } + + for _, route := range routes { + if _, ok := actualRoutePaths[route]; !ok { + return fmt.Errorf("Expected path %v but did not find it in %v", route, actualRoutePaths) + } + delete(actualRoutePaths, route) + } + + if len(actualRoutePaths) > 0 { + return fmt.Errorf("Found unexpected paths %v", actualRoutePaths) + } + + return nil + } +} + func TestAccAWSAPIGatewayV2Api_Tags(t *testing.T) { var v apigatewayv2.GetApiOutput resourceName := "aws_apigatewayv2_api.test" @@ -765,3 +990,273 @@ resource "aws_apigatewayv2_api" "test" { } `, rName) } + +func testAccAWSAPIGatewayV2ApiConfig_OpenAPI(rName string) string { + return fmt.Sprintf(` +resource "aws_apigatewayv2_api" "test" { + name = "%s" + protocol_type = "HTTP" + body = <