From 14d3b96cf7e25db74a7f4e447379a47ac6e1140a Mon Sep 17 00:00:00 2001 From: Nicholas DeJaco <54116900+ndejaco2@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:12:06 -0700 Subject: [PATCH] Add schema breaking change detection hook for AWS::AppSync::GraphQLSchema (#243) * Add schema breaking change detection hook for AWS::AppSync::GraphQLSchema * Add examples in README.md --- .../.gitignore | 23 ++ .../.rpdk-config | 29 ++ .../AppSync_BreakingChangeDetection/README.md | 177 ++++++++++ ...unity-appsync-breakingchangedetection.json | 28 ++ .../docs/README.md | 45 +++ .../hook-role.yaml | 40 +++ .../inputs/inputs_1_invalid_pre_update.json | 12 + .../inputs/inputs_1_pre_update.json | 12 + .../lombok.config | 1 + hooks/AppSync_BreakingChangeDetection/pom.xml | 263 +++++++++++++++ .../requirements-dev.txt | 6 + .../requirements.txt | 4 + .../run-tests.sh | 5 + .../CallbackContext.java | 10 + .../ClientBuilder.java | 13 + .../Configuration.java | 8 + .../PreUpdateHookHandler.java | 92 ++++++ .../schema/AppSyncDirectives.java | 277 ++++++++++++++++ .../schema/AppSyncScalars.java | 22 ++ .../schema/AppSyncSchemaDiffReporter.java | 104 ++++++ .../schema/AppSyncSchemaDiffUtil.java | 108 ++++++ .../schema/S3DefinitionUtil.java | 85 +++++ .../src/resources/log4j2.xml | 17 + .../PreUpdateHookHandlerTest.java | 311 ++++++++++++++++++ .../S3DefinitionUtilTest.java | 32 ++ .../src/test/resources/additionalSdl.graphql | 1 + .../test/resources/original-schema.graphql | 17 + .../schema-update-additional-sdl.graphql | 18 + .../resources/schema-update-breaking.graphql | 17 + .../resources/schema-update-dangerous.graphql | 18 + .../schema-update-non-breaking.graphql | 19 ++ .../schema-with-appsync-built-in-defs.graphql | 32 ++ .../template.yml | 24 ++ .../test/configuration.json | 9 + .../test/configuration_undo.json | 9 + .../test/setup.json | 23 ++ .../typeConfiguration.json | 11 + hooks/alpha-buildspec-java.yml | 2 +- hooks/beta-buildspec-java.yml | 2 +- hooks/prod-buildspec-java.yml | 2 +- 40 files changed, 1925 insertions(+), 3 deletions(-) create mode 100644 hooks/AppSync_BreakingChangeDetection/.gitignore create mode 100644 hooks/AppSync_BreakingChangeDetection/.rpdk-config create mode 100644 hooks/AppSync_BreakingChangeDetection/README.md create mode 100644 hooks/AppSync_BreakingChangeDetection/awscommunity-appsync-breakingchangedetection.json create mode 100644 hooks/AppSync_BreakingChangeDetection/docs/README.md create mode 100644 hooks/AppSync_BreakingChangeDetection/hook-role.yaml create mode 100644 hooks/AppSync_BreakingChangeDetection/inputs/inputs_1_invalid_pre_update.json create mode 100644 hooks/AppSync_BreakingChangeDetection/inputs/inputs_1_pre_update.json create mode 100644 hooks/AppSync_BreakingChangeDetection/lombok.config create mode 100644 hooks/AppSync_BreakingChangeDetection/pom.xml create mode 100644 hooks/AppSync_BreakingChangeDetection/requirements-dev.txt create mode 100644 hooks/AppSync_BreakingChangeDetection/requirements.txt create mode 100755 hooks/AppSync_BreakingChangeDetection/run-tests.sh create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/CallbackContext.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/ClientBuilder.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/Configuration.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/PreUpdateHookHandler.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncDirectives.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncScalars.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncSchemaDiffReporter.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncSchemaDiffUtil.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/S3DefinitionUtil.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/resources/log4j2.xml create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/java/com/awscommunity/appsync/breakingchangedetection/PreUpdateHookHandlerTest.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/java/com/awscommunity/appsync/breakingchangedetection/S3DefinitionUtilTest.java create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/resources/additionalSdl.graphql create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/resources/original-schema.graphql create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-additional-sdl.graphql create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-breaking.graphql create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-dangerous.graphql create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-non-breaking.graphql create mode 100644 hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-with-appsync-built-in-defs.graphql create mode 100644 hooks/AppSync_BreakingChangeDetection/template.yml create mode 100644 hooks/AppSync_BreakingChangeDetection/test/configuration.json create mode 100644 hooks/AppSync_BreakingChangeDetection/test/configuration_undo.json create mode 100644 hooks/AppSync_BreakingChangeDetection/test/setup.json create mode 100644 hooks/AppSync_BreakingChangeDetection/typeConfiguration.json diff --git a/hooks/AppSync_BreakingChangeDetection/.gitignore b/hooks/AppSync_BreakingChangeDetection/.gitignore new file mode 100644 index 00000000..faa92591 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/.gitignore @@ -0,0 +1,23 @@ +# macOS +.DS_Store +._* + +# Maven outputs +.classpath + +# IntelliJ +*.iml +.idea +out.java +out/ +.settings +.project + +# auto-generated files +target/ + +# our logs +rpdk.log* + +# contains credentials +sam-tests/ diff --git a/hooks/AppSync_BreakingChangeDetection/.rpdk-config b/hooks/AppSync_BreakingChangeDetection/.rpdk-config new file mode 100644 index 00000000..9b7b4db2 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/.rpdk-config @@ -0,0 +1,29 @@ +{ + "artifact_type": "HOOK", + "typeName": "AwsCommunity::AppSync::BreakingChangeDetection", + "language": "java", + "runtime": "java11", + "entrypoint": "com.awscommunity.appsync.breakingchangedetection.HookHandlerWrapper::handleRequest", + "testEntrypoint": "com.awscommunity.appsync.breakingchangedetection.HookHandlerWrapper::testEntrypoint", + "settings": { + "version": false, + "subparser_name": null, + "verbose": 0, + "force": false, + "type_name": null, + "artifact_type": null, + "endpoint_url": null, + "region": null, + "target_schemas": [], + "profile": null, + "namespace": [ + "com", + "awscommunity", + "appsync", + "breakingchangedetection" + ], + "codegen_template_path": "default", + "protocolVersion": "2.0.0" + }, + "executableEntrypoint": "com.awscommunity.appsync.breakingchangedetection.HookHandlerWrapperExecutable" +} diff --git a/hooks/AppSync_BreakingChangeDetection/README.md b/hooks/AppSync_BreakingChangeDetection/README.md new file mode 100644 index 00000000..5fd246bb --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/README.md @@ -0,0 +1,177 @@ +# AwsCommunity::AppSync::BreakingChangeDetection + +Validates that an AWS AppSync GraphQL schema update does not introduce a change that would break existing clients of the AWS AppSync GraphQL API. There are three categories of changes in a GraphQL schema: + +- **Breaking**: These are changes that are not backwards incompatible for existing API clients, such as changing the return type of an existing field or removing a field from the schema. +- **Dangerous**: These are changes that may be dangerous for existing API clients but are not necessarily breaking, such as adding a new value in an Enum or changing the default value on an argument. +- **Non-breaking**: These are changes that are backwards compatible for existing clients, such as adding a new field or type to the schema. + +By default, the hook will fail when an AppSync schema update introduces a change that is in the "Breaking" category. + +## Configuration + +```bash +# Create a basic type configuration json +cat < typeConfiguration.json +{ + "CloudFormationConfiguration": { + "HookConfiguration": { + "TargetStacks": "ALL", + "FailureMode": "FAIL", + "Properties": {} + } + } +} +EOF + +# enable the hook +aws cloudformation set-type-configuration \ + --configuration file://typeConfiguration.json \ + --type HOOK \ + --type-name AwsCommunity::AppSync::BreakingChangeDetection +``` + +## Examples + +- Original Template: + +``` +Resources: + BasicGraphQLApi: + Type: "AWS::AppSync::GraphQLApi" + Properties: + Name: BasicApi + AuthenticationType: "AWS_IAM" + + BasicGraphQLSchema: + Type: "AWS::AppSync::GraphQLSchema" + Properties: + ApiId: !GetAtt BasicGraphQLApi.ApiId + Definition: | + type Test { + version: String! + type: TestType + } + + type Query { + getTests: [Test]! + } + + type Mutation { + addTest(version: String!): Test + } + + enum TestType { + SIMPLE, + COMPLEX + } +``` +### Non-Breaking Change Example + +- Adding the new field Test.name is not a breaking change. + +``` +Resources: + BasicGraphQLApi: + Type: "AWS::AppSync::GraphQLApi" + Properties: + Name: BasicApi + AuthenticationType: "AWS_IAM" + + BasicGraphQLSchema: + Type: "AWS::AppSync::GraphQLSchema" + Properties: + ApiId: !GetAtt BasicGraphQLApi.ApiId + Definition: | + type Test { + version: String! + type: TestType + name: String + } + + type Query { + getTests: [Test]! + getTest(version: String): Test + } + + type Mutation { + addTest(version: String!): Test + } + + enum TestType { + SIMPLE, + COMPLEX + } +``` + +### Breaking Change Example + +- Removing the Query.getTest field is a breaking change because clients will no longer be able to query it. +``` +Resources: + BasicGraphQLApi: + Type: "AWS::AppSync::GraphQLApi" + Properties: + Name: BasicApi + AuthenticationType: "AWS_IAM" + + BasicGraphQLSchema: + Type: "AWS::AppSync::GraphQLSchema" + Properties: + ApiId: !GetAtt BasicGraphQLApi.ApiId + Definition: | + type Test { + version: String! + type: TestType + } + + type Query { + getTests: [Test]! + } + + type Mutation { + addTest(version: String!): Test + } + + enum TestType { + SIMPLE, + COMPLEX + } +``` + +### Dangerous Change Example + +- Adding an Enum Value to an existing enum type is considered a "Dangerous" change as it may require changes to handling on the client. It will not break existing queries. +``` +Resources: + BasicGraphQLApi: + Type: "AWS::AppSync::GraphQLApi" + Properties: + Name: BasicApi + AuthenticationType: "AWS_IAM" + + BasicGraphQLSchema: + Type: "AWS::AppSync::GraphQLSchema" + Properties: + ApiId: !GetAtt BasicGraphQLApi.ApiId + Definition: | + type Test { + version: String! + type: TestType + name: String + } + + type Query { + getTests: [Test]! + getTest(version: String): Test + } + + type Mutation { + addTest(version: String!): Test + } + + enum TestType { + SIMPLE, + COMPLEX + } +``` \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/awscommunity-appsync-breakingchangedetection.json b/hooks/AppSync_BreakingChangeDetection/awscommunity-appsync-breakingchangedetection.json new file mode 100644 index 00000000..dc8cb605 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/awscommunity-appsync-breakingchangedetection.json @@ -0,0 +1,28 @@ +{ + "typeName": "AwsCommunity::AppSync::BreakingChangeDetection", + "description": "PreUpdate hook to perform breaking change detection on an AWS AppSync schema change.", + "sourceUrl": "https://github.com/aws-cloudformation/community-registry-extensions/tree/main/hooks/AppSync_BreakingChangeDetection", + "documentationUrl": "https://github.com/aws-cloudformation/community-registry-extensions/tree/main/hooks/AppSync_BreakingChangeDetection/README.md", + "typeConfiguration": { + "properties": { + "ConsiderDangerousChangesBreaking": { + "description": "Whether to consider changes that are in the DANGEROUS category as BREAKING. Changes in the DANGEROUS category wont break existing requests but could affect the runtime behavior of clients. Adding an enum value is an example of a dangerous change.", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "required": [], + "handlers": { + "preUpdate": { + "targetNames": [ + "AWS::AppSync::GraphQLSchema" + ], + "permissions": [ + "s3:GetObject" + ] + } + }, + "additionalProperties": false +} diff --git a/hooks/AppSync_BreakingChangeDetection/docs/README.md b/hooks/AppSync_BreakingChangeDetection/docs/README.md new file mode 100644 index 00000000..2ca481ab --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/docs/README.md @@ -0,0 +1,45 @@ +# AwsCommunity::AppSync::BreakingChangeDetection + +## Activation + +To activate a hook in your account, use the following JSON as the `Configuration` request parameter for [`SetTypeConfiguration`](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_SetTypeConfiguration.html) API request. + +### Configuration + +
+{
+    "CloudFormationConfiguration": {
+        "HookConfiguration": {
+            "TargetStacks":  "ALL" | "NONE",
+            "FailureMode": "FAIL" | "WARN" ,
+            "Properties" : {
+                "ConsiderDangerousChangesBreaking" : Boolean
+            }
+        }
+    }
+}
+
+ +## Properties + +#### ConsiderDangerousChangesBreaking + +Whether to consider changes that are in the DANGEROUS category as BREAKING. Changes in the DANGEROUS category wont break existing requests but could affect the runtime behavior of clients. Adding an enum value is an example of a dangerous change. + +_Required_: No + +_Type_: Boolean + + +--- + +## Targets + +* `AWS::AppSync::GraphQLSchema` + +--- + +

Please note that the enum values for +TargetStacks and FailureMode +might go out of date, please refer to their official documentation page for up-to-date values.

+ diff --git a/hooks/AppSync_BreakingChangeDetection/hook-role.yaml b/hooks/AppSync_BreakingChangeDetection/hook-role.yaml new file mode 100644 index 00000000..9a79276a --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/hook-role.yaml @@ -0,0 +1,40 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during Hook operations on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 8400 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - hooks.cloudformation.amazonaws.com + - resources.cloudformation.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: + Ref: AWS::AccountId + StringLike: + aws:SourceArn: + Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/hook/AwsCommunity-AppSync-BreakingChangeDetection/* + Path: "/" + Policies: + - PolicyName: HookTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "s3:GetObject" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/hooks/AppSync_BreakingChangeDetection/inputs/inputs_1_invalid_pre_update.json b/hooks/AppSync_BreakingChangeDetection/inputs/inputs_1_invalid_pre_update.json new file mode 100644 index 00000000..7b95c5da --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/inputs/inputs_1_invalid_pre_update.json @@ -0,0 +1,12 @@ +{ + "AWS::AppSync::GraphQLSchema": { + "resourceProperties": { + "ApiId": "123", + "Definition": "schema {\nquery: Query\nmutation: Mutation\n }\n\n type Query {\n singlePost(id: ID!): Post2\n }\n\n type Mutation {\n putPost(id: ID!, title: String!): Post2\n }\n\n type Post2 {\n id: ID!\n title: String!\n }" + }, + "previousResourceProperties": { + "ApiId": "123", + "Definition": "schema {\nquery: Query\nmutation: Mutation\n }\n\n type Query {\n singlePost(id: ID!): Post\n }\n\n type Mutation {\n putPost(id: ID!, title: String!): Post\n }\n\n type Post {\n id: ID!\n title: String!\n }" + } + } +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/inputs/inputs_1_pre_update.json b/hooks/AppSync_BreakingChangeDetection/inputs/inputs_1_pre_update.json new file mode 100644 index 00000000..78790ca9 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/inputs/inputs_1_pre_update.json @@ -0,0 +1,12 @@ +{ + "AWS::AppSync::GraphQLSchema": { + "resourceProperties": { + "ApiId": "123", + "Definition": "schema {\nquery: Query\nmutation: Mutation\n }\n\n type Query {\n singlePost(id: ID!): Post\n }\n\n type Mutation {\n putPost(id: ID!, title: String!): Post\n }\n\n type Post {\n id: ID!\n title: String!\ndate: AWSDateTime\n }" + }, + "previousResourceProperties": { + "ApiId": "123", + "Definition": "schema {\nquery: Query\nmutation: Mutation\n }\n\n type Query {\n singlePost(id: ID!): Post\n }\n\n type Mutation {\n putPost(id: ID!, title: String!): Post\n }\n\n type Post {\n id: ID!\n title: String!\n }" + } + } +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/lombok.config b/hooks/AppSync_BreakingChangeDetection/lombok.config new file mode 100644 index 00000000..7a21e880 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/hooks/AppSync_BreakingChangeDetection/pom.xml b/hooks/AppSync_BreakingChangeDetection/pom.xml new file mode 100644 index 00000000..6e6cf707 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/pom.xml @@ -0,0 +1,263 @@ + + + 4.0.0 + + com.awscommunity.appsync.breakingchangedetection + awscommunity-appsync-breakingchangedetection-handler + awscommunity-appsync-breakingchangedetection-handler + 1.0-SNAPSHOT + jar + + + 11 + 11 + UTF-8 + UTF-8 + + + + + + + software.amazon.awssdk + bom + LATEST + pom + import + + + + + + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + + + + org.projectlombok + lombok + 1.18.4 + provided + + + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + + org.apache.logging.log4j + log4j-core + 2.17.1 + + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.17.1 + + + + + org.assertj + assertj-core + 3.12.2 + test + + + + org.junit.jupiter + junit-jupiter + 5.5.0-M1 + test + + + + org.mockito + mockito-core + 3.6.0 + test + + + + org.mockito + mockito-junit-jupiter + 3.6.0 + test + + + + com.graphql-java + graphql-java + 21.0 + + + + software.amazon.awssdk + s3 + 2.20.155 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + -Xlint:all,-options,-processing + -Werror + + 9 + 9 + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + *:* + + **/Log4j2Plugins.dat + + + + + + + package + + shade + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + generate + generate-sources + + exec + + + cfn + generate ${cfn.generate.args} + ${project.basedir} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/target/generated-sources/rpdk + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4 + + + maven-surefire-plugin + 3.0.0-M3 + + + org.jacoco + jacoco-maven-plugin + 0.8.4 + + + **/model/** + **/BaseHookConfiguration* + **/HookHandlerWrapper* + **/Configuration* + + + + + + prepare-agent + + + + report + test + + report + + + + jacoco-check + + check + + + + + PACKAGE + + + BRANCH + COVEREDRATIO + 0.9 + + + INSTRUCTION + COVEREDRATIO + 0.9 + + + + + + + + + + + + ${project.basedir} + + awssamples-appsyncschemabreakingchanges-hook.json + + + + ${project.basedir}/target/loaded-target-schemas + + **/*.json + + + + + diff --git a/hooks/AppSync_BreakingChangeDetection/requirements-dev.txt b/hooks/AppSync_BreakingChangeDetection/requirements-dev.txt new file mode 100644 index 00000000..a2e7449f --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/requirements-dev.txt @@ -0,0 +1,6 @@ +# To install the following requirements, run: +# python3 -m pip install -r requirements-dev.txt +cloudformation-cli>=0.2.33 +cloudformation-cli-java-plugin>=2.0.14 +cloudformation-cli-python-lib>2.1.15 +pytest>=7.2.0 \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/requirements.txt b/hooks/AppSync_BreakingChangeDetection/requirements.txt new file mode 100644 index 00000000..9ff3c284 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/requirements.txt @@ -0,0 +1,4 @@ +# To install the following requirements, run: +# python3 -m pip install -r requirements.txt +cloudformation-cli-python-lib>2.1.15 +pytest>=7.2.0 diff --git a/hooks/AppSync_BreakingChangeDetection/run-tests.sh b/hooks/AppSync_BreakingChangeDetection/run-tests.sh new file mode 100755 index 00000000..d72c2b0c --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -eou pipefail + +mvn clean verify javadoc:javadoc && cfn test -v --enforce-timeout 90 \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/CallbackContext.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/CallbackContext.java new file mode 100644 index 00000000..1610dc46 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/CallbackContext.java @@ -0,0 +1,10 @@ +package com.awscommunity.appsync.breakingchangedetection; + +import software.amazon.cloudformation.proxy.StdCallbackContext; + +@lombok.Getter +@lombok.Setter +@lombok.ToString +@lombok.EqualsAndHashCode(callSuper = true) +public class CallbackContext extends StdCallbackContext { +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/ClientBuilder.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/ClientBuilder.java new file mode 100644 index 00000000..ffc9b0ff --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/ClientBuilder.java @@ -0,0 +1,13 @@ +package com.awscommunity.appsync.breakingchangedetection; + +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.cloudformation.LambdaWrapper; + +public class ClientBuilder { + + public static S3Client getS3Client() { + return S3Client.builder() + .httpClient(LambdaWrapper.HTTP_CLIENT) + .build(); + } +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/Configuration.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/Configuration.java new file mode 100644 index 00000000..35bb9296 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/Configuration.java @@ -0,0 +1,8 @@ +package com.awscommunity.appsync.breakingchangedetection; + +class Configuration extends BaseHookConfiguration { + + public Configuration() { + super("awscommunity-appsync-breakingchangedetection.json"); + } +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/PreUpdateHookHandler.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/PreUpdateHookHandler.java new file mode 100644 index 00000000..5048815e --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/PreUpdateHookHandler.java @@ -0,0 +1,92 @@ +package com.awscommunity.appsync.breakingchangedetection; + +import com.awscommunity.appsync.breakingchangedetection.model.aws.appsync.graphqlschema.AwsAppsyncGraphqlschema; +import com.awscommunity.appsync.breakingchangedetection.model.aws.appsync.graphqlschema.AwsAppsyncGraphqlschemaTargetModel; +import com.awscommunity.appsync.breakingchangedetection.schema.AppSyncSchemaDiffReporter; +import com.awscommunity.appsync.breakingchangedetection.schema.AppSyncSchemaDiffUtil; +import com.google.common.collect.ImmutableSet; +import graphql.schema.idl.errors.SchemaProblem; +import software.amazon.cloudformation.exceptions.UnsupportedTargetException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.hook.HookHandlerRequest; +import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel; +import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTargetModel; + +import java.util.Collection; + +public class PreUpdateHookHandler extends BaseHookHandler { + + private static final Collection HOOK_TARGET_NAMES = ImmutableSet.of( + "AWS::AppSync::GraphQLSchema" + ); + + private static final String BREAKING_ERROR_MESSAGE = + "Breaking changes have been detected for this AWS::AppSync::GraphQLSchema:\n"; + + private static final String SUCCESS_MESSAGE = + "Successfully verified there are no breaking changes for AWS::AppSync::GraphQLSchema.\n"; + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final HookHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger, + final TypeConfigurationModel typeConfiguration) { + + final String targetName = request.getHookContext().getTargetName(); + + if (!HOOK_TARGET_NAMES.contains(targetName)) { + throw new UnsupportedTargetException(targetName); + } + + logger.log(String.format("Successfully invoked PreUpdateHookHandler for target %s.", targetName)); + + final ResourceHookTargetModel targetModel = request.getHookContext() + .getTargetModel(AwsAppsyncGraphqlschemaTargetModel.class); + final AwsAppsyncGraphqlschema previousResourceProperties = targetModel.getPreviousResourceProperties(); + final AwsAppsyncGraphqlschema resourceProperties = targetModel.getResourceProperties(); + + boolean considerDangerousChanges = false; + if (typeConfiguration != null) { + considerDangerousChanges = typeConfiguration.getConsiderDangerousChangesBreaking(); + } + + try { + final AppSyncSchemaDiffReporter reporter = AppSyncSchemaDiffUtil.diffSchema(previousResourceProperties, + resourceProperties, proxy, logger); + final String changeReport = reporter.getFormattedChangeReport(); + logger.log(changeReport); + if (reporter.validateChanges(!considerDangerousChanges)) { + return ProgressEvent.builder() + .status(OperationStatus.SUCCESS) + .message(SUCCESS_MESSAGE + changeReport) + .build(); + } else { + return ProgressEvent.builder() + .status(OperationStatus.FAILED) + .message(BREAKING_ERROR_MESSAGE + changeReport) + .errorCode(HandlerErrorCode.NonCompliant) + .build(); + } + } catch (SchemaProblem ex) { + logger.log(ex.toString()); + return ProgressEvent.builder() + .status(OperationStatus.FAILED) + .message("Unable to parse the new schema with the following errors: " + ex.getErrors().toString()) + .errorCode(HandlerErrorCode.InvalidRequest) + .build(); + } catch (Exception ex) { + logger.log(ex.toString()); + return ProgressEvent.builder() + .status(OperationStatus.FAILED) + .message("An unexpected error occurred validating the schema: " + ex.toString()) + .errorCode(HandlerErrorCode.HandlerInternalFailure) + .build(); + } + } +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncDirectives.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncDirectives.java new file mode 100644 index 00000000..29bce702 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncDirectives.java @@ -0,0 +1,277 @@ +package com.awscommunity.appsync.breakingchangedetection.schema; + +import com.google.common.collect.ImmutableSet; +import graphql.introspection.Introspection; +import graphql.language.DirectiveDefinition; +import graphql.language.DirectiveLocation; +import graphql.language.TypeName; +import graphql.language.InputValueDefinition; +import graphql.language.ListType; + +import java.util.List; +import java.util.Set; + +/** + * A class which contains set of all in built directives currently supported by AppSync. + */ +public class AppSyncDirectives { + + /** + * Cognito groups auth directive name. + */ + public static final String AUTH_DIRECTIVE_NAME = "aws_auth"; + + /** + * Multi-auth Cognito auth directive name. + */ + public static final String COGNITO_AUTH_DIRECTIVE_NAME = "aws_cognito_user_pools"; + + /** + * IAM auth directive name. + */ + public static final String IAM_AUTH_DIRECTIVE_NAME = "aws_iam"; + + /** + * API key directive name. + */ + public static final String API_KEY_AUTH_DIRECTIVE_NAME = "aws_api_key"; + + /** + * OIDC directive name. + */ + public static final String OIDC_AUTH_DIRECTIVE_NAME = "aws_oidc"; + + /** + * Lambda auth directive name. + */ + public static final String LAMBDA_AUTH_DIRECTIVE_NAME = "aws_lambda"; + + /** + * Cognito groups argument name. + */ + public static final String AUTH_DIRECTIVE_COGNITO_GROUP_ARG = "cognito_groups"; + + /** + * Name of the publish directive which notifies the service which subscription to publish to. + */ + public static final String PUBLISH_DIRECTIVE_NAME = "aws_publish"; + + /** + * The name of the publish argument. + */ + public static final String PUBLISH_DIRECTIVE_SUBSCRIPTION_ARG = "subscriptions"; + + /** + * Name of the subscribe directive which notifies the service which mutations trigger this subscription. + */ + public static final String SUBSCRIBE_DIRECTIVE_NAME = "aws_subscribe"; + + /** + * The name of the subscribe argument. + */ + public static final String SUBSCRIBE_DIRECTIVE_MUTATIONS_ARG = "mutations"; + + /** + * Name of the hidden directive for AppSync Merged APIs. + */ + public static final String HIDDEN_DIRECTIVE_NAME = "hidden"; + + /** + * Name of the canonical directive for AppSync Merged APIs. + */ + public static final String CANONICAL_DIRECTIVE_NAME = "canonical"; + + /** + * Name of the renamed directive for AppSync Merged APIs. + */ + public static final String RENAMED_DIRECTIVE_NAME = "renamed"; + + /** + * Name of the renamed to argument for the renamed directive for AppSync Merged APIs. + */ + public static final String RENAMED_TO_ARG_NAME = "to"; + + /** + * Directive on Field definition. + */ + private static final DirectiveLocation FIELD_DIRECTIVE_LOCATION = DirectiveLocation.newDirectiveLocation() + .name(Introspection.DirectiveLocation.FIELD_DEFINITION.name()) + .build(); + + /** + * Directive on Object definition. + */ + private static final DirectiveLocation OBJECT_DIRECTIVE_LOCATION = DirectiveLocation.newDirectiveLocation() + .name(Introspection.DirectiveLocation.OBJECT.name()) + .build(); + + /** + * Directive on Interface definition. + */ + private static final DirectiveLocation INTERFACE_DIRECTIVE_LOCATION = DirectiveLocation.newDirectiveLocation() + .name(Introspection.DirectiveLocation.INTERFACE.name()) + .build(); + + /** + * Directive on Union definition. + */ + private static final DirectiveLocation UNION_DIRECTIVE_LOCATION = DirectiveLocation.newDirectiveLocation() + .name(Introspection.DirectiveLocation.UNION.name()) + .build(); + + /** + * Directive on Enum definition. + */ + private static final DirectiveLocation ENUM_DIRECTIVE_LOCATION = DirectiveLocation.newDirectiveLocation() + .name(Introspection.DirectiveLocation.ENUM.name()) + .build(); + + /** + * Directive on Enum Value definition. + */ + private static final DirectiveLocation ENUM_VALUE_DIRECTIVE_LOCATION = DirectiveLocation.newDirectiveLocation() + .name(Introspection.DirectiveLocation.ENUM_VALUE.name()) + .build(); + + /** + * Directive on Input Object definition. + */ + private static final DirectiveLocation INPUT_OBJECT_DIRECTIVE_LOCATION = DirectiveLocation.newDirectiveLocation() + .name(Introspection.DirectiveLocation.INPUT_OBJECT.name()) + .build(); + + /** + * Directive on Input Field definition. + */ + private static final DirectiveLocation INPUT_FIELD_DEFINITION_DIRECTIVE_LOCATION = DirectiveLocation.newDirectiveLocation() + .name(Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION.name()) + .build(); + + /** + * List of all valid locations for the AppSync Merged APIs conflict resoluition directives. + */ + private static final List CONFLICT_RESOLUTION_DIRECTIVE_LOCATIONS = + List.of(FIELD_DIRECTIVE_LOCATION, OBJECT_DIRECTIVE_LOCATION, INTERFACE_DIRECTIVE_LOCATION, + UNION_DIRECTIVE_LOCATION, ENUM_DIRECTIVE_LOCATION, ENUM_VALUE_DIRECTIVE_LOCATION, + INPUT_OBJECT_DIRECTIVE_LOCATION, INPUT_FIELD_DEFINITION_DIRECTIVE_LOCATION); + + /** + * The api key directive + */ + public static final DirectiveDefinition API_KEY_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(API_KEY_AUTH_DIRECTIVE_NAME) + .directiveLocation(FIELD_DIRECTIVE_LOCATION) + .directiveLocation(OBJECT_DIRECTIVE_LOCATION) + .build(); + + /** + * The iam auth directive definition. + */ + public static final DirectiveDefinition IAM_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(IAM_AUTH_DIRECTIVE_NAME) + .directiveLocation(FIELD_DIRECTIVE_LOCATION) + .directiveLocation(OBJECT_DIRECTIVE_LOCATION) + .build(); + + /** + * The cognito auth directive definition. + */ + public static final DirectiveDefinition COGNITO_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(COGNITO_AUTH_DIRECTIVE_NAME) + .inputValueDefinition(InputValueDefinition.newInputValueDefinition() + .name(AUTH_DIRECTIVE_COGNITO_GROUP_ARG) + .type(new ListType(new TypeName("String"))) + .build()) + .directiveLocation(FIELD_DIRECTIVE_LOCATION) + .directiveLocation(OBJECT_DIRECTIVE_LOCATION) + .build(); + + /** + * The oidc auth directive definition. + */ + public static final DirectiveDefinition OIDC_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(OIDC_AUTH_DIRECTIVE_NAME) + .directiveLocation(FIELD_DIRECTIVE_LOCATION) + .directiveLocation(OBJECT_DIRECTIVE_LOCATION) + .build(); + + /** + * The lambda auth directive definition. + */ + public static final DirectiveDefinition LAMBDA_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(LAMBDA_AUTH_DIRECTIVE_NAME) + .directiveLocation(FIELD_DIRECTIVE_LOCATION) + .directiveLocation(OBJECT_DIRECTIVE_LOCATION) + .build(); + + /** + * The aws-auth directive. + */ + public static final DirectiveDefinition AUTH_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(AUTH_DIRECTIVE_NAME) + .inputValueDefinition(new InputValueDefinition(AUTH_DIRECTIVE_COGNITO_GROUP_ARG, new ListType(new TypeName("String")))) + .directiveLocation(FIELD_DIRECTIVE_LOCATION) + .build(); + + /** + * A directive which can be applied to mutations. This directive tells us the mutation will publish to a + * subscription. + */ + public static final DirectiveDefinition PUBLISH_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(PUBLISH_DIRECTIVE_NAME) + .inputValueDefinition(new InputValueDefinition(PUBLISH_DIRECTIVE_SUBSCRIPTION_ARG, new ListType(new TypeName("String")))) + .directiveLocation(FIELD_DIRECTIVE_LOCATION) + .build(); + + /** + * A directive which can be applied to subscriptions. This directive tells us the mutation which triggers this + * subscription. + */ + public static final DirectiveDefinition SUBSCRIBE_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(SUBSCRIBE_DIRECTIVE_NAME) + .inputValueDefinition(new InputValueDefinition(SUBSCRIBE_DIRECTIVE_MUTATIONS_ARG, new ListType(new TypeName("String")))) + .directiveLocation(FIELD_DIRECTIVE_LOCATION) + .build(); + + /** + * Hidden directive for AppSync Merged APIs. + */ + public static final DirectiveDefinition HIDDEN_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(HIDDEN_DIRECTIVE_NAME) + .directiveLocations(CONFLICT_RESOLUTION_DIRECTIVE_LOCATIONS) + .build(); + + /** + * Canonical directive for AppSync Merged APIs. + */ + public static final DirectiveDefinition CANONICAL_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(CANONICAL_DIRECTIVE_NAME) + .directiveLocations(CONFLICT_RESOLUTION_DIRECTIVE_LOCATIONS) + .build(); + + /** + * Renamed directive for AppSync Merged APIs. + */ + public static final DirectiveDefinition RENAMED_DIRECTIVE = DirectiveDefinition.newDirectiveDefinition() + .name(RENAMED_DIRECTIVE_NAME) + .inputValueDefinition(new InputValueDefinition(RENAMED_TO_ARG_NAME, new TypeName("String"))) + .directiveLocations(CONFLICT_RESOLUTION_DIRECTIVE_LOCATIONS) + .build(); + + /** + * A list of built in AppSync Directives. + */ + public static final Set APPSYNC_DIRECTIVE_DEFINITIONS = ImmutableSet.of( + AUTH_DIRECTIVE, + PUBLISH_DIRECTIVE, + SUBSCRIBE_DIRECTIVE, + API_KEY_DIRECTIVE, + OIDC_DIRECTIVE, + IAM_DIRECTIVE, + COGNITO_DIRECTIVE, + LAMBDA_DIRECTIVE, + HIDDEN_DIRECTIVE, + CANONICAL_DIRECTIVE, + RENAMED_DIRECTIVE + ); +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncScalars.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncScalars.java new file mode 100644 index 00000000..607ab3ba --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncScalars.java @@ -0,0 +1,22 @@ +package com.awscommunity.appsync.breakingchangedetection.schema; + +import graphql.language.ScalarTypeDefinition; + +import java.util.Set; +import java.util.stream.Collectors; + +public class AppSyncScalars { + + /** + * Set of names for built in scalars for AppSync. + */ + public static Set APPSYNC_SCALAR_NAMES = Set.of("AWSDate", "AWSTime", "AWSDateTime", "AWSTimestamp", + "AWSEmail", "AWSJSON", "AWSPhone", "AWSURL", "AWSIPAddress"); + + /** + * List of scalar type definitions for the built in scalars for AppSync. + */ + public static Set APPSYNC_SCALAR_DEFINITIONS = + APPSYNC_SCALAR_NAMES.stream().map(n -> ScalarTypeDefinition.newScalarTypeDefinition().name(n).build()) + .collect(Collectors.toSet()); +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncSchemaDiffReporter.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncSchemaDiffReporter.java new file mode 100644 index 00000000..66d130d1 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncSchemaDiffReporter.java @@ -0,0 +1,104 @@ +package com.awscommunity.appsync.breakingchangedetection.schema; + +import graphql.schema.diff.DiffEvent; +import graphql.schema.diff.reporting.DifferenceReporter; +import software.amazon.cloudformation.proxy.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class handles the reporting of breaking changes. It receives a callback during schema diff to be able to log + * and return the changes that were detected with a new version of the AppSync GraphQL Schema. + */ +public class AppSyncSchemaDiffReporter implements DifferenceReporter { + + private final List breakingChanges; + private final List dangerousChanges; + private final List infoChanges; + private final Logger logger; + private final String apiId; + + + /** + * Schema diff reporter for logging and recording the change events reported by SchemaDiff + * @param logger Logger + * @param apiId Api Id. + */ + public AppSyncSchemaDiffReporter(final Logger logger, + final String apiId) { + this.breakingChanges = new ArrayList<>(); + this.dangerousChanges = new ArrayList<>(); + this.infoChanges = new ArrayList<>(); + this.logger = logger; + this.apiId = apiId; + } + + /** + * Reports the status of a comparison between a field in the new version and old version. + * @param differenceEvent Difference event representing the potential change in a field. + */ + @Override + public void report(DiffEvent differenceEvent) { + switch (differenceEvent.getLevel()) { + case BREAKING: + this.breakingChanges.add(differenceEvent); + break; + case DANGEROUS: + this.dangerousChanges.add(differenceEvent); + break; + case INFO: + // A non-null category in the event indicates there was actually a change. + if (differenceEvent.getCategory() != null) { + this.infoChanges.add(differenceEvent); + } + + break; + default: + logger.log(String.format("Unexpected event level: %s, ignoring", differenceEvent.getLevel())); + } + } + + @Override + public void onEnd() { + } + + /** + * Determines whether a breaking change has occurred or not. There are some changes that are considered "DANGEROUS" + * but not "BREAKING." Use can pass a flag indicating whether the "DANGEROUS" changes should be considered a failure or not. + * @param allowDangerousChanges Whether to allow dangerous changes or consider them breaking. + * @return Boolean indicating whether there was a breaking change or not. + */ + public boolean validateChanges(boolean allowDangerousChanges) { + boolean hasNoBreakingChanges = this.breakingChanges.isEmpty(); + boolean hasNoDangerousChanges = this.dangerousChanges.isEmpty(); + if (allowDangerousChanges) { + return hasNoBreakingChanges; + } else { + return hasNoBreakingChanges && hasNoDangerousChanges; + } + } + + /** + * Get the formatted report of all relevant changes between the two schema versions. + * @return String containing the formatted change report. + */ + public String getFormattedChangeReport() { + final StringBuilder sb = new StringBuilder(); + appendChangeReports(sb, this.breakingChanges); + appendChangeReports(sb, this.dangerousChanges); + appendChangeReports(sb, this.infoChanges); + return sb.toString(); + } + + /** + * Append the formatted diff event reports to the input string builder. + * @param sb String builder + * @param changes Changes to append to the String. + */ + private void appendChangeReports(final StringBuilder sb, List changes) { + for (DiffEvent change : changes) { + sb.append(String.format("API Id: %s [%s] [%s] - %s\n", this.apiId, change.getLevel(), change.getCategory(), change.getReasonMsg())); + } + } +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncSchemaDiffUtil.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncSchemaDiffUtil.java new file mode 100644 index 00000000..5ba701c2 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/AppSyncSchemaDiffUtil.java @@ -0,0 +1,108 @@ +package com.awscommunity.appsync.breakingchangedetection.schema; + +import com.awscommunity.appsync.breakingchangedetection.model.aws.appsync.graphqlschema.AwsAppsyncGraphqlschema; + +import graphql.schema.GraphQLSchema; +import graphql.schema.diff.DiffSet; +import graphql.schema.idl.*; +import graphql.schema.diff.SchemaDiff; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; + +import java.util.Objects; + +import static com.awscommunity.appsync.breakingchangedetection.schema.AppSyncDirectives.APPSYNC_DIRECTIVE_DEFINITIONS; +import static com.awscommunity.appsync.breakingchangedetection.schema.AppSyncScalars.APPSYNC_SCALAR_DEFINITIONS; + +public class AppSyncSchemaDiffUtil { + + private static final String DEFINITION_S3_LOCATION = "DefinitionS3Location"; + + /** + * Diffs the previous AppSync schema with the new version and returns a reporter which breaks down the changes + * into different categories including breaking, dangerous, and info. + * @param previousResourceProperties Previous resource properties. + * @param resourceProperties Resource properties. + * @param proxy CloudFormation proxy client. + * @param logger Logger. + * @return AppSyncSchemaDiffReporter which captures the differents events of interest. + */ + public static AppSyncSchemaDiffReporter diffSchema(final AwsAppsyncGraphqlschema previousResourceProperties, + final AwsAppsyncGraphqlschema resourceProperties, + final AmazonWebServicesClientProxy proxy, + final Logger logger) { + final SchemaDiff schemaDiff = new SchemaDiff(SchemaDiff.Options.defaultOptions().enforceDirectives()); + final GraphQLSchema currentSchema = getGraphQLSchema(previousResourceProperties, proxy); + final GraphQLSchema newSchema = getGraphQLSchema(resourceProperties, proxy); + final DiffSet diffset = DiffSet.diffSet(currentSchema, newSchema); + + final AppSyncSchemaDiffReporter reporter = new AppSyncSchemaDiffReporter(logger, resourceProperties.getApiId()); + schemaDiff.diffSchema(diffset, reporter); + return reporter; + } + + /** + * This method gets the GraphQLSchema required for the SchemaDiff operation by loading the definitions, + * parsing them, and building the schema object. + * @param schemaConfig The schema definition to use for getting the SDL. + * @param proxy The CloudFormation proxy. + * @return + */ + private static GraphQLSchema getGraphQLSchema(final AwsAppsyncGraphqlschema schemaConfig, + final AmazonWebServicesClientProxy proxy) { + final String definition = getSdlString(schemaConfig, proxy); + final TypeDefinitionRegistry registry = parseSchema(definition); + return buildSchemaFromDefinitions(registry); + } + + /** + * This method is responsible for getting the schema SDL as a String. If the schema is defined directly in the Definition property, + * it simply return its contents. Otherwise, if the DefinitionS3Location property is used, it loads the schema from S3 and returns it + * as a String. + * @param schemaConfig The schema definition to use for getting the SDL. + * @param proxy The CloudFormation proxy. + * @return SDL String + */ + private static String getSdlString(final AwsAppsyncGraphqlschema schemaConfig, + final AmazonWebServicesClientProxy proxy) { + final String definition = Objects.toString(schemaConfig.get("Definition"), null); + if (definition != null) { + return definition; + } else { + final String s3Location = Objects.toString(schemaConfig.get(DEFINITION_S3_LOCATION), null); + if (null == s3Location) { + throw new IllegalArgumentException("No schema definition property found!"); + } + + final String s3Bucket = S3DefinitionUtil.getS3Bucket(s3Location, DEFINITION_S3_LOCATION); + final String s3Key = S3DefinitionUtil.getS3Key(s3Location, DEFINITION_S3_LOCATION); + return S3DefinitionUtil.readStringFromS3(proxy, s3Bucket, s3Key); + } + } + + /** + * Parse the SDL into a type definition registry. Note that we append the built in AppSync directive definitions. + * @param definition The SDL definition String. + * @return TypeDefinitionRegistry for use in generating the Schema object. + */ + private static TypeDefinitionRegistry parseSchema(final String definition) { + final SchemaParser schemaParser = new SchemaParser(); + final TypeDefinitionRegistry registry = schemaParser.parse(definition); + APPSYNC_DIRECTIVE_DEFINITIONS.forEach(registry::add); + APPSYNC_SCALAR_DEFINITIONS.forEach(registry::add); + return registry; + } + + /** + * Build the Schema object given the registry of type definitions. + * @param registry The type definition registry. + * @return GraphQLSchema + */ + private static GraphQLSchema buildSchemaFromDefinitions(final TypeDefinitionRegistry registry) { + final SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema( + SchemaGenerator.Options.defaultOptions(), + registry, + RuntimeWiring.MOCKED_WIRING); + } +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/S3DefinitionUtil.java b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/S3DefinitionUtil.java new file mode 100644 index 00000000..de2fbeb4 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/main/java/com/awscommunity/appsync/breakingchangedetection/schema/S3DefinitionUtil.java @@ -0,0 +1,85 @@ +package com.awscommunity.appsync.breakingchangedetection.schema; + +import com.awscommunity.appsync.breakingchangedetection.ClientBuilder; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; + +public class S3DefinitionUtil { + + /** + * The S3 path delimiter. + */ + public static final String S3_PATH_DELIMITER = "/"; + + /** + * The S3 min file length. + */ + public static final int S3_MIN_FILE_LENGTH = 3; + + /** + * Given a S3 original file path like "s3://bucket/folder/file", returns S3 + * compatible file key, and in this case it is "folder/file". + * + * @param path + * Raw file path + * @param fieldName + * The field we are trying to parse + * @return S3 compatible file key + */ + public static String getS3Key(final String path, final String fieldName) { + final String[] arr = StringUtils.splitByWholeSeparator(path, + S3_PATH_DELIMITER); + if (arr.length < S3_MIN_FILE_LENGTH) { + // invalid file location + throw new IllegalArgumentException("S3 location not valid for " + fieldName); + } + + // Remove the first segment "s3:" + final String[] pathArr = ArrayUtils.removeAll(arr, 0, 1); + final String key = StringUtils.join(pathArr, S3_PATH_DELIMITER); + return key; + } + + /** + * Given an original file path like "s3://bucket/folder/file", returns its + * bucket, and in this case it is "bucket". + * + * @param path + * Raw file path + * @param fieldName + * The field we are trying to parse + * @return bucket name + */ + public static String getS3Bucket(final String path, final String fieldName) { + final String[] arr = StringUtils.splitByWholeSeparator(path, + S3_PATH_DELIMITER); + if (arr.length < S3_MIN_FILE_LENGTH) { + throw new IllegalArgumentException("S3 location not valid for " + fieldName); + } + + final String bucketName = arr[1]; + return bucketName; + } + + /** + * Read a string from an s3 object at the given bucket and key. + * @param proxy Cloudformation Proxy. + * @param s3Bucket S3 bucket name. + * @param s3Key S3 Key name. + * @return String + */ + public static String readStringFromS3(final AmazonWebServicesClientProxy proxy, + final String s3Bucket, + final String s3Key) { + final S3Client s3Client = ClientBuilder.getS3Client(); + final GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(s3Bucket) + .key(s3Key) + .build(); + return proxy.injectCredentialsAndInvokeV2Bytes(getObjectRequest, s3Client::getObjectAsBytes) + .asUtf8String(); + } +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/resources/log4j2.xml b/hooks/AppSync_BreakingChangeDetection/src/resources/log4j2.xml new file mode 100644 index 00000000..8fbfe13f --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/java/com/awscommunity/appsync/breakingchangedetection/PreUpdateHookHandlerTest.java b/hooks/AppSync_BreakingChangeDetection/src/test/java/com/awscommunity/appsync/breakingchangedetection/PreUpdateHookHandlerTest.java new file mode 100644 index 00000000..3678c7a9 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/java/com/awscommunity/appsync/breakingchangedetection/PreUpdateHookHandlerTest.java @@ -0,0 +1,311 @@ +package com.awscommunity.appsync.breakingchangedetection; + +import com.amazonaws.util.StringInputStream; +import org.junit.jupiter.api.Assertions; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.cloudformation.exceptions.UnsupportedTargetException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.hook.HookContext; +import software.amazon.cloudformation.proxy.hook.HookHandlerRequest; +import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class PreUpdateHookHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @BeforeEach + public void setup() { + proxy = mock(AmazonWebServicesClientProxy.class); + logger = mock(Logger.class); + } + + @Test + public void handleRequest_UnsupportedTarget() throws IOException { + final HookHandlerRequest request = HookHandlerRequest.builder() + .hookContext(HookContext.builder().targetName("AWS::AppSync::GraphqlApi").targetModel(HookTargetModel.of(new HashMap<>())).build()) + .build(); + + Assertions.assertThrows(UnsupportedTargetException.class, () -> { + new PreUpdateHookHandler().handleRequest(proxy, request, null, logger, + TypeConfigurationModel.builder().build()); + }); + } + + @Test + public void handleRequest_Success() throws IOException { + final ProgressEvent response = executeSchemaDiffHook( + "original-schema.graphql", + "schema-update-non-breaking.graphql"); + + final String expectedMessage = "Successfully verified there are no breaking changes for AWS::AppSync::GraphQLSchema.\n" + + "API Id: 1 [INFO] [ADDITION] - The new API adds the field 'test.name'\n" + + "API Id: 1 [INFO] [ADDITION] - The new API adds the field 'Query.getTest'\n"; + assertResponse(response, OperationStatus.SUCCESS, expectedMessage, null); + } + + @Test + public void handleRequest_Breaking() throws IOException { + final ProgressEvent response = executeSchemaDiffHook( + "original-schema.graphql", + "schema-update-breaking.graphql", + false, + true); + + final String expectedMessage = "Breaking changes have been detected for this AWS::AppSync::GraphQLSchema:\n" + + "API Id: 1 [BREAKING] [INVALID] - The new API has changed field 'test.version' from type 'String!' to 'ID!'\n"; + + assertResponse(response, OperationStatus.FAILED, expectedMessage, HandlerErrorCode.NonCompliant); + } + + @Test + public void handleRequest_Dangerous() throws IOException { + final ProgressEvent response = executeSchemaDiffHook( + "original-schema.graphql", + "schema-update-dangerous.graphql"); + + final String expectedMessage = "Successfully verified there are no breaking changes for AWS::AppSync::GraphQLSchema.\n" + + "API Id: 1 [DANGEROUS] [ADDITION] - The new API has added a new enum value 'COMPOUND'\n"; + + assertResponse(response, OperationStatus.SUCCESS, expectedMessage, null); + } + + @Test + public void handleRequest_DangerousBreaking() throws IOException { + final ProgressEvent response = executeSchemaDiffHook( + "original-schema.graphql", + "schema-update-dangerous.graphql", + false, + true); + + final String expectedMessage = "Breaking changes have been detected for this AWS::AppSync::GraphQLSchema:\n" + + "API Id: 1 [DANGEROUS] [ADDITION] - The new API has added a new enum value 'COMPOUND'\n"; + + assertResponse(response, OperationStatus.FAILED, expectedMessage, HandlerErrorCode.NonCompliant); + } + + @Test + public void handleRequest_FailedToParse() throws IOException { + final ProgressEvent response = executeSchemaDiffHook( + "original-schema.graphql", + "schema-update-additional-sdl.graphql"); + + final String expectedMessage = "Unable to parse the new schema with the following errors: " + + "['additionalField' [@4:3] tried to use an undeclared directive 'newDirective']"; + assertResponse(response, OperationStatus.FAILED, expectedMessage, HandlerErrorCode.InvalidRequest); + } + + @Test + public void handleRequest_BuiltInTypes() throws IOException { + final ProgressEvent response = executeSchemaDiffHook( + "schema-with-appsync-built-in-defs.graphql", + "schema-with-appsync-built-in-defs.graphql"); + + final String expectedMessage = "Successfully verified there are no breaking changes for AWS::AppSync::GraphQLSchema.\n"; + assertResponse(response, OperationStatus.SUCCESS, expectedMessage, null); + } + + @SuppressWarnings("unchecked") + @Test + public void handleRequest_GetDefinitionFromS3Succeeds() throws IOException { + final GetObjectRequest originalRequest = GetObjectRequest.builder() + .key("original-schema.graphql") + .bucket("schema") + .build(); + + final GetObjectRequest updateRequest = GetObjectRequest.builder() + .key("schema-update-non-breaking.graphql") + .bucket("schema") + .build(); + + GetObjectResponse getObjectResponse = GetObjectResponse.builder().build(); + ResponseBytes originalResponse = ResponseBytes.fromInputStream(getObjectResponse, + new StringInputStream(getDefinition(originalRequest.key()))); + + ResponseBytes updateResponse = ResponseBytes.fromInputStream(getObjectResponse, + new StringInputStream(getDefinition(updateRequest.key()))); + + when(proxy.injectCredentialsAndInvokeV2Bytes(eq(originalRequest), any())).thenReturn(originalResponse); + when(proxy.injectCredentialsAndInvokeV2Bytes(eq(updateRequest), any())).thenReturn(updateResponse); + + final ProgressEvent response = executeSchemaDiffHook( + "s3://schema/original-schema.graphql", + "s3://schema/schema-update-non-breaking.graphql", + true, + false); + + final String expectedMessage = "Successfully verified there are no breaking changes for AWS::AppSync::GraphQLSchema.\n" + + "API Id: 1 [INFO] [ADDITION] - The new API adds the field 'test.name'\n" + + "API Id: 1 [INFO] [ADDITION] - The new API adds the field 'Query.getTest'\n"; + + assertResponse(response, OperationStatus.SUCCESS, expectedMessage, null); + verify(proxy, times(1)).injectCredentialsAndInvokeV2Bytes(eq(originalRequest), any()); + verify(proxy, times(1)).injectCredentialsAndInvokeV2Bytes(eq(updateRequest), any()); + } + + @Test + public void handleRequest_GetDefinitionFromS3Fails() throws IOException { + when(proxy.injectCredentialsAndInvokeV2Bytes(any(), any())).thenThrow(S3Exception.builder().statusCode(503) + .message("Test exception").build()); + + final ProgressEvent response = executeSchemaDiffHook( + "s3://schema/original-schema.graphql", + "s3://schema/schema-update-non-breaking.graphql", + true, + false); + + final String expectedMessage = "An unexpected error occurred validating the schema: " + + "software.amazon.awssdk.services.s3.model.S3Exception: Test exception"; + + assertResponse(response, OperationStatus.FAILED, expectedMessage, HandlerErrorCode.HandlerInternalFailure); + verify(proxy, times(1)).injectCredentialsAndInvokeV2Bytes(any(), any()); + } + + @Test() + public void handleRequest_invalidDefinition() { + final PreUpdateHookHandler handler = new PreUpdateHookHandler(); + final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder() + .considerDangerousChangesBreaking(false) + .build(); + + final Map targetModel = new HashMap<>(); + + final Map previousResourceProperties = new HashMap<>(); + previousResourceProperties.put("ApiId", "1"); + + final Map resourceProperties = new HashMap<>(); + resourceProperties.put("ApiId", "1"); + + targetModel.put("ResourceProperties", resourceProperties); + targetModel.put("PreviousResourceProperties", previousResourceProperties); + + final HookTargetModel model = HookTargetModel.of(targetModel); + + final HookHandlerRequest request = HookHandlerRequest.builder() + .hookContext(HookContext.builder().targetName("AWS::AppSync::GraphQLSchema").targetModel(model).build()) + .build(); + + final ProgressEvent response = + handler.handleRequest(proxy, request, null, logger, null); + + final String expectedMessage = "An unexpected error occurred validating the schema: " + + "java.lang.IllegalArgumentException: No schema definition property found!"; + assertResponse(response, OperationStatus.FAILED, expectedMessage, HandlerErrorCode.HandlerInternalFailure); + verify(proxy, never()).injectCredentialsAndInvokeV2Bytes(any(), any()); + } + + @Test() + public void handleRequest_invalidS3Definition() throws IOException { + final ProgressEvent response = executeSchemaDiffHook( + "s1", + "s2", + true, + false); + + final String expectedMessage = "An unexpected error occurred validating the schema: " + + "java.lang.IllegalArgumentException: S3 location not valid for DefinitionS3Location"; + + assertResponse(response, OperationStatus.FAILED, expectedMessage, HandlerErrorCode.HandlerInternalFailure); + verify(proxy, never()).injectCredentialsAndInvokeV2Bytes(any(), any()); + } + + private ProgressEvent executeSchemaDiffHook(final String oldSchemaDefinitionLocation, + final String newSchemaDefinitionLocation) throws IOException { + return executeSchemaDiffHook(oldSchemaDefinitionLocation, newSchemaDefinitionLocation, false, false); + } + + private ProgressEvent executeSchemaDiffHook(final String oldSchemaDefinitionLocation, + final String newSchemaDefinitionLocation, + boolean useS3, + boolean considerDangerousChangesBreaking) throws IOException { + final PreUpdateHookHandler handler = new PreUpdateHookHandler(); + final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder() + .considerDangerousChangesBreaking(considerDangerousChangesBreaking) + .build(); + + final HookTargetModel targetModel = getTargetModel( + useS3 ? oldSchemaDefinitionLocation : getDefinition(oldSchemaDefinitionLocation), + useS3 ? newSchemaDefinitionLocation : getDefinition(newSchemaDefinitionLocation), + useS3); + + final HookHandlerRequest request = HookHandlerRequest.builder() + .hookContext(HookContext.builder().targetName("AWS::AppSync::GraphQLSchema").targetModel(HookTargetModel.of(targetModel)).build()) + .build(); + + return handler.handleRequest(proxy, request, null, logger, typeConfiguration); + } + + private void assertResponse(final ProgressEvent response, + final OperationStatus expectedStatus, + final String expectedMsg, + final HandlerErrorCode errorCode) { + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(expectedStatus); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getMessage()).isEqualTo(expectedMsg); + assertThat(response.getErrorCode()).isEqualTo(errorCode); + } + + private String getDefinition(final String fileName) { + final ClassLoader classLoader = getClass().getClassLoader(); + final InputStream inputStream = classLoader.getResourceAsStream(fileName); + try { + return IOUtils.toString(inputStream, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private HookTargetModel getTargetModel(final String previousSDL, final String newSdl, boolean useS3) { + final Map targetModel = new HashMap<>(); + final String definitionProperty = useS3 ? "DefinitionS3Location" : "Definition"; + + final Map previousResourceProperties = new HashMap<>(); + previousResourceProperties.put(definitionProperty, previousSDL); + previousResourceProperties.put("ApiId", "1"); + + final Map resourceProperties = new HashMap<>(); + resourceProperties.put(definitionProperty, newSdl); + resourceProperties.put("ApiId", "1"); + + + targetModel.put("ResourceProperties", resourceProperties); + targetModel.put("PreviousResourceProperties", previousResourceProperties); + return HookTargetModel.of(targetModel); + } +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/java/com/awscommunity/appsync/breakingchangedetection/S3DefinitionUtilTest.java b/hooks/AppSync_BreakingChangeDetection/src/test/java/com/awscommunity/appsync/breakingchangedetection/S3DefinitionUtilTest.java new file mode 100644 index 00000000..c39513a4 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/java/com/awscommunity/appsync/breakingchangedetection/S3DefinitionUtilTest.java @@ -0,0 +1,32 @@ +package com.awscommunity.appsync.breakingchangedetection; + +import com.awscommunity.appsync.breakingchangedetection.schema.S3DefinitionUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class S3DefinitionUtilTest { + + @Test + public void testValidS3BucketLocation() { + Assertions.assertEquals("schema", S3DefinitionUtil.getS3Bucket("s3://schema/original-schema.graphql", + "DefinitionS3Location")); + } + + @Test + public void testValidS3KeyLocation() { + Assertions.assertEquals("original-schema.graphql", S3DefinitionUtil.getS3Key("s3://schema/original-schema.graphql", + "DefinitionS3Location")); + } + + @Test + public void testInvalidS3Key() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + S3DefinitionUtil.getS3Key("s3://", "DefinitionS3Location")); + } + + @Test + public void testInvalidS3Bucket() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + S3DefinitionUtil.getS3Bucket("s3://", "DefinitionS3Location")); + } +} diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/resources/additionalSdl.graphql b/hooks/AppSync_BreakingChangeDetection/src/test/resources/additionalSdl.graphql new file mode 100644 index 00000000..ffaf1b20 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/resources/additionalSdl.graphql @@ -0,0 +1 @@ +directive @newDirective on FIELD_DEFINITION \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/resources/original-schema.graphql b/hooks/AppSync_BreakingChangeDetection/src/test/resources/original-schema.graphql new file mode 100644 index 00000000..e88b14f6 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/resources/original-schema.graphql @@ -0,0 +1,17 @@ +type test { + version: String! + type: TestType +} + +type Query { + getTests: [test]! +} + +type Mutation { + addTest(version: String!): test +} + +enum TestType { + SIMPLE, + COMPLEX +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-additional-sdl.graphql b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-additional-sdl.graphql new file mode 100644 index 00000000..2d20aa92 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-additional-sdl.graphql @@ -0,0 +1,18 @@ +type test { + version: String! + type: TestType + additionalField: ID! @newDirective +} + +type Query { + getTests: [test]! +} + +type Mutation { + addTest(version: String!): test +} + +enum TestType { + SIMPLE, + COMPLEX +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-breaking.graphql b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-breaking.graphql new file mode 100644 index 00000000..14322032 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-breaking.graphql @@ -0,0 +1,17 @@ +type test { + version: ID! + type: TestType +} + +type Query { + getTests: [test]! +} + +type Mutation { + addTest(version: String!): test +} + +enum TestType { + SIMPLE, + COMPLEX +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-dangerous.graphql b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-dangerous.graphql new file mode 100644 index 00000000..7a000a31 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-dangerous.graphql @@ -0,0 +1,18 @@ +type test { + version: String! + type: TestType +} + +type Query { + getTests: [test]! +} + +type Mutation { + addTest(version: String!): test +} + +enum TestType { + SIMPLE, + COMPLEX, + COMPOUND +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-non-breaking.graphql b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-non-breaking.graphql new file mode 100644 index 00000000..0c7f5172 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-update-non-breaking.graphql @@ -0,0 +1,19 @@ +type test { + version: String! + type: TestType + name: String +} + +type Query { + getTests: [test]! + getTest(version: String): test +} + +type Mutation { + addTest(version: String!): test +} + +enum TestType { + SIMPLE, + COMPLEX +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-with-appsync-built-in-defs.graphql b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-with-appsync-built-in-defs.graphql new file mode 100644 index 00000000..9c643576 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/src/test/resources/schema-with-appsync-built-in-defs.graphql @@ -0,0 +1,32 @@ +directive @someDirective on FIELD_DEFINITION + +type Test @aws_lambda @aws_api_key @aws_oidc @aws_iam @aws_cognito_user_pools { + id: ID! @canonical + date: AWSDate @renamed(to: "w") + time: AWSTime @hidden + dateTime: AWSDateTime + json: AWSJSON + phone: AWSPhone + url: AWSURL, + ip: AWSIPAddress +} + +type Query { + getTests: [Test]! @aws_cognito_user_pools(cognito_groups: ["Readers"]) +} + +type Mutation { + addTest(version: String!): Test + @aws_publish(subscriptions: "onTestAdded") + @aws_auth(cognito_groups: ["Admin"]) +} + +type Subscription { + onTestAdded: Test + @aws_subscribe(mutations: ["addTest"]) +} + +enum TestType { + SIMPLE, + COMPLEX +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/template.yml b/hooks/AppSync_BreakingChangeDetection/template.yml new file mode 100644 index 00000000..9bab7d65 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/template.yml @@ -0,0 +1,24 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWSSamples::AppSyncSchemaBreakingChanges::Hook resource type + +Globals: + Function: + Timeout: 300 # docker start-up times can be long for SAM CLI + MemorySize: 1024 + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.awscommunity.appsync.breakingchangedetection.HookHandlerWrapper::handleRequest + Runtime: java11 + CodeUri: ./target/awscommunity-appsync-breakingchangedetection-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: com.awssamples.appsyncschemabreakingchanges.HookHandlerWrapper::testEntrypoint + Runtime: java11 + CodeUri: ./target/awscommunity-appsync-breakingchangedetection-handler-1.0-SNAPSHOT.jar + diff --git a/hooks/AppSync_BreakingChangeDetection/test/configuration.json b/hooks/AppSync_BreakingChangeDetection/test/configuration.json new file mode 100644 index 00000000..1e2060be --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/test/configuration.json @@ -0,0 +1,9 @@ +{ + "CloudFormationConfiguration": { + "HookConfiguration": { + "TargetStacks": "ALL", + "FailureMode": "FAIL", + "Properties": {} + } + } +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/test/configuration_undo.json b/hooks/AppSync_BreakingChangeDetection/test/configuration_undo.json new file mode 100644 index 00000000..69a252b8 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/test/configuration_undo.json @@ -0,0 +1,9 @@ +{ + "CloudFormationConfiguration": { + "HookConfiguration": { + "TargetStacks": "NONE", + "FailureMode": "WARN", + "Properties": {} + } + } +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/test/setup.json b/hooks/AppSync_BreakingChangeDetection/test/setup.json new file mode 100644 index 00000000..22468599 --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/test/setup.json @@ -0,0 +1,23 @@ +{ + "Resources": { + "BasicGraphQLApi": { + "Type": "AWS::AppSync::GraphQLApi", + "Properties": { + "Name": "TestAPI with IAM and a NONE datasource.", + "AuthenticationType": "AWS_IAM" + } + }, + "BasicSchema": { + "Type": "AWS::AppSync::GraphQLSchema", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "BasicGraphQLApi", + "ApiId" + ] + }, + "Definition": "schema {\nquery: Query\nmutation: Mutation\n }\n\n type Query {\n singlePost(id: ID!): Post\n }\n\n type Mutation {\n putPost(id: ID!, title: String!): Post\n }\n\n type Post {\n id: ID!\n title: String!\n }" + } + } + } +} \ No newline at end of file diff --git a/hooks/AppSync_BreakingChangeDetection/typeConfiguration.json b/hooks/AppSync_BreakingChangeDetection/typeConfiguration.json new file mode 100644 index 00000000..f08df99b --- /dev/null +++ b/hooks/AppSync_BreakingChangeDetection/typeConfiguration.json @@ -0,0 +1,11 @@ +{ + "CloudFormationConfiguration": { + "HookConfiguration": { + "TargetStacks": "ALL", + "FailureMode": "FAIL", + "Properties": { + "ConsiderDangerousChangesBreaking": false + } + } + } +} \ No newline at end of file diff --git a/hooks/alpha-buildspec-java.yml b/hooks/alpha-buildspec-java.yml index d06b04dc..50995d77 100644 --- a/hooks/alpha-buildspec-java.yml +++ b/hooks/alpha-buildspec-java.yml @@ -4,7 +4,7 @@ phases: install: runtime-versions: python: 3.7 - java: corretto8 + java: corretto11 commands: - echo Entered the install phase... - echo About to build $HOOK_PATH diff --git a/hooks/beta-buildspec-java.yml b/hooks/beta-buildspec-java.yml index e1c3d1c0..1384b992 100644 --- a/hooks/beta-buildspec-java.yml +++ b/hooks/beta-buildspec-java.yml @@ -4,7 +4,7 @@ phases: install: runtime-versions: python: 3.7 - java: corretto8 + java: corretto11 commands: - echo Entered the install phase... - echo About to build $HOOK_PATH diff --git a/hooks/prod-buildspec-java.yml b/hooks/prod-buildspec-java.yml index d819075f..d83c5f3d 100644 --- a/hooks/prod-buildspec-java.yml +++ b/hooks/prod-buildspec-java.yml @@ -4,7 +4,7 @@ phases: install: runtime-versions: python: 3.7 - java: corretto8 + java: corretto11 commands: - echo Entered the install phase... - echo About to build $HOOK_PATH