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 @@ -316,6 +316,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
49 changes: 49 additions & 0 deletions aws/resource_aws_appsync_graphql_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package aws

import (
"fmt"
"github.com/hashicorp/terraform/helper/resource"
"log"
"regexp"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/appsync"
Expand Down Expand Up @@ -130,6 +132,10 @@ func resourceAwsAppsyncGraphqlApi() *schema.Resource {
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"schema": {
Type: schema.TypeString,
Optional: true,
},
},
}
}
Expand Down Expand Up @@ -161,9 +167,45 @@ 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 v, ok := d.GetOk("schema"); ok {
if err := startSchemaCreationAndWaitForItToBeActive(d.Id(), v.(string), d.Timeout(schema.TimeoutCreate), conn); err != nil {
return err
}
}

return resourceAwsAppsyncGraphqlApiRead(d, meta)
}

func startSchemaCreationAndWaitForItToBeActive(apiID string, schema string, timeout time.Duration, conn *appsync.AppSync) error {
input := &appsync.StartSchemaCreationInput{
ApiId: aws.String(apiID),
Definition: []byte(schema),
}

if _, err := conn.StartSchemaCreation(input); err != nil {
return err
}

stateConf := &resource.StateChangeConf{
Pending: []string{appsync.SchemaStatusProcessing},
Target: []string{"SUCCESS"}, // should be appsync.SchemaStatusActive . I think this is a problem in documentation: https://docs.aws.amazon.com/appsync/latest/APIReference/API_GetSchemaCreationStatus.html
Timeout: timeout,
Refresh: func() (interface{}, string, error) {
result, err := conn.GetSchemaCreationStatus(&appsync.GetSchemaCreationStatusInput{
ApiId: aws.String(apiID),
})
if err != nil {
return 42, "", err
}

return result, *result.Status, nil
},
}
_, err := stateConf.WaitForState()

return err
}

func resourceAwsAppsyncGraphqlApiRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).appsyncconn

Expand Down Expand Up @@ -230,6 +272,13 @@ func resourceAwsAppsyncGraphqlApiUpdate(d *schema.ResourceData, meta interface{}
return err
}

if d.HasChange("schema") {
if v, ok := d.GetOk("schema"); ok {
if err := startSchemaCreationAndWaitForItToBeActive(d.Id(), v.(string), d.Timeout(schema.TimeoutUpdate), conn); err != nil {
return err
}
}
}
return resourceAwsAppsyncGraphqlApiRead(d, meta)
}

Expand Down
77 changes: 77 additions & 0 deletions aws/resource_aws_appsync_graphql_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,39 @@ func TestAccAWSAppsyncGraphqlApi_UserPoolConfig_DefaultAction(t *testing.T) {
})
}

func TestAccAWSAppsyncGraphqlApi_Schema(t *testing.T) {
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "aws_appsync_graphql_api.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsAppsyncGraphqlApiDestroy,
Steps: []resource.TestStep{
{
Config: testAccAppsyncGraphqlApiConfig_Schema(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsAppsyncGraphqlApiExists(resourceName),
testAccCheckAwsAppsyncTypeExists(resourceName, "Post"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"schema"},
},
{
Config: testAccAppsyncGraphqlApiConfig_SchemaUpdate(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsAppsyncGraphqlApiExists(resourceName),
testAccCheckAwsAppsyncTypeExists(resourceName, "PostV2"),
),
},
},
})
}

func testAccCheckAwsAppsyncGraphqlApiDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).appsyncconn
for _, rs := range s.RootModule().Resources {
Expand Down Expand Up @@ -565,6 +598,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 Down Expand Up @@ -705,3 +762,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)
}
160 changes: 160 additions & 0 deletions aws/resource_aws_appsync_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package aws

import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/appsync"
"github.com/hashicorp/terraform/helper/schema"
"strings"
)

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

_, err := 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)),
}

_, err := 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),
}

_, err = 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