diff --git a/aws/provider.go b/aws/provider.go index 99d3ff2c48a7..996899d9b518 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -318,6 +318,7 @@ func Provider() terraform.ResourceProvider { "aws_appsync_api_key": resourceAwsAppsyncApiKey(), "aws_appsync_datasource": resourceAwsAppsyncDatasource(), "aws_appsync_graphql_api": resourceAwsAppsyncGraphqlApi(), + "aws_appsync_resolver": resourceAwsAppsyncResolver(), "aws_athena_database": resourceAwsAthenaDatabase(), "aws_athena_named_query": resourceAwsAthenaNamedQuery(), "aws_autoscaling_attachment": resourceAwsAutoscalingAttachment(), diff --git a/aws/resource_aws_appsync_graphql_api.go b/aws/resource_aws_appsync_graphql_api.go index 2b2515f4bdf4..ba10165d96f1 100644 --- a/aws/resource_aws_appsync_graphql_api.go +++ b/aws/resource_aws_appsync_graphql_api.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "regexp" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/appsync" @@ -167,7 +166,7 @@ func resourceAwsAppsyncGraphqlApiCreate(d *schema.ResourceData, meta interface{} d.SetId(*resp.GraphqlApi.ApiId) - if err := resourceAwsAppsyncSchemaPut(d.Id(), d, meta); err != nil { + if err := resourceAwsAppsyncSchemaPut(d, meta); err != nil { return fmt.Errorf("error creating AppSync GraphQL API (%s) Schema: %s", d.Id(), err) } @@ -240,8 +239,10 @@ func resourceAwsAppsyncGraphqlApiUpdate(d *schema.ResourceData, meta interface{} return err } - if err := resourceAwsAppsyncSchemaPut(d.Id(), d, meta); err != nil { - return fmt.Errorf("error updating AppSync GraphQL API (%s) Schema: %s", d.Id(), err) + if d.HasChange("schema") { + if err := resourceAwsAppsyncSchemaPut(d, meta); err != nil { + return fmt.Errorf("error updating AppSync GraphQL API (%s) Schema: %s", d.Id(), err) + } } return resourceAwsAppsyncGraphqlApiRead(d, meta) @@ -375,40 +376,35 @@ func flattenAppsyncGraphqlApiUserPoolConfig(userPoolConfig *appsync.UserPoolConf return []interface{}{m} } -func resourceAwsAppsyncSchemaPut(apiId string, d *schema.ResourceData, meta interface{}) error { +func resourceAwsAppsyncSchemaPut(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).appsyncconn - if d.HasChange("schema") { + if v, ok := d.GetOk("schema"); ok { input := &appsync.StartSchemaCreationInput{ - ApiId: aws.String(apiId), - Definition: ([]byte)(d.Get("schema").(string)), + ApiId: aws.String(d.Id()), + Definition: ([]byte)(v.(string)), } if _, err := conn.StartSchemaCreation(input); err != nil { return err } activeSchemaConfig := &resource.StateChangeConf{ - Pending: []string{"PROCESSING"}, - Target: []string{"ACTIVE", "SUCCESS"}, + Pending: []string{appsync.SchemaStatusProcessing}, + Target: []string{"SUCCESS", appsync.SchemaStatusActive}, // should be only appsync.SchemaStatusActive . I think this is a problem in documentation: https://docs.aws.amazon.com/appsync/latest/APIReference/API_GetSchemaCreationStatus.html Refresh: func() (interface{}, string, error) { - conn := meta.(*AWSClient).appsyncconn - input := &appsync.GetSchemaCreationStatusInput{ - ApiId: aws.String(apiId), - } - result, err := conn.GetSchemaCreationStatus(input) - + result, err := conn.GetSchemaCreationStatus(&appsync.GetSchemaCreationStatusInput{ + ApiId: aws.String(d.Id()), + }) if err != nil { return 0, "", err } return result, *result.Status, nil }, - Timeout: d.Timeout(schema.TimeoutCreate), - Delay: 10 * time.Second, - MinTimeout: 5 * time.Second, + Timeout: d.Timeout(schema.TimeoutCreate), } if _, err := activeSchemaConfig.WaitForState(); err != nil { - return fmt.Errorf("Error waiting for schema creation status on AppSync API %s: %s", apiId, err) + return fmt.Errorf("Error waiting for schema creation status on AppSync API %s: %s", d.Id(), err) } } diff --git a/aws/resource_aws_appsync_graphql_api_test.go b/aws/resource_aws_appsync_graphql_api_test.go index 181d09a6be90..a19742678a5c 100644 --- a/aws/resource_aws_appsync_graphql_api_test.go +++ b/aws/resource_aws_appsync_graphql_api_test.go @@ -96,7 +96,7 @@ func TestAccAWSAppsyncGraphqlApi_basic(t *testing.T) { }) } -func TestAccAWSAppsyncGraphqlApi_schema(t *testing.T) { +func TestAccAWSAppsyncGraphqlApi_Schema(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_appsync_graphql_api.test" @@ -118,6 +118,7 @@ func TestAccAWSAppsyncGraphqlApi_schema(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "schema"), resource.TestCheckResourceAttrSet(resourceName, "uris.%"), resource.TestCheckResourceAttrSet(resourceName, "uris.GRAPHQL"), + testAccCheckAwsAppsyncTypeExists(resourceName, "Post"), ), }, { @@ -126,6 +127,13 @@ func TestAccAWSAppsyncGraphqlApi_schema(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{"schema"}, }, + { + Config: testAccAppsyncGraphqlApiConfig_SchemaUpdate(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncGraphqlApiExists(resourceName), + testAccCheckAwsAppsyncTypeExists(resourceName, "PostV2"), + ), + }, }, }) } @@ -647,6 +655,30 @@ func testAccCheckAwsAppsyncGraphqlApiExists(name string) resource.TestCheckFunc } } +func testAccCheckAwsAppsyncTypeExists(name, typeName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + conn := testAccProvider.Meta().(*AWSClient).appsyncconn + + input := &appsync.GetTypeInput{ + ApiId: aws.String(rs.Primary.ID), + TypeName: aws.String(typeName), + Format: aws.String(appsync.OutputTypeSdl), + } + + _, err := conn.GetType(input) + if err != nil { + return err + } + + return nil + } +} + func testAccAppsyncGraphqlApiConfig_AuthenticationType(rName, authenticationType string) string { return fmt.Sprintf(` resource "aws_appsync_graphql_api" "test" { @@ -656,16 +688,6 @@ resource "aws_appsync_graphql_api" "test" { `, authenticationType, rName) } -func testAccAppsyncGraphqlApiConfig_Schema(rName string) string { - return fmt.Sprintf(` -resource "aws_appsync_graphql_api" "test" { - authentication_type = "API_KEY" - name = %q - schema = "type Query { test:String }\nschema { query:Query }" -} -`, rName) -} - func testAccAppsyncGraphqlApiConfig_LogConfig_FieldLogLevel(rName, fieldLogLevel string) string { return fmt.Sprintf(` data "aws_partition" "current" {} @@ -797,3 +819,23 @@ resource "aws_appsync_graphql_api" "test" { } `, rName, rName, defaultAction) } + +func testAccAppsyncGraphqlApiConfig_Schema(rName string) string { + return fmt.Sprintf(` +resource "aws_appsync_graphql_api" "test" { + authentication_type = "API_KEY" + name = %q + schema = "type Mutation {\n\tputPost(id: ID!, title: String!): Post\n}\n\ntype Post {\n\tid: ID!\n\ttitle: String!\n}\n\ntype Query {\n\tsinglePost(id: ID!): Post\n}\n\nschema {\n\tquery: Query\n\tmutation: Mutation\n\n}\n" +} +`, rName) +} + +func testAccAppsyncGraphqlApiConfig_SchemaUpdate(rName string) string { + return fmt.Sprintf(` +resource "aws_appsync_graphql_api" "test" { + authentication_type = "API_KEY" + name = %q + schema = "type Mutation {\n\tputPostV2(id: ID!, title: String!): PostV2\n}\n\ntype PostV2 {\n\tid: ID!\n\ttitle: String!\n}\n\ntype Query {\n\tsinglePostV2(id: ID!): PostV2\n}\n\nschema {\n\tquery: Query\n\tmutation: Mutation\n\n}\n" +} +`, rName) +} diff --git a/aws/resource_aws_appsync_resolver.go b/aws/resource_aws_appsync_resolver.go new file mode 100644 index 000000000000..c36eb65c58c4 --- /dev/null +++ b/aws/resource_aws_appsync_resolver.go @@ -0,0 +1,182 @@ +package aws + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appsync" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsAppsyncResolver() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAppsyncResolverCreate, + Read: resourceAwsAppsyncResolverRead, + Update: resourceAwsAppsyncResolverUpdate, + Delete: resourceAwsAppsyncResolverDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "api_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "field": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "data_source": { + Type: schema.TypeString, + Required: true, + }, + "request_template": { + Type: schema.TypeString, + Required: true, + }, + "response_template": { + Type: schema.TypeString, + Required: true, // documentation bug, the api returns 400 if this is not specified. + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsAppsyncResolverCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appsyncconn + + input := &appsync.CreateResolverInput{ + ApiId: aws.String(d.Get("api_id").(string)), + DataSourceName: aws.String(d.Get("data_source").(string)), + TypeName: aws.String(d.Get("type").(string)), + FieldName: aws.String(d.Get("field").(string)), + RequestMappingTemplate: aws.String(d.Get("request_template").(string)), + ResponseMappingTemplate: aws.String(d.Get("response_template").(string)), + } + + mutexKey := fmt.Sprintf("appsync-schema-%s", d.Get("api_id").(string)) + awsMutexKV.Lock(mutexKey) + defer awsMutexKV.Unlock(mutexKey) + + _, err := retryOnAwsCode(appsync.ErrCodeConcurrentModificationException, func() (interface{}, error) { + return conn.CreateResolver(input) + }) + + if err != nil { + return err + } + + d.SetId(d.Get("api_id").(string) + "-" + d.Get("type").(string) + "-" + d.Get("field").(string)) + + return resourceAwsAppsyncResolverRead(d, meta) +} + +func resourceAwsAppsyncResolverRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appsyncconn + + apiID, typeName, fieldName, err := decodeAppsyncResolverID(d.Id()) + + if err != nil { + return err + } + + input := &appsync.GetResolverInput{ + ApiId: aws.String(apiID), + TypeName: aws.String(typeName), + FieldName: aws.String(fieldName), + } + + resp, err := conn.GetResolver(input) + if err != nil { + return err + } + + d.Set("api_id", apiID) + d.Set("arn", resp.Resolver.ResolverArn) + d.Set("type", resp.Resolver.TypeName) + d.Set("field", resp.Resolver.FieldName) + d.Set("data_source", resp.Resolver.DataSourceName) + d.Set("request_template", resp.Resolver.RequestMappingTemplate) + d.Set("response_template", resp.Resolver.ResponseMappingTemplate) + + return nil +} + +func resourceAwsAppsyncResolverUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appsyncconn + + input := &appsync.UpdateResolverInput{ + ApiId: aws.String(d.Get("api_id").(string)), + DataSourceName: aws.String(d.Get("data_source").(string)), + FieldName: aws.String(d.Get("field").(string)), + TypeName: aws.String(d.Get("type").(string)), + RequestMappingTemplate: aws.String(d.Get("request_template").(string)), + ResponseMappingTemplate: aws.String(d.Get("response_template").(string)), + } + + mutexKey := fmt.Sprintf("appsync-schema-%s", d.Get("api_id").(string)) + awsMutexKV.Lock(mutexKey) + defer awsMutexKV.Unlock(mutexKey) + + _, err := retryOnAwsCode(appsync.ErrCodeConcurrentModificationException, func() (interface{}, error) { + return conn.UpdateResolver(input) + }) + + if err != nil { + return err + } + + return resourceAwsAppsyncResolverRead(d, meta) +} + +func resourceAwsAppsyncResolverDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).appsyncconn + + apiID, typeName, fieldName, err := decodeAppsyncResolverID(d.Id()) + + if err != nil { + return err + } + + input := &appsync.DeleteResolverInput{ + ApiId: aws.String(apiID), + TypeName: aws.String(typeName), + FieldName: aws.String(fieldName), + } + + mutexKey := fmt.Sprintf("appsync-schema-%s", d.Get("api_id").(string)) + awsMutexKV.Lock(mutexKey) + defer awsMutexKV.Unlock(mutexKey) + + _, err = retryOnAwsCode(appsync.ErrCodeConcurrentModificationException, func() (interface{}, error) { + return conn.DeleteResolver(input) + }) + + if err != nil { + return err + } + + return nil +} + +func decodeAppsyncResolverID(id string) (string, string, string, error) { + idParts := strings.SplitN(id, "-", 3) + if len(idParts) != 3 { + return "", "", "", fmt.Errorf("expected ID in format ApiID-TypeName-FieldName, received: %s", id) + } + return idParts[0], idParts[1], idParts[2], nil +} diff --git a/aws/resource_aws_appsync_resolver_test.go b/aws/resource_aws_appsync_resolver_test.go new file mode 100644 index 000000000000..18b4ef350124 --- /dev/null +++ b/aws/resource_aws_appsync_resolver_test.go @@ -0,0 +1,559 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/appsync" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAwsAppsyncResolver_basic(t *testing.T) { + rName := fmt.Sprintf("tfacctest%d", acctest.RandInt()) + resourceName := "aws_appsync_resolver.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncResolver_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "appsync", regexp.MustCompile("apis/.+/types/.+/resolvers/.+")), + resource.TestCheckResourceAttr(resourceName, "data_source", rName), + resource.TestCheckResourceAttrSet(resourceName, "request_template"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppsyncResolver_DataSource(t *testing.T) { + rName := fmt.Sprintf("tfacctest%d", acctest.RandInt()) + resourceName := "aws_appsync_resolver.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncResolver_DataSource(rName, "test_ds_1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "data_source", "test_ds_1"), + ), + }, + { + Config: testAccAppsyncResolver_DataSource(rName, "test_ds_2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "data_source", "test_ds_2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAppsyncResolver_RequestTemplate(t *testing.T) { + rName := fmt.Sprintf("tfacctest%d", acctest.RandInt()) + resourceName := "aws_appsync_resolver.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncResolver_RequestTemplate(rName, "/"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "request_template", regexp.MustCompile("resourcePath\": \"/\"")), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppsyncResolver_RequestTemplate(rName, "/test"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "request_template", regexp.MustCompile("resourcePath\": \"/test\"")), + ), + }, + }, + }) +} + +func TestAccAwsAppsyncResolver_ResponseTemplate(t *testing.T) { + rName := fmt.Sprintf("tfacctest%d", acctest.RandInt()) + resourceName := "aws_appsync_resolver.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncResolver_ResponseTemplate(rName, 200), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "response_template", regexp.MustCompile(`ctx\.result\.statusCode == 200`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAppsyncResolver_ResponseTemplate(rName, 201), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "response_template", regexp.MustCompile(`ctx\.result\.statusCode == 201`)), + ), + }, + }, + }) +} + +func TestAccAwsAppsyncResolver_multipleResolvers(t *testing.T) { + rName := fmt.Sprintf("tfacctest%d", acctest.RandInt()) + resourceName := "aws_appsync_resolver.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAppsyncResolverDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAppsyncResolver_multipleResolvers(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAppsyncResolverExists(resourceName+"1"), + testAccCheckAwsAppsyncResolverExists(resourceName+"2"), + testAccCheckAwsAppsyncResolverExists(resourceName+"3"), + testAccCheckAwsAppsyncResolverExists(resourceName+"4"), + testAccCheckAwsAppsyncResolverExists(resourceName+"5"), + testAccCheckAwsAppsyncResolverExists(resourceName+"6"), + testAccCheckAwsAppsyncResolverExists(resourceName+"7"), + testAccCheckAwsAppsyncResolverExists(resourceName+"8"), + testAccCheckAwsAppsyncResolverExists(resourceName+"9"), + testAccCheckAwsAppsyncResolverExists(resourceName+"10"), + ), + }, + }, + }) +} + +func testAccCheckAwsAppsyncResolverDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).appsyncconn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_appsync_resolver" { + continue + } + + apiID, typeName, fieldName, err := decodeAppsyncResolverID(rs.Primary.ID) + + if err != nil { + return err + } + + input := &appsync.GetResolverInput{ + ApiId: aws.String(apiID), + TypeName: aws.String(typeName), + FieldName: aws.String(fieldName), + } + + _, err = conn.GetResolver(input) + if err != nil { + if isAWSErr(err, appsync.ErrCodeNotFoundException, "") { + return nil + } + return err + } + } + return nil +} + +func testAccCheckAwsAppsyncResolverExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + if rs.Primary.ID == "" { + return fmt.Errorf("Resource has no ID: %s", name) + } + + apiID, typeName, fieldName, err := decodeAppsyncResolverID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).appsyncconn + + input := &appsync.GetResolverInput{ + ApiId: aws.String(apiID), + TypeName: aws.String(typeName), + FieldName: aws.String(fieldName), + } + + _, err = conn.GetResolver(input) + + return err + } +} + +func testAccAppsyncResolver_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_appsync_graphql_api" "test" { + authentication_type = "API_KEY" + name = %q + schema = <> aws_appsync_api_key + > + aws_appsync_resolver + diff --git a/website/docs/r/appsync_resolver.html.markdown b/website/docs/r/appsync_resolver.html.markdown new file mode 100644 index 000000000000..7dafea95f718 --- /dev/null +++ b/website/docs/r/appsync_resolver.html.markdown @@ -0,0 +1,98 @@ +--- +layout: "aws" +page_title: "AWS: aws_appsync_resolver" +sidebar_current: "docs-aws-resource-appsync-resolver" +description: |- + Provides an AppSync Resolver. +--- + +# aws_appsync_resolver + +Provides an AppSync Resolver. + +## Example Usage + +```hcl +resource "aws_appsync_graphql_api" "test" { + authentication_type = "API_KEY" + name = "tf-example" + schema = <