UNPKG

@mapbox/cloudfriend

Version:

Helper functions for assembling CloudFormation templates in JavaScript

1,742 lines (1,578 loc) 48.1 kB
'use strict'; const cp = require('child_process'); const path = require('path'); const fs = require('fs'); const test = require('tape'); const cf = require('..'); const fixtures = require('./fixtures/shortcuts'); const util = require('util'); const sleep = util.promisify(setTimeout); const update = !!process.env.UPDATE; const noUndefined = (template) => JSON.parse(JSON.stringify(template)); test('[shortcuts] fixture validation', async (assert) => { // Runs cfn-lint, ignoring "warnings". Install via pip or Homebrew to run these // tests locally. const cfnLint = (filepath) => new Promise((resolve, reject) => { cp.exec(`cfn-lint ${filepath} --ignore-checks W`, (err, stdout) => { if (err) return reject(new Error(stdout)); return resolve(); }); }); const toValidate = fs .readdirSync(path.join(__dirname, 'fixtures', 'shortcuts')) .filter((filename) => path.extname(filename) === '.json'); while (toValidate.length) { const filename = toValidate.shift(); await Promise.all([ cfnLint(path.join(__dirname, 'fixtures', 'shortcuts', filename)) .then(() => assert.pass(`${filename} fixture passed validation`)) .catch((err) => { assert.fail(`${filename} fixture fails validation`); console.log(err.message); }), sleep(1000) ]); } assert.end(); }); test('[shortcuts] lambda', (assert) => { assert.throws( () => new cf.shortcuts.Lambda(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.Lambda({}), /You must provide a LogicalName, and Code/, 'throws without required parameters' ); assert.throws( () => new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, RoleArn: 'custom', Statement: [{ Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::my-bucket/*' }] }), /You cannot specify both Statements and a RoleArn/, 'throws for RoleArn and Statements both provided' ); let lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } }); let template = cf.merge(lambda); if (update) fixtures.update('lambda-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('lambda-defaults'), 'expected resources generated using all default values' ); lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { ImageUri: 'MyImage' } }); template = cf.merge(lambda); if (update) fixtures.update('lambda-docker', template); assert.deepEqual( noUndefined(template), fixtures.get('lambda-docker'), 'expected resources generated using all default values and a docker image' ); lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { ZipFile: 'fake code' } }); template = cf.merge(lambda); if (update) fixtures.update('lambda-zipfile', template); assert.deepEqual( noUndefined(template), fixtures.get('lambda-zipfile'), 'expected resources generated using all default values and inline code' ); lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, RoleArn: cf.getAtt('CustomLambdaRole', 'Arn') }); template = cf.merge(lambda, { Resources: { 'CustomLambdaRole': { Type: 'AWS::IAM::Role', Properties: { AssumeRolePolicyDocument: {} } } } }); if (update) fixtures.update('lambda-provided-role', template); assert.deepEqual( noUndefined(template), fixtures.get('lambda-provided-role'), 'expected resources generated if RoleArn provided' ); lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, DeadLetterConfig: { TargetArn: 'arn:aws:sqs:us-east-1:123456789012:queue/fake' }, Description: 'my description', Environment: { Variables: { MyCoolEnv: 'a' } }, FunctionName: 'my-function', Handler: 'index.something', KmsKeyArn: 'arn:aws:kms:us-east-1:123456789012:key/fake', Layers: ['arn:aws:lambda:us-east-2:590474943231:layer:AWS-Parameters-and-Secrets-Lambda-Extension:4'], MemorySize: 512, ReservedConcurrentExecutions: 10, Runtime: 'nodejs22.x', Tags: [{ Key: 'a', Value: 'b' }], Timeout: 30, TracingConfig: { Mode: 'Active' }, VpcConfig: { SecurityGroupIds: ['sg-12345678'], SubnetIds: ['fake'] }, Condition: 'Always', DependsOn: 'AnotherThing', Statement: [ { Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::fake/data' } ], AlarmName: 'my-alarm', AlarmDescription: 'some alarm', AlarmActions: ['devnull@mapbox.com'], Period: 120, EvaluationPeriods: 2, Statistic: 'Minimum', Threshold: 10, ComparisonOperator: 'LessThanThreshold', TreatMissingData: 'breaching', EvaluateLowSampleCountPercentile: 'ignore', ExtendedStatistics: 'p100', OKActions: ['devnull@mapbox.com'], LogRetentionInDays: 30 }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, lambda ); if (update) fixtures.update('lambda-full', template); assert.deepEqual( noUndefined(template), fixtures.get('lambda-full'), 'expected resources generated using no default values' ); assert.end(); }); test('[shortcuts] queue-lambda', (assert) => { assert.throws( () => new cf.shortcuts.QueueLambda(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.QueueLambda({}), /You must provide a LogicalName, and Code/, 'throws without basic lambda required parameters' ); assert.throws( () => new cf.shortcuts.QueueLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } }), /You must provide an EventSourceArn and ReservedConcurrentExecutions/, 'throws without queue-lambda required parameters' ); assert.throws( () => new cf.shortcuts.QueueLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:queue/fake', ReservedConcurrentExecutions: -1 }), /ReservedConcurrentExecutions must be greater than or equal to 0/, 'throws when ReservedConcurrentExecutions is a negative number' ); const zeroLambda = new cf.shortcuts.QueueLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:queue/fake', ReservedConcurrentExecutions: 0 }); const zeroTemplate = cf.merge(zeroLambda); if (update) fixtures.update('queue-lambda-zero', zeroTemplate); assert.deepEqual( noUndefined(zeroTemplate), fixtures.get('queue-lambda-zero'), 'expected resources generated' ); const lambda = new cf.shortcuts.QueueLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:queue/fake', ReservedConcurrentExecutions: 10 }); const template = cf.merge(lambda); if (update) fixtures.update('queue-lambda', template); assert.deepEqual( noUndefined(template), fixtures.get('queue-lambda'), 'expected resources generated' ); assert.end(); }); test('[shortcuts] scheduled-lambda', (assert) => { assert.throws( () => new cf.shortcuts.ScheduledLambda(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.ScheduledLambda({}), /You must provide a LogicalName, and Code/, 'throws without basic lambda required parameters' ); assert.throws( () => new cf.shortcuts.ScheduledLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } }), /You must provide a ScheduleExpression/, 'throws without scheduled-lambda required parameters' ); let lambda = new cf.shortcuts.ScheduledLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, ScheduleExpression: 'rate(1 hour)' }); let template = cf.merge(lambda); if (update) fixtures.update('scheduled-lambda-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('scheduled-lambda-defaults'), 'expected resources generated with defaults' ); lambda = new cf.shortcuts.ScheduledLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, ScheduleRoleArn: 'arn:aws:iam::012345678901:role/MyCoolRole', ScheduleGroupName: 'my-cool-stack', ScheduleExpression: 'rate(1 hour)', State: 'DISABLED' }); template = cf.merge(lambda); if (update) fixtures.update('scheduled-lambda-full', template); assert.deepEqual( noUndefined(template), fixtures.get('scheduled-lambda-full'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] event-lambda', (assert) => { assert.throws( () => new cf.shortcuts.EventLambda(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.EventLambda({}), /You must provide a LogicalName, and Code/, 'throws without basic lambda required parameters' ); assert.throws( () => new cf.shortcuts.EventLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } }), /You must provide an EventPattern/, 'throws without event-lambda required parameters' ); let lambda = new cf.shortcuts.EventLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventPattern: { source: ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], detail: { state: ['running'] } } }); let template = cf.merge(lambda); if (update) fixtures.update('event-lambda-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('event-lambda-defaults'), 'expected resources generated with defaults' ); lambda = new cf.shortcuts.EventLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventPattern: { source: ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], detail: { state: ['running'] } }, State: 'DISABLED' }); template = cf.merge(lambda); if (update) fixtures.update('event-lambda-full', template); assert.deepEqual( noUndefined(template), fixtures.get('event-lambda-full'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] stream-lambda', (assert) => { assert.throws( () => new cf.shortcuts.StreamLambda(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.StreamLambda({}), /You must provide a LogicalName, and Code/, 'throws without basic lambda required parameters' ); assert.throws( () => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } }), /You must provide an EventSourceArn/, 'throws without stream-lambda required parameters' ); let lambda = new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake' }); let template = cf.merge(lambda); if (update) fixtures.update('stream-lambda-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('stream-lambda-defaults'), 'expected resources generated via defaults' ); lambda = new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: { Filters: [ { Pattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] }) } ] }, BatchSize: 10000, MaximumBatchingWindowInSeconds: 300, Enabled: false, StartingPosition: 'TRIM_HORIZON' }); template = cf.merge(lambda); if (update) fixtures.update('stream-lambda-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('stream-lambda-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] StreamLambda FilterCriteria', (assert) => { assert.throws( () => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: ['test'] }), '`FilterCriteria` must be a JSON-like object', ); assert.throws( () => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: {} }), '`FilterCriteria` must contain property `Filter` of type array', ); assert.throws( () => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: { Filter: 613 } }), '`FilterCriteria` must contain property `Filter` of type array', ); assert.throws( () => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: { Filters: [ { NotPattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] }) }, { Pattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] }) } ] } }), 'An object in `FilterCriteria.Filter` was missing the required property `Pattern`', ); assert.throws( () => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: { Filters: [ { Pattern: '{"eventName":["INSERT","MODIFY"]}' }, { Pattern: { eventName: ['INSERT', 'MODIFY'] } } ] } }), 'An object in `FilterCriteria.Filter` contains a `Pattern` property that is not a JSON parseable string', ); assert.end(); }); test('[shortcuts] log-subscription-lambda', (assert) => { assert.throws( () => new cf.shortcuts.LogSubscriptionLambda(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.LogSubscriptionLambda({}), /You must provide a LogicalName, and Code/, 'throws without basic lambda required parameters' ); assert.throws( () => new cf.shortcuts.LogSubscriptionLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } }), /You must provide a LogGroupName/, 'throws without log-subscription-lambda required parameters' ); let lambda = new cf.shortcuts.LogSubscriptionLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, LogGroupName: 'my-log-group' }); let template = cf.merge(lambda); if (update) fixtures.update('log-subscription-lambda-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('log-subscription-lambda-defaults'), 'expected resources generated via defaults' ); lambda = new cf.shortcuts.LogSubscriptionLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, FilterPattern: '{ $.errorCode = 400 }', LogGroupName: 'my-log-group' }); template = cf.merge(lambda); if (update) fixtures.update('log-subscription-lambda-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('log-subscription-lambda-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] queue', (assert) => { assert.throws( () => new cf.shortcuts.Queue(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.Queue({}), /You must provide a LogicalName/, 'throws without required parameters' ); let queue = new cf.shortcuts.Queue({ LogicalName: 'MyQueue' }); let template = cf.merge(queue); if (update) fixtures.update('queue-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('queue-defaults'), 'expected resources generated for full defaults' ); queue = new cf.shortcuts.Queue({ LogicalName: 'MyQueue', VisibilityTimeout: 60, maxReceiveCount: 100, DelaySeconds: 60, KmsMasterKeyId: 'alias/my-key', KmsDataKeyReusePeriondSeconds: 86400, MaximumMessageSize: 1024, MessageRetentionPeriod: 60, QueueName: 'my-queue', ReceiveMessageWaitTimeSeconds: 20, Condition: 'Always', DependsOn: 'AnotherThing', TopicName: 'my-topic', DisplayName: 'topic-display-name', DeadLetterVisibilityTimeout: 60 }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, queue ); if (update) fixtures.update('queue-full', template); assert.deepEqual( noUndefined(template), fixtures.get('queue-full'), 'expected resources generated no defaults' ); queue = new cf.shortcuts.Queue({ LogicalName: 'MyQueue', ExistingTopicArn: 'arn:aws:sns:us-east-1:111122223333:MyTopic' }); template = cf.merge(queue); if (update) fixtures.update('queue-external-topic', template); assert.deepEqual( noUndefined(template), fixtures.get('queue-external-topic'), 'expected resources generated for external topic' ); queue = new cf.shortcuts.Queue({ LogicalName: 'MyQueue', ExistingTopicArn: { Ref: 'TopicForOtherThing' } }); template = cf.merge( { Resources: { TopicForOtherThing: { Type: 'AWS::SNS::Topic' } } }, queue ); if (update) fixtures.update('queue-external-topic-ref', template); assert.deepEqual( noUndefined(template), fixtures.get('queue-external-topic-ref'), 'expected resources generated for external topic identified by ref' ); queue = new cf.shortcuts.Queue({ LogicalName: 'MyFifoQueue', FifoQueue: true }); template = cf.merge(queue); if (update) fixtures.update('queue-fifo', template); assert.deepEqual( noUndefined(template), fixtures.get('queue-fifo'), 'expected resources generated for FIFO queue' ); queue = new cf.shortcuts.Queue({ LogicalName: 'MyFifoQueue', QueueName: 'custom-and-fancy', FifoQueue: true }); template = cf.merge(queue); if (update) fixtures.update('queue-fifo-queuename', template); assert.deepEqual( noUndefined(template), fixtures.get('queue-fifo-queuename'), 'expected resources generated for FIFO queue with specified QueueName' ); queue = new cf.shortcuts.Queue({ LogicalName: 'MyFifoFalseQueue', FifoQueue: false }); template = cf.merge(queue); assert.equal( template.Resources.MyFifoFalseQueue.Properties.FifoQueue, undefined, 'the FifoQueue value false is converted to undefined, to pass CFN validation' ); assert.end(); }); test('[shortcuts] s3 kinesis firehose', (assert) => { assert.throws( () => new cf.shortcuts.S3KinesisFirehose(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.S3KinesisFirehose({}), /You must provide a LogicalName/, 'throws without required LogicalName parameter' ); assert.throws( () => new cf.shortcuts.S3KinesisFirehose({ LogicalName: 'MyKinesisFirehose' }), /You must provide a DestinationBucket/, 'throws without required DestinationBucket parameter' ); let firehose = new cf.shortcuts.S3KinesisFirehose({ LogicalName: 'MyKinesisFirehose', DestinationBucket: 'mah-bukkit' }); let template = cf.merge(firehose); if (update) fixtures.update('firehose-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('firehose-defaults'), 'expected resources generated for full defaults' ); firehose = new cf.shortcuts.S3KinesisFirehose({ LogicalName: 'MyKinesisFirehose', DestinationBucket: 'mah-bukkit', KinesisStreamARN: 'arn:aws:kinesis:us-east-1:111122223333:stream/my-stream' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, firehose ); if (update) fixtures.update('firehose-with-stream', template); assert.deepEqual( noUndefined(template), fixtures.get('firehose-with-stream'), 'expected resources generated with stream' ); assert.end(); }); test('[shortcuts] role', (assert) => { assert.throws( () => new cf.shortcuts.Role(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.Role({}), /You must provide a LogicalName and AssumeRolePrincipals/, 'throws without required parameters' ); let role = new cf.shortcuts.Role({ LogicalName: 'MyRole', AssumeRolePrincipals: [{ Service: 'ec2.amazonaws.com' }] }); let template = cf.merge(role); if (update) fixtures.update('role-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('role-defaults'), 'expected resources generated with defaults' ); role = new cf.shortcuts.Role({ LogicalName: 'MyRole', AssumeRolePrincipals: [{ Service: 'ec2.amazonaws.com' }], Statement: [ { Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::fake/data' } ], ManagedPolicyArns: ['arn:aws:iam::123456789012:policy/fake'], MaxSessionDuration: 3600, Path: '/fake/', RoleName: 'my-role', Tags: [{ Key: 'pipeline-name', Value: 'test' }], Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, role ); if (update) fixtures.update('role-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('role-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] cross-account role', (assert) => { assert.throws( () => new cf.shortcuts.CrossAccountRole(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.CrossAccountRole({}), /You must provide a LogicalName and Accounts/, 'throws without required parameters' ); let role = new cf.shortcuts.CrossAccountRole({ LogicalName: 'MyRole', Accounts: [ '123456789012', 'arn:aws:iam::123456789012:root', { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root' } ] }); let template = cf.merge(role); if (update) fixtures.update('cross-account-role-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('cross-account-role-defaults'), 'expected resources generated with defaults' ); role = new cf.shortcuts.CrossAccountRole({ LogicalName: 'MyRole', Accounts: [ '123456789012', 'arn:aws:iam::123456789012:root', { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root' } ], Statement: [ { Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::fake/data' } ], ManagedPolicyArns: ['arn:aws:iam::123456789012:policy/fake'], MaxSessionDuration: 3600, Path: '/fake/', RoleName: 'my-role', Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, role ); if (update) fixtures.update('cross-account-role-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('cross-account-role-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] service role', (assert) => { assert.throws( () => new cf.shortcuts.ServiceRole(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.ServiceRole({}), /You must provide a LogicalName and Service/, 'throws without required parameters' ); let role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'lambda' }); let template = cf.merge(role); if (update) fixtures.update('service-role-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('service-role-defaults'), 'expected resources generated with defaults' ); role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'lambda.amazonaws.com' }); template = cf.merge(role); if (update) fixtures.update('service-role-no-url-suffix', template); assert.deepEqual( noUndefined(template), fixtures.get('service-role-no-url-suffix'), 'expected resources generated, service for which AWS::URLSuffix is invalid' ); role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'ec2' }); template = cf.merge(role); if (update) fixtures.update('service-role-url-suffix', template); assert.deepEqual( noUndefined(template), fixtures.get('service-role-url-suffix'), 'expected resources generated, service for which AWS::URLSuffix is invalid' ); role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'ec2.amazonaws.com' }); template = cf.merge(role); if (update) fixtures.update('service-role-url-suffix-with-replacement', template); assert.deepEqual( noUndefined(template), fixtures.get('service-role-url-suffix-with-replacement'), 'expected resources generated, service for which AWS::URLSuffix is invalid specified with a suffix' ); role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'lambda.amazonaws.com', Statement: [ { Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::fake/data' } ], ManagedPolicyArns: ['arn:aws:iam::123456789012:policy/fake'], MaxSessionDuration: 3600, Path: '/fake/', RoleName: 'my-role', Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, role ); if (update) fixtures.update('service-role-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('service-role-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] glue database', (assert) => { assert.throws( () => new cf.shortcuts.GlueDatabase(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.GlueDatabase({}), /You must provide a LogicalName and Name/, 'throws without required parameters' ); let db = new cf.shortcuts.GlueDatabase({ LogicalName: 'MyDatabase', Name: 'my_database' }); let template = cf.merge(db); if (update) fixtures.update('glue-database-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-database-defaults'), 'expected resources generated with defaults' ); db = new cf.shortcuts.GlueDatabase({ LogicalName: 'MyDatabase', Name: 'my_database', CatalogId: '123456', Description: 'my_database description', LocationUri: 'fakeuri', Parameters: { thing: 'a' }, Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-database-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-database-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] glue table', (assert) => { assert.throws( () => new cf.shortcuts.GlueTable(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.GlueTable({}), /You must provide a LogicalName, Name, DatabaseName, and Columns/, 'throws without required parameters' ); let db = new cf.shortcuts.GlueTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ] }); let template = cf.merge(db); if (update) fixtures.update('glue-table-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-table-defaults'), 'expected resources generated with defaults' ); db = new cf.shortcuts.GlueTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_table description', Retention: 12, TableType: 'EXTERNAL_TABLE', ViewExpandedText: '/* Presto View */', ViewOriginalText: '/* Presto View: abc123= */', BucketColumns: ['column'], Compressed: true, InputFormat: 'fake.input.format', Location: 's3://fake/location', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-table-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-table-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] glue json table', (assert) => { assert.throws( () => new cf.shortcuts.GlueJsonTable(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.GlueJsonTable({}), /You must provide a Location/, 'throws without required parameters' ); let db = new cf.shortcuts.GlueJsonTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], Location: 's3://fake/location' }); let template = cf.merge(db); if (update) fixtures.update('glue-json-table-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-json-table-defaults'), 'expected resources generated with defaults' ); db = new cf.shortcuts.GlueJsonTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_table description', Retention: 12, TableType: 'EXTERNAL_TABLE', ViewExpandedText: '/* Presto View */', ViewOriginalText: '/* Presto View: abc123= */', BucketColumns: ['column'], Compressed: true, Location: 's3://fake/location', InputFormat: 'fake.input.format', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-json-table-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-json-table-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] glue orc table', (assert) => { assert.throws( () => new cf.shortcuts.GlueOrcTable(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.GlueOrcTable({}), /You must provide a Location/, 'throws without required parameters' ); let db = new cf.shortcuts.GlueOrcTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], Location: 's3://fake/location' }); let template = cf.merge(db); if (update) fixtures.update('glue-orc-table-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-orc-table-defaults'), 'expected resources generated with defaults' ); db = new cf.shortcuts.GlueOrcTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_table description', Retention: 12, TableType: 'EXTERNAL_TABLE', ViewExpandedText: '/* Presto View */', ViewOriginalText: '/* Presto View: abc123= */', BucketColumns: ['column'], Compressed: true, Location: 's3://fake/location', InputFormat: 'fake.input.format', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-orc-table-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-orc-table-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] glue parquet table', (assert) => { assert.throws( () => new cf.shortcuts.GlueParquetTable(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.GlueParquetTable({}), /You must provide a Location/, 'throws without required parameters' ); let db = new cf.shortcuts.GlueParquetTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], Location: 's3://fake/location' }); let template = cf.merge(db); if (update) fixtures.update('glue-parquet-table-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-parquet-table-defaults'), 'expected resources generated with defaults' ); db = new cf.shortcuts.GlueParquetTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_table description', Retention: 12, TableType: 'EXTERNAL_TABLE', ViewExpandedText: '/* Presto View */', ViewOriginalText: '/* Presto View: abc123= */', BucketColumns: ['column'], Compressed: true, Location: 's3://fake/location', InputFormat: 'fake.input.format', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-parquet-table-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-parquet-table-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); test('[shortcuts] glue view', (assert) => { assert.throws( () => new cf.shortcuts.GluePrestoView(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.GluePrestoView({}), /You must provide a DatabaseName, Columns, and OriginalSql/, 'throws without required parameters' ); let db = new cf.shortcuts.GluePrestoView({ LogicalName: 'MyView', DatabaseName: 'my_database', Name: 'my_view', Columns: [ { Name: 'column', Type: 'string' } ], OriginalSql: 'SELECT * FROM another.table' }); let template = cf.merge(db); if (update) fixtures.update('glue-view-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-view-defaults'), 'expected resources generated with defaults' ); db = new cf.shortcuts.GluePrestoView({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_view', Columns: [ { Name: 'column', Type: 'string' } ], OriginalSql: 'SELECT * FROM another.table', CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_view description', Retention: 12, TableType: 'EXTERNAL_TABLE', BucketColumns: ['column'], Compressed: true, Location: 's3://fake/location', InputFormat: 'fake.input.format', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, SqlVariables: { env: { Ref: 'AWS::StackName' } }, Condition: 'Always', DependsOn: 'AnotherThing' }); template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-view-no-defaults', template); assert.deepEqual( noUndefined(template), fixtures.get('glue-view-no-defaults'), 'expected resources generated without defaults' ); assert.end(); }); const normalizeDeployment = (template) => { const str = JSON.stringify(template).replace( /Deployment([0-9a-f]{8})/g, 'Deployment' ); return JSON.parse(str); }; test('[shortcuts] hookshot passthrough', (assert) => { assert.throws( () => new cf.shortcuts.hookshot.Passthrough(), 'Options required', 'throws without options' ); assert.throws( () => new cf.shortcuts.hookshot.Passthrough({}), /You must provide a Prefix, and PassthroughTo/, 'throws without required parameters' ); assert.throws( () => new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination', LoggingLevel: 'HAM' }), /LoggingLevel must be one of OFF, INFO, or ERROR/, 'throws with invalid LoggingLevel' ); assert.throws( () => new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination', LoggingLevel: 'INFO', Runtime: 'python3.7' }), /Only valid nodejs runtimes are supported for hookshot lambdas, received: 'python3.7'/, 'throws with invalid lambda Runtime python3.7' ); assert.throws( () => new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination', LoggingLevel: 'INFO', Runtime: 'nodejs16.x' }), /Only nodejs runtimes >= 18 are supported for hookshot lambdas, received: 'nodejs16.x'/, 'throws with invalid lambda Runtime nodejs16.x' ); const to = new cf.shortcuts.Lambda({ LogicalName: 'Destination', Code: { ZipFile: 'module.exports.handler = (e, c, cb) => cb();' } }); let passthrough = new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination' }); let template = cf.merge(passthrough, to); if (update) fixtures.update('hookshot-passthrough', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment(fixtures.get('hookshot-passthrough')), 'expected resources generated with defaults' ); passthrough = new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination', AlarmActions: ['devnull@mapbox.com'] }); template = cf.merge(passthrough, to); if (update) fixtures.update('hookshot-passthrough-alarms', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment(fixtures.get('hookshot-passthrough-alarms')), 'expected resources generated with alarm config' ); passthrough = new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination', LoggingLevel: 'INFO' }); template = cf.merge(passthrough, to); if (update) fixtures.update('hookshot-passthrough-logging', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment(fixtures.get('hookshot-passthrough-logging')), 'expected resources generated with configured LoggingLevel' ); passthrough = new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination', DataTraceEnabled: true, MetricsEnabled: true }); template = cf.merge(passthrough, to); if (update) fixtures.update('hookshot-passthrough-enhanced-logging', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment(fixtures.get('hookshot-passthrough-enhanced-logging')), 'expected resources generated with detailed logging and metrics' ); passthrough = new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination', DataTraceEnabled: true, MetricsEnabled: true, LoggingLevel: 'INFO' }); template = cf.merge(passthrough, to); if (update) fixtures.update('hookshot-passthrough-full-blown-logging', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment( fixtures.get('hookshot-passthrough-full-blown-logging') ), 'LoggingLevel respected with detailed logging and metrics' ); passthrough = new cf.shortcuts.hookshot.Passthrough({ Prefix: 'Pass', PassthroughTo: 'Destination', AccessLogFormat: '{ "requestId":"$context.requestId" }' }); template = cf.merge(passthrough, to); if (update) fixtures.update('hookshot-passthrough-access-log-format', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment(fixtures.get('hookshot-passthrough-access-log-format')), 'expected resources generated with access logs' ); assert.end(); }); test('[shortcuts] hookshot github', (assert) => { assert.throws( () => new cf.shortcuts.hookshot.Github(), /You must provide a Prefix, and PassthroughTo/, 'throws without required parameters' ); assert.throws( () => new cf.shortcuts.hookshot.Github({ Prefix: 'Pass', PassthroughTo: 'Destination', Runtime: 'python3.7' }), /Only valid nodejs runtimes are supported for hookshot lambdas, received: 'python3.7'/, 'throws with invalid lambda Runtime python3.7' ); assert.throws( () => new cf.shortcuts.hookshot.Github({ Prefix: 'Pass', PassthroughTo: 'Destination', Runtime: 'nodejs16.x' }), /Only nodejs runtimes >= 18 are supported for hookshot lambdas, received: 'nodejs16.x'/, 'throws with invalid lambda Runtime nodejs16.x' ); const to = new cf.shortcuts.Lambda({ LogicalName: 'Destination', Code: { ZipFile: 'module.exports.handler = (e, c, cb) => cb();' } }); let github = new cf.shortcuts.hookshot.Github({ Prefix: 'Pass', PassthroughTo: 'Destination' }); let template = cf.merge(github, to); if (update) fixtures.update('hookshot-github', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment(fixtures.get('hookshot-github')), 'expected resources generated with defaults' ); github = new cf.shortcuts.hookshot.Github({ Prefix: 'Pass', PassthroughTo: 'Destination', WebhookSecret: 'abc123' }); template = cf.merge(github, to); if (update) fixtures.update('hookshot-github-secret-string', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment(fixtures.get('hookshot-github-secret-string')), 'expected resources generated when secret passed as string' ); github = new cf.shortcuts.hookshot.Github({ Prefix: 'Pass', PassthroughTo: 'Destination', WebhookSecret: cf.ref('SomeParameter') }); const Parameters = { SomeParameter: { Type: 'String' } }; template = cf.merge(github, to, { Parameters }); if (update) fixtures.update('hookshot-github-secret-ref', template); assert.deepEqual( normalizeDeployment(noUndefined(template)), normalizeDeployment(fixtures.get('hookshot-github-secret-ref')), 'expected resources generated when secret passed as ref' ); assert.end(); });