diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts index 795d7a67297d0..398be10eb39be 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/index.ts @@ -58,9 +58,9 @@ export interface CloudTrailProps { /** * How long to retain logs in CloudWatchLogs. Ignored if sendToCloudWatchLogs is false - * @default LogRetention.OneYear + * @default logs.RetentionDays.OneYear */ - readonly cloudWatchLogsRetentionTimeDays?: LogRetention; + readonly cloudWatchLogsRetentionTimeDays?: logs.RetentionDays; /** The AWS Key Management Service (AWS KMS) key ID that you want to use to encrypt CloudTrail logs. * @default none @@ -90,26 +90,6 @@ export enum ReadWriteType { All = "All" } -// TODO: This belongs in a CWL L2 -export enum LogRetention { - OneDay = 1, - ThreeDays = 3, - FiveDays = 5, - OneWeek = 7, - TwoWeeks = 14, - OneMonth = 30, - TwoMonths = 60, - ThreeMonths = 90, - FourMonths = 120, - FiveMonths = 150, - HalfYear = 180, - OneYear = 365, - FourHundredDays = 400, - EighteenMonths = 545, - TwoYears = 731, - FiveYears = 1827, - TenYears = 3653 -} /** * Cloud trail allows you to log events that happen in your AWS account * For example: @@ -145,7 +125,7 @@ export class CloudTrail extends cdk.Construct { let logsRole: iam.IRole | undefined; if (props.sendToCloudWatchLogs) { logGroup = new logs.CfnLogGroup(this, "LogGroup", { - retentionInDays: props.cloudWatchLogsRetentionTimeDays || LogRetention.OneYear + retentionInDays: props.cloudWatchLogsRetentionTimeDays || logs.RetentionDays.OneYear }); logsRole = new iam.Role(this, 'LogsRole', { assumedBy: new iam.ServicePrincipal(cloudTrailPrincipal) }); diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 7704ad357fb4e..831b5c347abc0 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -72,9 +72,10 @@ "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { "@aws-cdk/aws-kms": "^0.26.0", + "@aws-cdk/aws-logs": "^0.26.0", "@aws-cdk/cdk": "^0.26.0" }, "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts index c848538c00584..b9a25981ccc88 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts @@ -1,7 +1,8 @@ import { expect, haveResource, not, SynthUtils } from '@aws-cdk/assert'; +import { RetentionDays } from '@aws-cdk/aws-logs'; import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { CloudTrail, LogRetention, ReadWriteType } from '../lib'; +import { CloudTrail, ReadWriteType } from '../lib'; const ExpectedBucketPolicyProperties = { PolicyDocument: { @@ -105,7 +106,7 @@ export = { const stack = getTestStack(); new CloudTrail(stack, 'MyAmazingCloudTrail', { sendToCloudWatchLogs: true, - cloudWatchLogsRetentionTimeDays: LogRetention.OneWeek + cloudWatchLogsRetentionTimeDays: RetentionDays.OneWeek }); expect(stack).to(haveResource("AWS::CloudTrail::Trail")); diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index c266bae6cc922..9c296ea30a7b1 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -1,6 +1,7 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); +import logs = require('@aws-cdk/aws-logs'); import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); import { Code } from './code'; @@ -9,6 +10,7 @@ import { FunctionBase, FunctionImportProps, IFunction } from './function-base'; import { Version } from './lambda-version'; import { CfnFunction } from './lambda.generated'; import { ILayerVersion } from './layers'; +import { LogRetention } from './log-retention'; import { Runtime } from './runtime'; /** @@ -198,6 +200,15 @@ export interface FunctionProps { * You can also add event sources using `addEventSource`. */ readonly events?: IEventSource[]; + + /** + * The number of days log events are kept in CloudWatch Logs. When updating + * this property, unsetting it doesn't remove the log retention policy. To + * remove the retention policy, set the value to `Infinity`. + * + * @default logs never expire + */ + readonly logRetentionDays?: logs.RetentionDays; } /** @@ -395,6 +406,14 @@ export class Function extends FunctionBase { for (const event of props.events || []) { this.addEventSource(event); } + + // Log retention + if (props.logRetentionDays) { + new LogRetention(this, 'LogRetention', { + logGroupName: `/aws/lambda/${this.functionName}`, + retentionDays: props.logRetentionDays + }); + } } /** diff --git a/packages/@aws-cdk/aws-lambda/lib/index.ts b/packages/@aws-cdk/aws-lambda/lib/index.ts index c4cb9c7a7cdc0..003ee3af81430 100644 --- a/packages/@aws-cdk/aws-lambda/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/index.ts @@ -10,6 +10,7 @@ export * from './lambda-version'; export * from './singleton-lambda'; export * from './event-source'; export * from './event-source-mapping'; +export * from './log-retention'; // AWS::Lambda CloudFormation Resources: export * from './lambda.generated'; diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts new file mode 100644 index 0000000000000..4af3302d74374 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention-provider/index.ts @@ -0,0 +1,97 @@ +// tslint:disable:no-console +import AWS = require('aws-sdk'); + +/** + * Creates a log group and doesn't throw if it exists. + * + * @param logGroupName the name of the log group to create + */ +async function createLogGroupSafe(logGroupName: string) { + try { // Try to create the log group + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' }); + await cloudwatchlogs.createLogGroup({ logGroupName }).promise(); + } catch (e) { + if (e.code !== 'ResourceAlreadyExistsException') { + throw e; + } + } +} + +/** + * Puts or deletes a retention policy on a log group. + * + * @param logGroupName the name of the log group to create + * @param retentionInDays the number of days to retain the log events in the specified log group. + */ +async function setRetentionPolicy(logGroupName: string, retentionInDays?: number) { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28' }); + if (!retentionInDays) { + await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise(); + } else { + await cloudwatchlogs.putRetentionPolicy({ logGroupName, retentionInDays }).promise(); + } +} + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { + try { + console.log(JSON.stringify(event)); + + // The target log group + const logGroupName = event.ResourceProperties.LogGroupName; + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + // Act on the target log group + await createLogGroupSafe(logGroupName); + await setRetentionPolicy(logGroupName, parseInt(event.ResourceProperties.RetentionInDays, 10)); + + if (event.RequestType === 'Create') { + // Set a retention policy of 1 day on the logs of this function. The log + // group for this function should already exist at this stage because we + // already logged the event but due to the async nature of Lambda logging + // there could be a race condition. So we also try to create the log group + // of this function first. + await createLogGroupSafe(`/aws/lambda/${context.functionName}`); + await setRetentionPolicy(`/aws/lambda/${context.functionName}`, 1); + } + } + + await respond('SUCCESS', 'OK', logGroupName); + } catch (e) { + console.log(e); + + await respond('FAILED', e.message, event.ResourceProperties.LogGroupName); + } + + function respond(responseStatus: string, reason: string, physicalResourceId: string) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: reason, + PhysicalResourceId: physicalResourceId, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + Data: {} + }); + + console.log('Responding', responseBody); + + const parsedUrl = require('url').parse(event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: 'PUT', + headers: { 'content-type': '', 'content-length': responseBody.length } + }; + + return new Promise((resolve, reject) => { + try { + const request = require('https').request(requestOptions, resolve); + request.on('error', reject); + request.write(responseBody); + request.end(); + } catch (e) { + reject(e); + } + }); + } +} diff --git a/packages/@aws-cdk/aws-lambda/lib/log-retention.ts b/packages/@aws-cdk/aws-lambda/lib/log-retention.ts new file mode 100644 index 0000000000000..036fb38f9a719 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/log-retention.ts @@ -0,0 +1,64 @@ +import iam = require('@aws-cdk/aws-iam'); +import logs = require('@aws-cdk/aws-logs'); +import cdk = require('@aws-cdk/cdk'); +import path = require('path'); +import { Code } from './code'; +import { Runtime } from './runtime'; +import { SingletonFunction } from './singleton-lambda'; + +/** + * Construction properties for a LogRetention. + */ +export interface LogRetentionProps { + /** + * The log group name. + */ + readonly logGroupName: string; + + /** + * The number of days log events are kept in CloudWatch Logs. + */ + readonly retentionDays: logs.RetentionDays; +} + +/** + * Creates a custom resource to control the retention policy of a CloudWatch Logs + * log group. The log group is created if it doesn't already exist. The policy + * is removed when `retentionDays` is `undefined` or equal to `Infinity`. + */ +export class LogRetention extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props: LogRetentionProps) { + super(scope, id); + + // Custom resource provider + const provider = new SingletonFunction(this, 'Provider', { + code: Code.asset(path.join(__dirname, 'log-retention-provider')), + runtime: Runtime.NodeJS810, + handler: 'index.handler', + uuid: 'aae0aa3c-5b4d-4f87-b02d-85b201efdd8a', + lambdaPurpose: 'LogRetention', + }); + + if (provider.role && !provider.role.node.tryFindChild('DefaultPolicy')) { // Avoid duplicate statements + provider.role.addToPolicy( + new iam.PolicyStatement() + .addActions('logs:PutRetentionPolicy', 'logs:DeleteRetentionPolicy') + // We need '*' here because we will also put a retention policy on + // the log group of the provider function. Referencing it's name + // creates a CF circular dependency. + .addAllResources() + ); + } + + // Need to use a CfnResource here to prevent lerna dependency cycles + // @aws-cdk/aws-cloudformation -> @aws-cdk/aws-lambda -> @aws-cdk/aws-cloudformation + new cdk.CfnResource(this, 'Resource', { + type: 'Custom::LogRetention', + properties: { + ServiceToken: provider.functionArn, + LogGroupName: props.logGroupName, + RetentionInDays: props.retentionDays === Infinity ? undefined : props.retentionDays + } + }); + } +} diff --git a/packages/@aws-cdk/aws-lambda/package-lock.json b/packages/@aws-cdk/aws-lambda/package-lock.json new file mode 100644 index 0000000000000..a2bc8923e4b90 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/package-lock.json @@ -0,0 +1,424 @@ +{ + "name": "@aws-cdk/aws-lambda", + "version": "0.26.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", + "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/aws-lambda": { + "version": "8.10.23", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.23.tgz", + "integrity": "sha512-erfexxfuc1+T7b4OswooKwpIjpdgEOVz6ZrDDWSR+3v7Kjhs4EVowfUkF9KuLKhpcjz+VVHQ/pWIl7zSVbKbFQ==", + "dev": true + }, + "@types/nock": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.1.tgz", + "integrity": "sha512-eOVHXS5RnWOjTVhu3deCM/ruy9E6JCgeix2g7wpFiekQh3AaEAK1cz43tZDukKmtSmQnwvSySq7ubijCA32I7Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "dev": true + }, + "@types/sinon": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.10.tgz", + "integrity": "sha512-4w7SvsiUOtd4mUfund9QROPSJ5At/GQskDpqd87pJIRI6ULWSJqHI3GIZE337wQuN3aznroJGr94+o8fwvL37Q==", + "dev": true + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "aws-sdk": { + "version": "2.425.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.425.0.tgz", + "integrity": "sha512-SM2qZJPlZUKVzSSqNuCvONOhJ2kcFvU+hAwutjQeje2VKpSAbUbFCFWl6cki2FjiyGZYEPfl0Q+3ANJO8gx9BA==", + "dev": true, + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + } + }, + "aws-sdk-mock": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-4.3.1.tgz", + "integrity": "sha512-uOaf7/Tq9kSoRc2/EQfAn24AAwU6UwvR8xSFSg0vTRxK0xHHEZ5UB/KF6ibF2gj0I4977lM35237E5sbzhRxKA==", + "dev": true, + "requires": { + "aws-sdk": "^2.369.0", + "sinon": "^7.1.1", + "traverse": "^0.6.6" + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lolex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz", + "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "nise": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } + } + }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + } + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", + "dev": true + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "sinon": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.0.tgz", + "integrity": "sha512-0pYvgRv46fODzT/PByqb79MVNpyxsxf38WEiXTABOF8RfIMcIARfZ+1ORuxwAmHkreZ/jST3UDBdKCRhUy/e1A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.0", + "diff": "^3.5.0", + "lolex": "^3.1.0", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "dev": true + } + } +} diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index e804d9f979383..ebefb3e088128 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -59,10 +59,17 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.26.0", + "@types/aws-lambda": "^8.10.23", + "@types/nock": "^9.3.1", + "@types/sinon": "^7.0.10", + "aws-sdk": "^2.425.0", + "aws-sdk-mock": "^4.3.1", "cdk-build-tools": "^0.26.0", "cdk-integ-tools": "^0.26.0", "cfn2ts": "^0.26.0", - "pkglint": "^0.26.0" + "nock": "^10.0.6", + "pkglint": "^0.26.0", + "sinon": "^7.3.0" }, "dependencies": { "@aws-cdk/assets": "^0.26.0", @@ -97,4 +104,4 @@ "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json new file mode 100644 index 0000000000000..9ef732e1d267b --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json @@ -0,0 +1,383 @@ +{ + "Resources": { + "OneWeekServiceRole05A6F9F8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "OneWeekFE56F6A4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(JSON.stringify(event));" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "OneWeekServiceRole05A6F9F8", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "OneWeekServiceRole05A6F9F8" + ] + }, + "OneWeekLogRetention8E8911C1": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/lambda/", + { + "Ref": "OneWeekFE56F6A4" + } + ] + ] + }, + "RetentionInDays": 7 + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "Roles": [ + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3BucketB81211B5" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + ] + }, + "OneMonthServiceRoleFBD1064F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "OneMonth64E966BF": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(JSON.stringify(event));" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "OneMonthServiceRoleFBD1064F", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "OneMonthServiceRoleFBD1064F" + ] + }, + "OneMonthLogRetention814A40D9": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/lambda/", + { + "Ref": "OneMonth64E966BF" + } + ] + ] + }, + "RetentionInDays": 30 + } + }, + "OneYearServiceRole24D47762": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "OneYearA82EBDA9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(JSON.stringify(event));" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "OneYearServiceRole24D47762", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "OneYearServiceRole24D47762" + ] + }, + "OneYearLogRetentionBD83A067": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/lambda/", + { + "Ref": "OneYearA82EBDA9" + } + ] + ] + }, + "RetentionInDays": 365 + } + } + }, + "Parameters": { + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3BucketB81211B5": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-lambda-log-retention/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-lambda-log-retention/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.ts b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.ts new file mode 100644 index 0000000000000..08475cc79d5b8 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.ts @@ -0,0 +1,30 @@ +import logs = require('@aws-cdk/aws-logs'); +import cdk = require('@aws-cdk/cdk'); +import lambda = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-lambda-log-retention'); + +new lambda.Function(stack, 'OneWeek', { + code: new lambda.InlineCode('exports.handler = (event) => console.log(JSON.stringify(event));'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, + logRetentionDays: logs.RetentionDays.OneWeek +}); + +new lambda.Function(stack, 'OneMonth', { + code: new lambda.InlineCode('exports.handler = (event) => console.log(JSON.stringify(event));'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, + logRetentionDays: logs.RetentionDays.OneMonth +}); + +new lambda.Function(stack, 'OneYear', { + code: new lambda.InlineCode('exports.handler = (event) => console.log(JSON.stringify(event));'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810, + logRetentionDays: logs.RetentionDays.OneYear +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 6b2fb74abfc24..23b588681b04f 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1,6 +1,7 @@ import { countResources, expect, haveResource, MatchStyle, ResourcePart } from '@aws-cdk/assert'; import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); +import logs = require('@aws-cdk/aws-logs'); import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; @@ -1309,7 +1310,38 @@ export = { test.equal(rt.supportsInlineCode, false); test.done(); - } + }, + + 'specify log retention'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS, + logRetentionDays: logs.RetentionDays.OneMonth + }); + + // THEN + expect(stack).to(haveResource('Custom::LogRetention', { + 'LogGroupName': { + 'Fn::Join': [ + '', + [ + '/aws/lambda/', + { + Ref: 'MyLambdaCCE802FB' + } + ] + ] + }, + 'RetentionInDays': 30 + })); + + test.done(); + } }; function newTestLambda(scope: cdk.Construct) { diff --git a/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts b/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts new file mode 100644 index 0000000000000..86ced0d4226b7 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/test.log-retention-provider.ts @@ -0,0 +1,230 @@ +import AWS = require('aws-sdk-mock'); +import nock = require('nock'); +import { Test } from 'nodeunit'; +import sinon = require('sinon'); +import provider = require('../lib/log-retention-provider'); + +const eventCommon = { + ServiceToken: 'token', + ResponseURL: 'https://localhost', + StackId: 'stackId', + RequestId: 'requestId', + LogicalResourceId: 'logicalResourceId', + PhysicalResourceId: 'group', + ResourceType: 'Custom::LogRetention', +}; + +const context = { + functionName: 'provider' +} as AWSLambda.Context; + +function createRequest(type: string) { + return nock('https://localhost') + .put('/', (body: AWSLambda.CloudFormationCustomResourceResponse) => body.Status === type && body.PhysicalResourceId === 'group') + .reply(200); +} + +export = { + 'tearDown'(callback: any) { + AWS.restore(); + nock.cleanAll(); + callback(); + }, + + async 'create event'(test: Test) { + const createLogGroupFake = sinon.fake.resolves({}); + const putRetentionPolicyFake = sinon.fake.resolves({}); + const deleteRetentionPolicyFake = sinon.fake.resolves({}); + + AWS.mock('CloudWatchLogs', 'createLogGroup', createLogGroupFake); + AWS.mock('CloudWatchLogs', 'putRetentionPolicy', putRetentionPolicyFake); + AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', deleteRetentionPolicyFake); + + const event = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + RetentionInDays: '30', + LogGroupName: 'group' + } + }; + + const request = createRequest('SUCCESS'); + + await provider.handler(event as AWSLambda.CloudFormationCustomResourceCreateEvent, context); + + sinon.assert.calledWith(createLogGroupFake, { + logGroupName: 'group' + }); + + sinon.assert.calledWith(putRetentionPolicyFake, { + logGroupName: 'group', + retentionInDays: 30 + }); + + sinon.assert.calledWith(createLogGroupFake, { + logGroupName: '/aws/lambda/provider' + }); + + sinon.assert.calledWith(putRetentionPolicyFake, { + logGroupName: '/aws/lambda/provider', + retentionInDays: 1 + }); + + sinon.assert.notCalled(deleteRetentionPolicyFake); + + test.equal(request.isDone(), true); + + test.done(); + }, + + async 'update event with new log retention'(test: Test) { + const error = new Error() as NodeJS.ErrnoException; + error.code = 'ResourceAlreadyExistsException'; + + const createLogGroupFake = sinon.fake.rejects(error); + const putRetentionPolicyFake = sinon.fake.resolves({}); + const deleteRetentionPolicyFake = sinon.fake.resolves({}); + + AWS.mock('CloudWatchLogs', 'createLogGroup', createLogGroupFake); + AWS.mock('CloudWatchLogs', 'putRetentionPolicy', putRetentionPolicyFake); + AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', deleteRetentionPolicyFake); + + const event = { + ...eventCommon, + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'token', + RetentionInDays: '365', + LogGroupName: 'group' + }, + OldResourceProperties: { + ServiceToken: 'token', + LogGroupName: 'group', + RetentionInDays: '30' + } + }; + + const request = createRequest('SUCCESS'); + + await provider.handler(event as AWSLambda.CloudFormationCustomResourceUpdateEvent, context); + + sinon.assert.calledWith(createLogGroupFake, { + logGroupName: 'group' + }); + + sinon.assert.calledWith(putRetentionPolicyFake, { + logGroupName: 'group', + retentionInDays: 365 + }); + + sinon.assert.notCalled(deleteRetentionPolicyFake); + + test.equal(request.isDone(), true); + + test.done(); + }, + + async 'update event with log retention undefined'(test: Test) { + const error = new Error() as NodeJS.ErrnoException; + error.code = 'ResourceAlreadyExistsException'; + + const createLogGroupFake = sinon.fake.rejects(error); + const putRetentionPolicyFake = sinon.fake.resolves({}); + const deleteRetentionPolicyFake = sinon.fake.resolves({}); + + AWS.mock('CloudWatchLogs', 'createLogGroup', createLogGroupFake); + AWS.mock('CloudWatchLogs', 'putRetentionPolicy', putRetentionPolicyFake); + AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', deleteRetentionPolicyFake); + + const event = { + ...eventCommon, + RequestType: 'Update', + PhysicalResourceId: 'group', + ResourceProperties: { + ServiceToken: 'token', + LogGroupName: 'group' + }, + OldResourceProperties: { + ServiceToken: 'token', + LogGroupName: 'group', + RetentionInDays: '365' + } + }; + + const request = createRequest('SUCCESS'); + + await provider.handler(event as AWSLambda.CloudFormationCustomResourceUpdateEvent, context); + + sinon.assert.calledWith(createLogGroupFake, { + logGroupName: 'group' + }); + + sinon.assert.calledWith(deleteRetentionPolicyFake, { + logGroupName: 'group' + }); + + test.equal(request.isDone(), true); + + test.done(); + }, + + async 'delete event'(test: Test) { + const createLogGroupFake = sinon.fake.resolves({}); + const putRetentionPolicyFake = sinon.fake.resolves({}); + const deleteRetentionPolicyFake = sinon.fake.resolves({}); + + AWS.mock('CloudWatchLogs', 'createLogGroup', createLogGroupFake); + AWS.mock('CloudWatchLogs', 'putRetentionPolicy', putRetentionPolicyFake); + AWS.mock('CloudWatchLogs', 'deleteRetentionPolicy', deleteRetentionPolicyFake); + + const event = { + ...eventCommon, + RequestType: 'Delete', + PhysicalResourceId: 'group', + ResourceProperties: { + ServiceToken: 'token', + LogGroupName: 'group' + } + }; + + const request = createRequest('SUCCESS'); + + await provider.handler(event as AWSLambda.CloudFormationCustomResourceDeleteEvent, context); + + sinon.assert.notCalled(createLogGroupFake); + + sinon.assert.notCalled(putRetentionPolicyFake); + + sinon.assert.notCalled(deleteRetentionPolicyFake); + + test.equal(request.isDone(), true); + + test.done(); + }, + + async 'responds with FAILED on error'(test: Test) { + const createLogGroupFake = sinon.fake.rejects(new Error('UnkownError')); + + AWS.mock('CloudWatchLogs', 'createLogGroup', createLogGroupFake); + + const event = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + RetentionInDays: '30', + LogGroupName: 'group' + } + }; + + const request = createRequest('FAILED'); + + await provider.handler(event as AWSLambda.CloudFormationCustomResourceCreateEvent, context); + + test.equal(request.isDone(), true); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-lambda/test/test.log-retention.ts b/packages/@aws-cdk/aws-lambda/test/test.log-retention.ts new file mode 100644 index 0000000000000..be223b42148d7 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/test.log-retention.ts @@ -0,0 +1,57 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import logs = require('@aws-cdk/aws-logs'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import lambda = require('../lib'); + +// tslint:disable:object-literal-key-quotes + +export = { + 'log retention construct'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new lambda.LogRetention(stack, 'MyLambda', { + logGroupName: 'group', + retentionDays: logs.RetentionDays.OneMonth + }); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:PutRetentionPolicy", + "logs:DeleteRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "Roles": [ + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + } + ] + })); + + expect(stack).to(haveResource('Custom::LogRetention', { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": "group", + "RetentionInDays": 30 + })); + + test.done(); + + } +}; diff --git a/packages/@aws-cdk/aws-logs/lib/log-group.ts b/packages/@aws-cdk/aws-logs/lib/log-group.ts index a5c3a080b64c6..4c227fbf25c61 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-group.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-group.ts @@ -189,6 +189,96 @@ export abstract class LogGroupBase extends cdk.Construct implements ILogGroup { } } +/** + * How long, in days, the log contents will be retained. + */ +export enum RetentionDays { + /** + * 1 day + */ + OneDay = 1, + + /** + * 3 days + */ + ThreeDays = 3, + + /** + * 5 days + */ + FiveDays = 5, + + /** + * 1 week + */ + OneWeek = 7, + + /** + * 2 weeks + */ + TwoWeeks = 14, + + /** + * 1 month + */ + OneMonth = 30, + + /** + * 2 months + */ + TwoMonths = 60, + + /** + * 3 months + */ + ThreeMonths = 90, + + /** + * 4 months + */ + FourMonths = 120, + + /** + * 5 months + */ + FiveMonths = 150, + + /** + * 6 months + */ + SixMonths = 180, + + /** + * 1 year + */ + OneYear = 365, + + /** + * 13 months + */ + ThirteenMonths = 400, + + /** + * 18 months + */ + EighteenMonths = 545, + + /** + * 2 years + */ + TwoYears = 731, + + /** + * 5 years + */ + FiveYears = 1827, + + /** + * 10 years + */ + TenYears = 3653 +} + /** * Properties for a LogGroup */ @@ -207,7 +297,7 @@ export interface LogGroupProps { * * @default 731 days (2 years) */ - readonly retentionDays?: number; + readonly retentionDays?: RetentionDays; /** * Retain the log group if the stack or containing construct ceases to exist @@ -247,7 +337,7 @@ export class LogGroup extends LogGroupBase { super(scope, id); let retentionInDays = props.retentionDays; - if (retentionInDays === undefined) { retentionInDays = 731; } + if (retentionInDays === undefined) { retentionInDays = RetentionDays.TwoYears; } if (retentionInDays === Infinity) { retentionInDays = undefined; } if (retentionInDays !== undefined && retentionInDays <= 0) { diff --git a/packages/@aws-cdk/aws-logs/test/example.retention.lit.ts b/packages/@aws-cdk/aws-logs/test/example.retention.lit.ts index 72e7a180c6267..ed0fd084e18d4 100644 --- a/packages/@aws-cdk/aws-logs/test/example.retention.lit.ts +++ b/packages/@aws-cdk/aws-logs/test/example.retention.lit.ts @@ -1,5 +1,5 @@ import { Stack } from '@aws-cdk/cdk'; -import { LogGroup } from '../lib'; +import { LogGroup, RetentionDays } from '../lib'; const stack = new Stack(); @@ -7,7 +7,7 @@ function shortLogGroup() { /// !show // Configure log group for short retention const logGroup = new LogGroup(stack, 'LogGroup', { - retentionDays: 7 + retentionDays: RetentionDays.OneWeek }); /// !hide return logGroup; diff --git a/packages/@aws-cdk/aws-logs/test/test.loggroup.ts b/packages/@aws-cdk/aws-logs/test/test.loggroup.ts index f23d3b16f8002..418197a35839f 100644 --- a/packages/@aws-cdk/aws-logs/test/test.loggroup.ts +++ b/packages/@aws-cdk/aws-logs/test/test.loggroup.ts @@ -2,7 +2,7 @@ import { expect, haveResource, matchTemplate } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import { Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { LogGroup } from '../lib'; +import { LogGroup, RetentionDays } from '../lib'; export = { 'fixed retention'(test: Test) { @@ -11,7 +11,7 @@ export = { // WHEN new LogGroup(stack, 'LogGroup', { - retentionDays: 7 + retentionDays: RetentionDays.OneWeek }); // THEN