diff --git a/README.md b/README.md index f50a8fa..4d5ad6e 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ For now, the plugin only works with Lambda functions invoked by * CloudWatch Scheduled events * CloudWatch Logs * IoT rules +* AppSync DataSources [More events](https://serverless.com/framework/docs/providers/aws/events/) will be added soon. diff --git a/fixtures/16.input.appsync.json b/fixtures/16.input.appsync.json new file mode 100644 index 0000000..ecf5a90 --- /dev/null +++ b/fixtures/16.input.appsync.json @@ -0,0 +1,472 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The AWS CloudFormation template for this Serverless application", + "Resources": { + "ServerlessDeploymentBucket": { + "Type": "AWS::S3::Bucket" + }, + "HelloLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/canary-deployments-test-appsync-datasources-dev-Hello" + } + }, + "PreHookLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/canary-deployments-test-appsync-datasources-devpreHook" + } + }, + "PostHookLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/canary-deployments-test-appsync-datasources-devpostHook" + } + }, + "HelloLambdaAppSynDataSource": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Ref": "TestApi" + }, + "Name": "test-lambda-datasource", + "Description": "Hello Lambda DataSource", + "Type": "AWS_LAMBDA", + "ServiceRoleArn": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "LambdaConfig": { + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "HelloLambdaFunction", + "Arn" + ] + } + } + } + }, + "IamRoleLambdaExecution": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + "Policies": [ + { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "dev", + "canary-deployments-test-appsync-datasource", + "lambda" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream" + ], + "Resource": [ + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-dev-Hello:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-devpreHook:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-devpostHook:*" + } + ] + }, + { + "Effect": "Allow", + "Action": [ + "logs:PutLogEvents" + ], + "Resource": [ + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-dev-Hello:*:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-devpreHook:*:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-devpostHook:*:*" + } + ] + }, + { + "Effect": "Allow", + "Action": [ + "codedeploy:*" + ], + "Resource": [ + "*" + ] + } + ] + } + } + ], + "Path": "/", + "RoleName": { + "Fn::Join": [ + "-", + [ + "canary-deployments-test-appsync-datasource", + "dev", + "us-east-1", + "lambdaRole" + ] + ] + } + } + }, + "HelloLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/canary-deployments-test-appsync-datasource/dev/1520191533287-2018-03-04T19:25:33.287Z/canary-deployments-test-appsync-datasource.zip" + }, + "FunctionName": "canary-deployments-test-appsync-datasources-dev-Hello", + "Handler": "handler.hello", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "Timeout": 6 + }, + "DependsOn": [ + "HelloLogGroup", + "IamRoleLambdaExecution" + ] + }, + "HelloLambdaVersionFYAirphUvjV7H12yGxU1eQrqAiSBMjAi9hdLPgV62L8": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "HelloLambdaFunction" + }, + "CodeSha256": "sZvdDgxnAbKe1yaQga0XJPD82+o5jFWz+J3lR+q9UHU=" + } + }, + "PreHookLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/canary-deployments-test-appsync-datasource/dev/1520191533287-2018-03-04T19:25:33.287Z/canary-deployments-test-appsync-datasource.zip" + }, + "FunctionName": "canary-deployments-test-appsync-datasources-devpreHook", + "Handler": "hooks.pre", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "Timeout": 6 + }, + "DependsOn": [ + "PreHookLogGroup", + "IamRoleLambdaExecution" + ] + }, + "PreHookLambdaVersionIYyrXlfQM5jjU68REvnAzRxhgq9eoLqSsDjy0": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "PreHookLambdaFunction" + }, + "CodeSha256": "sZvdDgxnAbKe1yaQga0XJPD82+o5jFWz+J3lR+q9UHU=" + } + }, + "PostHookLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/canary-deployments-test-appsync-datasource/dev/1520191533287-2018-03-04T19:25:33.287Z/canary-deployments-test-appsync-datasource.zip" + }, + "FunctionName": "canary-deployments-test-appsync-datasources-devpostHook", + "Handler": "hooks.post", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "Timeout": 6 + }, + "DependsOn": [ + "PostHookLogGroup", + "IamRoleLambdaExecution" + ] + }, + "PostHookLambdaVersiondh0VUUAh9BrmvORqx3vDEIcHxolKWKCO1YL45mVTbg": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "PostHookLambdaFunction" + }, + "CodeSha256": "sZvdDgxnAbKe1yaQga0XJPD82+o5jFWz+J3lR+q9UHU=" + } + }, + "ApiGatewayRestApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "dev-canary-deployments-test-appsync-datasource", + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + } + } + }, + "ApiGatewayResourceHello": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "ApiGatewayRestApi", + "RootResourceId" + ] + }, + "PathPart": "hello", + "RestApiId": { + "Ref": "ApiGatewayRestApi" + } + } + }, + "ApiGatewayMethodHelloGet": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "RequestParameters": {}, + "ResourceId": { + "Ref": "ApiGatewayResourceHello" + }, + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "HelloLambdaFunction", + "Arn" + ] + }, + "/invocations" + ] + ] + } + }, + "MethodResponses": [] + } + }, + "ApiGatewayDeployment12345": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "StageName": "dev" + }, + "DependsOn": [ + "ApiGatewayMethodHelloGet" + ] + }, + "HelloLambdaPermissionApiGateway": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Fn::GetAtt": [ + "HelloLambdaFunction", + "Arn" + ] + }, + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "ApiGatewayRestApi" + }, + "/*/*" + ] + ] + } + } + }, + "S3BucketS3SampleBucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "s3SampleBucket", + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Event": "s3:ObjectCreated:*", + "Function": { + "Fn::GetAtt": [ + "HelloLambdaFunction", + "Arn" + ] + } + } + ] + } + }, + "DependsOn": [ + "HelloLambdaPermissionS3SampleBucketS3" + ] + }, + "HelloLambdaPermissionS3SampleBucketS3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Fn::GetAtt": [ + "HelloLambdaFunction", + "Arn" + ] + }, + "Action": "lambda:InvokeFunction", + "Principal": "s3.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::s3SampleBucket" + ] + ] + } + } + }, + "HelloFooAlarm": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "Namespace": "AWS/Lambda", + "MetricName": "Errors", + "Threshold": 1, + "Period": 60, + "EvaluationPeriods": 1, + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "OKActions": [], + "AlarmActions": [], + "InsufficientDataActions": [], + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { + "Ref": "HelloLambdaFunction" + } + } + ], + "TreatMissingData": "missing", + "Statistic": "Minimum" + } + } + }, + "Outputs": { + "ServerlessDeploymentBucketName": { + "Value": { + "Ref": "ServerlessDeploymentBucket" + } + }, + "HelloLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "HelloLambdaVersionFYAirphUvjV7H12yGxU1eQrqAiSBMjAi9hdLPgV62L8" + } + }, + "PreHookLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "PreHookLambdaVersionIYyrXlfQM5jjU68REvnAzRxhgq9eoLqSsDjy0" + } + }, + "PostHookLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "PostHookLambdaVersiondh0VUUAh9BrmvORqx3vDEIcHxolKWKCO1YL45mVTbg" + } + }, + "ServiceEndpoint": { + "Description": "URL of the service endpoint", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "ApiGatewayRestApi" + }, + ".execute-api.us-east-1.amazonaws.com/dev" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/fixtures/16.output.appsync.json b/fixtures/16.output.appsync.json new file mode 100644 index 0000000..c2976e4 --- /dev/null +++ b/fixtures/16.output.appsync.json @@ -0,0 +1,575 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The AWS CloudFormation template for this Serverless application", + "Resources": { + "ServerlessDeploymentBucket": { + "Type": "AWS::S3::Bucket" + }, + "HelloLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/canary-deployments-test-appsync-datasources-dev-Hello" + } + }, + "PreHookLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/canary-deployments-test-appsync-datasources-devpreHook" + } + }, + "PostHookLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": "/aws/lambda/canary-deployments-test-appsync-datasources-devpostHook" + } + }, + "HelloLambdaAppSynDataSource": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": { + "Ref": "TestApi" + }, + "Name": "test-lambda-datasource", + "Description": "Hello Lambda DataSource", + "Type": "AWS_LAMBDA", + "ServiceRoleArn": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "LambdaConfig": { + "LambdaFunctionArn": { + "Ref": "HelloLambdaFunctionAliasLive" + } + } + } + }, + "IamRoleLambdaExecution": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + "Policies": [ + { + "PolicyName": { + "Fn::Join": [ + "-", + [ + "dev", + "canary-deployments-test-appsync-datasource", + "lambda" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream" + ], + "Resource": [ + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-dev-Hello:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-devpreHook:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-devpostHook:*" + } + ] + }, + { + "Effect": "Allow", + "Action": [ + "logs:PutLogEvents" + ], + "Resource": [ + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-dev-Hello:*:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-devpreHook:*:*" + }, + { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/canary-deployments-test-appsync-datasources-devpostHook:*:*" + } + ] + }, + { + "Effect": "Allow", + "Action": [ + "codedeploy:*" + ], + "Resource": [ + "*" + ] + }, + { + "Action": [ + "codedeploy:PutLifecycleEventHookExecutionStatus" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Sub": "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${CanarydeploymentstestappsyncdatasourcesdevDeploymentApplication}/canary-deployments-test-appsync-datasources-dev-HelloLambdaFunctionDeploymentGroup" + } + ] + } + ] + } + } + ], + "Path": "/", + "RoleName": { + "Fn::Join": [ + "-", + [ + "canary-deployments-test-appsync-datasource", + "dev", + "us-east-1", + "lambdaRole" + ] + ] + } + } + }, + "HelloLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/canary-deployments-test-appsync-datasource/dev/1520191533287-2018-03-04T19:25:33.287Z/canary-deployments-test-appsync-datasource.zip" + }, + "FunctionName": "canary-deployments-test-appsync-datasources-dev-Hello", + "Handler": "handler.hello", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "Timeout": 6 + }, + "DependsOn": [ + "HelloLogGroup", + "IamRoleLambdaExecution" + ] + }, + "HelloLambdaVersionFYAirphUvjV7H12yGxU1eQrqAiSBMjAi9hdLPgV62L8": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "HelloLambdaFunction" + }, + "CodeSha256": "sZvdDgxnAbKe1yaQga0XJPD82+o5jFWz+J3lR+q9UHU=" + } + }, + "PreHookLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/canary-deployments-test-appsync-datasource/dev/1520191533287-2018-03-04T19:25:33.287Z/canary-deployments-test-appsync-datasource.zip" + }, + "FunctionName": "canary-deployments-test-appsync-datasources-devpreHook", + "Handler": "hooks.pre", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "Timeout": 6 + }, + "DependsOn": [ + "PreHookLogGroup", + "IamRoleLambdaExecution" + ] + }, + "PreHookLambdaVersionIYyrXlfQM5jjU68REvnAzRxhgq9eoLqSsDjy0": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "PreHookLambdaFunction" + }, + "CodeSha256": "sZvdDgxnAbKe1yaQga0XJPD82+o5jFWz+J3lR+q9UHU=" + } + }, + "PostHookLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "ServerlessDeploymentBucket" + }, + "S3Key": "serverless/canary-deployments-test-appsync-datasource/dev/1520191533287-2018-03-04T19:25:33.287Z/canary-deployments-test-appsync-datasource.zip" + }, + "FunctionName": "canary-deployments-test-appsync-datasources-devpostHook", + "Handler": "hooks.post", + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "Timeout": 6 + }, + "DependsOn": [ + "PostHookLogGroup", + "IamRoleLambdaExecution" + ] + }, + "PostHookLambdaVersiondh0VUUAh9BrmvORqx3vDEIcHxolKWKCO1YL45mVTbg": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "PostHookLambdaFunction" + }, + "CodeSha256": "sZvdDgxnAbKe1yaQga0XJPD82+o5jFWz+J3lR+q9UHU=" + } + }, + "ApiGatewayRestApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "dev-canary-deployments-test-appsync-datasource", + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + } + } + }, + "ApiGatewayResourceHello": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "ApiGatewayRestApi", + "RootResourceId" + ] + }, + "PathPart": "hello", + "RestApiId": { + "Ref": "ApiGatewayRestApi" + } + } + }, + "ApiGatewayMethodHelloGet": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "RequestParameters": {}, + "ResourceId": { + "Ref": "ApiGatewayResourceHello" + }, + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "ApiKeyRequired": false, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Ref": "HelloLambdaFunctionAliasLive" + }, + "/invocations" + ] + ] + } + }, + "MethodResponses": [] + } + }, + "ApiGatewayDeployment12345": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ApiGatewayRestApi" + }, + "StageName": "dev" + }, + "DependsOn": [ + "ApiGatewayMethodHelloGet" + ] + }, + "HelloLambdaPermissionApiGateway": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Ref": "HelloLambdaFunctionAliasLive" + }, + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "ApiGatewayRestApi" + }, + "/*/*" + ] + ] + } + } + }, + "S3BucketS3SampleBucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "s3SampleBucket", + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Event": "s3:ObjectCreated:*", + "Function": { + "Ref": "HelloLambdaFunctionAliasLive" + } + } + ] + } + }, + "DependsOn": [ + "HelloLambdaPermissionS3SampleBucketS3" + ] + }, + "HelloLambdaPermissionS3SampleBucketS3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { + "Ref": "HelloLambdaFunctionAliasLive" + }, + "Action": "lambda:InvokeFunction", + "Principal": "s3.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::s3SampleBucket" + ] + ] + } + } + }, + "HelloFooAlarm": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "Namespace": "AWS/Lambda", + "MetricName": "Errors", + "Threshold": 1, + "Period": 60, + "EvaluationPeriods": 1, + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "OKActions": [], + "AlarmActions": [], + "InsufficientDataActions": [], + "Dimensions": [ + { + "Name": "FunctionName", + "Value": { + "Ref": "HelloLambdaFunction" + } + } + ], + "TreatMissingData": "missing", + "Statistic": "Minimum" + } + }, + "CanarydeploymentstestappsyncdatasourcesdevDeploymentApplication": { + "Type": "AWS::CodeDeploy::Application", + "Properties": { + "ComputePlatform": "Lambda" + } + }, + "CodeDeployServiceRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambdaLimited", + "arn:aws:iam::aws:policy/AWSLambda_FullAccess" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "codedeploy.amazonaws.com" + ] + } + } + ] + } + } + }, + "HelloLambdaFunctionDeploymentGroup": { + "Type": "AWS::CodeDeploy::DeploymentGroup", + "Properties": { + "ApplicationName": { + "Ref": "CanarydeploymentstestappsyncdatasourcesdevDeploymentApplication" + }, + "AutoRollbackConfiguration": { + "Enabled": true, + "Events": [ + "DEPLOYMENT_FAILURE", + "DEPLOYMENT_STOP_ON_ALARM", + "DEPLOYMENT_STOP_ON_REQUEST" + ] + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "CodeDeployServiceRole", + "Arn" + ] + }, + "DeploymentConfigName": { + "Fn::Sub": [ + "CodeDeployDefault.Lambda${ConfigName}", + { + "ConfigName": "Linear10PercentEvery1Minute" + } + ] + }, + "DeploymentGroupName": "canary-deployments-test-appsync-datasources-dev-HelloLambdaFunctionDeploymentGroup", + "DeploymentStyle": { + "DeploymentType": "BLUE_GREEN", + "DeploymentOption": "WITH_TRAFFIC_CONTROL" + }, + "AlarmConfiguration": { + "Alarms": [ + { + "Name": { + "Ref": "HelloFooAlarm" + } + } + ], + "Enabled": true + } + } + }, + "HelloLambdaFunctionAliasLive": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionVersion": { + "Fn::GetAtt": [ + "HelloLambdaVersionFYAirphUvjV7H12yGxU1eQrqAiSBMjAi9hdLPgV62L8", + "Version" + ] + }, + "FunctionName": { + "Ref": "HelloLambdaFunction" + }, + "Name": "Live" + }, + "UpdatePolicy": { + "CodeDeployLambdaAliasUpdate": { + "ApplicationName": { + "Ref": "CanarydeploymentstestappsyncdatasourcesdevDeploymentApplication" + }, + "AfterAllowTrafficHook": { + "Ref": "PostHookLambdaFunction" + }, + "BeforeAllowTrafficHook": { + "Ref": "PreHookLambdaFunction" + }, + "DeploymentGroupName": { + "Ref": "HelloLambdaFunctionDeploymentGroup" + } + } + } + } + }, + "Outputs": { + "ServerlessDeploymentBucketName": { + "Value": { + "Ref": "ServerlessDeploymentBucket" + } + }, + "HelloLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "HelloLambdaVersionFYAirphUvjV7H12yGxU1eQrqAiSBMjAi9hdLPgV62L8" + } + }, + "PreHookLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "PreHookLambdaVersionIYyrXlfQM5jjU68REvnAzRxhgq9eoLqSsDjy0" + } + }, + "PostHookLambdaFunctionQualifiedArn": { + "Description": "Current Lambda function version", + "Value": { + "Ref": "PostHookLambdaVersiondh0VUUAh9BrmvORqx3vDEIcHxolKWKCO1YL45mVTbg" + } + }, + "ServiceEndpoint": { + "Description": "URL of the service endpoint", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "ApiGatewayRestApi" + }, + ".execute-api.us-east-1.amazonaws.com/dev" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/fixtures/16.service.appsync.json b/fixtures/16.service.appsync.json new file mode 100644 index 0000000..f13d1f4 --- /dev/null +++ b/fixtures/16.service.appsync.json @@ -0,0 +1,61 @@ +{ + "service": "canary-deployments-test-appsync-datasources", + "custom": { + "deploymentSettings": { + "stages": [ + "dev" + ] + } + }, + "functions": { + "hello": { + "handler": "handler.hello", + "events": [ + { + "http": "GET hello" + }, + { + "s3": "s3SampleBucket" + } + ], + "deploymentSettings": { + "type": "Linear10PercentEvery1Minute", + "alias": "Live", + "preTrafficHook": "preHook", + "postTrafficHook": "postHook", + "alarms": [ + "HelloFooAlarm" + ] + } + }, + "preHook": { + "handler": "hooks.pre" + }, + "postHook": { + "handler": "hooks.post" + } + }, + "resources": { + "Resources": { + "Type": "AWS::AppSync::DataSource", + "Properties": { + "ApiId": "TestApi", + "Description": "Hello Lambda DataSource", + "LambdaConfig": { + "Fn::GetAtt": [ + "HelloLambdaFunction", + "Arn" + ] + }, + "Name": "test-lambda-datasource", + "ServiceRoleArn": { + "Fn::GetAtt": [ + "IamRoleLambdaExecution", + "Arn" + ] + }, + "Type": "AWS_LAMBDA" + } + } + } +} \ No newline at end of file diff --git a/lib/CfTemplateGenerators/AppSyncDataSources.js b/lib/CfTemplateGenerators/AppSyncDataSources.js new file mode 100644 index 0000000..5f08974 --- /dev/null +++ b/lib/CfTemplateGenerators/AppSyncDataSources.js @@ -0,0 +1,30 @@ +function replaceAppSyncDataSourceWithAlias (dataSource, functionAlias, functionName) { + if (dataSource.Properties.Type !== 'AWS_LAMBDA') return dataSource + const isDataSourceLambda = () => { + const lambda = dataSource.Properties.LambdaConfig.LambdaFunctionArn || {} + const funcDetails = lambda['Fn::GetAtt'] || [] + const [funcName] = funcDetails + return funcName ? funcName === functionName : false + } + if (!isDataSourceLambda()) { + return dataSource + } + + const newLambda = { + Ref: functionAlias + } + + const newProperties = Object.assign({}, dataSource.Properties, { + LambdaConfig: { + LambdaFunctionArn: newLambda + } + }) + + return Object.assign({}, dataSource, { Properties: newProperties }) +} + +const AppSyncDataSources = { + replaceAppSyncDataSourceWithAlias +} + +module.exports = AppSyncDataSources diff --git a/lib/CfTemplateGenerators/AppSyncDataSources.test.js b/lib/CfTemplateGenerators/AppSyncDataSources.test.js new file mode 100644 index 0000000..e2d8898 --- /dev/null +++ b/lib/CfTemplateGenerators/AppSyncDataSources.test.js @@ -0,0 +1,66 @@ +const { expect } = require('chai') +const AppSyncDataSources = require('./AppSyncDataSources') + +describe('AppSyncDataSources', () => { + describe('.replaceAppSyncDataSourceWithAlias', () => { + const functionName = 'HelloLambdaFunction' + const appSyncDataSource = { + Type: 'AWS::AppSync::DataSource', + Properties: { + ApiId: { + 'Fn::GetAtt': [ + 'TestGraphQlApi', + 'ApiId' + ] + }, + Description: 'test event rule', + Name: 'Event Rule', + Type: 'AWS_LAMBDA', + ServiceRoleArn: { + 'Fn::GetAtt': [ + 'TestGraphQlApiRole', + 'Arn' + ] + }, + LambdaConfig: { + LambdaFunctionArn: { + 'Fn::GetAtt': [ + functionName, + 'Arn' + ] + } + } + } + } + + it('replaces the appSync DataSource function for an alias', () => { + const functionAlias = 'FunctionWithAlias' + const expected = { + Type: 'AWS::AppSync::DataSource', + Properties: { + ApiId: { + 'Fn::GetAtt': [ + 'TestGraphQlApi', + 'ApiId' + ] + }, + Description: 'test event rule', + Name: 'Event Rule', + Type: 'AWS_LAMBDA', + ServiceRoleArn: { + 'Fn::GetAtt': [ + 'TestGraphQlApiRole', + 'Arn' + ] + }, + LambdaConfig: { + LambdaFunctionArn: { Ref: functionAlias } + } + } + } + + const actual = AppSyncDataSources.replaceAppSyncDataSourceWithAlias(appSyncDataSource, functionAlias, functionName) + expect(actual).to.deep.equal(expected) + }) + }) +}) diff --git a/lib/CfTemplateGenerators/index.js b/lib/CfTemplateGenerators/index.js index 1e17473..3f09204 100644 --- a/lib/CfTemplateGenerators/index.js +++ b/lib/CfTemplateGenerators/index.js @@ -7,6 +7,7 @@ const S3 = require('./S3') const CloudWatchEvents = require('./CloudWatchEvents') const CloudWatchLogs = require('./CloudWatchLogs') const Iot = require('./Iot') +const AppSync = require('./AppSyncDataSources') module.exports.codeDeploy = CodeDeploy module.exports.iam = Iam @@ -17,3 +18,4 @@ module.exports.s3 = S3 module.exports.cloudWatchEvents = CloudWatchEvents module.exports.cloudWatchLogs = CloudWatchLogs module.exports.iot = Iot +module.exports.appSync = AppSync diff --git a/package-lock.json b/package-lock.json index 782d8de..93ce14c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,12 @@ { "name": "serverless-plugin-canary-deployments", - "version": "0.6.0", + "version": "0.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.6.0", + "name": "serverless-plugin-canary-deployments", + "version": "0.7.1", "license": "ISC", "dependencies": { "flat": "^4.1.0", diff --git a/serverless-plugin-canary-deployments.js b/serverless-plugin-canary-deployments.js index 2417619..46062db 100644 --- a/serverless-plugin-canary-deployments.js +++ b/serverless-plugin-canary-deployments.js @@ -18,7 +18,7 @@ class ServerlessCanaryDeployments { this.naming = this.awsProvider.naming this.service = this.serverless.service this.hooks = { - 'before:package:finalize': this.addCanaryDeploymentResources.bind(this) + 'after:aws:package:finalize:mergeCustomProviderResources': this.addCanaryDeploymentResources.bind(this) } this.addConfigSchema() } @@ -217,7 +217,8 @@ class ServerlessCanaryDeployments { 'AWS::S3::Bucket': CfGenerators.s3.replaceS3BucketFunctionWithAlias, 'AWS::Events::Rule': CfGenerators.cloudWatchEvents.replaceCloudWatchEventRuleTargetWithAlias, 'AWS::Logs::SubscriptionFilter': CfGenerators.cloudWatchLogs.replaceCloudWatchLogsDestinationArnWithAlias, - 'AWS::IoT::TopicRule': CfGenerators.iot.replaceIotTopicRuleActionArnWithAlias + 'AWS::IoT::TopicRule': CfGenerators.iot.replaceIotTopicRuleActionArnWithAlias, + 'AWS::AppSync::DataSource': CfGenerators.appSync.replaceAppSyncDataSourceWithAlias } const functionEvents = this.getEventsFor(functionName) const functionEventsEntries = _.entries(functionEvents) @@ -239,6 +240,7 @@ class ServerlessCanaryDeployments { const cloudWatchEvents = this.getCloudWatchEventsFor(functionName) const cloudWatchLogs = this.getCloudWatchLogsFor(functionName) const iotTopicRules = this.getIotTopicRulesFor(functionName) + const appSyncDataSources = this.getAppSyncDataSourcesFor(functionName) return Object.assign( {}, apiGatewayMethods, @@ -250,7 +252,8 @@ class ServerlessCanaryDeployments { cloudWatchEvents, cloudWatchLogs, snsSubscriptions, - iotTopicRules + iotTopicRules, + appSyncDataSources ) } @@ -392,6 +395,19 @@ class ServerlessCanaryDeployments { return getMappingsForFunction(this.compiledTpl.Resources) } + getAppSyncDataSourcesFor (functionName) { + const isAppSyncDataSource = _.matchesProperty('Type', 'AWS::AppSync::DataSource') + const isAppSyncDataSourceForFunction = _.matchesProperty( + 'Properties.LambdaConfig.LambdaFunctionArn.Fn::GetAtt[0]', + functionName + ) + const getMappingsForFunction = _.pipe( + _.pickBy(isAppSyncDataSource), + _.pickBy(isAppSyncDataSourceForFunction) + ) + return getMappingsForFunction(this.compiledTpl.Resources) + } + getVersionNameFor (functionName) { const isLambdaVersion = _.matchesProperty('Type', 'AWS::Lambda::Version') const isVersionForFunction = _.matchesProperty('Properties.FunctionName.Ref', functionName)