From f0af5b1b1551e03198098610f0377af11447e098 Mon Sep 17 00:00:00 2001 From: "k.goto" <24818752+go-to-k@users.noreply.github.com> Date: Sat, 24 Feb 2024 01:35:25 +0900 Subject: [PATCH] feat(appsync): `environmentVariables` property for GraphqlApi (#29064) ### Reason for this change AppSync now supports environment variables in GraphQL resolvers and functions. It would be good for `GraphqlApi` construct to have the property. - https://aws.amazon.com/jp/about-aws/whats-new/2024/02/aws-appsync-environment-variables-graph-ql-resolvers-functions/ - https://docs.aws.amazon.com/en_en/appsync/latest/devguide/environmental-variables.html - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-graphqlapi.html#cfn-appsync-graphqlapi-environmentvariables ### Description of changes The `environmentVariables` property is added to `GraphqlApi` construct. To add environment variables after the initiation, we can use `addEnvironmentVariables` method. ### Description of how you validated changes Both unit and integ tests. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../AppSyncEnvironmentVariables.assets.json | 19 ++ .../AppSyncEnvironmentVariables.template.json | 75 +++++ ...efaultTestDeployAssertCD36F8FC.assets.json | 19 ++ ...aultTestDeployAssertCD36F8FC.template.json | 36 +++ .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 125 ++++++++ .../tree.json | 177 +++++++++++ .../test/integ.environment-variables.ts | 21 ++ packages/aws-cdk-lib/aws-appsync/README.md | 19 +- .../aws-cdk-lib/aws-appsync/lib/graphqlapi.ts | 58 +++- .../appsync-environment-variables.test.ts | 289 ++++++++++++++++++ 12 files changed, 849 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/AppSyncEnvironmentVariables.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/AppSyncEnvironmentVariables.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.ts create mode 100644 packages/aws-cdk-lib/aws-appsync/test/appsync-environment-variables.test.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/AppSyncEnvironmentVariables.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/AppSyncEnvironmentVariables.assets.json new file mode 100644 index 0000000000000..453b43cf3a5ae --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/AppSyncEnvironmentVariables.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "ba980a08a405c501c84615d3ef6311627a0b3662fc43c917e9b52e1c0cc0db59": { + "source": { + "path": "AppSyncEnvironmentVariables.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "ba980a08a405c501c84615d3ef6311627a0b3662fc43c917e9b52e1c0cc0db59.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/AppSyncEnvironmentVariables.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/AppSyncEnvironmentVariables.template.json new file mode 100644 index 0000000000000..e5b312da4d4f2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/AppSyncEnvironmentVariables.template.json @@ -0,0 +1,75 @@ +{ + "Resources": { + "ApiF70053CD": { + "Type": "AWS::AppSync::GraphQLApi", + "Properties": { + "AuthenticationType": "API_KEY", + "EnvironmentVariables": { + "EnvKey1": "non-empty-1", + "EnvKey2": "non-empty-2" + }, + "Name": "Api" + } + }, + "ApiSchema510EECD7": { + "Type": "AWS::AppSync::GraphQLSchema", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "Definition": "type Test {\n id: String!\n name: String!\n}\ntype Query {\n getTests: [Test]!\n}\ntype Mutation {\n addTest(name: String!): Test\n}\n" + } + }, + "ApiDefaultApiKeyF991C37B": { + "Type": "AWS::AppSync::ApiKey", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + } + }, + "DependsOn": [ + "ApiSchema510EECD7" + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.assets.json new file mode 100644 index 0000000000000..d7e31361227a1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/integ.json new file mode 100644 index 0000000000000..d5fe84e256e52 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "IntegTestEnvironmentVariables/DefaultTest": { + "stacks": [ + "AppSyncEnvironmentVariables" + ], + "assertionStack": "IntegTestEnvironmentVariables/DefaultTest/DeployAssert", + "assertionStackName": "IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/manifest.json new file mode 100644 index 0000000000000..c040a334b1f13 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/manifest.json @@ -0,0 +1,125 @@ +{ + "version": "36.0.0", + "artifacts": { + "AppSyncEnvironmentVariables.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "AppSyncEnvironmentVariables.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "AppSyncEnvironmentVariables": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "AppSyncEnvironmentVariables.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ba980a08a405c501c84615d3ef6311627a0b3662fc43c917e9b52e1c0cc0db59.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "AppSyncEnvironmentVariables.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "AppSyncEnvironmentVariables.assets" + ], + "metadata": { + "/AppSyncEnvironmentVariables/Api/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ApiF70053CD" + } + ], + "/AppSyncEnvironmentVariables/Api/Schema": [ + { + "type": "aws:cdk:logicalId", + "data": "ApiSchema510EECD7" + } + ], + "/AppSyncEnvironmentVariables/Api/DefaultApiKey": [ + { + "type": "aws:cdk:logicalId", + "data": "ApiDefaultApiKeyF991C37B" + } + ], + "/AppSyncEnvironmentVariables/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/AppSyncEnvironmentVariables/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "AppSyncEnvironmentVariables" + }, + "IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "IntegTestEnvironmentVariablesDefaultTestDeployAssertCD36F8FC.assets" + ], + "metadata": { + "/IntegTestEnvironmentVariables/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/IntegTestEnvironmentVariables/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "IntegTestEnvironmentVariables/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/tree.json new file mode 100644 index 0000000000000..ca72042f15bde --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.js.snapshot/tree.json @@ -0,0 +1,177 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "AppSyncEnvironmentVariables": { + "id": "AppSyncEnvironmentVariables", + "path": "AppSyncEnvironmentVariables", + "children": { + "Api": { + "id": "Api", + "path": "AppSyncEnvironmentVariables/Api", + "children": { + "Resource": { + "id": "Resource", + "path": "AppSyncEnvironmentVariables/Api/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppSync::GraphQLApi", + "aws:cdk:cloudformation:props": { + "authenticationType": "API_KEY", + "environmentVariables": { + "EnvKey1": "non-empty-1", + "EnvKey2": "non-empty-2" + }, + "name": "Api" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Schema": { + "id": "Schema", + "path": "AppSyncEnvironmentVariables/Api/Schema", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppSync::GraphQLSchema", + "aws:cdk:cloudformation:props": { + "apiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "definition": "type Test {\n id: String!\n name: String!\n}\ntype Query {\n getTests: [Test]!\n}\ntype Mutation {\n addTest(name: String!): Test\n}\n" + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DefaultApiKey": { + "id": "DefaultApiKey", + "path": "AppSyncEnvironmentVariables/Api/DefaultApiKey", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::AppSync::ApiKey", + "aws:cdk:cloudformation:props": { + "apiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "LogGroup": { + "id": "LogGroup", + "path": "AppSyncEnvironmentVariables/Api/LogGroup", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "AppSyncEnvironmentVariables/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "AppSyncEnvironmentVariables/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "IntegTestEnvironmentVariables": { + "id": "IntegTestEnvironmentVariables", + "path": "IntegTestEnvironmentVariables", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "IntegTestEnvironmentVariables/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "IntegTestEnvironmentVariables/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "IntegTestEnvironmentVariables/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "IntegTestEnvironmentVariables/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "IntegTestEnvironmentVariables/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.ts new file mode 100644 index 0000000000000..5248720bf56f2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.environment-variables.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import * as cdk from 'aws-cdk-lib'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import * as appsync from 'aws-cdk-lib/aws-appsync'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'AppSyncEnvironmentVariables'); + +const api = new appsync.GraphqlApi(stack, 'Api', { + name: 'Api', + schema: appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.js-resolver.graphql')), + environmentVariables: { + EnvKey1: 'non-empty-1', + }, +}); + +api.addEnvironmentVariable('EnvKey2', 'non-empty-2'); + +new IntegTest(app, 'IntegTestEnvironmentVariables', { testCases: [stack] }); + +app.synth(); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-appsync/README.md b/packages/aws-cdk-lib/aws-appsync/README.md index a496118c76e90..d24828f11ff80 100644 --- a/packages/aws-cdk-lib/aws-appsync/README.md +++ b/packages/aws-cdk-lib/aws-appsync/README.md @@ -755,4 +755,21 @@ const api = new appsync.GraphqlApi(this, 'api', { definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.schema.graphql')), introspectionConfig: appsync.IntrospectionConfig.DISABLED, }); -``` \ No newline at end of file +``` + +## Environment Variables + +To use environment variables in resolvers, you can use the `environmentVariables` property and +the `addEnvironmentVariable` method. + +```ts +const api = new appsync.GraphqlApi(this, 'api', { + name: 'api', + definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.schema.graphql')), + environmentVariables: { + EnvKey1: 'non-empty-1', + }, +}); + +api.addEnvironmentVariable('EnvKey2', 'non-empty-2'); +``` diff --git a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts index 73ea5ff48756b..04b6ed99b793d 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi.ts @@ -8,7 +8,7 @@ import { IUserPool } from '../../aws-cognito'; import { ManagedPolicy, Role, IRole, ServicePrincipal, Grant, IGrantable } from '../../aws-iam'; import { IFunction } from '../../aws-lambda'; import { ILogGroup, LogGroup, LogRetention, RetentionDays } from '../../aws-logs'; -import { ArnFormat, CfnResource, Duration, Expiration, FeatureFlags, IResolvable, Stack } from '../../core'; +import { ArnFormat, CfnResource, Duration, Expiration, FeatureFlags, IResolvable, Lazy, Stack, Token } from '../../core'; import * as cxapi from '../../cx-api'; /** @@ -442,6 +442,20 @@ export interface GraphqlApiProps { * @default IntrospectionConfig.ENABLED */ readonly introspectionConfig?: IntrospectionConfig; + + /** + * A map containing the list of resources with their properties and environment variables. + * + * There are a few rules you must follow when creating keys and values: + * - Keys must begin with a letter. + * - Keys must be between 2 and 64 characters long. + * - Keys can only contain letters, numbers, and the underscore character (_). + * - Values can be up to 512 characters long. + * - You can configure up to 50 key-value pairs in a GraphQL API. + * + * @default - No environment variables. + */ + readonly environmentVariables?: { [key: string]: string }; } /** @@ -619,6 +633,7 @@ export class GraphqlApi extends GraphqlApiBase { private apiKeyResource?: CfnApiKey; private domainNameResource?: CfnDomainName; private mergedApiExecutionRole?: IRole; + private environmentVariables: { [key: string]: string } = {}; constructor(scope: Construct, id: string, props: GraphqlApiProps) { super(scope, id); @@ -644,6 +659,13 @@ export class GraphqlApi extends GraphqlApiBase { this.setupMergedApiExecutionRole(this.definition.sourceApiOptions); } + if (props.environmentVariables !== undefined) { + Object.entries(props.environmentVariables).forEach(([key, value]) => { + this.addEnvironmentVariable(key, value); + }); + } + this.node.addValidation({ validate: () => this.validateEnvironmentVariables() }); + this.api = new CfnGraphQLApi(this, 'Resource', { name: props.name, authenticationType: defaultMode.authorizationType, @@ -657,6 +679,7 @@ export class GraphqlApi extends GraphqlApiBase { mergedApiExecutionRoleArn: this.mergedApiExecutionRole?.roleArn, apiType: this.definition.sourceApiOptions ? 'MERGED' : undefined, introspectionConfig: props.introspectionConfig, + environmentVariables: Lazy.any({ produce: () => this.renderEnvironmentVariables() }), }); this.apiId = this.api.attrApiId; @@ -847,6 +870,39 @@ export class GraphqlApi extends GraphqlApiBase { return true; } + /** + * Add an environment variable to the construct. + */ + public addEnvironmentVariable(key: string, value: string) { + if (this.definition.sourceApiOptions) { + throw new Error('Environment variables are not supported for merged APIs'); + } + if (!Token.isUnresolved(key) && !/^[A-Za-z]+\w*$/.test(key)) { + throw new Error(`Key '${key}' must begin with a letter and can only contain letters, numbers, and underscores`); + } + if (!Token.isUnresolved(key) && (key.length < 2 || key.length > 64)) { + throw new Error(`Key '${key}' must be between 2 and 64 characters long, got ${key.length}`); + } + if (!Token.isUnresolved(value) && value.length > 512) { + throw new Error(`Value for '${key}' is too long. Values can be up to 512 characters long, got ${value.length}`); + } + + this.environmentVariables[key] = value; + } + + private validateEnvironmentVariables() { + const errors: string[] = []; + const entries = Object.entries(this.environmentVariables); + if (entries.length > 50) { + errors.push(`Only 50 environment variables can be set, got ${entries.length}`); + } + return errors; + } + + private renderEnvironmentVariables() { + return Object.entries(this.environmentVariables).length > 0 ? this.environmentVariables : undefined; + } + private setupLogConfig(config?: LogConfig) { if (!config) return undefined; const logsRoleArn: string = config.role?.roleArn ?? new Role(this, 'ApiLogsRole', { diff --git a/packages/aws-cdk-lib/aws-appsync/test/appsync-environment-variables.test.ts b/packages/aws-cdk-lib/aws-appsync/test/appsync-environment-variables.test.ts new file mode 100644 index 0000000000000..d5812dcc25cc2 --- /dev/null +++ b/packages/aws-cdk-lib/aws-appsync/test/appsync-environment-variables.test.ts @@ -0,0 +1,289 @@ +import * as path from 'path'; +import { IConstruct } from 'constructs'; +import { Match, Template } from '../../assertions'; +import * as cdk from '../../core'; +import * as appsync from '../lib'; + +let stack: cdk.Stack; +beforeEach(() => { + stack = new cdk.Stack(); +}); + +describe('environment variables', () => { + test('can set environment variables', () => { + // WHEN + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + environmentVariables: { + EnvKey1: 'non-empty-1', + EnvKey2: 'non-empty-2', + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLApi', { + EnvironmentVariables: { + EnvKey1: 'non-empty-1', + EnvKey2: 'non-empty-2', + }, + }); + }); + + test('can set environment variables by addEnvironmentVariable method', () => { + // WHEN + const api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + api.addEnvironmentVariable('EnvKey1', 'non-empty-1'); + api.addEnvironmentVariable('EnvKey2', 'non-empty-2'); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLApi', { + EnvironmentVariables: { + EnvKey1: 'non-empty-1', + EnvKey2: 'non-empty-2', + }, + }); + }); + + test('can set to undefined if environment variables is not specified', () => { + // WHEN + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::AppSync::GraphQLApi', { + EnvironmentVariables: Match.absent(), + }); + }); + + test('throws if environment variables key does not begin with a letter', () => { + expect(() => { + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + environmentVariables: { + '1EnvKey': 'non-empty-1', + }, + }); + }).toThrow(/Key '1EnvKey' must begin with a letter and can only contain letters, numbers, and underscores/); + }); + + test('throws if environment variables key by addEnvironmentVariable method does not begin with a letter', () => { + // WHEN + const api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + // THEN + expect(() => { + api.addEnvironmentVariable('1EnvKey', 'non-empty-1'); + }).toThrow(/Key '1EnvKey' must begin with a letter and can only contain letters, numbers, and underscores/); + }); + + test('throws if environment variables key is less than 2 characters long', () => { + expect(() => { + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + environmentVariables: { + a: 'non-empty-1', + }, + }); + }).toThrow(/Key 'a' must be between 2 and 64 characters long, got 1/); + }); + + test('throws if environment variables key by addEnvironmentVariable method is less than 2 characters long', () => { + // WHEN + const api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + // THEN + expect(() => { + api.addEnvironmentVariable('a', 'non-empty-1'); + }).toThrow(/Key 'a' must be between 2 and 64 characters long, got 1/); + }); + + test('throws if environment variables key is greater than 64 characters long', () => { + expect(() => { + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + environmentVariables: { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: 'non-empty-1', + }, + }); + }).toThrow(/Key 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' must be between 2 and 64 characters long, got 65/); + }); + + test('throws if environment variables key by addEnvironmentVariable method is greater than 64 characters long', () => { + // WHEN + const api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + // THEN + expect(() => { + api.addEnvironmentVariable('a'.repeat(65), 'non-empty-1'); + }).toThrow(/Key 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' must be between 2 and 64 characters long, got 65/); + }); + + test('throws if environment variables key contains invalid characters', () => { + expect(() => { + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + environmentVariables: { + '1|2|3': 'non-empty-1', + }, + }); + }).toThrow(/Key '1\|2\|3' must begin with a letter and can only contain letters, numbers, and underscores/); + }); + + test('throws if environment variables key by addEnvironmentVariable method contains invalid characters', () => { + // WHEN + const api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + // THEN + expect(() => { + api.addEnvironmentVariable('1|2|3', 'non-empty-1'); + }).toThrow(/Key '1\|2\|3' must begin with a letter and can only contain letters, numbers, and underscores/); + }); + + test('throws if length of environment variables value is greater than 512', () => { + expect(() => { + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + environmentVariables: { + EnvKey1: 'a'.repeat(513), + }, + }); + }).toThrow(/Value for 'EnvKey1' is too long. Values can be up to 512 characters long, got 513/); + }); + + test('throws if length of environment variables value by addEnvironmentVariable method is greater than 512', () => { + // WHEN + const api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + // THEN + expect(() => { + api.addEnvironmentVariable('EnvKey1', 'a'.repeat(513)); + }).toThrow(/Value for 'EnvKey1' is too long. Values can be up to 512 characters long, got 513/); + }); + + test('throws if length of key-value pairs for environment variables is greater than 50', () => { + // WHEN + const vars = {}; + for (let i = 0; i < 51; i++) { + vars[`EnvKey${i}`] = `non-empty-${i}`; + } + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + environmentVariables: vars, + }); + + const errors = validate(stack); + expect(errors.length).toEqual(1); + const error = errors[0]; + + // THEN + expect(error).toMatch(/Only 50 environment variables can be set, got 51/); + }); + + test('throws if length of key-value pairs for environment variables by addEnvironmentVariable method is greater than 50', () => { + // WHEN + const api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + for (let i = 0; i < 51; i++) { + api.addEnvironmentVariable(`EnvKey${i}`, `non-empty-${i}`); + } + + const errors = validate(stack); + expect(errors.length).toEqual(1); + const error = errors[0]; + + // THEN + expect(error).toMatch(/Only 50 environment variables can be set, got 51/); + }); + + test('throws if environment variables are set on merged API', () => { + // GIVEN + const source = new appsync.GraphqlApi(stack, 'source', { + name: 'source', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + // THEN + expect(() => { + new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSourceApis({ + sourceApis: [ + { + sourceApi: source, + }, + ], + }), + environmentVariables: { + EnvKey1: 'non-empty-1', + }, + }); + }).toThrow(/Environment variables are not supported for merged APIs/); + }); + + test('throws if environment variables are set by addEnvironmentVariable method on merged API', () => { + // GIVEN + const source = new appsync.GraphqlApi(stack, 'source', { + name: 'source', + definition: appsync.Definition.fromSchema(appsync.SchemaFile.fromAsset(path.join(__dirname, 'appsync.test.graphql'))), + }); + + // WHEN + const api = new appsync.GraphqlApi(stack, 'api', { + name: 'api', + definition: appsync.Definition.fromSourceApis({ + sourceApis: [ + { + sourceApi: source, + }, + ], + }), + }); + + // THEN + expect(() => { + api.addEnvironmentVariable('EnvKey1', 'non-empty-1'); + }).toThrow(/Environment variables are not supported for merged APIs/); + }); +}); + +function validate(construct: IConstruct): string[] { + try { + (construct.node.root as cdk.App).synth(); + return []; + } catch (err: any) { + if (!('message' in err) || !err.message.startsWith('Validation failed')) { + throw err; + } + return err.message.split('\n').slice(1); + } +} \ No newline at end of file