From a7cd6c189aec10a398ba89d270495b3d6eccb384 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Mon, 12 Sep 2022 15:06:15 +0100 Subject: [PATCH] feat(neptune): enable cloudwatch logs exports - introduce LogType and CloudwatchLogsExports for use in DatabaseClusterProps - introduce cloudwatchLogsExports prop to configure which log types should be exported to CloudWatch Logs and optionally set log retention - update tests and integ tests - update README related to #20248 closes #15888 --- packages/@aws-cdk/aws-neptune/README.md | 33 +++ packages/@aws-cdk/aws-neptune/lib/cluster.ts | 65 ++++++ packages/@aws-cdk/aws-neptune/package.json | 2 + .../aws-neptune/rosetta/default.ts-fixture | 1 + ...efaultTestDeployAssert6A1BBA9D.assets.json | 19 ++ ...aultTestDeployAssert6A1BBA9D.template.json | 36 +++ .../index.d.ts | 1 + .../index.js | 209 +++++++++++++++++ .../index.ts | 221 ++++++++++++++++++ .../aws-cdk-neptune-integ.assets.json | 17 +- .../aws-cdk-neptune-integ.template.json | 105 +++++++++ .../test/cluster.integ.snapshot/integ.json | 10 +- .../test/cluster.integ.snapshot/manifest.json | 93 +++++--- .../test/cluster.integ.snapshot/tree.json | 194 ++++++++++++++- .../@aws-cdk/aws-neptune/test/cluster.test.ts | 59 ++++- .../aws-neptune/test/integ.cluster.ts | 7 +- 16 files changed, 1023 insertions(+), 49 deletions(-) create mode 100644 packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/ClusterTestDefaultTestDeployAssert6A1BBA9D.assets.json create mode 100644 packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/ClusterTestDefaultTestDeployAssert6A1BBA9D.template.json create mode 100644 packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.d.ts create mode 100644 packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.js create mode 100644 packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.ts diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index bb1c45dbcfd55..f65ea9ffae5dc 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -143,3 +143,36 @@ new neptune.DatabaseCluster(this, 'Cluster', { autoMinorVersionUpgrade: true, }); ``` + +## Logging + +Neptune supports various methods for monitoring performance and usage. One of those methods is logging + +1. Neptune provides logs e.g. audit logs which can be viewed or downloaded via the AWS Console. Audit logs can be enabled using the `neptune_enable_audit_log` parameter in `ClusterParameterGroup` or `ParameterGroup` +2. Neptune provides the ability to export those logs to CloudWatch Logs + +```ts +// Cluster parameter group with the neptune_enable_audit_log param set to 1 +const clusterParameterGroup = new neptune.ClusterParameterGroup(this, 'ClusterParams', { + description: 'Cluster parameter group', + parameters: { + neptune_enable_audit_log: '1' + }, +}); + +const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + // Audit logs are enabled via the clusterParameterGroup + clusterParameterGroup, + // Optionally configuring audit logs to be exported to CloudWatch Logs + cloudwatchLogsExports: { + logTypes: [neptune.LogType.AUDIT], + logRetention: logs.RetentionDays.ONE_MONTH, + }, +}); +``` + +For more information on monitoring, refer to https://docs.aws.amazon.com/neptune/latest/userguide/monitoring.html. +For more information on audit logs, refer to https://docs.aws.amazon.com/neptune/latest/userguide/auditing.html. +For more information on exporting logs to CloudWatch Logs, refer to https://docs.aws.amazon.com/neptune/latest/userguide/cloudwatch-logs.html. diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts index f30bf46851f0f..b2a20617f961a 100644 --- a/packages/@aws-cdk/aws-neptune/lib/cluster.ts +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -1,6 +1,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import * as logs from '@aws-cdk/aws-logs'; import { Aws, Duration, IResource, Lazy, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Endpoint } from './endpoint'; @@ -70,6 +71,45 @@ export class EngineVersion { public constructor(public readonly version: string) {} } +/** + * Neptune log types that can be exported to CloudWatch logs + * + * @see https://docs.aws.amazon.com/neptune/latest/userguide/cloudwatch-logs.html + */ +export class LogType { + /** + * Audit logs + * + * @see @see https://docs.aws.amazon.com/neptune/latest/userguide/auditing.html + */ + public static readonly AUDIT = new LogType('audit'); + + /** + * Constructor for specifying a custom log type + * @param value the log type + */ + public constructor(public readonly value: string) {} +} + +/** + * Configurations to control exporting Neptune logs to CloudWatch Logs + */ +export interface CloudwatchLogsExports { + /** + * Log types that will be exported to CloudWatch Logs + */ + readonly logTypes: LogType[]; + + /** + * 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 `INFINITE`. + * + * @default logs.RetentionDays.INFINITE + */ + readonly logRetention?: logs.RetentionDays; +} + /** * Properties for a new database cluster */ @@ -242,6 +282,18 @@ export interface DatabaseClusterProps { * @default - false */ readonly autoMinorVersionUpgrade?: boolean; + + /** + * Configurations for exporting Neptune logs to CloudWatch Logs + * + * Exporting to CloudWatch Logs also requires enabling logging using parameter groups + * + * @see https://docs.aws.amazon.com/neptune/latest/userguide/cloudwatch-logs.html + * @see https://docs.aws.amazon.com/neptune/latest/userguide/auditing.html#auditing-enable + * + * @default - no logs are exported to CloudWatch logs + */ + readonly cloudwatchLogsExports?: CloudwatchLogsExports; } /** @@ -509,6 +561,8 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu preferredMaintenanceWindow: props.preferredMaintenanceWindow, // Encryption kmsKeyId: props.kmsKey?.keyArn, + // CloudWatch Logs exports + enableCloudwatchLogsExports: props.cloudwatchLogsExports?.logTypes.map(logType => logType.value), storageEncrypted, }); @@ -523,6 +577,17 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu this.clusterEndpoint = new Endpoint(cluster.attrEndpoint, port); this.clusterReadEndpoint = new Endpoint(cluster.attrReadEndpoint, port); + // Log retention + const retention = props.cloudwatchLogsExports?.logRetention; + if (retention) { + props.cloudwatchLogsExports?.logTypes.forEach(logType => { + new logs.LogRetention(this, `${logType}LogRetention`, { + logGroupName: `/aws/neptune/${this.clusterIdentifier}/${logType.value}`, + retention, + }); + }); + } + // Create the instances const instanceCount = props.instances ?? DatabaseCluster.DEFAULT_NUM_INSTANCES; if (instanceCount < 1) { diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index f1fc53edc893c..8ac6057948b1a 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -93,6 +93,7 @@ "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, @@ -100,6 +101,7 @@ "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, diff --git a/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture index 63e17311b5dc5..c42645f4be506 100644 --- a/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture @@ -2,6 +2,7 @@ import { Duration, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as iam from '@aws-cdk/aws-iam'; import * as ec2 from '@aws-cdk/aws-ec2'; +import * as logs from '@aws-cdk/aws-logs'; import * as neptune from '@aws-cdk/aws-neptune'; class Fixture extends Stack { diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/ClusterTestDefaultTestDeployAssert6A1BBA9D.assets.json b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/ClusterTestDefaultTestDeployAssert6A1BBA9D.assets.json new file mode 100644 index 0000000000000..fed1d52c49ba8 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/ClusterTestDefaultTestDeployAssert6A1BBA9D.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "ClusterTestDefaultTestDeployAssert6A1BBA9D.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/ClusterTestDefaultTestDeployAssert6A1BBA9D.template.json b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/ClusterTestDefaultTestDeployAssert6A1BBA9D.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/ClusterTestDefaultTestDeployAssert6A1BBA9D.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.d.ts b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.d.ts new file mode 100644 index 0000000000000..9bbf5854684b6 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.d.ts @@ -0,0 +1 @@ +export declare function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context): Promise; diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.js b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.js new file mode 100644 index 0000000000000..d8d501f248a23 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.js @@ -0,0 +1,209 @@ +"use strict"; +/* eslint-disable no-console */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handler = void 0; +// eslint-disable-next-line import/no-extraneous-dependencies +const 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. + * @param region to create the log group in + * @param options CloudWatch API SDK options. + */ +async function createLogGroupSafe(logGroupName, region, options) { + // If we set the log retention for a lambda, then due to the async nature of + // Lambda logging there could be a race condition when the same log group is + // already being created by the lambda execution. This can sometime result in + // an error "OperationAbortedException: A conflicting operation is currently + // in progress...Please try again." + // To avoid an error, we do as requested and try again. + let retryCount = options?.maxRetries == undefined ? 10 : options.maxRetries; + const delay = options?.retryOptions?.base == undefined ? 10 : options.retryOptions.base; + do { + try { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', region, ...options }); + await cloudwatchlogs.createLogGroup({ logGroupName }).promise(); + return; + } + catch (error) { + if (error.code === 'ResourceAlreadyExistsException') { + // The log group is already created by the lambda execution + return; + } + if (error.code === 'OperationAbortedException') { + if (retryCount > 0) { + retryCount--; + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + else { + // The log group is still being created by another execution but we are out of retries + throw new Error('Out of attempts to create a logGroup'); + } + } + throw error; + } + } while (true); // exit happens on retry count check +} +//delete a log group +async function deleteLogGroup(logGroupName, region, options) { + let retryCount = options?.maxRetries == undefined ? 10 : options.maxRetries; + const delay = options?.retryOptions?.base == undefined ? 10 : options.retryOptions.base; + do { + try { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', region, ...options }); + await cloudwatchlogs.deleteLogGroup({ logGroupName }).promise(); + return; + } + catch (error) { + if (error.code === 'ResourceNotFoundException') { + // The log group doesn't exist + return; + } + if (error.code === 'OperationAbortedException') { + if (retryCount > 0) { + retryCount--; + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + else { + // The log group is still being deleted by another execution but we are out of retries + throw new Error('Out of attempts to delete a logGroup'); + } + } + throw error; + } + } while (true); // exit happens on retry count check +} +/** + * Puts or deletes a retention policy on a log group. + * + * @param logGroupName the name of the log group to create + * @param region the region of the log group + * @param options CloudWatch API SDK options. + * @param retentionInDays the number of days to retain the log events in the specified log group. + */ +async function setRetentionPolicy(logGroupName, region, options, retentionInDays) { + // The same as in createLogGroupSafe(), here we could end up with the race + // condition where a log group is either already being created or its retention + // policy is being updated. This would result in an OperationAbortedException, + // which we will try to catch and retry the command a number of times before failing + let retryCount = options?.maxRetries == undefined ? 10 : options.maxRetries; + const delay = options?.retryOptions?.base == undefined ? 10 : options.retryOptions.base; + do { + try { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', region, ...options }); + if (!retentionInDays) { + await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise(); + } + else { + await cloudwatchlogs.putRetentionPolicy({ logGroupName, retentionInDays }).promise(); + } + return; + } + catch (error) { + if (error.code === 'OperationAbortedException') { + if (retryCount > 0) { + retryCount--; + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + else { + // The log group is still being created by another execution but we are out of retries + throw new Error('Out of attempts to create a logGroup'); + } + } + throw error; + } + } while (true); // exit happens on retry count check +} +async function handler(event, context) { + try { + console.log(JSON.stringify({ ...event, ResponseURL: '...' })); + // The target log group + const logGroupName = event.ResourceProperties.LogGroupName; + // The region of the target log group + const logGroupRegion = event.ResourceProperties.LogGroupRegion; + // Parse to AWS SDK retry options + const retryOptions = parseRetryOptions(event.ResourceProperties.SdkRetry); + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + // Act on the target log group + await createLogGroupSafe(logGroupName, logGroupRegion, retryOptions); + await setRetentionPolicy(logGroupName, logGroupRegion, retryOptions, parseInt(event.ResourceProperties.RetentionInDays, 10)); + if (event.RequestType === 'Create') { + // Set a retention policy of 1 day on the logs of this very function. + // Due to the async nature of the log group creation, the log group for this function might + // still be not created yet at this point. Therefore we attempt to create it. + // In case it is being created, createLogGroupSafe will handle the conflict. + const region = process.env.AWS_REGION; + await createLogGroupSafe(`/aws/lambda/${context.functionName}`, region, retryOptions); + // If createLogGroupSafe fails, the log group is not created even after multiple attempts. + // In this case we have nothing to set the retention policy on but an exception will skip + // the next line. + await setRetentionPolicy(`/aws/lambda/${context.functionName}`, region, retryOptions, 1); + } + } + //When the requestType is delete, delete the log group if the removal policy is delete + if (event.RequestType === 'Delete' && event.ResourceProperties.RemovalPolicy === 'destroy') { + await deleteLogGroup(logGroupName, logGroupRegion, retryOptions); + //else retain the log group + } + await respond('SUCCESS', 'OK', logGroupName); + } + catch (e) { + console.log(e); + await respond('FAILED', e.message, event.ResourceProperties.LogGroupName); + } + function respond(responseStatus, reason, physicalResourceId) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: reason, + PhysicalResourceId: physicalResourceId, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + Data: { + // Add log group name as part of the response so that it's available via Fn::GetAtt + LogGroupName: event.ResourceProperties.LogGroupName, + }, + }); + console.log('Responding', responseBody); + // eslint-disable-next-line @typescript-eslint/no-require-imports + 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 { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const request = require('https').request(requestOptions, resolve); + request.on('error', reject); + request.write(responseBody); + request.end(); + } + catch (e) { + reject(e); + } + }); + } + function parseRetryOptions(rawOptions) { + const retryOptions = {}; + if (rawOptions) { + if (rawOptions.maxRetries) { + retryOptions.maxRetries = parseInt(rawOptions.maxRetries, 10); + } + if (rawOptions.base) { + retryOptions.retryOptions = { + base: parseInt(rawOptions.base, 10), + }; + } + } + return retryOptions; + } +} +exports.handler = handler; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsK0JBQStCOzs7QUFFL0IsNkRBQTZEO0FBQzdELCtCQUErQjtBQVMvQjs7Ozs7O0dBTUc7QUFDSCxLQUFLLFVBQVUsa0JBQWtCLENBQUMsWUFBb0IsRUFBRSxNQUFlLEVBQUUsT0FBeUI7SUFDaEcsNEVBQTRFO0lBQzVFLDRFQUE0RTtJQUM1RSw2RUFBNkU7SUFDN0UsNEVBQTRFO0lBQzVFLG1DQUFtQztJQUNuQyx1REFBdUQ7SUFDdkQsSUFBSSxVQUFVLEdBQUcsT0FBTyxFQUFFLFVBQVUsSUFBSSxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQztJQUM1RSxNQUFNLEtBQUssR0FBRyxPQUFPLEVBQUUsWUFBWSxFQUFFLElBQUksSUFBSSxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUM7SUFDeEYsR0FBRztRQUNELElBQUk7WUFDRixNQUFNLGNBQWMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxjQUFjLENBQUMsRUFBRSxVQUFVLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxHQUFHLE9BQU8sRUFBRSxDQUFDLENBQUM7WUFDaEcsTUFBTSxjQUFjLENBQUMsY0FBYyxDQUFDLEVBQUUsWUFBWSxFQUFFLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUNoRSxPQUFPO1NBQ1I7UUFBQyxPQUFPLEtBQUssRUFBRTtZQUNkLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxnQ0FBZ0MsRUFBRTtnQkFDbkQsMkRBQTJEO2dCQUMzRCxPQUFPO2FBQ1I7WUFDRCxJQUFJLEtBQUssQ0FBQyxJQUFJLEtBQUssMkJBQTJCLEVBQUU7Z0JBQzlDLElBQUksVUFBVSxHQUFHLENBQUMsRUFBRTtvQkFDbEIsVUFBVSxFQUFF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QUUsTUFBTSxDQUFDLENBQUM7Z0JBQzVCLE9BQU8sQ0FBQyxLQUFLLENBQUMsWUFBWSxDQUFDLENBQUM7Z0JBQzVCLE9BQU8sQ0FBQyxHQUFHLEVBQUUsQ0FBQzthQUNmO1lBQUMsT0FBTyxDQUFDLEVBQUU7Z0JBQ1YsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDO2FBQ1g7UUFDSCxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFRCxTQUFTLGlCQUFpQixDQUFDLFVBQWU7UUFDeEMsTUFBTSxZQUFZLEdBQW9CLEVBQUUsQ0FBQztRQUN6QyxJQUFJLFVBQVUsRUFBRTtZQUNkLElBQUksVUFBVSxDQUFDLFVBQVUsRUFBRTtnQkFDekIsWUFBWSxDQUFDLFVBQVUsR0FBRyxRQUFRLENBQUMsVUFBVSxDQUFDLFVBQVUsRUFBRSxFQUFFLENBQUMsQ0FBQzthQUMvRDtZQUNELElBQUksVUFBVSxDQUFDLElBQUksRUFBRTtnQkFDbkIsWUFBWSxDQUFDLFlBQVksR0FBRztvQkFDMUIsSUFBSSxFQUFFLFFBQVEsQ0FBQyxVQUFVLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQztpQkFDcEMsQ0FBQzthQUNIO1NBQ0Y7UUFDRCxPQUFPLFlBQVksQ0FBQztJQUN0QixDQUFDO0FBQ0gsQ0FBQztBQWpHRCwwQkFpR0MiLCJzb3VyY2VzQ29udGVudCI6WyIvKiBlc2xpbnQtZGlzYWJsZSBuby1jb25zb2xlICovXG5cbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBpbXBvcnQvbm8tZXh0cmFuZW91cy1kZXBlbmRlbmNpZXNcbmltcG9ydCAqIGFzIEFXUyBmcm9tICdhd3Mtc2RrJztcbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBpbXBvcnQvbm8tZXh0cmFuZW91cy1kZXBlbmRlbmNpZXNcbmltcG9ydCB0eXBlIHsgUmV0cnlEZWxheU9wdGlvbnMgfSBmcm9tICdhd3Mtc2RrL2xpYi9jb25maWctYmFzZSc7XG5cbmludGVyZmFjZSBTZGtSZXRyeU9wdGlvbnMge1xuICBtYXhSZXRyaWVzPzogbnVtYmVyO1xuICByZXRyeU9wdGlvbnM/OiBSZXRyeURlbGF5T3B0aW9ucztcbn1cblxuLyoqXG4gKiBDcmVhdGVzIGEgbG9nIGdyb3VwIGFuZCBkb2Vzbid0IHRocm93IGlmIGl0IGV4aXN0cy5cbiAqXG4gKiBAcGFyYW0gbG9nR3JvdXBOYW1lIHRoZSBuYW1lIG9mIHRoZSBsb2cgZ3JvdXAgdG8gY3JlYXRlLlxuICogQHBhcmFtIHJlZ2lvbiB0byBjcmVhdGUgdGhlIGxvZyBncm91cCBpblxuICogQHBhcmFtIG9wdGlvbnMgQ2xvdWRXYXRjaCBBUEkgU0RLIG9wdGlvbnMuXG4gKi9cbmFzeW5jIGZ1bmN0aW9uIGNyZWF0ZUxvZ0dyb3VwU2FmZShsb2dHcm91cE5hbWU6IHN0cmluZywgcmVnaW9uPzogc3RyaW5nLCBvcHRpb25zPzogU2RrUmV0cnlPcHRpb25zKSB7XG4gIC8vIElmIHdlIHNldCB0aGUgbG9nIHJldGVudGlvbiBmb3IgYSBsYW1iZGEsIHRoZW4gZHVlIHRvIHRoZSBhc3luYyBuYXR1cmUgb2ZcbiAgLy8gTGFtYmRhIGxvZ2dpbmcgdGhlcmUgY291bGQgYmUgYSByYWNlIGNvbmRpdGlvbiB3aGVuIHRoZSBzYW1lIGxvZyBncm91cCBpc1xuICAvLyBhbHJlYWR5IGJlaW5nIGNyZWF0ZWQgYnkgdGhlIGxhbWJkYSBleGVjdXRpb24uIFRoaXMgY2FuIHNvbWV0aW1lIHJlc3VsdCBpblxuICAvLyBhbiBlcnJvciBcIk9wZXJhdGlvbkFib3J0ZWRFeGNlcHRpb246IEEgY29uZmxpY3Rpbmcgb3BlcmF0aW9uIGlzIGN1cnJlbnRseVxuICAvLyBpbiBwcm9ncmVzcy4uLlBsZWFzZSB0cnkgYWdhaW4uXCJcbiAgLy8gVG8gYXZvaWQgYW4gZXJyb3IsIHdlIGRvIGFzIHJlcXVlc3RlZCBhbmQgdHJ5IGFnYWluLlxuICBsZXQgcmV0cnlDb3VudCA9IG9wdGlvbnM/Lm1heFJldHJpZXMgPT0gdW5kZWZpbmVkID8gMTAgOiBvcHRpb25zLm1heFJldHJpZXM7XG4gIGNvbnN0IGRlbGF5ID0gb3B0aW9ucz8ucmV0cnlPcHRpb25zPy5iYXNlID09IHVuZGVmaW5lZCA/IDEwIDogb3B0aW9ucy5yZXRyeU9wdGlvbnMuYmFzZTtcbiAgZG8ge1xuICAgIHRyeSB7XG4gICAgICBjb25zdCBjbG91ZHdhdGNobG9ncyA9IG5ldyBBV1MuQ2xvdWRXYXRjaExvZ3MoeyBhcGlWZXJzaW9uOiAnMjAxNC0wMy0yOCcsIHJlZ2lvbiwgLi4ub3B0aW9ucyB9KTtcbiAgICAgIGF3YWl0IGNsb3Vkd2F0Y2hsb2dzLmNyZWF0ZUxvZ0dyb3VwKHsgbG9nR3JvdXBOYW1lIH0pLnByb21pc2UoKTtcbiAgICAgIHJldHVybjtcbiAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgaWYgKGVycm9yLmNvZGUgPT09ICdSZXNvdXJjZUFscmVhZHlFeGlzdHNFeGNlcHRpb24nKSB7XG4gICAgICAgIC8vIFRoZSBsb2cgZ3JvdXAgaXMgYWxyZWFkeSBjcmVhdGVkIGJ5IHRoZSBsYW1iZGEgZXhlY3V0aW9uXG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIGlmIChlcnJvci5jb2RlID09PSAnT3BlcmF0aW9uQWJvcnRlZEV4Y2VwdGlvbicpIHtcbiAgICAgICAgaWYgKHJldHJ5Q291bnQgPiAwKSB7XG4gICAgICAgICAgcmV0cnlDb3VudC0tO1xuICAgICAgICAgIGF3YWl0IG5ldyBQcm9taXNlKHJlc29sdmUgPT4gc2V0VGltZW91dChyZXNvbHZlLCBkZWxheSkpO1xuICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIC8vIFRoZSBsb2cgZ3JvdXAgaXMgc3RpbGwgYmVpbmcgY3JlYXRlZCBieSBhbm90aGVyIGV4ZWN1dGlvbiBidXQgd2UgYXJlIG91dCBvZiByZXRyaWVzXG4gICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdPdXQgb2YgYXR0ZW1wdHMgdG8gY3JlYXRlIGEgbG9nR3JvdXAnKTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgdGhyb3cgZXJyb3I7XG4gICAgfVxuICB9IHdoaWxlICh0cnVlKTsgLy8gZXhpdCBoYXBwZW5zIG9uIHJldHJ5IGNvdW50IGNoZWNrXG59XG5cbi8vZGVsZXRlIGEgbG9nIGdyb3VwXG5hc3luYyBmdW5jdGlvbiBkZWxldGVMb2dHcm91cChsb2dHcm91cE5hbWU6IHN0cmluZywgcmVnaW9uPzogc3RyaW5nLCBvcHRpb25zPzogU2RrUmV0cnlPcHRpb25zKSB7XG4gIGxldCByZXRyeUNvdW50ID0gb3B0aW9ucz8ubWF4UmV0cmllcyA9PSB1bmRlZmluZWQgPyAxMCA6IG9wdGlvbnMubWF4UmV0cmllcztcbiAgY29uc3QgZGVsYXkgPSBvcHRpb25zPy5yZXRyeU9wdGlvbnM/LmJhc2UgPT0gdW5kZWZpbmVkID8gMTAgOiBvcHRpb25zLnJldHJ5T3B0aW9ucy5iYXNlO1xuICBkbyB7XG4gICAgdHJ5IHtcbiAgICAgIGNvbnN0IGNsb3Vkd2F0Y2hsb2dzID0gbmV3IEFXUy5DbG91ZFdhdGNoTG9ncyh7IGFwaVZlcnNpb246ICcyMDE0LTAzLTI4JywgcmVnaW9uLCAuLi5vcHRpb25zIH0pO1xuICAgICAgYXdhaXQgY2xvdWR3YXRjaGxvZ3MuZGVsZXRlTG9nR3JvdXAoeyBsb2dHcm91cE5hbWUgfSkucHJvbWlzZSgpO1xuICAgICAgcmV0dXJuO1xuICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICBpZiAoZXJyb3IuY29kZSA9PT0gJ1Jlc291cmNlTm90Rm91bmRFeGNlcHRpb24nKSB7XG4gICAgICAgIC8vIFRoZSBsb2cgZ3JvdXAgZG9lc24ndCBleGlzdFxuICAgICAgICByZXR1cm47XG4gICAgICB9XG4gICAgICBpZiAoZXJyb3IuY29kZSA9PT0gJ09wZXJhdGlvbkFib3J0ZWRFeGNlcHRpb24nKSB7XG4gICAgICAgIGlmIChyZXRyeUNvdW50ID4gMCkge1xuICAgICAgICAgIHJldHJ5Q291bnQtLTtcbiAgICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZShyZXNvbHZlID0+IHNldFRpbWVvdXQocmVzb2x2ZSwgZGVsYXkpKTtcbiAgICAgICAgICBjb250aW51ZTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAvLyBUaGUgbG9nIGdyb3VwIGlzIHN0aWxsIGJlaW5nIGRlbGV0ZWQgYnkgYW5vdGhlciBleGVjdXRpb24gYnV0IHdlIGFyZSBvdXQgb2YgcmV0cmllc1xuICAgICAgICAgIHRocm93IG5ldyBFcnJvcignT3V0IG9mIGF0dGVtcHRzIHRvIGRlbGV0ZSBhIGxvZ0dyb3VwJyk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIHRocm93IGVycm9yO1xuICAgIH1cbiAgfSB3aGlsZSAodHJ1ZSk7IC8vIGV4aXQgaGFwcGVucyBvbiByZXRyeSBjb3VudCBjaGVja1xufVxuXG4vKipcbiAqIFB1dHMgb3IgZGVsZXRlcyBhIHJldGVudGlvbiBwb2xpY3kgb24gYSBsb2cgZ3JvdXAuXG4gKlxuICogQHBhcmFtIGxvZ0dyb3VwTmFtZSB0aGUgbmFtZSBvZiB0aGUgbG9nIGdyb3VwIHRvIGNyZWF0ZVxuICogQHBhcmFtIHJlZ2lvbiB0aGUgcmVnaW9uIG9mIHRoZSBsb2cgZ3JvdXBcbiAqIEBwYXJhbSBvcHRpb25zIENsb3VkV2F0Y2ggQVBJIFNESyBvcHRpb25zLlxuICogQHBhcmFtIHJldGVudGlvbkluRGF5cyB0aGUgbnVtYmVyIG9mIGRheXMgdG8gcmV0YWluIHRoZSBsb2cgZXZlbnRzIGluIHRoZSBzcGVjaWZpZWQgbG9nIGdyb3VwLlxuICovXG5hc3luYyBmdW5jdGlvbiBzZXRSZXRlbnRpb25Qb2xpY3kobG9nR3JvdXBOYW1lOiBzdHJpbmcsIHJlZ2lvbj86IHN0cmluZywgb3B0aW9ucz86IFNka1JldHJ5T3B0aW9ucywgcmV0ZW50aW9uSW5EYXlzPzogbnVtYmVyKSB7XG4gIC8vIFRoZSBzYW1lIGFzIGluIGNyZWF0ZUxvZ0dyb3VwU2FmZSgpLCBoZXJlIHdlIGNvdWxkIGVuZCB1cCB3aXRoIHRoZSByYWNlXG4gIC8vIGNvbmRpdGlvbiB3aGVyZSBhIGxvZyBncm91cCBpcyBlaXRoZXIgYWxyZWFkeSBiZWluZyBjcmVhdGVkIG9yIGl0cyByZXRlbnRpb25cbiAgLy8gcG9saWN5IGlzIGJlaW5nIHVwZGF0ZWQuIFRoaXMgd291bGQgcmVzdWx0IGluIGFuIE9wZXJhdGlvbkFib3J0ZWRFeGNlcHRpb24sXG4gIC8vIHdoaWNoIHdlIHdpbGwgdHJ5IHRvIGNhdGNoIGFuZCByZXRyeSB0aGUgY29tbWFuZCBhIG51bWJlciBvZiB0aW1lcyBiZWZvcmUgZmFpbGluZ1xuICBsZXQgcmV0cnlDb3VudCA9IG9wdGlvbnM/Lm1heFJldHJpZXMgPT0gdW5kZWZpbmVkID8gMTAgOiBvcHRpb25zLm1heFJldHJpZXM7XG4gIGNvbnN0IGRlbGF5ID0gb3B0aW9ucz8ucmV0cnlPcHRpb25zPy5iYXNlID09IHVuZGVmaW5lZCA/IDEwIDogb3B0aW9ucy5yZXRyeU9wdGlvbnMuYmFzZTtcbiAgZG8ge1xuICAgIHRyeSB7XG4gICAgICBjb25zdCBjbG91ZHdhdGNobG9ncyA9IG5ldyBBV1MuQ2xvdWRXYXRjaExvZ3MoeyBhcGlWZXJzaW9uOiAnMjAxNC0wMy0yOCcsIHJlZ2lvbiwgLi4ub3B0aW9ucyB9KTtcbiAgICAgIGlmICghcmV0ZW50aW9uSW5EYXlzKSB7XG4gICAgICAgIGF3YWl0IGNsb3Vkd2F0Y2hsb2dzLmRlbGV0ZVJldGVudGlvblBvbGljeSh7IGxvZ0dyb3VwTmFtZSB9KS5wcm9taXNlKCk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBhd2FpdCBjbG91ZHdhdGNobG9ncy5wdXRSZXRlbnRpb25Qb2xpY3koeyBsb2dHcm91cE5hbWUsIHJldGVudGlvbkluRGF5cyB9KS5wcm9taXNlKCk7XG4gICAgICB9XG4gICAgICByZXR1cm47XG5cbiAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgaWYgKGVycm9yLmNvZGUgPT09ICdPcGVyYXRpb25BYm9ydGVkRXhjZXB0aW9uJykge1xuICAgICAgICBpZiAocmV0cnlDb3VudCA+IDApIHtcbiAgICAgICAgICByZXRyeUNvdW50LS07XG4gICAgICAgICAgYXdhaXQgbmV3IFByb21pc2UocmVzb2x2ZSA9PiBzZXRUaW1lb3V0KHJlc29sdmUsIGRlbGF5KSk7XG4gICAgICAgICAgY29udGludWU7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgLy8gVGhlIGxvZyBncm91cCBpcyBzdGlsbCBiZWluZyBjcmVhdGVkIGJ5IGFub3RoZXIgZXhlY3V0aW9uIGJ1dCB3ZSBhcmUgb3V0IG9mIHJldHJpZXNcbiAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ091dCBvZiBhdHRlbXB0cyB0byBjcmVhdGUgYSBsb2dHcm91cCcpO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgICB0aHJvdyBlcnJvcjtcbiAgICB9XG4gIH0gd2hpbGUgKHRydWUpOyAvLyBleGl0IGhhcHBlbnMgb24gcmV0cnkgY291bnQgY2hlY2tcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGhhbmRsZXIoZXZlbnQ6IEFXU0xhbWJkYS5DbG91ZEZvcm1hdGlvbkN1c3RvbVJlc291cmNlRXZlbnQsIGNvbnRleHQ6IEFXU0xhbWJkYS5Db250ZXh0KSB7XG4gIHRyeSB7XG4gICAgY29uc29sZS5sb2coSlNPTi5zdHJpbmdpZnkoeyAuLi5ldmVudCwgUmVzcG9uc2VVUkw6ICcuLi4nIH0pKTtcblxuICAgIC8vIFRoZSB0YXJnZXQgbG9nIGdyb3VwXG4gICAgY29uc3QgbG9nR3JvdXBOYW1lID0gZXZlbnQuUmVzb3VyY2VQcm9wZXJ0aWVzLkxvZ0dyb3VwTmFtZTtcblxuICAgIC8vIFRoZSByZWdpb24gb2YgdGhlIHRhcmdldCBsb2cgZ3JvdXBcbiAgICBjb25zdCBsb2dHcm91cFJlZ2lvbiA9IGV2ZW50LlJlc291cmNlUHJvcGVydGllcy5Mb2dHcm91cFJlZ2lvbjtcblxuICAgIC8vIFBhcnNlIHRvIEFXUyBTREsgcmV0cnkgb3B0aW9uc1xuICAgIGNvbnN0IHJldHJ5T3B0aW9ucyA9IHBhcnNlUmV0cnlPcHRpb25zKGV2ZW50LlJlc291cmNlUHJvcGVydGllcy5TZGtSZXRyeSk7XG5cbiAgICBpZiAoZXZlbnQuUmVxdWVzdFR5cGUgPT09ICdDcmVhdGUnIHx8IGV2ZW50LlJlcXVlc3RUeXBlID09PSAnVXBkYXRlJykge1xuICAgICAgLy8gQWN0IG9uIHRoZSB0YXJnZXQgbG9nIGdyb3VwXG4gICAgICBhd2FpdCBjcmVhdGVMb2dHcm91cFNhZmUobG9nR3JvdXBOYW1lLCBsb2dHcm91cFJlZ2lvbiwgcmV0cnlPcHRpb25zKTtcbiAgICAgIGF3YWl0IHNldFJldGVudGlvblBvbGljeShsb2dHcm91cE5hbWUsIGxvZ0dyb3VwUmVnaW9uLCByZXRyeU9wdGlvbnMsIHBhcnNlSW50KGV2ZW50LlJlc291cmNlUHJvcGVydGllcy5SZXRlbnRpb25JbkRheXMsIDEwKSk7XG5cbiAgICAgIGlmIChldmVudC5SZXF1ZXN0VHlwZSA9PT0gJ0NyZWF0ZScpIHtcbiAgICAgICAgLy8gU2V0IGEgcmV0ZW50aW9uIHBvbGljeSBvZiAxIGRheSBvbiB0aGUgbG9ncyBvZiB0aGlzIHZlcnkgZnVuY3Rpb24uXG4gICAgICAgIC8vIER1ZSB0byB0aGUgYXN5bmMgbmF0dXJlIG9mIHRoZSBsb2cgZ3JvdXAgY3JlYXRpb24sIHRoZSBsb2cgZ3JvdXAgZm9yIHRoaXMgZnVuY3Rpb24gbWlnaHRcbiAgICAgICAgLy8gc3RpbGwgYmUgbm90IGNyZWF0ZWQgeWV0IGF0IHRoaXMgcG9pbnQuIFRoZXJlZm9yZSB3ZSBhdHRlbXB0IHRvIGNyZWF0ZSBpdC5cbiAgICAgICAgLy8gSW4gY2FzZSBpdCBpcyBiZWluZyBjcmVhdGVkLCBjcmVhdGVMb2dHcm91cFNhZmUgd2lsbCBoYW5kbGUgdGhlIGNvbmZsaWN0LlxuICAgICAgICBjb25zdCByZWdpb24gPSBwcm9jZXNzLmVudi5BV1NfUkVHSU9OO1xuICAgICAgICBhd2FpdCBjcmVhdGVMb2dHcm91cFNhZmUoYC9hd3MvbGFtYmRhLyR7Y29udGV4dC5mdW5jdGlvbk5hbWV9YCwgcmVnaW9uLCByZXRyeU9wdGlvbnMpO1xuICAgICAgICAvLyBJZiBjcmVhdGVMb2dHcm91cFNhZmUgZmFpbHMsIHRoZSBsb2cgZ3JvdXAgaXMgbm90IGNyZWF0ZWQgZXZlbiBhZnRlciBtdWx0aXBsZSBhdHRlbXB0cy5cbiAgICAgICAgLy8gSW4gdGhpcyBjYXNlIHdlIGhhdmUgbm90aGluZyB0byBzZXQgdGhlIHJldGVudGlvbiBwb2xpY3kgb24gYnV0IGFuIGV4Y2VwdGlvbiB3aWxsIHNraXBcbiAgICAgICAgLy8gdGhlIG5leHQgbGluZS5cbiAgICAgICAgYXdhaXQgc2V0UmV0ZW50aW9uUG9saWN5KGAvYXdzL2xhbWJkYS8ke2NvbnRleHQuZnVuY3Rpb25OYW1lfWAsIHJlZ2lvbiwgcmV0cnlPcHRpb25zLCAxKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvL1doZW4gdGhlIHJlcXVlc3RUeXBlIGlzIGRlbGV0ZSwgZGVsZXRlIHRoZSBsb2cgZ3JvdXAgaWYgdGhlIHJlbW92YWwgcG9saWN5IGlzIGRlbGV0ZVxuICAgIGlmIChldmVudC5SZXF1ZXN0VHlwZSA9PT0gJ0RlbGV0ZScgJiYgZXZlbnQuUmVzb3VyY2VQcm9wZXJ0aWVzLlJlbW92YWxQb2xpY3kgPT09ICdkZXN0cm95Jykge1xuICAgICAgYXdhaXQgZGVsZXRlTG9nR3JvdXAobG9nR3JvdXBOYW1lLCBsb2dHcm91cFJlZ2lvbiwgcmV0cnlPcHRpb25zKTtcbiAgICAgIC8vZWxzZSByZXRhaW4gdGhlIGxvZyBncm91cFxuICAgIH1cblxuICAgIGF3YWl0IHJlc3BvbmQoJ1NVQ0NFU1MnLCAnT0snLCBsb2dHcm91cE5hbWUpO1xuICB9IGNhdGNoIChlKSB7XG4gICAgY29uc29sZS5sb2coZSk7XG5cbiAgICBhd2FpdCByZXNwb25kKCdGQUlMRUQnLCBlLm1lc3NhZ2UsIGV2ZW50LlJlc291cmNlUHJvcGVydGllcy5Mb2dHcm91cE5hbWUpO1xuICB9XG5cbiAgZnVuY3Rpb24gcmVzcG9uZChyZXNwb25zZVN0YXR1czogc3RyaW5nLCByZWFzb246IHN0cmluZywgcGh5c2ljYWxSZXNvdXJjZUlkOiBzdHJpbmcpIHtcbiAgICBjb25zdCByZXNwb25zZUJvZHkgPSBKU09OLnN0cmluZ2lmeSh7XG4gICAgICBTdGF0dXM6IHJlc3BvbnNlU3RhdHVzLFxuICAgICAgUmVhc29uOiByZWFzb24sXG4gICAgICBQaHlzaWNhbFJlc291cmNlSWQ6IHBoeXNpY2FsUmVzb3VyY2VJZCxcbiAgICAgIFN0YWNrSWQ6IGV2ZW50LlN0YWNrSWQsXG4gICAgICBSZXF1ZXN0SWQ6IGV2ZW50LlJlcXVlc3RJZCxcbiAgICAgIExvZ2ljYWxSZXNvdXJjZUlkOiBldmVudC5Mb2dpY2FsUmVzb3VyY2VJZCxcbiAgICAgIERhdGE6IHtcbiAgICAgICAgLy8gQWRkIGxvZyBncm91cCBuYW1lIGFzIHBhcnQgb2YgdGhlIHJlc3BvbnNlIHNvIHRoYXQgaXQncyBhdmFpbGFibGUgdmlhIEZuOjpHZXRBdHRcbiAgICAgICAgTG9nR3JvdXBOYW1lOiBldmVudC5SZXNvdXJjZVByb3BlcnRpZXMuTG9nR3JvdXBOYW1lLFxuICAgICAgfSxcbiAgICB9KTtcblxuICAgIGNvbnNvbGUubG9nKCdSZXNwb25kaW5nJywgcmVzcG9uc2VCb2R5KTtcblxuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tcmVxdWlyZS1pbXBvcnRzXG4gICAgY29uc3QgcGFyc2VkVXJsID0gcmVxdWlyZSgndXJsJykucGFyc2UoZXZlbnQuUmVzcG9uc2VVUkwpO1xuICAgIGNvbnN0IHJlcXVlc3RPcHRpb25zID0ge1xuICAgICAgaG9zdG5hbWU6IHBhcnNlZFVybC5ob3N0bmFtZSxcbiAgICAgIHBhdGg6IHBhcnNlZFVybC5wYXRoLFxuICAgICAgbWV0aG9kOiAnUFVUJyxcbiAgICAgIGhlYWRlcnM6IHsgJ2NvbnRlbnQtdHlwZSc6ICcnLCAnY29udGVudC1sZW5ndGgnOiByZXNwb25zZUJvZHkubGVuZ3RoIH0sXG4gICAgfTtcblxuICAgIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICB0cnkge1xuICAgICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgQHR5cGVzY3JpcHQtZXNsaW50L25vLXJlcXVpcmUtaW1wb3J0c1xuICAgICAgICBjb25zdCByZXF1ZXN0ID0gcmVxdWlyZSgnaHR0cHMnKS5yZXF1ZXN0KHJlcXVlc3RPcHRpb25zLCByZXNvbHZlKTtcbiAgICAgICAgcmVxdWVzdC5vbignZXJyb3InLCByZWplY3QpO1xuICAgICAgICByZXF1ZXN0LndyaXRlKHJlc3BvbnNlQm9keSk7XG4gICAgICAgIHJlcXVlc3QuZW5kKCk7XG4gICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIHJlamVjdChlKTtcbiAgICAgIH1cbiAgICB9KTtcbiAgfVxuXG4gIGZ1bmN0aW9uIHBhcnNlUmV0cnlPcHRpb25zKHJhd09wdGlvbnM6IGFueSk6IFNka1JldHJ5T3B0aW9ucyB7XG4gICAgY29uc3QgcmV0cnlPcHRpb25zOiBTZGtSZXRyeU9wdGlvbnMgPSB7fTtcbiAgICBpZiAocmF3T3B0aW9ucykge1xuICAgICAgaWYgKHJhd09wdGlvbnMubWF4UmV0cmllcykge1xuICAgICAgICByZXRyeU9wdGlvbnMubWF4UmV0cmllcyA9IHBhcnNlSW50KHJhd09wdGlvbnMubWF4UmV0cmllcywgMTApO1xuICAgICAgfVxuICAgICAgaWYgKHJhd09wdGlvbnMuYmFzZSkge1xuICAgICAgICByZXRyeU9wdGlvbnMucmV0cnlPcHRpb25zID0ge1xuICAgICAgICAgIGJhc2U6IHBhcnNlSW50KHJhd09wdGlvbnMuYmFzZSwgMTApLFxuICAgICAgICB9O1xuICAgICAgfVxuICAgIH1cbiAgICByZXR1cm4gcmV0cnlPcHRpb25zO1xuICB9XG59XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.ts b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.ts new file mode 100644 index 0000000000000..1bb38a9f3d774 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347/index.ts @@ -0,0 +1,221 @@ +/* eslint-disable no-console */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import * as AWS from 'aws-sdk'; +// eslint-disable-next-line import/no-extraneous-dependencies +import type { RetryDelayOptions } from 'aws-sdk/lib/config-base'; + +interface SdkRetryOptions { + maxRetries?: number; + retryOptions?: RetryDelayOptions; +} + +/** + * Creates a log group and doesn't throw if it exists. + * + * @param logGroupName the name of the log group to create. + * @param region to create the log group in + * @param options CloudWatch API SDK options. + */ +async function createLogGroupSafe(logGroupName: string, region?: string, options?: SdkRetryOptions) { + // If we set the log retention for a lambda, then due to the async nature of + // Lambda logging there could be a race condition when the same log group is + // already being created by the lambda execution. This can sometime result in + // an error "OperationAbortedException: A conflicting operation is currently + // in progress...Please try again." + // To avoid an error, we do as requested and try again. + let retryCount = options?.maxRetries == undefined ? 10 : options.maxRetries; + const delay = options?.retryOptions?.base == undefined ? 10 : options.retryOptions.base; + do { + try { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', region, ...options }); + await cloudwatchlogs.createLogGroup({ logGroupName }).promise(); + return; + } catch (error) { + if (error.code === 'ResourceAlreadyExistsException') { + // The log group is already created by the lambda execution + return; + } + if (error.code === 'OperationAbortedException') { + if (retryCount > 0) { + retryCount--; + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } else { + // The log group is still being created by another execution but we are out of retries + throw new Error('Out of attempts to create a logGroup'); + } + } + throw error; + } + } while (true); // exit happens on retry count check +} + +//delete a log group +async function deleteLogGroup(logGroupName: string, region?: string, options?: SdkRetryOptions) { + let retryCount = options?.maxRetries == undefined ? 10 : options.maxRetries; + const delay = options?.retryOptions?.base == undefined ? 10 : options.retryOptions.base; + do { + try { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', region, ...options }); + await cloudwatchlogs.deleteLogGroup({ logGroupName }).promise(); + return; + } catch (error) { + if (error.code === 'ResourceNotFoundException') { + // The log group doesn't exist + return; + } + if (error.code === 'OperationAbortedException') { + if (retryCount > 0) { + retryCount--; + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } else { + // The log group is still being deleted by another execution but we are out of retries + throw new Error('Out of attempts to delete a logGroup'); + } + } + throw error; + } + } while (true); // exit happens on retry count check +} + +/** + * Puts or deletes a retention policy on a log group. + * + * @param logGroupName the name of the log group to create + * @param region the region of the log group + * @param options CloudWatch API SDK options. + * @param retentionInDays the number of days to retain the log events in the specified log group. + */ +async function setRetentionPolicy(logGroupName: string, region?: string, options?: SdkRetryOptions, retentionInDays?: number) { + // The same as in createLogGroupSafe(), here we could end up with the race + // condition where a log group is either already being created or its retention + // policy is being updated. This would result in an OperationAbortedException, + // which we will try to catch and retry the command a number of times before failing + let retryCount = options?.maxRetries == undefined ? 10 : options.maxRetries; + const delay = options?.retryOptions?.base == undefined ? 10 : options.retryOptions.base; + do { + try { + const cloudwatchlogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', region, ...options }); + if (!retentionInDays) { + await cloudwatchlogs.deleteRetentionPolicy({ logGroupName }).promise(); + } else { + await cloudwatchlogs.putRetentionPolicy({ logGroupName, retentionInDays }).promise(); + } + return; + + } catch (error) { + if (error.code === 'OperationAbortedException') { + if (retryCount > 0) { + retryCount--; + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } else { + // The log group is still being created by another execution but we are out of retries + throw new Error('Out of attempts to create a logGroup'); + } + } + throw error; + } + } while (true); // exit happens on retry count check +} + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { + try { + console.log(JSON.stringify({ ...event, ResponseURL: '...' })); + + // The target log group + const logGroupName = event.ResourceProperties.LogGroupName; + + // The region of the target log group + const logGroupRegion = event.ResourceProperties.LogGroupRegion; + + // Parse to AWS SDK retry options + const retryOptions = parseRetryOptions(event.ResourceProperties.SdkRetry); + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + // Act on the target log group + await createLogGroupSafe(logGroupName, logGroupRegion, retryOptions); + await setRetentionPolicy(logGroupName, logGroupRegion, retryOptions, parseInt(event.ResourceProperties.RetentionInDays, 10)); + + if (event.RequestType === 'Create') { + // Set a retention policy of 1 day on the logs of this very function. + // Due to the async nature of the log group creation, the log group for this function might + // still be not created yet at this point. Therefore we attempt to create it. + // In case it is being created, createLogGroupSafe will handle the conflict. + const region = process.env.AWS_REGION; + await createLogGroupSafe(`/aws/lambda/${context.functionName}`, region, retryOptions); + // If createLogGroupSafe fails, the log group is not created even after multiple attempts. + // In this case we have nothing to set the retention policy on but an exception will skip + // the next line. + await setRetentionPolicy(`/aws/lambda/${context.functionName}`, region, retryOptions, 1); + } + } + + //When the requestType is delete, delete the log group if the removal policy is delete + if (event.RequestType === 'Delete' && event.ResourceProperties.RemovalPolicy === 'destroy') { + await deleteLogGroup(logGroupName, logGroupRegion, retryOptions); + //else retain the log group + } + + 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: { + // Add log group name as part of the response so that it's available via Fn::GetAtt + LogGroupName: event.ResourceProperties.LogGroupName, + }, + }); + + console.log('Responding', responseBody); + + // eslint-disable-next-line @typescript-eslint/no-require-imports + 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 { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const request = require('https').request(requestOptions, resolve); + request.on('error', reject); + request.write(responseBody); + request.end(); + } catch (e) { + reject(e); + } + }); + } + + function parseRetryOptions(rawOptions: any): SdkRetryOptions { + const retryOptions: SdkRetryOptions = {}; + if (rawOptions) { + if (rawOptions.maxRetries) { + retryOptions.maxRetries = parseInt(rawOptions.maxRetries, 10); + } + if (rawOptions.base) { + retryOptions.retryOptions = { + base: parseInt(rawOptions.base, 10), + }; + } + } + return retryOptions; + } +} diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/aws-cdk-neptune-integ.assets.json b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/aws-cdk-neptune-integ.assets.json index 406c273a3a088..9ae91a12eb69b 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/aws-cdk-neptune-integ.assets.json +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/aws-cdk-neptune-integ.assets.json @@ -1,7 +1,20 @@ { "version": "21.0.0", "files": { - "315715ffe6004c7bd7c9874629785c10fd8f65a20d6995ea8eb20188dfb82b7d": { + "d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347": { + "source": { + "path": "asset.d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "05e1648bea40cd5f3ddef60ec15e6927f25f55c2a0716a3c25fd7105d554c873": { "source": { "path": "aws-cdk-neptune-integ.template.json", "packaging": "file" @@ -9,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "315715ffe6004c7bd7c9874629785c10fd8f65a20d6995ea8eb20188dfb82b7d.json", + "objectKey": "05e1648bea40cd5f3ddef60ec15e6927f25f55c2a0716a3c25fd7105d554c873.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/aws-cdk-neptune-integ.template.json b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/aws-cdk-neptune-integ.template.json index 67f5870712c2a..7fce71617ab27 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/aws-cdk-neptune-integ.template.json +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/aws-cdk-neptune-integ.template.json @@ -502,6 +502,9 @@ "DBSubnetGroupName": { "Ref": "DatabaseSubnets3C9252C9" }, + "EnableCloudwatchLogsExports": [ + "audit" + ], "KmsKeyId": { "Fn::GetAtt": [ "DbSecurity381C2C15", @@ -521,6 +524,30 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "DatabaseobjectObjectLogRetentionA247AF0C": { + "Type": "Custom::LogRetention", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", + "Arn" + ] + }, + "LogGroupName": { + "Fn::Join": [ + "", + [ + "/aws/neptune/", + { + "Ref": "DatabaseB269D8BB" + }, + "/audit" + ] + ] + }, + "RetentionInDays": 30 + } + }, "DatabaseInstance1844F58FD": { "Type": "AWS::Neptune::DBInstance", "Properties": { @@ -538,6 +565,84 @@ ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "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:DeleteRetentionPolicy", + "logs:PutRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "Roles": [ + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + } + ] + } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "d01c24641c7d8cb6488393ffceaefff282370a9a522bf9d77b21da73fa257347.zip" + }, + "Role": { + "Fn::GetAtt": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", + "Arn" + ] + } + }, + "DependsOn": [ + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + ] } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/integ.json b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/integ.json index 7f298dac51aa6..e2061d5ea4e11 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/integ.json +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/integ.json @@ -1,14 +1,12 @@ { "version": "21.0.0", "testCases": { - "integ.cluster": { + "ClusterTest/DefaultTest": { "stacks": [ "aws-cdk-neptune-integ" ], - "diffAssets": false, - "stackUpdateWorkflow": true + "assertionStack": "ClusterTest/DefaultTest/DeployAssert", + "assertionStackName": "ClusterTestDefaultTestDeployAssert6A1BBA9D" } - }, - "synthContext": {}, - "enableLookups": false + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/manifest.json index c9e9d7159e499..4a91845045428 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/manifest.json @@ -23,7 +23,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/315715ffe6004c7bd7c9874629785c10fd8f65a20d6995ea8eb20188dfb82b7d.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/05e1648bea40cd5f3ddef60ec15e6927f25f55c2a0716a3c25fd7105d554c873.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -213,80 +213,97 @@ "data": "DatabaseB269D8BB" } ], - "/aws-cdk-neptune-integ/Database/Instance1": [ + "/aws-cdk-neptune-integ/Database/[object Object]LogRetention/Resource": [ { "type": "aws:cdk:logicalId", - "data": "DatabaseInstance1844F58FD" + "data": "DatabaseobjectObjectLogRetentionA247AF0C" } ], - "/aws-cdk-neptune-integ/BootstrapVersion": [ + "/aws-cdk-neptune-integ/Database/Instance1": [ { "type": "aws:cdk:logicalId", - "data": "BootstrapVersion" + "data": "DatabaseInstance1844F58FD" } ], - "/aws-cdk-neptune-integ/CheckBootstrapVersion": [ + "/aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/Resource": [ { "type": "aws:cdk:logicalId", - "data": "CheckBootstrapVersion" + "data": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" } ], - "Params1200F93288": [ + "/aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy/Resource": [ { "type": "aws:cdk:logicalId", - "data": "Params1200F93288", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] + "data": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB" } ], - "Database12Subnets4179194B": [ + "/aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Resource": [ { "type": "aws:cdk:logicalId", - "data": "Database12Subnets4179194B", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] + "data": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A" } ], - "Database12SecurityGroup4F4302E8": [ + "/aws-cdk-neptune-integ/BootstrapVersion": [ { "type": "aws:cdk:logicalId", - "data": "Database12SecurityGroup4F4302E8", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] + "data": "BootstrapVersion" } ], - "Database12SecurityGroupfrom00000IndirectPort3A40EE2B": [ + "/aws-cdk-neptune-integ/CheckBootstrapVersion": [ { "type": "aws:cdk:logicalId", - "data": "Database12SecurityGroupfrom00000IndirectPort3A40EE2B", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] + "data": "CheckBootstrapVersion" } + ] + }, + "displayName": "aws-cdk-neptune-integ" + }, + "ClusterTestDefaultTestDeployAssert6A1BBA9D.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ClusterTestDefaultTestDeployAssert6A1BBA9D.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ClusterTestDefaultTestDeployAssert6A1BBA9D": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ClusterTestDefaultTestDeployAssert6A1BBA9D.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ClusterTestDefaultTestDeployAssert6A1BBA9D.assets" ], - "Database12D6A36FB9": [ + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ClusterTestDefaultTestDeployAssert6A1BBA9D.assets" + ], + "metadata": { + "/ClusterTest/DefaultTest/DeployAssert/BootstrapVersion": [ { "type": "aws:cdk:logicalId", - "data": "Database12D6A36FB9", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] + "data": "BootstrapVersion" } ], - "Database12Instance10D9E6224": [ + "/ClusterTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ { "type": "aws:cdk:logicalId", - "data": "Database12Instance10D9E6224", - "trace": [ - "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" - ] + "data": "CheckBootstrapVersion" } ] }, - "displayName": "aws-cdk-neptune-integ" + "displayName": "ClusterTest/DefaultTest/DeployAssert" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/tree.json b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/tree.json index 0d2da0fe35492..911b09880a035 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-neptune/test/cluster.integ.snapshot/tree.json @@ -9,7 +9,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.92" + "version": "10.1.95" } }, "aws-cdk-neptune-integ": { @@ -855,6 +855,9 @@ "dbSubnetGroupName": { "Ref": "DatabaseSubnets3C9252C9" }, + "enableCloudwatchLogsExports": [ + "audit" + ], "kmsKeyId": { "Fn::GetAtt": [ "DbSecurity381C2C15", @@ -877,6 +880,24 @@ "version": "0.0.0" } }, + "[object Object]LogRetention": { + "id": "[object Object]LogRetention", + "path": "aws-cdk-neptune-integ/Database/[object Object]LogRetention", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-neptune-integ/Database/[object Object]LogRetention/Resource", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-logs.LogRetention", + "version": "0.0.0" + } + }, "Instance1": { "id": "Instance1", "path": "aws-cdk-neptune-integ/Database/Instance1", @@ -900,12 +921,183 @@ "fqn": "@aws-cdk/aws-neptune.DatabaseCluster", "version": "0.0.0" } + }, + "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a": { + "id": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a", + "children": { + "Code": { + "id": "Code", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code", + "children": { + "Stage": { + "id": "Stage", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "ServiceRole": { + "id": "ServiceRole", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "logs:DeleteRetentionPolicy", + "logs:PutRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "policyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", + "roles": [ + { + "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-neptune-integ/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Resource", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.95" + } } }, "constructInfo": { "fqn": "@aws-cdk/core.Stack", "version": "0.0.0" } + }, + "ClusterTest": { + "id": "ClusterTest", + "path": "ClusterTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "ClusterTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "ClusterTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.95" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "ClusterTest/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index e92386cfefcb0..b9cf836249c7d 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -2,9 +2,10 @@ import { Match, Template } from '@aws-cdk/assertions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; -import { ClusterParameterGroup, DatabaseCluster, EngineVersion, InstanceType } from '../lib'; +import { ClusterParameterGroup, DatabaseCluster, EngineVersion, InstanceType, LogType } from '../lib'; describe('DatabaseCluster', () => { @@ -660,6 +661,62 @@ describe('DatabaseCluster', () => { }); + test('cloudwatchLogsExports is enabled when configured', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + cloudwatchLogsExports: { + logTypes: [LogType.AUDIT], + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { + EnableCloudwatchLogsExports: ['audit'], + }); + Template.fromStack(stack).resourceCountIs('Custom::LogRetention', 0); + }); + + test('cloudwatchLogsExports log retention is enabled when configured', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + cloudwatchLogsExports: { + logTypes: [LogType.AUDIT], + logRetention: logs.RetentionDays.ONE_MONTH, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Neptune::DBCluster', { + EnableCloudwatchLogsExports: ['audit'], + }); + Template.fromStack(stack).hasResourceProperties('Custom::LogRetention', { + LogGroupName: { + 'Fn::Join': [ + '', + [ + '/aws/neptune/', + { + Ref: 'ClusterEB0386A7', + }, + '/audit', + ], + ], + }, + RetentionInDays: 30, + }); + }); }); function testStack() { diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts index 0d7014c049b3a..827e0af6e2a8a 100644 --- a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts @@ -1,8 +1,9 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; +import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; import * as integ from '@aws-cdk/integ-tests'; -import { DatabaseCluster, InstanceType } from '../lib'; +import { DatabaseCluster, InstanceType, LogType } from '../lib'; import { ClusterParameterGroup } from '../lib/parameter-group'; /* @@ -39,6 +40,10 @@ const cluster = new DatabaseCluster(stack, 'Database', { kmsKey, removalPolicy: cdk.RemovalPolicy.DESTROY, autoMinorVersionUpgrade: true, + cloudwatchLogsExports: { + logTypes: [LogType.AUDIT], + logRetention: logs.RetentionDays.ONE_MONTH, + }, }); cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world');