diff --git a/integration/combination/test_connectors.py b/integration/combination/test_connectors.py index 9ac1ad8b7..cfff9ff85 100644 --- a/integration/combination/test_connectors.py +++ b/integration/combination/test_connectors.py @@ -26,6 +26,7 @@ def tearDown(self): @parameterized.expand( [ + ("combination/connector_appsync_to_lambda",), ("combination/connector_appsync_to_table",), ("combination/connector_function_to_function",), ("combination/connector_restapi_to_function",), diff --git a/integration/resources/expected/combination/connector_appsync_to_lambda.json b/integration/resources/expected/combination/connector_appsync_to_lambda.json new file mode 100644 index 000000000..77169dc89 --- /dev/null +++ b/integration/resources/expected/combination/connector_appsync_to_lambda.json @@ -0,0 +1,46 @@ +[ + { + "LogicalResourceId": "HelloLambda", + "ResourceType": "AWS::Lambda::Function" + }, + { + "LogicalResourceId": "HelloLambdaRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "AppSyncApi", + "ResourceType": "AWS::AppSync::GraphQLApi" + }, + { + "LogicalResourceId": "ApiKey", + "ResourceType": "AWS::AppSync::ApiKey" + }, + { + "LogicalResourceId": "ApiSchema", + "ResourceType": "AWS::AppSync::GraphQLSchema" + }, + { + "LogicalResourceId": "AppSyncLambdaDataSource", + "ResourceType": "AWS::AppSync::DataSource" + }, + { + "LogicalResourceId": "TriggerFunction", + "ResourceType": "AWS::Lambda::Function" + }, + { + "LogicalResourceId": "TriggerFunctionRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "AppSyncApiLambdaInvocationRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "DataSourceToLambdaConnectorPolicy", + "ResourceType": "AWS::IAM::ManagedPolicy" + }, + { + "LogicalResourceId": "AppSyncSayHelloResolver", + "ResourceType": "AWS::AppSync::Resolver" + } +] diff --git a/integration/resources/templates/combination/connector_appsync_to_lambda.yaml b/integration/resources/templates/combination/connector_appsync_to_lambda.yaml new file mode 100644 index 000000000..5e521c11c --- /dev/null +++ b/integration/resources/templates/combination/connector_appsync_to_lambda.yaml @@ -0,0 +1,162 @@ +Resources: + AppSyncApiLambdaInvocationRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: appsync.amazonaws.com + Action: + - sts:AssumeRole + + HelloLambda: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (_) => { + return "Hello World" + } + Handler: index.handler + Runtime: nodejs14.x + + AppSyncApi: + Type: AWS::AppSync::GraphQLApi + Properties: + AuthenticationType: API_KEY + Name: AppSyncApi + + ApiSchema: + Type: AWS::AppSync::GraphQLSchema + Properties: + ApiId: !GetAtt AppSyncApi.ApiId + Definition: | + type Query { + sayHello: String! + } + schema { + query: Query + } + + AppSyncLambdaDataSource: + Type: AWS::AppSync::DataSource + Properties: + ApiId: !GetAtt AppSyncApi.ApiId + Name: AppSyncLambdaDataSource + Type: AWS_LAMBDA + ServiceRoleArn: !GetAtt AppSyncApiLambdaInvocationRole.Arn + LambdaConfig: + LambdaFunctionArn: !GetAtt HelloLambda.Arn + + AppSyncSayHelloResolver: + DependsOn: ApiSchema + Type: AWS::AppSync::Resolver + Properties: + ApiId: !GetAtt AppSyncApi.ApiId + TypeName: Query + FieldName: sayHello + DataSourceName: !GetAtt AppSyncLambdaDataSource.Name + RequestMappingTemplate: | + { + "version" : "2017-02-28", + "operation": "Invoke", + "payload": $util.toJson($context.args) + } + ResponseMappingTemplate: | + $util.toJson($context.result) + + DataSourceToLambdaConnector: + Type: AWS::Serverless::Connector + Properties: + Source: + Id: AppSyncLambdaDataSource + Destination: + Id: HelloLambda + Permissions: + - Write + + ApiKey: + Type: AWS::AppSync::ApiKey + Properties: + ApiId: !GetAtt AppSyncApi.ApiId + + TriggerFunction: + Type: AWS::Serverless::Function + Properties: + Environment: + Variables: + API_KEY: !GetAtt ApiKey.ApiKey + GRAPHQL_URL: !GetAtt AppSyncApi.GraphQLUrl + Runtime: nodejs14.x + Handler: index.handler + InlineCode: | + const https = require("https"); + + exports.handler = async (_) => { + const queries = { + sayHello: /* GraphQL */ ` + query { + sayHello + } + `, + }; + + + const fetch = async (url, options) => + new Promise((resolve, reject) => { + const req = https.request(url, options, (res) => { + const body = []; + res.on("data", (chunk) => body.push(chunk)); + res.on("end", () => { + const resString = Buffer.concat(body).toString(); + resolve(resString); + }); + }); + + req.on("error", (err) => { + reject(err); + }); + + req.on("timeout", () => { + req.destroy(); + reject(new Error("Request time out")); + }); + + req.write(options.body); + req.end(); + }); + + + const makeRequest = async (queryName) => { + const options = { + method: "POST", + headers: { + "x-api-key": process.env.API_KEY, + }, + body: JSON.stringify({ query: queries[queryName] }), + timeout: 10000, // ms + }; + + const response = await fetch(process.env.GRAPHQL_URL, options); + const body = JSON.parse(response); + const data = body.data?.[queryName]; + + if (body.errors !== undefined) { + throw JSON.stringify(body.errors); + } + + if (data !== "Hello World") { + throw new Error(`${queryName} error: '${data}' must be 'Hello World'`); + } + + return body.data; + }; + + + return await makeRequest("sayHello"); + }; + + +Metadata: + SamTransformTest: true diff --git a/samtranslator/model/connector_profiles/profiles.json b/samtranslator/model/connector_profiles/profiles.json index 4960e0fa3..6d22ae63f 100644 --- a/samtranslator/model/connector_profiles/profiles.json +++ b/samtranslator/model/connector_profiles/profiles.json @@ -767,6 +767,29 @@ } } } + }, + "AWS::Lambda::Function": { + "Type": "AWS_IAM_ROLE_MANAGED_POLICY", + "Properties": { + "SourcePolicy": true, + "AccessCategories": { + "Write": { + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:InvokeAsync", + "lambda:InvokeFunction" + ], + "Resource": [ + "%{Destination.Arn}", + "%{Destination.Arn}:*" + ] + } + ] + } + } + } } } } diff --git a/tests/translator/input/connector_appsync_to_lambda.yaml b/tests/translator/input/connector_appsync_to_lambda.yaml new file mode 100644 index 000000000..e8c94bf29 --- /dev/null +++ b/tests/translator/input/connector_appsync_to_lambda.yaml @@ -0,0 +1,60 @@ +Resources: + AppSyncApiLambdaInvocationRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: appsync.amazonaws.com + Action: + - sts:AssumeRole + + HelloLambda: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (_) => { + return "Hello World" + } + Handler: index.handler + Runtime: nodejs14.x + + AppSyncApi: + Type: AWS::AppSync::GraphQLApi + Properties: + AuthenticationType: API_KEY + Name: AppSyncApi + + ApiSchema: + Type: AWS::AppSync::GraphQLSchema + Properties: + ApiId: !GetAtt AppSyncApi.ApiId + Definition: | + type Query { + sayHello: String! + } + schema { + query: Query + } + + AppSyncLambdaDataSource: + Type: AWS::AppSync::DataSource + Properties: + ApiId: !GetAtt AppSyncApi.ApiId + Name: AppSyncLambdaDataSource + Type: AWS_LAMBDA + ServiceRoleArn: !GetAtt AppSyncApiLambdaInvocationRole.Arn + LambdaConfig: + LambdaFunctionArn: !GetAtt HelloLambda.Arn + + DataSourceToLambdaConnector: + Type: AWS::Serverless::Connector + Properties: + Source: + Id: AppSyncLambdaDataSource + Destination: + Id: HelloLambda + Permissions: + - Write diff --git a/tests/translator/output/aws-cn/connector_appsync_to_lambda.json b/tests/translator/output/aws-cn/connector_appsync_to_lambda.json new file mode 100644 index 000000000..5c87af6c1 --- /dev/null +++ b/tests/translator/output/aws-cn/connector_appsync_to_lambda.json @@ -0,0 +1,176 @@ +{ + "Resources": { + "ApiSchema": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "AppSyncApi", + "ApiId" + ] + }, + "Definition": "type Query {\n sayHello: String!\n}\nschema {\n query: Query\n}\n" + }, + "Type": "AWS::AppSync::GraphQLSchema" + }, + "AppSyncApi": { + "Properties": { + "AuthenticationType": "API_KEY", + "Name": "AppSyncApi" + }, + "Type": "AWS::AppSync::GraphQLApi" + }, + "AppSyncApiLambdaInvocationRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::IAM::Role" + }, + "AppSyncLambdaDataSource": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "AppSyncApi", + "ApiId" + ] + }, + "LambdaConfig": { + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + } + }, + "Name": "AppSyncLambdaDataSource", + "ServiceRoleArn": { + "Fn::GetAtt": [ + "AppSyncApiLambdaInvocationRole", + "Arn" + ] + }, + "Type": "AWS_LAMBDA" + }, + "Type": "AWS::AppSync::DataSource" + }, + "DataSourceToLambdaConnectorPolicy": { + "Metadata": { + "aws:sam:connectors": { + "DataSourceToLambdaConnector": { + "Destination": { + "Type": "AWS::Serverless::Function" + }, + "Source": { + "Type": "AWS::AppSync::DataSource" + } + } + } + }, + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "lambda:InvokeAsync", + "lambda:InvokeFunction" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + }, + { + "Fn::Sub": [ + "${DestinationArn}:*", + { + "DestinationArn": { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + } + } + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "Roles": [ + { + "Ref": "AppSyncApiLambdaInvocationRole" + } + ] + }, + "Type": "AWS::IAM::ManagedPolicy" + }, + "HelloLambda": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async (_) => {\n return \"Hello World\"\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "HelloLambdaRole", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "HelloLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +} diff --git a/tests/translator/output/aws-us-gov/connector_appsync_to_lambda.json b/tests/translator/output/aws-us-gov/connector_appsync_to_lambda.json new file mode 100644 index 000000000..b6b799b2f --- /dev/null +++ b/tests/translator/output/aws-us-gov/connector_appsync_to_lambda.json @@ -0,0 +1,176 @@ +{ + "Resources": { + "ApiSchema": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "AppSyncApi", + "ApiId" + ] + }, + "Definition": "type Query {\n sayHello: String!\n}\nschema {\n query: Query\n}\n" + }, + "Type": "AWS::AppSync::GraphQLSchema" + }, + "AppSyncApi": { + "Properties": { + "AuthenticationType": "API_KEY", + "Name": "AppSyncApi" + }, + "Type": "AWS::AppSync::GraphQLApi" + }, + "AppSyncApiLambdaInvocationRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::IAM::Role" + }, + "AppSyncLambdaDataSource": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "AppSyncApi", + "ApiId" + ] + }, + "LambdaConfig": { + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + } + }, + "Name": "AppSyncLambdaDataSource", + "ServiceRoleArn": { + "Fn::GetAtt": [ + "AppSyncApiLambdaInvocationRole", + "Arn" + ] + }, + "Type": "AWS_LAMBDA" + }, + "Type": "AWS::AppSync::DataSource" + }, + "DataSourceToLambdaConnectorPolicy": { + "Metadata": { + "aws:sam:connectors": { + "DataSourceToLambdaConnector": { + "Destination": { + "Type": "AWS::Serverless::Function" + }, + "Source": { + "Type": "AWS::AppSync::DataSource" + } + } + } + }, + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "lambda:InvokeAsync", + "lambda:InvokeFunction" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + }, + { + "Fn::Sub": [ + "${DestinationArn}:*", + { + "DestinationArn": { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + } + } + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "Roles": [ + { + "Ref": "AppSyncApiLambdaInvocationRole" + } + ] + }, + "Type": "AWS::IAM::ManagedPolicy" + }, + "HelloLambda": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async (_) => {\n return \"Hello World\"\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "HelloLambdaRole", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "HelloLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +} diff --git a/tests/translator/output/connector_appsync_to_lambda.json b/tests/translator/output/connector_appsync_to_lambda.json new file mode 100644 index 000000000..3e0162c55 --- /dev/null +++ b/tests/translator/output/connector_appsync_to_lambda.json @@ -0,0 +1,176 @@ +{ + "Resources": { + "ApiSchema": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "AppSyncApi", + "ApiId" + ] + }, + "Definition": "type Query {\n sayHello: String!\n}\nschema {\n query: Query\n}\n" + }, + "Type": "AWS::AppSync::GraphQLSchema" + }, + "AppSyncApi": { + "Properties": { + "AuthenticationType": "API_KEY", + "Name": "AppSyncApi" + }, + "Type": "AWS::AppSync::GraphQLApi" + }, + "AppSyncApiLambdaInvocationRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": "appsync.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::IAM::Role" + }, + "AppSyncLambdaDataSource": { + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "AppSyncApi", + "ApiId" + ] + }, + "LambdaConfig": { + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + } + }, + "Name": "AppSyncLambdaDataSource", + "ServiceRoleArn": { + "Fn::GetAtt": [ + "AppSyncApiLambdaInvocationRole", + "Arn" + ] + }, + "Type": "AWS_LAMBDA" + }, + "Type": "AWS::AppSync::DataSource" + }, + "DataSourceToLambdaConnectorPolicy": { + "Metadata": { + "aws:sam:connectors": { + "DataSourceToLambdaConnector": { + "Destination": { + "Type": "AWS::Serverless::Function" + }, + "Source": { + "Type": "AWS::AppSync::DataSource" + } + } + } + }, + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "lambda:InvokeAsync", + "lambda:InvokeFunction" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + }, + { + "Fn::Sub": [ + "${DestinationArn}:*", + { + "DestinationArn": { + "Fn::GetAtt": [ + "HelloLambda", + "Arn" + ] + } + } + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "Roles": [ + { + "Ref": "AppSyncApiLambdaInvocationRole" + } + ] + }, + "Type": "AWS::IAM::ManagedPolicy" + }, + "HelloLambda": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async (_) => {\n return \"Hello World\"\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "HelloLambdaRole", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "HelloLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + } + } +}