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

Added support for appsync_graphql_api schema dn appsync_resolver #6451

Merged
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
36 changes: 16 additions & 20 deletions aws/resource_aws_appsync_graphql_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"log"
"regexp"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/appsync"
Expand Down Expand Up @@ -167,7 +166,7 @@ func resourceAwsAppsyncGraphqlApiCreate(d *schema.ResourceData, meta interface{}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this pull request -- when running the acceptance testing for aws_appsync_graphql_api, was hitting this pretty often with random tests and our default 20 concurrency:

--- FAIL: TestAccAWSAppsyncGraphqlApi_Schema (11.25s)
    testing.go:538: Step 0 error: Error applying: 1 error occurred:
          * aws_appsync_graphql_api.test: 1 error occurred:
          * aws_appsync_graphql_api.test: ConcurrentModificationException: Can't create new GraphQL API, a GraphQL API creation is already in progress.

--- FAIL: TestAccAWSAppsyncGraphqlApi_OpenIDConnectConfig_ClientID (14.78s)
    testing.go:538: Step 0 error: Error applying: 1 error occurred:
          * aws_appsync_graphql_api.test: 1 error occurred:
          * aws_appsync_graphql_api.test: ConcurrentModificationException: Can't create new GraphQL API, a GraphQL API creation is already in progress.

Judging by the lack of error context before ConcurrentModificationException here and from the debug logs this was occurring during the CreateGraphqlApi call. In a followup commit to ensure a great user experience, I went ahead and added the error context to that call so we can know where this similar looking error is happening (return fmt.Errorf("error creating AppSync GraphQL API: %d", err)) and updated the AWS Go SDK AppSync client to automatically enable retries for that call in aws/config.go:

	client.appsyncconn.Handlers.Retry.PushBack(func(r *request.Request) {
		if r.Operation.Name == "CreateGraphqlApi" {
			if isAWSErr(r.Error, appsync.ErrCodeConcurrentModificationException, "a GraphQL API creation is already in progress") {
				r.Retryable = aws.Bool(true)
			}
		}
	})

I mention this here because we may be able to remove the wrapping retryOnAwsCode calls below in the aws_graphql_resolver resource by adding other API calls with their appropriate error messaging to the above logic. If you are interested in simplifying the code after this pull request, we'd happily accept that. 👍

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)
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}

Expand Down
64 changes: 53 additions & 11 deletions aws/resource_aws_appsync_graphql_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"),
),
},
{
Expand All @@ -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"),
),
},
},
})
}
Expand Down Expand Up @@ -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" {
Expand All @@ -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" {}
Expand Down Expand Up @@ -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)
}
182 changes: 182 additions & 0 deletions aws/resource_aws_appsync_resolver.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return error context here for operators and code maintainers:

Suggested change
return err
return fmt.Errorf("error creating AppSync Resolver: %s", 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prior to returning an error when an AppSync Resolver cannot be found (which can happen when the GraphQL API or Resolver is deleted outside Terraform), we should instead attempt to catch the error and signal to Terraform that this resource needs to be recreated instead.

Suggested change
if err != nil {
if isAWSErr(err, appsync.ErrCodeNotFoundException, "") {
log.Printf("[WARN] AppSync Resolver (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}
if err != nil {

By returning the appsync.GraphqlApi and appsync.Resolver API objects from the Exists testing functions for both resources, this can be tested with the following:

func TestAccAwsAppsyncResolver_disappears(t *testing.T) {
	var api1 appsync.GraphqlApi
	var resolver1 appsync.Resolver
	rName := fmt.Sprintf("tfacctest%d", acctest.RandInt())
	appsyncGraphqlApiResourceName := "aws_appsync_graphql_api.test"
	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(
					testAccCheckAwsAppsyncGraphqlApiExists(appsyncGraphqlApiResourceName, &api1),
					testAccCheckAwsAppsyncResolverExists(resourceName, &resolver1),
					testAccCheckAwsAppsyncResolverDisappears(&api1, &resolver1),
				),
				ExpectNonEmptyPlan: true,
			},
		},
	})
}

func testAccCheckAwsAppsyncResolverDisappears(api *appsync.GraphqlApi, resolver *appsync.Resolver) resource.TestCheckFunc {
	return func(s *terraform.State) error {
		conn := testAccProvider.Meta().(*AWSClient).appsyncconn

		input := &appsync.DeleteResolverInput{
			ApiId:     api.ApiId,
			FieldName: resolver.FieldName,
			TypeName:  resolver.TypeName,
		}

		_, err := conn.DeleteResolver(input)

		return err
	}
}

Prior to resource code update:

--- FAIL: TestAccAwsAppsyncResolver_disappears (12.60s)
    testing.go:538: Step 0 error: Error on follow-up refresh: 1 error occurred:
        	* aws_appsync_resolver.test: 1 error occurred:
        	* aws_appsync_resolver.test: aws_appsync_resolver.test: NotFoundException: No resolver found.

After code update:

--- PASS: TestAccAwsAppsyncResolver_disappears (17.10s)

return err
Copy link
Contributor

@bflad bflad Mar 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return error context here for operators and code maintainers:

Suggested change
return err
return fmt.Errorf("error getting AppSync Resolver (%s): %s", d.Id(), 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return error context here for operators and code maintainers:

Suggested change
return err
return fmt.Errorf("error updating AppSync Resolver (%s): %s", d.Id(), 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return error context here for operators and code maintainers:

Suggested change
return err
return fmt.Errorf("error deleting AppSync Resolver (%s): %s", d.Id(), 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
}
Loading