From 3ca9dec272d0e91e24b3249718e08357122208b6 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 20 Sep 2022 15:00:29 +0900 Subject: [PATCH 01/41] feat(cdk): output access logs bucket - `ContentsDistribution` extracts the name of the S3 bucket that store CloudFront access logs, which the CloudFront distribution provisions on behalf of us. It highly depends on the implementation details of CDK. - `CkdStack` outputs the name of the S3 bucket for CloudFront access logs. issue codemonger-io/codemonger#30 --- cdk/lib/cdk-stack.ts | 6 +++++ cdk/lib/contents-distribution.ts | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index a9e3398..04309ce 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -36,5 +36,11 @@ export class CdkStack extends Stack { description: 'Domain name of the CloudFront distribution for contents of the codemonger website', value: contentsDistribution.distribution.distributionDomainName, }); + if (contentsDistribution.accessLogsBucketName != null) { + new CfnOutput(this, 'ContentsAccessLogsBucketName', { + description: 'Name of the S3 bucket for CloudFront access logs', + value: contentsDistribution.accessLogsBucketName, + }); + } } } diff --git a/cdk/lib/contents-distribution.ts b/cdk/lib/contents-distribution.ts index 1cf106e..49de376 100644 --- a/cdk/lib/contents-distribution.ts +++ b/cdk/lib/contents-distribution.ts @@ -2,6 +2,8 @@ import * as path from 'path'; import { Duration, + Fn, + Stack, aws_certificatemanager as acm, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, @@ -45,6 +47,8 @@ const NO_DOMAIN_NAME_CDK_CONTEXT = 'codemonger:no-domain-name'; export class ContentsDistribution extends Construct { /** CloudFront distribution for contents of the codemonger website. */ readonly distribution: cloudfront.IDistribution; + /** Name of the S3 bucket for access logs. */ + readonly accessLogsBucketName: string | undefined; constructor(scope: Construct, id: string, props: Props) { super(scope, id); @@ -109,6 +113,45 @@ export class ContentsDistribution extends Construct { ...certificateParams, }, ); + + // obtains the logging bucket created by the distribution. + // we have to access the underlying CloudFormation properties. + // + // see below for how to access the CloudFormation properties, + // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.CfnDistribution.html + const cfnDistribution = + this.distribution.node.defaultChild as cloudfront.CfnDistribution; + const stack = Stack.of(this); + const distributionConfig = stack.resolve(cfnDistribution.distributionConfig) as cloudfront.CfnDistribution.DistributionConfigProperty; + if (distributionConfig.logging != null) { + const loggingConfig = stack.resolve(distributionConfig.logging) as cloudfront.CfnDistribution.LoggingProperty; + // loggingConfig.bucket should be an `Fn::GetAtt` intrinsic function to + // obtain the regional domain name from the bucket for access logs. + // so we can extract the logical name of the bucket and then obtain the + // bucket name by Ref. + // + // TODO: too much dependence on the CDK implementation + // https://github.com/aws/aws-cdk/blob/7d8ef0bad461a05caa41d140678481c5afb9d33e/packages/%40aws-cdk/aws-cloudfront/lib/distribution.ts#L443-L457 + const bucketRef: any = loggingConfig.bucket; + if (typeof bucketRef !== 'object') { + throw new Error( + 'logical name of the bucket for access logs must be available', + ); + } + const getAtt: any = bucketRef['Fn::GetAtt']; + if (!Array.isArray(getAtt)) { + throw new Error( + 'logical name of the bucket for access logs must be available', + ); + } + const bucketLogicalId = getAtt[0]; + if (typeof bucketLogicalId !== 'string') { + throw new Error( + 'logical name of the bucket for access logs must be available', + ); + } + this.accessLogsBucketName = Fn.ref(bucketLogicalId); + } } } From 1a13ccf16dceb2e366486c34b766f3fa546979cd Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 20 Sep 2022 15:27:01 +0900 Subject: [PATCH 02/41] chore(cdk-ops): update CDK - Bumps the CDK version to 2.42.0. --- cdk-ops/package-lock.json | 61 +++++++++++++++++++++++++-------------- cdk-ops/package.json | 7 +++-- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/cdk-ops/package-lock.json b/cdk-ops/package-lock.json index f75f51d..77865df 100644 --- a/cdk-ops/package-lock.json +++ b/cdk-ops/package-lock.json @@ -8,10 +8,11 @@ "name": "cdk-ops", "version": "0.1.0", "dependencies": { + "@aws-cdk/aws-lambda-python-alpha": "^2.42.0-alpha.0", "@aws-sdk/client-cloudformation": "^3.112.0", - "aws-cdk-lib": "^2.28.1", + "aws-cdk-lib": "^2.42.0", "cdk-common": "file:../cdk-common", - "constructs": "^10.1.42", + "constructs": "^10.1.106", "source-map-support": "^0.5.21" }, "bin": { @@ -21,7 +22,7 @@ "@types/jest": "^27.5.0", "@types/node": "10.17.27", "@types/prettier": "2.6.0", - "aws-cdk": "^2.28.1", + "aws-cdk": "^2.42.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "ts-node": "^10.7.0", @@ -52,6 +53,18 @@ "node": ">=6.0.0" } }, + "node_modules/@aws-cdk/aws-lambda-python-alpha": { + "version": "2.42.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda-python-alpha/-/aws-lambda-python-alpha-2.42.0-alpha.0.tgz", + "integrity": "sha512-V5fi76QYOWXVhyHRuZ8og/s34goGHuJ9hyEVuG3dsO1WMEx3fovtzkXTdh3BSwZ/Pl4hYJY4blNO+XtVSXcjBA==", + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.42.0", + "constructs": "^10.0.0" + } + }, "node_modules/@aws-crypto/ie11-detection": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.0.tgz", @@ -2090,9 +2103,9 @@ "dev": true }, "node_modules/aws-cdk": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.28.1.tgz", - "integrity": "sha512-0Kklrj9HHg6HkYZQuTnJ+2+RLTqlVcxECUmlDudBxbPxJQcc5pEA9stfo8wwh1CtoWYuF4A4moP7B19Yvw4nJg==", + "version": "2.42.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.42.0.tgz", + "integrity": "sha512-BdkPhkj2PRkGSfsXh7kduUkJg+y234heWOaKzMEiauCt2Bj72wYwZhYG60TAFue7K7ngSjKzUeQ+G7SfKZcudg==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -2105,9 +2118,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.28.1.tgz", - "integrity": "sha512-YFJKvv3lU7HUfud+4uyQT5tNyJhidBB1ftBmc6NzML+UXAuelPZICJLdGuoE/dA6dU3mB/sT15S+fTzATTwvfg==", + "version": "2.42.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.42.0.tgz", + "integrity": "sha512-jHHcUm2baKv87L7MO0fktAhmtDZfnHLPsLIktzgW2zCDRNUCFoUPkQ+44eUkZkgmL2sEOSMEQFhbNCxmbW4OSQ==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -2613,9 +2626,9 @@ "dev": true }, "node_modules/constructs": { - "version": "10.1.42", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.42.tgz", - "integrity": "sha512-5AELa/PFtZG+WTjn9HoXhqsDZYV6l3J7Li9xw6vREYVMasF8cnVbTZvA4crP1gIyKtBAxAlnZCmzmCbicnH6eg==", + "version": "10.1.106", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.106.tgz", + "integrity": "sha512-xcNB+/5jKk7+9w4pXe5jThpUEDDbhtWLeXlhy9GVdFa/tuasOVEiowZOZMjPvcXrujGgSkVleebo6ZNzvYyZug==", "engines": { "node": ">= 14.17.0" } @@ -5392,6 +5405,12 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@aws-cdk/aws-lambda-python-alpha": { + "version": "2.42.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda-python-alpha/-/aws-lambda-python-alpha-2.42.0-alpha.0.tgz", + "integrity": "sha512-V5fi76QYOWXVhyHRuZ8og/s34goGHuJ9hyEVuG3dsO1WMEx3fovtzkXTdh3BSwZ/Pl4hYJY4blNO+XtVSXcjBA==", + "requires": {} + }, "@aws-crypto/ie11-detection": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.0.tgz", @@ -7062,18 +7081,18 @@ "dev": true }, "aws-cdk": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.28.1.tgz", - "integrity": "sha512-0Kklrj9HHg6HkYZQuTnJ+2+RLTqlVcxECUmlDudBxbPxJQcc5pEA9stfo8wwh1CtoWYuF4A4moP7B19Yvw4nJg==", + "version": "2.42.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.42.0.tgz", + "integrity": "sha512-BdkPhkj2PRkGSfsXh7kduUkJg+y234heWOaKzMEiauCt2Bj72wYwZhYG60TAFue7K7ngSjKzUeQ+G7SfKZcudg==", "dev": true, "requires": { "fsevents": "2.3.2" } }, "aws-cdk-lib": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.28.1.tgz", - "integrity": "sha512-YFJKvv3lU7HUfud+4uyQT5tNyJhidBB1ftBmc6NzML+UXAuelPZICJLdGuoE/dA6dU3mB/sT15S+fTzATTwvfg==", + "version": "2.42.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.42.0.tgz", + "integrity": "sha512-jHHcUm2baKv87L7MO0fktAhmtDZfnHLPsLIktzgW2zCDRNUCFoUPkQ+44eUkZkgmL2sEOSMEQFhbNCxmbW4OSQ==", "requires": { "@balena/dockerignore": "^1.0.2", "case": "1.6.3", @@ -7433,9 +7452,9 @@ "dev": true }, "constructs": { - "version": "10.1.42", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.42.tgz", - "integrity": "sha512-5AELa/PFtZG+WTjn9HoXhqsDZYV6l3J7Li9xw6vREYVMasF8cnVbTZvA4crP1gIyKtBAxAlnZCmzmCbicnH6eg==" + "version": "10.1.106", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.106.tgz", + "integrity": "sha512-xcNB+/5jKk7+9w4pXe5jThpUEDDbhtWLeXlhy9GVdFa/tuasOVEiowZOZMjPvcXrujGgSkVleebo6ZNzvYyZug==" }, "convert-source-map": { "version": "1.8.0", diff --git a/cdk-ops/package.json b/cdk-ops/package.json index a315f28..c985622 100644 --- a/cdk-ops/package.json +++ b/cdk-ops/package.json @@ -14,17 +14,18 @@ "@types/jest": "^27.5.0", "@types/node": "10.17.27", "@types/prettier": "2.6.0", - "aws-cdk": "^2.28.1", + "aws-cdk": "^2.42.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", "ts-node": "^10.7.0", "typescript": "~3.9.7" }, "dependencies": { + "@aws-cdk/aws-lambda-python-alpha": "^2.42.0-alpha.0", "@aws-sdk/client-cloudformation": "^3.112.0", - "aws-cdk-lib": "^2.28.1", + "aws-cdk-lib": "^2.42.0", "cdk-common": "file:../cdk-common", - "constructs": "^10.1.42", + "constructs": "^10.1.106", "source-map-support": "^0.5.21" } } From ae2887c91533f8ae4df45330e201fe975a321cb4 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 20 Sep 2022 15:28:25 +0900 Subject: [PATCH 03/41] fix(cdk-ops): name of the main stack - The issue that the main stack could not be resolved is fixed. It was caused because the name of the main stack contained an extra dash before the deployment stage. Removes the extra dash from the stack name. --- cdk-ops/lib/codemonger-resources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdk-ops/lib/codemonger-resources.ts b/cdk-ops/lib/codemonger-resources.ts index 9fccf01..ff0b32d 100644 --- a/cdk-ops/lib/codemonger-resources.ts +++ b/cdk-ops/lib/codemonger-resources.ts @@ -71,7 +71,7 @@ export async function resolveCodemongerResourceNames(): async function fetchStackOutput(stage: DeploymentStage): Promise> { - const stackName = `${CODEMONGER_STACK_PREFIX}-${stage}`; + const stackName = `${CODEMONGER_STACK_PREFIX}${stage}`; const client = new CloudFormationClient({}); const command = new DescribeStacksCommand({ StackName: stackName, From 16042a75fb55ae85de0e2a0649c209122d1b2715 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 20 Sep 2022 15:31:56 +0900 Subject: [PATCH 04/41] feat(cdk-ops): resolve access logs bucket - `resolveCodemongerResourceNames` resolves the name of the S3 bucket for access logs of the development stage. - `CodemongerResources` binds the S3 bucket for access logs of the development stage. issue codemonger-io/codemonger#30 --- cdk-ops/lib/codemonger-resources.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cdk-ops/lib/codemonger-resources.ts b/cdk-ops/lib/codemonger-resources.ts index ff0b32d..b0a9667 100644 --- a/cdk-ops/lib/codemonger-resources.ts +++ b/cdk-ops/lib/codemonger-resources.ts @@ -21,6 +21,8 @@ export type CodemongerResourceNames = { developmentDistributionDomainName: string; /** Name of the S3 bucket for production contents. */ productionContentsBucketName: string; + /** Name of the S3 bucket of CloudFront access logs for development. */ + developmentContentsAccessLogsBucketName: string; }; /** @@ -50,10 +52,16 @@ export async function resolveCodemongerResourceNames(): if (productionContentsBucketName == null) { throw new Error('contents bucket for production is not available'); } + const developmentContentsAccessLogsBucketName = + developmentOutputs.get('ContentsAccessLogsBucketName'); + if (developmentContentsAccessLogsBucketName == null) { + throw new Error('access logs bucket for development is not available'); + } return { developmentContentsBucketName, developmentDistributionDomainName, productionContentsBucketName, + developmentContentsAccessLogsBucketName, }; } @@ -104,6 +112,8 @@ export class CodemongerResources extends Construct { readonly productionContentsBucket: s3.IBucket; /** Domain name for production. */ readonly productionDomainName = CODEMONGER_DOMAIN_NAME; + /** S3 bucket of CloudFront access logs for development. */ + readonly developmentContentsAccessLogsBucket: s3.IBucket; constructor( scope: Construct, @@ -113,6 +123,7 @@ export class CodemongerResources extends Construct { super(scope, id); const { + developmentContentsAccessLogsBucketName, developmentContentsBucketName, developmentDistributionDomainName, productionContentsBucketName, @@ -129,5 +140,10 @@ export class CodemongerResources extends Construct { 'ProductionContentsBucket', productionContentsBucketName, ); + this.developmentContentsAccessLogsBucket = s3.Bucket.fromBucketName( + this, + 'DevelopmentContentsAccessLogsBucket', + developmentContentsAccessLogsBucketName, + ); } } From 0211abbc84cae4edd4b9f6ca90a92fb67ca0ddc2 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 20 Sep 2022 15:35:08 +0900 Subject: [PATCH 05/41] feat(cdk-ops): mask access logs - Introduces a new Lambda function `lambda/mask-access-logs` that masks IP addresses in CloudFront access logs. It saves masked access logs in another S3 bucket. It will be triggered when a new log file is put into the S3 bucket for access logs in the future but we have to manually invoke it for now. - A new CDK construct `AccessLogsMasking` provisions the Lambda function `lambda/mask-access-logs`. It also provisions an S3 bucket where `lambda/mask-access-logs` saves masked access logs. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/mask-access-logs/index.py | 288 +++++++++++++++++++++++ cdk-ops/lib/access-logs-masking.ts | 58 +++++ 2 files changed, 346 insertions(+) create mode 100644 cdk-ops/lambda/mask-access-logs/index.py create mode 100644 cdk-ops/lib/access-logs-masking.ts diff --git a/cdk-ops/lambda/mask-access-logs/index.py b/cdk-ops/lambda/mask-access-logs/index.py new file mode 100644 index 0000000..012cc1e --- /dev/null +++ b/cdk-ops/lambda/mask-access-logs/index.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- + +"""Masks information in CloudFront access logs. + +You have to specify the following environment variables, +* SOURCE_BUCKET_NAME: name of the S3 bucket containing access logs files to be + masked. +""" + +import array +import csv +import gzip +import io +import ipaddress +import logging +import os +import sys +from contextlib import contextmanager +from typing import Dict, Iterable, Iterator, TextIO +import boto3 + + +SOURCE_BUCKET_NAME = os.environ.get('SOURCE_BUCKET_NAME') +DESTINATION_BUCKET_NAME = os.environ.get('DESTINATION_BUCKET_NAME') + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +s3 = boto3.resource('s3') +source_bucket = s3.Bucket(SOURCE_BUCKET_NAME) +destination_bucket = s3.Bucket(DESTINATION_BUCKET_NAME) + + +def translate_logs(logs_in: Iterable[str]) -> Iterator[str]: + """Translates CloudFront access logs read from a given iterator and returns + a new iterator of translated lines. + + CloudFront access logs starts with the following lines, + + .. code-block:: + + #Version: 1.0 + #Fields: date time ... + + Column names follows the prefix "#Fields:" in the second line. + To parse CloudFront access logs as valid TSV data with a header line, we + have to skip the first line, drop ``#Fields:`` from the second line and + replace space characters with tabs in the second line. + """ + for line in logs_in: + if line.startswith('#Version:'): + continue + if line.startswith('#Fields:'): + columns = line.split(' ')[1:] + yield '\t'.join(columns) + yield line + + +def mask_row(row: Dict[str, str]) -> Dict[str, str]: + """Masks a given row in CloudFront access logs. + """ + addr = row['c-ip'] + if addr is not None: + row['c-ip'] = mask_ip_address(addr) + return row + + +def mask_ip_address(addr: str) -> str: + """Masks a given IP address. + + Leaves 8 MSBs of an IPv4 address. + Leaves 32 MSBs of an IPv6 address. + Reference: https://cloudonaut.io/anonymize-cloudfront-access-logs/ + """ + ip_addr = ipaddress.ip_address(addr) + if ip_addr.version == 4: + return mask_ip_address_v4(addr) + if ip_addr.version == 6: + return mask_ip_address_v6(addr) + # invalid IP address + raise ValueError(f'invalid IP address: {addr}') + + +def mask_ip_address_v4(addr: str) -> str: + """Masks a given IPv4 address. + + Leaves 8 MSBs. + """ + # makes strict=False to ignore host bits + net = ipaddress.ip_network(f'{addr}/8', strict=False) + return str(net.network_address) + + +def mask_ip_address_v6(addr: str) -> str: + """Masks a given IPv6 address. + + Leaves 32 MSBs. + """ + # makes strict=False to ignore host bits + net = ipaddress.ip_network(f'{addr}/32', strict=False) + return str(net.network_address) + + +def process_logs(logs_in: Iterator[str], logs_out: TextIO): + """Processes given CloudFront logs and outputs to given stream. + """ + tsv_in = csv.DictReader(translate_logs(logs_in), delimiter='\t') + # drops the first row as it contains column names + next(tsv_in) + column_names = tsv_in.fieldnames + if column_names is None: + raise ValueError('no field names are specified in the input') + tsv_out = csv.DictWriter( + logs_out, + fieldnames=column_names, + delimiter='\t', + ) + tsv_out.writeheader() + for row in tsv_in: + row = mask_row(row) + tsv_out.writerow(row) + + +def lambda_handler(event, _): + """Masks information in a given CloudFront access logs file on S3. + """ + LOGGER.debug('masking access logs: %s', str(event)) + src = source_bucket.Object(event['key']) + results = src.get() + with open_body(results) as body: + with gzip.open(body, mode='rt') as tsv_in: + dest = destination_bucket.Object(event['key']) + with S3OutputStream(dest) as masked_out: + with gzip.open(masked_out, mode='wt') as tsv_out: + process_logs(tsv_in, tsv_out) + return {} + + +class S3OutputStream(io.RawIOBase): + """File object that can write an S3 object. + """ + + MIN_PART_SIZE_IN_BYTES = 5 * 1024 * 1024 # 5MB + + def __init__(self, dest_object): + self.dest_object = dest_object + # initiates the multipart upload + self.multipart_upload = self.dest_object.initiate_multipart_upload( + ServerSideEncryption='AES256', + ) + self.uploaded_part_etags = [] + self.part_buffer = array.array('B') + + + def writable(self): + return True + + + def write(self, b): + # appends to the part buffer + self.part_buffer.extend(b) + if len(self.part_buffer) >= S3OutputStream.MIN_PART_SIZE_IN_BYTES: + self.upload_part() + return len(b) + + + def upload_part(self): + """Uploads the buffered part and flushes the buffer. + """ + part_number = self.next_part_number + LOGGER.debug( + 'multipart upload [%d]: size=%d', + part_number, + len(self.part_buffer), + ) + # according to the boto3 documentation, + # Part requires an str for its parameter, but actually an int. + part = self.multipart_upload.Part(part_number) + res = part.upload(Body=self.part_buffer.tobytes()) + self.uploaded_part_etags.append(res['ETag']) + # resets the part buffer + self.part_buffer = array.array('B') + + + @property + def next_part_number(self): + """Next part number. + """ + return len(self.uploaded_part_etags) + 1 # part number from 1 + + + def close(self): + """Completes the multipart upload. + """ + if self.multipart_upload is not None: + LOGGER.debug('closing the multipart upload') + try: + # uploads the last part if it remains + if len(self.part_buffer) > 0: + self.upload_part() + # lists parts and completes + part_list = [ + { + 'ETag': etag, + 'PartNumber': i + 1, + } for (i, etag) in enumerate(self.uploaded_part_etags) + ] + self.multipart_upload.complete( + MultipartUpload={ + 'Parts': part_list, + }, + ) + except: + LOGGER.warning( + 'aborting the multipart upload (as close failed)', + ) + self.multipart_upload.abort() + raise + finally: + self.multipart_upload = None + + + def abort(self): + """Aborts the multipart upload. + """ + if self.multipart_upload is not None: + LOGGER.debug('aborting the multipart upload') + self.multipart_upload.abort() + self.multipart_upload = None + + + def __exit__(self, exc_type, exc_value, traceback): + """Calls ``abort`` if an exception has occurred. + """ + if exc_type is not None: + self.abort() + return False # propagates the exception + return super().__exit__(exc_type, exc_value, traceback) + + + def __del__(self): + """Calls ``abort``. + + You have to use ``with`` statement or explicitly call ``close`` to + complete the multipart upload. + """ + self.abort() + + +@contextmanager +def open_body(s3_get_results): + """Enables ``with`` statement for a body got from an S3 bucket. + """ + body = s3_get_results['Body'] + try: + yield body + finally: + body.close() + + +if __name__ == '__main__': + import argparse + arg_parser = argparse.ArgumentParser( + description='Masks CloudFront access logs', + ) + arg_parser.add_argument( + 'logs_path', + metavar='LOGS', + type=str, + help='path to a gzipped TSV file containing CloudFront access logs', + ) + arg_parser.add_argument( + '--out', + dest='out_path', + metavar='OUT', + type=str, + help='path to a file where masked CloudFront access logs are to be' + ' saved (gzipped)', + ) + logging.basicConfig(level=logging.DEBUG) + LOGGER.debug('filtering access logs') + args = arg_parser.parse_args() + if args.out_path is not None: + results_out = gzip.open(args.out_path, mode='wt') + else: + results_out = sys.stdout + with gzip.open(args.logs_path, mode='rt') as text_in: + process_logs(text_in, results_out) diff --git a/cdk-ops/lib/access-logs-masking.ts b/cdk-ops/lib/access-logs-masking.ts new file mode 100644 index 0000000..cdbb71a --- /dev/null +++ b/cdk-ops/lib/access-logs-masking.ts @@ -0,0 +1,58 @@ +import * as path from 'path'; + +import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; +import { RemovalPolicy, aws_lambda as lambda, aws_s3 as s3 } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import type { DeploymentStage } from 'cdk-common'; + +export interface Props { + /** S3 bucket that stores CloudFront access logs. */ + accessLogsBucket: s3.IBucket; + /** Deployment stage. */ + deploymentStage: DeploymentStage; +} + +/** CDK construct that provisions resources to mask CloudFront access logs. */ +export class AccessLogsMasking extends Construct { + /** S3 bucket for masked access logs. */ + readonly maskedAccessLogsBucket: s3.IBucket; + + constructor(scope: Construct, id: string, props: Props) { + super(scope, id); + + const { accessLogsBucket } = props; + + // provisions an S3 bucket for masked access logs. + this.maskedAccessLogsBucket = new s3.Bucket( + this, + 'MaskedAccessLogsBucket', + { + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + encryption: s3.BucketEncryption.S3_MANAGED, + enforceSSL: true, + removalPolicy: RemovalPolicy.RETAIN, + }, + ); + + // Lambda functions + // - masks CloudFront access logs. + const maskAccessLogsLambda = new PythonFunction( + this, + 'MaskAccessLogsLambda', + { + description: 'Masks information in a given CloudFront access logs file', + runtime: lambda.Runtime.PYTHON_3_8, + entry: path.join('lambda', 'mask-access-logs'), + index: 'index.py', + handler: 'lambda_handler', + environment: { + SOURCE_BUCKET_NAME: accessLogsBucket.bucketName, + DESTINATION_BUCKET_NAME: this.maskedAccessLogsBucket.bucketName, + }, + }, + ); + accessLogsBucket.grantRead(maskAccessLogsLambda); + this.maskedAccessLogsBucket.grantPut(maskAccessLogsLambda); + } +} From 827846f467f5ea8a25221ead5bf814d1ca61d20b Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 20 Sep 2022 15:40:55 +0900 Subject: [PATCH 06/41] feat(cdk-ops): provision AccessLogsMasking - `CdkOpsStack` provisions `AccessLogsMasking`. issue codemonger-io/codemonger#30 --- cdk-ops/lib/cdk-ops-stack.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index 8f36313..a7bae06 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -1,6 +1,7 @@ import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; +import { AccessLogsMasking } from './access-logs-masking'; import { CodemongerResources, CodemongerResourceNames, @@ -24,5 +25,14 @@ export class CdkOpsStack extends Stack { const pipeline = new ContentsPipeline(this, 'ContentsPipeline', { codemongerResources, }); + const developmentContentsAccessLogsMasking = new AccessLogsMasking( + this, + 'DevelopmentContentsAccessLogsMasking', + { + accessLogsBucket: + codemongerResources.developmentContentsAccessLogsBucket, + deploymentStage: 'development', + }, + ); } } From 714c370330c7b88ba4eb2864dda4bdaeb4660c5e Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Wed, 21 Sep 2022 18:34:41 +0900 Subject: [PATCH 07/41] feat(cdk-ops): process S3 event via SQS - `lambda/mask-access-logs` now supposes that the input event is a list of SQS messages containing "ObjectCreated:*" S3 events. - `AccessLogsMasking` provisions an SQS queue to capture "ObjectCreated:*" S3 events from the S3 bucket for access logs. It sets the SQS queue as an event source of `MaskAccessLogsLambda`. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/mask-access-logs/index.py | 63 ++++++++++++++++++++++-- cdk-ops/lib/access-logs-masking.ts | 55 ++++++++++++++++++++- 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/cdk-ops/lambda/mask-access-logs/index.py b/cdk-ops/lambda/mask-access-logs/index.py index 012cc1e..67c4fbc 100644 --- a/cdk-ops/lambda/mask-access-logs/index.py +++ b/cdk-ops/lambda/mask-access-logs/index.py @@ -12,6 +12,7 @@ import gzip import io import ipaddress +import json import logging import os import sys @@ -123,17 +124,71 @@ def process_logs(logs_in: Iterator[str], logs_out: TextIO): def lambda_handler(event, _): """Masks information in a given CloudFront access logs file on S3. + + ``event`` is supposed to be an SQS message event described at + https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html """ - LOGGER.debug('masking access logs: %s', str(event)) - src = source_bucket.Object(event['key']) + for record in event['Records']: + try: + message = json.loads(record['body']) + except json.JSONDecodeError: + LOGGER.error('invalid SQS record: %s', str(record)) + continue + # may receive a test message "s3:TestEvent" + # and a test message does not have "Records" + entries = message.get('Records') + if entries is None: + LOGGER.debug('maybe a test message: %s', str(message)) + continue + for entry in entries: + event_name = entry.get('eventName', '?') + if event_name.startswith('ObjectCreated:'): + s3_object = entry.get('s3') + if s3_object is not None: + process_s3_object(s3_object) + else: + LOGGER.error('invalid S3 event: %s', str(entry)) + else: + LOGGER.error( + 'event "%s" other than S3 object creation was notified.' + ' please check the event source configuration', + event_name, + ) + return {} + + +def process_s3_object(s3_object): + """Processes a given S3 object event. + + ``s3_object`` must conform to an S3 object creation event described at + https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html + """ + LOGGER.debug('processing S3 object event: %s', str(s3_object)) + # makes sure that the source bucket matches + bucket_name = s3_object.get('bucket', {}).get('name') + if bucket_name is None: + LOGGER.error('no bucket name in S3 object event: %s', str(s3_object)) + return + if bucket_name != SOURCE_BUCKET_NAME: + LOGGER.warning( + 'bucket name must be %s but %s was given.' + ' please check the event source configuration', + SOURCE_BUCKET_NAME, + bucket_name, + ) + return + key = s3_object.get('object', {}).get('key') + if key is None: + LOGGER.error('no object key in S3 object event: %s', str(s3_object)) + return + src = source_bucket.Object(key) results = src.get() with open_body(results) as body: with gzip.open(body, mode='rt') as tsv_in: - dest = destination_bucket.Object(event['key']) + dest = destination_bucket.Object(key) with S3OutputStream(dest) as masked_out: with gzip.open(masked_out, mode='wt') as tsv_out: process_logs(tsv_in, tsv_out) - return {} class S3OutputStream(io.RawIOBase): diff --git a/cdk-ops/lib/access-logs-masking.ts b/cdk-ops/lib/access-logs-masking.ts index cdbb71a..cb11aaf 100644 --- a/cdk-ops/lib/access-logs-masking.ts +++ b/cdk-ops/lib/access-logs-masking.ts @@ -1,7 +1,15 @@ import * as path from 'path'; import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; -import { RemovalPolicy, aws_lambda as lambda, aws_s3 as s3 } from 'aws-cdk-lib'; +import { + Duration, + RemovalPolicy, + aws_lambda as lambda, + aws_lambda_event_sources as lambda_event, + aws_s3 as s3, + aws_s3_notifications as s3n, + aws_sqs as sqs, +} from 'aws-cdk-lib'; import { Construct } from 'constructs'; import type { DeploymentStage } from 'cdk-common'; @@ -23,7 +31,7 @@ export class AccessLogsMasking extends Construct { const { accessLogsBucket } = props; - // provisions an S3 bucket for masked access logs. + // S3 bucket for masked access logs. this.maskedAccessLogsBucket = new s3.Bucket( this, 'MaskedAccessLogsBucket', @@ -31,12 +39,20 @@ export class AccessLogsMasking extends Construct { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, encryption: s3.BucketEncryption.S3_MANAGED, enforceSSL: true, + lifecycleRules: [ + { + // safeguard for incomplete multipart uploads. + // minimum resoluation is one day. + abortIncompleteMultipartUploadAfter: Duration.days(1), + }, + ], removalPolicy: RemovalPolicy.RETAIN, }, ); // Lambda functions // - masks CloudFront access logs. + const maskAccessLogsLambdaTimeout = Duration.seconds(30); const maskAccessLogsLambda = new PythonFunction( this, 'MaskAccessLogsLambda', @@ -50,9 +66,44 @@ export class AccessLogsMasking extends Construct { SOURCE_BUCKET_NAME: accessLogsBucket.bucketName, DESTINATION_BUCKET_NAME: this.maskedAccessLogsBucket.bucketName, }, + timeout: maskAccessLogsLambdaTimeout, }, ); accessLogsBucket.grantRead(maskAccessLogsLambda); this.maskedAccessLogsBucket.grantPut(maskAccessLogsLambda); + + // SQS queue to capture creation of access logs files. + const maxBatchingWindow = Duration.minutes(5); // least frequency + const newLogsQueue = new sqs.Queue(this, 'NewLogsQueue', { + retentionPeriod: Duration.days(1), + // at least (6 * Lambda timeout) + (maximum batch window) + // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-eventsource + visibilityTimeout: maxBatchingWindow.plus( + Duration.seconds(6 * maskAccessLogsLambdaTimeout.toSeconds()), + ), + }); + accessLogsBucket.addEventNotification( + s3.EventType.OBJECT_CREATED, + new s3n.SqsDestination(newLogsQueue), + ); + // triggers MaskAccessLogsLambda when the SQS queue receives a message + maskAccessLogsLambda.addEventSource( + new lambda_event.SqsEventSource(newLogsQueue, { + enabled: true, + batchSize: 10, + maxBatchingWindow, + // the following filter did not work as I intended, and I gave up. + /* + filters: [ + // SQS queue may receive a test message "s3:TestEvent". + // non-test message must contain the "Records" field. + lambda.FilterCriteria.filter({ + body: { + Records: lambda.FilterRule.exists(), + }, + }), + ], */ + }), + ); } } From e459febac069460fbbf6170bbd700b54722db05f Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Fri, 23 Sep 2022 09:52:58 +0900 Subject: [PATCH 08/41] feat(cdk-ops): ignores non-existing logs - `lambda/mask-access-logs` ignores access logs files that no longer exist. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/mask-access-logs/index.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cdk-ops/lambda/mask-access-logs/index.py b/cdk-ops/lambda/mask-access-logs/index.py index 67c4fbc..db3cedd 100644 --- a/cdk-ops/lambda/mask-access-logs/index.py +++ b/cdk-ops/lambda/mask-access-logs/index.py @@ -171,7 +171,7 @@ def process_s3_object(s3_object): return if bucket_name != SOURCE_BUCKET_NAME: LOGGER.warning( - 'bucket name must be %s but %s was given.' + 'bucket name must be "%s" but "%s" was given.' ' please check the event source configuration', SOURCE_BUCKET_NAME, bucket_name, @@ -182,7 +182,11 @@ def process_s3_object(s3_object): LOGGER.error('no object key in S3 object event: %s', str(s3_object)) return src = source_bucket.Object(key) - results = src.get() + try: + results = src.get() + except s3.meta.client.exceptions.NoSuchKey: + LOGGER.debug('object "%s" no longer exists', key) + return with open_body(results) as body: with gzip.open(body, mode='rt') as tsv_in: dest = destination_bucket.Object(key) From 3af141e3507488d061bf82b43ffd09228885ae70 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Fri, 23 Sep 2022 10:04:41 +0900 Subject: [PATCH 09/41] feat(cdk-ops): delete masked access logs - Provisions a new SQS queue that notifies when a new object is created in the S3 bucket for masked CloudFront access logs; i.e., masking a new CloudFront access logs file has finished. - Introduces a new Lambda function `lambda/delete-access-logs` that deletes files in the original bucket for CloudFront access logs. The above SQS queue triggers this function. - Reorders definitions in `AccessLogsMasking` because a Lambda function and its trigger SQS queue are strongly tied. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/delete-access-logs/index.py | 94 ++++++++++++++++++++++ cdk-ops/lib/access-logs-masking.ts | 50 ++++++++++-- 2 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 cdk-ops/lambda/delete-access-logs/index.py diff --git a/cdk-ops/lambda/delete-access-logs/index.py b/cdk-ops/lambda/delete-access-logs/index.py new file mode 100644 index 0000000..aacce70 --- /dev/null +++ b/cdk-ops/lambda/delete-access-logs/index.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +"""Deletes the original CloudFront access logs file corresponding to a given +masked access logs file. + +You have to specify the following environment variables, +* ``SOURCE_BUCKET_NAME``: bucket name of the original CloudFront access logs + files +* ``DESTINATION_BUCKET_NAME``: bucket name of the masked CloudFront access logs + files +""" + +import json +import logging +import os +import boto3 + + +SOURCE_BUCKET_NAME = os.environ['SOURCE_BUCKET_NAME'] +DESTINATION_BUCKET_NAME = os.environ['DESTINATION_BUCKET_NAME'] + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +s3 = boto3.resource('s3') +source_bucket = s3.Bucket(SOURCE_BUCKET_NAME) + + +def lambda_handler(event, _): + """Delete original CloudFront access logs files. + + ``event`` is supposed to be SQS events described at + https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html + """ + for record in event['Records']: + body = record.get('body') + if body is None: + LOGGER.error('invalid SQS record: %s', str(record)) + continue + try: + message = json.loads(body) + except json.JSONDecodeError: + LOGGER.error('invalid SQS record: %s', str(record)) + continue + # may receive a test message "s3:TestEvent" + # and a test message does not have "Records" + entries = message.get('Records') + if entries is None: + LOGGER.debug('maybe a test message: %s', str(message)) + continue + for entry in entries: + event_name = entry.get('eventName', '?') + if event_name.startswith('ObjectCreated:'): + s3_object = entry.get('s3') + if s3_object is None: + LOGGER.error('invalid S3 event: %s', str(entry)) + else: + process_s3_object(s3_object) + else: + LOGGER.error( + 'event "%s" other than S3 object creation was notified.' + ' please check the event source configuration', + event_name, + ) + return {} + + +def process_s3_object(s3_object): + """Processes a given S3 object event. + + ``s3_object`` must conform to an S3 object creation event described at + https://docs.aws.amazon.com/lambda/latest/dg/with-s3.html + """ + LOGGER.debug('processing S3 object event: %s', str(s3_object)) + # makes sure that the destination bucket matches + bucket_name = s3_object.get('bucket', {}).get('name') + if bucket_name is None: + LOGGER.error('no bucket name in S3 object event: %s', str(s3_object)) + return + if bucket_name != DESTINATION_BUCKET_NAME: + LOGGER.warning( + 'bucket name must be "%s" but "%s" was given.' + ' please check the event source configuration', + DESTINATION_BUCKET_NAME, + bucket_name, + ) + return + key = s3_object.get('object', {}).get('key') + if key is None: + LOGGER.error('no object key in S3 object event: %s', str(s3_object)) + return + src = source_bucket.Object(key) + res = src.delete() + LOGGER.debug('deleted object "%s": %s', key, str(res)) diff --git a/cdk-ops/lib/access-logs-masking.ts b/cdk-ops/lib/access-logs-masking.ts index cb11aaf..39b80fc 100644 --- a/cdk-ops/lib/access-logs-masking.ts +++ b/cdk-ops/lib/access-logs-masking.ts @@ -50,8 +50,8 @@ export class AccessLogsMasking extends Construct { }, ); - // Lambda functions - // - masks CloudFront access logs. + // masks newly created CloudFront access logs + // - Lambda function const maskAccessLogsLambdaTimeout = Duration.seconds(30); const maskAccessLogsLambda = new PythonFunction( this, @@ -71,8 +71,8 @@ export class AccessLogsMasking extends Construct { ); accessLogsBucket.grantRead(maskAccessLogsLambda); this.maskedAccessLogsBucket.grantPut(maskAccessLogsLambda); - - // SQS queue to capture creation of access logs files. + // - SQS queue to capture creation of access logs files, which triggers + // the above Lambda function const maxBatchingWindow = Duration.minutes(5); // least frequency const newLogsQueue = new sqs.Queue(this, 'NewLogsQueue', { retentionPeriod: Duration.days(1), @@ -86,7 +86,6 @@ export class AccessLogsMasking extends Construct { s3.EventType.OBJECT_CREATED, new s3n.SqsDestination(newLogsQueue), ); - // triggers MaskAccessLogsLambda when the SQS queue receives a message maskAccessLogsLambda.addEventSource( new lambda_event.SqsEventSource(newLogsQueue, { enabled: true, @@ -105,5 +104,46 @@ export class AccessLogsMasking extends Construct { ], */ }), ); + + // deletes original CloudFront access logs. + // - Lambda function + const deleteAccessLogsLambdaTimeout = Duration.seconds(10); + const deleteAccessLogsLambda = new PythonFunction( + this, + 'DeleteAccessLogsLambda', + { + description: 'Deletes the original CloudFront access logs file', + runtime: lambda.Runtime.PYTHON_3_8, + entry: path.join('lambda', 'delete-access-logs'), + index: 'index.py', + handler: 'lambda_handler', + environment: { + SOURCE_BUCKET_NAME: accessLogsBucket.bucketName, + // bucket name for masked logs is necessary to verify input events. + DESTINATION_BUCKET_NAME: this.maskedAccessLogsBucket.bucketName, + }, + timeout: deleteAccessLogsLambdaTimeout, + }, + ); + accessLogsBucket.grantDelete(deleteAccessLogsLambda); + // - SQS queue to capture creation of masked access logs files, which + // triggers the above Lambda function + const maskedLogsQueue = new sqs.Queue(this, 'MaskedLogsQueue', { + retentionPeriod: Duration.days(1), + visibilityTimeout: maxBatchingWindow.plus( + Duration.seconds(6 * deleteAccessLogsLambdaTimeout.toSeconds()), + ), + }); + this.maskedAccessLogsBucket.addEventNotification( + s3.EventType.OBJECT_CREATED, + new s3n.SqsDestination(maskedLogsQueue), + ); + deleteAccessLogsLambda.addEventSource( + new lambda_event.SqsEventSource(maskedLogsQueue, { + enabled: true, + batchSize: 10, + maxBatchingWindow, + }), + ); } } From 3433cd6f0e44457bb665c3f92b5f4fae1e7df104 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 24 Sep 2022 13:58:17 +0900 Subject: [PATCH 10/41] chore(cdk-ops): rename AccessLogsMasking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `AccessLogsMasking` → `AccessLogsETL`. Because the construct will not just mask access logs but do more transformation. So Extract, Transform, and Load (ETL) should be more suitable. issue codemonger-io/codemonger#30 --- .../lib/{access-logs-masking.ts => access-logs-etl.ts} | 10 ++++++++-- cdk-ops/lib/cdk-ops-stack.ts | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) rename cdk-ops/lib/{access-logs-masking.ts => access-logs-etl.ts} (96%) diff --git a/cdk-ops/lib/access-logs-masking.ts b/cdk-ops/lib/access-logs-etl.ts similarity index 96% rename from cdk-ops/lib/access-logs-masking.ts rename to cdk-ops/lib/access-logs-etl.ts index 39b80fc..e867396 100644 --- a/cdk-ops/lib/access-logs-masking.ts +++ b/cdk-ops/lib/access-logs-etl.ts @@ -21,8 +21,14 @@ export interface Props { deploymentStage: DeploymentStage; } -/** CDK construct that provisions resources to mask CloudFront access logs. */ -export class AccessLogsMasking extends Construct { +/** + * CDK construct that provisions resources to process CloudFront access logs. + * + * @remarks + * + * Defines extract, transform, and load (ETL) operations. + */ +export class AccessLogsETL extends Construct { /** S3 bucket for masked access logs. */ readonly maskedAccessLogsBucket: s3.IBucket; diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index a7bae06..135e4ed 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -1,7 +1,7 @@ import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; -import { AccessLogsMasking } from './access-logs-masking'; +import { AccessLogsETL } from './access-logs-etl'; import { CodemongerResources, CodemongerResourceNames, @@ -25,9 +25,9 @@ export class CdkOpsStack extends Stack { const pipeline = new ContentsPipeline(this, 'ContentsPipeline', { codemongerResources, }); - const developmentContentsAccessLogsMasking = new AccessLogsMasking( + const developmentContentsAccessLogsETL = new AccessLogsETL( this, - 'DevelopmentContentsAccessLogsMasking', + 'DevelopmentContentsAccessLogsETL', { accessLogsBucket: codemongerResources.developmentContentsAccessLogsBucket, From 1fffb18ba15455d2b1dd06c43c0cf445e1cf88aa Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Mon, 26 Sep 2022 10:15:10 +0900 Subject: [PATCH 11/41] feat(cdk-ops): prepend prefix to masked logs - `lambda/mask-access-logs` prepends a prefix to the keys of masked access logs files. The prefix is "masked/". issue codemonger-io/codemonger#30 --- cdk-ops/lambda/delete-access-logs/index.py | 23 +++++++++++++++---- cdk-ops/lambda/mask-access-logs/index.py | 26 ++++++++++++++++------ cdk-ops/lib/access-logs-etl.ts | 4 ++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/cdk-ops/lambda/delete-access-logs/index.py b/cdk-ops/lambda/delete-access-logs/index.py index aacce70..f4aeb2e 100644 --- a/cdk-ops/lambda/delete-access-logs/index.py +++ b/cdk-ops/lambda/delete-access-logs/index.py @@ -4,10 +4,12 @@ masked access logs file. You have to specify the following environment variables, -* ``SOURCE_BUCKET_NAME``: bucket name of the original CloudFront access logs - files -* ``DESTINATION_BUCKET_NAME``: bucket name of the masked CloudFront access logs - files +* ``SOURCE_BUCKET_NAME``: name of the S3 bucket containing original CloudFront + access logs files +* ``DESTINATION_BUCKET_NAME``: name of the S3 bucket containing transformed + CloudFront access logs files +* ``DESTINATION_KEY_PREFIX``: prefix of S3 object keys, which corresponds to + masked access logs """ import json @@ -18,6 +20,7 @@ SOURCE_BUCKET_NAME = os.environ['SOURCE_BUCKET_NAME'] DESTINATION_BUCKET_NAME = os.environ['DESTINATION_BUCKET_NAME'] +DESTINATION_KEY_PREFIX = os.environ['DESTINATION_KEY_PREFIX'] LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) @@ -31,6 +34,9 @@ def lambda_handler(event, _): ``event`` is supposed to be SQS events described at https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html + + Each SQS event is supposed to be an object-creation notification from the + S3 bucket containing masked access logs. """ for record in event['Records']: body = record.get('body') @@ -89,6 +95,15 @@ def process_s3_object(s3_object): if key is None: LOGGER.error('no object key in S3 object event: %s', str(s3_object)) return + if not key.startswith(DESTINATION_KEY_PREFIX): + LOGGER.warning( + '"%s" does not have the preifx "%s".' + ' please check the event source configuration', + key, + DESTINATION_KEY_PREFIX, + ) + return + key = key[len(DESTINATION_KEY_PREFIX):] src = source_bucket.Object(key) res = src.delete() LOGGER.debug('deleted object "%s": %s', key, str(res)) diff --git a/cdk-ops/lambda/mask-access-logs/index.py b/cdk-ops/lambda/mask-access-logs/index.py index db3cedd..2117010 100644 --- a/cdk-ops/lambda/mask-access-logs/index.py +++ b/cdk-ops/lambda/mask-access-logs/index.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- -"""Masks information in CloudFront access logs. +"""Masks information in CloudFront access logs files. You have to specify the following environment variables, -* SOURCE_BUCKET_NAME: name of the S3 bucket containing access logs files to be - masked. +* SOURCE_BUCKET_NAME: name of the S3 bucket containing CloudFront access logs + files to be masked. +* DESTINATION_BUCKET_NAME: name of the S3 bucket where masked CloudFront access + logs files are to be written. +* DESTINATION_KEY_PREFIX: prefix to be prepended to the keys of objects in the + destination bucket. """ import array @@ -21,8 +25,9 @@ import boto3 -SOURCE_BUCKET_NAME = os.environ.get('SOURCE_BUCKET_NAME') -DESTINATION_BUCKET_NAME = os.environ.get('DESTINATION_BUCKET_NAME') +SOURCE_BUCKET_NAME = os.environ['SOURCE_BUCKET_NAME'] +DESTINATION_BUCKET_NAME = os.environ['DESTINATION_BUCKET_NAME'] +DESTINATION_KEY_PREFIX = os.environ['DESTINATION_KEY_PREFIX'] LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) @@ -123,10 +128,17 @@ def process_logs(logs_in: Iterator[str], logs_out: TextIO): def lambda_handler(event, _): - """Masks information in a given CloudFront access logs file on S3. + """Masks information in given CloudFront access logs files on S3. ``event`` is supposed to be an SQS message event described at https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html + + Each SQS message event is supposed to be an object-creation notification + from the S3 bucket specified by ``SOURCE_BUCKET_NAME``. + + This handler masks information in the given S3 objects and stores masked + results into the S3 bucket specified by ``DESTINATION_BUCKET_NAME`` with + the same object key but with ``DESTINATION_KEY_PREFIX`` prefixed. """ for record in event['Records']: try: @@ -189,7 +201,7 @@ def process_s3_object(s3_object): return with open_body(results) as body: with gzip.open(body, mode='rt') as tsv_in: - dest = destination_bucket.Object(key) + dest = destination_bucket.Object(f'{DESTINATION_KEY_PREFIX}{key}') with S3OutputStream(dest) as masked_out: with gzip.open(masked_out, mode='wt') as tsv_out: process_logs(tsv_in, tsv_out) diff --git a/cdk-ops/lib/access-logs-etl.ts b/cdk-ops/lib/access-logs-etl.ts index e867396..21a7b77 100644 --- a/cdk-ops/lib/access-logs-etl.ts +++ b/cdk-ops/lib/access-logs-etl.ts @@ -58,6 +58,7 @@ export class AccessLogsETL extends Construct { // masks newly created CloudFront access logs // - Lambda function + const maskedAccessLogsKeyPrefix = 'masked/'; const maskAccessLogsLambdaTimeout = Duration.seconds(30); const maskAccessLogsLambda = new PythonFunction( this, @@ -71,6 +72,7 @@ export class AccessLogsETL extends Construct { environment: { SOURCE_BUCKET_NAME: accessLogsBucket.bucketName, DESTINATION_BUCKET_NAME: this.maskedAccessLogsBucket.bucketName, + DESTINATION_KEY_PREFIX: maskedAccessLogsKeyPrefix, }, timeout: maskAccessLogsLambdaTimeout, }, @@ -127,6 +129,7 @@ export class AccessLogsETL extends Construct { SOURCE_BUCKET_NAME: accessLogsBucket.bucketName, // bucket name for masked logs is necessary to verify input events. DESTINATION_BUCKET_NAME: this.maskedAccessLogsBucket.bucketName, + DESTINATION_KEY_PREFIX: maskedAccessLogsKeyPrefix, }, timeout: deleteAccessLogsLambdaTimeout, }, @@ -143,6 +146,7 @@ export class AccessLogsETL extends Construct { this.maskedAccessLogsBucket.addEventNotification( s3.EventType.OBJECT_CREATED, new s3n.SqsDestination(maskedLogsQueue), + { prefix: maskedAccessLogsKeyPrefix }, ); deleteAccessLogsLambda.addEventSource( new lambda_event.SqsEventSource(maskedLogsQueue, { From 58327f30402bb68a618230f339ab03dbc31fa203 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Mon, 26 Sep 2022 17:23:02 +0900 Subject: [PATCH 12/41] feat(cdk-ops): prefix date - `lambda/mask-access-logs` prefixes the date of access log records to the output S3 object key. `lambda/delete-access-logs` excludes prefixed dates to locate the original access logs file. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/delete-access-logs/index.py | 16 +- cdk-ops/lambda/mask-access-logs/index.py | 208 ++++++++++++++++----- 2 files changed, 171 insertions(+), 53 deletions(-) diff --git a/cdk-ops/lambda/delete-access-logs/index.py b/cdk-ops/lambda/delete-access-logs/index.py index f4aeb2e..c45f02e 100644 --- a/cdk-ops/lambda/delete-access-logs/index.py +++ b/cdk-ops/lambda/delete-access-logs/index.py @@ -16,6 +16,7 @@ import logging import os import boto3 +from botocore.exceptions import ClientError SOURCE_BUCKET_NAME = os.environ['SOURCE_BUCKET_NAME'] @@ -103,7 +104,14 @@ def process_s3_object(s3_object): DESTINATION_KEY_PREFIX, ) return - key = key[len(DESTINATION_KEY_PREFIX):] - src = source_bucket.Object(key) - res = src.delete() - LOGGER.debug('deleted object "%s": %s', key, str(res)) + # key should be like, + # {DESTINATION_KEY_PREFIX}{year}/{month}/{date}/{original_key} + # so the last segment separated by a slash ('/') is the key for the + # original access logs file. + src_key = key.split('/')[-1] + src = source_bucket.Object(src_key) + try: + res = src.delete() + LOGGER.debug('deleted object "%s": %s', src_key, str(res)) + except ClientError as exc: + LOGGER.error('failed to delete object "%s": %s', src_key, str(exc)) diff --git a/cdk-ops/lambda/mask-access-logs/index.py b/cdk-ops/lambda/mask-access-logs/index.py index 2117010..dae4ce5 100644 --- a/cdk-ops/lambda/mask-access-logs/index.py +++ b/cdk-ops/lambda/mask-access-logs/index.py @@ -19,10 +19,11 @@ import json import logging import os -import sys +import time from contextlib import contextmanager -from typing import Dict, Iterable, Iterator, TextIO +from typing import Dict, Iterable, Iterator, Sequence, TextIO import boto3 +from botocore.exceptions import ClientError SOURCE_BUCKET_NAME = os.environ['SOURCE_BUCKET_NAME'] @@ -107,7 +108,7 @@ def mask_ip_address_v6(addr: str) -> str: return str(net.network_address) -def process_logs(logs_in: Iterator[str], logs_out: TextIO): +def process_logs(src_key: str, logs_in: Iterator[str]): """Processes given CloudFront logs and outputs to given stream. """ tsv_in = csv.DictReader(translate_logs(logs_in), delimiter='\t') @@ -116,15 +117,10 @@ def process_logs(logs_in: Iterator[str], logs_out: TextIO): column_names = tsv_in.fieldnames if column_names is None: raise ValueError('no field names are specified in the input') - tsv_out = csv.DictWriter( - logs_out, - fieldnames=column_names, - delimiter='\t', - ) - tsv_out.writeheader() - for row in tsv_in: - row = mask_row(row) - tsv_out.writerow(row) + with LogDispatcher(src_key, column_names) as dispatcher: + for row in tsv_in: + row = mask_row(row) + dispatcher.writerow(row) def lambda_handler(event, _): @@ -138,7 +134,12 @@ def lambda_handler(event, _): This handler masks information in the given S3 objects and stores masked results into the S3 bucket specified by ``DESTINATION_BUCKET_NAME`` with - the same object key but with ``DESTINATION_KEY_PREFIX`` prefixed. + the same object key but with ``DESTINATION_KEY_PREFIX``, year, month, and + date prefixed. + + ``{DESTINATION_KEY_PREFIX}{year}/{month}/{date}/{key}`` + + where ``year``, ``month``, and ``date`` are the timestamp of a log record. """ for record in event['Records']: try: @@ -201,10 +202,7 @@ def process_s3_object(s3_object): return with open_body(results) as body: with gzip.open(body, mode='rt') as tsv_in: - dest = destination_bucket.Object(f'{DESTINATION_KEY_PREFIX}{key}') - with S3OutputStream(dest) as masked_out: - with gzip.open(masked_out, mode='wt') as tsv_out: - process_logs(tsv_in, tsv_out) + process_logs(key, tsv_in) class S3OutputStream(io.RawIOBase): @@ -296,8 +294,10 @@ def abort(self): """ if self.multipart_upload is not None: LOGGER.debug('aborting the multipart upload') - self.multipart_upload.abort() - self.multipart_upload = None + try: + self.multipart_upload.abort() + finally: + self.multipart_upload = None def __exit__(self, exc_type, exc_value, traceback): @@ -318,6 +318,146 @@ def __del__(self): self.abort() +class GzippedTsvOnS3: + """Gzipped TSV file in an S3 bucket. + """ + + underlying: S3OutputStream + gzipped: TextIO + tsv_writer: csv.DictWriter + + + def __init__( + self, + underlying: S3OutputStream, + gzipped: TextIO, + tsv_writer: csv.DictWriter, + ): + self.underlying = underlying + self.gzipped = gzipped + self.tsv_writer = tsv_writer + + + def close(self): + """Completes the upload of the CSV file. + """ + try: + self.gzipped.close() + except IOError as exc: + LOGGER.error('failed to close a gzip stream: %s', str(exc)) + self.underlying.abort() + else: + try: + self.underlying.close() + except ClientError as exc: + LOGGER.error( + 'failed to finish an S3 object upload: %s', + str(exc), + ) + + + def abort(self): + """Aborts the upload of the CSV file. + """ + try: + self.gzipped.close() + except IOError as exc: + LOGGER.error('failed to close a gzip stream: %s', str(exc)) + try: + self.underlying.abort() + except ClientError as exc: + # TODO: possible exceptions? + LOGGER.error( + 'failed to abort an S3 object upload: %s', + str(exc), + ) + + +class LogDispatcher: + """Distributes access log records to S3 objects corresponding to their + dates. + + You should wrap this object in a ``with`` statement. + """ + + LOG_DATE_FORMAT = '%Y-%m-%d' + + dest_map: Dict[time.struct_time, GzippedTsvOnS3] + + + def __init__(self, src_key: str, column_names: Sequence[str]): + """Initializes with the column names. + """ + self.src_key = src_key + self.column_names = column_names + self.dest_map = {} + + + def writerow(self, row: Dict[str, str]): + """Writes a given row into a matching S3 object. + + Ignores an invalid row. + """ + try: + date = time.strptime(row['date'], LogDispatcher.LOG_DATE_FORMAT) + except KeyError: + LOGGER.warning('log record must have date: %s', str(row)) + except ValueError: + LOGGER.warning('invalid date format: %s', row['date']) + else: + dest = self.get_destination(date) + dest.writerow(row) + + + def get_destination(self, date: time.struct_time) -> csv.DictWriter: + """Obtains the output stream corresponding to a given date. + + Opens a new ``S3OutputStream`` if none has been opened yet. + """ + if date in self.dest_map: + return self.dest_map[date].tsv_writer + year = f'{date.tm_year:04d}' + month = f'{date.tm_mon:02d}' + mday = f'{date.tm_mday:02d}' + key = f'{DESTINATION_KEY_PREFIX}{year}/{month}/{mday}/{self.src_key}' + dest_stream = S3OutputStream(destination_bucket.Object(key)) + dest_gzip = gzip.open(dest_stream, mode='wt') + dest_tsv = csv.DictWriter( + dest_gzip, + fieldnames=self.column_names, + delimiter='\t', + ) + self.dest_map[date] = GzippedTsvOnS3(dest_stream, dest_gzip, dest_tsv) + dest_tsv.writeheader() + return dest_tsv + + + def close(self): + """Completes log dispatch and S3 object uploads. + """ + for dest in self.dest_map.values(): + dest.close() + + + def abort(self): + """Aborts log dispatch and S3 object uploads. + """ + for dest in self.dest_map.values(): + dest.abort() + + + def __enter__(self): + return self + + + def __exit__(self, exc_type, _exc_val, _exc_tb): + if exc_type is None: + self.close() + else: + self.abort() + return False + + @contextmanager def open_body(s3_get_results): """Enables ``with`` statement for a body got from an S3 bucket. @@ -327,33 +467,3 @@ def open_body(s3_get_results): yield body finally: body.close() - - -if __name__ == '__main__': - import argparse - arg_parser = argparse.ArgumentParser( - description='Masks CloudFront access logs', - ) - arg_parser.add_argument( - 'logs_path', - metavar='LOGS', - type=str, - help='path to a gzipped TSV file containing CloudFront access logs', - ) - arg_parser.add_argument( - '--out', - dest='out_path', - metavar='OUT', - type=str, - help='path to a file where masked CloudFront access logs are to be' - ' saved (gzipped)', - ) - logging.basicConfig(level=logging.DEBUG) - LOGGER.debug('filtering access logs') - args = arg_parser.parse_args() - if args.out_path is not None: - results_out = gzip.open(args.out_path, mode='wt') - else: - results_out = sys.stdout - with gzip.open(args.logs_path, mode='rt') as text_in: - process_logs(text_in, results_out) From 344b6d37834372e9e7cb92bd2778e80a6325c5ab Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 1 Oct 2022 16:22:56 +0900 Subject: [PATCH 13/41] feat(cdk-ops): configure env - Configures the `env` property of the CDK stack so that correct availability zones (AZs) in the region can be listed. CDK lists only two AZs without these changes. issue codemonger-io/codemonger#30 --- cdk-ops/bin/cdk-ops.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cdk-ops/bin/cdk-ops.ts b/cdk-ops/bin/cdk-ops.ts index b27a6fe..ed92b34 100644 --- a/cdk-ops/bin/cdk-ops.ts +++ b/cdk-ops/bin/cdk-ops.ts @@ -23,6 +23,15 @@ resolveCodemongerResourceNames() // env: { account: '123456789012', region: 'us-east-1' }, /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ + env: { + // without the following properties `account` and `region`, + // the stack becomes "environment-agnostic." + // only two availability zones (AZs) are visible in an + // evironment-agnostic stack. + // https://docs.aws.amazon.com/cdk/v2/guide/environments.html + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, codemongerResourceNames: names, tags: { project: 'codemonger', From 91dfd3275d8316ab535186e89302856a1ced08d0 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 1 Oct 2022 16:29:05 +0900 Subject: [PATCH 14/41] feat(cdk-ops): latest boto3 layer - Introduces a new CDK construct `LatestBoto3Layer` that provisions a Lambda layer containing the latest boto3. `lambda/latest-boto3/requirements.txt` lists the modules and versions to be packaged as a Lambda layer. The latest boto3 is necessary because the default boto3 on the Lambda runtime does not support Redshift Serverless. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/latest-boto3/requirements.txt | 2 ++ cdk-ops/lib/cdk-ops-stack.ts | 2 ++ cdk-ops/lib/latest-boto3-layer.ts | 28 ++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 cdk-ops/lambda/latest-boto3/requirements.txt create mode 100644 cdk-ops/lib/latest-boto3-layer.ts diff --git a/cdk-ops/lambda/latest-boto3/requirements.txt b/cdk-ops/lambda/latest-boto3/requirements.txt new file mode 100644 index 0000000..031ddd5 --- /dev/null +++ b/cdk-ops/lambda/latest-boto3/requirements.txt @@ -0,0 +1,2 @@ +boto3==1.24.75 +botocore==1.27.75 diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index 135e4ed..994a951 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -7,6 +7,7 @@ import { CodemongerResourceNames, } from './codemonger-resources'; import { ContentsPipeline } from './contents-pipeline'; +import { LatestBoto3Layer } from './latest-boto3-layer'; type Props = StackProps & Readonly<{ // names of the main codemonger resources. @@ -22,6 +23,7 @@ export class CdkOpsStack extends Stack { 'CodemongerResources', props.codemongerResourceNames, ); + const latestBoto3 = new LatestBoto3Layer(this, 'LatestBoto3'); const pipeline = new ContentsPipeline(this, 'ContentsPipeline', { codemongerResources, }); diff --git a/cdk-ops/lib/latest-boto3-layer.ts b/cdk-ops/lib/latest-boto3-layer.ts new file mode 100644 index 0000000..409bc67 --- /dev/null +++ b/cdk-ops/lib/latest-boto3-layer.ts @@ -0,0 +1,28 @@ +import * as path from 'path'; + +import { aws_lambda as lambda } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { PythonLayerVersion } from '@aws-cdk/aws-lambda-python-alpha'; + +/** CDK construct that provisions a Lambda layer containing the latest boto3. */ +export class LatestBoto3Layer extends Construct { + /** Lambda layer containing the latest boto3. */ + readonly layer: lambda.ILayerVersion; + + constructor(scope: Construct, id: string) { + super(scope, id); + + this.layer = new PythonLayerVersion(this, 'LambdaLayer', { + description: 'Lambda layer containing the latest boto3', + entry: path.join('lambda', 'latest-boto3'), + compatibleRuntimes: [ + lambda.Runtime.PYTHON_3_8, + lambda.Runtime.PYTHON_3_9, + ], + compatibleArchitectures: [ + lambda.Architecture.ARM_64, + lambda.Architecture.X86_64, + ], + }); + } +} From b2b6987d0a88061fe4a0fe1eb0043517bc821897 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 1 Oct 2022 16:47:11 +0900 Subject: [PATCH 15/41] feat(cdk-ops): provision data warehouse for access logs - Introduces a new CDK construct `DataWarehouse` that provisions the Redshift Serverless namespace and workgroup for access logs. It also provisions a VPC where the Redshift Serverless cluster will reside. In addition to the Redshift Serverless cluster, it provisions a Lambda function `lambda/populate-dw-database` that populates the database and tables to store access logs. - `CdkOpsStack` provisions `DataWarehouse`. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/populate-dw-database/index.py | 234 +++++++++++++++++++ cdk-ops/lib/cdk-ops-stack.ts | 5 + cdk-ops/lib/data-warehouse.ts | 175 ++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 cdk-ops/lambda/populate-dw-database/index.py create mode 100644 cdk-ops/lib/data-warehouse.ts diff --git a/cdk-ops/lambda/populate-dw-database/index.py b/cdk-ops/lambda/populate-dw-database/index.py new file mode 100644 index 0000000..7086980 --- /dev/null +++ b/cdk-ops/lambda/populate-dw-database/index.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- + +"""Populates the data warehouse database and tables. + +You have to configure the following environment variables, +- ``WORKGROUP_NAME``: name of the Redshift Serverless workgroup to connect to +- ``ADMIN_SECRET_ARN``: ARN of the admin secret +""" + +import logging +import os +import time +from typing import Dict, Optional, Sequence, Tuple +import boto3 + + +WORKGROUP_NAME = os.environ['WORKGROUP_NAME'] +ADMIN_SECRET_ARN = os.environ['ADMIN_SECRET_ARN'] +ADMIN_DATABASE_NAME = os.environ['ADMIN_DATABASE_NAME'] +ACCESS_LOGS_DATABASE_NAME = os.environ['ACCESS_LOGS_DATABASE_NAME'] +REFERER_TABLE_NAME = os.environ['REFERER_TABLE_NAME'] +PAGE_TABLE_NAME = os.environ['PAGE_TABLE_NAME'] +EDGE_LOCATION_TABLE_NAME = os.environ['EDGE_LOCATION_TABLE_NAME'] +USER_AGENT_TABLE_NAME = os.environ['USER_AGENT_TABLE_NAME'] +RESULT_TYPE_TABLE_NAME = os.environ['RESULT_TYPE_TABLE_NAME'] +ACCESS_LOG_TABLE_NAME = os.environ['ACCESS_LOG_TABLE_NAME'] + +POLLING_INTERVAL_IN_S = 0.05 +MAX_POLLING_COUNTER = round(60 / POLLING_INTERVAL_IN_S) # > 1 minute + +RUNNING_STATUSES = ['SUBMITTED', 'PICKED', 'STARTED'] + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +redshift_data = boto3.client('redshift-data') + + +class DataWarehouseException(Exception): + """Exception raised when a data warehouse operation fails. + """ + + message: str + + + def __init__(self, message: str): + """Initializes with a given message. + """ + self.message = message + + + def __str__(self): + classname = type(self).__name__ + return f'{classname}({self.message})' + + + def __repr__(self): + classname = type(self).__name__ + return f'{classname}({repr(self.message)})' + + +def get_create_database_statement() -> str: + """Returns an SQL statement to create the database. + """ + return f'CREATE DATABASE {ACCESS_LOGS_DATABASE_NAME}' + + +def get_create_tables_script() -> Sequence[str]: + """Returns SQL statements to create tables. + """ + return [ + get_create_referer_table_statement(), + get_create_page_table_statement(), + get_create_edge_location_table_statement(), + get_create_user_agent_table_statement(), + get_create_result_type_table_statement(), + get_create_access_log_table_statement(), + ] + + +def get_create_referer_table_statement() -> str: + """Returns an SQL statement to create the table for referers. + """ + return ''.join([ + f'CREATE TABLE IF NOT EXISTS {REFERER_TABLE_NAME} (', + ' id BIGINT IDENTITY(1, 1) DISTKEY,', + ' url VARCHAR NOT NULL,', + ' PRIMARY KEY (id)', + ')', + ]) + + +def get_create_page_table_statement() -> str: + """Returns an SQL statement to create the table for pages. + """ + return ''.join([ + f'CREATE TABLE IF NOT EXISTS {PAGE_TABLE_NAME} (', + ' id INT IDENTITY(1, 1),', + ' path VARCHAR NOT NULL,' + ' PRIMARY KEY (id)', + ')', + ]) + + +def get_create_edge_location_table_statement() -> str: + """Returns an SQL statement to create the table for edge locations. + """ + return ''.join([ + f'CREATE TABLE IF NOT EXISTS {EDGE_LOCATION_TABLE_NAME} (', + ' id INT IDENTITY(1, 1),', + ' code VARCHAR NOT NULL,', + ' PRIMARY KEY (id)', + ')' + ]) + + +def get_create_user_agent_table_statement() -> str: + """Returns an SQL statement to create the table for user agents. + """ + return ''.join([ + f'CREATE TABLE IF NOT EXISTS {USER_AGENT_TABLE_NAME} (' + ' id BIGINT IDENTITY(1, 1),', + ' user_agent VARCHAR NOT NULL,', + ' PRIMARY KEY (id)', + ')', + ]) + + +def get_create_result_type_table_statement() -> str: + """Returns an SQL statement to create the table for result types. + """ + return ''.join([ + f'CREATE TABLE IF NOT EXISTS {RESULT_TYPE_TABLE_NAME} (' + ' id INT IDENTITY(1, 1),', + ' result_type VARCHAR NOT NULL,', + ' PRIMARY KEY (id)', + ')', + ]) + + +def get_create_access_log_table_statement() -> str: + """Returns an SQL statement to create the table for access logs. + """ + return ''.join([ + f'CREATE TABLE IF NOT EXISTS {ACCESS_LOG_TABLE_NAME} (', + ' datetime TIMESTAMP SORTKEY NOT NULL,', + ' edge_location INT NOT NULL,', + ' sc_bytes BIGINT NOT NULL,', + ' cs_method VARCHAR NOT NULL,', + ' page INT NOT NULL,', + ' status SMALLINT NOT NULL,', + ' referer BIGINT DISTKEY,', + ' user_agent BIGINT NOT NULL,', + ' cs_protocol VARCHAR NOT NULL,', + ' cs_bytes BIGINT NOT NULL,', + ' time_taken FLOAT4 NOT NULL,', + ' edge_response_result_type INT NOT NULL,', + ' time_to_first_byte FLOAT4 NOT NULL,', + f' FOREIGN KEY (edge_location) REFERENCES {EDGE_LOCATION_TABLE_NAME},' + f' FOREIGN KEY (page) REFERENCES {PAGE_TABLE_NAME},' + f' FOREIGN KEY (referer) REFERENCES {REFERER_TABLE_NAME},' + f' FOREIGN KEY (user_agent) REFERENCES {USER_AGENT_TABLE_NAME},' + f' FOREIGN KEY (edge_response_result_type) REFERENCES {RESULT_TYPE_TABLE_NAME}' + ')', + ]) + + +def wait_for_statement(statement_id: str) -> Tuple[Optional[str], Dict]: + """Waits for a given statement to finish. + + :returns: final status of the statement. + ``None`` if polling has timed out. + """ + counter = 0 + while counter < MAX_POLLING_COUNTER: + res = redshift_data.describe_statement(Id=statement_id) + if counter % 20 == 0: + LOGGER.debug('polling statement status [%d]: %s', counter, str(res)) + if res['Status'] not in RUNNING_STATUSES: + LOGGER.debug( + 'statement done in: %.3f ms', + res.get('Duration', 0) * 0.001 * 0.001, # ns → ms + ) + return res['Status'], res + time.sleep(POLLING_INTERVAL_IN_S) + counter += 1 + return None, res + + +def lambda_handler(event, _): + """Populates the data warehouse database and tables. + """ + LOGGER.debug( + 'populating data warehouse database and tables: %s', + str(event), + ) + # populates the database + res = redshift_data.execute_statement( + WorkgroupName=WORKGROUP_NAME, + SecretArn=ADMIN_SECRET_ARN, + Database=ADMIN_DATABASE_NAME, + Sql=get_create_database_statement(), + ) + status, res = wait_for_statement(res['Id']) + if status != 'FINISHED': + if status == 'FAILED': + # ignores the error if the database already exists + if not res.get('Error', '').lower().endswith('already exists'): + raise DataWarehouseException( + f'failed to create the database: {res.get("Error")}', + ) + else: + raise DataWarehouseException( + f'failed to create the database: {status or "timeout"}', + ) + # populates the tables + res = redshift_data.batch_execute_statement( + WorkgroupName=WORKGROUP_NAME, + SecretArn=ADMIN_SECRET_ARN, + Database=ACCESS_LOGS_DATABASE_NAME, + Sqls=get_create_tables_script(), + ) + status, res = wait_for_statement(res['Id']) + if status != 'FINISHED': + if status == 'FAILED': + raise DataWarehouseException( + f'failed to populate tables: {res.get("Error")}', + ) + raise DataWarehouseException( + f'failed to populate tables: {status or "timeout"}', + ) + return { + 'statusCode': 200, + } diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index 994a951..ce2ee7b 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -7,6 +7,7 @@ import { CodemongerResourceNames, } from './codemonger-resources'; import { ContentsPipeline } from './contents-pipeline'; +import { DataWarehouse } from './data-warehouse'; import { LatestBoto3Layer } from './latest-boto3-layer'; type Props = StackProps & Readonly<{ @@ -27,6 +28,10 @@ export class CdkOpsStack extends Stack { const pipeline = new ContentsPipeline(this, 'ContentsPipeline', { codemongerResources, }); + const dataWarehouse = new DataWarehouse(this, 'DevelopmentDataWarehouse', { + latestBoto3, + deploymentStage: 'development', + }); const developmentContentsAccessLogsETL = new AccessLogsETL( this, 'DevelopmentContentsAccessLogsETL', diff --git a/cdk-ops/lib/data-warehouse.ts b/cdk-ops/lib/data-warehouse.ts new file mode 100644 index 0000000..ab00882 --- /dev/null +++ b/cdk-ops/lib/data-warehouse.ts @@ -0,0 +1,175 @@ +import * as path from 'path'; + +import { + Duration, + aws_ec2 as ec2, + aws_iam as iam, + aws_lambda as lambda, + aws_redshiftserverless as redshift, + aws_secretsmanager as secrets, +} from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; + +import type { DeploymentStage } from 'cdk-common'; + +import { LatestBoto3Layer } from './latest-boto3-layer'; + +/** Name of the admin user. */ +export const ADMIN_USER_NAME = 'dwadmin'; + +/** Subnet group name of the cluster for Redshift Serverless. */ +export const CLUSTER_SUBNET_GROUP_NAME = 'dw-cluster'; + +export interface Props { + /** Lambda layer containing the latest boto3. */ + latestBoto3: LatestBoto3Layer; + /** Deployment stage. */ + deploymentStage: DeploymentStage; +} + +/** Provisions resources for the data warehouse. */ +export class DataWarehouse extends Construct { + /** VPC for Redshift Serverless clusters. */ + readonly vpc: ec2.IVpc; + /** Secret for the admin user. */ + readonly adminSecret: secrets.ISecret; + /** IAM role of Redshift Serverless namespace. */ + readonly namespaceRole: iam.IRole; + + constructor(scope: Construct, id: string, props: Props) { + super(scope, id); + + const { deploymentStage, latestBoto3 } = props; + + this.vpc = new ec2.Vpc(this, `DwVpc`, { + cidr: '192.168.0.0/16', + enableDnsSupport: false, + enableDnsHostnames: false, + subnetConfiguration: [ + { + name: CLUSTER_SUBNET_GROUP_NAME, + subnetType: ec2.SubnetType.PRIVATE_ISOLATED, + // to reserve private addresses for the future + // allocates up to 1024 private addresses in each subnet + cidrMask: 22, + }, + ], + }); + + // provisions Redshift Serverless resources + // - secret for admin + this.adminSecret = new secrets.Secret(this, 'DwAdminSecret', { + description: `Data Warehouse secret (${deploymentStage})`, + generateSecretString: { + // the following requirement is too strict, but should not matter. + excludePunctuation: true, + // the structure of a secret value for Redshift is described below + // https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html#reference_secret_json_structure_RS + // + // whether it also works with Redshift Serverless is unclear. + // as far as I tested, only "username" and "password" are required. + secretStringTemplate: JSON.stringify({ + username: ADMIN_USER_NAME, + }), + generateStringKey: 'password', + }, + }); + // - IAM role for the namespace + this.namespaceRole = new iam.Role(this, 'DwNamespaceRole', { + description: `Data Warehouse Role (${deploymentStage})`, + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('redshift-serverless.amazonaws.com'), + new iam.ServicePrincipal('redshift.amazonaws.com'), + ), + }); + // - namespace + const dwNamespace = new redshift.CfnNamespace(this, 'DwNamespace', { + namespaceName: `datawarehouse-${deploymentStage}`, + adminUsername: ADMIN_USER_NAME, + adminUserPassword: + this.adminSecret.secretValueFromJson('password').unsafeUnwrap(), + defaultIamRoleArn: this.namespaceRole.roleArn, + iamRoles: [this.namespaceRole.roleArn], + tags: [ + { + key: 'project', + value: 'codemonger', + }, + { + key: 'stage', + value: deploymentStage, + }, + ], + }); + dwNamespace.addDependsOn( + this.adminSecret.node.defaultChild as secrets.CfnSecret, + ); + // - workgroup + const workgroup = new redshift.CfnWorkgroup(this, 'DwWorkgroup', { + workgroupName: `datawarehouse-${deploymentStage}`, + namespaceName: dwNamespace.namespaceName, + baseCapacity: 32, + subnetIds: this.getSubnetIdsForCluster(), + tags: [ + { + key: 'project', + value: 'codemonger', + }, + { + key: 'stage', + value: deploymentStage, + }, + ], + }); + workgroup.addDependsOn(dwNamespace); + + // Lambda function that populates the database and tables. + const populateDwDatabaseLambda = new PythonFunction( + this, + 'PopulateDwDatabaseLambda', + { + description: `Populates the data warehouse database and tables (${deploymentStage})`, + runtime: lambda.Runtime.PYTHON_3_8, + architecture: lambda.Architecture.ARM_64, + entry: path.join('lambda', 'populate-dw-database'), + index: 'index.py', + handler: 'lambda_handler', + layers: [latestBoto3.layer], + environment: { + WORKGROUP_NAME: workgroup.workgroupName, + ADMIN_SECRET_ARN: this.adminSecret.secretArn, + ADMIN_DATABASE_NAME: 'dev', + ACCESS_LOGS_DATABASE_NAME: 'access_logs', + PAGE_TABLE_NAME: 'page', + REFERER_TABLE_NAME: 'referer', + EDGE_LOCATION_TABLE_NAME: 'edge_location', + USER_AGENT_TABLE_NAME: 'user_agent', + RESULT_TYPE_TABLE_NAME: 'result_type', + ACCESS_LOG_TABLE_NAME: 'access_log', + }, + timeout: Duration.minutes(15), + // a Lambda function does not have to join the VPC + // as long as it uses Redshift Data API. + // + // if want to directly connect to the Redshift cluster from a Lambda, + // we have to put the Lambda in the VPC and allocate a VPC endpoint. + // but I cannot afford VPC endpoints for now. + // + // alternatively, we could run the Redshift cluster in a public subnet. + }, + ); + // Redshift Data API uses the execution role of the Lambda function to + // retrieve the secret. + this.adminSecret.grantRead(populateDwDatabaseLambda); + // TODO: too permissive? + populateDwDatabaseLambda.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonRedshiftDataFullAccess')); + } + + /** Returns subnet IDs for the cluster of Redshift Serverless. */ + getSubnetIdsForCluster(): string[] { + return this.vpc.selectSubnets({ + subnetGroupName: CLUSTER_SUBNET_GROUP_NAME, + }).subnetIds; + } +} From 268279fb6424224af1ba13a7542f8763c4e024d2 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 8 Oct 2022 12:14:43 +0900 Subject: [PATCH 16/41] chore(cdk-ops): install cdk2-python-library-layer - Installs `cdk2-python-library-layer` to create a Lambda layer from a local Python package. issue codemonger-io/codemonger#30 --- cdk-ops/package-lock.json | 102 ++++++++++++++++++++++++++++++++++++-- cdk-ops/package.json | 1 + 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/cdk-ops/package-lock.json b/cdk-ops/package-lock.json index 77865df..14a5426 100644 --- a/cdk-ops/package-lock.json +++ b/cdk-ops/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/client-cloudformation": "^3.112.0", "aws-cdk-lib": "^2.42.0", "cdk-common": "file:../cdk-common", + "cdk2-python-library-layer": "github:kikuomax/cdk-python-library-layer#v0.1.0-v2", "constructs": "^10.1.106", "source-map-support": "^0.5.21" }, @@ -2525,6 +2526,21 @@ "resolved": "../cdk-common", "link": true }, + "node_modules/cdk2-python-library-layer": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/kikuomax/cdk-python-library-layer.git#d1133709d2e9e3833acc4ab097a641568e3ede2b", + "license": "MIT", + "dependencies": { + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "aws-cdk-lib": ">=2.0.0", + "constructs": ">=10.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3032,6 +3048,27 @@ "node": ">= 6" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3129,8 +3166,7 @@ "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/has": { "version": "1.0.3", @@ -4127,6 +4163,25 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7370,6 +7425,13 @@ "typescript": "^3.9.10" } }, + "cdk2-python-library-layer": { + "version": "git+ssh://git@github.com/kikuomax/cdk-python-library-layer.git#d1133709d2e9e3833acc4ab097a641568e3ede2b", + "from": "cdk2-python-library-layer@https://github.com/kikuomax/cdk-python-library-layer.git#v0.1.0-v2", + "requires": { + "fs-extra": "^10.0.0" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7756,6 +7818,23 @@ "mime-types": "^2.1.12" } }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7822,8 +7901,7 @@ "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "has": { "version": "1.0.3", @@ -8590,6 +8668,22 @@ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "dev": true }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", diff --git a/cdk-ops/package.json b/cdk-ops/package.json index c985622..0b4c80d 100644 --- a/cdk-ops/package.json +++ b/cdk-ops/package.json @@ -25,6 +25,7 @@ "@aws-sdk/client-cloudformation": "^3.112.0", "aws-cdk-lib": "^2.42.0", "cdk-common": "file:../cdk-common", + "cdk2-python-library-layer": "github:kikuomax/cdk-python-library-layer#v0.1.0-v2", "constructs": "^10.1.106", "source-map-support": "^0.5.21" } From 570d1519c6ef94dc6cc62561ea3500b3d65c8859 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 8 Oct 2022 12:11:50 +0900 Subject: [PATCH 17/41] feat(cdk-ops): add libdatawarehouse - Introduces a new Lambda layer `lambda/libdatawarehouse` that provides utilities to handle the data warehouse for CloudFront access logs. A new CDK construct `LibdatawarehouseLayer` provisions it. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/libdatawarehouse/.gitignore | 2 + .../lambda/libdatawarehouse/pyproject.toml | 3 ++ cdk-ops/lambda/libdatawarehouse/setup.cfg | 13 +++++++ .../src/libdatawarehouse/__init__.py | 8 ++++ .../src/libdatawarehouse/data_api.py | 37 +++++++++++++++++++ .../src/libdatawarehouse/exceptions.py | 22 +++++++++++ .../src/libdatawarehouse/py.typed | 0 .../src/libdatawarehouse/tables.py | 16 ++++++++ cdk-ops/lib/libdatawarehouse-layer.ts | 20 ++++++++++ 9 files changed, 121 insertions(+) create mode 100644 cdk-ops/lambda/libdatawarehouse/.gitignore create mode 100644 cdk-ops/lambda/libdatawarehouse/pyproject.toml create mode 100644 cdk-ops/lambda/libdatawarehouse/setup.cfg create mode 100644 cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/__init__.py create mode 100644 cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/data_api.py create mode 100644 cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/exceptions.py create mode 100644 cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/py.typed create mode 100644 cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/tables.py create mode 100644 cdk-ops/lib/libdatawarehouse-layer.ts diff --git a/cdk-ops/lambda/libdatawarehouse/.gitignore b/cdk-ops/lambda/libdatawarehouse/.gitignore new file mode 100644 index 0000000..d3b6142 --- /dev/null +++ b/cdk-ops/lambda/libdatawarehouse/.gitignore @@ -0,0 +1,2 @@ +/build +*.egg-info diff --git a/cdk-ops/lambda/libdatawarehouse/pyproject.toml b/cdk-ops/lambda/libdatawarehouse/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/cdk-ops/lambda/libdatawarehouse/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/cdk-ops/lambda/libdatawarehouse/setup.cfg b/cdk-ops/lambda/libdatawarehouse/setup.cfg new file mode 100644 index 0000000..35d5dcf --- /dev/null +++ b/cdk-ops/lambda/libdatawarehouse/setup.cfg @@ -0,0 +1,13 @@ +[metadata] +name = libdatawarehouse +version = attr: libdatawarehouse.VERSION +description = Library for Codemonger Data Warehouse + +[options] +packages = + libdatawarehouse +package_dir = + = src + +[options.package_data] +libdatawarehouse = *.pyi, py.typed diff --git a/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/__init__.py b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/__init__.py new file mode 100644 index 0000000..77e4928 --- /dev/null +++ b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +"""Library for Codemonger Data Warehouse. +""" + +VERSION = '0.1.0' + +ACCESS_LOGS_DATABASE_NAME = 'access_logs' diff --git a/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/data_api.py b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/data_api.py new file mode 100644 index 0000000..095d669 --- /dev/null +++ b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/data_api.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +"""Provides utilities to access the Redshift Data API. +""" + +import time +from typing import Dict, Optional, Tuple + + +RUNNING_STATUSES = ['SUBMITTED', 'PICKED', 'STARTED'] + + +def wait_for_results( + client, + statement_id: str, + polling_interval: float = 0.05, + polling_timeout: int = round(300 / 0.05), +) -> Tuple[Optional[str], Dict]: + """Waits for a given statement to finish. + + :param RedshiftDataAPIService.Client client: Redshift Data API client. + + :param float polling_interval: interval in seconds between two consecutive + pollings. + + :param int polling_timeout: timeout represented as the number of pollings. + """ + polling_counter = 0 + while True: + res = client.describe_statement(Id=statement_id) + status = res['Status'] + if status not in RUNNING_STATUSES: + return status, res + polling_counter += 1 + if polling_counter >= polling_timeout: + return None, res + time.sleep(polling_interval) diff --git a/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/exceptions.py b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/exceptions.py new file mode 100644 index 0000000..7e16388 --- /dev/null +++ b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/exceptions.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +"""Common exceptions. +""" + +class DataWarehouseException(Exception): + """Base exception raised when a data warehouse operation fails. + """ + def __init__(self, message: str): + """Initializes with a message. + """ + self.message = message + + + def __str__(self) -> str: + classname = type(self).__name__ + return f'{classname}({self.message})' + + + def __repr__(self) -> str: + classname = type(self).__name__ + return f'{classname}({repr(self.message)})' diff --git a/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/py.typed b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/tables.py b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/tables.py new file mode 100644 index 0000000..1a727fa --- /dev/null +++ b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/tables.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +"""Tables in the data warehouse. +""" + +ACCESS_LOG_TABLE_NAME = 'access_log' + +REFERER_TABLE_NAME = 'referer' + +PAGE_TABLE_NAME = 'page' + +USER_AGENT_TABLE_NAME = 'user_agent' + +EDGE_LOCATION_TABLE_NAME = 'edge_location' + +RESULT_TYPE_TABLE_NAME = 'result_type' diff --git a/cdk-ops/lib/libdatawarehouse-layer.ts b/cdk-ops/lib/libdatawarehouse-layer.ts new file mode 100644 index 0000000..8e91260 --- /dev/null +++ b/cdk-ops/lib/libdatawarehouse-layer.ts @@ -0,0 +1,20 @@ +import * as path from 'path'; +import { aws_lambda as lambda } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +import { PythonLibraryLayer } from 'cdk2-python-library-layer'; + +/** CDK construct that provisions a Lambda layer of `libdatawarehouse`. */ +export class LibdatawarehouseLayer extends Construct { + /** Lambda layer of `libdatawarehouse`. */ + readonly layer: lambda.ILayerVersion; + + constructor(scope: Construct, id: string) { + super(scope, id); + + this.layer = new PythonLibraryLayer(this, 'Layer', { + runtime: lambda.Runtime.PYTHON_3_8, + entry: path.join('lambda', 'libdatawarehouse'), + }); + } +} From 29b0b25b1269069b7797a3b1d5d94e4ebfb4e1c9 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 8 Oct 2022 12:20:08 +0900 Subject: [PATCH 18/41] feat(cdk-ops): use libdatawarehouse - `lambda/populate-dw-database` uses `libdatawarehouse` to replace commonly used types and functions. - `CdkOpsStack` provisions `LibdatawarehouseLayer` and passes it to `DataWarehouse`. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/populate-dw-database/index.py | 123 +++++++------------ cdk-ops/lib/cdk-ops-stack.ts | 4 + cdk-ops/lib/data-warehouse.ts | 14 +-- 3 files changed, 55 insertions(+), 86 deletions(-) diff --git a/cdk-ops/lambda/populate-dw-database/index.py b/cdk-ops/lambda/populate-dw-database/index.py index 7086980..0f5d4c7 100644 --- a/cdk-ops/lambda/populate-dw-database/index.py +++ b/cdk-ops/lambda/populate-dw-database/index.py @@ -5,30 +5,20 @@ You have to configure the following environment variables, - ``WORKGROUP_NAME``: name of the Redshift Serverless workgroup to connect to - ``ADMIN_SECRET_ARN``: ARN of the admin secret +- ``ADMIN_DATABASE_NAME``: name of the admin database """ import logging import os -import time -from typing import Dict, Optional, Sequence, Tuple +from typing import Sequence import boto3 +from libdatawarehouse import ACCESS_LOGS_DATABASE_NAME, data_api, tables +from libdatawarehouse.exceptions import DataWarehouseException WORKGROUP_NAME = os.environ['WORKGROUP_NAME'] ADMIN_SECRET_ARN = os.environ['ADMIN_SECRET_ARN'] ADMIN_DATABASE_NAME = os.environ['ADMIN_DATABASE_NAME'] -ACCESS_LOGS_DATABASE_NAME = os.environ['ACCESS_LOGS_DATABASE_NAME'] -REFERER_TABLE_NAME = os.environ['REFERER_TABLE_NAME'] -PAGE_TABLE_NAME = os.environ['PAGE_TABLE_NAME'] -EDGE_LOCATION_TABLE_NAME = os.environ['EDGE_LOCATION_TABLE_NAME'] -USER_AGENT_TABLE_NAME = os.environ['USER_AGENT_TABLE_NAME'] -RESULT_TYPE_TABLE_NAME = os.environ['RESULT_TYPE_TABLE_NAME'] -ACCESS_LOG_TABLE_NAME = os.environ['ACCESS_LOG_TABLE_NAME'] - -POLLING_INTERVAL_IN_S = 0.05 -MAX_POLLING_COUNTER = round(60 / POLLING_INTERVAL_IN_S) # > 1 minute - -RUNNING_STATUSES = ['SUBMITTED', 'PICKED', 'STARTED'] LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) @@ -36,31 +26,8 @@ redshift_data = boto3.client('redshift-data') -class DataWarehouseException(Exception): - """Exception raised when a data warehouse operation fails. - """ - - message: str - - - def __init__(self, message: str): - """Initializes with a given message. - """ - self.message = message - - - def __str__(self): - classname = type(self).__name__ - return f'{classname}({self.message})' - - - def __repr__(self): - classname = type(self).__name__ - return f'{classname}({repr(self.message)})' - - def get_create_database_statement() -> str: - """Returns an SQL statement to create the database. + """Returns an SQL statement to create the database for access logs. """ return f'CREATE DATABASE {ACCESS_LOGS_DATABASE_NAME}' @@ -70,11 +37,19 @@ def get_create_tables_script() -> Sequence[str]: """ return [ get_create_referer_table_statement(), + get_grant_public_table_access_statement(tables.REFERER_TABLE_NAME), get_create_page_table_statement(), + get_grant_public_table_access_statement(tables.PAGE_TABLE_NAME), get_create_edge_location_table_statement(), + get_grant_public_table_access_statement( + tables.EDGE_LOCATION_TABLE_NAME, + ), get_create_user_agent_table_statement(), + get_grant_public_table_access_statement(tables.USER_AGENT_TABLE_NAME), get_create_result_type_table_statement(), + get_grant_public_table_access_statement(tables.RESULT_TYPE_TABLE_NAME), get_create_access_log_table_statement(), + get_grant_public_table_access_statement(tables.ACCESS_LOG_TABLE_NAME), ] @@ -82,9 +57,9 @@ def get_create_referer_table_statement() -> str: """Returns an SQL statement to create the table for referers. """ return ''.join([ - f'CREATE TABLE IF NOT EXISTS {REFERER_TABLE_NAME} (', + f'CREATE TABLE IF NOT EXISTS {tables.REFERER_TABLE_NAME} (', ' id BIGINT IDENTITY(1, 1) DISTKEY,', - ' url VARCHAR NOT NULL,', + ' url VARCHAR(2048) NOT NULL SORTKEY UNIQUE,', ' PRIMARY KEY (id)', ')', ]) @@ -94,9 +69,9 @@ def get_create_page_table_statement() -> str: """Returns an SQL statement to create the table for pages. """ return ''.join([ - f'CREATE TABLE IF NOT EXISTS {PAGE_TABLE_NAME} (', + f'CREATE TABLE IF NOT EXISTS {tables.PAGE_TABLE_NAME} (', ' id INT IDENTITY(1, 1),', - ' path VARCHAR NOT NULL,' + ' path VARCHAR(2048) NOT NULL SORTKEY UNIQUE,' ' PRIMARY KEY (id)', ')', ]) @@ -106,9 +81,9 @@ def get_create_edge_location_table_statement() -> str: """Returns an SQL statement to create the table for edge locations. """ return ''.join([ - f'CREATE TABLE IF NOT EXISTS {EDGE_LOCATION_TABLE_NAME} (', + f'CREATE TABLE IF NOT EXISTS {tables.EDGE_LOCATION_TABLE_NAME} (', ' id INT IDENTITY(1, 1),', - ' code VARCHAR NOT NULL,', + ' code VARCHAR NOT NULL SORTKEY UNIQUE,', ' PRIMARY KEY (id)', ')' ]) @@ -118,9 +93,9 @@ def get_create_user_agent_table_statement() -> str: """Returns an SQL statement to create the table for user agents. """ return ''.join([ - f'CREATE TABLE IF NOT EXISTS {USER_AGENT_TABLE_NAME} (' + f'CREATE TABLE IF NOT EXISTS {tables.USER_AGENT_TABLE_NAME} (' ' id BIGINT IDENTITY(1, 1),', - ' user_agent VARCHAR NOT NULL,', + ' user_agent VARCHAR(2048) NOT NULL SORTKEY UNIQUE,', ' PRIMARY KEY (id)', ')', ]) @@ -130,9 +105,9 @@ def get_create_result_type_table_statement() -> str: """Returns an SQL statement to create the table for result types. """ return ''.join([ - f'CREATE TABLE IF NOT EXISTS {RESULT_TYPE_TABLE_NAME} (' + f'CREATE TABLE IF NOT EXISTS {tables.RESULT_TYPE_TABLE_NAME} (' ' id INT IDENTITY(1, 1),', - ' result_type VARCHAR NOT NULL,', + ' result_type VARCHAR NOT NULL SORTKEY UNIQUE,', ' PRIMARY KEY (id)', ')', ]) @@ -142,7 +117,7 @@ def get_create_access_log_table_statement() -> str: """Returns an SQL statement to create the table for access logs. """ return ''.join([ - f'CREATE TABLE IF NOT EXISTS {ACCESS_LOG_TABLE_NAME} (', + f'CREATE TABLE IF NOT EXISTS {tables.ACCESS_LOG_TABLE_NAME} (', ' datetime TIMESTAMP SORTKEY NOT NULL,', ' edge_location INT NOT NULL,', ' sc_bytes BIGINT NOT NULL,', @@ -156,35 +131,19 @@ def get_create_access_log_table_statement() -> str: ' time_taken FLOAT4 NOT NULL,', ' edge_response_result_type INT NOT NULL,', ' time_to_first_byte FLOAT4 NOT NULL,', - f' FOREIGN KEY (edge_location) REFERENCES {EDGE_LOCATION_TABLE_NAME},' - f' FOREIGN KEY (page) REFERENCES {PAGE_TABLE_NAME},' - f' FOREIGN KEY (referer) REFERENCES {REFERER_TABLE_NAME},' - f' FOREIGN KEY (user_agent) REFERENCES {USER_AGENT_TABLE_NAME},' - f' FOREIGN KEY (edge_response_result_type) REFERENCES {RESULT_TYPE_TABLE_NAME}' + f' FOREIGN KEY (edge_location) REFERENCES {tables.EDGE_LOCATION_TABLE_NAME},' + f' FOREIGN KEY (page) REFERENCES {tables.PAGE_TABLE_NAME},' + f' FOREIGN KEY (referer) REFERENCES {tables.REFERER_TABLE_NAME},' + f' FOREIGN KEY (user_agent) REFERENCES {tables.USER_AGENT_TABLE_NAME},' + f' FOREIGN KEY (edge_response_result_type) REFERENCES {tables.RESULT_TYPE_TABLE_NAME}' ')', ]) -def wait_for_statement(statement_id: str) -> Tuple[Optional[str], Dict]: - """Waits for a given statement to finish. - - :returns: final status of the statement. - ``None`` if polling has timed out. +def get_grant_public_table_access_statement(table_name: str) -> str: + """Returns an SQL statement to grant access on a given table to public. """ - counter = 0 - while counter < MAX_POLLING_COUNTER: - res = redshift_data.describe_statement(Id=statement_id) - if counter % 20 == 0: - LOGGER.debug('polling statement status [%d]: %s', counter, str(res)) - if res['Status'] not in RUNNING_STATUSES: - LOGGER.debug( - 'statement done in: %.3f ms', - res.get('Duration', 0) * 0.001 * 0.001, # ns → ms - ) - return res['Status'], res - time.sleep(POLLING_INTERVAL_IN_S) - counter += 1 - return None, res + return f'GRANT SELECT,INSERT,UPDATE,DELETE ON {table_name} TO PUBLIC' def lambda_handler(event, _): @@ -201,11 +160,13 @@ def lambda_handler(event, _): Database=ADMIN_DATABASE_NAME, Sql=get_create_database_statement(), ) - status, res = wait_for_statement(res['Id']) + status, res = data_api.wait_for_results(redshift_data, res['Id']) if status != 'FINISHED': if status == 'FAILED': - # ignores the error if the database already exists - if not res.get('Error', '').lower().endswith('already exists'): + # just warns if the database already exists + if res.get('Error', '').lower().endswith('already exists'): + LOGGER.warning('database already exists') + else: raise DataWarehouseException( f'failed to create the database: {res.get("Error")}', ) @@ -213,6 +174,10 @@ def lambda_handler(event, _): raise DataWarehouseException( f'failed to create the database: {status or "timeout"}', ) + LOGGER.debug( + 'populated database in %.3f ms', + res.get('Duration', 0) * 0.001 * 0.001, # ns → ms + ) # populates the tables res = redshift_data.batch_execute_statement( WorkgroupName=WORKGROUP_NAME, @@ -220,7 +185,7 @@ def lambda_handler(event, _): Database=ACCESS_LOGS_DATABASE_NAME, Sqls=get_create_tables_script(), ) - status, res = wait_for_statement(res['Id']) + status, res = data_api.wait_for_results(redshift_data, res['Id']) if status != 'FINISHED': if status == 'FAILED': raise DataWarehouseException( @@ -229,6 +194,10 @@ def lambda_handler(event, _): raise DataWarehouseException( f'failed to populate tables: {status or "timeout"}', ) + LOGGER.debug( + 'populated tables in %.3f ms', + res.get('Duration') * 0.001 * 0.001, # ns → ms + ) return { 'statusCode': 200, } diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index ce2ee7b..c8ee5e4 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -9,6 +9,7 @@ import { import { ContentsPipeline } from './contents-pipeline'; import { DataWarehouse } from './data-warehouse'; import { LatestBoto3Layer } from './latest-boto3-layer'; +import { LibdatawarehouseLayer } from './libdatawarehouse-layer'; type Props = StackProps & Readonly<{ // names of the main codemonger resources. @@ -25,11 +26,14 @@ export class CdkOpsStack extends Stack { props.codemongerResourceNames, ); const latestBoto3 = new LatestBoto3Layer(this, 'LatestBoto3'); + const libdatawarehouse = + new LibdatawarehouseLayer(this, 'Libdatawarehouse'); const pipeline = new ContentsPipeline(this, 'ContentsPipeline', { codemongerResources, }); const dataWarehouse = new DataWarehouse(this, 'DevelopmentDataWarehouse', { latestBoto3, + libdatawarehouse, deploymentStage: 'development', }); const developmentContentsAccessLogsETL = new AccessLogsETL( diff --git a/cdk-ops/lib/data-warehouse.ts b/cdk-ops/lib/data-warehouse.ts index ab00882..5b4cc95 100644 --- a/cdk-ops/lib/data-warehouse.ts +++ b/cdk-ops/lib/data-warehouse.ts @@ -14,6 +14,7 @@ import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; import type { DeploymentStage } from 'cdk-common'; import { LatestBoto3Layer } from './latest-boto3-layer'; +import { LibdatawarehouseLayer } from './libdatawarehouse-layer'; /** Name of the admin user. */ export const ADMIN_USER_NAME = 'dwadmin'; @@ -24,6 +25,8 @@ export const CLUSTER_SUBNET_GROUP_NAME = 'dw-cluster'; export interface Props { /** Lambda layer containing the latest boto3. */ latestBoto3: LatestBoto3Layer; + /** Lambda layer containing libdatawarehouse. */ + libdatawarehouse: LibdatawarehouseLayer; /** Deployment stage. */ deploymentStage: DeploymentStage; } @@ -40,7 +43,7 @@ export class DataWarehouse extends Construct { constructor(scope: Construct, id: string, props: Props) { super(scope, id); - const { deploymentStage, latestBoto3 } = props; + const { deploymentStage, latestBoto3, libdatawarehouse } = props; this.vpc = new ec2.Vpc(this, `DwVpc`, { cidr: '192.168.0.0/16', @@ -135,18 +138,11 @@ export class DataWarehouse extends Construct { entry: path.join('lambda', 'populate-dw-database'), index: 'index.py', handler: 'lambda_handler', - layers: [latestBoto3.layer], + layers: [latestBoto3.layer, libdatawarehouse.layer], environment: { WORKGROUP_NAME: workgroup.workgroupName, ADMIN_SECRET_ARN: this.adminSecret.secretArn, ADMIN_DATABASE_NAME: 'dev', - ACCESS_LOGS_DATABASE_NAME: 'access_logs', - PAGE_TABLE_NAME: 'page', - REFERER_TABLE_NAME: 'referer', - EDGE_LOCATION_TABLE_NAME: 'edge_location', - USER_AGENT_TABLE_NAME: 'user_agent', - RESULT_TYPE_TABLE_NAME: 'result_type', - ACCESS_LOG_TABLE_NAME: 'access_log', }, timeout: Duration.minutes(15), // a Lambda function does not have to join the VPC From a9796ab2647b815118a081041349893eff62803b Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 8 Oct 2022 12:52:51 +0900 Subject: [PATCH 19/41] feat(cdk-ops): load access logs - Introduces a new Lambda function `lambda/load-access-logs` that loads CloudFront access logs from the S3 bucket onto the data warehouse. `AccessLogsETL` provisions this function. - `DataWarehouse` introduces a new method `grantQuery` that allows a given `IGrantable` to call the Redshift Data API. The permission is too permissive because I could not figure out how to obtain the ARN of the Redshift Serverless namespace. `DataWarehouse` exports the Redshift Serverless workgroup so that `AccessLogsETL` can configure `lambda/load-access-logs`. - `CdkOpsStack` passes `DataWarehouse`, `LatestBoto3Layer`, and `LibdatawarehouseLayer` to `AccessLogsETL`. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/load-access-logs/index.py | 516 +++++++++++++++++++++++ cdk-ops/lib/access-logs-etl.ts | 62 ++- cdk-ops/lib/cdk-ops-stack.ts | 3 + cdk-ops/lib/data-warehouse.ts | 50 ++- 4 files changed, 617 insertions(+), 14 deletions(-) create mode 100644 cdk-ops/lambda/load-access-logs/index.py diff --git a/cdk-ops/lambda/load-access-logs/index.py b/cdk-ops/lambda/load-access-logs/index.py new file mode 100644 index 0000000..4192ce3 --- /dev/null +++ b/cdk-ops/lambda/load-access-logs/index.py @@ -0,0 +1,516 @@ +# -*- coding: utf-8 -*- + +"""Loads CloudFront access logs onto the data warehouse. + +You have to specify the following environment variables, +* ``SOURCE_BUCKET_NAME``: name of the S3 bucket containing access logs to be + loaded. +* ``SOURCE_OBJECT_KEY_PREFIX``: prefix of the S3 object keys to be loaded. +* ``REDSHIFT_WORKGROUP_NAME``: name of the Redshift Serverless workgroup. +* ``COPY_ROLE_ARN``: ARN of the IAM role to COPY data from the S3 object. +""" + +import datetime +import logging +import os +import boto3 +from libdatawarehouse import ACCESS_LOGS_DATABASE_NAME, data_api, tables +from libdatawarehouse.exceptions import DataWarehouseException + + +SOURCE_BUCKET_NAME = os.environ['SOURCE_BUCKET_NAME'] +SOURCE_KEY_PREFIX = os.environ['SOURCE_KEY_PREFIX'] +REDSHIFT_WORKGROUP_NAME = os.environ['REDSHIFT_WORKGROUP_NAME'] +COPY_ROLE_ARN = os.environ['COPY_ROLE_ARN'] + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +redshift = boto3.client('redshift-serverless') +redshift_data = boto3.client('redshift-data') + + +def execute_load_script(date: datetime.datetime): + """Executes the script to load CloudFront access logs. + + :param datetime.datetime date: date on which CloudFront access logs are to + be loaded. + """ + batch_res = redshift_data.batch_execute_statement( + WorkgroupName=REDSHIFT_WORKGROUP_NAME, + Database=ACCESS_LOGS_DATABASE_NAME, + Sqls=[ + # drops remaining temporary tables just in case + get_drop_raw_access_log_table_statement(), + get_drop_referer_stage_table_statement(), + get_drop_page_stage_table_statement(), + get_drop_edge_location_stage_table_statement(), + get_drop_user_agent_stage_table_statement(), + get_drop_access_log_stage_2_table_statement(), + get_drop_access_log_stage_table_statement(), + + get_create_raw_access_log_table_statement(), + get_load_access_logs_statement(date), + get_create_access_log_stage_table_statement(), + get_drop_raw_access_log_table_statement(), + get_create_referer_stage_table_statement(), + get_delete_existing_referers_statement(), + get_insert_referers_statement(), + get_drop_referer_stage_table_statement(), + get_create_page_stage_table_statement(), + get_delete_existing_pages_statement(), + get_insert_pages_statement(), + get_drop_page_stage_table_statement(), + get_create_edge_location_stage_table_statement(), + get_delete_existing_edge_locations_statement(), + get_insert_edge_locations_statement(), + get_drop_edge_location_stage_table_statement(), + get_create_user_agent_stage_table_statement(), + get_delete_existing_user_agents_statement(), + get_insert_user_agents_statement(), + get_drop_user_agent_stage_table_statement(), + get_create_result_type_stage_table_statement(), + get_delete_existing_result_types_statement(), + get_insert_result_types_statement(), + get_drop_result_type_stage_table_statement(), + get_encode_foreign_keys_statement(), + get_insert_access_logs_statement(), + get_drop_access_log_stage_2_table_statement(), + get_drop_access_log_stage_table_statement(), + ], + ) + statement_id = batch_res['Id'] + status, res = data_api.wait_for_results(redshift_data, statement_id) + if status != 'FINISHED': + if status is not None: + if status == 'FAILED': + LOGGER.error('failed to load access logs: %s', res.get('Error')) + raise DataWarehouseException( + f'failed to load access logs: {status}', + ) + raise DataWarehouseException('loading access logs timed out') + LOGGER.debug( + 'loaded access logs in %.3f ms', + res.get('Duration', 0) * 0.001 * 0.001, # ns → ms + ) + + +def get_create_raw_access_log_table_statement() -> str: + """Returns an SQL statement that creates a temporary table to load raw + access logs from the S3 bucket. + """ + return ''.join([ + 'CREATE TABLE #raw_access_log (', + ' date DATE,', + ' time TIME,', + ' edge_location VARCHAR,', + ' sc_bytes BIGINT,', + ' c_ip VARCHAR,', + ' cs_method VARCHAR,', + ' cs_host VARCHAR,', + ' cs_uri_stem VARCHAR(2048),', + ' status SMALLINT,', + ' referer VARCHAR(2048),', + ' user_agent VARCHAR(2048),', + ' cs_uri_query VARCHAR,', + ' cs_cookie VARCHAR,', + ' edge_result_type VARCHAR,', + ' edge_request_id VARCHAR,', + ' host_header VARCHAR,', + ' cs_protocol VARCHAR,', + ' cs_bytes BIGINT,', + ' time_taken FLOAT4,', + ' forwarded_for VARCHAR,', + ' ssl_protocol VARCHAR,', + ' ssl_cipher VARCHAR,', + ' edge_response_result_type VARCHAR,', + ' cs_protocol_version VARCHAR,', + ' fle_status VARCHAR,', + ' fle_encrypted_fields VARCHAR,', + ' c_port INT,', + ' time_to_first_byte FLOAT4,', + ' edge_detailed_result_type VARCHAR,', + ' sc_content_type VARCHAR,', + ' sc_content_len BIGINT,', + ' sc_range_start BIGINT,', + ' sc_range_end BIGINT', + ')', + 'SORTKEY (date, time)', + ]) + + +def get_load_access_logs_statement(date: datetime.datetime) -> str: + """Returns an SQL statement that loads access logs from the S3 bucket. + """ + date_part = f'{date.year:04d}/{date.month:02d}/{date.day:02d}/' + return ''.join([ + 'COPY #raw_access_log', + f" FROM 's3://{SOURCE_BUCKET_NAME}/{SOURCE_KEY_PREFIX}{date_part}'", + f" IAM_ROLE '{COPY_ROLE_ARN}'", + ' GZIP', + " DELIMITER '\t'", + ' IGNOREHEADER 1', + " NULL AS '-'", + ]) + + +def get_create_access_log_stage_table_statement() -> str: + """Returns an SQL statement that creates a temporary table to select and + format access log columns. + """ + return ''.join([ + 'CREATE TABLE #access_log_stage (', + ' datetime,', + ' edge_location,', + ' sc_bytes,', + ' cs_method,', + ' cs_uri_stem,', + ' status,', + ' referer,', + ' user_agent,', + ' cs_protocol,', + ' cs_bytes,', + ' time_taken,', + ' edge_response_result_type,', + ' time_to_first_byte', + ')', + ' SORTKEY (datetime)', + ' AS SELECT', + ' ("date" || \' \' || "time")::TIMESTAMP,', + ' edge_location,', + ' sc_bytes,', + ' cs_method,', + ' cs_uri_stem,', + ' status,', + " CASE WHEN referer IS NULL THEN '-' ELSE referer END,", + ' user_agent,', + ' cs_protocol,', + ' cs_bytes,', + ' time_taken,', + ' edge_response_result_type,', + ' time_to_first_byte', + ' FROM #raw_access_log', + ]) + + +def get_drop_raw_access_log_table_statement() -> str: + """Returns an SQL statement that drops the temporary table to load raw + access logs from the S3 bucket. + """ + return get_drop_table_statement('#raw_access_log') + + +def get_create_referer_stage_table_statement() -> str: + """Returns an SQL statement that creates a temporary table to aggregate + referers. + """ + return ''.join([ + 'CREATE TABLE #referer_stage (url)', + ' SORTKEY (url)', + ' AS SELECT referer FROM #access_log_stage', + ]) + + +def get_delete_existing_referers_statement() -> str: + """Returns an SQL statement that deletes existing referers from the + temporary referer table. + """ + return ''.join([ + 'DELETE FROM #referer_stage', + f' USING {tables.REFERER_TABLE_NAME}', + ' WHERE', + f' #referer_stage.url = {tables.REFERER_TABLE_NAME}.url', + ]) + + +def get_insert_referers_statement() -> str: + """Returns an SQL statement that inserts new referers in the temporary + table into the referer table. + """ + return ''.join([ + f'INSERT INTO {tables.REFERER_TABLE_NAME} (url)', + ' SELECT url FROM #referer_stage GROUP BY url', + ]) + + +def get_drop_referer_stage_table_statement() -> str: + """Returns an SQL statement that drops the temporary table to aggregate + referers. + """ + return get_drop_table_statement('#referer_stage') + + +def get_create_page_stage_table_statement() -> str: + """Returns an SQL statement that creates a temporary table to aggregate + pages. + """ + return ''.join([ + 'CREATE TABLE #page_stage (path)', + ' SORTKEY (path)', + ' AS SELECT cs_uri_stem FROM #access_log_stage', + ]) + + +def get_delete_existing_pages_statement() -> str: + """Returns an SQL statement that deletes existing pages from the temporary + page table. + """ + return ''.join([ + 'DELETE FROM #page_stage', + f' USING {tables.PAGE_TABLE_NAME}', + ' WHERE', + f' #page_stage.path = {tables.PAGE_TABLE_NAME}.path', + ]) + + +def get_insert_pages_statement() -> str: + """Returns an SQL statement that inserts new pages in the temporary table + into the stage table. + """ + return ''.join([ + f'INSERT INTO {tables.PAGE_TABLE_NAME} (path)', + ' SELECT path FROM #page_stage GROUP BY path', + ]) + + +def get_drop_page_stage_table_statement() -> str: + """Returns an SQL statement that drops the temporary table to aggregate + pages. + """ + return get_drop_table_statement('#page_stage') + + +def get_create_edge_location_stage_table_statement() -> str: + """Returns an SQL statement that creates a temporary table to aggregate edge + locations. + """ + return ''.join([ + 'CREATE TABLE #edge_location_stage (code)', + ' SORTKEY (code)', + ' AS SELECT edge_location FROM #access_log_stage', + ]) + + +def get_delete_existing_edge_locations_statement() -> str: + """Returns an SQL statement that deletes existing edge locations from the + tempoary edge location table. + """ + return ''.join([ + 'DELETE FROM #edge_location_stage', + f' USING {tables.EDGE_LOCATION_TABLE_NAME}', + ' WHERE', + f' #edge_location_stage.code = {tables.EDGE_LOCATION_TABLE_NAME}.code', + ]) + + +def get_insert_edge_locations_statement() -> str: + """Returns an SQL statement that inserts new edge locations in the temporary + table into the edge location table. + """ + return ''.join([ + f'INSERT INTO {tables.EDGE_LOCATION_TABLE_NAME} (code)', + ' SELECT code FROM #edge_location_stage GROUP BY code', + ]) + + +def get_drop_edge_location_stage_table_statement() -> str: + """Returns an SQL statement that drops the temporary table to aggregate edge + locations. + """ + return get_drop_table_statement('#edge_location_stage') + + +def get_create_user_agent_stage_table_statement() -> str: + """Returns an SQL statement that creates a temporary table to aggregate user + agents. + """ + return ''.join([ + 'CREATE TABLE #user_agent_stage (user_agent)', + ' SORTKEY (user_agent)', + ' AS SELECT user_agent FROM #access_log_stage', + ]) + + +def get_delete_existing_user_agents_statement() -> str: + """Returns an SQL statement that deletes existing user agents from the + temporary user agent table. + """ + return ''.join([ + 'DELETE FROM #user_agent_stage', + f' USING {tables.USER_AGENT_TABLE_NAME}', + ' WHERE', + f' #user_agent_stage.user_agent = {tables.USER_AGENT_TABLE_NAME}.user_agent', + ]) + + +def get_insert_user_agents_statement() -> str: + """Returns an SQL statement that inserts user agents in the temporary table + into the user agent table. + """ + return ''.join([ + f'INSERT INTO {tables.USER_AGENT_TABLE_NAME} (user_agent)', + ' SELECT user_agent FROM #user_agent_stage GROUP BY user_agent', + ]) + + +def get_drop_user_agent_stage_table_statement() -> str: + """Returns an SQL statement that drops the temporary table to aggregate user + agents. + """ + return get_drop_table_statement('#user_agent_stage') + + +def get_create_result_type_stage_table_statement() -> str: + """Returns an SQL statement that creates a temporary table to aggregate + result types. + """ + return ''.join([ + 'CREATE TABLE #result_type_stage (result_type)', + ' SORTKEY (result_type)', + ' AS SELECT edge_response_result_type FROM #access_log_stage', + ]) + + +def get_delete_existing_result_types_statement() -> str: + """Returns an SQL statement that deletes existing result types from the + temporary result type table. + """ + return ''.join([ + 'DELETE FROM #result_type_stage', + f' USING {tables.RESULT_TYPE_TABLE_NAME}', + ' WHERE', + f' #result_type_stage.result_type = {tables.RESULT_TYPE_TABLE_NAME}.result_type', + ]) + + +def get_insert_result_types_statement() -> str: + """Returns an SQL statement that inserts result types in the temporary table + into the result type table. + """ + return ''.join([ + f'INSERT INTO {tables.RESULT_TYPE_TABLE_NAME} (result_type)', + ' SELECT result_type FROM #result_type_stage GROUP BY result_type', + ]) + + +def get_drop_result_type_stage_table_statement() -> str: + """Returns an SQL statement that drops the temporary table to aggregate + result types. + """ + return get_drop_table_statement('#result_type_stage') + + +def get_encode_foreign_keys_statement() -> str: + """Returns an SQL statement that decodes foreign keys in the temporary + access log table and creates a temporary second staging table. + """ + return ''.join([ + 'CREATE TABLE #access_log_stage_2 (', + ' datetime,', + ' edge_location,', + ' sc_bytes,', + ' cs_method,', + ' page,', + ' status,', + ' referer,', + ' user_agent,', + ' cs_protocol,', + ' cs_bytes,', + ' time_taken,', + ' edge_response_result_type,', + ' time_to_first_byte', + ')', + ' DISTKEY (referer)', + ' SORTKEY (datetime)', + ' AS SELECT', + ' #access_log_stage.datetime,', + f' {tables.EDGE_LOCATION_TABLE_NAME}.id,', + ' #access_log_stage.sc_bytes,', + ' #access_log_stage.cs_method,', + f' {tables.PAGE_TABLE_NAME}.id,', + ' #access_log_stage.status,', + f' {tables.REFERER_TABLE_NAME}.id,', + f' {tables.USER_AGENT_TABLE_NAME}.id,', + ' #access_log_stage.cs_protocol,', + ' #access_log_stage.cs_bytes,', + ' #access_log_stage.time_taken,', + f' {tables.RESULT_TYPE_TABLE_NAME}.id,', + ' #access_log_stage.time_to_first_byte', + ' FROM', + ' #access_log_stage,' + f' {tables.EDGE_LOCATION_TABLE_NAME},', + f' {tables.PAGE_TABLE_NAME},', + f' {tables.REFERER_TABLE_NAME},', + f' {tables.USER_AGENT_TABLE_NAME},', + f' {tables.RESULT_TYPE_TABLE_NAME}', + ' WHERE', + f' (#access_log_stage.edge_location = {tables.EDGE_LOCATION_TABLE_NAME}.code)', + f' AND (#access_log_stage.cs_uri_stem = {tables.PAGE_TABLE_NAME}.path)', + f' AND (#access_log_stage.referer = {tables.REFERER_TABLE_NAME}.url)', + f' AND (#access_log_stage.user_agent = {tables.USER_AGENT_TABLE_NAME}.user_agent)', + ' AND (#access_log_stage.edge_response_result_type =', + f' {tables.RESULT_TYPE_TABLE_NAME}.result_type)', + ]) + + +def get_insert_access_logs_statement() -> str: + """Returns an SQL statement that inserts access logs in the temporary second + staging table into the access log table. + """ + return ''.join([ + f'INSERT INTO {tables.ACCESS_LOG_TABLE_NAME}', + ' SELECT * FROM #access_log_stage_2', + ]) + +def get_drop_access_log_stage_2_table_statement() -> str: + """Returns an SQL statement that drops the temporary second staging table + for access logs. + """ + return get_drop_table_statement('#access_log_stage_2') + + +def get_drop_access_log_stage_table_statement() -> str: + """Returns an SQL statement that drops the temporary table to select and + format access log columns. + """ + return get_drop_table_statement('#access_log_stage') + + +def get_drop_table_statement(table_name: str) -> str: + """Returns an SQL statement that drops a given table. + """ + return f'DROP TABLE IF EXISTS {table_name}' + + +def parse_time(time_str: str) -> datetime.datetime: + """Parses a given "time" string. + """ + return datetime.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S%z') + + +def lambda_handler(event, _): + """Loads CloudFront access logs onto the data warehouse. + + This function is indented to be invoked by Amazon EventBridge. + So ``event`` must be an object with ``time`` field. + + .. code-block:: python + + { + 'time': '2020-04-28T07:20:20Z' + } + + Loads CloudFront access logs on the day before the date specified to + ``time``. + """ + LOGGER.debug('loading access logs: %s', str(event)) + invocation_date = parse_time(event['time']) + target_date = invocation_date - datetime.timedelta(days=1) + LOGGER.debug('loading access logs on:%s', str(target_date)) + res = redshift.get_credentials( + workgroupName=REDSHIFT_WORKGROUP_NAME, + dbName=ACCESS_LOGS_DATABASE_NAME, + ) + LOGGER.debug('accessing database as %s', res['dbUser']) + execute_load_script(target_date) + return {} diff --git a/cdk-ops/lib/access-logs-etl.ts b/cdk-ops/lib/access-logs-etl.ts index 21a7b77..58bb537 100644 --- a/cdk-ops/lib/access-logs-etl.ts +++ b/cdk-ops/lib/access-logs-etl.ts @@ -4,6 +4,7 @@ import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; import { Duration, RemovalPolicy, + aws_iam as iam, aws_lambda as lambda, aws_lambda_event_sources as lambda_event, aws_s3 as s3, @@ -14,9 +15,19 @@ import { Construct } from 'constructs'; import type { DeploymentStage } from 'cdk-common'; +import { DataWarehouse } from './data-warehouse'; +import { LatestBoto3Layer } from './latest-boto3-layer'; +import { LibdatawarehouseLayer } from './libdatawarehouse-layer'; + export interface Props { /** S3 bucket that stores CloudFront access logs. */ accessLogsBucket: s3.IBucket; + /** Data warehouse. */ + dataWarehouse: DataWarehouse; + /** Lambda layer containing the latest boto3. */ + latestBoto3: LatestBoto3Layer; + /** Lambda layer of the data warehouse library. */ + libdatawarehouse: LibdatawarehouseLayer; /** Deployment stage. */ deploymentStage: DeploymentStage; } @@ -30,15 +41,20 @@ export interface Props { */ export class AccessLogsETL extends Construct { /** S3 bucket for masked access logs. */ - readonly maskedAccessLogsBucket: s3.IBucket; + readonly outputAccessLogsBucket: s3.IBucket; constructor(scope: Construct, id: string, props: Props) { super(scope, id); - const { accessLogsBucket } = props; + const { + accessLogsBucket, + dataWarehouse, + latestBoto3, + libdatawarehouse, + } = props; - // S3 bucket for masked access logs. - this.maskedAccessLogsBucket = new s3.Bucket( + // S3 bucket for processed access logs. + this.outputAccessLogsBucket = new s3.Bucket( this, 'MaskedAccessLogsBucket', { @@ -55,6 +71,9 @@ export class AccessLogsETL extends Construct { removalPolicy: RemovalPolicy.RETAIN, }, ); + // allows the data warehouse to read the bucket + // so that it can COPY objects from the bucket. + this.outputAccessLogsBucket.grantRead(dataWarehouse.namespaceRole); // masks newly created CloudFront access logs // - Lambda function @@ -71,14 +90,14 @@ export class AccessLogsETL extends Construct { handler: 'lambda_handler', environment: { SOURCE_BUCKET_NAME: accessLogsBucket.bucketName, - DESTINATION_BUCKET_NAME: this.maskedAccessLogsBucket.bucketName, + DESTINATION_BUCKET_NAME: this.outputAccessLogsBucket.bucketName, DESTINATION_KEY_PREFIX: maskedAccessLogsKeyPrefix, }, timeout: maskAccessLogsLambdaTimeout, }, ); accessLogsBucket.grantRead(maskAccessLogsLambda); - this.maskedAccessLogsBucket.grantPut(maskAccessLogsLambda); + this.outputAccessLogsBucket.grantPut(maskAccessLogsLambda); // - SQS queue to capture creation of access logs files, which triggers // the above Lambda function const maxBatchingWindow = Duration.minutes(5); // least frequency @@ -128,7 +147,7 @@ export class AccessLogsETL extends Construct { environment: { SOURCE_BUCKET_NAME: accessLogsBucket.bucketName, // bucket name for masked logs is necessary to verify input events. - DESTINATION_BUCKET_NAME: this.maskedAccessLogsBucket.bucketName, + DESTINATION_BUCKET_NAME: this.outputAccessLogsBucket.bucketName, DESTINATION_KEY_PREFIX: maskedAccessLogsKeyPrefix, }, timeout: deleteAccessLogsLambdaTimeout, @@ -143,7 +162,7 @@ export class AccessLogsETL extends Construct { Duration.seconds(6 * deleteAccessLogsLambdaTimeout.toSeconds()), ), }); - this.maskedAccessLogsBucket.addEventNotification( + this.outputAccessLogsBucket.addEventNotification( s3.EventType.OBJECT_CREATED, new s3n.SqsDestination(maskedLogsQueue), { prefix: maskedAccessLogsKeyPrefix }, @@ -155,5 +174,32 @@ export class AccessLogsETL extends Construct { maxBatchingWindow, }), ); + + // loads processed logs onto the data warehouse once a day. + const loadAccessLogsLambda = new PythonFunction( + this, + 'LoadAccessLogsLambda', + { + description: + 'Loads processed CloudFront access logs onto the data warehouse', + runtime: lambda.Runtime.PYTHON_3_8, + architecture: lambda.Architecture.ARM_64, + entry: path.join('lambda', 'load-access-logs'), + index: 'index.py', + handler: 'lambda_handler', + layers: [latestBoto3.layer, libdatawarehouse.layer], + environment: { + SOURCE_BUCKET_NAME: this.outputAccessLogsBucket.bucketName, + SOURCE_KEY_PREFIX: maskedAccessLogsKeyPrefix, + REDSHIFT_WORKGROUP_NAME: dataWarehouse.workgroupName, + COPY_ROLE_ARN: dataWarehouse.namespaceRole.roleArn, + }, + timeout: Duration.minutes(15), + }, + ); + dataWarehouse.grantQuery(loadAccessLogsLambda); + // loadAccessLogsLambda does not need permissions to read + // outputAccessLogsBucket + // TODO: schedule running loadAccessLogsLambda } } diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index c8ee5e4..4f57522 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -42,6 +42,9 @@ export class CdkOpsStack extends Stack { { accessLogsBucket: codemongerResources.developmentContentsAccessLogsBucket, + dataWarehouse, + latestBoto3, + libdatawarehouse, deploymentStage: 'development', }, ); diff --git a/cdk-ops/lib/data-warehouse.ts b/cdk-ops/lib/data-warehouse.ts index 5b4cc95..5492d61 100644 --- a/cdk-ops/lib/data-warehouse.ts +++ b/cdk-ops/lib/data-warehouse.ts @@ -1,7 +1,9 @@ import * as path from 'path'; import { + Arn, Duration, + Stack, aws_ec2 as ec2, aws_iam as iam, aws_lambda as lambda, @@ -35,10 +37,15 @@ export interface Props { export class DataWarehouse extends Construct { /** VPC for Redshift Serverless clusters. */ readonly vpc: ec2.IVpc; + // TODO: unnecessary exposure of `adminSecret` /** Secret for the admin user. */ readonly adminSecret: secrets.ISecret; - /** IAM role of Redshift Serverless namespace. */ + /** Default IAM role associated with the Redshift Serverless namespace. */ readonly namespaceRole: iam.IRole; + /** Name of the Redshift Serverless workgroup. */ + readonly workgroupName: string; + /** Redshift Serverless workgroup. */ + readonly workgroup: redshift.CfnWorkgroup; constructor(scope: Construct, id: string, props: Props) { super(scope, id); @@ -109,8 +116,9 @@ export class DataWarehouse extends Construct { this.adminSecret.node.defaultChild as secrets.CfnSecret, ); // - workgroup - const workgroup = new redshift.CfnWorkgroup(this, 'DwWorkgroup', { - workgroupName: `datawarehouse-${deploymentStage}`, + this.workgroupName = `datawarehouse-${deploymentStage}`; + this.workgroup = new redshift.CfnWorkgroup(this, 'DwWorkgroup', { + workgroupName: this.workgroupName, namespaceName: dwNamespace.namespaceName, baseCapacity: 32, subnetIds: this.getSubnetIdsForCluster(), @@ -125,7 +133,7 @@ export class DataWarehouse extends Construct { }, ], }); - workgroup.addDependsOn(dwNamespace); + this.workgroup.addDependsOn(dwNamespace); // Lambda function that populates the database and tables. const populateDwDatabaseLambda = new PythonFunction( @@ -140,7 +148,7 @@ export class DataWarehouse extends Construct { handler: 'lambda_handler', layers: [latestBoto3.layer, libdatawarehouse.layer], environment: { - WORKGROUP_NAME: workgroup.workgroupName, + WORKGROUP_NAME: this.workgroupName, ADMIN_SECRET_ARN: this.adminSecret.secretArn, ADMIN_DATABASE_NAME: 'dev', }, @@ -148,7 +156,7 @@ export class DataWarehouse extends Construct { // a Lambda function does not have to join the VPC // as long as it uses Redshift Data API. // - // if want to directly connect to the Redshift cluster from a Lambda, + // if we want to directly connect to the Redshift cluster from a Lambda, // we have to put the Lambda in the VPC and allocate a VPC endpoint. // but I cannot afford VPC endpoints for now. // @@ -168,4 +176,34 @@ export class DataWarehouse extends Construct { subnetGroupName: CLUSTER_SUBNET_GROUP_NAME, }).subnetIds; } + + /** + * Grants permissions to query this data warehouse via the Redshift Data API. + * + * Allows `grantee` to call `redshift-serverless:GetCredentials`. + */ + grantQuery(grantee: iam.IGrantable): iam.Grant { + iam.Grant + .addToPrincipal({ + grantee, + actions: ['redshift-serverless:GetCredentials'], + resourceArns: [ + // TODO: how can we get the ARN of the workgroup? + Arn.format( + { + service: 'redshift-serverless', + resource: 'workgroup', + resourceName: '*', + }, + Stack.of(this.workgroup), + ), + ], + }) + .assertSuccess(); + return iam.Grant.addToPrincipal({ + grantee, + actions: ['redshift-data:*'], + resourceArns: ['*'], + }); + } } From 68efb84528bc85ca2139c55f86e4d7d5a7359953 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sat, 8 Oct 2022 17:11:09 +0900 Subject: [PATCH 20/41] feat(cdk-ops): add sequential numbers to rows - `lambda/populate-dw-database` introduces a new column `seq_num` to the `access_log` table, which helps sorting rows in the same order in the original log file. - `lambda/mask-access-logs` prepends a new column `row_num` that records row numbers in the original access logs file. Row numbers are unique only in a single access logs file. - `lambda/load-access-logs` maps `row_num` to `seq_num` in the `access_log` table. - `lambda/mask-access-logs` also masks the "x-forwarded-for" column as IP addresses. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/load-access-logs/index.py | 13 +++++-- cdk-ops/lambda/mask-access-logs/index.py | 40 ++++++++++++++++---- cdk-ops/lambda/populate-dw-database/index.py | 5 ++- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/cdk-ops/lambda/load-access-logs/index.py b/cdk-ops/lambda/load-access-logs/index.py index 4192ce3..0792f4c 100644 --- a/cdk-ops/lambda/load-access-logs/index.py +++ b/cdk-ops/lambda/load-access-logs/index.py @@ -84,7 +84,7 @@ def execute_load_script(date: datetime.datetime): if status != 'FINISHED': if status is not None: if status == 'FAILED': - LOGGER.error('failed to load access logs: %s', res.get('Error')) + LOGGER.error('failed to load access logs: %s', str(res)) raise DataWarehouseException( f'failed to load access logs: {status}', ) @@ -101,6 +101,7 @@ def get_create_raw_access_log_table_statement() -> str: """ return ''.join([ 'CREATE TABLE #raw_access_log (', + ' seq_num INT,', ' date DATE,', ' time TIME,', ' edge_location VARCHAR,', @@ -135,7 +136,7 @@ def get_create_raw_access_log_table_statement() -> str: ' sc_range_start BIGINT,', ' sc_range_end BIGINT', ')', - 'SORTKEY (date, time)', + 'SORTKEY (date, time, seq_num)', ]) @@ -161,6 +162,7 @@ def get_create_access_log_stage_table_statement() -> str: return ''.join([ 'CREATE TABLE #access_log_stage (', ' datetime,', + ' seq_num,', ' edge_location,', ' sc_bytes,', ' cs_method,', @@ -174,9 +176,10 @@ def get_create_access_log_stage_table_statement() -> str: ' edge_response_result_type,', ' time_to_first_byte', ')', - ' SORTKEY (datetime)', + ' SORTKEY ("datetime", seq_num)', ' AS SELECT', ' ("date" || \' \' || "time")::TIMESTAMP,', + ' seq_num,', ' edge_location,', ' sc_bytes,', ' cs_method,', @@ -407,6 +410,7 @@ def get_encode_foreign_keys_statement() -> str: return ''.join([ 'CREATE TABLE #access_log_stage_2 (', ' datetime,', + ' seq_num,', ' edge_location,', ' sc_bytes,', ' cs_method,', @@ -421,9 +425,10 @@ def get_encode_foreign_keys_statement() -> str: ' time_to_first_byte', ')', ' DISTKEY (referer)', - ' SORTKEY (datetime)', + ' SORTKEY ("datetime", seq_num)', ' AS SELECT', ' #access_log_stage.datetime,', + ' #access_log_stage.seq_num,', f' {tables.EDGE_LOCATION_TABLE_NAME}.id,', ' #access_log_stage.sc_bytes,', ' #access_log_stage.cs_method,', diff --git a/cdk-ops/lambda/mask-access-logs/index.py b/cdk-ops/lambda/mask-access-logs/index.py index dae4ce5..d144621 100644 --- a/cdk-ops/lambda/mask-access-logs/index.py +++ b/cdk-ops/lambda/mask-access-logs/index.py @@ -67,8 +67,11 @@ def mask_row(row: Dict[str, str]) -> Dict[str, str]: """Masks a given row in CloudFront access logs. """ addr = row['c-ip'] - if addr is not None: + if addr != '-': row['c-ip'] = mask_ip_address(addr) + addr = row['x-forwarded-for'] + if addr != '-': + row['x-forwarded-for'] = mask_ip_address(addr) return row @@ -325,6 +328,7 @@ class GzippedTsvOnS3: underlying: S3OutputStream gzipped: TextIO tsv_writer: csv.DictWriter + _next_row_number: int def __init__( @@ -336,6 +340,17 @@ def __init__( self.underlying = underlying self.gzipped = gzipped self.tsv_writer = tsv_writer + self._next_row_number = 1 + + + def next_row_number(self) -> int: + """Returns the next row number. + + Every call of this method increments the row number. + """ + row_number = self._next_row_number + self._next_row_number += 1 + return row_number def close(self): @@ -382,14 +397,18 @@ class LogDispatcher: LOG_DATE_FORMAT = '%Y-%m-%d' + ROW_NUMBER_COLUMN = 'row_num' + dest_map: Dict[time.struct_time, GzippedTsvOnS3] def __init__(self, src_key: str, column_names: Sequence[str]): """Initializes with the column names. + + Prepends a column for row numbers to ``column_names``. """ self.src_key = src_key - self.column_names = column_names + self.column_names = [LogDispatcher.ROW_NUMBER_COLUMN] + column_names self.dest_map = {} @@ -397,6 +416,8 @@ def writerow(self, row: Dict[str, str]): """Writes a given row into a matching S3 object. Ignores an invalid row. + + Prepends a row number column to ``row``. """ try: date = time.strptime(row['date'], LogDispatcher.LOG_DATE_FORMAT) @@ -406,16 +427,20 @@ def writerow(self, row: Dict[str, str]): LOGGER.warning('invalid date format: %s', row['date']) else: dest = self.get_destination(date) - dest.writerow(row) + ext_row = row.copy() + ext_row.update({ + LogDispatcher.ROW_NUMBER_COLUMN: f'{dest.next_row_number():d}', + }) + dest.tsv_writer.writerow(ext_row) - def get_destination(self, date: time.struct_time) -> csv.DictWriter: + def get_destination(self, date: time.struct_time) -> GzippedTsvOnS3: """Obtains the output stream corresponding to a given date. Opens a new ``S3OutputStream`` if none has been opened yet. """ if date in self.dest_map: - return self.dest_map[date].tsv_writer + return self.dest_map[date] year = f'{date.tm_year:04d}' month = f'{date.tm_mon:02d}' mday = f'{date.tm_mday:02d}' @@ -427,9 +452,10 @@ def get_destination(self, date: time.struct_time) -> csv.DictWriter: fieldnames=self.column_names, delimiter='\t', ) - self.dest_map[date] = GzippedTsvOnS3(dest_stream, dest_gzip, dest_tsv) + dest = GzippedTsvOnS3(dest_stream, dest_gzip, dest_tsv) + self.dest_map[date] = dest dest_tsv.writeheader() - return dest_tsv + return dest def close(self): diff --git a/cdk-ops/lambda/populate-dw-database/index.py b/cdk-ops/lambda/populate-dw-database/index.py index 0f5d4c7..9394aaa 100644 --- a/cdk-ops/lambda/populate-dw-database/index.py +++ b/cdk-ops/lambda/populate-dw-database/index.py @@ -118,7 +118,8 @@ def get_create_access_log_table_statement() -> str: """ return ''.join([ f'CREATE TABLE IF NOT EXISTS {tables.ACCESS_LOG_TABLE_NAME} (', - ' datetime TIMESTAMP SORTKEY NOT NULL,', + ' datetime TIMESTAMP NOT NULL,', + ' seq_num INT NOT NULL,', ' edge_location INT NOT NULL,', ' sc_bytes BIGINT NOT NULL,', ' cs_method VARCHAR NOT NULL,', @@ -136,7 +137,7 @@ def get_create_access_log_table_statement() -> str: f' FOREIGN KEY (referer) REFERENCES {tables.REFERER_TABLE_NAME},' f' FOREIGN KEY (user_agent) REFERENCES {tables.USER_AGENT_TABLE_NAME},' f' FOREIGN KEY (edge_response_result_type) REFERENCES {tables.RESULT_TYPE_TABLE_NAME}' - ')', + ') SORTKEY (datetime, seq_num)', ]) From 27d4910950ba5f246757fdc43780b114ab566edd Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sun, 9 Oct 2022 17:06:27 +0900 Subject: [PATCH 21/41] fix(cdk-ops): check existence of access logs - The bug that `lambda/load-access-logs` crashed when there were no access logs on a given date. It now makes sure that there are access logs on a given date before running the script. - `AccessLogsETL` grants `lambda/load-access-logs` read permissions of the S3 bucket of access logs. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/load-access-logs/index.py | 51 +++++++++++++++++++----- cdk-ops/lib/access-logs-etl.ts | 4 +- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/cdk-ops/lambda/load-access-logs/index.py b/cdk-ops/lambda/load-access-logs/index.py index 0792f4c..65f3375 100644 --- a/cdk-ops/lambda/load-access-logs/index.py +++ b/cdk-ops/lambda/load-access-logs/index.py @@ -26,10 +26,24 @@ LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) +s3 = boto3.client('s3') + redshift = boto3.client('redshift-serverless') redshift_data = boto3.client('redshift-data') +def has_access_logs(date: datetime.datetime) -> bool: + """Returns whether there are access logs on a given date. + """ + access_logs_prefix = get_access_logs_prefix(date) + res = s3.list_objects_v2( + Bucket=SOURCE_BUCKET_NAME, + Prefix=access_logs_prefix, + MaxKeys=1, + ) + return len(res.get('Contents', [])) > 0 + + def execute_load_script(date: datetime.datetime): """Executes the script to load CloudFront access logs. @@ -143,10 +157,10 @@ def get_create_raw_access_log_table_statement() -> str: def get_load_access_logs_statement(date: datetime.datetime) -> str: """Returns an SQL statement that loads access logs from the S3 bucket. """ - date_part = f'{date.year:04d}/{date.month:02d}/{date.day:02d}/' + access_logs_prefix = get_access_logs_prefix(date) return ''.join([ 'COPY #raw_access_log', - f" FROM 's3://{SOURCE_BUCKET_NAME}/{SOURCE_KEY_PREFIX}{date_part}'", + f" FROM 's3://{SOURCE_BUCKET_NAME}/{access_logs_prefix}'", f" IAM_ROLE '{COPY_ROLE_ARN}'", ' GZIP', " DELIMITER '\t'", @@ -493,6 +507,22 @@ def parse_time(time_str: str) -> datetime.datetime: return datetime.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S%z') +def get_access_logs_prefix(date: datetime.datetime) -> str: + """Returns the S3 object key prefix of access log files on a given date. + + A returned string contains a trailing slash (/). + """ + return f'{SOURCE_KEY_PREFIX}{format_date_part(date)}' + + +def format_date_part(date: datetime.datetime) -> str: + """Converts a given date into the date part of an S3 object path. + + A returned string contains a trailing slash (/). + """ + return f'{date.year:04d}/{date.month:02d}/{date.day:02d}/' + + def lambda_handler(event, _): """Loads CloudFront access logs onto the data warehouse. @@ -511,11 +541,14 @@ def lambda_handler(event, _): LOGGER.debug('loading access logs: %s', str(event)) invocation_date = parse_time(event['time']) target_date = invocation_date - datetime.timedelta(days=1) - LOGGER.debug('loading access logs on:%s', str(target_date)) - res = redshift.get_credentials( - workgroupName=REDSHIFT_WORKGROUP_NAME, - dbName=ACCESS_LOGS_DATABASE_NAME, - ) - LOGGER.debug('accessing database as %s', res['dbUser']) - execute_load_script(target_date) + if has_access_logs(target_date): + LOGGER.debug('loading access logs on %s', str(target_date)) + res = redshift.get_credentials( + workgroupName=REDSHIFT_WORKGROUP_NAME, + dbName=ACCESS_LOGS_DATABASE_NAME, + ) + LOGGER.debug('accessing database as %s', res['dbUser']) + execute_load_script(target_date) + else: + LOGGER.debug('no access logs on %s', str(target_date)) return {} diff --git a/cdk-ops/lib/access-logs-etl.ts b/cdk-ops/lib/access-logs-etl.ts index 58bb537..2eafcb2 100644 --- a/cdk-ops/lib/access-logs-etl.ts +++ b/cdk-ops/lib/access-logs-etl.ts @@ -195,11 +195,11 @@ export class AccessLogsETL extends Construct { COPY_ROLE_ARN: dataWarehouse.namespaceRole.roleArn, }, timeout: Duration.minutes(15), + memorySize: 256, }, ); + this.outputAccessLogsBucket.grantRead(loadAccessLogsLambda); dataWarehouse.grantQuery(loadAccessLogsLambda); - // loadAccessLogsLambda does not need permissions to read - // outputAccessLogsBucket // TODO: schedule running loadAccessLogsLambda } } From b0d98080f1551075d6764284f78fdb067c2d1bb7 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sun, 9 Oct 2022 19:08:08 +0900 Subject: [PATCH 22/41] chore(cdk-ops): add thoughts about VACUUM - Adds thoughts about VACUUM as comments to `lambda/load-access-logs`. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/load-access-logs/index.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cdk-ops/lambda/load-access-logs/index.py b/cdk-ops/lambda/load-access-logs/index.py index 65f3375..71b65f6 100644 --- a/cdk-ops/lambda/load-access-logs/index.py +++ b/cdk-ops/lambda/load-access-logs/index.py @@ -549,6 +549,11 @@ def lambda_handler(event, _): ) LOGGER.debug('accessing database as %s', res['dbUser']) execute_load_script(target_date) + # we need VACUUM to sort updated tables. + # run VACUUM in a different session (e.g., Step Functions) because, + # - VACUUM needs an owner or superuser privilege + # - VACUUM is time consuming + # - only one VACUUM can run at the same time else: LOGGER.debug('no access logs on %s', str(target_date)) return {} From b669dc049518cb791a5aa3d262188fbedfdcf59b Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Mon, 10 Oct 2022 13:27:32 +0900 Subject: [PATCH 23/41] feat(cdk-ops): schedule loading access logs - `AccessLogsETL` schedules to run `lambda/load-access-logs` at 2:00 AM every day for production. It schedules to load access logs every hour for development. The rules are disabled by default. issue codemonger-io/codemonger#30 --- cdk-ops/lib/access-logs-etl.ts | 35 +++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/cdk-ops/lib/access-logs-etl.ts b/cdk-ops/lib/access-logs-etl.ts index 2eafcb2..d95cc58 100644 --- a/cdk-ops/lib/access-logs-etl.ts +++ b/cdk-ops/lib/access-logs-etl.ts @@ -4,6 +4,8 @@ import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; import { Duration, RemovalPolicy, + aws_events as events, + aws_events_targets as events_targets, aws_iam as iam, aws_lambda as lambda, aws_lambda_event_sources as lambda_event, @@ -49,6 +51,7 @@ export class AccessLogsETL extends Construct { const { accessLogsBucket, dataWarehouse, + deploymentStage, latestBoto3, libdatawarehouse, } = props; @@ -83,7 +86,7 @@ export class AccessLogsETL extends Construct { this, 'MaskAccessLogsLambda', { - description: 'Masks information in a given CloudFront access logs file', + description: `Masks information in a given CloudFront access logs file (${deploymentStage})`, runtime: lambda.Runtime.PYTHON_3_8, entry: path.join('lambda', 'mask-access-logs'), index: 'index.py', @@ -139,7 +142,7 @@ export class AccessLogsETL extends Construct { this, 'DeleteAccessLogsLambda', { - description: 'Deletes the original CloudFront access logs file', + description: `Deletes the original CloudFront access logs file (${deploymentStage})`, runtime: lambda.Runtime.PYTHON_3_8, entry: path.join('lambda', 'delete-access-logs'), index: 'index.py', @@ -176,12 +179,12 @@ export class AccessLogsETL extends Construct { ); // loads processed logs onto the data warehouse once a day. + // - Lambda function const loadAccessLogsLambda = new PythonFunction( this, 'LoadAccessLogsLambda', { - description: - 'Loads processed CloudFront access logs onto the data warehouse', + description: `Loads processed CloudFront access logs onto the data warehouse (${deploymentStage})`, runtime: lambda.Runtime.PYTHON_3_8, architecture: lambda.Architecture.ARM_64, entry: path.join('lambda', 'load-access-logs'), @@ -200,6 +203,28 @@ export class AccessLogsETL extends Construct { ); this.outputAccessLogsBucket.grantRead(loadAccessLogsLambda); dataWarehouse.grantQuery(loadAccessLogsLambda); - // TODO: schedule running loadAccessLogsLambda + // - schedules running loadAccessLogsLambda + const loadSchedule = new events.Rule(this, 'LoadAccessLogsSchedule', { + description: `Periodically loads access logs (${deploymentStage})`, + // do not forget to enable the rule + enabled: false, + schedule: events.Schedule.cron( + deploymentStage === 'development' ? { + // every hour for development + // DO NOT FORGET to disable the rule after testing it + minute: '0', + } : { + // at 2:00 AM every day for production + hour: '2', + minute: '0', + }, + ), + targets: [ + new events_targets.LambdaFunction(loadAccessLogsLambda, { + maxEventAge: Duration.hours(1), + retryAttempts: 2, + }), + ], + }); } } From ae03435eb073a3e41f57507908fbd528d3137da1 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Mon, 10 Oct 2022 13:31:52 +0900 Subject: [PATCH 24/41] fix(cdk-ops): ignore invalid keys - Fixes the bug that `lambda/delete-access-logs` crashed when an empty folder was created in the S3 bucket. It ignores the event if the object key ends with a slash; i.e., the last segment is empty. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/delete-access-logs/index.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cdk-ops/lambda/delete-access-logs/index.py b/cdk-ops/lambda/delete-access-logs/index.py index c45f02e..2c28a40 100644 --- a/cdk-ops/lambda/delete-access-logs/index.py +++ b/cdk-ops/lambda/delete-access-logs/index.py @@ -109,9 +109,12 @@ def process_s3_object(s3_object): # so the last segment separated by a slash ('/') is the key for the # original access logs file. src_key = key.split('/')[-1] - src = source_bucket.Object(src_key) - try: - res = src.delete() - LOGGER.debug('deleted object "%s": %s', src_key, str(res)) - except ClientError as exc: - LOGGER.error('failed to delete object "%s": %s', src_key, str(exc)) + if len(src_key) > 0: + src = source_bucket.Object(src_key) + try: + res = src.delete() + LOGGER.debug('deleted object "%s": %s', src_key, str(res)) + except ClientError as exc: + LOGGER.error('failed to delete object "%s": %s', src_key, str(exc)) + else: + LOGGER.warning('ignoring invalid key: %s', key) From e7a8ea8847eefd0b5ab21a9084f7de5bdb4a13b3 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Mon, 10 Oct 2022 15:48:59 +0900 Subject: [PATCH 25/41] feat(cdk-ops): add vacuum workflow - `DataWarehouse` introduces a Step Functions State Machine (workflow) to run VACUUM over the tables. The workflow picks the table one by one and applies a new Lambda function `lambda/vacuum-table` to it. `lambda/vacuum-table` runs `VACUUM` over a given table. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/vacuum-table/index.py | 64 ++++++++++++++++++++++++++++ cdk-ops/lib/data-warehouse.ts | 61 ++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 cdk-ops/lambda/vacuum-table/index.py diff --git a/cdk-ops/lambda/vacuum-table/index.py b/cdk-ops/lambda/vacuum-table/index.py new file mode 100644 index 0000000..3fe066d --- /dev/null +++ b/cdk-ops/lambda/vacuum-table/index.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +"""Runs VACUUM over a given table. + +You have to specify the following environment variables. +* ``WORKGROUP_NAME``: name of the Redshift Serverless workgroup. +* ``ADMIN_SECRET_ARN``: ARN of the secret containing the admin password. +""" + +import logging +import os +import boto3 +from libdatawarehouse import ACCESS_LOGS_DATABASE_NAME, data_api + + +WORKGROUP_NAME = os.environ['WORKGROUP_NAME'] +ADMIN_SECRET_ARN = os.environ['ADMIN_SECRET_ARN'] + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +redshift_data = boto3.client('redshift-data') + + +def lambda_handler(event, _): + """Runs VACUUM over a given table. + + ``event`` must be a ``dict`` similar to the following, + + .. code-block:: python + + { + 'tableName': '', + 'mode': 'SORT ONLY' + } + """ + LOGGER.debug('running VACUUM: %s', str(event)) + table_name = event['tableName'] + # TODO: verify table_name + mode = event['mode'] + # TODO: verify mode + queue_res = redshift_data.execute_statement( + WorkgroupName=WORKGROUP_NAME, + SecretArn=ADMIN_SECRET_ARN, + Database=ACCESS_LOGS_DATABASE_NAME, + Sql=f'VACUUM {mode} {table_name}', + ) + status, res = data_api.wait_for_results(redshift_data, queue_res['Id']) + if status == 'FAILED': + LOGGER.error('VACUUM over %s failed: %s', table_name, str(res)) + elif status is None: + LOGGER.error('VACUUM over %s timed out', table_name) + status = 'TIMEOUT' + elif status == 'FINISHED': + LOGGER.debug( + 'VACUUM over %s finished in %.3f ms', + table_name, + res.get('Duration', 0) * 0.001 * 0.001, # ns → ms + ) + else: + LOGGER.error('VACUUM over %s failed: %s', table_name, status) + return { + 'status': status, + } diff --git a/cdk-ops/lib/data-warehouse.ts b/cdk-ops/lib/data-warehouse.ts index 5492d61..77ec928 100644 --- a/cdk-ops/lib/data-warehouse.ts +++ b/cdk-ops/lib/data-warehouse.ts @@ -9,6 +9,8 @@ import { aws_lambda as lambda, aws_redshiftserverless as redshift, aws_secretsmanager as secrets, + aws_stepfunctions as sfn, + aws_stepfunctions_tasks as sfn_tasks, } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; @@ -46,6 +48,8 @@ export class DataWarehouse extends Construct { readonly workgroupName: string; /** Redshift Serverless workgroup. */ readonly workgroup: redshift.CfnWorkgroup; + /** Step Functions to run VACUUM over tables. */ + readonly vacuumWorkflow: sfn.IStateMachine; constructor(scope: Construct, id: string, props: Props) { super(scope, id); @@ -168,6 +172,63 @@ export class DataWarehouse extends Construct { this.adminSecret.grantRead(populateDwDatabaseLambda); // TODO: too permissive? populateDwDatabaseLambda.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonRedshiftDataFullAccess')); + + // Step Functions that perform VACUUM over tables. + // - Lambda function that runs VACUUM over a given table + const vacuumTableLambda = new PythonFunction(this, 'VacuumTableLambda', { + description: `Runs VACUUM over a table (${deploymentStage})`, + runtime: lambda.Runtime.PYTHON_3_8, + architecture: lambda.Architecture.ARM_64, + entry: path.join('lambda', 'vacuum-table'), + index: 'index.py', + handler: 'lambda_handler', + layers: [latestBoto3.layer, libdatawarehouse.layer], + environment: { + WORKGROUP_NAME: this.workgroupName, + ADMIN_SECRET_ARN: this.adminSecret.secretArn, + }, + timeout: Duration.minutes(15), + }); + this.adminSecret.grantRead(vacuumTableLambda); + this.grantQuery(vacuumTableLambda); + // - state machine + // - lists table names + const listTableNamesState = new sfn.Pass(this, 'ListTables', { + comment: 'Lists table names', + result: sfn.Result.fromArray([ + 'access_log', + 'referer', + 'page', + 'edge_location', + 'user_agent', + 'result_type', + ]), + resultPath: '$.tables', + // produces something like + // { + // mode: 'SORT ONLY', + // tableNames: ['access_log', ...] + // } + }); + this.vacuumWorkflow = new sfn.StateMachine(this, 'VacuumWorkflow', { + definition: + listTableNamesState.next( + new sfn.Map(this, 'MapTables', { + comment: 'Iterates over tables', + maxConcurrency: 1, // sequential + itemsPath: '$.tables', + parameters: { + 'tableName.$': '$$.Map.Item.Value', + 'mode.$': '$.mode', + }, + }).iterator( + new sfn_tasks.LambdaInvoke(this, 'VacuumTable', { + lambdaFunction: vacuumTableLambda, + }), + ), + ), + timeout: Duration.hours(1), + }); } /** Returns subnet IDs for the cluster of Redshift Serverless. */ From 8a58bc98029a3b7694faed6dc7a68bf01614fc3e Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 11 Oct 2022 00:38:10 +0900 Subject: [PATCH 26/41] feat(cdk-ops): start VACUUM after load - `lambda/load-access-logs` starts the VACUUM workflow over the updated tables. "SORT ONLY" is sufficient because `lambda/load-access-logs` never deletes rows. issue codemonger-io/codemonger#30 --- cdk-ops/lambda/load-access-logs/index.py | 21 +++++++++++++++++++-- cdk-ops/lib/access-logs-etl.ts | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/cdk-ops/lambda/load-access-logs/index.py b/cdk-ops/lambda/load-access-logs/index.py index 71b65f6..56d9eff 100644 --- a/cdk-ops/lambda/load-access-logs/index.py +++ b/cdk-ops/lambda/load-access-logs/index.py @@ -11,6 +11,7 @@ """ import datetime +import json import logging import os import boto3 @@ -22,6 +23,7 @@ SOURCE_KEY_PREFIX = os.environ['SOURCE_KEY_PREFIX'] REDSHIFT_WORKGROUP_NAME = os.environ['REDSHIFT_WORKGROUP_NAME'] COPY_ROLE_ARN = os.environ['COPY_ROLE_ARN'] +VACUUM_WORKFLOW_ARN = os.environ['VACUUM_WORKFLOW_ARN'] LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) @@ -30,6 +32,7 @@ redshift = boto3.client('redshift-serverless') redshift_data = boto3.client('redshift-data') +stepfunctions = boto3.client('stepfunctions') def has_access_logs(date: datetime.datetime) -> bool: @@ -523,6 +526,19 @@ def format_date_part(date: datetime.datetime) -> str: return f'{date.year:04d}/{date.month:02d}/{date.day:02d}/' +def start_vacuum(): + """Starts VACUUM over the updated tables. + """ + res = stepfunctions.start_execution( + stateMachineArn=VACUUM_WORKFLOW_ARN, + input=json.dumps({ + # "SORT ONLY" is sufficient because no deletes have been performed + 'mode': 'SORT ONLY', + }), + ) + LOGGER.debug('started VACUUM: %s', str(res)) + + def lambda_handler(event, _): """Loads CloudFront access logs onto the data warehouse. @@ -549,11 +565,12 @@ def lambda_handler(event, _): ) LOGGER.debug('accessing database as %s', res['dbUser']) execute_load_script(target_date) - # we need VACUUM to sort updated tables. - # run VACUUM in a different session (e.g., Step Functions) because, + # we need VACUUM to sort the updated tables. + # runs VACUUM in a different session (e.g., Step Functions) because, # - VACUUM needs an owner or superuser privilege # - VACUUM is time consuming # - only one VACUUM can run at the same time + start_vacuum() else: LOGGER.debug('no access logs on %s', str(target_date)) return {} diff --git a/cdk-ops/lib/access-logs-etl.ts b/cdk-ops/lib/access-logs-etl.ts index d95cc58..62a1ad8 100644 --- a/cdk-ops/lib/access-logs-etl.ts +++ b/cdk-ops/lib/access-logs-etl.ts @@ -196,6 +196,7 @@ export class AccessLogsETL extends Construct { SOURCE_KEY_PREFIX: maskedAccessLogsKeyPrefix, REDSHIFT_WORKGROUP_NAME: dataWarehouse.workgroupName, COPY_ROLE_ARN: dataWarehouse.namespaceRole.roleArn, + VACUUM_WORKFLOW_ARN: dataWarehouse.vacuumWorkflow.stateMachineArn, }, timeout: Duration.minutes(15), memorySize: 256, @@ -203,6 +204,7 @@ export class AccessLogsETL extends Construct { ); this.outputAccessLogsBucket.grantRead(loadAccessLogsLambda); dataWarehouse.grantQuery(loadAccessLogsLambda); + dataWarehouse.vacuumWorkflow.grantStartExecution(loadAccessLogsLambda); // - schedules running loadAccessLogsLambda const loadSchedule = new events.Rule(this, 'LoadAccessLogsSchedule', { description: `Periodically loads access logs (${deploymentStage})`, From 605f682b4aaabc92a02bb8eb811caff8c227bcec Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 11 Oct 2022 17:17:52 +0900 Subject: [PATCH 27/41] chore(cdk-ops): output ARN of load-access-logs - `CdkOpsStack` outputs the ARN of `lambda/load-access-logs`. `DataWarehouse` exposes `lambda/load-access-logs`. issue codemonger-io/codemonger#30 --- cdk-ops/lib/cdk-ops-stack.ts | 23 ++++++++++++++++------- cdk-ops/lib/data-warehouse.ts | 8 +++++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index 4f57522..fdfb98c 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -1,4 +1,4 @@ -import { Stack, StackProps } from 'aws-cdk-lib'; +import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { AccessLogsETL } from './access-logs-etl'; @@ -31,22 +31,31 @@ export class CdkOpsStack extends Stack { const pipeline = new ContentsPipeline(this, 'ContentsPipeline', { codemongerResources, }); - const dataWarehouse = new DataWarehouse(this, 'DevelopmentDataWarehouse', { - latestBoto3, - libdatawarehouse, - deploymentStage: 'development', - }); + const developmentDataWarehouse = new DataWarehouse( + this, + 'DevelopmentDataWarehouse', + { + latestBoto3, + libdatawarehouse, + deploymentStage: 'development', + }, + ); const developmentContentsAccessLogsETL = new AccessLogsETL( this, 'DevelopmentContentsAccessLogsETL', { accessLogsBucket: codemongerResources.developmentContentsAccessLogsBucket, - dataWarehouse, + dataWarehouse: developmentDataWarehouse, latestBoto3, libdatawarehouse, deploymentStage: 'development', }, ); + // Outputs + new CfnOutput(this, 'PopulateDevelopmentDwDatabaseLambdaArn', { + description: 'ARN of the Lambda function that populates the data warehouse database and tables (development)', + value: developmentDataWarehouse.populateDwDatabaseLambda.functionArn, + }); } } diff --git a/cdk-ops/lib/data-warehouse.ts b/cdk-ops/lib/data-warehouse.ts index 77ec928..8017190 100644 --- a/cdk-ops/lib/data-warehouse.ts +++ b/cdk-ops/lib/data-warehouse.ts @@ -48,6 +48,8 @@ export class DataWarehouse extends Construct { readonly workgroupName: string; /** Redshift Serverless workgroup. */ readonly workgroup: redshift.CfnWorkgroup; + /** Lambda function to populate the database and tables. */ + readonly populateDwDatabaseLambda: lambda.IFunction; /** Step Functions to run VACUUM over tables. */ readonly vacuumWorkflow: sfn.IStateMachine; @@ -140,7 +142,7 @@ export class DataWarehouse extends Construct { this.workgroup.addDependsOn(dwNamespace); // Lambda function that populates the database and tables. - const populateDwDatabaseLambda = new PythonFunction( + this.populateDwDatabaseLambda = new PythonFunction( this, 'PopulateDwDatabaseLambda', { @@ -169,9 +171,9 @@ export class DataWarehouse extends Construct { ); // Redshift Data API uses the execution role of the Lambda function to // retrieve the secret. - this.adminSecret.grantRead(populateDwDatabaseLambda); + this.adminSecret.grantRead(this.populateDwDatabaseLambda); // TODO: too permissive? - populateDwDatabaseLambda.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonRedshiftDataFullAccess')); + this.populateDwDatabaseLambda.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonRedshiftDataFullAccess')); // Step Functions that perform VACUUM over tables. // - Lambda function that runs VACUUM over a given table From c60c5411d7b81946b0873a63d4afbfffd050970b Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 11 Oct 2022 17:44:22 +0900 Subject: [PATCH 28/41] chore(cdk-ops): install @aws-sdk/client-lambda - Installs `@aws-sdk/client-lmabda` to run a Lambda function in a build script. - Bumps the CDK version to 2.45.0. - Bumps the `constructs` version to 10.1.128. - Bumps the `@aws-sdk/client-cloudformation` version to 3.186.0. - Bumps the `ts-node` version to 10.9.1. issue codemonger-io/codemonger#30 --- cdk-ops/package-lock.json | 2854 ++++++++++++++++++++----------------- cdk-ops/package.json | 16 +- 2 files changed, 1541 insertions(+), 1329 deletions(-) diff --git a/cdk-ops/package-lock.json b/cdk-ops/package-lock.json index 14a5426..43deed5 100644 --- a/cdk-ops/package-lock.json +++ b/cdk-ops/package-lock.json @@ -8,13 +8,15 @@ "name": "cdk-ops", "version": "0.1.0", "dependencies": { - "@aws-cdk/aws-lambda-python-alpha": "^2.42.0-alpha.0", - "@aws-sdk/client-cloudformation": "^3.112.0", - "aws-cdk-lib": "^2.42.0", + "@aws-cdk/aws-lambda-python-alpha": "^2.45.0-alpha.0", + "@aws-sdk/client-cloudformation": "^3.186.0", + "@aws-sdk/client-lambda": "^3.186.0", + "aws-cdk-lib": "^2.45.0", "cdk-common": "file:../cdk-common", "cdk2-python-library-layer": "github:kikuomax/cdk-python-library-layer#v0.1.0-v2", - "constructs": "^10.1.106", - "source-map-support": "^0.5.21" + "constructs": "^10.1.128", + "source-map-support": "^0.5.21", + "yargs": "^17.6.0" }, "bin": { "cdk-ops": "bin/cdk-ops.js" @@ -23,10 +25,10 @@ "@types/jest": "^27.5.0", "@types/node": "10.17.27", "@types/prettier": "2.6.0", - "aws-cdk": "^2.42.0", + "aws-cdk": "^2.45.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "ts-node": "^10.7.0", + "ts-node": "^10.9.1", "typescript": "~3.9.7" } }, @@ -55,21 +57,21 @@ } }, "node_modules/@aws-cdk/aws-lambda-python-alpha": { - "version": "2.42.0-alpha.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda-python-alpha/-/aws-lambda-python-alpha-2.42.0-alpha.0.tgz", - "integrity": "sha512-V5fi76QYOWXVhyHRuZ8og/s34goGHuJ9hyEVuG3dsO1WMEx3fovtzkXTdh3BSwZ/Pl4hYJY4blNO+XtVSXcjBA==", + "version": "2.45.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda-python-alpha/-/aws-lambda-python-alpha-2.45.0-alpha.0.tgz", + "integrity": "sha512-seRYApZ/FUSi19PHS/3cXfpeijdrc8JuMlnUl688IE3FOxAYaT+geFx5KldW3+9mG5cRSxCYlXukf3NOW2ZCjw==", "engines": { "node": ">= 14.15.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.42.0", + "aws-cdk-lib": "^2.45.0", "constructs": "^10.0.0" } }, "node_modules/@aws-crypto/ie11-detection": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.0.tgz", - "integrity": "sha512-pkVXf/dq6PITJ0jzYZ69VhL8VFOFoPZLZqtU/12SGnzYuJOOGNfF41q9GxdI1yqC8R13Rq3jOLKDFpUJFT5eTA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", + "integrity": "sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==", "dependencies": { "tslib": "^1.11.1" } @@ -115,9 +117,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@aws-crypto/supports-web-crypto": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.0.tgz", - "integrity": "sha512-Ge7WQ3E0OC7FHYprsZV3h0QIcpdyJLvIeg+uTuHqRYm8D6qCFJoiC+edSzSyFiHtZf+NOQDJ1q46qxjtzIY2nA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz", + "integrity": "sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==", "dependencies": { "tslib": "^1.11.1" } @@ -128,11 +130,11 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@aws-crypto/util": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.1.tgz", - "integrity": "sha512-JJmFFwvbm08lULw4Nm5QOLg8+lAQeC8aCXK5xrtxntYzYXCGfHwUJ4Is3770Q7HmICsXthGQ+ZsDL7C2uH3yBQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.2.tgz", + "integrity": "sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==", "dependencies": { - "@aws-sdk/types": "^3.1.0", + "@aws-sdk/types": "^3.110.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } @@ -143,11 +145,11 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@aws-sdk/abort-controller": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.110.0.tgz", - "integrity": "sha512-zok/WEVuK7Jh6V9YeA56pNZtxUASon9LTkS7vE65A4UFmNkPGNBCNgoiBcbhWfxwrZ8wtXcQk6rtUut39831mA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.186.0.tgz", + "integrity": "sha512-JFvvvtEcbYOvVRRXasi64Dd1VcOz5kJmPvtzsJ+HzMHvPbGGs/aopOJAZQJMJttzJmJwVTay0QL6yag9Kk8nYA==", "dependencies": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -155,44 +157,44 @@ } }, "node_modules/@aws-sdk/client-cloudformation": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.112.0.tgz", - "integrity": "sha512-CQ0HQ5qoXcjP1uoz9AtRYCfar0s/3xq4xGHcCqaeSa6j7nQicHFfT9GsfF5oK9OkE8wZ1EX5OJSUHMm0ONvMQQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.186.0.tgz", + "integrity": "sha512-CAZAX9anSOmrqW9YX34pHIz05Nje2GRd3p/U2NrOs4X4gu9jeg0yf2Ea6O9u6UTSH6hniZKZD3ITbmSFA/5Lgw==", "dependencies": { "@aws-crypto/sha256-browser": "2.0.0", "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/client-sts": "3.112.0", - "@aws-sdk/config-resolver": "3.110.0", - "@aws-sdk/credential-provider-node": "3.112.0", - "@aws-sdk/fetch-http-handler": "3.110.0", - "@aws-sdk/hash-node": "3.110.0", - "@aws-sdk/invalid-dependency": "3.110.0", - "@aws-sdk/middleware-content-length": "3.110.0", - "@aws-sdk/middleware-host-header": "3.110.0", - "@aws-sdk/middleware-logger": "3.110.0", - "@aws-sdk/middleware-recursion-detection": "3.110.0", - "@aws-sdk/middleware-retry": "3.110.0", - "@aws-sdk/middleware-serde": "3.110.0", - "@aws-sdk/middleware-signing": "3.110.0", - "@aws-sdk/middleware-stack": "3.110.0", - "@aws-sdk/middleware-user-agent": "3.110.0", - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/node-http-handler": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/smithy-client": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/url-parser": "3.110.0", - "@aws-sdk/util-base64-browser": "3.109.0", - "@aws-sdk/util-base64-node": "3.55.0", - "@aws-sdk/util-body-length-browser": "3.55.0", - "@aws-sdk/util-body-length-node": "3.55.0", - "@aws-sdk/util-defaults-mode-browser": "3.110.0", - "@aws-sdk/util-defaults-mode-node": "3.110.0", - "@aws-sdk/util-user-agent-browser": "3.110.0", - "@aws-sdk/util-user-agent-node": "3.110.0", - "@aws-sdk/util-utf8-browser": "3.109.0", - "@aws-sdk/util-utf8-node": "3.109.0", - "@aws-sdk/util-waiter": "3.110.0", + "@aws-sdk/client-sts": "3.186.0", + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/credential-provider-node": "3.186.0", + "@aws-sdk/fetch-http-handler": "3.186.0", + "@aws-sdk/hash-node": "3.186.0", + "@aws-sdk/invalid-dependency": "3.186.0", + "@aws-sdk/middleware-content-length": "3.186.0", + "@aws-sdk/middleware-host-header": "3.186.0", + "@aws-sdk/middleware-logger": "3.186.0", + "@aws-sdk/middleware-recursion-detection": "3.186.0", + "@aws-sdk/middleware-retry": "3.186.0", + "@aws-sdk/middleware-serde": "3.186.0", + "@aws-sdk/middleware-signing": "3.186.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/middleware-user-agent": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/node-http-handler": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/smithy-client": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", + "@aws-sdk/util-base64-node": "3.186.0", + "@aws-sdk/util-body-length-browser": "3.186.0", + "@aws-sdk/util-body-length-node": "3.186.0", + "@aws-sdk/util-defaults-mode-browser": "3.186.0", + "@aws-sdk/util-defaults-mode-node": "3.186.0", + "@aws-sdk/util-user-agent-browser": "3.186.0", + "@aws-sdk/util-user-agent-node": "3.186.0", + "@aws-sdk/util-utf8-browser": "3.186.0", + "@aws-sdk/util-utf8-node": "3.186.0", + "@aws-sdk/util-waiter": "3.186.0", "entities": "2.2.0", "fast-xml-parser": "3.19.0", "tslib": "^2.3.1", @@ -202,41 +204,86 @@ "node": ">=12.0.0" } }, + "node_modules/@aws-sdk/client-lambda": { + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.186.0.tgz", + "integrity": "sha512-Wr/s591KMRhRRMnv4eWpJ56Kvt5wMjHFW/XGb4NBEPBkLnd/92jVDXp76tGhmbILa/zNpMBaHTx2eqDvHA6AXQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/client-sts": "3.186.0", + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/credential-provider-node": "3.186.0", + "@aws-sdk/fetch-http-handler": "3.186.0", + "@aws-sdk/hash-node": "3.186.0", + "@aws-sdk/invalid-dependency": "3.186.0", + "@aws-sdk/middleware-content-length": "3.186.0", + "@aws-sdk/middleware-host-header": "3.186.0", + "@aws-sdk/middleware-logger": "3.186.0", + "@aws-sdk/middleware-recursion-detection": "3.186.0", + "@aws-sdk/middleware-retry": "3.186.0", + "@aws-sdk/middleware-serde": "3.186.0", + "@aws-sdk/middleware-signing": "3.186.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/middleware-user-agent": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/node-http-handler": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/smithy-client": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", + "@aws-sdk/util-base64-node": "3.186.0", + "@aws-sdk/util-body-length-browser": "3.186.0", + "@aws-sdk/util-body-length-node": "3.186.0", + "@aws-sdk/util-defaults-mode-browser": "3.186.0", + "@aws-sdk/util-defaults-mode-node": "3.186.0", + "@aws-sdk/util-user-agent-browser": "3.186.0", + "@aws-sdk/util-user-agent-node": "3.186.0", + "@aws-sdk/util-utf8-browser": "3.186.0", + "@aws-sdk/util-utf8-node": "3.186.0", + "@aws-sdk/util-waiter": "3.186.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@aws-sdk/client-sso": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.112.0.tgz", - "integrity": "sha512-FwFmiapxuVQiyMdDaBvCpajnJkVWEUHBdO+7rIpzgKHkODEPou5/AwboaGRPEFYULOyYeI0HiDFzpK0G6de+7Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.186.0.tgz", + "integrity": "sha512-qwLPomqq+fjvp42izzEpBEtGL2+dIlWH5pUCteV55hTEwHgo+m9LJPIrMWkPeoMBzqbNiu5n6+zihnwYlCIlEA==", "dependencies": { "@aws-crypto/sha256-browser": "2.0.0", "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/config-resolver": "3.110.0", - "@aws-sdk/fetch-http-handler": "3.110.0", - "@aws-sdk/hash-node": "3.110.0", - "@aws-sdk/invalid-dependency": "3.110.0", - "@aws-sdk/middleware-content-length": "3.110.0", - "@aws-sdk/middleware-host-header": "3.110.0", - "@aws-sdk/middleware-logger": "3.110.0", - "@aws-sdk/middleware-recursion-detection": "3.110.0", - "@aws-sdk/middleware-retry": "3.110.0", - "@aws-sdk/middleware-serde": "3.110.0", - "@aws-sdk/middleware-stack": "3.110.0", - "@aws-sdk/middleware-user-agent": "3.110.0", - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/node-http-handler": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/smithy-client": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/url-parser": "3.110.0", - "@aws-sdk/util-base64-browser": "3.109.0", - "@aws-sdk/util-base64-node": "3.55.0", - "@aws-sdk/util-body-length-browser": "3.55.0", - "@aws-sdk/util-body-length-node": "3.55.0", - "@aws-sdk/util-defaults-mode-browser": "3.110.0", - "@aws-sdk/util-defaults-mode-node": "3.110.0", - "@aws-sdk/util-user-agent-browser": "3.110.0", - "@aws-sdk/util-user-agent-node": "3.110.0", - "@aws-sdk/util-utf8-browser": "3.109.0", - "@aws-sdk/util-utf8-node": "3.109.0", + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/fetch-http-handler": "3.186.0", + "@aws-sdk/hash-node": "3.186.0", + "@aws-sdk/invalid-dependency": "3.186.0", + "@aws-sdk/middleware-content-length": "3.186.0", + "@aws-sdk/middleware-host-header": "3.186.0", + "@aws-sdk/middleware-logger": "3.186.0", + "@aws-sdk/middleware-recursion-detection": "3.186.0", + "@aws-sdk/middleware-retry": "3.186.0", + "@aws-sdk/middleware-serde": "3.186.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/middleware-user-agent": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/node-http-handler": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/smithy-client": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", + "@aws-sdk/util-base64-node": "3.186.0", + "@aws-sdk/util-body-length-browser": "3.186.0", + "@aws-sdk/util-body-length-node": "3.186.0", + "@aws-sdk/util-defaults-mode-browser": "3.186.0", + "@aws-sdk/util-defaults-mode-node": "3.186.0", + "@aws-sdk/util-user-agent-browser": "3.186.0", + "@aws-sdk/util-user-agent-node": "3.186.0", + "@aws-sdk/util-utf8-browser": "3.186.0", + "@aws-sdk/util-utf8-node": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -244,43 +291,43 @@ } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.112.0.tgz", - "integrity": "sha512-hSApRO2wg3jk9VRGM6SCZO3aFP7DKVSUqs6FrvlXlj+JU88ZKObjrGE61cCzXoD89Dh+b9t8A2T6W51Nzriaxw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.186.0.tgz", + "integrity": "sha512-lyAPI6YmIWWYZHQ9fBZ7QgXjGMTtktL5fk8kOcZ98ja+8Vu0STH1/u837uxqvZta8/k0wijunIL3jWUhjsNRcg==", "dependencies": { "@aws-crypto/sha256-browser": "2.0.0", "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/config-resolver": "3.110.0", - "@aws-sdk/credential-provider-node": "3.112.0", - "@aws-sdk/fetch-http-handler": "3.110.0", - "@aws-sdk/hash-node": "3.110.0", - "@aws-sdk/invalid-dependency": "3.110.0", - "@aws-sdk/middleware-content-length": "3.110.0", - "@aws-sdk/middleware-host-header": "3.110.0", - "@aws-sdk/middleware-logger": "3.110.0", - "@aws-sdk/middleware-recursion-detection": "3.110.0", - "@aws-sdk/middleware-retry": "3.110.0", - "@aws-sdk/middleware-sdk-sts": "3.110.0", - "@aws-sdk/middleware-serde": "3.110.0", - "@aws-sdk/middleware-signing": "3.110.0", - "@aws-sdk/middleware-stack": "3.110.0", - "@aws-sdk/middleware-user-agent": "3.110.0", - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/node-http-handler": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/smithy-client": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/url-parser": "3.110.0", - "@aws-sdk/util-base64-browser": "3.109.0", - "@aws-sdk/util-base64-node": "3.55.0", - "@aws-sdk/util-body-length-browser": "3.55.0", - "@aws-sdk/util-body-length-node": "3.55.0", - "@aws-sdk/util-defaults-mode-browser": "3.110.0", - "@aws-sdk/util-defaults-mode-node": "3.110.0", - "@aws-sdk/util-user-agent-browser": "3.110.0", - "@aws-sdk/util-user-agent-node": "3.110.0", - "@aws-sdk/util-utf8-browser": "3.109.0", - "@aws-sdk/util-utf8-node": "3.109.0", + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/credential-provider-node": "3.186.0", + "@aws-sdk/fetch-http-handler": "3.186.0", + "@aws-sdk/hash-node": "3.186.0", + "@aws-sdk/invalid-dependency": "3.186.0", + "@aws-sdk/middleware-content-length": "3.186.0", + "@aws-sdk/middleware-host-header": "3.186.0", + "@aws-sdk/middleware-logger": "3.186.0", + "@aws-sdk/middleware-recursion-detection": "3.186.0", + "@aws-sdk/middleware-retry": "3.186.0", + "@aws-sdk/middleware-sdk-sts": "3.186.0", + "@aws-sdk/middleware-serde": "3.186.0", + "@aws-sdk/middleware-signing": "3.186.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/middleware-user-agent": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/node-http-handler": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/smithy-client": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", + "@aws-sdk/util-base64-node": "3.186.0", + "@aws-sdk/util-body-length-browser": "3.186.0", + "@aws-sdk/util-body-length-node": "3.186.0", + "@aws-sdk/util-defaults-mode-browser": "3.186.0", + "@aws-sdk/util-defaults-mode-node": "3.186.0", + "@aws-sdk/util-user-agent-browser": "3.186.0", + "@aws-sdk/util-user-agent-node": "3.186.0", + "@aws-sdk/util-utf8-browser": "3.186.0", + "@aws-sdk/util-utf8-node": "3.186.0", "entities": "2.2.0", "fast-xml-parser": "3.19.0", "tslib": "^2.3.1" @@ -290,14 +337,14 @@ } }, "node_modules/@aws-sdk/config-resolver": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.110.0.tgz", - "integrity": "sha512-7VvtKy4CL63BAktQ2vgsjhWDSXpkXO5YdiI56LQnHztrvSuJBBaxJ7R1p/k0b2tEUhYKUziAIW8EKE/7EGPR4g==", - "dependencies": { - "@aws-sdk/signature-v4": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-config-provider": "3.109.0", - "@aws-sdk/util-middleware": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.186.0.tgz", + "integrity": "sha512-l8DR7Q4grEn1fgo2/KvtIfIHJS33HGKPQnht8OPxkl0dMzOJ0jxjOw/tMbrIcPnr2T3Fi7LLcj3dY1Fo1poruQ==", + "dependencies": { + "@aws-sdk/signature-v4": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-config-provider": "3.186.0", + "@aws-sdk/util-middleware": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -305,12 +352,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.110.0.tgz", - "integrity": "sha512-oFU3IYk/Bl5tdsz1qigtm3I25a9cvXPqlE8VjYjxVDdLujF5zd/4HLbhP4GQWhpEwZmM1ijcSNfLcyywVevTZg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.186.0.tgz", + "integrity": "sha512-N9LPAqi1lsQWgxzmU4NPvLPnCN5+IQ3Ai1IFf3wM6FFPNoSUd1kIA2c6xaf0BE7j5Kelm0raZOb4LnV3TBAv+g==", "dependencies": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -318,14 +365,14 @@ } }, "node_modules/@aws-sdk/credential-provider-imds": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.110.0.tgz", - "integrity": "sha512-atl+7/dAB+8fG9XI2fYyCgXKYDbOzot65VAwis+14bOEUCVp7PCJifBEZ/L8GEq564p+Fa2p1IpV0wuQXxqFUQ==", - "dependencies": { - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/url-parser": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.186.0.tgz", + "integrity": "sha512-iJeC7KrEgPPAuXjCZ3ExYZrRQvzpSdTZopYgUm5TnNZ8S1NU/4nvv5xVy61JvMj3JQAeG8UDYYgC421Foc8wQw==", + "dependencies": { + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -333,17 +380,17 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.112.0.tgz", - "integrity": "sha512-ebgZ6/jZdTGHQ3zfq/ccmS+7YmLk6yUWHDmh69VK+B1Dd+S1jFwbD9EQ+pYWCp/gEl9F620NSwb6KghRylPWEQ==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.110.0", - "@aws-sdk/credential-provider-imds": "3.110.0", - "@aws-sdk/credential-provider-sso": "3.112.0", - "@aws-sdk/credential-provider-web-identity": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.186.0.tgz", + "integrity": "sha512-ecrFh3MoZhAj5P2k/HXo/hMJQ3sfmvlommzXuZ/D1Bj2yMcyWuBhF1A83Fwd2gtYrWRrllsK3IOMM5Jr8UIVZA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.186.0", + "@aws-sdk/credential-provider-imds": "3.186.0", + "@aws-sdk/credential-provider-sso": "3.186.0", + "@aws-sdk/credential-provider-web-identity": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -351,19 +398,19 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.112.0.tgz", - "integrity": "sha512-7txS7P3BAaU4cksFw/PnoVskVvO8h/TPvOl/BxFtCiUdwA6FRltLvBeMlN08fwUoqgM6z06q8areBdeDqCHOSw==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.110.0", - "@aws-sdk/credential-provider-imds": "3.110.0", - "@aws-sdk/credential-provider-ini": "3.112.0", - "@aws-sdk/credential-provider-process": "3.110.0", - "@aws-sdk/credential-provider-sso": "3.112.0", - "@aws-sdk/credential-provider-web-identity": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.186.0.tgz", + "integrity": "sha512-HIt2XhSRhEvVgRxTveLCzIkd/SzEBQfkQ6xMJhkBtfJw1o3+jeCk+VysXM0idqmXytctL0O3g9cvvTHOsUgxOA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.186.0", + "@aws-sdk/credential-provider-imds": "3.186.0", + "@aws-sdk/credential-provider-ini": "3.186.0", + "@aws-sdk/credential-provider-process": "3.186.0", + "@aws-sdk/credential-provider-sso": "3.186.0", + "@aws-sdk/credential-provider-web-identity": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -371,13 +418,13 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.110.0.tgz", - "integrity": "sha512-JJcZePvRTfQHYj/+EEY13yItnZH/e8exlARFUjN0L13UrgHpOJtDQBa+YBHXo6MbTFQh+re25z2kzc+zOYSMNQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.186.0.tgz", + "integrity": "sha512-ATRU6gbXvWC1TLnjOEZugC/PBXHBoZgBADid4fDcEQY1vF5e5Ux1kmqkJxyHtV5Wl8sE2uJfwWn+FlpUHRX67g==", "dependencies": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -385,14 +432,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.112.0.tgz", - "integrity": "sha512-b6rOrSXbNK3fGyPvNpyF5zdktmAoNOqHCTmFSUcxRxOipyRGb5JACsbjWthIQkpWkpNCT8GFNLEg9spXPFIdLA==", - "dependencies": { - "@aws-sdk/client-sso": "3.112.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.186.0.tgz", + "integrity": "sha512-mJ+IZljgXPx99HCmuLgBVDPLepHrwqnEEC/0wigrLCx6uz3SrAWmGZsNbxSEtb2CFSAaczlTHcU/kIl7XZIyeQ==", + "dependencies": { + "@aws-sdk/client-sso": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -400,12 +447,12 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.110.0.tgz", - "integrity": "sha512-e4e5u7v3fsUFZsMcFMhMy1NdJBQpunYcLwpYlszm3OEICwTTekQ+hVvnVRd134doHvzepE4yp9sAop0Cj+IRVQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.186.0.tgz", + "integrity": "sha512-KqzI5eBV72FE+8SuOQAu+r53RXGVHg4AuDJmdXyo7Gc4wS/B9FNElA8jVUjjYgVnf0FSiri+l41VzQ44dCopSA==", "dependencies": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -413,24 +460,24 @@ } }, "node_modules/@aws-sdk/fetch-http-handler": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.110.0.tgz", - "integrity": "sha512-vk+K4GeCZL2J2rtvKO+T0Q7i3MDpEGZBMg5K2tj9sMcEQwty0BF0aFnP7Eu2l4/Zif2z1mWuUFM2WcZI6DVnbw==", - "dependencies": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/querystring-builder": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-base64-browser": "3.109.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.186.0.tgz", + "integrity": "sha512-k2v4AAHRD76WnLg7arH94EvIclClo/YfuqO7NoQ6/KwOxjRhs4G6TgIsAZ9E0xmqoJoV81Xqy8H8ldfy9F8LEw==", + "dependencies": { + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/querystring-builder": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", "tslib": "^2.3.1" } }, "node_modules/@aws-sdk/hash-node": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.110.0.tgz", - "integrity": "sha512-wakl+kP2O8wTGYiQ3InZy+CVfGrIpFfq9fo4zif9PZac0BbUbguUU1dkY34uZiaf+4o2/9MoDYrHU2HYeXKxWw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.186.0.tgz", + "integrity": "sha512-G3zuK8/3KExDTxqrGqko+opOMLRF0BwcwekV/wm3GKIM/NnLhHblBs2zd/yi7VsEoWmuzibfp6uzxgFpEoJ87w==", "dependencies": { - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-buffer-from": "3.55.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-buffer-from": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -438,18 +485,18 @@ } }, "node_modules/@aws-sdk/invalid-dependency": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.110.0.tgz", - "integrity": "sha512-O8J1InmtJkoiUMbQDtxBfOzgigBp9iSVsNXQrhs2qHh3826cJOfE7NGT3u+NMw73Pk5j2cfmOh1+7k/76IqxOg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.186.0.tgz", + "integrity": "sha512-hjeZKqORhG2DPWYZ776lQ9YO3gjw166vZHZCZU/43kEYaCZHsF4mexHwHzreAY6RfS25cH60Um7dUh1aeVIpkw==", "dependencies": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "node_modules/@aws-sdk/is-array-buffer": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.55.0.tgz", - "integrity": "sha512-NbiPHVYuPxdqdFd6FxzzN3H1BQn/iWA3ri3Ry7AyLeP/tGs1yzEWMwf8BN8TSMALI0GXT6Sh0GDWy3Ok5xB6DA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.186.0.tgz", + "integrity": "sha512-fObm+P6mjWYzxoFY4y2STHBmSdgKbIAXez0xope563mox62I8I4hhVPUCaDVydXvDpJv8tbedJMk0meJl22+xA==", "dependencies": { "tslib": "^2.3.1" }, @@ -458,12 +505,12 @@ } }, "node_modules/@aws-sdk/middleware-content-length": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.110.0.tgz", - "integrity": "sha512-hKU+zdqfAJQg22LXMVu/z35nNIHrVAKpVKPe9+WYVdL/Z7JKUPK7QymqKGOyDuDbzW6OxyulC1zKGEX12zGmdA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.186.0.tgz", + "integrity": "sha512-Ol3c1ks3IK1s+Okc/rHIX7w2WpXofuQdoAEme37gHeml+8FtUlWH/881h62xfMdf+0YZpRuYv/eM7lBmJBPNJw==", "dependencies": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -471,12 +518,12 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.110.0.tgz", - "integrity": "sha512-/Cknn1vL2LTlclI0MX2RzmtdPlCJ5palCRXxm/mod1oHwg4oNTKRlUX3LUD+L8g7JuJ4h053Ch9KS/A0vanE5Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.186.0.tgz", + "integrity": "sha512-5bTzrRzP2IGwyF3QCyMGtSXpOOud537x32htZf344IvVjrqZF/P8CDfGTkHkeBCIH+wnJxjK+l/QBb3ypAMIqQ==", "dependencies": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -484,11 +531,11 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.110.0.tgz", - "integrity": "sha512-+pz+a+8dfTnzLj79nHrv3aONMp/N36/erMd+7JXeR84QEosVLrFBUwKA8x5x6O3s1iBbQzRKMYEIuja9xn1BPA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.186.0.tgz", + "integrity": "sha512-/1gGBImQT8xYh80pB7QtyzA799TqXtLZYQUohWAsFReYB7fdh5o+mu2rX0FNzZnrLIh2zBUNs4yaWGsnab4uXg==", "dependencies": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -496,12 +543,12 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.110.0.tgz", - "integrity": "sha512-Wav782zd7bcd1e6txRob76CDOdVOaUQ8HXoywiIm/uFrEEUZvhs2mgnXjVUVCMBUehdNgnL99z420aS13JeL/Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.186.0.tgz", + "integrity": "sha512-Za7k26Kovb4LuV5tmC6wcVILDCt0kwztwSlB991xk4vwNTja8kKxSt53WsYG8Q2wSaW6UOIbSoguZVyxbIY07Q==", "dependencies": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -509,14 +556,14 @@ } }, "node_modules/@aws-sdk/middleware-retry": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.110.0.tgz", - "integrity": "sha512-lwLAQQveCiUqymQvVYjCee6QOXw3Zqbc9yq+pxYdXbs1Cv1XMA6PeJeUU5r5KEVuSceBLyyrnl6E0R1l1om1MQ==", - "dependencies": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/service-error-classification": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-middleware": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.186.0.tgz", + "integrity": "sha512-/VI9emEKhhDzlNv9lQMmkyxx3GjJ8yPfXH3HuAeOgM1wx1BjCTLRYEWnTbQwq7BDzVENdneleCsGAp7yaj80Aw==", + "dependencies": { + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/service-error-classification": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-middleware": "3.186.0", "tslib": "^2.3.1", "uuid": "^8.3.2" }, @@ -525,15 +572,15 @@ } }, "node_modules/@aws-sdk/middleware-sdk-sts": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.110.0.tgz", - "integrity": "sha512-EjY/YFdlr5jECde6qIrTIyGBbn/34CKcQGKvmvRd31+3qaClIJLAwNuHfcVzWvCUGbAslsfvdbOpLju33pSQRA==", - "dependencies": { - "@aws-sdk/middleware-signing": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/signature-v4": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.186.0.tgz", + "integrity": "sha512-GDcK0O8rjtnd+XRGnxzheq1V2jk4Sj4HtjrxW/ROyhzLOAOyyxutBt+/zOpDD6Gba3qxc69wE+Cf/qngOkEkDw==", + "dependencies": { + "@aws-sdk/middleware-signing": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/signature-v4": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -541,11 +588,11 @@ } }, "node_modules/@aws-sdk/middleware-serde": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.110.0.tgz", - "integrity": "sha512-brVupxgEAmcZ9cZvdHEH8zncjvGKIiud8pOe4fiimp5NpHmjBLew4jUbnOKNZNAjaidcKUtz//cxtutD6yXEww==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.186.0.tgz", + "integrity": "sha512-6FEAz70RNf18fKL5O7CepPSwTKJEIoyG9zU6p17GzKMgPeFsxS5xO94Hcq5tV2/CqeHliebjqhKY7yi+Pgok7g==", "dependencies": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -553,14 +600,15 @@ } }, "node_modules/@aws-sdk/middleware-signing": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.110.0.tgz", - "integrity": "sha512-y6ZKrGYfgDlFMzWhZmoq5J1UctBgZOUvMmnU9sSeZ020IlEPiOxFMvR0Zu6TcYThp8uy3P0wyjQtGYeTl9Z/kA==", - "dependencies": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/signature-v4": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.186.0.tgz", + "integrity": "sha512-riCJYG/LlF/rkgVbHkr4xJscc0/sECzDivzTaUmfb9kJhAwGxCyNqnTvg0q6UO00kxSdEB9zNZI2/iJYVBijBQ==", + "dependencies": { + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/signature-v4": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-middleware": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -568,9 +616,9 @@ } }, "node_modules/@aws-sdk/middleware-stack": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.110.0.tgz", - "integrity": "sha512-iaLHw6ctOuGa9UxNueU01Xes+15dR+mqioRpUOUZ9Zx+vhXVpD7C8lnNqhRnYeFXs10/rNIzASgsIrAHTlnlIQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.186.0.tgz", + "integrity": "sha512-fENMoo0pW7UBrbuycPf+3WZ+fcUgP9PnQ0jcOK3WWZlZ9d2ewh4HNxLh4EE3NkNYj4VIUFXtTUuVNHlG8trXjQ==", "dependencies": { "tslib": "^2.3.1" }, @@ -579,12 +627,12 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.110.0.tgz", - "integrity": "sha512-Y6FgiZr99DilYq6AjeaaWcNwVlSQpNGKrILzvV4Tmz03OaBIspe4KL+8EZ2YA/sAu5Lpw80vItdezqDOwGAlnQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.186.0.tgz", + "integrity": "sha512-fb+F2PF9DLKOVMgmhkr+ltN8ZhNJavTla9aqmbd01846OLEaN1n5xEnV7p8q5+EznVBWDF38Oz9Ae5BMt3Hs7w==", "dependencies": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -592,13 +640,13 @@ } }, "node_modules/@aws-sdk/node-config-provider": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.110.0.tgz", - "integrity": "sha512-46p4dCPGYctuybTQTwLpjenA1QFHeyJw/OyggGbtUJUy+833+ldnAwcPVML2aXJKUKv3APGI8vq1kaloyNku3Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.186.0.tgz", + "integrity": "sha512-De93mgmtuUUeoiKXU8pVHXWKPBfJQlS/lh1k2H9T2Pd9Tzi0l7p5ttddx4BsEx4gk+Pc5flNz+DeptiSjZpa4A==", "dependencies": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -606,14 +654,14 @@ } }, "node_modules/@aws-sdk/node-http-handler": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.110.0.tgz", - "integrity": "sha512-/rP+hY516DpP8fZhwFW5xM/ElH0w6lxw/15VvZCoY5EnOLAF5XIsJdzscWPSEW2FHCylBM4SNrKhGar14BDXhA==", - "dependencies": { - "@aws-sdk/abort-controller": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/querystring-builder": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.186.0.tgz", + "integrity": "sha512-CbkbDuPZT9UNJ4dAZJWB3BV+Z65wFy7OduqGkzNNrKq6ZYMUfehthhUOTk8vU6RMe/0FkN+J0fFXlBx/bs/cHw==", + "dependencies": { + "@aws-sdk/abort-controller": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/querystring-builder": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -621,11 +669,11 @@ } }, "node_modules/@aws-sdk/property-provider": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.110.0.tgz", - "integrity": "sha512-7NkpmYeOkK3mhWBNU+/zSDqwzeaSPH1qrq4L//WV7WS/weYyE/jusQeZoOxVsuZQnQEXHt5O2hKVeUwShl12xA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.186.0.tgz", + "integrity": "sha512-nWKqt36UW3xV23RlHUmat+yevw9up+T+953nfjcmCBKtgWlCWu/aUzewTRhKj3VRscbN+Wer95SBw9Lr/MMOlQ==", "dependencies": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -633,11 +681,11 @@ } }, "node_modules/@aws-sdk/protocol-http": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.110.0.tgz", - "integrity": "sha512-qdi2gCbJiyPyLn+afebPNp/5nVCRh1X7t7IRIFl3FHVEC+o54u/ojay/MLZ4M/+X9Fa4Zxsb0Wpp3T0xAHVDBg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.186.0.tgz", + "integrity": "sha512-l/KYr/UBDUU5ginqTgHtFfHR3X6ljf/1J1ThIiUg3C3kVC/Zwztm7BEOw8hHRWnWQGU/jYasGYcrcPLdQqFZyQ==", "dependencies": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -645,12 +693,12 @@ } }, "node_modules/@aws-sdk/querystring-builder": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.110.0.tgz", - "integrity": "sha512-7V3CDXj519izmbBn9ZE68ymASwGriA+Aq+cb/yHSVtffnvXjPtvONNw7G/5iVblisGLSCUe2hSvpYtcaXozbHw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.186.0.tgz", + "integrity": "sha512-mweCpuLufImxfq/rRBTEpjGuB4xhQvbokA+otjnUxlPdIobytLqEs7pCGQfLzQ7+1ZMo8LBXt70RH4A2nSX/JQ==", "dependencies": { - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-uri-escape": "3.55.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-uri-escape": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -658,11 +706,11 @@ } }, "node_modules/@aws-sdk/querystring-parser": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.110.0.tgz", - "integrity": "sha512-//pJHH7hrhdDMZGBPKXKymmC/tJM7gFT0w/qbu/yd3Wm4W2fMB+8gkmj6EZctx7jrsWlfRQuvFejKqEfapur/g==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.186.0.tgz", + "integrity": "sha512-0iYfEloghzPVXJjmnzHamNx1F1jIiTW9Svy5ZF9LVqyr/uHZcQuiWYsuhWloBMLs8mfWarkZM02WfxZ8buAuhg==", "dependencies": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -670,18 +718,19 @@ } }, "node_modules/@aws-sdk/service-error-classification": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.110.0.tgz", - "integrity": "sha512-ccgCE0pU/4RmXR6CP3fLAdhPAve7bK/yXBbGzpSHGAQOXqNxYzOsAvQ30Jg6X+qjLHsI/HR2pLIE65z4k6tynw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.186.0.tgz", + "integrity": "sha512-DRl3ORk4tF+jmH5uvftlfaq0IeKKpt0UPAOAFQ/JFWe+TjOcQd/K+VC0iiIG97YFp3aeFmH1JbEgsNxd+8fdxw==", "engines": { "node": ">= 12.0.0" } }, "node_modules/@aws-sdk/shared-ini-file-loader": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.110.0.tgz", - "integrity": "sha512-E1ERoqEoG206XNBYWCKLgHkzCbTxdpDEGbsLET2DnvjFsT0s9p2dPvVux3bYl7JVAhyGduE+qcqWk7MzhFCBNQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.186.0.tgz", + "integrity": "sha512-2FZqxmICtwN9CYid4dwfJSz/gGFHyStFQ3HCOQ8DsJUf2yREMSBsVmKqsyWgOrYcQ98gPcD5GIa7QO5yl3XF6A==", "dependencies": { + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -689,15 +738,15 @@ } }, "node_modules/@aws-sdk/signature-v4": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.110.0.tgz", - "integrity": "sha512-utxxdllOnmQDhbpipnFAbuQ4c2pwefZ+2hi48jKvQRULQ2PO4nxLmdZm6B0FXaTijbKsyO7GrMik+EZ6mi3ARQ==", - "dependencies": { - "@aws-sdk/is-array-buffer": "3.55.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-hex-encoding": "3.109.0", - "@aws-sdk/util-middleware": "3.110.0", - "@aws-sdk/util-uri-escape": "3.55.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.186.0.tgz", + "integrity": "sha512-18i96P5c4suMqwSNhnEOqhq4doqqyjH4fn0YV3F8TkekHPIWP4mtIJ0PWAN4eievqdtcKgD/GqVO6FaJG9texw==", + "dependencies": { + "@aws-sdk/is-array-buffer": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-hex-encoding": "3.186.0", + "@aws-sdk/util-middleware": "3.186.0", + "@aws-sdk/util-uri-escape": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -705,12 +754,12 @@ } }, "node_modules/@aws-sdk/smithy-client": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.110.0.tgz", - "integrity": "sha512-gNLYrmdAe/1hVF2Nv2LF4OkL1A0a1o708pEMZHzql9xP164omRDaLrGDhz9tH7tsJEgLz+Bf4E8nTuISeDwvGg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.186.0.tgz", + "integrity": "sha512-rdAxSFGSnrSprVJ6i1BXi65r4X14cuya6fYe8dSdgmFSa+U2ZevT97lb3tSINCUxBGeMXhENIzbVGkRZuMh+DQ==", "dependencies": { - "@aws-sdk/middleware-stack": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -718,37 +767,37 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.110.0.tgz", - "integrity": "sha512-dLVoqODU3laaqNFPyN1QLtlQnwX4gNPMXptEBIt/iJpuZf66IYJe6WCzVZGt4Zfa1CnUmrlA428AzdcA/KCr2A==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.186.0.tgz", + "integrity": "sha512-NatmSU37U+XauMFJCdFI6nougC20JUFZar+ump5wVv0i54H+2Refg1YbFDxSs0FY28TSB9jfhWIpfFBmXgL5MQ==", "engines": { "node": ">= 12.0.0" } }, "node_modules/@aws-sdk/url-parser": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.110.0.tgz", - "integrity": "sha512-tILFB8/Q73yzgO0dErJNnELmmBszd0E6FucwAnG3hfDefjqCBe09Q/1yhu2aARXyRmZa4AKp0sWcdwIWHc8dnA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.186.0.tgz", + "integrity": "sha512-jfdJkKqJZp8qjjwEjIGDqbqTuajBsddw02f86WiL8bPqD8W13/hdqbG4Fpwc+Bm6GwR6/4MY6xWXFnk8jDUKeA==", "dependencies": { - "@aws-sdk/querystring-parser": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/querystring-parser": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "node_modules/@aws-sdk/util-base64-browser": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.109.0.tgz", - "integrity": "sha512-lAZ6fyDGiRLaIsKT9qh7P9FGuNyZ4gAbr1YOSQk/5mHtaTuUvxlPptZuInNM/0MPQm6lpcot00D8IWTucn4PbA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.186.0.tgz", + "integrity": "sha512-TpQL8opoFfzTwUDxKeon/vuc83kGXpYqjl6hR8WzmHoQgmFfdFlV+0KXZOohra1001OP3FhqvMqaYbO8p9vXVQ==", "dependencies": { "tslib": "^2.3.1" } }, "node_modules/@aws-sdk/util-base64-node": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.55.0.tgz", - "integrity": "sha512-UQ/ZuNoAc8CFMpSiRYmevaTsuRKzLwulZTnM8LNlIt9Wx1tpNvqp80cfvVj7yySKROtEi20wq29h31dZf1eYNQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.186.0.tgz", + "integrity": "sha512-wH5Y/EQNBfGS4VkkmiMyZXU+Ak6VCoFM1GKWopV+sj03zR2D4FHexi4SxWwEBMpZCd6foMtihhbNBuPA5fnh6w==", "dependencies": { - "@aws-sdk/util-buffer-from": "3.55.0", + "@aws-sdk/util-buffer-from": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -756,17 +805,17 @@ } }, "node_modules/@aws-sdk/util-body-length-browser": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.55.0.tgz", - "integrity": "sha512-Ei2OCzXQw5N6ZkTMZbamUzc1z+z1R1Ja5tMEagz5BxuX4vWdBObT+uGlSzL8yvTbjoPjnxWA2aXyEqaUP3JS8Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.186.0.tgz", + "integrity": "sha512-zKtjkI/dkj9oGkjo+7fIz+I9KuHrVt1ROAeL4OmDESS8UZi3/O8uMDFMuCp8jft6H+WFuYH6qRVWAVwXMiasXw==", "dependencies": { "tslib": "^2.3.1" } }, "node_modules/@aws-sdk/util-body-length-node": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.55.0.tgz", - "integrity": "sha512-lU1d4I+9wJwydduXs0SxSfd+mHKjxeyd39VwOv6i2KSwWkPbji9UQqpflKLKw+r45jL7+xU/zfeTUg5Tt/3Gew==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.186.0.tgz", + "integrity": "sha512-U7Ii8u8Wvu9EnBWKKeuwkdrWto3c0j7LG677Spe6vtwWkvY70n9WGfiKHTgBpVeLNv8jvfcx5+H0UOPQK1o9SQ==", "dependencies": { "tslib": "^2.3.1" }, @@ -775,11 +824,11 @@ } }, "node_modules/@aws-sdk/util-buffer-from": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.55.0.tgz", - "integrity": "sha512-uVzKG1UgvnV7XX2FPTylBujYMKBPBaq/qFBxfl0LVNfrty7YjpfieQxAe6yRLD+T0Kir/WDQwGvYC+tOYG3IGA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.186.0.tgz", + "integrity": "sha512-be2GCk2lsLWg/2V5Y+S4/9pOMXhOQo4DR4dIqBdR2R+jrMMHN9Xsr5QrkT6chcqLaJ/SBlwiAEEi3StMRmCOXA==", "dependencies": { - "@aws-sdk/is-array-buffer": "3.55.0", + "@aws-sdk/is-array-buffer": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -787,9 +836,9 @@ } }, "node_modules/@aws-sdk/util-config-provider": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.109.0.tgz", - "integrity": "sha512-GrAZl/aBv0A28LkyNyq8SPJ5fmViCwz80fWLMeWx/6q5AbivuILogjlWwEZSvZ9zrlHOcFC0+AnCa5pQrjaslw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.186.0.tgz", + "integrity": "sha512-71Qwu/PN02XsRLApyxG0EUy/NxWh/CXxtl2C7qY14t+KTiRapwbDkdJ1cMsqYqghYP4BwJoj1M+EFMQSSlkZQQ==", "dependencies": { "tslib": "^2.3.1" }, @@ -798,12 +847,12 @@ } }, "node_modules/@aws-sdk/util-defaults-mode-browser": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.110.0.tgz", - "integrity": "sha512-Y2dcOOD20S3bv/IjUqpdKIiDt6995SXNG5Pu/LeSdXNyLCOIm9rX4gHTxl9fC1KK5M/gR9fGJ362f67WwqEEqw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.186.0.tgz", + "integrity": "sha512-U8GOfIdQ0dZ7RRVpPynGteAHx4URtEh+JfWHHVfS6xLPthPHWTbyRhkQX++K/F8Jk+T5U8Anrrqlea4TlcO2DA==", "dependencies": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "bowser": "^2.11.0", "tslib": "^2.3.1" }, @@ -812,15 +861,15 @@ } }, "node_modules/@aws-sdk/util-defaults-mode-node": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.110.0.tgz", - "integrity": "sha512-Cr3Z5nyrw1KowjbW76xp8hkT/zJtYjAVZ9PS4l84KxIicbVvDOBpxG3yNddkuQcavmlH6G4wH9uM5DcnpKDncg==", - "dependencies": { - "@aws-sdk/config-resolver": "3.110.0", - "@aws-sdk/credential-provider-imds": "3.110.0", - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.186.0.tgz", + "integrity": "sha512-N6O5bpwCiE4z8y7SPHd7KYlszmNOYREa+mMgtOIXRU3VXSEHVKVWTZsHKvNTTHpW0qMqtgIvjvXCo3vsch5l3A==", + "dependencies": { + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/credential-provider-imds": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -828,9 +877,9 @@ } }, "node_modules/@aws-sdk/util-hex-encoding": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.109.0.tgz", - "integrity": "sha512-s8CgTNrn3cLkrdiohfxLuOYPCanzvHn/aH5RW6DaMoeQiG5Hl9QUiP/WtdQ9QQx3xvpQFpmvxIaSBwSgFNLQxA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.186.0.tgz", + "integrity": "sha512-UL9rdgIZz1E/jpAfaKH8QgUxNK9VP5JPgoR0bSiaefMjnsoBh0x/VVMsfUyziOoJCMLebhJzFowtwrSKEGsxNg==", "dependencies": { "tslib": "^2.3.1" }, @@ -839,9 +888,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.55.0.tgz", - "integrity": "sha512-0sPmK2JaJE2BbTcnvybzob/VrFKCXKfN4CUKcvn0yGg/me7Bz+vtzQRB3Xp+YSx+7OtWxzv63wsvHoAnXvgxgg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.186.0.tgz", + "integrity": "sha512-fmQLkH16ga6c5fWsA+kBYklQJjlPlcc8uayTR4avi5g3Nxqm6wPpyUwo5CppwjwWMeS+NXG0HgITtkkGntcRNg==", "dependencies": { "tslib": "^2.3.1" }, @@ -850,9 +899,9 @@ } }, "node_modules/@aws-sdk/util-middleware": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.110.0.tgz", - "integrity": "sha512-PTVWrI5fA9d5hHJs6RzX2dIS2jRQ3uW073Fm0BePpQeDdZrEk+S5KNwRhUtpN6sdSV45vm6S9rrjZUG51qwGmA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.186.0.tgz", + "integrity": "sha512-fddwDgXtnHyL9mEZ4s1tBBsKnVQHqTUmFbZKUUKPrg9CxOh0Y/zZxEa5Olg/8dS/LzM1tvg0ATkcyd4/kEHIhg==", "dependencies": { "tslib": "^2.3.1" }, @@ -861,9 +910,9 @@ } }, "node_modules/@aws-sdk/util-uri-escape": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.55.0.tgz", - "integrity": "sha512-mmdDLUpFCN2nkfwlLdOM54lTD528GiGSPN1qb8XtGLgZsJUmg3uJSFIN2lPeSbEwJB3NFjVas/rnQC48i7mV8w==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.186.0.tgz", + "integrity": "sha512-imtOrJFpIZAipAg8VmRqYwv1G/x4xzyoxOJ48ZSn1/ZGnKEEnB6n6E9gwYRebi4mlRuMSVeZwCPLq0ey5hReeQ==", "dependencies": { "tslib": "^2.3.1" }, @@ -872,42 +921,50 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.110.0.tgz", - "integrity": "sha512-rNdhmHDMV5dNJctqlBWimkZLJRB+x03DB+61pm+SKSFk6gPIVIvc1WNXqDFphkiswT4vA13ZUkGHzt+N4+noQQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.186.0.tgz", + "integrity": "sha512-fbRcTTutMk4YXY3A2LePI4jWSIeHOT8DaYavpc/9Xshz/WH9RTGMmokeVOcClRNBeDSi5cELPJJ7gx6SFD3ZlQ==", "dependencies": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "bowser": "^2.11.0", "tslib": "^2.3.1" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.110.0.tgz", - "integrity": "sha512-OQ915TPCCBwZWz5Np8zkNWn7U6KvrTZfFoCOy/VIemK3dUqmnBZ7HqGpuZx8SwJ2R9JE1x+j0niYSJ5fWJZZKA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.186.0.tgz", + "integrity": "sha512-oWZR7hN6NtOgnT6fUvHaafgbipQc2xJCRB93XHiF9aZGptGNLJzznIOP7uURdn0bTnF73ejbUXWLQIm8/6ue6w==", "dependencies": { - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { "node": ">= 12.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, "node_modules/@aws-sdk/util-utf8-browser": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.109.0.tgz", - "integrity": "sha512-FmcGSz0v7Bqpl1SE8G1Gc0CtDpug+rvqNCG/szn86JApD/f5x8oByjbEiAyTU2ZH2VevUntx6EW68ulHyH+x+w==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.186.0.tgz", + "integrity": "sha512-n+IdFYF/4qT2WxhMOCeig8LndDggaYHw3BJJtfIBZRiS16lgwcGYvOUmhCkn0aSlG1f/eyg9YZHQG0iz9eLdHQ==", "dependencies": { "tslib": "^2.3.1" } }, "node_modules/@aws-sdk/util-utf8-node": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.109.0.tgz", - "integrity": "sha512-Ti/ZBdvz2eSTElsucjzNmzpyg2MwfD1rXmxD0hZuIF8bPON/0+sZYnWd5CbDw9kgmhy28dmKue086tbZ1G0iLQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.186.0.tgz", + "integrity": "sha512-7qlE0dOVdjuRbZTb7HFywnHHCrsN7AeQiTnsWT63mjXGDbPeUWQQw3TrdI20um3cxZXnKoeudGq8K6zbXyQ4iA==", "dependencies": { - "@aws-sdk/util-buffer-from": "3.55.0", + "@aws-sdk/util-buffer-from": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -915,12 +972,12 @@ } }, "node_modules/@aws-sdk/util-waiter": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.110.0.tgz", - "integrity": "sha512-8dE6W6XYfjk1gx/aeb8NeLfMMLkLFhlV1lmKpFSBJhY8msajU8aQahTuykq5JW8QT/wCGbqbu7dH35SdX7kO+A==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.186.0.tgz", + "integrity": "sha512-oSm45VadBBWC/K2W1mrRNzm9RzbXt6VopBQ5iTDU7B3qIXlyAG9k1JqOvmYIdYq1oOgjM3Hv2+9sngi3+MZs1A==", "dependencies": { - "@aws-sdk/abort-controller": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/abort-controller": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" }, "engines": { @@ -928,42 +985,42 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", "dev": true, "dependencies": { - "@babel/highlight": "^7.16.7" + "@babel/highlight": "^7.18.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.5.tgz", - "integrity": "sha512-BxhE40PVCBxVEJsSBhB6UWyAuqJRxGsAw8BdHMJ3AKGydcwuWW4kOO3HmqBQAdcq/OP+/DlTVxLvsCzRTnZuGg==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.4.tgz", + "integrity": "sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.5.tgz", - "integrity": "sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.18.2", - "@babel/helper-compilation-targets": "^7.18.2", - "@babel/helper-module-transforms": "^7.18.0", - "@babel/helpers": "^7.18.2", - "@babel/parser": "^7.18.5", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.18.5", - "@babel/types": "^7.18.4", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -979,13 +1036,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", - "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "version": "7.19.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.5.tgz", + "integrity": "sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.2", - "@jridgewell/gen-mapping": "^0.3.0", + "@babel/types": "^7.19.4", + "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, "engines": { @@ -993,12 +1050,12 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", - "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.0", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" }, @@ -1007,14 +1064,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz", - "integrity": "sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", + "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", + "@babel/compat-data": "^7.19.3", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", "semver": "^6.3.0" }, "engines": { @@ -1025,142 +1082,151 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz", - "integrity": "sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ==", + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", "dev": true, "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", "dev": true, "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", "dev": true, "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz", - "integrity": "sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", + "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.18.0", - "@babel/types": "^7.18.0" + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz", - "integrity": "sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", + "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.2.tgz", - "integrity": "sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", + "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", "dev": true, "dependencies": { - "@babel/types": "^7.18.2" + "@babel/types": "^7.19.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", "dev": true, "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.2.tgz", - "integrity": "sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz", + "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==", "dev": true, "dependencies": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.18.2", - "@babel/types": "^7.18.2" + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.4", + "@babel/types": "^7.19.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz", - "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -1240,9 +1306,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.5.tgz", - "integrity": "sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz", + "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1399,12 +1465,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.12.tgz", - "integrity": "sha512-TYY0SXFiO31YXtNg3HtFwNJHjLsAyIIhAhNWkQ5whPPS7HWUFlg9z0Ta4qAQNjQbP1wsSt/oKkmZ/4/WWdMUpw==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", + "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.17.12" + "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" @@ -1414,33 +1480,33 @@ } }, "node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.5.tgz", - "integrity": "sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.18.2", - "@babel/helper-environment-visitor": "^7.18.2", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.18.5", - "@babel/types": "^7.18.4", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.4.tgz", + "integrity": "sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.4", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.19.4", + "@babel/types": "^7.19.4", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1449,12 +1515,13 @@ } }, "node_modules/@babel/types": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", - "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz", + "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1768,37 +1835,37 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", - "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", - "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", - "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", - "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "node_modules/@sinonjs/commons": { @@ -1885,9 +1952,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.17.1.tgz", - "integrity": "sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", + "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", "dev": true, "dependencies": { "@babel/types": "^7.3.0" @@ -1976,9 +2043,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2049,7 +2116,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2058,7 +2124,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2104,9 +2169,9 @@ "dev": true }, "node_modules/aws-cdk": { - "version": "2.42.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.42.0.tgz", - "integrity": "sha512-BdkPhkj2PRkGSfsXh7kduUkJg+y234heWOaKzMEiauCt2Bj72wYwZhYG60TAFue7K7ngSjKzUeQ+G7SfKZcudg==", + "version": "2.45.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.45.0.tgz", + "integrity": "sha512-AIug6Ugvtd3I0+U3gTNZtJVDhOgpGpxwWMoOQUlX6xKGwDgQxWrWdq2QWe7ZyKgCRnY9SM90fa+Yxbx+VYk9Bw==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -2119,9 +2184,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.42.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.42.0.tgz", - "integrity": "sha512-jHHcUm2baKv87L7MO0fktAhmtDZfnHLPsLIktzgW2zCDRNUCFoUPkQ+44eUkZkgmL2sEOSMEQFhbNCxmbW4OSQ==", + "version": "2.45.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.45.0.tgz", + "integrity": "sha512-oEeZZF8xjub9KYAB7n01A60wwQXSzNapmiih3t5uf9aEvlvqT+0as8/WrPdNIeAaf9Lhb0WQXdZ2o2DlsFHbAg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -2434,9 +2499,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.20.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz", - "integrity": "sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "funding": [ { @@ -2449,11 +2514,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001349", - "electron-to-chromium": "^1.4.147", - "escalade": "^3.1.1", - "node-releases": "^2.0.5", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" }, "bin": { "browserslist": "cli.js" @@ -2507,9 +2571,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001355", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz", - "integrity": "sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==", + "version": "1.0.30001418", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", + "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", "dev": true, "funding": [ { @@ -2567,9 +2631,9 @@ } }, "node_modules/ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "node_modules/cjs-module-lexer": { @@ -2579,14 +2643,16 @@ "dev": true }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/co": { @@ -2609,7 +2675,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2620,8 +2685,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -2642,21 +2706,18 @@ "dev": true }, "node_modules/constructs": { - "version": "10.1.106", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.106.tgz", - "integrity": "sha512-xcNB+/5jKk7+9w4pXe5jThpUEDDbhtWLeXlhy9GVdFa/tuasOVEiowZOZMjPvcXrujGgSkVleebo6ZNzvYyZug==", + "version": "10.1.128", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.128.tgz", + "integrity": "sha512-X2QvdedBwVRAqwU5I2Hv+xcB7xumYO/Z+PNozubDIRgtetWRWIOOkZCRXeERm7xfCjLVyQdAPHDKVuoVuGRoHA==", "engines": { "node": ">= 14.17.0" } }, "node_modules/convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/create-require": { "version": "1.1.1", @@ -2734,9 +2795,9 @@ } }, "node_modules/decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.1.tgz", + "integrity": "sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw==", "dev": true }, "node_modules/dedent": { @@ -2818,9 +2879,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.158", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.158.tgz", - "integrity": "sha512-gppO3/+Y6sP432HtvwvuU8S+YYYLH4PmAYvQwqUtt9HDOmEsBwQfLnK9T8+1NIKwAS1BEygIjTaATC4H5EzvxQ==", + "version": "1.4.276", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz", + "integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==", "dev": true }, "node_modules/emittery": { @@ -2838,8 +2899,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/entities": { "version": "2.2.0", @@ -2862,7 +2922,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -3001,9 +3060,9 @@ } }, "node_modules/fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, "dependencies": { "bser": "2.1.1" @@ -3061,14 +3120,6 @@ "node": ">=12" } }, - "node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3108,7 +3159,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -3306,9 +3356,9 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -3321,7 +3371,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -3384,9 +3433,9 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz", - "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "dependencies": { "@babel/core": "^7.12.3", @@ -3428,9 +3477,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -3543,6 +3592,35 @@ } } }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-config": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", @@ -3961,9 +4039,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4174,14 +4252,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4365,9 +4435,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", - "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, "node_modules/normalize-path": { @@ -4392,9 +4462,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", "dev": true }, "node_modules/once": { @@ -4619,9 +4689,9 @@ } }, "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, "node_modules/punycode": { @@ -4633,6 +4703,12 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -4643,18 +4719,23 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "dev": true, "dependencies": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -4710,12 +4791,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4837,7 +4912,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4851,7 +4925,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4902,9 +4975,9 @@ } }, "node_modules/supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", "dev": true, "dependencies": { "has-flag": "^4.0.0", @@ -4996,19 +5069,29 @@ } }, "node_modules/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", "dev": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { "node": ">=6" } }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", @@ -5065,9 +5148,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5080,9 +5163,9 @@ } }, "node_modules/ts-node": { - "version": "10.8.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", - "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -5192,12 +5275,47 @@ } }, "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "engines": { - "node": ">= 4.0.0" + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" } }, "node_modules/uuid": { @@ -5333,7 +5451,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5365,9 +5482,9 @@ } }, "node_modules/ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "dev": true, "engines": { "node": ">=8.3.0" @@ -5401,7 +5518,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -5413,21 +5529,20 @@ "dev": true }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { @@ -5439,6 +5554,14 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -5461,15 +5584,15 @@ } }, "@aws-cdk/aws-lambda-python-alpha": { - "version": "2.42.0-alpha.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda-python-alpha/-/aws-lambda-python-alpha-2.42.0-alpha.0.tgz", - "integrity": "sha512-V5fi76QYOWXVhyHRuZ8og/s34goGHuJ9hyEVuG3dsO1WMEx3fovtzkXTdh3BSwZ/Pl4hYJY4blNO+XtVSXcjBA==", + "version": "2.45.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda-python-alpha/-/aws-lambda-python-alpha-2.45.0-alpha.0.tgz", + "integrity": "sha512-seRYApZ/FUSi19PHS/3cXfpeijdrc8JuMlnUl688IE3FOxAYaT+geFx5KldW3+9mG5cRSxCYlXukf3NOW2ZCjw==", "requires": {} }, "@aws-crypto/ie11-detection": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.0.tgz", - "integrity": "sha512-pkVXf/dq6PITJ0jzYZ69VhL8VFOFoPZLZqtU/12SGnzYuJOOGNfF41q9GxdI1yqC8R13Rq3jOLKDFpUJFT5eTA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", + "integrity": "sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==", "requires": { "tslib": "^1.11.1" }, @@ -5521,9 +5644,9 @@ } }, "@aws-crypto/supports-web-crypto": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.0.tgz", - "integrity": "sha512-Ge7WQ3E0OC7FHYprsZV3h0QIcpdyJLvIeg+uTuHqRYm8D6qCFJoiC+edSzSyFiHtZf+NOQDJ1q46qxjtzIY2nA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz", + "integrity": "sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==", "requires": { "tslib": "^1.11.1" }, @@ -5536,11 +5659,11 @@ } }, "@aws-crypto/util": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.1.tgz", - "integrity": "sha512-JJmFFwvbm08lULw4Nm5QOLg8+lAQeC8aCXK5xrtxntYzYXCGfHwUJ4Is3770Q7HmICsXthGQ+ZsDL7C2uH3yBQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.2.tgz", + "integrity": "sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==", "requires": { - "@aws-sdk/types": "^3.1.0", + "@aws-sdk/types": "^3.110.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" }, @@ -5553,677 +5676,721 @@ } }, "@aws-sdk/abort-controller": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.110.0.tgz", - "integrity": "sha512-zok/WEVuK7Jh6V9YeA56pNZtxUASon9LTkS7vE65A4UFmNkPGNBCNgoiBcbhWfxwrZ8wtXcQk6rtUut39831mA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.186.0.tgz", + "integrity": "sha512-JFvvvtEcbYOvVRRXasi64Dd1VcOz5kJmPvtzsJ+HzMHvPbGGs/aopOJAZQJMJttzJmJwVTay0QL6yag9Kk8nYA==", "requires": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/client-cloudformation": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.112.0.tgz", - "integrity": "sha512-CQ0HQ5qoXcjP1uoz9AtRYCfar0s/3xq4xGHcCqaeSa6j7nQicHFfT9GsfF5oK9OkE8wZ1EX5OJSUHMm0ONvMQQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.186.0.tgz", + "integrity": "sha512-CAZAX9anSOmrqW9YX34pHIz05Nje2GRd3p/U2NrOs4X4gu9jeg0yf2Ea6O9u6UTSH6hniZKZD3ITbmSFA/5Lgw==", "requires": { "@aws-crypto/sha256-browser": "2.0.0", "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/client-sts": "3.112.0", - "@aws-sdk/config-resolver": "3.110.0", - "@aws-sdk/credential-provider-node": "3.112.0", - "@aws-sdk/fetch-http-handler": "3.110.0", - "@aws-sdk/hash-node": "3.110.0", - "@aws-sdk/invalid-dependency": "3.110.0", - "@aws-sdk/middleware-content-length": "3.110.0", - "@aws-sdk/middleware-host-header": "3.110.0", - "@aws-sdk/middleware-logger": "3.110.0", - "@aws-sdk/middleware-recursion-detection": "3.110.0", - "@aws-sdk/middleware-retry": "3.110.0", - "@aws-sdk/middleware-serde": "3.110.0", - "@aws-sdk/middleware-signing": "3.110.0", - "@aws-sdk/middleware-stack": "3.110.0", - "@aws-sdk/middleware-user-agent": "3.110.0", - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/node-http-handler": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/smithy-client": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/url-parser": "3.110.0", - "@aws-sdk/util-base64-browser": "3.109.0", - "@aws-sdk/util-base64-node": "3.55.0", - "@aws-sdk/util-body-length-browser": "3.55.0", - "@aws-sdk/util-body-length-node": "3.55.0", - "@aws-sdk/util-defaults-mode-browser": "3.110.0", - "@aws-sdk/util-defaults-mode-node": "3.110.0", - "@aws-sdk/util-user-agent-browser": "3.110.0", - "@aws-sdk/util-user-agent-node": "3.110.0", - "@aws-sdk/util-utf8-browser": "3.109.0", - "@aws-sdk/util-utf8-node": "3.109.0", - "@aws-sdk/util-waiter": "3.110.0", + "@aws-sdk/client-sts": "3.186.0", + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/credential-provider-node": "3.186.0", + "@aws-sdk/fetch-http-handler": "3.186.0", + "@aws-sdk/hash-node": "3.186.0", + "@aws-sdk/invalid-dependency": "3.186.0", + "@aws-sdk/middleware-content-length": "3.186.0", + "@aws-sdk/middleware-host-header": "3.186.0", + "@aws-sdk/middleware-logger": "3.186.0", + "@aws-sdk/middleware-recursion-detection": "3.186.0", + "@aws-sdk/middleware-retry": "3.186.0", + "@aws-sdk/middleware-serde": "3.186.0", + "@aws-sdk/middleware-signing": "3.186.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/middleware-user-agent": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/node-http-handler": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/smithy-client": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", + "@aws-sdk/util-base64-node": "3.186.0", + "@aws-sdk/util-body-length-browser": "3.186.0", + "@aws-sdk/util-body-length-node": "3.186.0", + "@aws-sdk/util-defaults-mode-browser": "3.186.0", + "@aws-sdk/util-defaults-mode-node": "3.186.0", + "@aws-sdk/util-user-agent-browser": "3.186.0", + "@aws-sdk/util-user-agent-node": "3.186.0", + "@aws-sdk/util-utf8-browser": "3.186.0", + "@aws-sdk/util-utf8-node": "3.186.0", + "@aws-sdk/util-waiter": "3.186.0", "entities": "2.2.0", "fast-xml-parser": "3.19.0", "tslib": "^2.3.1", "uuid": "^8.3.2" } }, + "@aws-sdk/client-lambda": { + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.186.0.tgz", + "integrity": "sha512-Wr/s591KMRhRRMnv4eWpJ56Kvt5wMjHFW/XGb4NBEPBkLnd/92jVDXp76tGhmbILa/zNpMBaHTx2eqDvHA6AXQ==", + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/client-sts": "3.186.0", + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/credential-provider-node": "3.186.0", + "@aws-sdk/fetch-http-handler": "3.186.0", + "@aws-sdk/hash-node": "3.186.0", + "@aws-sdk/invalid-dependency": "3.186.0", + "@aws-sdk/middleware-content-length": "3.186.0", + "@aws-sdk/middleware-host-header": "3.186.0", + "@aws-sdk/middleware-logger": "3.186.0", + "@aws-sdk/middleware-recursion-detection": "3.186.0", + "@aws-sdk/middleware-retry": "3.186.0", + "@aws-sdk/middleware-serde": "3.186.0", + "@aws-sdk/middleware-signing": "3.186.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/middleware-user-agent": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/node-http-handler": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/smithy-client": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", + "@aws-sdk/util-base64-node": "3.186.0", + "@aws-sdk/util-body-length-browser": "3.186.0", + "@aws-sdk/util-body-length-node": "3.186.0", + "@aws-sdk/util-defaults-mode-browser": "3.186.0", + "@aws-sdk/util-defaults-mode-node": "3.186.0", + "@aws-sdk/util-user-agent-browser": "3.186.0", + "@aws-sdk/util-user-agent-node": "3.186.0", + "@aws-sdk/util-utf8-browser": "3.186.0", + "@aws-sdk/util-utf8-node": "3.186.0", + "@aws-sdk/util-waiter": "3.186.0", + "tslib": "^2.3.1" + } + }, "@aws-sdk/client-sso": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.112.0.tgz", - "integrity": "sha512-FwFmiapxuVQiyMdDaBvCpajnJkVWEUHBdO+7rIpzgKHkODEPou5/AwboaGRPEFYULOyYeI0HiDFzpK0G6de+7Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.186.0.tgz", + "integrity": "sha512-qwLPomqq+fjvp42izzEpBEtGL2+dIlWH5pUCteV55hTEwHgo+m9LJPIrMWkPeoMBzqbNiu5n6+zihnwYlCIlEA==", "requires": { "@aws-crypto/sha256-browser": "2.0.0", "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/config-resolver": "3.110.0", - "@aws-sdk/fetch-http-handler": "3.110.0", - "@aws-sdk/hash-node": "3.110.0", - "@aws-sdk/invalid-dependency": "3.110.0", - "@aws-sdk/middleware-content-length": "3.110.0", - "@aws-sdk/middleware-host-header": "3.110.0", - "@aws-sdk/middleware-logger": "3.110.0", - "@aws-sdk/middleware-recursion-detection": "3.110.0", - "@aws-sdk/middleware-retry": "3.110.0", - "@aws-sdk/middleware-serde": "3.110.0", - "@aws-sdk/middleware-stack": "3.110.0", - "@aws-sdk/middleware-user-agent": "3.110.0", - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/node-http-handler": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/smithy-client": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/url-parser": "3.110.0", - "@aws-sdk/util-base64-browser": "3.109.0", - "@aws-sdk/util-base64-node": "3.55.0", - "@aws-sdk/util-body-length-browser": "3.55.0", - "@aws-sdk/util-body-length-node": "3.55.0", - "@aws-sdk/util-defaults-mode-browser": "3.110.0", - "@aws-sdk/util-defaults-mode-node": "3.110.0", - "@aws-sdk/util-user-agent-browser": "3.110.0", - "@aws-sdk/util-user-agent-node": "3.110.0", - "@aws-sdk/util-utf8-browser": "3.109.0", - "@aws-sdk/util-utf8-node": "3.109.0", + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/fetch-http-handler": "3.186.0", + "@aws-sdk/hash-node": "3.186.0", + "@aws-sdk/invalid-dependency": "3.186.0", + "@aws-sdk/middleware-content-length": "3.186.0", + "@aws-sdk/middleware-host-header": "3.186.0", + "@aws-sdk/middleware-logger": "3.186.0", + "@aws-sdk/middleware-recursion-detection": "3.186.0", + "@aws-sdk/middleware-retry": "3.186.0", + "@aws-sdk/middleware-serde": "3.186.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/middleware-user-agent": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/node-http-handler": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/smithy-client": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", + "@aws-sdk/util-base64-node": "3.186.0", + "@aws-sdk/util-body-length-browser": "3.186.0", + "@aws-sdk/util-body-length-node": "3.186.0", + "@aws-sdk/util-defaults-mode-browser": "3.186.0", + "@aws-sdk/util-defaults-mode-node": "3.186.0", + "@aws-sdk/util-user-agent-browser": "3.186.0", + "@aws-sdk/util-user-agent-node": "3.186.0", + "@aws-sdk/util-utf8-browser": "3.186.0", + "@aws-sdk/util-utf8-node": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/client-sts": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.112.0.tgz", - "integrity": "sha512-hSApRO2wg3jk9VRGM6SCZO3aFP7DKVSUqs6FrvlXlj+JU88ZKObjrGE61cCzXoD89Dh+b9t8A2T6W51Nzriaxw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.186.0.tgz", + "integrity": "sha512-lyAPI6YmIWWYZHQ9fBZ7QgXjGMTtktL5fk8kOcZ98ja+8Vu0STH1/u837uxqvZta8/k0wijunIL3jWUhjsNRcg==", "requires": { "@aws-crypto/sha256-browser": "2.0.0", "@aws-crypto/sha256-js": "2.0.0", - "@aws-sdk/config-resolver": "3.110.0", - "@aws-sdk/credential-provider-node": "3.112.0", - "@aws-sdk/fetch-http-handler": "3.110.0", - "@aws-sdk/hash-node": "3.110.0", - "@aws-sdk/invalid-dependency": "3.110.0", - "@aws-sdk/middleware-content-length": "3.110.0", - "@aws-sdk/middleware-host-header": "3.110.0", - "@aws-sdk/middleware-logger": "3.110.0", - "@aws-sdk/middleware-recursion-detection": "3.110.0", - "@aws-sdk/middleware-retry": "3.110.0", - "@aws-sdk/middleware-sdk-sts": "3.110.0", - "@aws-sdk/middleware-serde": "3.110.0", - "@aws-sdk/middleware-signing": "3.110.0", - "@aws-sdk/middleware-stack": "3.110.0", - "@aws-sdk/middleware-user-agent": "3.110.0", - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/node-http-handler": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/smithy-client": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/url-parser": "3.110.0", - "@aws-sdk/util-base64-browser": "3.109.0", - "@aws-sdk/util-base64-node": "3.55.0", - "@aws-sdk/util-body-length-browser": "3.55.0", - "@aws-sdk/util-body-length-node": "3.55.0", - "@aws-sdk/util-defaults-mode-browser": "3.110.0", - "@aws-sdk/util-defaults-mode-node": "3.110.0", - "@aws-sdk/util-user-agent-browser": "3.110.0", - "@aws-sdk/util-user-agent-node": "3.110.0", - "@aws-sdk/util-utf8-browser": "3.109.0", - "@aws-sdk/util-utf8-node": "3.109.0", + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/credential-provider-node": "3.186.0", + "@aws-sdk/fetch-http-handler": "3.186.0", + "@aws-sdk/hash-node": "3.186.0", + "@aws-sdk/invalid-dependency": "3.186.0", + "@aws-sdk/middleware-content-length": "3.186.0", + "@aws-sdk/middleware-host-header": "3.186.0", + "@aws-sdk/middleware-logger": "3.186.0", + "@aws-sdk/middleware-recursion-detection": "3.186.0", + "@aws-sdk/middleware-retry": "3.186.0", + "@aws-sdk/middleware-sdk-sts": "3.186.0", + "@aws-sdk/middleware-serde": "3.186.0", + "@aws-sdk/middleware-signing": "3.186.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/middleware-user-agent": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/node-http-handler": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/smithy-client": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", + "@aws-sdk/util-base64-node": "3.186.0", + "@aws-sdk/util-body-length-browser": "3.186.0", + "@aws-sdk/util-body-length-node": "3.186.0", + "@aws-sdk/util-defaults-mode-browser": "3.186.0", + "@aws-sdk/util-defaults-mode-node": "3.186.0", + "@aws-sdk/util-user-agent-browser": "3.186.0", + "@aws-sdk/util-user-agent-node": "3.186.0", + "@aws-sdk/util-utf8-browser": "3.186.0", + "@aws-sdk/util-utf8-node": "3.186.0", "entities": "2.2.0", "fast-xml-parser": "3.19.0", "tslib": "^2.3.1" } }, "@aws-sdk/config-resolver": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.110.0.tgz", - "integrity": "sha512-7VvtKy4CL63BAktQ2vgsjhWDSXpkXO5YdiI56LQnHztrvSuJBBaxJ7R1p/k0b2tEUhYKUziAIW8EKE/7EGPR4g==", - "requires": { - "@aws-sdk/signature-v4": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-config-provider": "3.109.0", - "@aws-sdk/util-middleware": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.186.0.tgz", + "integrity": "sha512-l8DR7Q4grEn1fgo2/KvtIfIHJS33HGKPQnht8OPxkl0dMzOJ0jxjOw/tMbrIcPnr2T3Fi7LLcj3dY1Fo1poruQ==", + "requires": { + "@aws-sdk/signature-v4": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-config-provider": "3.186.0", + "@aws-sdk/util-middleware": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-env": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.110.0.tgz", - "integrity": "sha512-oFU3IYk/Bl5tdsz1qigtm3I25a9cvXPqlE8VjYjxVDdLujF5zd/4HLbhP4GQWhpEwZmM1ijcSNfLcyywVevTZg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.186.0.tgz", + "integrity": "sha512-N9LPAqi1lsQWgxzmU4NPvLPnCN5+IQ3Ai1IFf3wM6FFPNoSUd1kIA2c6xaf0BE7j5Kelm0raZOb4LnV3TBAv+g==", "requires": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-imds": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.110.0.tgz", - "integrity": "sha512-atl+7/dAB+8fG9XI2fYyCgXKYDbOzot65VAwis+14bOEUCVp7PCJifBEZ/L8GEq564p+Fa2p1IpV0wuQXxqFUQ==", - "requires": { - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/url-parser": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.186.0.tgz", + "integrity": "sha512-iJeC7KrEgPPAuXjCZ3ExYZrRQvzpSdTZopYgUm5TnNZ8S1NU/4nvv5xVy61JvMj3JQAeG8UDYYgC421Foc8wQw==", + "requires": { + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/url-parser": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-ini": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.112.0.tgz", - "integrity": "sha512-ebgZ6/jZdTGHQ3zfq/ccmS+7YmLk6yUWHDmh69VK+B1Dd+S1jFwbD9EQ+pYWCp/gEl9F620NSwb6KghRylPWEQ==", - "requires": { - "@aws-sdk/credential-provider-env": "3.110.0", - "@aws-sdk/credential-provider-imds": "3.110.0", - "@aws-sdk/credential-provider-sso": "3.112.0", - "@aws-sdk/credential-provider-web-identity": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.186.0.tgz", + "integrity": "sha512-ecrFh3MoZhAj5P2k/HXo/hMJQ3sfmvlommzXuZ/D1Bj2yMcyWuBhF1A83Fwd2gtYrWRrllsK3IOMM5Jr8UIVZA==", + "requires": { + "@aws-sdk/credential-provider-env": "3.186.0", + "@aws-sdk/credential-provider-imds": "3.186.0", + "@aws-sdk/credential-provider-sso": "3.186.0", + "@aws-sdk/credential-provider-web-identity": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-node": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.112.0.tgz", - "integrity": "sha512-7txS7P3BAaU4cksFw/PnoVskVvO8h/TPvOl/BxFtCiUdwA6FRltLvBeMlN08fwUoqgM6z06q8areBdeDqCHOSw==", - "requires": { - "@aws-sdk/credential-provider-env": "3.110.0", - "@aws-sdk/credential-provider-imds": "3.110.0", - "@aws-sdk/credential-provider-ini": "3.112.0", - "@aws-sdk/credential-provider-process": "3.110.0", - "@aws-sdk/credential-provider-sso": "3.112.0", - "@aws-sdk/credential-provider-web-identity": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.186.0.tgz", + "integrity": "sha512-HIt2XhSRhEvVgRxTveLCzIkd/SzEBQfkQ6xMJhkBtfJw1o3+jeCk+VysXM0idqmXytctL0O3g9cvvTHOsUgxOA==", + "requires": { + "@aws-sdk/credential-provider-env": "3.186.0", + "@aws-sdk/credential-provider-imds": "3.186.0", + "@aws-sdk/credential-provider-ini": "3.186.0", + "@aws-sdk/credential-provider-process": "3.186.0", + "@aws-sdk/credential-provider-sso": "3.186.0", + "@aws-sdk/credential-provider-web-identity": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-process": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.110.0.tgz", - "integrity": "sha512-JJcZePvRTfQHYj/+EEY13yItnZH/e8exlARFUjN0L13UrgHpOJtDQBa+YBHXo6MbTFQh+re25z2kzc+zOYSMNQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.186.0.tgz", + "integrity": "sha512-ATRU6gbXvWC1TLnjOEZugC/PBXHBoZgBADid4fDcEQY1vF5e5Ux1kmqkJxyHtV5Wl8sE2uJfwWn+FlpUHRX67g==", "requires": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-sso": { - "version": "3.112.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.112.0.tgz", - "integrity": "sha512-b6rOrSXbNK3fGyPvNpyF5zdktmAoNOqHCTmFSUcxRxOipyRGb5JACsbjWthIQkpWkpNCT8GFNLEg9spXPFIdLA==", - "requires": { - "@aws-sdk/client-sso": "3.112.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.186.0.tgz", + "integrity": "sha512-mJ+IZljgXPx99HCmuLgBVDPLepHrwqnEEC/0wigrLCx6uz3SrAWmGZsNbxSEtb2CFSAaczlTHcU/kIl7XZIyeQ==", + "requires": { + "@aws-sdk/client-sso": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-web-identity": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.110.0.tgz", - "integrity": "sha512-e4e5u7v3fsUFZsMcFMhMy1NdJBQpunYcLwpYlszm3OEICwTTekQ+hVvnVRd134doHvzepE4yp9sAop0Cj+IRVQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.186.0.tgz", + "integrity": "sha512-KqzI5eBV72FE+8SuOQAu+r53RXGVHg4AuDJmdXyo7Gc4wS/B9FNElA8jVUjjYgVnf0FSiri+l41VzQ44dCopSA==", "requires": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/fetch-http-handler": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.110.0.tgz", - "integrity": "sha512-vk+K4GeCZL2J2rtvKO+T0Q7i3MDpEGZBMg5K2tj9sMcEQwty0BF0aFnP7Eu2l4/Zif2z1mWuUFM2WcZI6DVnbw==", - "requires": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/querystring-builder": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-base64-browser": "3.109.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.186.0.tgz", + "integrity": "sha512-k2v4AAHRD76WnLg7arH94EvIclClo/YfuqO7NoQ6/KwOxjRhs4G6TgIsAZ9E0xmqoJoV81Xqy8H8ldfy9F8LEw==", + "requires": { + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/querystring-builder": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-base64-browser": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/hash-node": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.110.0.tgz", - "integrity": "sha512-wakl+kP2O8wTGYiQ3InZy+CVfGrIpFfq9fo4zif9PZac0BbUbguUU1dkY34uZiaf+4o2/9MoDYrHU2HYeXKxWw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.186.0.tgz", + "integrity": "sha512-G3zuK8/3KExDTxqrGqko+opOMLRF0BwcwekV/wm3GKIM/NnLhHblBs2zd/yi7VsEoWmuzibfp6uzxgFpEoJ87w==", "requires": { - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-buffer-from": "3.55.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-buffer-from": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/invalid-dependency": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.110.0.tgz", - "integrity": "sha512-O8J1InmtJkoiUMbQDtxBfOzgigBp9iSVsNXQrhs2qHh3826cJOfE7NGT3u+NMw73Pk5j2cfmOh1+7k/76IqxOg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.186.0.tgz", + "integrity": "sha512-hjeZKqORhG2DPWYZ776lQ9YO3gjw166vZHZCZU/43kEYaCZHsF4mexHwHzreAY6RfS25cH60Um7dUh1aeVIpkw==", "requires": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/is-array-buffer": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.55.0.tgz", - "integrity": "sha512-NbiPHVYuPxdqdFd6FxzzN3H1BQn/iWA3ri3Ry7AyLeP/tGs1yzEWMwf8BN8TSMALI0GXT6Sh0GDWy3Ok5xB6DA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.186.0.tgz", + "integrity": "sha512-fObm+P6mjWYzxoFY4y2STHBmSdgKbIAXez0xope563mox62I8I4hhVPUCaDVydXvDpJv8tbedJMk0meJl22+xA==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/middleware-content-length": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.110.0.tgz", - "integrity": "sha512-hKU+zdqfAJQg22LXMVu/z35nNIHrVAKpVKPe9+WYVdL/Z7JKUPK7QymqKGOyDuDbzW6OxyulC1zKGEX12zGmdA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.186.0.tgz", + "integrity": "sha512-Ol3c1ks3IK1s+Okc/rHIX7w2WpXofuQdoAEme37gHeml+8FtUlWH/881h62xfMdf+0YZpRuYv/eM7lBmJBPNJw==", "requires": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/middleware-host-header": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.110.0.tgz", - "integrity": "sha512-/Cknn1vL2LTlclI0MX2RzmtdPlCJ5palCRXxm/mod1oHwg4oNTKRlUX3LUD+L8g7JuJ4h053Ch9KS/A0vanE5Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.186.0.tgz", + "integrity": "sha512-5bTzrRzP2IGwyF3QCyMGtSXpOOud537x32htZf344IvVjrqZF/P8CDfGTkHkeBCIH+wnJxjK+l/QBb3ypAMIqQ==", "requires": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/middleware-logger": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.110.0.tgz", - "integrity": "sha512-+pz+a+8dfTnzLj79nHrv3aONMp/N36/erMd+7JXeR84QEosVLrFBUwKA8x5x6O3s1iBbQzRKMYEIuja9xn1BPA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.186.0.tgz", + "integrity": "sha512-/1gGBImQT8xYh80pB7QtyzA799TqXtLZYQUohWAsFReYB7fdh5o+mu2rX0FNzZnrLIh2zBUNs4yaWGsnab4uXg==", "requires": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/middleware-recursion-detection": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.110.0.tgz", - "integrity": "sha512-Wav782zd7bcd1e6txRob76CDOdVOaUQ8HXoywiIm/uFrEEUZvhs2mgnXjVUVCMBUehdNgnL99z420aS13JeL/Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.186.0.tgz", + "integrity": "sha512-Za7k26Kovb4LuV5tmC6wcVILDCt0kwztwSlB991xk4vwNTja8kKxSt53WsYG8Q2wSaW6UOIbSoguZVyxbIY07Q==", "requires": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/middleware-retry": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.110.0.tgz", - "integrity": "sha512-lwLAQQveCiUqymQvVYjCee6QOXw3Zqbc9yq+pxYdXbs1Cv1XMA6PeJeUU5r5KEVuSceBLyyrnl6E0R1l1om1MQ==", - "requires": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/service-error-classification": "3.110.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-middleware": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.186.0.tgz", + "integrity": "sha512-/VI9emEKhhDzlNv9lQMmkyxx3GjJ8yPfXH3HuAeOgM1wx1BjCTLRYEWnTbQwq7BDzVENdneleCsGAp7yaj80Aw==", + "requires": { + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/service-error-classification": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-middleware": "3.186.0", "tslib": "^2.3.1", "uuid": "^8.3.2" } }, "@aws-sdk/middleware-sdk-sts": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.110.0.tgz", - "integrity": "sha512-EjY/YFdlr5jECde6qIrTIyGBbn/34CKcQGKvmvRd31+3qaClIJLAwNuHfcVzWvCUGbAslsfvdbOpLju33pSQRA==", - "requires": { - "@aws-sdk/middleware-signing": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/signature-v4": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.186.0.tgz", + "integrity": "sha512-GDcK0O8rjtnd+XRGnxzheq1V2jk4Sj4HtjrxW/ROyhzLOAOyyxutBt+/zOpDD6Gba3qxc69wE+Cf/qngOkEkDw==", + "requires": { + "@aws-sdk/middleware-signing": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/signature-v4": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/middleware-serde": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.110.0.tgz", - "integrity": "sha512-brVupxgEAmcZ9cZvdHEH8zncjvGKIiud8pOe4fiimp5NpHmjBLew4jUbnOKNZNAjaidcKUtz//cxtutD6yXEww==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.186.0.tgz", + "integrity": "sha512-6FEAz70RNf18fKL5O7CepPSwTKJEIoyG9zU6p17GzKMgPeFsxS5xO94Hcq5tV2/CqeHliebjqhKY7yi+Pgok7g==", "requires": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/middleware-signing": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.110.0.tgz", - "integrity": "sha512-y6ZKrGYfgDlFMzWhZmoq5J1UctBgZOUvMmnU9sSeZ020IlEPiOxFMvR0Zu6TcYThp8uy3P0wyjQtGYeTl9Z/kA==", - "requires": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/signature-v4": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.186.0.tgz", + "integrity": "sha512-riCJYG/LlF/rkgVbHkr4xJscc0/sECzDivzTaUmfb9kJhAwGxCyNqnTvg0q6UO00kxSdEB9zNZI2/iJYVBijBQ==", + "requires": { + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/signature-v4": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-middleware": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/middleware-stack": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.110.0.tgz", - "integrity": "sha512-iaLHw6ctOuGa9UxNueU01Xes+15dR+mqioRpUOUZ9Zx+vhXVpD7C8lnNqhRnYeFXs10/rNIzASgsIrAHTlnlIQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.186.0.tgz", + "integrity": "sha512-fENMoo0pW7UBrbuycPf+3WZ+fcUgP9PnQ0jcOK3WWZlZ9d2ewh4HNxLh4EE3NkNYj4VIUFXtTUuVNHlG8trXjQ==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/middleware-user-agent": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.110.0.tgz", - "integrity": "sha512-Y6FgiZr99DilYq6AjeaaWcNwVlSQpNGKrILzvV4Tmz03OaBIspe4KL+8EZ2YA/sAu5Lpw80vItdezqDOwGAlnQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.186.0.tgz", + "integrity": "sha512-fb+F2PF9DLKOVMgmhkr+ltN8ZhNJavTla9aqmbd01846OLEaN1n5xEnV7p8q5+EznVBWDF38Oz9Ae5BMt3Hs7w==", "requires": { - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/node-config-provider": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.110.0.tgz", - "integrity": "sha512-46p4dCPGYctuybTQTwLpjenA1QFHeyJw/OyggGbtUJUy+833+ldnAwcPVML2aXJKUKv3APGI8vq1kaloyNku3Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.186.0.tgz", + "integrity": "sha512-De93mgmtuUUeoiKXU8pVHXWKPBfJQlS/lh1k2H9T2Pd9Tzi0l7p5ttddx4BsEx4gk+Pc5flNz+DeptiSjZpa4A==", "requires": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/shared-ini-file-loader": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/shared-ini-file-loader": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/node-http-handler": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.110.0.tgz", - "integrity": "sha512-/rP+hY516DpP8fZhwFW5xM/ElH0w6lxw/15VvZCoY5EnOLAF5XIsJdzscWPSEW2FHCylBM4SNrKhGar14BDXhA==", - "requires": { - "@aws-sdk/abort-controller": "3.110.0", - "@aws-sdk/protocol-http": "3.110.0", - "@aws-sdk/querystring-builder": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.186.0.tgz", + "integrity": "sha512-CbkbDuPZT9UNJ4dAZJWB3BV+Z65wFy7OduqGkzNNrKq6ZYMUfehthhUOTk8vU6RMe/0FkN+J0fFXlBx/bs/cHw==", + "requires": { + "@aws-sdk/abort-controller": "3.186.0", + "@aws-sdk/protocol-http": "3.186.0", + "@aws-sdk/querystring-builder": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/property-provider": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.110.0.tgz", - "integrity": "sha512-7NkpmYeOkK3mhWBNU+/zSDqwzeaSPH1qrq4L//WV7WS/weYyE/jusQeZoOxVsuZQnQEXHt5O2hKVeUwShl12xA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.186.0.tgz", + "integrity": "sha512-nWKqt36UW3xV23RlHUmat+yevw9up+T+953nfjcmCBKtgWlCWu/aUzewTRhKj3VRscbN+Wer95SBw9Lr/MMOlQ==", "requires": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/protocol-http": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.110.0.tgz", - "integrity": "sha512-qdi2gCbJiyPyLn+afebPNp/5nVCRh1X7t7IRIFl3FHVEC+o54u/ojay/MLZ4M/+X9Fa4Zxsb0Wpp3T0xAHVDBg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.186.0.tgz", + "integrity": "sha512-l/KYr/UBDUU5ginqTgHtFfHR3X6ljf/1J1ThIiUg3C3kVC/Zwztm7BEOw8hHRWnWQGU/jYasGYcrcPLdQqFZyQ==", "requires": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/querystring-builder": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.110.0.tgz", - "integrity": "sha512-7V3CDXj519izmbBn9ZE68ymASwGriA+Aq+cb/yHSVtffnvXjPtvONNw7G/5iVblisGLSCUe2hSvpYtcaXozbHw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.186.0.tgz", + "integrity": "sha512-mweCpuLufImxfq/rRBTEpjGuB4xhQvbokA+otjnUxlPdIobytLqEs7pCGQfLzQ7+1ZMo8LBXt70RH4A2nSX/JQ==", "requires": { - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-uri-escape": "3.55.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-uri-escape": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/querystring-parser": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.110.0.tgz", - "integrity": "sha512-//pJHH7hrhdDMZGBPKXKymmC/tJM7gFT0w/qbu/yd3Wm4W2fMB+8gkmj6EZctx7jrsWlfRQuvFejKqEfapur/g==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.186.0.tgz", + "integrity": "sha512-0iYfEloghzPVXJjmnzHamNx1F1jIiTW9Svy5ZF9LVqyr/uHZcQuiWYsuhWloBMLs8mfWarkZM02WfxZ8buAuhg==", "requires": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/service-error-classification": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.110.0.tgz", - "integrity": "sha512-ccgCE0pU/4RmXR6CP3fLAdhPAve7bK/yXBbGzpSHGAQOXqNxYzOsAvQ30Jg6X+qjLHsI/HR2pLIE65z4k6tynw==" + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.186.0.tgz", + "integrity": "sha512-DRl3ORk4tF+jmH5uvftlfaq0IeKKpt0UPAOAFQ/JFWe+TjOcQd/K+VC0iiIG97YFp3aeFmH1JbEgsNxd+8fdxw==" }, "@aws-sdk/shared-ini-file-loader": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.110.0.tgz", - "integrity": "sha512-E1ERoqEoG206XNBYWCKLgHkzCbTxdpDEGbsLET2DnvjFsT0s9p2dPvVux3bYl7JVAhyGduE+qcqWk7MzhFCBNQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.186.0.tgz", + "integrity": "sha512-2FZqxmICtwN9CYid4dwfJSz/gGFHyStFQ3HCOQ8DsJUf2yREMSBsVmKqsyWgOrYcQ98gPcD5GIa7QO5yl3XF6A==", "requires": { + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/signature-v4": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.110.0.tgz", - "integrity": "sha512-utxxdllOnmQDhbpipnFAbuQ4c2pwefZ+2hi48jKvQRULQ2PO4nxLmdZm6B0FXaTijbKsyO7GrMik+EZ6mi3ARQ==", - "requires": { - "@aws-sdk/is-array-buffer": "3.55.0", - "@aws-sdk/types": "3.110.0", - "@aws-sdk/util-hex-encoding": "3.109.0", - "@aws-sdk/util-middleware": "3.110.0", - "@aws-sdk/util-uri-escape": "3.55.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.186.0.tgz", + "integrity": "sha512-18i96P5c4suMqwSNhnEOqhq4doqqyjH4fn0YV3F8TkekHPIWP4mtIJ0PWAN4eievqdtcKgD/GqVO6FaJG9texw==", + "requires": { + "@aws-sdk/is-array-buffer": "3.186.0", + "@aws-sdk/types": "3.186.0", + "@aws-sdk/util-hex-encoding": "3.186.0", + "@aws-sdk/util-middleware": "3.186.0", + "@aws-sdk/util-uri-escape": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/smithy-client": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.110.0.tgz", - "integrity": "sha512-gNLYrmdAe/1hVF2Nv2LF4OkL1A0a1o708pEMZHzql9xP164omRDaLrGDhz9tH7tsJEgLz+Bf4E8nTuISeDwvGg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.186.0.tgz", + "integrity": "sha512-rdAxSFGSnrSprVJ6i1BXi65r4X14cuya6fYe8dSdgmFSa+U2ZevT97lb3tSINCUxBGeMXhENIzbVGkRZuMh+DQ==", "requires": { - "@aws-sdk/middleware-stack": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/middleware-stack": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/types": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.110.0.tgz", - "integrity": "sha512-dLVoqODU3laaqNFPyN1QLtlQnwX4gNPMXptEBIt/iJpuZf66IYJe6WCzVZGt4Zfa1CnUmrlA428AzdcA/KCr2A==" + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.186.0.tgz", + "integrity": "sha512-NatmSU37U+XauMFJCdFI6nougC20JUFZar+ump5wVv0i54H+2Refg1YbFDxSs0FY28TSB9jfhWIpfFBmXgL5MQ==" }, "@aws-sdk/url-parser": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.110.0.tgz", - "integrity": "sha512-tILFB8/Q73yzgO0dErJNnELmmBszd0E6FucwAnG3hfDefjqCBe09Q/1yhu2aARXyRmZa4AKp0sWcdwIWHc8dnA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.186.0.tgz", + "integrity": "sha512-jfdJkKqJZp8qjjwEjIGDqbqTuajBsddw02f86WiL8bPqD8W13/hdqbG4Fpwc+Bm6GwR6/4MY6xWXFnk8jDUKeA==", "requires": { - "@aws-sdk/querystring-parser": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/querystring-parser": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/util-base64-browser": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.109.0.tgz", - "integrity": "sha512-lAZ6fyDGiRLaIsKT9qh7P9FGuNyZ4gAbr1YOSQk/5mHtaTuUvxlPptZuInNM/0MPQm6lpcot00D8IWTucn4PbA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.186.0.tgz", + "integrity": "sha512-TpQL8opoFfzTwUDxKeon/vuc83kGXpYqjl6hR8WzmHoQgmFfdFlV+0KXZOohra1001OP3FhqvMqaYbO8p9vXVQ==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-base64-node": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.55.0.tgz", - "integrity": "sha512-UQ/ZuNoAc8CFMpSiRYmevaTsuRKzLwulZTnM8LNlIt9Wx1tpNvqp80cfvVj7yySKROtEi20wq29h31dZf1eYNQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.186.0.tgz", + "integrity": "sha512-wH5Y/EQNBfGS4VkkmiMyZXU+Ak6VCoFM1GKWopV+sj03zR2D4FHexi4SxWwEBMpZCd6foMtihhbNBuPA5fnh6w==", "requires": { - "@aws-sdk/util-buffer-from": "3.55.0", + "@aws-sdk/util-buffer-from": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/util-body-length-browser": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.55.0.tgz", - "integrity": "sha512-Ei2OCzXQw5N6ZkTMZbamUzc1z+z1R1Ja5tMEagz5BxuX4vWdBObT+uGlSzL8yvTbjoPjnxWA2aXyEqaUP3JS8Q==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.186.0.tgz", + "integrity": "sha512-zKtjkI/dkj9oGkjo+7fIz+I9KuHrVt1ROAeL4OmDESS8UZi3/O8uMDFMuCp8jft6H+WFuYH6qRVWAVwXMiasXw==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-body-length-node": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.55.0.tgz", - "integrity": "sha512-lU1d4I+9wJwydduXs0SxSfd+mHKjxeyd39VwOv6i2KSwWkPbji9UQqpflKLKw+r45jL7+xU/zfeTUg5Tt/3Gew==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.186.0.tgz", + "integrity": "sha512-U7Ii8u8Wvu9EnBWKKeuwkdrWto3c0j7LG677Spe6vtwWkvY70n9WGfiKHTgBpVeLNv8jvfcx5+H0UOPQK1o9SQ==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-buffer-from": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.55.0.tgz", - "integrity": "sha512-uVzKG1UgvnV7XX2FPTylBujYMKBPBaq/qFBxfl0LVNfrty7YjpfieQxAe6yRLD+T0Kir/WDQwGvYC+tOYG3IGA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.186.0.tgz", + "integrity": "sha512-be2GCk2lsLWg/2V5Y+S4/9pOMXhOQo4DR4dIqBdR2R+jrMMHN9Xsr5QrkT6chcqLaJ/SBlwiAEEi3StMRmCOXA==", "requires": { - "@aws-sdk/is-array-buffer": "3.55.0", + "@aws-sdk/is-array-buffer": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/util-config-provider": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.109.0.tgz", - "integrity": "sha512-GrAZl/aBv0A28LkyNyq8SPJ5fmViCwz80fWLMeWx/6q5AbivuILogjlWwEZSvZ9zrlHOcFC0+AnCa5pQrjaslw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.186.0.tgz", + "integrity": "sha512-71Qwu/PN02XsRLApyxG0EUy/NxWh/CXxtl2C7qY14t+KTiRapwbDkdJ1cMsqYqghYP4BwJoj1M+EFMQSSlkZQQ==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-defaults-mode-browser": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.110.0.tgz", - "integrity": "sha512-Y2dcOOD20S3bv/IjUqpdKIiDt6995SXNG5Pu/LeSdXNyLCOIm9rX4gHTxl9fC1KK5M/gR9fGJ362f67WwqEEqw==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.186.0.tgz", + "integrity": "sha512-U8GOfIdQ0dZ7RRVpPynGteAHx4URtEh+JfWHHVfS6xLPthPHWTbyRhkQX++K/F8Jk+T5U8Anrrqlea4TlcO2DA==", "requires": { - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "bowser": "^2.11.0", "tslib": "^2.3.1" } }, "@aws-sdk/util-defaults-mode-node": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.110.0.tgz", - "integrity": "sha512-Cr3Z5nyrw1KowjbW76xp8hkT/zJtYjAVZ9PS4l84KxIicbVvDOBpxG3yNddkuQcavmlH6G4wH9uM5DcnpKDncg==", - "requires": { - "@aws-sdk/config-resolver": "3.110.0", - "@aws-sdk/credential-provider-imds": "3.110.0", - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/property-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.186.0.tgz", + "integrity": "sha512-N6O5bpwCiE4z8y7SPHd7KYlszmNOYREa+mMgtOIXRU3VXSEHVKVWTZsHKvNTTHpW0qMqtgIvjvXCo3vsch5l3A==", + "requires": { + "@aws-sdk/config-resolver": "3.186.0", + "@aws-sdk/credential-provider-imds": "3.186.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/property-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/util-hex-encoding": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.109.0.tgz", - "integrity": "sha512-s8CgTNrn3cLkrdiohfxLuOYPCanzvHn/aH5RW6DaMoeQiG5Hl9QUiP/WtdQ9QQx3xvpQFpmvxIaSBwSgFNLQxA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.186.0.tgz", + "integrity": "sha512-UL9rdgIZz1E/jpAfaKH8QgUxNK9VP5JPgoR0bSiaefMjnsoBh0x/VVMsfUyziOoJCMLebhJzFowtwrSKEGsxNg==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-locate-window": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.55.0.tgz", - "integrity": "sha512-0sPmK2JaJE2BbTcnvybzob/VrFKCXKfN4CUKcvn0yGg/me7Bz+vtzQRB3Xp+YSx+7OtWxzv63wsvHoAnXvgxgg==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.186.0.tgz", + "integrity": "sha512-fmQLkH16ga6c5fWsA+kBYklQJjlPlcc8uayTR4avi5g3Nxqm6wPpyUwo5CppwjwWMeS+NXG0HgITtkkGntcRNg==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-middleware": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.110.0.tgz", - "integrity": "sha512-PTVWrI5fA9d5hHJs6RzX2dIS2jRQ3uW073Fm0BePpQeDdZrEk+S5KNwRhUtpN6sdSV45vm6S9rrjZUG51qwGmA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.186.0.tgz", + "integrity": "sha512-fddwDgXtnHyL9mEZ4s1tBBsKnVQHqTUmFbZKUUKPrg9CxOh0Y/zZxEa5Olg/8dS/LzM1tvg0ATkcyd4/kEHIhg==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-uri-escape": { - "version": "3.55.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.55.0.tgz", - "integrity": "sha512-mmdDLUpFCN2nkfwlLdOM54lTD528GiGSPN1qb8XtGLgZsJUmg3uJSFIN2lPeSbEwJB3NFjVas/rnQC48i7mV8w==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.186.0.tgz", + "integrity": "sha512-imtOrJFpIZAipAg8VmRqYwv1G/x4xzyoxOJ48ZSn1/ZGnKEEnB6n6E9gwYRebi4mlRuMSVeZwCPLq0ey5hReeQ==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-user-agent-browser": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.110.0.tgz", - "integrity": "sha512-rNdhmHDMV5dNJctqlBWimkZLJRB+x03DB+61pm+SKSFk6gPIVIvc1WNXqDFphkiswT4vA13ZUkGHzt+N4+noQQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.186.0.tgz", + "integrity": "sha512-fbRcTTutMk4YXY3A2LePI4jWSIeHOT8DaYavpc/9Xshz/WH9RTGMmokeVOcClRNBeDSi5cELPJJ7gx6SFD3ZlQ==", "requires": { - "@aws-sdk/types": "3.110.0", + "@aws-sdk/types": "3.186.0", "bowser": "^2.11.0", "tslib": "^2.3.1" } }, "@aws-sdk/util-user-agent-node": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.110.0.tgz", - "integrity": "sha512-OQ915TPCCBwZWz5Np8zkNWn7U6KvrTZfFoCOy/VIemK3dUqmnBZ7HqGpuZx8SwJ2R9JE1x+j0niYSJ5fWJZZKA==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.186.0.tgz", + "integrity": "sha512-oWZR7hN6NtOgnT6fUvHaafgbipQc2xJCRB93XHiF9aZGptGNLJzznIOP7uURdn0bTnF73ejbUXWLQIm8/6ue6w==", "requires": { - "@aws-sdk/node-config-provider": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/node-config-provider": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/util-utf8-browser": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.109.0.tgz", - "integrity": "sha512-FmcGSz0v7Bqpl1SE8G1Gc0CtDpug+rvqNCG/szn86JApD/f5x8oByjbEiAyTU2ZH2VevUntx6EW68ulHyH+x+w==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.186.0.tgz", + "integrity": "sha512-n+IdFYF/4qT2WxhMOCeig8LndDggaYHw3BJJtfIBZRiS16lgwcGYvOUmhCkn0aSlG1f/eyg9YZHQG0iz9eLdHQ==", "requires": { "tslib": "^2.3.1" } }, "@aws-sdk/util-utf8-node": { - "version": "3.109.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.109.0.tgz", - "integrity": "sha512-Ti/ZBdvz2eSTElsucjzNmzpyg2MwfD1rXmxD0hZuIF8bPON/0+sZYnWd5CbDw9kgmhy28dmKue086tbZ1G0iLQ==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.186.0.tgz", + "integrity": "sha512-7qlE0dOVdjuRbZTb7HFywnHHCrsN7AeQiTnsWT63mjXGDbPeUWQQw3TrdI20um3cxZXnKoeudGq8K6zbXyQ4iA==", "requires": { - "@aws-sdk/util-buffer-from": "3.55.0", + "@aws-sdk/util-buffer-from": "3.186.0", "tslib": "^2.3.1" } }, "@aws-sdk/util-waiter": { - "version": "3.110.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.110.0.tgz", - "integrity": "sha512-8dE6W6XYfjk1gx/aeb8NeLfMMLkLFhlV1lmKpFSBJhY8msajU8aQahTuykq5JW8QT/wCGbqbu7dH35SdX7kO+A==", + "version": "3.186.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.186.0.tgz", + "integrity": "sha512-oSm45VadBBWC/K2W1mrRNzm9RzbXt6VopBQ5iTDU7B3qIXlyAG9k1JqOvmYIdYq1oOgjM3Hv2+9sngi3+MZs1A==", "requires": { - "@aws-sdk/abort-controller": "3.110.0", - "@aws-sdk/types": "3.110.0", + "@aws-sdk/abort-controller": "3.186.0", + "@aws-sdk/types": "3.186.0", "tslib": "^2.3.1" } }, "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", "dev": true, "requires": { - "@babel/highlight": "^7.16.7" + "@babel/highlight": "^7.18.6" } }, "@babel/compat-data": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.5.tgz", - "integrity": "sha512-BxhE40PVCBxVEJsSBhB6UWyAuqJRxGsAw8BdHMJ3AKGydcwuWW4kOO3HmqBQAdcq/OP+/DlTVxLvsCzRTnZuGg==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.4.tgz", + "integrity": "sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==", "dev": true }, "@babel/core": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.5.tgz", - "integrity": "sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.18.2", - "@babel/helper-compilation-targets": "^7.18.2", - "@babel/helper-module-transforms": "^7.18.0", - "@babel/helpers": "^7.18.2", - "@babel/parser": "^7.18.5", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.18.5", - "@babel/types": "^7.18.4", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -6232,23 +6399,23 @@ } }, "@babel/generator": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", - "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "version": "7.19.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.5.tgz", + "integrity": "sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==", "dev": true, "requires": { - "@babel/types": "^7.18.2", - "@jridgewell/gen-mapping": "^0.3.0", + "@babel/types": "^7.19.4", + "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", - "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.0", + "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" } @@ -6256,121 +6423,127 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz", - "integrity": "sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ==", + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", + "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", "dev": true, "requires": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", + "@babel/compat-data": "^7.19.3", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", "semver": "^6.3.0" } }, "@babel/helper-environment-visitor": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz", - "integrity": "sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ==", + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", "dev": true }, "@babel/helper-function-name": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", - "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", "dev": true, "requires": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" } }, "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" } }, "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" } }, "@babel/helper-module-transforms": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz", - "integrity": "sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", + "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.18.0", - "@babel/types": "^7.18.0" + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" } }, "@babel/helper-plugin-utils": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz", - "integrity": "sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", + "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", "dev": true }, "@babel/helper-simple-access": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.2.tgz", - "integrity": "sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", + "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", "dev": true, "requires": { - "@babel/types": "^7.18.2" + "@babel/types": "^7.19.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.18.6" } }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", "dev": true }, "@babel/helpers": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.2.tgz", - "integrity": "sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz", + "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==", "dev": true, "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.18.2", - "@babel/types": "^7.18.2" + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.4", + "@babel/types": "^7.19.4" } }, "@babel/highlight": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz", - "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -6434,9 +6607,9 @@ } }, "@babel/parser": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.5.tgz", - "integrity": "sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz", + "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -6548,50 +6721,51 @@ } }, "@babel/plugin-syntax-typescript": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.12.tgz", - "integrity": "sha512-TYY0SXFiO31YXtNg3HtFwNJHjLsAyIIhAhNWkQ5whPPS7HWUFlg9z0Ta4qAQNjQbP1wsSt/oKkmZ/4/WWdMUpw==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", + "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.17.12" + "@babel/helper-plugin-utils": "^7.18.6" } }, "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dev": true, "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" } }, "@babel/traverse": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.5.tgz", - "integrity": "sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.18.2", - "@babel/helper-environment-visitor": "^7.18.2", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.18.5", - "@babel/types": "^7.18.4", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.4.tgz", + "integrity": "sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.4", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.19.4", + "@babel/types": "^7.19.4", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", - "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz", + "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, @@ -6843,31 +7017,31 @@ } }, "@jridgewell/resolve-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", - "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true }, "@jridgewell/set-array": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", - "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "dev": true }, "@jridgewell/sourcemap-codec": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", - "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", - "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "@sinonjs/commons": { @@ -6951,9 +7125,9 @@ } }, "@types/babel__traverse": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.17.1.tgz", - "integrity": "sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", + "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -7042,9 +7216,9 @@ "dev": true }, "acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true }, "acorn-globals": { @@ -7092,14 +7266,12 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -7136,18 +7308,18 @@ "dev": true }, "aws-cdk": { - "version": "2.42.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.42.0.tgz", - "integrity": "sha512-BdkPhkj2PRkGSfsXh7kduUkJg+y234heWOaKzMEiauCt2Bj72wYwZhYG60TAFue7K7ngSjKzUeQ+G7SfKZcudg==", + "version": "2.45.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.45.0.tgz", + "integrity": "sha512-AIug6Ugvtd3I0+U3gTNZtJVDhOgpGpxwWMoOQUlX6xKGwDgQxWrWdq2QWe7ZyKgCRnY9SM90fa+Yxbx+VYk9Bw==", "dev": true, "requires": { "fsevents": "2.3.2" } }, "aws-cdk-lib": { - "version": "2.42.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.42.0.tgz", - "integrity": "sha512-jHHcUm2baKv87L7MO0fktAhmtDZfnHLPsLIktzgW2zCDRNUCFoUPkQ+44eUkZkgmL2sEOSMEQFhbNCxmbW4OSQ==", + "version": "2.45.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.45.0.tgz", + "integrity": "sha512-oEeZZF8xjub9KYAB7n01A60wwQXSzNapmiih3t5uf9aEvlvqT+0as8/WrPdNIeAaf9Lhb0WQXdZ2o2DlsFHbAg==", "requires": { "@balena/dockerignore": "^1.0.2", "case": "1.6.3", @@ -7365,16 +7537,15 @@ "dev": true }, "browserslist": { - "version": "4.20.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz", - "integrity": "sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001349", - "electron-to-chromium": "^1.4.147", - "escalade": "^3.1.1", - "node-releases": "^2.0.5", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" } }, "bs-logger": { @@ -7413,9 +7584,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001355", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz", - "integrity": "sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==", + "version": "1.0.30001418", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", + "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", "dev": true }, "cdk-common": { @@ -7427,7 +7598,7 @@ }, "cdk2-python-library-layer": { "version": "git+ssh://git@github.com/kikuomax/cdk-python-library-layer.git#d1133709d2e9e3833acc4ab097a641568e3ede2b", - "from": "cdk2-python-library-layer@https://github.com/kikuomax/cdk-python-library-layer.git#v0.1.0-v2", + "from": "cdk2-python-library-layer@github:kikuomax/cdk-python-library-layer#v0.1.0-v2", "requires": { "fs-extra": "^10.0.0" } @@ -7449,9 +7620,9 @@ "dev": true }, "ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "cjs-module-lexer": { @@ -7461,13 +7632,12 @@ "dev": true }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, @@ -7487,7 +7657,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -7495,8 +7664,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "combined-stream": { "version": "1.0.8", @@ -7514,18 +7682,15 @@ "dev": true }, "constructs": { - "version": "10.1.106", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.106.tgz", - "integrity": "sha512-xcNB+/5jKk7+9w4pXe5jThpUEDDbhtWLeXlhy9GVdFa/tuasOVEiowZOZMjPvcXrujGgSkVleebo6ZNzvYyZug==" + "version": "10.1.128", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.128.tgz", + "integrity": "sha512-X2QvdedBwVRAqwU5I2Hv+xcB7xumYO/Z+PNozubDIRgtetWRWIOOkZCRXeERm7xfCjLVyQdAPHDKVuoVuGRoHA==" }, "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "create-require": { "version": "1.1.1", @@ -7588,9 +7753,9 @@ } }, "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.1.tgz", + "integrity": "sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw==", "dev": true }, "dedent": { @@ -7653,9 +7818,9 @@ } }, "electron-to-chromium": { - "version": "1.4.158", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.158.tgz", - "integrity": "sha512-gppO3/+Y6sP432HtvwvuU8S+YYYLH4PmAYvQwqUtt9HDOmEsBwQfLnK9T8+1NIKwAS1BEygIjTaATC4H5EzvxQ==", + "version": "1.4.276", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz", + "integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==", "dev": true }, "emittery": { @@ -7667,8 +7832,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "entities": { "version": "2.2.0", @@ -7687,8 +7851,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-string-regexp": { "version": "2.0.0", @@ -7780,9 +7943,9 @@ "integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==" }, "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, "requires": { "bser": "2.1.1" @@ -7826,13 +7989,6 @@ "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" - }, - "dependencies": { - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - } } }, "fs.realpath": { @@ -7863,8 +8019,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-package-type": { "version": "0.1.0", @@ -8008,9 +8163,9 @@ "dev": true }, "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", "dev": true, "requires": { "has": "^1.0.3" @@ -8019,8 +8174,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-generator-fn": { "version": "2.1.0", @@ -8065,9 +8219,9 @@ "dev": true }, "istanbul-lib-instrument": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz", - "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "requires": { "@babel/core": "^7.12.3", @@ -8100,9 +8254,9 @@ } }, "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -8176,6 +8330,34 @@ "jest-validate": "^27.5.1", "prompts": "^2.0.1", "yargs": "^16.2.0" + }, + "dependencies": { + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + } } }, "jest-config": { @@ -8516,9 +8698,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -8675,13 +8857,6 @@ "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" - }, - "dependencies": { - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - } } }, "kleur": { @@ -8831,9 +9006,9 @@ "dev": true }, "node-releases": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", - "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, "normalize-path": { @@ -8852,9 +9027,9 @@ } }, "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", + "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", "dev": true }, "once": { @@ -9018,9 +9193,9 @@ } }, "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, "punycode": { @@ -9029,6 +9204,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -9038,16 +9219,21 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "dev": true, "requires": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -9082,12 +9268,6 @@ "glob": "^7.1.3" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9185,7 +9365,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -9196,7 +9375,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -9229,9 +9407,9 @@ } }, "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", "dev": true, "requires": { "has-flag": "^4.0.0", @@ -9299,14 +9477,23 @@ } }, "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", "dev": true, "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + } } }, "tr46": { @@ -9335,9 +9522,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -9346,9 +9533,9 @@ } }, "ts-node": { - "version": "10.8.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", - "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", @@ -9416,10 +9603,29 @@ "dev": true }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } }, "uuid": { "version": "8.3.2", @@ -9529,7 +9735,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -9555,9 +9760,9 @@ } }, "ws": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", - "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "dev": true, "requires": {} }, @@ -9576,8 +9781,7 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { "version": "4.0.0", @@ -9586,18 +9790,24 @@ "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.0.0" + }, + "dependencies": { + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } } }, "yargs-parser": { diff --git a/cdk-ops/package.json b/cdk-ops/package.json index 0b4c80d..eca57e1 100644 --- a/cdk-ops/package.json +++ b/cdk-ops/package.json @@ -14,19 +14,21 @@ "@types/jest": "^27.5.0", "@types/node": "10.17.27", "@types/prettier": "2.6.0", - "aws-cdk": "^2.42.0", + "aws-cdk": "^2.45.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "ts-node": "^10.7.0", + "ts-node": "^10.9.1", "typescript": "~3.9.7" }, "dependencies": { - "@aws-cdk/aws-lambda-python-alpha": "^2.42.0-alpha.0", - "@aws-sdk/client-cloudformation": "^3.112.0", - "aws-cdk-lib": "^2.42.0", + "@aws-cdk/aws-lambda-python-alpha": "^2.45.0-alpha.0", + "@aws-sdk/client-cloudformation": "^3.186.0", + "@aws-sdk/client-lambda": "^3.186.0", + "aws-cdk-lib": "^2.45.0", "cdk-common": "file:../cdk-common", "cdk2-python-library-layer": "github:kikuomax/cdk-python-library-layer#v0.1.0-v2", - "constructs": "^10.1.106", - "source-map-support": "^0.5.21" + "constructs": "^10.1.128", + "source-map-support": "^0.5.21", + "yargs": "^17.6.0" } } From 6d7ac6092373f1bd874a39b077dde0986080e7d5 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 11 Oct 2022 17:50:46 +0900 Subject: [PATCH 29/41] feat(cdk-ops): add data warehouse population script - Introduces a new script `bin/populate-data-warehouse.js` that populates the database and tables of the data warehouse on a given deployment stage. A new npm script `populate-dw` runs this script. - Tells `git` not to ignore `bin/populate-data-warehouse.js`. issue codemonger-io/codemonger#0 --- cdk-ops/.gitignore | 1 + cdk-ops/bin/populate-data-warehouse.js | 76 ++++++++++++++++++++++++++ cdk-ops/package.json | 3 +- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 cdk-ops/bin/populate-data-warehouse.js diff --git a/cdk-ops/.gitignore b/cdk-ops/.gitignore index 04d61cd..007324c 100644 --- a/cdk-ops/.gitignore +++ b/cdk-ops/.gitignore @@ -1,5 +1,6 @@ *.js !jest.config.js +!/bin/populate-data-warehouse.js *.d.ts node_modules diff --git a/cdk-ops/bin/populate-data-warehouse.js b/cdk-ops/bin/populate-data-warehouse.js new file mode 100644 index 0000000..3572ef4 --- /dev/null +++ b/cdk-ops/bin/populate-data-warehouse.js @@ -0,0 +1,76 @@ +/* Populates the data warehouse. */ + +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); +const { + CloudFormationClient, + DescribeStacksCommand, +} = require('@aws-sdk/client-cloudformation'); +const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda'); + +const CODEMONGER_OPERATIONS_STACK_NAME = 'codemonger-operations'; + +yargs(hideBin(process.argv)) + .command( + '$0 ', + 'populates the data warehouse', + _yargs => { + _yargs.positional('stage', { + describe: 'deployment stage of the data warehouse', + choices: ['development', 'production'], + }); + }, + run, + ) + .help() + .argv; + +async function run({ stage }) { + console.log('obtaining populate function for', stage); + const functionArn = await getPopulateFunctionArn(stage); + console.log('running populate function for', stage); + await runPopulate(functionArn); + console.log('populated the data warehouse for', stage); +} + +// obtains the ARN of the Lambda function that populates the database and +// tables. +async function getPopulateFunctionArn(stage) { + const client = new CloudFormationClient({}); + const command = new DescribeStacksCommand({ + StackName: CODEMONGER_OPERATIONS_STACK_NAME, + }); + const results = await client.send(command); + const outputs = (results.Stacks ?? [])[0]?.Outputs; + if (outputs == null) { + throw new Error( + `please deploy the latest stack ${CODEMONGER_OPERATIONS_STACK_NAME}`, + ); + } + const outputKey = stage === 'production' + ? 'PopulateProductionDwDatabaseLambdaArn' + : 'PopulateDevelopmentDwDatabaseLambdaArn'; + const output = outputs.find(o => o.OutputKey === outputKey); + if (output == null) { + throw new Error( + `please deploy the latest stack ${CODEMONGER_OPERATIONS_STACK_NAME}`, + ); + } + return output.OutputValue; +} + +// runs a given populate function. +async function runPopulate(functionArn) { + const client = new LambdaClient({}); + const command = new InvokeCommand({ + FunctionName: functionArn, + Payload: '{}', + }); + const results = await client.send(command); + if (results.StatusCode !== 200) { + const decoder = new TextDecoder(); + const payload = decoder.decode(results.Payload); + console.error('failed to populate the data warehouse', payload); + throw new Error('failed to populate the data warehouse'); + } +} diff --git a/cdk-ops/package.json b/cdk-ops/package.json index eca57e1..2a63e0d 100644 --- a/cdk-ops/package.json +++ b/cdk-ops/package.json @@ -8,7 +8,8 @@ "build": "tsc", "watch": "tsc -w", "test": "jest", - "cdk": "cdk" + "cdk": "cdk", + "populate-dw": "node bin/populate-data-warehouse.js" }, "devDependencies": { "@types/jest": "^27.5.0", From 27d794b86954b46fd84c1642d3cd11fbd6e468a8 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 11 Oct 2022 18:30:20 +0900 Subject: [PATCH 30/41] feat(cdk-ops): add production data warehouse - `CdkOpsStack` provisions `DataWarehouse` and `AccessLogsETL` for production. - `CodemongerResources` resolves the S3 bucket containing CloudFront access logs for production. issue codemonger-io/codemonger#30 --- cdk-ops/lib/cdk-ops-stack.ts | 25 +++++++++++++++++++++++++ cdk-ops/lib/codemonger-resources.ts | 16 ++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index fdfb98c..7422626 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -52,10 +52,35 @@ export class CdkOpsStack extends Stack { deploymentStage: 'development', }, ); + const productionDataWarehouse = new DataWarehouse( + this, + 'ProductionDataWarehouse', + { + latestBoto3, + libdatawarehouse, + deploymentStage: 'production', + }, + ); + const productionContentsAccessLogsETL = new AccessLogsETL( + this, + 'ProductionContentsAccessLogsETL', + { + accessLogsBucket: + codemongerResources.productionContentsAccessLogsBucket, + dataWarehouse: productionDataWarehouse, + latestBoto3, + libdatawarehouse, + deploymentStage: 'production', + }, + ); // Outputs new CfnOutput(this, 'PopulateDevelopmentDwDatabaseLambdaArn', { description: 'ARN of the Lambda function that populates the data warehouse database and tables (development)', value: developmentDataWarehouse.populateDwDatabaseLambda.functionArn, }); + new CfnOutput(this, 'PopulateProductionDwDatabaseLambdaArn', { + description: 'ARN of the Lambda function that populates the data warehouse database and tables (production)', + value: productionDataWarehouse.populateDwDatabaseLambda.functionArn, + }); } } diff --git a/cdk-ops/lib/codemonger-resources.ts b/cdk-ops/lib/codemonger-resources.ts index b0a9667..a45c90d 100644 --- a/cdk-ops/lib/codemonger-resources.ts +++ b/cdk-ops/lib/codemonger-resources.ts @@ -23,6 +23,8 @@ export type CodemongerResourceNames = { productionContentsBucketName: string; /** Name of the S3 bucket of CloudFront access logs for development. */ developmentContentsAccessLogsBucketName: string; + /** Name of the S3 bucket of CloudFront access logs for production. */ + productionContentsAccessLogsBucketName: string; }; /** @@ -57,11 +59,17 @@ export async function resolveCodemongerResourceNames(): if (developmentContentsAccessLogsBucketName == null) { throw new Error('access logs bucket for development is not available'); } + const productionContentsAccessLogsBucketName = + productionOutputs.get('ContentsAccessLogsBucketName'); + if (productionContentsAccessLogsBucketName == null) { + throw new Error('access logs bucket for production is not available'); + } return { developmentContentsBucketName, developmentDistributionDomainName, productionContentsBucketName, developmentContentsAccessLogsBucketName, + productionContentsAccessLogsBucketName, }; } @@ -114,6 +122,8 @@ export class CodemongerResources extends Construct { readonly productionDomainName = CODEMONGER_DOMAIN_NAME; /** S3 bucket of CloudFront access logs for development. */ readonly developmentContentsAccessLogsBucket: s3.IBucket; + /** S3 bucket of CloudFront access logs for development. */ + readonly productionContentsAccessLogsBucket: s3.IBucket; constructor( scope: Construct, @@ -126,6 +136,7 @@ export class CodemongerResources extends Construct { developmentContentsAccessLogsBucketName, developmentContentsBucketName, developmentDistributionDomainName, + productionContentsAccessLogsBucketName, productionContentsBucketName, } = resourceNames; @@ -145,5 +156,10 @@ export class CodemongerResources extends Construct { 'DevelopmentContentsAccessLogsBucket', developmentContentsAccessLogsBucketName, ); + this.productionContentsAccessLogsBucket = s3.Bucket.fromBucketName( + this, + 'ProductionContentsAccessLogsBucket', + productionContentsAccessLogsBucketName, + ); } } From 00f8ea793d7bcf9a52ad7429f2ca1c72802cd5b5 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Fri, 14 Oct 2022 10:12:04 +0900 Subject: [PATCH 31/41] fix(cdk-ops): intuitive timeout - Fixes the bug that `libdatawarehouse.data_api.wait_for_results` did not time out. It now monitors the elapsed time rather than the loop count. It also takes a new parameter `cancel_at_timeout` that makes the function cancel the statement when it times out. It is `True` by default. issue codemonger-io/codemonger#30 --- .../src/libdatawarehouse/data_api.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/data_api.py b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/data_api.py index 095d669..826ea90 100644 --- a/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/data_api.py +++ b/cdk-ops/lambda/libdatawarehouse/src/libdatawarehouse/data_api.py @@ -14,7 +14,8 @@ def wait_for_results( client, statement_id: str, polling_interval: float = 0.05, - polling_timeout: int = round(300 / 0.05), + timeout: float = 300.0, + cancel_at_timeout: bool = True, ) -> Tuple[Optional[str], Dict]: """Waits for a given statement to finish. @@ -23,15 +24,20 @@ def wait_for_results( :param float polling_interval: interval in seconds between two consecutive pollings. - :param int polling_timeout: timeout represented as the number of pollings. + :param float timeout: timeout in seconds. + + :param bool cancel_at_timeout: whether cancels the statement when it times + out. """ - polling_counter = 0 + start_time = time.time() while True: res = client.describe_statement(Id=statement_id) status = res['Status'] if status not in RUNNING_STATUSES: return status, res - polling_counter += 1 - if polling_counter >= polling_timeout: + elapsed = time.time() - start_time + if elapsed >= timeout: + if cancel_at_timeout: + client.cancel_statement(Id=statement_id) return None, res time.sleep(polling_interval) From 284ae05de575ca0d87fea6f30ea1fb80c3758b2a Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Fri, 14 Oct 2022 10:16:22 +0900 Subject: [PATCH 32/41] feat(cdk-ops): enable enhanced VPC routing - Turns on `enhancedVpcRouting` of the Redshift Serverless workgroup for development. To avoid an accidental update of the production database, creates a branch of `DataWarehouse` as `DataWarehouseV2` for development. issue codemonger-io/codemonger#30 --- cdk-ops/lib/cdk-ops-stack.ts | 3 +- cdk-ops/lib/data-warehouse-v2.ts | 278 +++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 cdk-ops/lib/data-warehouse-v2.ts diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index 7422626..b9470b0 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -8,6 +8,7 @@ import { } from './codemonger-resources'; import { ContentsPipeline } from './contents-pipeline'; import { DataWarehouse } from './data-warehouse'; +import { DataWarehouseV2 } from './data-warehouse-v2'; import { LatestBoto3Layer } from './latest-boto3-layer'; import { LibdatawarehouseLayer } from './libdatawarehouse-layer'; @@ -31,7 +32,7 @@ export class CdkOpsStack extends Stack { const pipeline = new ContentsPipeline(this, 'ContentsPipeline', { codemongerResources, }); - const developmentDataWarehouse = new DataWarehouse( + const developmentDataWarehouse = new DataWarehouseV2( this, 'DevelopmentDataWarehouse', { diff --git a/cdk-ops/lib/data-warehouse-v2.ts b/cdk-ops/lib/data-warehouse-v2.ts new file mode 100644 index 0000000..984b3d7 --- /dev/null +++ b/cdk-ops/lib/data-warehouse-v2.ts @@ -0,0 +1,278 @@ +import * as path from 'path'; + +import { + Arn, + Duration, + Stack, + aws_ec2 as ec2, + aws_iam as iam, + aws_lambda as lambda, + aws_redshiftserverless as redshift, + aws_secretsmanager as secrets, + aws_stepfunctions as sfn, + aws_stepfunctions_tasks as sfn_tasks, +} from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; + +import type { DeploymentStage } from 'cdk-common'; + +import { LatestBoto3Layer } from './latest-boto3-layer'; +import { LibdatawarehouseLayer } from './libdatawarehouse-layer'; + +/** Name of the admin user. */ +export const ADMIN_USER_NAME = 'dwadmin'; + +/** Subnet group name of the cluster for Redshift Serverless. */ +export const CLUSTER_SUBNET_GROUP_NAME = 'dw-cluster'; + +export interface Props { + /** Lambda layer containing the latest boto3. */ + latestBoto3: LatestBoto3Layer; + /** Lambda layer containing libdatawarehouse. */ + libdatawarehouse: LibdatawarehouseLayer; + /** Deployment stage. */ + deploymentStage: DeploymentStage; +} + +/** Provisions resources for the data warehouse. */ +export class DataWarehouseV2 extends Construct { + /** VPC for Redshift Serverless clusters. */ + readonly vpc: ec2.IVpc; + // TODO: unnecessary exposure of `adminSecret` + /** Secret for the admin user. */ + readonly adminSecret: secrets.ISecret; + /** Default IAM role associated with the Redshift Serverless namespace. */ + readonly namespaceRole: iam.IRole; + /** Name of the Redshift Serverless workgroup. */ + readonly workgroupName: string; + /** Redshift Serverless workgroup. */ + readonly workgroup: redshift.CfnWorkgroup; + /** Lambda function to populate the database and tables. */ + readonly populateDwDatabaseLambda: lambda.IFunction; + /** Step Functions to run VACUUM over tables. */ + readonly vacuumWorkflow: sfn.IStateMachine; + + constructor(scope: Construct, id: string, props: Props) { + super(scope, id); + + const { deploymentStage, latestBoto3, libdatawarehouse } = props; + + this.vpc = new ec2.Vpc(this, `DwVpc`, { + cidr: '192.168.0.0/16', + enableDnsSupport: true, + enableDnsHostnames: true, + subnetConfiguration: [ + { + name: CLUSTER_SUBNET_GROUP_NAME, + subnetType: ec2.SubnetType.PRIVATE_ISOLATED, + // to reserve private addresses for the future + // allocates up to 1024 private addresses in each subnet + cidrMask: 22, + }, + ], + gatewayEndpoints: { + S3: { + service: ec2.GatewayVpcEndpointAwsService.S3, + }, + }, + }); + + // provisions Redshift Serverless resources + // - secret for admin + this.adminSecret = new secrets.Secret(this, 'DwAdminSecret', { + description: `Data Warehouse secret (${deploymentStage})`, + generateSecretString: { + // the following requirement is too strict, but should not matter. + excludePunctuation: true, + // the structure of a secret value for Redshift is described below + // https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html#reference_secret_json_structure_RS + // + // whether it also works with Redshift Serverless is unclear. + // as far as I tested, only "username" and "password" are required. + secretStringTemplate: JSON.stringify({ + username: ADMIN_USER_NAME, + }), + generateStringKey: 'password', + }, + }); + // - IAM role for the namespace + this.namespaceRole = new iam.Role(this, 'DwNamespaceRole', { + description: `Data Warehouse Role (${deploymentStage})`, + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('redshift-serverless.amazonaws.com'), + new iam.ServicePrincipal('redshift.amazonaws.com'), + ), + }); + // - namespace + const dwNamespace = new redshift.CfnNamespace(this, 'DwNamespace', { + namespaceName: `datawarehouse-${deploymentStage}`, + adminUsername: ADMIN_USER_NAME, + adminUserPassword: + this.adminSecret.secretValueFromJson('password').unsafeUnwrap(), + defaultIamRoleArn: this.namespaceRole.roleArn, + iamRoles: [this.namespaceRole.roleArn], + tags: [ + { + key: 'project', + value: 'codemonger', + }, + { + key: 'stage', + value: deploymentStage, + }, + ], + }); + dwNamespace.addDependsOn( + this.adminSecret.node.defaultChild as secrets.CfnSecret, + ); + // - workgroup + this.workgroupName = `datawarehouse-${deploymentStage}`; + this.workgroup = new redshift.CfnWorkgroup(this, 'DwWorkgroup', { + workgroupName: this.workgroupName, + namespaceName: dwNamespace.namespaceName, + baseCapacity: 32, + subnetIds: this.getSubnetIdsForCluster(), + enhancedVpcRouting: true, + tags: [ + { + key: 'project', + value: 'codemonger', + }, + { + key: 'stage', + value: deploymentStage, + }, + ], + }); + this.workgroup.addDependsOn(dwNamespace); + + // Lambda function that populates the database and tables. + this.populateDwDatabaseLambda = new PythonFunction( + this, + 'PopulateDwDatabaseLambda', + { + description: `Populates the data warehouse database and tables (${deploymentStage})`, + runtime: lambda.Runtime.PYTHON_3_8, + architecture: lambda.Architecture.ARM_64, + entry: path.join('lambda', 'populate-dw-database'), + index: 'index.py', + handler: 'lambda_handler', + layers: [latestBoto3.layer, libdatawarehouse.layer], + environment: { + WORKGROUP_NAME: this.workgroupName, + ADMIN_SECRET_ARN: this.adminSecret.secretArn, + ADMIN_DATABASE_NAME: 'dev', + }, + timeout: Duration.minutes(15), + // a Lambda function does not have to join the VPC + // as long as it uses Redshift Data API. + // + // if we want to directly connect to the Redshift cluster from a Lambda, + // we have to put the Lambda in the VPC and allocate a VPC endpoint. + // but I cannot afford VPC endpoints for now. + // + // alternatively, we could run the Redshift cluster in a public subnet. + }, + ); + // Redshift Data API uses the execution role of the Lambda function to + // retrieve the secret. + this.adminSecret.grantRead(this.populateDwDatabaseLambda); + // TODO: too permissive? + this.populateDwDatabaseLambda.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonRedshiftDataFullAccess')); + + // Step Functions that perform VACUUM over tables. + // - Lambda function that runs VACUUM over a given table + const vacuumTableLambda = new PythonFunction(this, 'VacuumTableLambda', { + description: `Runs VACUUM over a table (${deploymentStage})`, + runtime: lambda.Runtime.PYTHON_3_8, + architecture: lambda.Architecture.ARM_64, + entry: path.join('lambda', 'vacuum-table'), + index: 'index.py', + handler: 'lambda_handler', + layers: [latestBoto3.layer, libdatawarehouse.layer], + environment: { + WORKGROUP_NAME: this.workgroupName, + ADMIN_SECRET_ARN: this.adminSecret.secretArn, + }, + timeout: Duration.minutes(15), + }); + this.adminSecret.grantRead(vacuumTableLambda); + this.grantQuery(vacuumTableLambda); + // - state machine + // - lists table names + const listTableNamesState = new sfn.Pass(this, 'ListTables', { + comment: 'Lists table names', + result: sfn.Result.fromArray([ + 'access_log', + 'referer', + 'page', + 'edge_location', + 'user_agent', + 'result_type', + ]), + resultPath: '$.tables', + // produces something like + // { + // mode: 'SORT ONLY', + // tableNames: ['access_log', ...] + // } + }); + this.vacuumWorkflow = new sfn.StateMachine(this, 'VacuumWorkflow', { + definition: + listTableNamesState.next( + new sfn.Map(this, 'MapTables', { + comment: 'Iterates over tables', + maxConcurrency: 1, // sequential + itemsPath: '$.tables', + parameters: { + 'tableName.$': '$$.Map.Item.Value', + 'mode.$': '$.mode', + }, + }).iterator( + new sfn_tasks.LambdaInvoke(this, 'VacuumTable', { + lambdaFunction: vacuumTableLambda, + }), + ), + ), + timeout: Duration.hours(1), + }); + } + + /** Returns subnet IDs for the cluster of Redshift Serverless. */ + getSubnetIdsForCluster(): string[] { + return this.vpc.selectSubnets({ + subnetGroupName: CLUSTER_SUBNET_GROUP_NAME, + }).subnetIds; + } + + /** + * Grants permissions to query this data warehouse via the Redshift Data API. + * + * Allows `grantee` to call `redshift-serverless:GetCredentials`. + */ + grantQuery(grantee: iam.IGrantable): iam.Grant { + iam.Grant + .addToPrincipal({ + grantee, + actions: ['redshift-serverless:GetCredentials'], + resourceArns: [ + // TODO: how can we get the ARN of the workgroup? + Arn.format( + { + service: 'redshift-serverless', + resource: 'workgroup', + resourceName: '*', + }, + Stack.of(this.workgroup), + ), + ], + }) + .assertSuccess(); + return iam.Grant.addToPrincipal({ + grantee, + actions: ['redshift-data:*'], + resourceArns: ['*'], + }); + } +} From 00ab4f6d67b2d321c5b127de93edd5e883887262 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Sun, 16 Oct 2022 02:01:16 +0900 Subject: [PATCH 33/41] fix(cdk-ops): NULL user_agent - Fixes the bug that `lambda/load-access-logs` crashed when `user_agent` was `NULL`. Replaces `NULL` with a dash (`-`). issue codemonger-io/codemonger#30 --- cdk-ops/lambda/load-access-logs/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdk-ops/lambda/load-access-logs/index.py b/cdk-ops/lambda/load-access-logs/index.py index 56d9eff..98bcfa1 100644 --- a/cdk-ops/lambda/load-access-logs/index.py +++ b/cdk-ops/lambda/load-access-logs/index.py @@ -203,7 +203,7 @@ def get_create_access_log_stage_table_statement() -> str: ' cs_uri_stem,', ' status,', " CASE WHEN referer IS NULL THEN '-' ELSE referer END,", - ' user_agent,', + " CASE WHEN user_agent IS NULL THEN '-' ELSE user_agent END,", ' cs_protocol,', ' cs_bytes,', ' time_taken,', From 39d177945bf57d4f66573f76a8e3d21f4d2201dd Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 18 Oct 2022 18:56:29 +0900 Subject: [PATCH 34/41] feat(cdk-ops): enable enhancedVpcRouting - Enables `enhancedVpcRouting` of the Redshift Serverless workgroup for production. Merges the changes on `DataWarehouseV2` back to `DataWarehouse`. `DataWarehouseV2` is no longer used. issue codemonger-io/codemonger#30 --- cdk-ops/lib/cdk-ops-stack.ts | 3 +- cdk-ops/lib/data-warehouse-v2.ts | 278 ------------------------------- cdk-ops/lib/data-warehouse.ts | 10 +- 3 files changed, 9 insertions(+), 282 deletions(-) delete mode 100644 cdk-ops/lib/data-warehouse-v2.ts diff --git a/cdk-ops/lib/cdk-ops-stack.ts b/cdk-ops/lib/cdk-ops-stack.ts index b9470b0..7422626 100644 --- a/cdk-ops/lib/cdk-ops-stack.ts +++ b/cdk-ops/lib/cdk-ops-stack.ts @@ -8,7 +8,6 @@ import { } from './codemonger-resources'; import { ContentsPipeline } from './contents-pipeline'; import { DataWarehouse } from './data-warehouse'; -import { DataWarehouseV2 } from './data-warehouse-v2'; import { LatestBoto3Layer } from './latest-boto3-layer'; import { LibdatawarehouseLayer } from './libdatawarehouse-layer'; @@ -32,7 +31,7 @@ export class CdkOpsStack extends Stack { const pipeline = new ContentsPipeline(this, 'ContentsPipeline', { codemongerResources, }); - const developmentDataWarehouse = new DataWarehouseV2( + const developmentDataWarehouse = new DataWarehouse( this, 'DevelopmentDataWarehouse', { diff --git a/cdk-ops/lib/data-warehouse-v2.ts b/cdk-ops/lib/data-warehouse-v2.ts deleted file mode 100644 index 984b3d7..0000000 --- a/cdk-ops/lib/data-warehouse-v2.ts +++ /dev/null @@ -1,278 +0,0 @@ -import * as path from 'path'; - -import { - Arn, - Duration, - Stack, - aws_ec2 as ec2, - aws_iam as iam, - aws_lambda as lambda, - aws_redshiftserverless as redshift, - aws_secretsmanager as secrets, - aws_stepfunctions as sfn, - aws_stepfunctions_tasks as sfn_tasks, -} from 'aws-cdk-lib'; -import { Construct } from 'constructs'; -import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; - -import type { DeploymentStage } from 'cdk-common'; - -import { LatestBoto3Layer } from './latest-boto3-layer'; -import { LibdatawarehouseLayer } from './libdatawarehouse-layer'; - -/** Name of the admin user. */ -export const ADMIN_USER_NAME = 'dwadmin'; - -/** Subnet group name of the cluster for Redshift Serverless. */ -export const CLUSTER_SUBNET_GROUP_NAME = 'dw-cluster'; - -export interface Props { - /** Lambda layer containing the latest boto3. */ - latestBoto3: LatestBoto3Layer; - /** Lambda layer containing libdatawarehouse. */ - libdatawarehouse: LibdatawarehouseLayer; - /** Deployment stage. */ - deploymentStage: DeploymentStage; -} - -/** Provisions resources for the data warehouse. */ -export class DataWarehouseV2 extends Construct { - /** VPC for Redshift Serverless clusters. */ - readonly vpc: ec2.IVpc; - // TODO: unnecessary exposure of `adminSecret` - /** Secret for the admin user. */ - readonly adminSecret: secrets.ISecret; - /** Default IAM role associated with the Redshift Serverless namespace. */ - readonly namespaceRole: iam.IRole; - /** Name of the Redshift Serverless workgroup. */ - readonly workgroupName: string; - /** Redshift Serverless workgroup. */ - readonly workgroup: redshift.CfnWorkgroup; - /** Lambda function to populate the database and tables. */ - readonly populateDwDatabaseLambda: lambda.IFunction; - /** Step Functions to run VACUUM over tables. */ - readonly vacuumWorkflow: sfn.IStateMachine; - - constructor(scope: Construct, id: string, props: Props) { - super(scope, id); - - const { deploymentStage, latestBoto3, libdatawarehouse } = props; - - this.vpc = new ec2.Vpc(this, `DwVpc`, { - cidr: '192.168.0.0/16', - enableDnsSupport: true, - enableDnsHostnames: true, - subnetConfiguration: [ - { - name: CLUSTER_SUBNET_GROUP_NAME, - subnetType: ec2.SubnetType.PRIVATE_ISOLATED, - // to reserve private addresses for the future - // allocates up to 1024 private addresses in each subnet - cidrMask: 22, - }, - ], - gatewayEndpoints: { - S3: { - service: ec2.GatewayVpcEndpointAwsService.S3, - }, - }, - }); - - // provisions Redshift Serverless resources - // - secret for admin - this.adminSecret = new secrets.Secret(this, 'DwAdminSecret', { - description: `Data Warehouse secret (${deploymentStage})`, - generateSecretString: { - // the following requirement is too strict, but should not matter. - excludePunctuation: true, - // the structure of a secret value for Redshift is described below - // https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html#reference_secret_json_structure_RS - // - // whether it also works with Redshift Serverless is unclear. - // as far as I tested, only "username" and "password" are required. - secretStringTemplate: JSON.stringify({ - username: ADMIN_USER_NAME, - }), - generateStringKey: 'password', - }, - }); - // - IAM role for the namespace - this.namespaceRole = new iam.Role(this, 'DwNamespaceRole', { - description: `Data Warehouse Role (${deploymentStage})`, - assumedBy: new iam.CompositePrincipal( - new iam.ServicePrincipal('redshift-serverless.amazonaws.com'), - new iam.ServicePrincipal('redshift.amazonaws.com'), - ), - }); - // - namespace - const dwNamespace = new redshift.CfnNamespace(this, 'DwNamespace', { - namespaceName: `datawarehouse-${deploymentStage}`, - adminUsername: ADMIN_USER_NAME, - adminUserPassword: - this.adminSecret.secretValueFromJson('password').unsafeUnwrap(), - defaultIamRoleArn: this.namespaceRole.roleArn, - iamRoles: [this.namespaceRole.roleArn], - tags: [ - { - key: 'project', - value: 'codemonger', - }, - { - key: 'stage', - value: deploymentStage, - }, - ], - }); - dwNamespace.addDependsOn( - this.adminSecret.node.defaultChild as secrets.CfnSecret, - ); - // - workgroup - this.workgroupName = `datawarehouse-${deploymentStage}`; - this.workgroup = new redshift.CfnWorkgroup(this, 'DwWorkgroup', { - workgroupName: this.workgroupName, - namespaceName: dwNamespace.namespaceName, - baseCapacity: 32, - subnetIds: this.getSubnetIdsForCluster(), - enhancedVpcRouting: true, - tags: [ - { - key: 'project', - value: 'codemonger', - }, - { - key: 'stage', - value: deploymentStage, - }, - ], - }); - this.workgroup.addDependsOn(dwNamespace); - - // Lambda function that populates the database and tables. - this.populateDwDatabaseLambda = new PythonFunction( - this, - 'PopulateDwDatabaseLambda', - { - description: `Populates the data warehouse database and tables (${deploymentStage})`, - runtime: lambda.Runtime.PYTHON_3_8, - architecture: lambda.Architecture.ARM_64, - entry: path.join('lambda', 'populate-dw-database'), - index: 'index.py', - handler: 'lambda_handler', - layers: [latestBoto3.layer, libdatawarehouse.layer], - environment: { - WORKGROUP_NAME: this.workgroupName, - ADMIN_SECRET_ARN: this.adminSecret.secretArn, - ADMIN_DATABASE_NAME: 'dev', - }, - timeout: Duration.minutes(15), - // a Lambda function does not have to join the VPC - // as long as it uses Redshift Data API. - // - // if we want to directly connect to the Redshift cluster from a Lambda, - // we have to put the Lambda in the VPC and allocate a VPC endpoint. - // but I cannot afford VPC endpoints for now. - // - // alternatively, we could run the Redshift cluster in a public subnet. - }, - ); - // Redshift Data API uses the execution role of the Lambda function to - // retrieve the secret. - this.adminSecret.grantRead(this.populateDwDatabaseLambda); - // TODO: too permissive? - this.populateDwDatabaseLambda.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonRedshiftDataFullAccess')); - - // Step Functions that perform VACUUM over tables. - // - Lambda function that runs VACUUM over a given table - const vacuumTableLambda = new PythonFunction(this, 'VacuumTableLambda', { - description: `Runs VACUUM over a table (${deploymentStage})`, - runtime: lambda.Runtime.PYTHON_3_8, - architecture: lambda.Architecture.ARM_64, - entry: path.join('lambda', 'vacuum-table'), - index: 'index.py', - handler: 'lambda_handler', - layers: [latestBoto3.layer, libdatawarehouse.layer], - environment: { - WORKGROUP_NAME: this.workgroupName, - ADMIN_SECRET_ARN: this.adminSecret.secretArn, - }, - timeout: Duration.minutes(15), - }); - this.adminSecret.grantRead(vacuumTableLambda); - this.grantQuery(vacuumTableLambda); - // - state machine - // - lists table names - const listTableNamesState = new sfn.Pass(this, 'ListTables', { - comment: 'Lists table names', - result: sfn.Result.fromArray([ - 'access_log', - 'referer', - 'page', - 'edge_location', - 'user_agent', - 'result_type', - ]), - resultPath: '$.tables', - // produces something like - // { - // mode: 'SORT ONLY', - // tableNames: ['access_log', ...] - // } - }); - this.vacuumWorkflow = new sfn.StateMachine(this, 'VacuumWorkflow', { - definition: - listTableNamesState.next( - new sfn.Map(this, 'MapTables', { - comment: 'Iterates over tables', - maxConcurrency: 1, // sequential - itemsPath: '$.tables', - parameters: { - 'tableName.$': '$$.Map.Item.Value', - 'mode.$': '$.mode', - }, - }).iterator( - new sfn_tasks.LambdaInvoke(this, 'VacuumTable', { - lambdaFunction: vacuumTableLambda, - }), - ), - ), - timeout: Duration.hours(1), - }); - } - - /** Returns subnet IDs for the cluster of Redshift Serverless. */ - getSubnetIdsForCluster(): string[] { - return this.vpc.selectSubnets({ - subnetGroupName: CLUSTER_SUBNET_GROUP_NAME, - }).subnetIds; - } - - /** - * Grants permissions to query this data warehouse via the Redshift Data API. - * - * Allows `grantee` to call `redshift-serverless:GetCredentials`. - */ - grantQuery(grantee: iam.IGrantable): iam.Grant { - iam.Grant - .addToPrincipal({ - grantee, - actions: ['redshift-serverless:GetCredentials'], - resourceArns: [ - // TODO: how can we get the ARN of the workgroup? - Arn.format( - { - service: 'redshift-serverless', - resource: 'workgroup', - resourceName: '*', - }, - Stack.of(this.workgroup), - ), - ], - }) - .assertSuccess(); - return iam.Grant.addToPrincipal({ - grantee, - actions: ['redshift-data:*'], - resourceArns: ['*'], - }); - } -} diff --git a/cdk-ops/lib/data-warehouse.ts b/cdk-ops/lib/data-warehouse.ts index 8017190..e2af1f7 100644 --- a/cdk-ops/lib/data-warehouse.ts +++ b/cdk-ops/lib/data-warehouse.ts @@ -60,8 +60,8 @@ export class DataWarehouse extends Construct { this.vpc = new ec2.Vpc(this, `DwVpc`, { cidr: '192.168.0.0/16', - enableDnsSupport: false, - enableDnsHostnames: false, + enableDnsSupport: true, + enableDnsHostnames: true, subnetConfiguration: [ { name: CLUSTER_SUBNET_GROUP_NAME, @@ -71,6 +71,11 @@ export class DataWarehouse extends Construct { cidrMask: 22, }, ], + gatewayEndpoints: { + S3: { + service: ec2.GatewayVpcEndpointAwsService.S3, + }, + }, }); // provisions Redshift Serverless resources @@ -128,6 +133,7 @@ export class DataWarehouse extends Construct { namespaceName: dwNamespace.namespaceName, baseCapacity: 32, subnetIds: this.getSubnetIdsForCluster(), + enhancedVpcRouting: true, tags: [ { key: 'project', From 7973ee57697a9bbe1469f626780c19fc8d80faff Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Tue, 18 Oct 2022 19:01:26 +0900 Subject: [PATCH 35/41] docs(cdk-ops): add data warehouse architecture - Adds the explanation about the AWS architecture of the data warehouse. `docs/data-warehouse-aws-architecture.drawio` is the original data file of `draw.io`: https://github.com/jgraph/drawio issue codemonger-io/codemonger#30 --- .../data-warehouse-aws-architecture.drawio | 1 + .../docs/data-warehouse-aws-architecture.png | Bin 0 -> 148865 bytes cdk-ops/docs/data-warehouse.md | 121 ++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 cdk-ops/docs/data-warehouse-aws-architecture.drawio create mode 100644 cdk-ops/docs/data-warehouse-aws-architecture.png create mode 100644 cdk-ops/docs/data-warehouse.md diff --git a/cdk-ops/docs/data-warehouse-aws-architecture.drawio b/cdk-ops/docs/data-warehouse-aws-architecture.drawio new file mode 100644 index 0000000..5b44d6c --- /dev/null +++ b/cdk-ops/docs/data-warehouse-aws-architecture.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/cdk-ops/docs/data-warehouse-aws-architecture.png b/cdk-ops/docs/data-warehouse-aws-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..735dd9b38f0ba07e270211478e7b225e1b86080e GIT binary patch literal 148865 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 VLl3Qx@Hy~DMpE&9p7=xW{{zO7B#i(7 literal 0 HcmV?d00001 diff --git a/cdk-ops/docs/data-warehouse.md b/cdk-ops/docs/data-warehouse.md new file mode 100644 index 0000000..1d82924 --- /dev/null +++ b/cdk-ops/docs/data-warehouse.md @@ -0,0 +1,121 @@ +English / 日本語 + +# Data warehouse for access logs + +This CDK stack provisions a data warehouse for access logs. +The data warehouse is backed by [Amazon Redshift Serverless](https://aws.amazon.com/redshift/redshift-serverless/). + +## AWS architecture + +The following diagram shows the AWS architecture of the data warehouse. + +![AWS architecture for data warehouse](./data-warehouse-aws-architecture.png) + +### Amazon CloudFront + +`Amazon CloudFront` distributes the contents of our website and saves access logs in [`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket). + +### Amazon S3 access log bucket + +`Amazon S3 access log bucket` is an [Amazon S3 (S3)](https://aws.amazon.com/s3/) bucket that stores access logs created by [`Amazon CloudFront`](#amazon-cloudfront). +This bucket sends an event to [`MaskAccessLogs queue`](#maskaccesslogs-queue) when an access logs file is PUT into this bucket. + +### MaskAccessLogs queue + +`MaskAccessLogs queue` is an [Amazon Simple Queue Service (SQS)](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html) queue that invokes [`MaskAccessLogs`](#maskaccesslogs). +[`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket) sends an event to this queue when an access logs file is PUT into it. + +### MaskAccessLogs + +`MaskAccessLogs` is an [AWS Lambda (Lambda)](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) function that transforms access logs in [`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket). +This function masks IP addresses, `c-ip` and `x-forwarded-for`, in the [CloudFront access logs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#LogFileFormat). +This function also introduces a new column of row numbers to retain the order of the access log records. +This function saves transformed results in [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). +While [`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket) flat-spreads access logs files, this function creates a folder hierarchy corresponding to the year, month, and day of access log records. +This folder structure helps [`LoadAccessLogs`](#loadaccesslogs) to process access logs on a specific date in a batch. + +### Amazon S3 transformed log bucket + +`Amazon S3 transformed log bucket` is an S3 bucket that stores access logs transformed by [`MaskAccessLogs`](#maskaccesslogs). +This bucket sends an event to [`DeleteAccessLogs queue`](#deleteaccesslogs-queue) when a transformed access logs file is PUT into this bucket. + +### DeleteAccessLogs queue + +`DeleteAccessLogs queue` is an SQS queue that invokes [`DeleteAccessLogs`](#deleteaccesslogs). +[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket) sends an event to this queue when a transformed access logs file is PUT into it. + +### DeleteAccessLogs + +`DeleteAccessLogs` is a Lambda function that deletes an access logs file in [`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket), which has been transformed by [`MaskAccessLogs`](#maskaccesslogs) and saved in [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). + +### Amazon Redshift Serverless + +`Amazon Redshift Serverless` is a bundle of [Amazon Redshift Serverless](https://aws.amazon.com/redshift/) resources, which is the core of the data warehouse. +It has one [fact table](https://en.wikipedia.org/wiki/Fact_table), +- `access_log` + +and five [dimension tables](https://en.wikipedia.org/wiki/Dimension_(data_warehouse)), +- `referer` +- `page` +- `edge_location` +- `user_agent` +- `result_type` + +Nodes of `Amazon Redshift Serverless` reside in a private subnet. +Lambda functions, [`PopulateDwDatabase`](#populatedwdatabase), [`LoadAccessLogs`](#loadaccesslogs), and [`VacuumTable`](#vacuumtable) operate `Amazon Redshift Serverless` via [`Amazon Redshift Data API`](#amazon-redshift-data-api). + +The default role of the Amazon Redshift Serverless namespace ([`Redshift namespace role`](#redshift-namespace-role)) can read object from [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). +`Amazon Redshift Serverless` accesses [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket) through [`Gateway endpoint`](#gateway-endpoint). + +This CDK stack creates an admin user when it provisions `Amazon Redshift Serverless`. +[`AWS Secrets Manager`](#aws-secrets-manager) generates and manages the password of the admin user. + +### Redshift namespace role + +`Redshift namespace role` is an [AWS Identity and Access Management (IAM)](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) role that is the default role of the namespace of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) and can read objects from [`Access S3 transformed log bucket`](#access-s3-transformed-log-bucket). + +### Gateway endpoint + +`Gateway endpoint` ensures traffic between [`Amazon Redshift Serverless`](#amazon-redshift-serverless) and [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket) never goes through the Internet. +Please refer to ["Enhanced VPC routing in Amazon Redshift" - *Amazon Redshift Management Guide*](https://docs.aws.amazon.com/redshift/latest/mgmt/enhanced-vpc-routing.html) for more details. + +### AWS Secrets Manager + +`AWS Secrets Manager` generates and manages the password of the admin user of [`Amazon Redshift Serverless`](#amazon-redshift-serverless). +Please refer to [*AWS Secrets Manager User Guide*](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html). + +Unfortunately, the secret managed by `AWS Secrets Manager` does not sync with the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) except for the first time it is generated. +So you have to manually reset the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless), in case `AWS Secrets Manager` generates a new secret. + +### Amazon Redshift Data API + +`Amazon Redshift Data API` relieves clients of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) of managing connections to the database. +Please refer to ["Using the Amazon Redshift Data API" - *Amazon Redshift Management Guide*](https://docs.aws.amazon.com/redshift/latest/mgmt/data-api.html) for more details. + +### PopulateDwDatabase + +`PopulateDwDatabase` is a Lambda function that populates the database and tables to store access logs on [`Amazon Redshift Serverless`](#amazon-redshift-serverless). +This function obtains the admin credentials of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) from [`AWS Secrets Manager`](#aws-secrets-manager). +The administrator (`Admin`) has to run this function after deploying this CDK stack. + +### Amazon EventBridge + +`Amazon EventBridge` defines an [Amazon EventBridge rule](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html) that runs [`LoadAccessLogs`](#loadaccesslogs) every 2 AM in UTC. + +### LoadAccessLogs + +`LoadAccessLogs` is a Lambda function that loads access logs on a specific date onto [`Amazon Redshift Serverless`](#amazon-redshift-serverless). +This function executes [`AWS Step Functions`](#aws-step-functions) after loading access logs finishes. +[`Amazon EventBridge`](#amazon-eventbridge) runs this function every day. + +While this function is intended to be invoked by [`Amazon EventBridge`](#amazon-eventbridge), you can also manually run this function with a proper payload. + +### AWS Step Functions + +`AWS Step Functions` defines an [AWS Step Functions state machine](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html) that runs [`VacuumTable`](#vacuumtable) over every table on [`Amazon Redshift Serverless`](#amazon-redshift-serverless); `access_log`, `referer`, `page`, `edge_location`, `user_agent`, and `result_type`. +Since only a single execution of the [`VACUUM` SQL command](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html) is allowed at once, `AWS Step Functions` processes tables with [`VacuumTable`](#vacuumtable) one by one. + +### VacuumTable + +`VacuumTable` is a Lambda function that runs the [`VACUUM` SQL command](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html) over a specified table. +This function obtains the admin credentials of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) from [`AWS Secrets Manager`](#aws-secrets-manager). \ No newline at end of file From 4b7a7b9b3782faa02edfbe5b22046217cd92a09d Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Wed, 19 Oct 2022 10:28:04 +0900 Subject: [PATCH 36/41] docs(cdk-ops): update README - Adds the following information to `README.md`, - link to the documentation about the AWS architecture of the data warehouse for access logs - trouble shooting of the admin password of the data warehouse - how to populate the database and tables on the data warehouse - enabling the EventBridge rule for access log loading issue codemonger-io/codemonger#30 --- cdk-ops/README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/cdk-ops/README.md b/cdk-ops/README.md index 5988491..c332ab5 100644 --- a/cdk-ops/README.md +++ b/cdk-ops/README.md @@ -21,6 +21,11 @@ This CDK stack provisions a [AWS CodePipeline](https://docs.aws.amazon.com/codep The workflow is triggered when the `main` branch is updated; e.g., a pull request is merged. An author of a pull request has to locally review contents with [`zola serve`](https://www.getzola.org/documentation/getting-started/cli-usage/#serve) before making the pull request. +## Data warehouse for access logs + +This CDK stack provisions a data warehouse for access logs. +Please refer to [`docs/data-warehouse.md`](./docs/data-warehouse.md) for more details. + ## Prerequisites ### Deploying CDK stack for contents @@ -106,6 +111,42 @@ npx cdk deploy --toolkit-stack-name $TOOLKIT_STACK_NAME -c "@aws-cdk/core:bootst After deploying the CDK stack, you will find the CloudFormation stack `codemonger-operation` created or updated. +#### Admin user of the Amazon Redshift Serverless namespace + +This CDK stack creates the admin user of the [Amazon Redshift Serverless (Redshift Serverless)](https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-serverless.html) namespace when it provisions the namespace. +The password of the admin user is created as a secret managed by [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html). +Since **CloudFormation cannot change the admin username and password of the Redshift Serverless namespace** once it is provisioned, the **admin password is lost in case the secret is updated (regenerated)**. + +If this happens, you have to manually update the admin password as another superuser. +You can change the admin password on the Redshift Serverless console, or you can assume the CloudFormation execution role\* on [Query Editor v2](https://aws.amazon.com/redshift/query-editor-v2/) to reset the admin password. + +\* Redshift Serverless gives the creator of a new namespace an admin privilege of it. +Because we are using CDK (CloudFormation) to provision a Redshift Serverless namespace, the execution role of CloudFormation deserves the power. + +## Post deployment + +### Populating the database and tables on the data warehouse + +After deploying this CDK stack, you have to populate the database and tables on the data warehouse. +Please run the following commands. + +```sh +npm run populate-dw -- development +npm run populate-dw -- production +``` + +The `populate-dw` script runs [`bin/populate-data-warehouse.js`](./bin/populate-data-warehouse.js). + +### Enabling the daily access log loading + +This CDK stack provisions an [Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html) rule that runs the Lambda function that loads CloudFront access logs onto the data warehouse once a day. +Since the rule is disabled by default, you have to enable the rule to run the daily access log loading. +There are separate rules for development\* and production. + +Please make sure that you have [populated the database and tables on the data warehouse](#populating-the-database-and-tables-on-the-data-warehouse). + +\* The rule for development triggers **every hour**. + ## Why am I not using exports? This CDK stack depends on the main codemonger CloudFormation stacks. From 9b61fec1742c6ea1f8e2193ed6dfd317f35ab9e8 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Wed, 19 Oct 2022 10:59:33 +0900 Subject: [PATCH 37/41] docs(cdk-ops): refine data-warehouse.md issue codemonger-io/codemonger#30 --- cdk-ops/docs/data-warehouse.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cdk-ops/docs/data-warehouse.md b/cdk-ops/docs/data-warehouse.md index 1d82924..020dc2a 100644 --- a/cdk-ops/docs/data-warehouse.md +++ b/cdk-ops/docs/data-warehouse.md @@ -23,7 +23,7 @@ This bucket sends an event to [`MaskAccessLogs queue`](#maskaccesslogs-queue) wh ### MaskAccessLogs queue `MaskAccessLogs queue` is an [Amazon Simple Queue Service (SQS)](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html) queue that invokes [`MaskAccessLogs`](#maskaccesslogs). -[`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket) sends an event to this queue when an access logs file is PUT into it. +[`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket) sends an event to this queue when an access logs file is PUT into the bucket. ### MaskAccessLogs @@ -31,7 +31,7 @@ This bucket sends an event to [`MaskAccessLogs queue`](#maskaccesslogs-queue) wh This function masks IP addresses, `c-ip` and `x-forwarded-for`, in the [CloudFront access logs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#LogFileFormat). This function also introduces a new column of row numbers to retain the order of the access log records. This function saves transformed results in [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). -While [`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket) flat-spreads access logs files, this function creates a folder hierarchy corresponding to the year, month, and day of access log records. +While [`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket) spreads access logs files flat, this function creates a folder hierarchy corresponding to the year, month, and day of access log records. This folder structure helps [`LoadAccessLogs`](#loadaccesslogs) to process access logs on a specific date in a batch. ### Amazon S3 transformed log bucket @@ -42,7 +42,7 @@ This bucket sends an event to [`DeleteAccessLogs queue`](#deleteaccesslogs-queue ### DeleteAccessLogs queue `DeleteAccessLogs queue` is an SQS queue that invokes [`DeleteAccessLogs`](#deleteaccesslogs). -[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket) sends an event to this queue when a transformed access logs file is PUT into it. +[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket) sends an event to this queue when a transformed access logs file is PUT into the bucket. ### DeleteAccessLogs @@ -64,7 +64,7 @@ and five [dimension tables](https://en.wikipedia.org/wiki/Dimension_(data_wareho Nodes of `Amazon Redshift Serverless` reside in a private subnet. Lambda functions, [`PopulateDwDatabase`](#populatedwdatabase), [`LoadAccessLogs`](#loadaccesslogs), and [`VacuumTable`](#vacuumtable) operate `Amazon Redshift Serverless` via [`Amazon Redshift Data API`](#amazon-redshift-data-api). -The default role of the Amazon Redshift Serverless namespace ([`Redshift namespace role`](#redshift-namespace-role)) can read object from [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). +The default role of the Amazon Redshift Serverless namespace ([`Redshift namespace role`](#redshift-namespace-role)) can read objects from [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). `Amazon Redshift Serverless` accesses [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket) through [`Gateway endpoint`](#gateway-endpoint). This CDK stack creates an admin user when it provisions `Amazon Redshift Serverless`. @@ -72,7 +72,7 @@ This CDK stack creates an admin user when it provisions `Amazon Redshift Serverl ### Redshift namespace role -`Redshift namespace role` is an [AWS Identity and Access Management (IAM)](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) role that is the default role of the namespace of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) and can read objects from [`Access S3 transformed log bucket`](#access-s3-transformed-log-bucket). +`Redshift namespace role` is an [AWS Identity and Access Management (IAM)](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) role that is the default role of the namespace of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) and can read objects from [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). ### Gateway endpoint @@ -85,7 +85,7 @@ Please refer to ["Enhanced VPC routing in Amazon Redshift" - *Amazon Redshift Ma Please refer to [*AWS Secrets Manager User Guide*](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html). Unfortunately, the secret managed by `AWS Secrets Manager` does not sync with the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) except for the first time it is generated. -So you have to manually reset the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless), in case `AWS Secrets Manager` generates a new secret. +So you have to manually reset the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) in case `AWS Secrets Manager` generates a new secret. ### Amazon Redshift Data API @@ -105,8 +105,8 @@ The administrator (`Admin`) has to run this function after deploying this CDK st ### LoadAccessLogs `LoadAccessLogs` is a Lambda function that loads access logs on a specific date onto [`Amazon Redshift Serverless`](#amazon-redshift-serverless). -This function executes [`AWS Step Functions`](#aws-step-functions) after loading access logs finishes. -[`Amazon EventBridge`](#amazon-eventbridge) runs this function every day. +This function executes [`AWS Step Functions`](#aws-step-functions) after the access log loading finishes. +[`Amazon EventBridge`](#amazon-eventbridge) runs this function once a day. While this function is intended to be invoked by [`Amazon EventBridge`](#amazon-eventbridge), you can also manually run this function with a proper payload. From 81cea1eeeea5a764cec540e2b50ab425466286aa Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Wed, 19 Oct 2022 11:53:36 +0900 Subject: [PATCH 38/41] =?UTF-8?q?docs(cdk-ops):=20translate=20data-warehou?= =?UTF-8?q?se.md=20=E2=86=92=20ja?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Translates `docs/data-warehouse.md` into Japanese. Also refines the English version. issue codemonger-io/codemonger#30 --- cdk-ops/docs/data-warehouse.ja.md | 122 ++++++++++++++++++++++++++++++ cdk-ops/docs/data-warehouse.md | 9 ++- 2 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 cdk-ops/docs/data-warehouse.ja.md diff --git a/cdk-ops/docs/data-warehouse.ja.md b/cdk-ops/docs/data-warehouse.ja.md new file mode 100644 index 0000000..3a92c88 --- /dev/null +++ b/cdk-ops/docs/data-warehouse.ja.md @@ -0,0 +1,122 @@ +[English](./data-warehouse.md) / 日本語 + +# アクセスログ用のデータウェアハウス + +このCDKスタックはアクセスログ用のデータウェアハウスを確保します。 +データウェアハウスは[Amazon Redshift Serverless](https://aws.amazon.com/redshift/redshift-serverless/)を使って実現しています。 + +## AWSアーキテクチャ + +以下の図はデータウェアハウスのAWSアーキテクチャを表しています。 + +![データウェアハウスのAWSアーキテクチャ](./data-warehouse-aws-architecture.png) + +### Amazon CloudFront + +`Amazon CloudFront`は我々のウェブサイトのコンテンツを配布しアクセスログを[`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket)に保存します。 + +### Amazon S3 access log bucket + +`Amazon S3 access log bucket`は[Amazon S3 (S3)](https://aws.amazon.com/s3/)のバケットで、[`Amazon CloudFront`](#amazon-cloudfront)が作成したアクセスログを格納します。 +このバケットはアクセスログファイルがPUTされた際、[`MaskAccessLogs queue`](#maskaccesslogs-queue)にイベントを送ります。 + +### MaskAccessLogs queue + +`MaskAccessLogs queue`は[Amazon Simple Queue Service (SQS)](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html)のキューで、[`MaskAccessLogs`](#maskaccesslogs)を呼び出します。 +[`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket)はアクセスログファイルがPUTされた際、このキューにイベントを送ります。 + +### MaskAccessLogs + +`MaskAccessLogs`は[AWS Lambda (Lambda)](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html)関数で、[`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket)のアクセスログを変換します。 +この関数は[CloudFrontアクセスログ](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#LogFileFormat)内のIPアドレス(`c-ip`と`x-forwarded-for`)をマスクします。 +この関数はアクセスログレコードの順序を保持するために行番号のカラムも追加します。 +この関数は変換結果を[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket)に保存します。 +[`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket)はアクセスログファイルをフラットに展開するのに対して、この関数はアクセスログレコードの年月日に相当するフォルダ階層を作成します。 +このフォルダ構造は[`LoadAccessLogs`](#loadaccesslogs)が特定の日付のアクセスログをバッチで処理するのに役立ちます。 + +### Amazon S3 transformed log bucket + +`Amazon S3 transformed log bucket`はS3バケットで、[`MaskAccessLogs`](#maskaccesslogs)が変換したアクセスログを格納します。 +このバケットは変換されたアクセスログファイルがPUTされると[`DeleteAccessLogs queue`](#deleteaccesslogs-queue)にイベントを送ります。 + +### DeleteAccessLogs queue + +`DeleteAccessLogs queue`はSQSキューで、[`DeleteAccessLogs`](#deleteaccesslogs)を呼び出します。 +[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket)は変換されたアクセスログがPUTされるとこのキューにイベントを送ります。 + +### DeleteAccessLogs + +`DeleteAccessLogs`はLambda関数で、[`MaskAccessLogs`](#maskaccesslogs)が変換し[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket)に保存したアクセスログファイルを[`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket)から削除します。 + +### Amazon Redshift Serverless + +`Amazon Redshift Serverless`は[Amazon Redshift Serverless](https://aws.amazon.com/redshift/)のリソースをまとめたもので、データウェアハウスのコアとなります。 + +ひとつの[ファクトテーブル](https://en.wikipedia.org/wiki/Fact_table)と +- `access_log` + +5つの[ディメンジョンテーブル](https://en.wikipedia.org/wiki/Dimension_(data_warehouse))からなります。 +- `referer` +- `page` +- `edge_location` +- `user_agent` +- `result_type` + +`Amazon Redshift Serverless`のノードはプライベートサブネットに配置されます。 +Lambda関数([`PopulateDwDatabase`](#populatedwdatabase), [`LoadAccessLogs`](#loadaccesslogs), [`VacuumTable`](#vacuumtable))は[`Amazon Redshift Data API`](#amazon-redshift-data-api)を介して`Amazon Redshift Serverless`を操作します。 + +[Amazon Redshift Serverlessネームスペース](https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-workgroup-namespace.html)のデフォルトロール([`Redshift namespace role`](#redshift-namespace-role))は[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket)からオブジェクトを読み込むことができます。 +`Amazon Redshift Serverless`は[`Gateway endpoint`](#gateway-endpoint)を介して[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket)にアクセスします。 + +このCDKスタックは`Amazon Redshift Serverless`を確保する際に管理ユーザーを作成します。 +[`AWS Secrets Manager`](#aws-secrets-manager)は管理ユーザーのパスワードを生成・管理します。 + +### Redshift namespace role + +`Redshift namespace role`は[AWS Identity and Access Management (IAM)](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)のロールで、[`Amazon Redshift Serverless`](#amazon-redshift-serverless)のネームスペースのデフォルトロールであり[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket)からオブジェクトを読み込むことができます。 + +### Gateway endpoint + +`Gateway endpoint`は[`Amazon Redshift Serverless`](#amazon-redshift-serverless)と[`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket)の間のトラフィックがインターネットに出て行かないようにします。 +詳しくは["Enhanced VPC routing in Amazon Redshift" - *Amazon Redshift Management Guide*](https://docs.aws.amazon.com/redshift/latest/mgmt/enhanced-vpc-routing.html)をご参照ください。 + +### AWS Secrets Manager + +`AWS Secrets Manager`は[`Amazon Redshift Serverless`](#amazon-redshift-serverless)の管理ユーザーのパスワードを生成・管理します。 +[*AWS Secrets Manager User Guide*](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html)もご参照ください。 + +残念ながら、`AWS Secrets Manager`が管理するシークレットは[`Amazon Redshift Serverless`](#amazon-redshift-serverless)の管理パスワードと同期していません(初回生成時を除く)。 +なので`AWS Secrets Manager`が新しいシークレットを生成してしまった際には、[`Amazon Redshift Serverless`](#amazon-redshift-serverless)の管理パスワードを手作業でリセットしなければなりません。 + +### Amazon Redshift Data API + +`Amazon Redshift Data API`は[`Amazon Redshift Serverless`](#amazon-redshift-serverless)のクライアントをデータベースへの接続を管理することから解放してくれます。 +詳しくは["Using the Amazon Redshift Data API" - *Amazon Redshift Management Guide*](https://docs.aws.amazon.com/redshift/latest/mgmt/data-api.html)をご参照ください。 + +### PopulateDwDatabase + +`PopulateDwDatabase`はLambda関数で、アクセスログを格納するデータベースとテーブルを[`Amazon Redshift Serverless`](#amazon-redshift-serverless)に作成します。 +この関数は[`Amazon Redshift Serverless`](#amazon-redshift-serverless)の管理クレデンシャルを[`AWS Secrets Manager`](#aws-secrets-manager)から取得します。 +管理者(`Admin`)はこのCDKスタックをデプロイした後にこの関数を呼び出さなければなりません。 + +### Amazon EventBridge + +`Amazon EventBridge`は毎日午前2時(UTC)に[`LoadAccessLogs`](#loadaccesslogs)を実行する[Amazon EventBridgeのルール](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html)を定義します。 + +### LoadAccessLogs + +`LoadAccessLogs`はLambda関数で、指定した日付のアクセスログを[`Amazon Redshift Serverless`](#amazon-redshift-serverless)に読み込みます。 +この関数はアクセスログの読み込みが終了すると[`AWS Step Functions`](#aws-step-functions)を実行します。 +[`Amazon EventBridge`](#amazon-eventbridge)は1日に1回この関数を実行します。 + +この関数は[`Amazon EventBridge`](#amazon-eventbridge)から呼び出すことを想定していますが、適切なペイロードを与えて手作業で実行することもできます。 + +### AWS Step Functions + +`AWS Step Functions`は[`Amazon Redshift Serverless`](#amazon-redshift-serverless)のすべてのテーブル(`access_log`, `referer`, `page`, `edge_location`, `user_agent`, `result_type`)に対して[`VacuumTable`](#vacuumtable)を実行する[AWS Step Functionsのステートマシン](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html)を定義します。 +[`VACUUM` SQLコマンド](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html)の実行は同時に1つしか許されていないので、`AWS Step Functions`はテーブルをひとつずつ[`VacuumTable`](#vacuumtable)で処理します。 + +### VacuumTable + +`VacuumTable`はLambda関数で、[`VACUUM` SQLコマンド](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html)を指定したテーブルに対して実行します。 +この関数は[`Amazon Redshift Serverless`](#amazon-redshift-serverless)の管理クレデンシャルを[`AWS Secrets Manager`](#aws-secrets-manager)から取得します。 \ No newline at end of file diff --git a/cdk-ops/docs/data-warehouse.md b/cdk-ops/docs/data-warehouse.md index 020dc2a..8727631 100644 --- a/cdk-ops/docs/data-warehouse.md +++ b/cdk-ops/docs/data-warehouse.md @@ -1,4 +1,4 @@ -English / 日本語 +English / [日本語](./data-warehouse.ja.md) # Data warehouse for access logs @@ -46,11 +46,12 @@ This bucket sends an event to [`DeleteAccessLogs queue`](#deleteaccesslogs-queue ### DeleteAccessLogs -`DeleteAccessLogs` is a Lambda function that deletes an access logs file in [`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket), which has been transformed by [`MaskAccessLogs`](#maskaccesslogs) and saved in [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). +`DeleteAccessLogs` is a Lambda function that deletes an access logs file from [`Amazon S3 access log bucket`](#amazon-s3-access-log-bucket), which [`MaskAccessLogs`](#maskaccesslogs) has transformed and saved in [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). ### Amazon Redshift Serverless `Amazon Redshift Serverless` is a bundle of [Amazon Redshift Serverless](https://aws.amazon.com/redshift/) resources, which is the core of the data warehouse. + It has one [fact table](https://en.wikipedia.org/wiki/Fact_table), - `access_log` @@ -64,7 +65,7 @@ and five [dimension tables](https://en.wikipedia.org/wiki/Dimension_(data_wareho Nodes of `Amazon Redshift Serverless` reside in a private subnet. Lambda functions, [`PopulateDwDatabase`](#populatedwdatabase), [`LoadAccessLogs`](#loadaccesslogs), and [`VacuumTable`](#vacuumtable) operate `Amazon Redshift Serverless` via [`Amazon Redshift Data API`](#amazon-redshift-data-api). -The default role of the Amazon Redshift Serverless namespace ([`Redshift namespace role`](#redshift-namespace-role)) can read objects from [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). +The default role of the [Amazon Redshift Serverless namespace](https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-workgroup-namespace.html) ([`Redshift namespace role`](#redshift-namespace-role)) can read objects from [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket). `Amazon Redshift Serverless` accesses [`Amazon S3 transformed log bucket`](#amazon-s3-transformed-log-bucket) through [`Gateway endpoint`](#gateway-endpoint). This CDK stack creates an admin user when it provisions `Amazon Redshift Serverless`. @@ -82,7 +83,7 @@ Please refer to ["Enhanced VPC routing in Amazon Redshift" - *Amazon Redshift Ma ### AWS Secrets Manager `AWS Secrets Manager` generates and manages the password of the admin user of [`Amazon Redshift Serverless`](#amazon-redshift-serverless). -Please refer to [*AWS Secrets Manager User Guide*](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html). +Please also refer to [*AWS Secrets Manager User Guide*](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html). Unfortunately, the secret managed by `AWS Secrets Manager` does not sync with the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) except for the first time it is generated. So you have to manually reset the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) in case `AWS Secrets Manager` generates a new secret. From b2b47ad5dd3550177e9494a8b1f13f50f85e986f Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Wed, 19 Oct 2022 12:23:41 +0900 Subject: [PATCH 39/41] =?UTF-8?q?docs(cdk-ops):=20translate=20README=20?= =?UTF-8?q?=E2=86=92=20ja?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Translates the updated part of `README` into Japanese. Also refines the English version. issue codemonger-io/codemonger#30 --- cdk-ops/README.ja.md | 43 +++++++++++++++++++++++++++++++++++++++++++ cdk-ops/README.md | 2 ++ 2 files changed, 45 insertions(+) diff --git a/cdk-ops/README.ja.md b/cdk-ops/README.ja.md index 0d85dc2..f3f7534 100644 --- a/cdk-ops/README.ja.md +++ b/cdk-ops/README.ja.md @@ -21,6 +21,11 @@ codemongerウェブサイトのコンテンツを保管し配信するAWSリソ ワークフローは`main`ブランチが更新された際(例えばプルリクエストがマージされた際)に開始されます。 プルリクエストの作成前に作成者は[`zola serve`](https://www.getzola.org/documentation/getting-started/cli-usage/#serve)でローカルにコンテンツをレビューしなければなりません。 +## アクセスログ用のデータウェアハウス + +このCDKスタックはアクセスログ用のデータウェアハウスを確保します。 +詳しくは[`docs/data-warehouse.ja.md`](./docs/data-warehouse.ja.md)をご参照ください。 + ## 事前準備 ### コンテンツのためのCDKスタックをデプロイする @@ -106,6 +111,44 @@ npx cdk deploy --toolkit-stack-name $TOOLKIT_STACK_NAME -c "@aws-cdk/core:bootst CDKスタックをデプロイすると、CloudFormationスタック`codemonger-operation`が作成または更新されます。 +#### Amazon Redshift Serverlessネームスペースの管理ユーザー + +このCDKスタックは[Amazon Redshift Serverless (Redshift Serverless)](https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-serverless.html)ネームスペースの確保時に管理ユーザーを作成します。 +管理ユーザーのパスワードは[AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html)の管理するシークレットとして作成されます。 +**CloudFormationはRedshift Serverlessネームスペースの管理ユーザー名とパスワードを一度作成すると変更することができない**ので、**シークレットが更新(再生成)されると管理パスワードが失われます**。 + +これが起きてしまったら、別のスーパーユーザーで管理パスワードを手作業で更新しなければなりません。 +Redshift Serverlessコンソールで管理パスワードを変更するか、CloudFormationの実行ロール\*で[Query Editor v2](https://aws.amazon.com/redshift/query-editor-v2/)を実行して管理パスワードをリセットすることもできます。 + +\* Redshift Serverlessはネームスペースの作成者に管理権限を与えます。 +Redshift Serverlessネームスペースの確保にCDK (CloudFormation)を使用しているので、CloudFormationの実行ロールがその力を授かることになります。 + +## デプロイ後 + +### データウェアハウスにデータベースとテーブルを作成する + +このCDKスタックをデプロイした後、データウェアハウスにデータベースとテーブルを作成しなければなりません。 +以下のコマンドを実行してください。 + +```sh +npm run populate-dw -- development +npm run populate-dw -- production +``` + +`populate-dw`スクリプトは[`bin/populate-data-warehouse.js`](./bin/populate-data-warehouse.js)を実行します。 + +この手続きはCDKスタックを最初に確保した際に一度だけ必要です。 + +### 日々のアクセスログ読み込みを有効にする + +このCDKスタックは、CloudFrontのアクセスログをデータウェアハウスに読み込むLambda関数を1日に1回実行する[Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html)のルールを確保します。 +ルールはデフォルトで無効化されているので、日々のアクセスログ読み込みを実行するには有効化しなければなりません。 +開発用\*と製品用で別々のルールがあります。 + +確実に[データウェアハウスにデータベースとテーブルを作成](#データウェアハウスにデータベースとテーブルを作成する)しておいてください。 + +\* 開発用のルールは**毎時**トリガーされます。 + ## なぜExportを使わないのか? このCDKスタックはメインとなるcodemongerのCloudFormationスタックに依存します。 diff --git a/cdk-ops/README.md b/cdk-ops/README.md index c332ab5..d9257e6 100644 --- a/cdk-ops/README.md +++ b/cdk-ops/README.md @@ -137,6 +137,8 @@ npm run populate-dw -- production The `populate-dw` script runs [`bin/populate-data-warehouse.js`](./bin/populate-data-warehouse.js). +This procedure is necessary only once when you deploy this CDK stack for the first time. + ### Enabling the daily access log loading This CDK stack provisions an [Amazon EventBridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html) rule that runs the Lambda function that loads CloudFront access logs onto the data warehouse once a day. From 8ba5e960f6baf3eea8504842539b07097128efcc Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Wed, 19 Oct 2022 12:37:52 +0900 Subject: [PATCH 40/41] docs(cdk-ops): link to Redshift admin info - `docs/data-warehouse` includes a link to the section that explains how to deal with the situation where the AWS Secrets Manager regenarates the admin secret. issue codemonger-io/codemonger#30 --- cdk-ops/docs/data-warehouse.ja.md | 2 +- cdk-ops/docs/data-warehouse.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cdk-ops/docs/data-warehouse.ja.md b/cdk-ops/docs/data-warehouse.ja.md index 3a92c88..b55aff6 100644 --- a/cdk-ops/docs/data-warehouse.ja.md +++ b/cdk-ops/docs/data-warehouse.ja.md @@ -86,7 +86,7 @@ Lambda関数([`PopulateDwDatabase`](#populatedwdatabase), [`LoadAccessLogs`](#lo [*AWS Secrets Manager User Guide*](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html)もご参照ください。 残念ながら、`AWS Secrets Manager`が管理するシークレットは[`Amazon Redshift Serverless`](#amazon-redshift-serverless)の管理パスワードと同期していません(初回生成時を除く)。 -なので`AWS Secrets Manager`が新しいシークレットを生成してしまった際には、[`Amazon Redshift Serverless`](#amazon-redshift-serverless)の管理パスワードを手作業でリセットしなければなりません。 +なので`AWS Secrets Manager`が新しいシークレットを生成してしまった際には、[`Amazon Redshift Serverless`](#amazon-redshift-serverless)の管理パスワードを手作業でリセットしなければなりません(対処方法は[READMEの「Amazon Redshift Serverlessネームスペースの管理ユーザー」](../README.ja.md#amazon-redshift-serverlessネームスペースの管理ユーザー)をご参照ください)。 ### Amazon Redshift Data API diff --git a/cdk-ops/docs/data-warehouse.md b/cdk-ops/docs/data-warehouse.md index 8727631..4a80014 100644 --- a/cdk-ops/docs/data-warehouse.md +++ b/cdk-ops/docs/data-warehouse.md @@ -86,7 +86,7 @@ Please refer to ["Enhanced VPC routing in Amazon Redshift" - *Amazon Redshift Ma Please also refer to [*AWS Secrets Manager User Guide*](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html). Unfortunately, the secret managed by `AWS Secrets Manager` does not sync with the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) except for the first time it is generated. -So you have to manually reset the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) in case `AWS Secrets Manager` generates a new secret. +So you have to manually reset the admin password of [`Amazon Redshift Serverless`](#amazon-redshift-serverless) in case `AWS Secrets Manager` generates a new secret; please refer to ["Admin user of the Amazon Redshift Serverless namespace" in *README*](../README.md#admin-user-of-the-amazon-redshift-serverless-namespace) for how to deal with it. ### Amazon Redshift Data API From c658d268249f14d476d52fbb77961d47e2c5d7a8 Mon Sep 17 00:00:00 2001 From: Kikuo Emoto Date: Wed, 19 Oct 2022 12:51:46 +0900 Subject: [PATCH 41/41] docs(cdk-ops): update README - Replaces the section "Continuous delivery" with "DevOps" because the `cdk-ops` no longer contains only the continuous delivery but also the data warehouse. issue codemonger-io/codemonger#30 --- README.ja.md | 7 +++++-- README.md | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.ja.md b/README.ja.md index e2eca95..458f14f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -16,7 +16,10 @@ サブフォルダ[`zola`](zola/README.ja.md)をご覧ください。 -## Continuous Delivery +## DevOps + +以下の["DevOps"](https://en.wikipedia.org/wiki/DevOps)機能も提供します。 +- Continuous Delivery: このレポジトリの`main`ブランチが更新されると、codemongerウェブサイトを更新するためのワークフローが開始します。 +- データウェアハウス: codemongerウェブサイトのアクセスログはデータウェアハウスに格納されます。 -このレポジトリの`main`ブランチが更新されると、codemongerウェブサイトを更新するためのワークフローが開始します。 詳しくはサブフォルダ[`cdk-ops`](cdk-ops/README.ja.md)をご覧ください。 \ No newline at end of file diff --git a/README.md b/README.md index 5459deb..ce97c82 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,10 @@ Please refer to the subfolder [`cdk`](cdk). Please refer to the subfolder [`zola`](zola). -## Continuous delivery +## DevOps + +The following ["DevOps"](https://en.wikipedia.org/wiki/DevOps) features are also provided, +- Continuous delivery: when the `main` branch of this repository is updated, the workflow to update the codemonger website starts. +- Data warehouse: access logs of the codemonger website are stored in the data warehouse. -When the `main` branch of this repository is updated, the workflow to update the codemonger website starts. Please refer to the subfolder [`cdk-ops`](cdk-ops) for more details. \ No newline at end of file