diff --git a/.gitignore b/.gitignore index a7a42f1..4b0bb4d 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,8 @@ dist node_modules cdktf.out cdktf.log +cdk.context.json +cdk.out *terraform.*.tfstate* .gen .terraform diff --git a/amazon-msk/cdk/secure-public-access/README.md b/amazon-msk/cdk/secure-public-access/README.md index 2caaa90..430e1b7 100644 --- a/amazon-msk/cdk/secure-public-access/README.md +++ b/amazon-msk/cdk/secure-public-access/README.md @@ -27,31 +27,51 @@ If you don't have an existing MSK cluster you can use our example MSK deployment ## Required CDK Context Variables -You can set these variables in your `context` in `cdk.json` file. +You can set these variables in your `context` in `cdk.json` file under `zilla-plus` object. -### `vpcId`: MSK Cluster Name +### `vpcId`: VPC ID The VPC ID where the MSK cluster lives. The stack will add Public Subnets and Internet Gateway and run Zilla Plus on the provided VPC. ```bash aws ec2 describe-subnets --subnet-ids $(aws kafka describe-cluster --cluster-arn --query "ClusterInfo.BrokerNodeGroupInfo.ClientSubnets[0]" --output text) --query "Subnets[0].VpcId" --output text ``` +### `msk` related variables -### `mskBootstrapServers`: MSK Bootstrap Servers and Authentication Method +```json + "msk": + { + "bootstrapServers": "" + } +``` + +#### `bootstrapServers`: MSK Bootstrap Servers and Authentication Method To get the bootstrap servers of the MSK cluster run: ```bash aws kafka get-bootstrap-brokers \ - --cluster-arn arn:aws:kafka:us-east-1:445711703002:cluster/my-msk-cluster/83bf3e6e-c31d-4a16-9c0e-3584e845d2d7-20 \ + --cluster-arn \ --query '{BootstrapBrokerStringTls: BootstrapBrokerStringTls, BootstrapBrokerStringSaslScram: BootstrapBrokerStringSaslScram, BootstrapBrokerStringSaslIam: BootstrapBrokerStringSaslIam}' \ --output table ``` -Use the `Bootstrap Server` of your desired authentication method to set the `mskBootstrapServers` variable. -Set the desired client authentication method based on the MSK cluster setup, using `mskClientAuthentication` variable. Allowed values are: `SASL/SCRAM`, `mTLS`, `Unauthorized`. +Use the `Bootstrap Server` of your desired authentication method to set the `bootstrapServers` variable. +Set the desired client authentication method based on the MSK cluster setup, using `clientAuthentication` variable. Allowed values are: `SASL/SCRAM`, `mTLS`, `Unauthorized`. + +### `public` Zilla Plus variables + +```json + "public": + { + "wildcardDNS": "", + "certificate": "", + "port": "" + } +``` -### `publicTlsCertificateKey`: Public TLS Certificate Key +#### `certificate`: Public TLS Certificate Key You need the ARN of either the Certificte Manager certificate or the Secrets Manager secret that contains your public TLS certificate private key. @@ -71,36 +91,37 @@ aws secretsmanager list-secrets --query 'SecretList[*].[Name,ARN]' --output tabl Find and note down the ARN of the secret that contains your public TLS certificate private key. -### `publicWildcardDNS`: Public Wildcard DNS +#### `wildcardDNS`: Public Wildcard DNS This variable defines the public wildcard DNS pattern for bootstrap servers to be used by Kafka clients. It should match the wildcard DNS of the public TLS certificate. -### `zillaPlusCapacity`: Zilla Plus Capacity +#### `port`: Public TCP Port -> Default: `2` +> Default: `9094` -This variable defines the initial number of Zilla Plus instances. +This variable defines the public port number to be used by Kafka clients. -### `zillaPlusInstanceType`: Zilla Plus EC2 Instance Type -> Default: `t3.small` +### `capacity`: Zilla Plus Capacity + +> Default: `2` This variable defines the initial number of Zilla Plus instances. -### `publicPort`: Public TCP Port +### `instanceType`: Zilla Plus EC2 Instance Type -> Default: `9094` +> Default: `t3.small` -This variable defines the public port number to be used by Kafka clients. +This variable defines the initial number of Zilla Plus instances. ### mTLS Specific Variables You only need to add these if you choose mTLS as client authentication method -#### `mskCertificateAuthorityArn`: MSK Certificate Authority ARN +#### `certificateAuthorityArn`: MSK Certificate Authority ARN -This variable defines the ACM Private Certificate Authority ARN used to authorize clients connecting to the MSK cluster. +This variable defines the ACM Private Certificate Authority ARN used to authorize clients connecting to the MSK cluster. You can set this in the context variable in your `cdk.json` file under `zilla-plus` object in the `msk` variables section. List all ACM Private Certificate Authorities: @@ -116,7 +137,7 @@ These features all have default values and can be configured using cdk context v ### Internet Gateway ID -If you already have an Internet Gateway in the MSK's VPN it should be provided via the `igwId` context variable. If not set the deployment will attempt to create on in the VPC. +If you already have an Internet Gateway in the MSK's VPN it should be provided via the `igwId` context variable in your `cdk.json` under `zilla-plus` object. If not set the deployment will attempt to create on in the VPC. To query the igwId of your MSK's VPN use the following command: ```bash @@ -124,9 +145,13 @@ VPC_ID=$(aws kafka describe-cluster --cluster-arn --query "Clu aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" --query "InternetGateways[0].InternetGatewayId" --output text ``` +### Public TLS Certificate via AWS Certificate Manager for Nitro Enclaves + +If you want to enable Zilla-plus Nitro Enclaves support all you have to do is provide the `public.certificate` context variable via ACM. + ### Custom Zilla Plus Role -By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `zillaPlusRoleName` context variable in your `cdk.json`. +By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `roleName` context variable in your `cdk.json` under `zilla-plus` object. List all IAM roles: @@ -138,7 +163,7 @@ Note down the role name `RoleName` of the desired IAM role. ### Custom Zilla Plus Security Groups -By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `zillaPlusSecurityGroups` context variable in your `cdk.json`. +By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `securityGroups` context variable in your `cdk.json` under `zilla-plus` object. List all security groups: @@ -148,10 +173,10 @@ aws ec2 describe-security-groups --query 'SecurityGroups[*].[GroupId, GroupName] Note down the security group IDs (GroupId) of the desired security groups. -#### Separate Public Certificate Authority ARN +### Separate Public Certificate Authority ARN This variable defines the ACM Private Certificate Authority ARN used to authorize clients connecting to the Public Zilla Plus. -By default Zilla Plus will use the `mskCertificateAuthorityArn` for the Public Certificate Authority. If you want to change this set `publicCertificateAuthorityArn` context variable in your `cdk.json` file. +By default Zilla Plus will use the `msk.certificateAuthorityArn` for the Public Certificate Authority. If you want to change this set `certificateAuthorityArn` context variable in your `cdk.json` file under `zilla-plus` object in the `public` variables section. List all ACM Private Certificate Authorities: @@ -161,35 +186,50 @@ aws acm-pca list-certificate-authorities --query 'CertificateAuthorities[*].[Arn Note down the ARN of the ACM Private Certificate Authority you want to use. -### Disable CloudWatch Integration +### CloudWatch Integration + +```json + "cloudwatch": + { + "disabled": false, + "logs": + { + "group": "" + }, + "metrics": + { + "namespace": "" + } + } +``` -By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `cloudwatchDisabled` context variable to `true`. +By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `cloudwatch.disabled` context variable to `true`. You can create or use existing log groups and metric namespaces in CloudWatch. By default, the deployment creates a CloudWatch Log Groups and Custom Metrics Namespace. If you want to define your own, follow these steps. -#### List All CloudWatch Log Groups (cloudwatch_logs_group) +#### List All CloudWatch Log Groups ```bash aws logs describe-log-groups --query 'logGroups[*].[logGroupName]' --output table ``` This command will return a table listing the names of all the log groups in your CloudWatch. -In your `cdk.json` file add the desired CloudWatch Logs Group for variable name `cloudWatchLogGroupName` +In your `cdk.json` file add the desired CloudWatch Logs Group for variable name `logs.group` under `zilla-plus` object in the `cloudwatch` variables section. -#### List All CloudWatch Custom Metric Namespaces (cloudwatch_metrics_namespace) +#### List All CloudWatch Custom Metric Namespaces ```bash aws cloudwatch list-metrics --query 'Metrics[*].Namespace' --output text | tr '\t' '\n' | sort | uniq | grep -v '^AWS' ``` -In your `cdk.json` file add the desired CloudWatch Metrics Namespace for variable name `cloudWatchMetricsNamespace` +In your `cdk.json` file add the desired CloudWatch Metrics Namespace for variable name `metrics.namespace` under `zilla-plus` object in the `cloudwatch` variables section. ### Enable SSH Access -To enable SSH access to the instances you will need the name of an existing EC2 KeyPair to set the `zillaPlusSSHKey` context variable. +To enable SSH access to the instances you will need the name of an existing EC2 KeyPair to set the `sshKey` context variable under `zilla-plus` object. List all EC2 KeyPairs: diff --git a/amazon-msk/cdk/secure-public-access/cdk.json b/amazon-msk/cdk/secure-public-access/cdk.json index 5f6130e..cddec8b 100644 --- a/amazon-msk/cdk/secure-public-access/cdk.json +++ b/amazon-msk/cdk/secure-public-access/cdk.json @@ -17,11 +17,26 @@ ] }, "context": { - "vpcId": "", - "mskBootstrapServers": "", - "publicTlsCertificateKey": "", + "zilla-plus": + { + "vpcId": "", + "msk": + { + "bootstrapServers": "", + "clientAuthentication": "" + }, + "public": + { + "wildcardDNS": "", + "certificate": "" + }, + "cloudwatch": + { + "disabled": false, + "logGroupName": "zilla-plus-spa-loggroup", + "metricsNamespace": "zilla-plus-spa-metrics" + } + }, "@aws-cdk/aws-lambda:recognizeLayerVersion": true, "@aws-cdk/core:checkSecretUsage": true, "@aws-cdk/core:target-partitions": [ diff --git a/amazon-msk/cdk/secure-public-access/lib/secure-public-access-stack.ts b/amazon-msk/cdk/secure-public-access/lib/secure-public-access-stack.ts index da1145e..5d73e6f 100644 --- a/amazon-msk/cdk/secure-public-access/lib/secure-public-access-stack.ts +++ b/amazon-msk/cdk/secure-public-access/lib/secure-public-access-stack.ts @@ -1,5 +1,5 @@ import * as cdk from 'aws-cdk-lib'; -import { Construct } from 'constructs'; +import { Construct, Node } from 'constructs'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as iam from 'aws-cdk-lib/aws-iam'; import { aws_logs as logs, aws_elasticloadbalancingv2 as elbv2, aws_autoscaling as autoscaling} from 'aws-cdk-lib'; @@ -24,26 +24,45 @@ export class ZillaPlusSecurePublicAccessStack extends cdk.Stack { const mandatoryVariables = [ 'vpcId', - 'mskBootstrapServers', - 'mskClientAuthentication', - 'publicTlsCertificateKey', - 'publicWildcardDNS', + 'msk', + 'public' ]; - function validateContextKeys(node: import('constructs').Node, keys: string[]): void { - const missingKeys = keys.filter((key) => !node.tryGetContext(key)); + function validateContextKeys(node: | object, keys: string[]): void { + const missingKeys = []; + if (node instanceof Node) { + missingKeys.push(...keys.filter((key) => !node.tryGetContext(key))); + } else if (typeof node === 'object' && node !== null) { + missingKeys.push(...keys.filter((key) => !(key in node))); + } else { + throw new Error(`Invalid node type. Must be either a constructs.Node or a JSON object.`); + } if (missingKeys.length > 0) { throw new Error(`Missing required context variables: ${missingKeys.join(', ')}`); } } - validateContextKeys(this.node, mandatoryVariables); - - const vpcId = this.node.tryGetContext('vpcId'); - const mskBootstrapServers = this.node.tryGetContext('mskBootstrapServers'); - const mskClientAuthentication = this.node.tryGetContext('mskClientAuthentication'); - const publicTlsCertificateKey = this.node.tryGetContext('publicTlsCertificateKey'); - const publicWildcardDNS = this.node.tryGetContext('publicWildcardDNS'); + const zillaPlusContext = this.node.tryGetContext('zilla-plus'); + validateContextKeys(zillaPlusContext, mandatoryVariables); + + const vpcId = zillaPlusContext.vpcId; + const msk = zillaPlusContext.msk; + const mandatoryMSKVariables = [ + 'bootstrapServers', + 'clientAuthentication' + ]; + validateContextKeys(msk, mandatoryMSKVariables); + const mskBootstrapServers = msk.bootstrapServers; + const mskClientAuthentication = msk.clientAuthentication; + + const publicVar = zillaPlusContext.public; + const mandatoryPublicVariables = [ + 'certificate', + 'wildcardDNS' + ]; + validateContextKeys(publicVar, mandatoryPublicVariables); + const publicTlsCertificateKey = publicVar.certificate; + const publicWildcardDNS = publicVar.wildcardDNS; const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: vpcId }); const subnets = vpc.selectSubnets(); @@ -52,7 +71,7 @@ export class ZillaPlusSecurePublicAccessStack extends cdk.Stack { return; } - let igwId = this.node.tryGetContext('igwId');; + let igwId = zillaPlusContext.igwId; if (!igwId) { const internetGateway = new ec2.CfnInternetGateway(this, `InternetGateway-${id}`, { @@ -133,9 +152,9 @@ export class ZillaPlusSecurePublicAccessStack extends cdk.Stack { if (mTLSEnabled) { - validateContextKeys(this.node, ['mskCertificateAuthorityArn']); - const mskCertificateAuthorityArn = this.node.tryGetContext('mskCertificateAuthorityArn'); - const publicCertificateAuthority = this.node.tryGetContext('publicCertificateAuthorityArn') ?? mskCertificateAuthorityArn; + validateContextKeys(msk, ['certificateAuthorityArn']); + const mskCertificateAuthorityArn = msk.certificateAuthorityArn; + const publicCertificateAuthority = publicVar.certificateAuthorityArn ?? mskCertificateAuthorityArn; data.public = { certificateAuthority: publicCertificateAuthority } @@ -144,7 +163,7 @@ export class ZillaPlusSecurePublicAccessStack extends cdk.Stack { } } - let zillaPlusRole = this.node.tryGetContext('zillaPlusRoleName'); + let zillaPlusRole = zillaPlusContext.roleName; if (!zillaPlusRole) { const iamRole = new iam.Role(this, `ZillaPlusRole-${id}`, { @@ -243,7 +262,7 @@ export class ZillaPlusSecurePublicAccessStack extends cdk.Stack { zillaPlusRole = iamInstanceProfile.ref; } - let zillaPlusSecurityGroups = this.node.tryGetContext('zillaPlusSecurityGroups'); + let zillaPlusSecurityGroups = zillaPlusContext.securityGroups; if (zillaPlusSecurityGroups) { zillaPlusSecurityGroups = zillaPlusSecurityGroups.split(','); @@ -251,7 +270,7 @@ export class ZillaPlusSecurePublicAccessStack extends cdk.Stack { const zillaPlusSG = new ec2.SecurityGroup(this, `ZillaPlusSecurityGroup-${id}`, { vpc: vpc, description: 'Security group for Zilla Plus', - securityGroupName: 'zilla-plus-security-group', + securityGroupName: `zilla-plus-security-group-${id}`, }); zillaPlusSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcpRange(9092, 9096), 'Allow inbound traffic on Kafka ports'); @@ -260,16 +279,16 @@ export class ZillaPlusSecurePublicAccessStack extends cdk.Stack { zillaPlusSecurityGroups = [zillaPlusSG.securityGroupId]; } - const zillaPlusCapacity = this.node.tryGetContext('zillaPlusCapacity') ?? 2; + const zillaPlusCapacity = zillaPlusContext.capacity ?? 2; - const publicPort = this.node.tryGetContext('publicPort') ?? 9094; + const publicPort = publicVar.port ?? 9094; if (!publicTlsCertificateViaAcm) { cdk.aws_secretsmanager.Secret.fromSecretNameV2(this, 'PublicTlsCertificate', publicTlsCertificateKey); } - let keyName = this.node.tryGetContext('zillaPlusSSHKey'); + let keyName = zillaPlusContext.sshKey; let acmYamlContent = ''; let enclavesAcmServiceStart = ''; @@ -296,14 +315,15 @@ systemctl start nitro-enclaves-acm.service `; } - const cloudwatchDisabled = this.node.tryGetContext('cloudwatchDisabled') ?? false; + const cloudwatch = zillaPlusContext.cloudwatch; + const cloudwatchDisabled = cloudwatch?.disabled ?? false; if (!cloudwatchDisabled) { const defaultLogGroupName = `${id}-group`; const defaultMetricNamespace = `${id}-namespace`; - const logGroupName = this.node.tryGetContext('cloudWatchLogGroupName') ?? defaultLogGroupName; - const metricNamespace = this.node.tryGetContext('cloudWatchMetricsNamespace') ?? defaultMetricNamespace; + const logGroupName = cloudwatch?.logs?.group ?? defaultLogGroupName; + const metricNamespace = cloudwatch?.metrics?.namespace ?? defaultMetricNamespace; const cloudWatchLogGroup = new logs.LogGroup(this, `LogGroup-${id}`, { logGroupName: logGroupName, @@ -326,9 +346,9 @@ systemctl start nitro-enclaves-acm.service } const defaultInstanceType = publicTlsCertificateViaAcm ? 'c6i.xlarge' : 't3.small'; - const instanceType = this.node.tryGetContext('zillaPlusInstanceType') ?? defaultInstanceType; + const instanceType = zillaPlusContext.instanceType ?? defaultInstanceType; - let imageId = this.node.tryGetContext('zillaPlusAMI'); + let imageId = zillaPlusContext.ami; if (!imageId) { const ami = ec2.MachineImage.lookup({ name: 'Aklivity Zilla Plus *', @@ -373,7 +393,7 @@ systemctl start nitro-enclaves-acm.service data.public = { ...data.public, port: publicPort, - tlsCertificateKey: publicTlsCertificateKey, + certificate: publicTlsCertificateKey, wildcardDNS: publicWildcardDNS } data.externalHost = externalHost; diff --git a/amazon-msk/cdk/secure-public-access/test/secure-public-access.test.ts b/amazon-msk/cdk/secure-public-access/test/secure-public-access.test.ts index 49bca92..137a723 100644 --- a/amazon-msk/cdk/secure-public-access/test/secure-public-access.test.ts +++ b/amazon-msk/cdk/secure-public-access/test/secure-public-access.test.ts @@ -31,14 +31,22 @@ test('Secure Public Access Stack created', () => { ] } ] - }, - "isTest": "true", - "vpcId": "vpc-12345", - "mskBootstrapServers": "b-1.mymskcluster.****.us-east-1.amazonaws.com:9096", - "mskClientAuthentication": "SASL/SCRAM", - "mskCertificateAuthorityArn": "arn:aws:acm-pca:us-east-1:****:certificate-authority/*********", - "publicWildcardDNS": "*.example.aklivity.io", - "publicTlsCertificateKey": "arn:aws:acm:us-east-1:****:certificate//*********", + }, + "zilla-plus": + { + "vpcId": "vpc-12345", + "msk": + { + "bootstrapServers": "b-1.mymskcluster.****.us-east-1.amazonaws.com:9096", + "clientAuthentication": "SASL/SCRAM", + "certificateAuthorityArn": "arn:aws:acm-pca:us-east-1:****:certificate-authority/*********", + }, + "public": + { + "wildcardDNS": "*.example.aklivity.io", + "certificate": "arn:aws:acm:us-east-1:****:certificate//*********" + } + } } } ); diff --git a/amazon-msk/cdk/secure-public-access/zilla.yaml.mustache b/amazon-msk/cdk/secure-public-access/zilla.yaml.mustache index 0e0387d..c62c8f3 100644 --- a/amazon-msk/cdk/secure-public-access/zilla.yaml.mustache +++ b/amazon-msk/cdk/secure-public-access/zilla.yaml.mustache @@ -44,7 +44,7 @@ bindings: vault: secure options: keys: - - {{{public.tlsCertificateKey}}} + - {{{public.certificate}}} {{#mTLS}} trust: - {{{public.certificateAuthority}}} diff --git a/amazon-msk/cdk/web-streaming/.gitignore b/amazon-msk/cdk/web-streaming/.gitignore new file mode 100644 index 0000000..f60797b --- /dev/null +++ b/amazon-msk/cdk/web-streaming/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/amazon-msk/cdk/web-streaming/.npmignore b/amazon-msk/cdk/web-streaming/.npmignore new file mode 100644 index 0000000..c1d6d45 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/amazon-msk/cdk/web-streaming/README.md b/amazon-msk/cdk/web-streaming/README.md new file mode 100644 index 0000000..d9d4b50 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/README.md @@ -0,0 +1,310 @@ +# Web Streaming deploy via CDK + +This guide will help you gather the necessary AWS values required to configure and deploy Zilla Plus Web Streaming using CDK. + +## Prerequisites + +1. Be subscribed to [Zilla Plus for Amazon MSK](https://aws.amazon.com/marketplace/pp/prodview-jshnzslazfm44). +1. [Install Node.js](https://nodejs.org/en/download/package-manager). +1. [Install AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). +1. [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html). +1. Configure AWS CLI: Run `aws configure` and follow the prompts to set up your AWS credentials. +1. Set your aws region: `aws configure set region us-east-1` +1. Verify your region and credentials: `aws configure list` + + ```text + Name Value Type Location + ---- ----- ---- -------- + profile None None + access_key ****************XXXX env + secret_key ****************XXXX env + region us-east-1 env ['AWS_REGION', 'AWS_DEFAULT_REGION'] + ``` + +## (optional) Create an example MSK cluster + +If you don't have an existing MSK cluster you can use our example MSK deployment with basic configuration and SASL/SCRAM access. Follow the instructions inside the [example-cluster](../example-cluster/README.md) folder to deploy the example MSK cluster. Note the `MskClusterArn` from the outputs as you'll need this later. You will need to set the MSK client auth method variable to the desired one that is set up for the MSK cluster. + +## Required CDK Context Variables + +You can set these variables in your `context` in `cdk.json` file under `zilla-plus` object. + +### `vpcId`: VPC ID +The VPC ID where the MSK cluster lives. The stack will add Public Subnets and Internet Gateway and run Zilla Plus on the provided VPC. + +```bash +aws ec2 describe-subnets --subnet-ids $(aws kafka describe-cluster --cluster-arn --query "ClusterInfo.BrokerNodeGroupInfo.ClientSubnets[0]" --output text) --query "Subnets[0].VpcId" --output text +``` + + +### `msk` related variables + +```json + "msk": + { + "bootstrapServers": "", + "credentials": "" + }, +``` + +#### `bootstrapServers`: MSK Bootstrap Servers + +To get the bootstrap servers of the MSK cluster run: + +```bash +aws kafka get-bootstrap-brokers \ + --cluster-arn arn:aws:kafka:us-east-1:445711703002:cluster/my-msk-cluster/83bf3e6e-c31d-4a16-9c0e-3584e845d2d7-20 \ + --query '{BootstrapBrokerStringSaslScram: BootstrapBrokerStringSaslScram}' \ + --output table +``` + +Use the `SASL/SCRAM Bootstrap Server` to set the `msk.bootstrapServers` variable. + +#### `credentials`: MSK Credentials Secret Name + +Provide the Secret Name that is associated with your MSK cluster. If you use our provided example cluster, there is already a secret associated with the cluster called `AmazonMSK_alice`. + +List all secrets ub Secrets Manager that can be associated with MSK: + +```bash +aws secretsmanager list-secrets --query "SecretList[?starts_with(Name, 'AmazonMSK_')].Name" --output table +``` + +### `mappings`: Kafka Topic Mappings + +```json + "mappings": + [ + {"topic": ""}, + {"topic": "", "path": ""} + ] +``` + +This array variable defines the Kafka topics exposed through REST and SSE. If `path` is not specified, the topic will be exposed on `/` +To enable a custom path for the Kafka topic, set the `path` field to the path where the Kafka topic should be exposed. + +### `public` Zilla Plus variables + +```json + "public": + { + "certificate": "", + "port": "" + } +``` + +#### `certificate`: Public TLS Certificate Key + +You need the ARN of the Secrets Manager secret that contains your public TLS certificate private key. + +List all secrets in Secrets Manager: + +```bash +aws secretsmanager list-secrets --query 'SecretList[*].[Name,ARN]' --output table +``` + +Find and note down the ARN of the secret that contains your public TLS certificate private key. + + +#### `port`: Public TCP Port + +> Default: `7143` + +This variable defines the public port number to be used by REST and SSE clients. + + +### `capacity`: Zilla Plus Capacity + +> Default: `2` + +This variable defines the initial number of Zilla Plus instances. + +### `instanceType`: Zilla Plus EC2 Instance Type + +> Default: `t3.small` + +This variable defines the initial number of Zilla Plus instances. + +## Optional Features + +These features all have default values and can be configured using cdk context variables. If you don't plan to configure any of these features you can skip this section and go to the [Deploy stack using CDK](#deploy-stack-using-cdk) section. + +### Internet Gateway ID + +If you already have an Internet Gateway in the MSK's VPN it should be provided via the `igwId` context variable in your `cdk.json` under `zilla-plus` object. If not set the deployment will attempt to create on in the VPC. + +To query the igwId of your MSK's VPN use the following command: +```bash +VPC_ID=$(aws kafka describe-cluster --cluster-arn --query "ClusterInfo.VpcConfig.VpcId" --output text) +aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" --query "InternetGateways[0].InternetGatewayId" --output text +``` + + +### Custom Zilla Plus Role + +By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `roleName` context variable in your `cdk.json` under `zilla-plus` object. + +List all IAM roles: + +```bash +aws iam list-roles --query 'Roles[*].[RoleName,Arn]' --output table +``` + +Note down the role name `RoleName` of the desired IAM role. + +### Custom Zilla Plus Security Groups + +By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `securityGroups` context variable in your `cdk.json`. + +List all security groups: + +```bash +aws ec2 describe-security-groups --query 'SecurityGroups[*].[GroupId, GroupName]' --output table +``` + +Note down the security group IDs (GroupId) of the desired security groups. + +### CloudWatch Integration + +```json + "cloudwatch": + { + "disabled": false, + "logs": + { + "group": "" + }, + "metrics": + { + "namespace": "" + } + } +``` + +By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `cloudwatch.disabled` context variable to `true`. + +You can create or use existing log groups and metric namespaces in CloudWatch. + +By default, the deployment creates a CloudWatch Log Groups and Custom Metrics Namespace. +If you want to define your own, follow these steps. + +#### List All CloudWatch Log Groups + +```bash +aws logs describe-log-groups --query 'logGroups[*].[logGroupName]' --output table +``` + +This command will return a table listing the names of all the log groups in your CloudWatch. +In your `cdk.json` file add the desired CloudWatch Logs Group for variable name `logs.group` under `zilla-plus` object in the `cloudwatch` variables section. + +#### List All CloudWatch Custom Metric Namespaces + +```bash +aws cloudwatch list-metrics --query 'Metrics[*].Namespace' --output text | tr '\t' '\n' | sort | uniq | grep -v '^AWS' +``` + +In your `cdk.json` file add the desired CloudWatch Metrics Namespace for variable name `metrics.namespace` under `zilla-plus` object in the `cloudwatch` variables section. + +### Enable JWT Access Tokens + +To enable the JWT authentication and API access control, you need to provide the `jwt` context variable. You will also need to set the JWT Issuer (`issuer`), JWT Audience (`audience`) and JWKS URL (`keys_url`) context variable inside the `jwt` object. Example: + +```json + "jwt": { + "issuer" : "https://auth.example.com", + "audience": "https://api.example.com", + "keysUrl": "https://{yourDomain}/.well-known/jwks.json" + } +``` + + +### Enable Glue Schema Registry + +To enable the Glue Schema Registry for schema fetching, set the context variable `glueRegistry` to the name of the Glue Registry. + +1. List all Glue Registries: + +```bash +aws glue list-registries --query 'Registries[*].[RegistryName]' --output table +``` + +Note down the Glue Registry name (RegistryName) you want to use. + + +### Enable SSH Access + +To enable SSH access to the instances you will need the name of an existing EC2 KeyPair to set the `sshKey` context variable. + +List all EC2 KeyPairs: + +```bash +aws ec2 describe-key-pairs --query 'KeyPairs[*].[KeyName]' --output table +``` + +Note down the KeyPair name `KeyName` you want to use. + +## Deploy stack using CDK + +### Install Project Dependencies + +Install the node.js dependencies specified in the `package.json` file: + +```bash +npm install +``` + +### Synthesizing the CloudFormation Template + +Run the following command to synthesize your stack into a CloudFormation template: + +```bash +cdk synth +``` + +This generates the cdk.out directory containing the synthesized CloudFormation template. + +### Bootstrap the environment (if needed) + +If this is your first time deploying in a specific AWS environment, bootstrap it: + +```bash +cdk bootstrap +``` + +### Deploy the stack +Deploy your resources to AWS: + + +```bash +cdk deploy +``` + +### Configure Global DNS + +This ensures that any new Kafka brokers added to the cluster can still be reached via the Zilla proxy. When using a wildcard DNS name for your own domain, such as `*.example.aklivity.io` then the DNS entries are setup in your DNS provider. After deploying the stack, check the outputs, where you can find the NetworkLoadBalancer DNS. `NetworkLoadBalancerOutput = "network-load-balancer-******.elb.us-east-1.amazonaws.com"` Lookup the IP addresses of your load balancer using `nslookup` and the DNS of the NetworkLoadBalancer. + +```bash +nslookup network-load-balancer-******.elb.us-east-1.amazonaws.com +``` + +For testing purposes you can edit your local /etc/hosts file instead of updating your DNS provider. For example: + +```bash +X.X.X.X web.example.aklivity.io +``` + +### Test the Zilla Plus REST and SSE + +If you added `web.example.aklivity.io` as the domain, open a terminal and use `curl` to open an SSE connection. + +```bash +curl -N --http2 -H "Accept:text/event-stream" -v "https://web.example.aklivity.io:7143/" +``` + +Note that `your path` defaults to the exposed Kafka topic in your config. + +In another terminal, use `curl` to POST and notice the data arriving on your SSE stream. + +```bash +curl -d 'Hello, World' -X POST https://web.example.aklivity.io:7143/ +``` diff --git a/amazon-msk/cdk/web-streaming/bin/web-streaming.ts b/amazon-msk/cdk/web-streaming/bin/web-streaming.ts new file mode 100644 index 0000000..466a337 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/bin/web-streaming.ts @@ -0,0 +1,10 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { WebStreamingStack } from '../lib/web-streaming-stack'; + +const app = new cdk.App(); +new WebStreamingStack(app, 'WebStreamingStack', { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION +}}); diff --git a/amazon-msk/cdk/web-streaming/cdk.json b/amazon-msk/cdk/web-streaming/cdk.json new file mode 100644 index 0000000..dee4648 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/cdk.json @@ -0,0 +1,99 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/web-streaming.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "zilla-plus": + { + "vpcId": "", + "msk": + { + "bootstrapServers": "", + "credentials": "" + }, + "public": + { + "certificate": "", + "port": 7143 + }, + "mappings": + [ + {"topic": "pets"} + ] + }, + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true + } +} diff --git a/amazon-msk/cdk/web-streaming/jest.config.js b/amazon-msk/cdk/web-streaming/jest.config.js new file mode 100644 index 0000000..08263b8 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/amazon-msk/cdk/web-streaming/lib/subnet-calculator.ts b/amazon-msk/cdk/web-streaming/lib/subnet-calculator.ts new file mode 100644 index 0000000..9a6c7e5 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/lib/subnet-calculator.ts @@ -0,0 +1,44 @@ +var ip = require('ip'); + +function cidrBlocksOverlap(cidr1: string, cidr2: string): boolean { + const range1 = ip.cidrSubnet(cidr1); + const range2 = ip.cidrSubnet(cidr2); + + const start1 = ip.toLong(range1.firstAddress); + const end1 = ip.toLong(range1.lastAddress); + const start2 = ip.toLong(range2.firstAddress); + const end2 = ip.toLong(range2.lastAddress); + + return start1 <= end2 && start2 <= end1; +} + +export function findAvailableCidrBlocks(vpcCidrBlock: string, subnetCidrBlocks: string[], requiredCount: number = 2): string[] { + const availableCidrs: string[] = []; + const vpcRange = ip.cidrSubnet(vpcCidrBlock); + + // Start searching within the VPC range, block by block + let currentBlock = ip.fromLong(ip.toLong(vpcRange.networkAddress)); + const subnetMask = parseInt(subnetCidrBlocks[0].split('/')[1], 10); + + while (availableCidrs.length < requiredCount) { + const candidateCidr = `${currentBlock}/${subnetMask}`; + if (!subnetCidrBlocks.some(subnet => cidrBlocksOverlap(candidateCidr, subnet))) { + availableCidrs.push(candidateCidr); + } + + // Increment to the next block based on the subnet mask + const increment = Math.pow(2, 32 - subnetMask); + currentBlock = ip.fromLong(ip.toLong(currentBlock) + increment); + + // Break if we go beyond the VPC range + if (ip.toLong(currentBlock) > ip.toLong(vpcRange.lastAddress)) { + break; + } + } + + if (availableCidrs.length < requiredCount) { + throw new Error("Not enough available CIDR blocks in the specified VPC range."); + } + + return availableCidrs; +} diff --git a/amazon-msk/cdk/web-streaming/lib/web-streaming-stack.ts b/amazon-msk/cdk/web-streaming/lib/web-streaming-stack.ts new file mode 100644 index 0000000..85ab423 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/lib/web-streaming-stack.ts @@ -0,0 +1,474 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct, Node } from 'constructs'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { aws_logs as logs, aws_elasticloadbalancingv2 as elbv2, aws_autoscaling as autoscaling} from 'aws-cdk-lib'; +import * as subnetCalculator from './subnet-calculator'; +import Mustache = require("mustache"); +import fs = require("fs"); + +interface TemplateData { + name: string; + glue?: object; + cloudwatch?: object; + mappings?: Array; + public?: object; + kafka?: object; + jwt?: object; +} + + +export class WebStreamingStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const mandatoryVariables = [ + 'vpcId', + 'msk', + 'public', + 'mappings', + ]; + + function validateContextKeys(node: object, keys: string[]): void { + const missingKeys = []; + if (node instanceof Node) { + missingKeys.push(...keys.filter((key) => !node.tryGetContext(key))); + } else if (typeof node === 'object' && node !== null) { + missingKeys.push(...keys.filter((key) => !(key in node))); + } else { + throw new Error(`Invalid node type. Must be either a constructs.Node or a JSON object.`); + } + if (missingKeys.length > 0) { + throw new Error(`Missing required context variables: ${missingKeys.join(', ')}`); + } + } + + const zillaPlusContext = this.node.tryGetContext('zilla-plus'); + validateContextKeys(zillaPlusContext, mandatoryVariables); + const vpcId = zillaPlusContext.vpcId; + const msk = zillaPlusContext.msk; + const mandatoryMSKVariables = [ + 'bootstrapServers', + 'credentials' + ]; + validateContextKeys(msk, mandatoryMSKVariables); + const mskBootstrapServers = msk.bootstrapServers; + const mskCredentialsSecretName = msk.credentials; + const publicVar = zillaPlusContext.public; + const mandatoryPublicVariables = [ + 'certificate', + ]; + validateContextKeys(publicVar, mandatoryPublicVariables); + + const publicTlsCertificateKey = publicVar.certificate; + const mappings = zillaPlusContext.mappings; + + mappings.forEach((mapping: { path: string; topic: string; }) => { + if (!mapping.path) { + mapping.path = `/${mapping.topic}`; + } + }); + + const kafkaTopics: string[] = mappings.map((mapping: { topic: any; }) => mapping.topic); + + const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: vpcId }); + const subnets = vpc.selectSubnets(); + if (subnets.isPendingLookup) { + // return before using the vpc, the cdk will rerun immediately after the lookup + return; + } + + let igwId = zillaPlusContext.igwId;; + if (!igwId) + { + const internetGateway = new ec2.CfnInternetGateway(this, `InternetGateway-${id}`, { + tags: [{ key: 'Name', value: 'my-igw' }], + }); + igwId = internetGateway.ref; + + new ec2.CfnVPCGatewayAttachment(this, `VpcGatewayAttachment-${id}`, { + vpcId: vpcId, + internetGatewayId: igwId, + }); + } + + + const publicRouteTable = new ec2.CfnRouteTable(this, `PublicRouteTable-${id}`, { + vpcId: vpcId, + tags: [{ key: 'Name', value: 'public-route-table' }], + }); + + new ec2.CfnRoute(this, `PublicRoute-${id}`, { + routeTableId: publicRouteTable.ref, + destinationCidrBlock: '0.0.0.0/0', + gatewayId: igwId, + }); + + const existingSubnets = vpc.isolatedSubnets.concat(vpc.publicSubnets, vpc.privateSubnets); + const existingCidrBlocks = existingSubnets.map((subnet) => subnet.ipv4CidrBlock); + + const availableCidrBlocks = subnetCalculator.findAvailableCidrBlocks( + vpc.vpcCidrBlock, + existingCidrBlocks, + 2); + + const availabilityZones = cdk.Fn.getAzs(); + const subnetIds: string[] = []; + + for (let i = 0; i < 2; i++) { + const az = cdk.Fn.select(i, availabilityZones); + const cidrBlock = cdk.Fn.select(i, availableCidrBlocks); + + const subnet = new ec2.CfnSubnet(this, `Subnet${i + 1}`, { + vpcId: vpcId, + cidrBlock: cidrBlock, + availabilityZone: az, + mapPublicIpOnLaunch: true, + tags: [ + { + key: 'Name', + value: `public-subnet-${i + 1}-${id}`, + }, + ], + }); + + subnetIds.push(subnet.ref); + + new ec2.CfnSubnetRouteTableAssociation(this, `Subnet${i + 1}RouteTableAssociation`, { + subnetId: subnet.ref, + routeTableId: publicRouteTable.ref, + }); + } + + let zillaPlusRole = zillaPlusContext.roleName; + + if (!zillaPlusRole) { + const iamRole = new iam.Role(this, `ZillaPlusRole-${id}`, { + roleName: `zilla_plus_role-${id}`, + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal('ec2.amazonaws.com'), + new iam.ServicePrincipal('cloudformation.amazonaws.com') + ), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), + iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCertificateManagerReadOnly'), + iam.ManagedPolicy.fromAwsManagedPolicyName('AWSGlueSchemaRegistryReadonlyAccess'), + iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess'), + ], + inlinePolicies: { + CCProxySecretsManagerRead: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + sid: 'VisualEditor0', + effect: iam.Effect.ALLOW, + actions: [ + 'acm-pca:GetCertificate', + 'acm-pca:GetCertificateAuthorityCertificate', + 'acm-pca:DescribeCertificateAuthority', + 'tag:GetResources', + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + resources: [ + '*', + ], + }), + ], + }), + }, + }); + + const iamInstanceProfile = new iam.CfnInstanceProfile(this, `ZillaPlusInstanceProfile-${id}`, { + instanceProfileName: `zilla_plus_role-${id}`, + roles: [iamRole.roleName], + }); + + const iamPolicy = new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + sid: 'secretStatement', + effect: iam.Effect.ALLOW, + actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], + resources: ['arn:aws:secretsmanager:*:*:secret:*'], + }), + new iam.PolicyStatement({ + sid: 'cloudwatchStatement', + effect: iam.Effect.ALLOW, + actions: ['logs:*', 'cloudwatch:GenerateQuery', 'cloudwatch:PutMetricData'], + resources: ['*'], + }), + ], + }); + + new iam.CfnPolicy(this, `ZillaPlusRolePolicy-${id}`, { + policyName: `ZillaPlusRolePolicy-${id}`, + roles: [iamRole.roleName], + policyDocument: iamPolicy.toJSON(), + }); + + zillaPlusRole = iamInstanceProfile.ref; + } + + const publicPort = publicVar.port ?? 7143; + + let zillaPlusSecurityGroups = zillaPlusContext.securityGroups; + + if (zillaPlusSecurityGroups) { + zillaPlusSecurityGroups = zillaPlusSecurityGroups.split(','); + } else { + const zillaPlusSG = new ec2.SecurityGroup(this, `ZillaPlusSecurityGroup-${id}`, { + vpc: vpc, + description: 'Security group for Zilla Plus', + securityGroupName: `zilla-plus-security-group-${id}`, + }); + + zillaPlusSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(publicPort)); + zillaPlusSG.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.allTcp()); + + zillaPlusSecurityGroups = [zillaPlusSG.securityGroupId]; + } + + const zillaPlusCapacity = zillaPlusContext.capacity ?? 2; + const keyName = zillaPlusContext.sshKey; + const instanceType = zillaPlusContext.instanceType ?? 't3.small'; + + let imageId = zillaPlusContext.ami; + if (!imageId) { + const ami = ec2.MachineImage.lookup({ + name: 'Aklivity Zilla Plus *', + filters: { + 'product-code': ['ca5mgk85pjtbyuhtfluzisgzy'], + 'is-public': ['true'], + }, + }); + imageId = ami.getImage(this).imageId; + } + + + const data: TemplateData = { + name: 'web', + } + + const jwt = zillaPlusContext.jwt; + if (jwt) + { + const mandatoryJWTVariables = [ + 'issuer', + 'audience', + 'keysUrl' + ]; + validateContextKeys(jwt, mandatoryJWTVariables); + const issuer = jwt.issuer; + const audience = jwt.audience; + const keysUrl = jwt.keysUrl; + + data.jwt = { + issuer: issuer, + audience: audience, + keysUrl: keysUrl + } + } + + const cloudwatch = zillaPlusContext.cloudwatch; + const cloudwatchDisabled = cloudwatch?.disabled ?? false; + + if (!cloudwatchDisabled) { + const defaultLogGroupName = `${id}-group`; + const defaultMetricNamespace = `${id}-namespace`; + + const logGroupName = cloudwatch?.logs?.group ?? defaultLogGroupName; + const metricNamespace = cloudwatch?.metrics?.namespace ?? defaultMetricNamespace; + + const cloudWatchLogGroup = new logs.LogGroup(this, `LogGroup-${id}`, { + logGroupName: logGroupName, + retention: logs.RetentionDays.ONE_MONTH, + }); + + new logs.LogStream(this, `LogStream-${id}`, { + logGroup: cloudWatchLogGroup, + logStreamName: 'events', + }); + + data.cloudwatch = { + logs: { + group: cloudWatchLogGroup.logGroupName, + }, + metrics: { + namespace: metricNamespace, + }, + }; + } + + const glueRegistry = zillaPlusContext.glueRegistry; + if (glueRegistry) + { + data.glue = { + registry: glueRegistry + } + } + + const nlb = new elbv2.CfnLoadBalancer(this, `NetworkLoadBalancer-${id}`, { + name: `nlb-${id}`, + scheme: 'internet-facing', + subnets: subnetIds, + type: 'network', + ipAddressType: 'ipv4', + }); + + const nlbTargetGroup = new elbv2.CfnTargetGroup(this, `NLBTargetGroup-${id}`, { + name: `nlb-tg-${id}`, + port: publicPort, + protocol: 'TCP', + vpcId: vpcId, + }); + + new elbv2.CfnListener(this, `NLBListener-${id}`, { + loadBalancerArn: nlb.ref, + port: publicPort, + protocol: 'TCP', + defaultActions: [ + { + type: 'forward', + targetGroupArn: nlbTargetGroup.ref, + }, + ], + }); + + const kafkaSaslUsername = `\${{aws.secrets.${mskCredentialsSecretName}#username}}`; + const kafkaSaslPassword = `\${{aws.secrets.${mskCredentialsSecretName}#password}}`; + const kafkaBootstrapServers = `['${mskBootstrapServers.split(",").join("','")}']`; + + data.kafka = { + bootstrapServers: kafkaBootstrapServers, + sasl : { + username: kafkaSaslUsername, + password: kafkaSaslPassword + } + } + data.public = { + port: publicPort, + certificate: publicTlsCertificateKey + } + data.mappings = mappings; + + const kafkaTopicCreationDisabled = zillaPlusContext.kafkaTopicCreationDisabled ?? false; + + const yamlTemplate: string = fs.readFileSync('zilla.yaml.mustache', 'utf8'); + const renderedYaml: string = Mustache.render(yamlTemplate, data); + + const cfnHupConfContent = ` +[main] +stack=${id} +region=${this.region} + `; + + const cfnAutoReloaderConfContent = ` +[cfn-auto-reloader-hook] +triggers=post.update +path=Resources.ZillaPlusLaunchTemplate.Metadata.AWS::CloudFormation::Init +action=/opt/aws/bin/cfn-init -v --stack ${id} --resource ZillaPlusLaunchTemplate --region ${this.region} +runas=root + `; + + let topicsCommand = ""; + kafkaTopics.forEach((t: String) => { + topicsCommand = topicsCommand.concat(` +./kafka-topics.sh --create --if-not-exists --bootstrap-server ${mskBootstrapServers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${t} --config 'cleanup.policy=compact'`); + }); + + let kafkaTopicCreationCommand = ""; + + if (!kafkaTopicCreationDisabled) { + kafkaTopicCreationCommand = ` +wget https://archive.apache.org/dist/kafka/3.5.1/kafka_2.13-3.5.1.tgz +tar -xzf kafka_2.13-3.5.1.tgz +cd kafka_2.13-3.5.1/libs +wget https://github.com/aws/aws-msk-iam-auth/releases/download/v1.1.1/aws-msk-iam-auth-1.1.1-all.jar +cd ../bin +SECRET_STRING=$(aws secretsmanager get-secret-value --secret-id ${mskCredentialsSecretName} --query SecretString --output text) +USERNAME=$(echo $SECRET_STRING | jq -r '.username') +PASSWORD=$(echo $SECRET_STRING | jq -r '.password') + +cat < client.properties +sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username=$USERNAME password=$PASSWORD; +security.protocol=SASL_SSL +sasl.mechanism=SCRAM-SHA-512 +EOF +${topicsCommand} + `; + } + + const userData = `#!/bin/bash -xe +yum update -y aws-cfn-bootstrap +cat <<'END_HELP' > /etc/zilla/zilla.yaml +${renderedYaml} +END_HELP + +chown ec2-user:ec2-user /etc/zilla/zilla.yaml + +mkdir /etc/cfn +cat < /etc/cfn/cfn-hup.conf +${cfnHupConfContent} +EOF + +chown root:root /etc/cfn/cfn-hup.conf +chmod 0400 /etc/cfn/cfn-hup.conf + +mkdir /etc/cfn/hooks.d +cat < /etc/cfn/hooks.d/cfn-auto-reloader.conf +${cfnAutoReloaderConfContent} +EOF + +chown root:root /etc/cfn/hooks.d/cfn-auto-reloader.conf +chmod 0400 /etc/cfn/hooks.d/cfn-auto-reloader.conf + +systemctl enable cfn-hup +systemctl start cfn-hup +systemctl enable amazon-ssm-agent +systemctl start amazon-ssm-agent +systemctl enable zilla-plus +systemctl start zilla-plus + +${kafkaTopicCreationCommand} + + `; + + const zillaPlusLaunchTemplate = new ec2.CfnLaunchTemplate(this, `ZillaPlusLaunchTemplate-${id}`, { + launchTemplateData: { + imageId: imageId, + instanceType: instanceType, + networkInterfaces: [ + { + associatePublicIpAddress: true, + deviceIndex: 0, + groups: zillaPlusSecurityGroups, + }, + ], + iamInstanceProfile: { + name: zillaPlusRole, + }, + keyName: keyName, + userData: cdk.Fn.base64(userData) + }, + }); + + new autoscaling.CfnAutoScalingGroup(this, `ZillaPlusGroup-${id}`, { + vpcZoneIdentifier: subnetIds, + launchTemplate: { + launchTemplateId: zillaPlusLaunchTemplate.ref, + version: '1' + }, + minSize: '1', + maxSize: '5', + desiredCapacity: zillaPlusCapacity.toString(), + targetGroupArns: [nlbTargetGroup.ref] + }); + + new cdk.CfnOutput(this, 'NetworkLoadBalancerOutput', + { + description: "Public DNS name of newly created NLB for Zilla Plus", + value: nlb.attrDnsName + }); + + } +} diff --git a/amazon-msk/cdk/web-streaming/package-lock.json b/amazon-msk/cdk/web-streaming/package-lock.json new file mode 100644 index 0000000..5d66829 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/package-lock.json @@ -0,0 +1,4467 @@ +{ + "name": "web-streaming", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-streaming", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "^2.173.1", + "constructs": "^10.0.0", + "ip": "^2.0.1", + "mustache": "^4.2.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "web-streaming": "bin/web-streaming.js" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/mustache": "^4.2.5", + "@types/node": "22.7.9", + "aws-cdk": "2.173.2", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.218", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.218.tgz", + "integrity": "sha512-t5M+dk7qNM0N6ZbHVDs/6lO7xPw4ogUUsTbXzAfnqrpZQ/wKpphwgBxTNrHMV+NKOFxyCbF5fWUAD7I36jRqZw==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-kubectl-v20": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.3.tgz", + "integrity": "sha512-cDG1w3ieM6eOT9mTefRuTypk95+oyD7P5X/wRltwmYxU7nZc3+076YEVS6vrjDKr3ADYbfn0lDKpfB1FBtO9CQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "38.0.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz", + "integrity": "sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "^1.4.1", + "semver": "^7.6.3" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/mustache": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.173.2", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.173.2.tgz", + "integrity": "sha512-qyMU4FoRJdZDUpsOBqyRBALBjf5A2N/MaHKX9iJUkbTET+d+nR07x3ai4TcEES+8pqPFHMTKpQMRDXs9Py/15w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.173.2", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.173.2.tgz", + "integrity": "sha512-cL9+z8Pl3VZGoO7BwdsrFAOeud/vSl3at7OvmhihbNprMN15XuFUx/rViAU5OI1m92NbV4NBzYSLbSeCwYLNyw==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-kubectl-v20": "^2.1.3", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^38.0.1", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.2.0", + "ignore": "^5.3.2", + "jsonschema": "^1.4.1", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.6.3", + "table": "^6.8.2", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.3", + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.8.2", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "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, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "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, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "license": "Apache-2.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.78", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.78.tgz", + "integrity": "sha512-UmwIt7HRKN1rsJfddG5UG7rCTCTAKoS9JeOy/R0zSenAyaZ8SU3RuXlwcratxhdxGRNpk03iq8O7BA3W7ibLVw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/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, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "license": "MIT" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/amazon-msk/cdk/web-streaming/package.json b/amazon-msk/cdk/web-streaming/package.json new file mode 100644 index 0000000..b2d5b31 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/package.json @@ -0,0 +1,30 @@ +{ + "name": "web-streaming", + "version": "0.1.0", + "bin": { + "web-streaming": "bin/web-streaming.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", + "@types/mustache": "^4.2.5", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "aws-cdk": "2.173.2", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "^2.173.1", + "constructs": "^10.0.0", + "ip": "^2.0.1", + "source-map-support": "^0.5.21", + "mustache": "^4.2.0" + } +} diff --git a/amazon-msk/cdk/web-streaming/test/web-streaming.test.ts b/amazon-msk/cdk/web-streaming/test/web-streaming.test.ts new file mode 100644 index 0000000..c547523 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/test/web-streaming.test.ts @@ -0,0 +1,138 @@ +import * as cdk from 'aws-cdk-lib'; +import * as web from '../lib/web-streaming-stack'; +import { Template } from 'aws-cdk-lib/assertions'; + +test('Secure Public Access Stack created', () => { + + const app = new cdk.App( { + context: { + "vpc-provider:account=12345678:filter.vpc-id=vpc-12345:region=us-east-1:returnAsymmetricSubnets=true": { + "vpcId": "vpc-12345", + "vpcCidrBlock": "10.0.0.0/16", + "ownerAccountId": "12345678", + "availabilityZones": [], + "subnetGroups": [ + { + "name": "PrivateSubnet", + "type": "Isolated", + "subnets": [ + { + "subnetId": "subnet-1", + "cidr": "10.0.0.0/24", + "availabilityZone": "us-east-1a", + "routeTableId": "rtb-1234" + }, + { + "subnetId": "subnet-2", + "cidr": "10.0.1.0/24", + "availabilityZone": "us-east-1b", + "routeTableId": "rtb-5678" + } + ] + } + ] + }, + "zilla-plus": + { + "vpcId": "vpc-12345", + "msk": + { + "bootstrapServers": "b-1.mymskcluter.****.us-east-1.amazonaws.com:9096", + "credentials": "AmazonMSK_Alice" + }, + "public": + { + "certificate": "arn:aws:acm:us-east-1:****:certificate//*********" + }, + "mappings": + [ + {"topic": "pets"} + ] + } + } + } + ); + const stack = new web.WebStreamingStack(app, 'MyTestStack', { + env: { + account: '12345678', + region: 'us-east-1' + } + + }); + + const template = Template.fromStack(stack); + + console.log(template); + + template.hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { + DesiredCapacity: "2", + MaxSize: "5", + MinSize: "1", + TargetGroupARNs: [ + { + "Ref": "NLBTargetGroupMyTestStack" + } + ], + VPCZoneIdentifier: [ + { + "Ref": "Subnet1" + }, + { + "Ref": "Subnet2" + } + ] + }); + + template.hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', { + Name: `nlb-tg-MyTestStack`, + Port: 7143, + Protocol: `TCP`, + VpcId: `vpc-12345` + }); + + template.hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', { + LoadBalancerArn: + { + Ref: `NetworkLoadBalancerMyTestStack` + }, + Port: 7143, + Protocol: `TCP`, + }); + + + template.hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + IpAddressType: `ipv4`, + Name: `nlb-MyTestStack`, + Scheme: `internet-facing`, + Subnets: [ + { + "Ref": "Subnet1" + }, + { + "Ref": "Subnet2" + } + ], + Type: `network` + }); + + template.hasResourceProperties('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: + { + IamInstanceProfile: + { + Name: + { + Ref: `ZillaPlusInstanceProfileMyTestStack` + } + }, + ImageId: `ami-1234`, + NetworkInterfaces: + [ + { + AssociatePublicIpAddress: true, + DeviceIndex: 0 + } + ] + }, + }); +}); diff --git a/amazon-msk/cdk/web-streaming/tsconfig.json b/amazon-msk/cdk/web-streaming/tsconfig.json new file mode 100644 index 0000000..aaa7dc5 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/amazon-msk/cdk/web-streaming/zilla.yaml.mustache b/amazon-msk/cdk/web-streaming/zilla.yaml.mustache new file mode 100644 index 0000000..732ac91 --- /dev/null +++ b/amazon-msk/cdk/web-streaming/zilla.yaml.mustache @@ -0,0 +1,252 @@ +name: {{name}} +vaults: + secure: + type: aws-secrets +{{#jwt}} +guards: + auth_jwt: + type: jwt + options: + issuer: {{{jwt.issuer}}} + audience: {{{jwt.audience}}} + keys: {{{jwt.keysUrl}}} +{{/jwt}} +{{#glue}} +catalogs: + glue_catalog: + type: aws-glue + options: + registry: {{glue.registry}} +{{/glue}} +telemetry: + metrics: + - stream.active.received + - stream.active.sent + - stream.opens.received + - stream.opens.sent + - stream.data.received + - stream.data.sent + - stream.errors.received + - stream.errors.sent + - stream.closes.received + - stream.closes.sent + exporters: + stdout_logs_exporter: + type: stdout +{{#cloudwatch}} + aws0: + type: aws-cloudwatch + options: + logs: + group: {{cloudwatch.logs.group}} + stream: events + metrics: + namespace: {{cloudwatch.metrics.namespace}} +{{/cloudwatch}} +bindings: + tcp_server: + type: tcp + kind: server + options: + host: 0.0.0.0 + port: {{public.port}} + telemetry: + metrics: + - stream.* + exit: tls_server + tls_server: + type: tls + kind: server + vault: secure + options: + keys: + - {{public.certificate}} + telemetry: + metrics: + - stream.* + exit: http_server + http_server: + type: http + kind: server + telemetry: + metrics: + - stream.* + options: + access-control: + policy: cross-origin +{{#jwt}} + authorization: + auth_jwt: + credentials: + headers: + authorization: Bearer {credentials} +{{/jwt}} + routes: +{{#mappings}} + - when: + - headers: + :path: {{{path}}} + accept: text/event-stream +{{#jwt}} + guarded: + auth_jwt: + - sse:stream +{{/jwt}} + exit: sse_server + - when: + - headers: + :path: {{{path}}}/* + - headers: + :path: {{{path}}} +{{#jwt}} + guarded: + auth_jwt: + - http:stream +{{/jwt}} + exit: http_kafka_mapping +{{/mappings}} + sse_server: + type: sse + kind: server + telemetry: + metrics: + - stream.* + exit: sse_kafka_mapping + sse_kafka_mapping: + type: sse-kafka + kind: proxy + telemetry: + metrics: + - stream.* + routes: +{{#mappings}} + - when: + - path: {{{path}}} + exit: kafka_cache_client + with: + topic: {{topic}} +{{/mappings}} + http_kafka_mapping: + type: http-kafka + kind: proxy + telemetry: + metrics: + - stream.* + routes: +{{#mappings}} + - when: + - method: POST + path: {{{path}}} + exit: kafka_cache_client + with: + capability: produce + topic: {{topic}} + key: ${idempotencyKey} + - when: + - method: PUT + path: {{{path}}}/{id} + exit: kafka_cache_client + with: + capability: produce + topic: {{topic}} + key: ${params.id} + - when: + - method: DELETE + path: {{{path}}}/{id} + exit: kafka_cache_client + with: + capability: produce + topic: {{topic}} + key: ${params.id} + - when: + - method: GET + path: {{{path}}} + exit: kafka_cache_client + with: + capability: fetch + topic: {{topic}} + merge: + content-type: application/json + - when: + - method: GET + path: {{{path}}}/{id} + exit: kafka_cache_client + with: + capability: fetch + topic: {{topic}} + filters: + - key: ${params.id} +{{/mappings}} + kafka_cache_client: + type: kafka + kind: cache_client +{{#glue}} + options: + topics: +{{#mappings}} + - name: {{topic}} + value: + model: avro + view: json + catalog: + glue_catalog: + - strategy: topic + version: latest +{{/mappings}} +{{/glue}} + telemetry: + metrics: + - stream.* + exit: kafka_cache_server + kafka_cache_server: + type: kafka + kind: cache_server + options: + bootstrap: +{{#mappings}} + - {{topic}} +{{/mappings}} +{{#glue}} + topics: +{{#mappings}} + - name: {{topic}} + value: + model: avro + view: json + catalog: + glue_catalog: + - strategy: topic + version: latest +{{/mappings}} +{{/glue}} + telemetry: + metrics: + - stream.* + exit: kafka_client + kafka_client: + type: kafka + kind: client + options: + servers: {{{kafka.bootstrapServers}}} + sasl: + mechanism: scram-sha-512 + username: '{{kafka.sasl.username}}' + password: '{{kafka.sasl.password}}' + telemetry: + metrics: + - stream.* + exit: tls_client + tls_client: + type: tls + kind: client + vault: secure + telemetry: + metrics: + - stream.* + exit: tcp_client + tcp_client: + type: tcp + kind: client + telemetry: + metrics: + - stream.* diff --git a/amazon-msk/cdktf/example-cluster/__tests__/main-test.ts b/amazon-msk/cdktf/example-cluster/__tests__/main-test.ts index 7110381..0e5ebe3 100644 --- a/amazon-msk/cdktf/example-cluster/__tests__/main-test.ts +++ b/amazon-msk/cdktf/example-cluster/__tests__/main-test.ts @@ -1,6 +1,6 @@ import "cdktf/lib/testing/adapters/jest"; import { Testing } from "cdktf"; -import { ZillaPlusExampleMskCluster } from "../main"; +import { ZillaPlusExampleMskCluster } from "../example-cluster-stack"; import { MskCluster } from "@cdktf/provider-aws/lib/msk-cluster"; import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group"; import { Subnet } from "@cdktf/provider-aws/lib/subnet"; @@ -26,6 +26,9 @@ describe("Zilla Plus Example MSK Cluster Test", () => { security_groups: ["${aws_security_group.MskSecurityGroup.id}"], }, client_authentication: { + sasl: { + scram: true + }, unauthenticated: true, }, cluster_name: "my-msk-cluster", diff --git a/amazon-msk/cdktf/example-cluster/cdktf.json b/amazon-msk/cdktf/example-cluster/cdktf.json index de4e547..6f9cb48 100644 --- a/amazon-msk/cdktf/example-cluster/cdktf.json +++ b/amazon-msk/cdktf/example-cluster/cdktf.json @@ -6,6 +6,9 @@ "terraformProviders": [], "terraformModules": [], "context": { - + "zilla-plus": + { + "mskCertificateAuthority": "" + } } } diff --git a/amazon-msk/cdktf/example-cluster/example-cluster-stack.ts b/amazon-msk/cdktf/example-cluster/example-cluster-stack.ts new file mode 100644 index 0000000..fe3f247 --- /dev/null +++ b/amazon-msk/cdktf/example-cluster/example-cluster-stack.ts @@ -0,0 +1,218 @@ +import { Construct } from "constructs"; +import { TerraformOutput, TerraformStack } from "cdktf"; +import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group"; +import { Subnet } from "@cdktf/provider-aws/lib/subnet"; +import { Vpc } from "@cdktf/provider-aws/lib/vpc"; +import { MskCluster } from "@cdktf/provider-aws/lib/msk-cluster"; +import { AwsProvider } from "@cdktf/provider-aws/lib/provider"; +import { DataAwsRegion } from "@cdktf/provider-aws/lib/data-aws-region"; +import { DataAwsAcmpcaCertificateAuthority } from "@cdktf/provider-aws/lib/data-aws-acmpca-certificate-authority"; +import { SecretsmanagerSecret } from "@cdktf/provider-aws/lib/secretsmanager-secret"; +import { SecretsmanagerSecretVersion } from "@cdktf/provider-aws/lib/secretsmanager-secret-version"; +import { IamPolicy } from "@cdktf/provider-aws/lib/iam-policy"; +import { IamPolicyAttachment } from "@cdktf/provider-aws/lib/iam-policy-attachment"; +import { IamRole } from "@cdktf/provider-aws/lib/iam-role"; +import { MskScramSecretAssociation } from "@cdktf/provider-aws/lib/msk-scram-secret-association"; +import { KmsKey } from "@cdktf/provider-aws/lib/kms-key"; +import { KmsAlias } from "@cdktf/provider-aws/lib/kms-alias"; + +export class ZillaPlusExampleMskCluster extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const awsProvider = new AwsProvider(this, "AWS", {}); + + const zillaPlusContext = this.node.tryGetContext('zilla-plus'); + const mskCertificateAuthority = zillaPlusContext?.mskCertificateAuthorityArn; + + if (mskCertificateAuthority) { + new DataAwsAcmpcaCertificateAuthority(this, "MSKCertificateAuthority", { + arn: mskCertificateAuthority, + }); + } + + const vpc = new Vpc(this, "MskVpc", { + cidrBlock: "10.0.0.0/16", + enableDnsHostnames: true, + enableDnsSupport: true, + tags: { + Name: "msk-vpc", + }, + }); + + const region = new DataAwsRegion(this, "CurrentRegion", { + provider: awsProvider, + }); + + const privateSubnet1 = new Subnet(this, "PrivateSubnet1", { + vpcId: vpc.id, + cidrBlock: "10.0.128.0/20", + availabilityZone: `${region.name}a`, + mapPublicIpOnLaunch: false, + tags: { + Name: "msk-private-subnet-1", + }, + }); + + const privateSubnet2 = new Subnet(this, "PrivateSubnet2", { + vpcId: vpc.id, + cidrBlock: "10.0.144.0/20", + availabilityZone: `${region.name}b`, + mapPublicIpOnLaunch: false, + tags: { + Name: "msk-private-subnet-2", + }, + }); + + const securityGroup = new SecurityGroup(this, "MskSecurityGroup", { + vpcId: vpc.id, + description: "Security group for MSK cluster", + ingress: [ + { + fromPort: 9092, + toPort: 9096, + protocol: "tcp", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 65535, + protocol: "tcp", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + tags: { + Name: "msk-security-group", + }, + }); + + const kmsKey = new KmsKey(this, "MskKmsKey", { + policy: JSON.stringify({ + Version: "2012-10-17", + Id: "key-consolepolicy-3", + Statement: [ + { + Sid: "Enable IAM User Permissions", + Effect: "Allow", + Principal: { + AWS: "*", + }, + Action: "kms:*", + Resource: "*", + }, + ], + }), + description: "KMS key for MSK", + keyUsage: "ENCRYPT_DECRYPT", + customerMasterKeySpec: "SYMMETRIC_DEFAULT", + isEnabled: true, + }); + + new KmsAlias(this, "MskKmsKeyAlias", { + name: "alias/AmazonMSK_key", + targetKeyId: kmsKey.keyId, + }); + + const saslScramSecret = new SecretsmanagerSecret(this, "SaslScramSecret", { + name: "AmazonMSK_alice", + kmsKeyId: kmsKey.keyId, + }); + + new SecretsmanagerSecretVersion(this, "SaslScramSecretVersion", { + secretId: saslScramSecret.id, + secretString: JSON.stringify({ + username: "alice", + password: "alice-secret", + }), + }); + + const mskIamRole = new IamRole(this, "MskIamRole", { + assumeRolePolicy: JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: { + Service: "kafka.amazonaws.com", + }, + Action: "sts:AssumeRole", + }, + ], + }), + }); + + const mskSecretsManagerPolicy = new IamPolicy( + this, + "MskSecretsManagerPolicy", + { + policy: JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Action: [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + ], + Resource: saslScramSecret.arn, + }, + ], + }), + } + ); + + new IamPolicyAttachment(this, "MskSecretsManagerPolicyAttachment", { + name: "msk-secrets-manager-policy-attachment", + roles: [mskIamRole.name], + policyArn: mskSecretsManagerPolicy.arn, + }); + + const mskCluster = new MskCluster(this, "MskCluster", { + clusterName: "my-msk-cluster", + kafkaVersion: "3.5.1", + numberOfBrokerNodes: 2, + brokerNodeGroupInfo: { + instanceType: "kafka.t3.small", + clientSubnets: [privateSubnet1.id, privateSubnet2.id], + securityGroups: [securityGroup.id], + }, + encryptionInfo: { + encryptionInTransit: { + clientBroker: "TLS_PLAINTEXT", + inCluster: true, + }, + }, + clientAuthentication: { + unauthenticated: true, + sasl: { + scram: true, + }, + }, + }); + + new MskScramSecretAssociation(this, "MskSecretsScramSecretAssociation", { + clusterArn: mskCluster.arn, + secretArnList: [saslScramSecret.arn], + }); + + if (mskCertificateAuthority) { + mskCluster.clientAuthentication.putTls({ + certificateAuthorityArns: [mskCertificateAuthority], + }); + } + + new TerraformOutput(this, "vpcId", { + value: vpc.id, + }); + + new TerraformOutput(this, "privateSubnetIds", { + value: [privateSubnet1.id, privateSubnet2.id], + }); + + new TerraformOutput(this, "mskClusterName", { + value: mskCluster.clusterName, + }); + } +} diff --git a/amazon-msk/cdktf/example-cluster/main.ts b/amazon-msk/cdktf/example-cluster/main.ts index b1fbc8d..81acdd3 100644 --- a/amazon-msk/cdktf/example-cluster/main.ts +++ b/amazon-msk/cdktf/example-cluster/main.ts @@ -1,230 +1,5 @@ -import { Construct } from "constructs"; -import { App, TerraformOutput, TerraformStack, TerraformVariable } from "cdktf"; -import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group"; -import { Subnet } from "@cdktf/provider-aws/lib/subnet"; -import { Vpc } from "@cdktf/provider-aws/lib/vpc"; -import { MskCluster } from "@cdktf/provider-aws/lib/msk-cluster"; -import { AwsProvider } from "@cdktf/provider-aws/lib/provider"; -import { DataAwsRegion } from "@cdktf/provider-aws/lib/data-aws-region"; -import { DataAwsAcmpcaCertificateAuthority } from "@cdktf/provider-aws/lib/data-aws-acmpca-certificate-authority"; -import { SecretsmanagerSecret } from "@cdktf/provider-aws/lib/secretsmanager-secret"; -import { SecretsmanagerSecretVersion } from "@cdktf/provider-aws/lib/secretsmanager-secret-version"; -import { IamPolicy } from "@cdktf/provider-aws/lib/iam-policy"; -import { IamPolicyAttachment } from "@cdktf/provider-aws/lib/iam-policy-attachment"; -import { IamRole } from "@cdktf/provider-aws/lib/iam-role"; -import { MskScramSecretAssociation } from "@cdktf/provider-aws/lib/msk-scram-secret-association"; -import { KmsKey } from "@cdktf/provider-aws/lib/kms-key"; -import { KmsAlias } from "@cdktf/provider-aws/lib/kms-alias"; - -export class ZillaPlusExampleMskCluster extends TerraformStack { - constructor(scope: Construct, id: string) { - super(scope, id); - - const awsProvider = new AwsProvider(this, "AWS", {}); - - const MTLS_ENABLED = process.env.MTLS_ENABLED === "true"; - let mskCertificateAuthority = ""; - if (MTLS_ENABLED) { - const mskCertificateAuthorityVar = new TerraformVariable( - this, - "msk_certificate_authority_arn", - { - type: "string", - description: - "ACM Private Certificate Authority ARN used to authorize clients connecting to the MSK cluster", - } - ); - new DataAwsAcmpcaCertificateAuthority(this, "MSKCertificateAuthority", { - arn: mskCertificateAuthorityVar.stringValue, - }); - mskCertificateAuthority = mskCertificateAuthorityVar.stringValue; - } - - const vpc = new Vpc(this, "MskVpc", { - cidrBlock: "10.0.0.0/16", - enableDnsHostnames: true, - enableDnsSupport: true, - tags: { - Name: "msk-vpc", - }, - }); - - const region = new DataAwsRegion(this, "CurrentRegion", { - provider: awsProvider, - }); - - const privateSubnet1 = new Subnet(this, "PrivateSubnet1", { - vpcId: vpc.id, - cidrBlock: "10.0.128.0/20", - availabilityZone: `${region.name}a`, - mapPublicIpOnLaunch: false, - tags: { - Name: "msk-private-subnet-1", - }, - }); - - const privateSubnet2 = new Subnet(this, "PrivateSubnet2", { - vpcId: vpc.id, - cidrBlock: "10.0.144.0/20", - availabilityZone: `${region.name}b`, - mapPublicIpOnLaunch: false, - tags: { - Name: "msk-private-subnet-2", - }, - }); - - const securityGroup = new SecurityGroup(this, "MskSecurityGroup", { - vpcId: vpc.id, - description: "Security group for MSK cluster", - ingress: [ - { - fromPort: 9092, - toPort: 9096, - protocol: "tcp", - cidrBlocks: ["0.0.0.0/0"], - }, - ], - egress: [ - { - fromPort: 0, - toPort: 65535, - protocol: "tcp", - cidrBlocks: ["0.0.0.0/0"], - }, - ], - tags: { - Name: "msk-security-group", - }, - }); - - const kmsKey = new KmsKey(this, "MskKmsKey", { - policy: JSON.stringify({ - Version: "2012-10-17", - Id: "key-consolepolicy-3", - Statement: [ - { - Sid: "Enable IAM User Permissions", - Effect: "Allow", - Principal: { - AWS: "*", - }, - Action: "kms:*", - Resource: "*", - }, - ], - }), - description: "KMS key for MSK", - keyUsage: "ENCRYPT_DECRYPT", - customerMasterKeySpec: "SYMMETRIC_DEFAULT", - isEnabled: true, - }); - - new KmsAlias(this, "MskKmsKeyAlias", { - name: "alias/AmazonMSK_key2", - targetKeyId: kmsKey.keyId, - }); - - const saslScramSecret = new SecretsmanagerSecret(this, "SaslScramSecret", { - name: "AmazonMSK_alice", - kmsKeyId: kmsKey.keyId, - }); - - new SecretsmanagerSecretVersion(this, "SaslScramSecretVersion", { - secretId: saslScramSecret.id, - secretString: JSON.stringify({ - username: "alice", - password: "alice-secret", - }), - }); - - const mskIamRole = new IamRole(this, "MskIamRole", { - assumeRolePolicy: JSON.stringify({ - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Principal: { - Service: "kafka.amazonaws.com", - }, - Action: "sts:AssumeRole", - }, - ], - }), - }); - - const mskSecretsManagerPolicy = new IamPolicy( - this, - "MskSecretsManagerPolicy", - { - policy: JSON.stringify({ - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Action: [ - "secretsmanager:GetSecretValue", - "secretsmanager:DescribeSecret", - ], - Resource: saslScramSecret.arn, - }, - ], - }), - } - ); - - new IamPolicyAttachment(this, "MskSecretsManagerPolicyAttachment", { - name: "msk-secrets-manager-policy-attachment", - roles: [mskIamRole.name], - policyArn: mskSecretsManagerPolicy.arn, - }); - - const mskCluster = new MskCluster(this, "MskCluster", { - clusterName: "my-msk-cluster", - kafkaVersion: "3.5.1", - numberOfBrokerNodes: 2, - brokerNodeGroupInfo: { - instanceType: "kafka.t3.small", - clientSubnets: [privateSubnet1.id, privateSubnet2.id], - securityGroups: [securityGroup.id], - }, - encryptionInfo: { - encryptionInTransit: { - clientBroker: "TLS_PLAINTEXT", - inCluster: true, - }, - }, - clientAuthentication: { - unauthenticated: true, - sasl: { - scram: true, - }, - }, - }); - - new MskScramSecretAssociation(this, "MskSecretsScramSecretAssociation", { - clusterArn: mskCluster.arn, - secretArnList: [saslScramSecret.arn], - }); - - if (MTLS_ENABLED) { - mskCluster.clientAuthentication.putTls({ - certificateAuthorityArns: [mskCertificateAuthority], - }); - } - - new TerraformOutput(this, "vpcId", { - value: vpc.id, - }); - - new TerraformOutput(this, "privateSubnetIds", { - value: [privateSubnet1.id, privateSubnet2.id], - }); - - new TerraformOutput(this, "mskClusterName", { - value: mskCluster.clusterName, - }); - } -} +import { App } from "cdktf"; +import { ZillaPlusExampleMskCluster } from "./example-cluster-stack"; const app = new App(); new ZillaPlusExampleMskCluster(app, "zilla-plus-example-cluster"); diff --git a/amazon-msk/cdktf/iot-ingest-and-control/.env.example b/amazon-msk/cdktf/iot-ingest-and-control/.env.example deleted file mode 100644 index fdb10ce..0000000 --- a/amazon-msk/cdktf/iot-ingest-and-control/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -MQTT_KAFKA_TOPIC_CREATION_DISABLED=false -CREATE_ZILLA_PLUS_ROLE=true -CREATE_ZILLA_PLUS_SECURITY_GROUP=true -CLOUDWATCH_DISABLED=false -SSH_KEY_ENABLED=false -CLOUDWATCH_DISABLED=false - diff --git a/amazon-msk/cdktf/iot-ingest-and-control/README.md b/amazon-msk/cdktf/iot-ingest-and-control/README.md index a21fc40..a564891 100644 --- a/amazon-msk/cdktf/iot-ingest-and-control/README.md +++ b/amazon-msk/cdktf/iot-ingest-and-control/README.md @@ -25,15 +25,22 @@ This guide will help you gather the necessary AWS values required to configure a If you don't have an existing MSK cluster you can use our example MSK deployment with basic configuration and Unauthorized access. Follow the instructions inside the [example-cluster](../example-cluster/README.md) folder to deploy the example MSK cluster. Note the `mskClusterName` from the outputs as you'll need this later. -## Required Terraform Variables +## Required CDKTF Context Variables -You can set these variable values in your `terraform.tfvars` file. To create a `.tfvars` from the example file run: +You can set these variables in your `context` in `cdktf.json` file under `zilla-plus` object. -```bash -cp terraform.tfvars.example terraform.tfvars + +### `msk` related variables + +```json + "msk": + { + "cluster": "", + "credentials": "" + }, ``` -### `msk_cluster_name`: MSK Cluster Name +#### `cluster`: MSK Cluster Name To get a list all MSK clusters run: @@ -43,11 +50,27 @@ aws kafka list-clusters --query 'ClusterInfoList[*].[ClusterName,ClusterArn]' -- Use the `ClusterName` of your desired MSK cluster for this variable. -### `msk_access_credentials_name`: MSK access credentials Secret Name +#### `credentials`: MSK Credentials Secret Name Provide the Secret Name that is associated with your MSK cluster. If you use our provided example cluster, there is already a secret associated with the cluster called `AmazonMSK_alice`. -### `public_tls_certificate_key`: Public TLS Certificate Key +List all secrets ub Secrets Manager that can be associated with MSK: + +```bash +aws secretsmanager list-secrets --query "SecretList[?starts_with(Name, 'AmazonMSK_')].Name" --output table +``` + +### `public` Zilla Plus variables + +```json + "public": + { + "certificate": "", + "port": "" + } +``` + +#### `certificate`: Public TLS Certificate Key You need the ARN of the Secrets Manager secret that contains your public TLS certificate private key. @@ -59,41 +82,31 @@ aws secretsmanager list-secrets --query 'SecretList[*].[Name,ARN]' --output tabl Find and note down the ARN of the secret that contains your public TLS certificate private key. -### `zilla_plus_capacity`: Zilla Plus Capacity +#### `port`: Public TCP Port -> Default: `2` +> Default: `8883` -This variable defines the initial number of Zilla Plus instances. +This variable defines the public port number to be used by REST and SSE clients. -### `zilla_plus_instance_type`: Zilla Plus EC2 Instance Type +### `capacity`: Zilla Plus Capacity -> Default: `t3.small` +> Default: `2` This variable defines the initial number of Zilla Plus instances. -### `public_port`: Public TCP Port +### `instanceType`: Zilla Plus EC2 Instance Type -> Default: `8883` +> Default: `t3.small` -This variable defines the public port number to be used by MQTT clients. +This variable defines the initial number of Zilla Plus instances. ## Optional Features -These features all have default values and can be configured using environment variables and terraform variables. If you don't plan to configure any of these features you can skip this section and go to the [Deploy stack using Terraform](#deploy-stack-using-terraform) section. - -### Environment Variables - -You can set these variable values in your runtime environment or with a `.env` file. If you don't plan on modifying any of the environment variable defaults you can skip this step. - -Create a `.env` file from the example file. - -```bash -cp .env.example .env -``` +These features all have default values and can be configured using cdk context variables. If you don't plan to configure any of these features you can skip this section and go to the [Deploy stack using Terraform](#deploy-stack-using-terraform) section. ### Internet Gateway ID -If you already have an Internet Gateway in the MSK's VPN it should be provided via the `IGW_ID` environment variable. If not set the deployment will attempt to create on in the VPC. +If you already have an Internet Gateway in the MSK's VPN it should be provided via the `igwId` context variable in your `cdktf.json` under `zilla-plus` object. If not set the deployment will attempt to create on in the VPC. To query the IGW_ID of your MSK's VPN use the following comman: ```bash @@ -104,21 +117,30 @@ aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC ### Kafka topics -By default, the deployment creates the provided Kafka topics required by Zilla Plus. To disable this set the environment variable `MQTT_KAFKA_TOPIC_CREATION_DISABLED` to `true` and set the `kafka_topic_mqtt_sessions`, `kafka_topic_mqtt_messages`, and `kafka_topic_mqtt_retained` in your `terraform.tfvars` file. +By default, the deployment creates the provided Kafka topics required by Zilla Plus. To disable this set the context variable `kafkaTopicCreationDisabled` to `true` and set the `sessions`, `messages`, and `retained` variables under the `topics` context variable in your `cdktf.json` file. -#### `kafka_topic_mqtt_sessions`: Kafka Topic for MQTT Sessions +```json + "topics": + { + "sessions": "", + "messages": "", + "retained": "" + } +``` + +#### `topics.sessions`: Kafka Topic for MQTT Sessions > Default: `mqtt-sessions` This variable defines the Kafka topic storing MQTT sessions with a cleanup policy set to "compact". -#### `kafka_topic_mqtt_messages`: Kafka Topic for MQTT Messages +#### `topics.messages`: Kafka Topic for MQTT Messages > Default: `mqtt-messages` This variable defines the Kafka topic storing MQTT messages with a cleanup policy set to "delete". -#### `kafka_topic_mqtt_retained`: Kafka Topic for MQTT Retained Messages +#### `topics.retained`: Kafka Topic for MQTT Retained Messages > Default: `mqtt-retained` @@ -126,7 +148,7 @@ This variable defines the Kafka topic storing MQTT retained messages with a clea ### Custom Zilla Plus Role -By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `CREATE_ZILLA_PLUS_ROLE` environment variable to `false` and adding `zilla_plus_role` to your `terraform.tfvars` file. +By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `roleName` context variable in your `cdktf.json` under `zilla-plus` object. List all IAM roles: @@ -138,7 +160,7 @@ Note down the role name `RoleName` of the desired IAM role. ### Custom Zilla Plus Security Groups -By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `CREATE_ZILLA_PLUS_SECURITY_GROUP` environment variable to `false` and adding `zilla_plus_security_groups` to your `terraform.tfvars` file. +By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `securityGroups` context variable in your `cdktf.json`. List all security groups: @@ -148,9 +170,24 @@ aws ec2 describe-security-groups --query 'SecurityGroups[*].[GroupId, GroupName] Note down the security group IDs (GroupId) of the desired security groups. -### Disable CloudWatch Integration +### CloudWatch Integration + +```json + "cloudwatch": + { + "disabled": false, + "logs": + { + "group": "" + }, + "metrics": + { + "namespace": "" + } + } +``` -By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `CLOUDWATCH_DISABLED` environment variable to `true`. +By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `cloudwatch.disabled` context variable to `true`. You can create or use existing log groups and metric namespaces in CloudWatch. @@ -164,7 +201,8 @@ aws logs describe-log-groups --query 'logGroups[*].[logGroupName]' --output tabl ``` This command will return a table listing the names of all the log groups in your CloudWatch. -In your `terraform.tfvars` file add the desired CloudWatch Logs Group for variable name `cloudwatch_logs_group` +In your `cdktf.json` file add the desired CloudWatch Logs Group for variable name `logs.group` under `zilla-plus` object in the `cloudwatch` variables section. + #### List All CloudWatch Custom Metric Namespaces @@ -172,11 +210,11 @@ In your `terraform.tfvars` file add the desired CloudWatch Logs Group for variab aws cloudwatch list-metrics --query 'Metrics[*].Namespace' --output text | tr '\t' '\n' | sort | uniq | grep -v '^AWS' ``` -In your `terraform.tfvars` file add the desired CloudWatch Metrics Namespace for variable name `cloudwatch_metrics_namespace` +In your `cdktf.json` file add the desired CloudWatch Metrics Namespace for variable name `metrics.namespace` under `zilla-plus` object in the `cloudwatch` variables section. ### Enable SSH Access -To enable SSH access to the instances, set the `SSH_KEY_ENABLED` environment variable to `true`. You will also need the name of an existing EC2 KeyPair to set the `zilla_plus_ssh_key` terraform variable. +To enable SSH access to the instances you will need the name of an existing EC2 KeyPair to set the `sshKey` context variable. List all EC2 KeyPairs: @@ -220,12 +258,6 @@ This command will generate the necessary Terraform JSON configuration files in t After synthesizing the configuration you can use `terraform` to deploy zilla. -Move your `.tfvars` file into the the generated dir or you can manually enter these values when prompted, or use a .tfvars file to provide them. - -```bash -cp terraform.tfvars cdktf.out/stacks/iot-ingest-and-control/terraform.tfvars -``` - Initialize terraform. ```bash diff --git a/amazon-msk/cdktf/iot-ingest-and-control/__tests__/main-test.ts b/amazon-msk/cdktf/iot-ingest-and-control/__tests__/main-test.ts index 3094df6..0cae382 100644 --- a/amazon-msk/cdktf/iot-ingest-and-control/__tests__/main-test.ts +++ b/amazon-msk/cdktf/iot-ingest-and-control/__tests__/main-test.ts @@ -1,6 +1,6 @@ import "cdktf/lib/testing/adapters/jest"; import { Testing } from "cdktf"; -import { ZillaPlusIotAndControlStack } from "../main"; +import { ZillaPlusIotAndControlStack } from "../iot-ingest-and-control-stack"; import { CloudwatchLogGroup } from "@cdktf/provider-aws/lib/cloudwatch-log-group"; import { autoscalingGroup, launchTemplate } from "@cdktf/provider-aws"; import { LbTargetGroup } from "@cdktf/provider-aws/lib/lb-target-group"; @@ -11,7 +11,22 @@ describe("Zilla Plus IOT and Control Stack Test", () => { let output: string; beforeAll(() => { - const app = Testing.app(); + const app = Testing.app({ + context: { + "zilla-plus": + { + "msk": + { + "cluster": "test-cluster", + "credentials": "test-credentials" + }, + "public": + { + "certificate": "test-certificate" + } + } + }}); + const stack = new ZillaPlusIotAndControlStack(app, "test"); output = Testing.synth(stack); }); @@ -20,7 +35,7 @@ describe("Zilla Plus IOT and Control Stack Test", () => { expect(output).toHaveResourceWithProperties( autoscalingGroup.AutoscalingGroup, { - desired_capacity: "${var.zilla_plus_capacity}", + desired_capacity: 2, launch_template: { id: "${aws_launch_template.ZillaPlusLaunchTemplate-test.id}", }, @@ -37,14 +52,14 @@ describe("Zilla Plus IOT and Control Stack Test", () => { it("should have cloudwatch group resource", async () => { expect(output).toHaveResourceWithProperties(CloudwatchLogGroup, { - name: "${var.cloudwatch_logs_group-test}", + name: "test-group", }); }); it("should have load balancer target group", async () => { expect(output).toHaveResourceWithProperties(LbTargetGroup, { name: "nlb-tg-test", - port: "${var.public_tcp_port}", + port: 8883, protocol: "TCP", vpc_id: "${data.aws_vpc.Vpc.id}", }); @@ -73,7 +88,7 @@ describe("Zilla Plus IOT and Control Stack Test", () => { }, ], load_balancer_arn: "${aws_lb.NetworkLoadBalancer-test.arn}", - port: "${var.public_tcp_port}", + port: 8883, protocol: "TCP", }); }); @@ -84,8 +99,7 @@ describe("Zilla Plus IOT and Control Stack Test", () => { name: "${aws_iam_instance_profile.zilla_plus_instance_profile.name}", }, image_id: "${data.aws_ami.LatestAmi.image_id}", - instance_type: "${var.zilla_plus_instance_type}", - key_name: "", + instance_type: "t3.small", network_interfaces: [ { associate_public_ip_address: "true", diff --git a/amazon-msk/cdktf/iot-ingest-and-control/cdktf.json b/amazon-msk/cdktf/iot-ingest-and-control/cdktf.json index 2974980..c9fd453 100644 --- a/amazon-msk/cdktf/iot-ingest-and-control/cdktf.json +++ b/amazon-msk/cdktf/iot-ingest-and-control/cdktf.json @@ -8,6 +8,17 @@ ], "terraformModules": [], "context": { - + "zilla-plus": + { + "msk": + { + "cluster": "", + "credentials": "" + }, + "public": + { + "certificate": "" + } + } } } diff --git a/amazon-msk/cdktf/iot-ingest-and-control/iot-ingest-and-control-stack.ts b/amazon-msk/cdktf/iot-ingest-and-control/iot-ingest-and-control-stack.ts new file mode 100644 index 0000000..070fa92 --- /dev/null +++ b/amazon-msk/cdktf/iot-ingest-and-control/iot-ingest-and-control-stack.ts @@ -0,0 +1,542 @@ +import { Construct, Node } from "constructs"; +import { TerraformStack, TerraformOutput, Fn, Op } from "cdktf"; +import { AwsProvider } from "@cdktf/provider-aws/lib/provider"; +import { Lb } from "@cdktf/provider-aws/lib/lb"; +import { LbListener } from "@cdktf/provider-aws/lib/lb-listener"; +import { dataAwsAmi, launchTemplate } from "@cdktf/provider-aws"; +import { autoscalingGroup } from "@cdktf/provider-aws"; +import { LbTargetGroup } from "@cdktf/provider-aws/lib/lb-target-group"; +import { DataAwsSecretsmanagerSecretVersion } from "@cdktf/provider-aws/lib/data-aws-secretsmanager-secret-version"; +import { CloudwatchLogGroup } from "@cdktf/provider-aws/lib/cloudwatch-log-group"; +import { DataAwsMskCluster } from "@cdktf/provider-aws/lib/data-aws-msk-cluster"; +import { DataAwsRegion } from "@cdktf/provider-aws/lib/data-aws-region"; +import { DataAwsAvailabilityZones } from "@cdktf/provider-aws/lib/data-aws-availability-zones"; +import { DataAwsMskBrokerNodes } from "@cdktf/provider-aws/lib/data-aws-msk-broker-nodes"; +import { DataAwsSubnet } from "@cdktf/provider-aws/lib/data-aws-subnet"; +import { DataAwsSubnets } from "@cdktf/provider-aws/lib/data-aws-subnets"; +import { DataAwsVpc } from "@cdktf/provider-aws/lib/data-aws-vpc"; +import { InternetGateway } from "@cdktf/provider-aws/lib/internet-gateway"; +import { Route } from "@cdktf/provider-aws/lib/route"; +import { RouteTable } from "@cdktf/provider-aws/lib/route-table"; +import { RouteTableAssociation } from "@cdktf/provider-aws/lib/route-table-association"; +import { Subnet } from "@cdktf/provider-aws/lib/subnet"; +import { IamInstanceProfile } from "@cdktf/provider-aws/lib/iam-instance-profile"; +import { IamRole } from "@cdktf/provider-aws/lib/iam-role"; +import { IamRolePolicy } from "@cdktf/provider-aws/lib/iam-role-policy"; +import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group"; +import Mustache = require("mustache"); +import fs = require("fs"); +import { DataAwsInternetGateway } from "@cdktf/provider-aws/lib/data-aws-internet-gateway"; +import { CloudwatchLogStream } from "@cdktf/provider-aws/lib/cloudwatch-log-stream"; + +interface TemplateData { + name: string; + cloudwatch?: object; + public?: object; + topics?: object; + kafka?: object; + +} + +function validateContextKeys(node: object, keys: string[]): void { + const missingKeys = []; + if (node instanceof Node) { + missingKeys.push(...keys.filter((key) => !node.tryGetContext(key))); + } else if (typeof node === 'object' && node !== null) { + missingKeys.push(...keys.filter((key) => !(key in node))); + } else { + var err =new Error(`Invalid node type. Must be either a constructs.Node or a JSON object.`); + throw err; + } + if (missingKeys.length > 0) { + throw new Error(`Missing required context variables: ${missingKeys.join(', ')}`); + } +} + +export class ZillaPlusIotAndControlStack extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const awsProvider = new AwsProvider(this, "AWS", {}); + + const region = new DataAwsRegion(this, "CurrentRegion", { + provider: awsProvider, + }); + + const mandatoryVariables = [ + 'msk', + 'public', + ]; + + const zillaPlusContext = this.node.tryGetContext('zilla-plus'); + validateContextKeys(zillaPlusContext, mandatoryVariables); + const msk = zillaPlusContext.msk; + const mandatoryMSKVariables = [ + 'cluster', + 'credentials' + ]; + + validateContextKeys(msk, mandatoryMSKVariables); + const mskClusterName = msk.cluster; + const mskCredentialsSecretName = msk.credentials; + + const publicVar = zillaPlusContext.public; + const mandatoryPublicVariables = [ + 'certificate', + ]; + validateContextKeys(publicVar, mandatoryPublicVariables); + const publicTlsCertificateKey = publicVar.certificate; + + const topics = zillaPlusContext.topics; + const kafkaTopicMqttSessions = topics?.sessions ?? "mqtt-sessions"; + const kafkaTopicMqttRetained = topics?.retained ?? "mqtt-retained"; + const kafkaTopicMqttMessages = topics?.messages ?? "mqtt-messages"; + + const mskCluster = new DataAwsMskCluster(this, "MSKCluster", { + clusterName: mskClusterName, + }); + + // Validate that the Credentials exists + new DataAwsSecretsmanagerSecretVersion(this, "mskAccessCredentials", { + secretId: mskCredentialsSecretName, + }); + + const mskClusterBrokerNodes = new DataAwsMskBrokerNodes(this, "MSKClusterBrokerNodes", { + clusterArn: mskCluster.arn, + }); + + const subnetId = mskClusterBrokerNodes.nodeInfoList.get(0).clientSubnet; + + const subnet = new DataAwsSubnet(this, "Subnet", { + id: subnetId, + }); + + const vpc = new DataAwsVpc(this, "Vpc", { + id: subnet.vpcId, + }); + + const subnets = new DataAwsSubnets(this, "PublicSubnets", { + filter: [ + { + name: "vpc-id", + values: [vpc.id], + }, + { + name: "mapPublicIpOnLaunch", + values: ["true"] + }, + ], + }); + + let igwId = zillaPlusContext.igwId;; + if (!igwId) + { + const igw = new InternetGateway(this, `InternetGateway-${id}`, { + vpcId: vpc.id, + tags: { + Name: "my-igw", + }, + }); + igwId = igw.id; + } + else + { + const existingIgw = new DataAwsInternetGateway(this, `ExistingInternetGateway-${id}`, { + filter: [ + { + name: "attachment.vpc-id", + values: [vpc.id], + }, + ], + }); + igwId = existingIgw.id; + } + + const publicRouteTable = new RouteTable(this, "PublicRouteTable", { + vpcId: vpc.id, + tags: { + Name: `public-route-table-${id}`, + }, + }); + + new Route(this, "PublicRoute", { + routeTableId: publicRouteTable.id, + destinationCidrBlock: "0.0.0.0/0", + gatewayId: igwId, + }); + + const availabilityZones = new DataAwsAvailabilityZones(this, "AZs", {}); + const subnetOffset = Fn.lengthOf(subnets.ids); + const subnetMask = Fn.parseint(Fn.element(Fn.split("/", vpc.cidrBlock), 1), 10); + const availableIpv4 = subnet.availableIpAddressCount; + // Math magic to find next power of 2 and based on that the subnetAddressPower + const subnetAddressPower = Fn.log(Fn.pow(2, Fn.ceil(Fn.log(availableIpv4, 2))), 2); + const subnetsMax = Op.sub(32, Op.add(subnetAddressPower, subnetMask)); + + const subnetIds = []; + for (let i = 1; i < 3; i++) { + const az = Fn.element(availabilityZones.names, i); + const subnetIndex = Op.add(subnetOffset, i); + const cidrBlock = Fn.cidrsubnet(vpc.cidrBlock, subnetsMax, Op.add(subnetIndex, i)); + + const subnet = new Subnet(this, `PublicSubnet${i}-${id}`, { + vpcId: vpc.id, + cidrBlock: cidrBlock, + availabilityZone: az, + mapPublicIpOnLaunch: true, + tags: { + Name: `public-subnet-${subnetIndex + 1}-${id}`, + }, + }); + + subnetIds.push(subnet.id); + + new RouteTableAssociation(this, `PublicSubnet${i}RouteTableAssociation-${id}`, { + subnetId: subnet.id, + routeTableId: publicRouteTable.id, + }); + } + + const bootstrapBrokers = [Fn.element(Fn.split(",", mskCluster.bootstrapBrokersSaslScram), 0)]; + + let zillaPlusRole = zillaPlusContext.roleName; + if (!zillaPlusRole) { + const iamRole = new IamRole(this, `zilla_plus_role-${id}`, { + name: `zilla_plus_role-${id}`, + assumeRolePolicy: JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: { + Service: "ec2.amazonaws.com", + }, + Action: "sts:AssumeRole", + }, + { + Effect: "Allow", + Principal: { + Service: "cloudformation.amazonaws.com", + }, + Action: "sts:AssumeRole", + }, + ], + }), + managedPolicyArns: [ + "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", + "arn:aws:iam::aws:policy/AWSCertificateManagerReadOnly", + "arn:aws:iam::aws:policy/AWSGlueSchemaRegistryReadonlyAccess", + "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess", + ], + inlinePolicy: [ + { + name: "ZillaPlusSecretsManagerRead", + policy: JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Sid: "VisualEditor0", + Effect: "Allow", + Action: [ + "acm-pca:GetCertificate", + "acm-pca:GetCertificateAuthorityCertificate", + "acm-pca:DescribeCertificateAuthority", + "tag:GetResources", + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "ogs:*", + "cloudwatch:GenerateQuery", + "cloudwatch:PutMetricData" + ], + Resource: ["*"], + }, + ], + }), + }, + ], + }); + + const iamInstanceProfile = new IamInstanceProfile(this, "zilla_plus_instance_profile", { + name: `zilla_plus_role-${id}`, + role: iamRole.name, + }); + + new IamRolePolicy(this, "ZillaPlusRolePolicy", { + role: iamRole.name, + policy: JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Sid: "secretStatement", + Effect: "Allow", + Action: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], + Resource: ["arn:aws:secretsmanager:*:*:secret:*"], + }, + { + Sid: "cloudwatchStatement", + Effect: "Allow", + Action: ['logs:*', 'cloudwatch:GenerateQuery', 'cloudwatch:PutMetricData'], + Resource: ["*"], + } + ], + }), + }); + + zillaPlusRole = iamInstanceProfile.name; + } + + const publicPort = publicVar.port ?? 8883; + + let zillaPlusSecurityGroups = zillaPlusContext.securityGroups; + + if (zillaPlusSecurityGroups) { + zillaPlusSecurityGroups = zillaPlusSecurityGroups.split(','); + } else { + const zillaPlusSG = new SecurityGroup(this, `ZillaPlusSecurityGroup-${id}`, { + vpcId: vpc.id, + description: "Security group for Zilla Plus", + ingress: [ + { + fromPort: publicPort, + toPort: publicPort, + protocol: "tcp", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 65535, + protocol: "tcp", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + tags: { + Name: "zilla-plus-security-group", + }, + }); + zillaPlusSecurityGroups = [zillaPlusSG.id]; + } + + const zillaPlusCapacity = zillaPlusContext.capacity ?? 2; + const keyName = zillaPlusContext.sshKey; + const instanceType = zillaPlusContext.instanceType ?? 't3.small'; + + // Validate that the Certificate Key exists + new DataAwsSecretsmanagerSecretVersion(this, "publicTlsCertificate", { + secretId: publicTlsCertificateKey, + }); + + const data: TemplateData = { + name: 'iot', + } + + const cloudwatch = zillaPlusContext.cloudwatch; + const cloudwatchDisabled = cloudwatch?.disabled ?? false; + + if (!cloudwatchDisabled) { + const defaultLogGroupName = `${id}-group`; + const defaultMetricNamespace = `${id}-namespace`; + + const logGroupName = cloudwatch?.logs?.group ?? defaultLogGroupName; + const metricNamespace = cloudwatch?.metrics?.namespace ?? defaultMetricNamespace; + + + const cloudWatchLogGroup = new CloudwatchLogGroup(this, `loggroup-${id}`, { + name: logGroupName + }); + + new CloudwatchLogStream(this, `LogStream-${id}`, { + logGroupName: cloudWatchLogGroup.name, + name: 'events' + }); + + data.cloudwatch = { + logs: { + group: logGroupName + }, + metrics: { + namespace: metricNamespace + }, + }; + } + + let imageId = zillaPlusContext.ami; + if (!imageId) { + const ami = new dataAwsAmi.DataAwsAmi(this, "LatestAmi", { + mostRecent: true, + filter: [ + { + name: "product-code", + values: ["ca5mgk85pjtbyuhtfluzisgzy"], + }, + { + name: "is-public", + values: ["true"], + }, + ] + }); + imageId = ami.imageId; + } + + const nlb = new Lb(this, `NetworkLoadBalancer-${id}`, { + name: `nlb-${id}`, + loadBalancerType: "network", + internal: false, + subnets: subnetIds, + securityGroups: zillaPlusSecurityGroups, + enableCrossZoneLoadBalancing: true, + }); + + const nlbTargetGroup = new LbTargetGroup(this, `NLBTargetGroup-${id}`, { + name: `nlb-tg-${id}`, + port: publicPort, + protocol: "TCP", + vpcId: vpc.id, + }); + + new LbListener(this, "NLBListener", { + loadBalancerArn: nlb.arn, + port: publicPort, + protocol: "TCP", + defaultAction: [ + { + type: "forward", + targetGroupArn: nlbTargetGroup.arn, + }, + ], + }); + + const kafkaSaslUsername = `\${{aws.secrets.${mskCredentialsSecretName}#username}}`; + const kafkaSaslPassword = `\${{aws.secrets.${mskCredentialsSecretName}#password}}`; + const kafkaBootstrapServers = `['${Fn.join(`','`, Fn.split(",", mskCluster.bootstrapBrokersSaslScram))}']`; + + data.kafka = { + bootstrapServers: kafkaBootstrapServers, + sasl : { + username: kafkaSaslUsername, + password: kafkaSaslPassword + } + } + data.public = { + port: publicPort, + tlsCertificateKey: publicTlsCertificateKey + } + data.topics = { + sessions: kafkaTopicMqttSessions, + messages: kafkaTopicMqttMessages, + retained: kafkaTopicMqttRetained + }; + + const kafkaTopicCreationDisabled = zillaPlusContext.kafkaTopicCreationDisabled ?? false; + + const yamlTemplate: string = fs.readFileSync('zilla.yaml.mustache', 'utf8'); + const renderedYaml: string = Mustache.render(yamlTemplate, data); + + const cfnHupConfContent = ` +[main] +stack=${id} +region=${region} + `; + + const cfnAutoReloaderConfContent = ` +[cfn-auto-reloader-hook] +triggers=post.update +path=Resources.MSKProxyLaunchTemplate.Metadata.AWS::CloudFormation::Init +action=/opt/aws/bin/cfn-init -v --stack ${id} --resource ZillaPlusLaunchTemplate --region ${region} +runas=root + `; + + let kafkaTopicCreationCommand = ""; + + if (!kafkaTopicCreationDisabled) { + kafkaTopicCreationCommand = ` +wget https://archive.apache.org/dist/kafka/3.5.1/kafka_2.13-3.5.1.tgz +tar -xzf kafka_2.13-3.5.1.tgz +cd kafka_2.13-3.5.1/libs +wget https://github.com/aws/aws-msk-iam-auth/releases/download/v1.1.1/aws-msk-iam-auth-1.1.1-all.jar +cd ../bin +SECRET_STRING=$(aws secretsmanager get-secret-value --secret-id ${mskCredentialsSecretName} --query SecretString --output text) +USERNAME=$(echo $SECRET_STRING | jq -r '.username') +PASSWORD=$(echo $SECRET_STRING | jq -r '.password') + +cat < client.properties +sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username=$USERNAME password=$PASSWORD; +security.protocol=SASL_SSL +sasl.mechanism=SCRAM-SHA-512 +EOF +./kafka-topics.sh --create --bootstrap-server ${bootstrapBrokers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${kafkaTopicMqttSessions} --config 'cleanup.policy=compact' +./kafka-topics.sh --create --bootstrap-server ${bootstrapBrokers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${kafkaTopicMqttRetained} --config 'cleanup.policy=compact' +./kafka-topics.sh --create --bootstrap-server ${bootstrapBrokers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${kafkaTopicMqttMessages} + `; + } + + const userData = `#!/bin/bash -xe +yum update -y aws-cfn-bootstrap +cat <<'END_HELP' > /etc/zilla/zilla.yaml +${renderedYaml} +END_HELP + +chown ec2-user:ec2-user /etc/zilla/zilla.yaml + +mkdir /etc/cfn +cat < /etc/cfn/cfn-hup.conf +${cfnHupConfContent} +EOF + +chown root:root /etc/cfn/cfn-hup.conf +chmod 0400 /etc/cfn/cfn-hup.conf + +mkdir /etc/cfn/hooks.d +cat < /etc/cfn/hooks.d/cfn-auto-reloader.conf +${cfnAutoReloaderConfContent} +EOF + +chown root:root /etc/cfn/hooks.d/cfn-auto-reloader.conf +chmod 0400 /etc/cfn/hooks.d/cfn-auto-reloader.conf + +systemctl enable cfn-hup +systemctl start cfn-hup +systemctl enable amazon-ssm-agent +systemctl start amazon-ssm-agent +systemctl enable zilla-plus +systemctl start zilla-plus + +${kafkaTopicCreationCommand} + + `; + + const MSKProxyLaunchTemplate = new launchTemplate.LaunchTemplate(this, `ZillaPlusLaunchTemplate-${id}`, { + imageId: imageId, + instanceType: instanceType, + networkInterfaces: [ + { + associatePublicIpAddress: "true", + deviceIndex: 0, + securityGroups: zillaPlusSecurityGroups, + }, + ], + iamInstanceProfile: { + name: zillaPlusRole, + }, + keyName: keyName, + userData: Fn.base64encode(userData), + }); + + new autoscalingGroup.AutoscalingGroup(this, `zillaPlusGroup-${id}`, { + vpcZoneIdentifier: subnetIds, + launchTemplate: { + id: MSKProxyLaunchTemplate.id, + }, + minSize: 1, + maxSize: 5, + desiredCapacity: zillaPlusCapacity, + targetGroupArns: [nlbTargetGroup.arn], + }); + + new TerraformOutput(this, "NetworkLoadBalancerOutput", { + description: "Public DNS name of newly created NLB for Public MSK Proxy", + value: nlb.dnsName, + }); + } +} diff --git a/amazon-msk/cdktf/iot-ingest-and-control/main.ts b/amazon-msk/cdktf/iot-ingest-and-control/main.ts index a916c6f..4b836ed 100644 --- a/amazon-msk/cdktf/iot-ingest-and-control/main.ts +++ b/amazon-msk/cdktf/iot-ingest-and-control/main.ts @@ -1,559 +1,5 @@ -import { Construct } from "constructs"; -import { App, TerraformStack, TerraformOutput, TerraformVariable, Fn, Op } from "cdktf"; -import { AwsProvider } from "@cdktf/provider-aws/lib/provider"; -import { Lb } from "@cdktf/provider-aws/lib/lb"; -import { LbListener } from "@cdktf/provider-aws/lib/lb-listener"; -import { dataAwsAmi, launchTemplate } from "@cdktf/provider-aws"; -import { autoscalingGroup } from "@cdktf/provider-aws"; -import { LbTargetGroup } from "@cdktf/provider-aws/lib/lb-target-group"; -import { DataAwsSecretsmanagerSecretVersion } from "@cdktf/provider-aws/lib/data-aws-secretsmanager-secret-version"; -import { CloudwatchLogGroup } from "@cdktf/provider-aws/lib/cloudwatch-log-group"; -import { DataAwsMskCluster } from "@cdktf/provider-aws/lib/data-aws-msk-cluster"; -import instanceTypes from "./instance-types"; -import { DataAwsRegion } from "@cdktf/provider-aws/lib/data-aws-region"; -import { DataAwsAvailabilityZones } from "@cdktf/provider-aws/lib/data-aws-availability-zones"; -import { DataAwsMskBrokerNodes } from "@cdktf/provider-aws/lib/data-aws-msk-broker-nodes"; -import { DataAwsSubnet } from "@cdktf/provider-aws/lib/data-aws-subnet"; -import { DataAwsSubnets } from "@cdktf/provider-aws/lib/data-aws-subnets"; -import { DataAwsVpc } from "@cdktf/provider-aws/lib/data-aws-vpc"; -import { InternetGateway } from "@cdktf/provider-aws/lib/internet-gateway"; -import { Route } from "@cdktf/provider-aws/lib/route"; -import { RouteTable } from "@cdktf/provider-aws/lib/route-table"; -import { RouteTableAssociation } from "@cdktf/provider-aws/lib/route-table-association"; -import { Subnet } from "@cdktf/provider-aws/lib/subnet"; -import { IamInstanceProfile } from "@cdktf/provider-aws/lib/iam-instance-profile"; -import { IamRole } from "@cdktf/provider-aws/lib/iam-role"; -import { IamRolePolicy } from "@cdktf/provider-aws/lib/iam-role-policy"; -import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group"; -import { UserVariables } from "./variables"; -import Mustache = require("mustache"); -import fs = require("fs"); -import { DataAwsInternetGateway } from "@cdktf/provider-aws/lib/data-aws-internet-gateway"; - -interface TemplateData { - name: string; - cloudwatch?: object; - public?: object; - topics?: object; - kafka?: object; - -} - -export class ZillaPlusIotAndControlStack extends TerraformStack { - constructor(scope: Construct, id: string) { - super(scope, id); - const userVariables = new UserVariables(this, "main"); - - const awsProvider = new AwsProvider(this, "AWS", {}); - - const region = new DataAwsRegion(this, "CurrentRegion", { - provider: awsProvider, - }); - - const mskClusterName = new TerraformVariable(this, "msk_cluster_name", { - type: "string", - description: "The name of the MSK cluster", - }); - - const mskCluster = new DataAwsMskCluster(this, "MSKCluster", { - clusterName: mskClusterName.stringValue, - }); - - const mskAccessCredentialsName = new TerraformVariable(this, "msk_access_credentials_name", { - type: "string", - description: "The MSK Access Credentials Secret Name with JSON properties; username, password", - }); - // Validate that the Credentials exists - const secret = new DataAwsSecretsmanagerSecretVersion(this, "mskAccessCredentials", { - secretId: mskAccessCredentialsName.stringValue, - }); - - const mskClusterBrokerNodes = new DataAwsMskBrokerNodes(this, "MSKClusterBrokerNodes", { - clusterArn: mskCluster.arn, - }); - - const subnetId = mskClusterBrokerNodes.nodeInfoList.get(0).clientSubnet; - - const subnet = new DataAwsSubnet(this, "Subnet", { - id: subnetId, - }); - - const vpc = new DataAwsVpc(this, "Vpc", { - id: subnet.vpcId, - }); - - const subnets = new DataAwsSubnets(this, "PublicSubnets", { - filter: [ - { - name: "vpc-id", - values: [vpc.id], - }, - { - name: "mapPublicIpOnLaunch", - values: ["true"] - }, - ], - }); - - let igwId; - if (userVariables.igwId) - { - const existingIgw = new DataAwsInternetGateway(this, `ExistingInternetGateway-${id}`, { - filter: [ - { - name: "attachment.vpc-id", - values: [vpc.id], - }, - ], - }); - igwId = existingIgw.id; - } - else - { - const igw = new InternetGateway(this, `InternetGateway-${id}`, { - vpcId: vpc.id, - tags: { - Name: `my-igw-${id}`, - }, - }); - igwId = igw.id; - } - - const publicRouteTable = new RouteTable(this, "PublicRouteTable", { - vpcId: vpc.id, - tags: { - Name: `public-route-table-${id}`, - }, - }); - - new Route(this, "PublicRoute", { - routeTableId: publicRouteTable.id, - destinationCidrBlock: "0.0.0.0/0", - gatewayId: igwId, - }); - - const availabilityZones = new DataAwsAvailabilityZones(this, "AZs", {}); - const subnetOffset = Fn.lengthOf(subnets.ids); - const subnetMask = Fn.parseint(Fn.element(Fn.split("/", vpc.cidrBlock), 1), 10); - const availableIpv4 = subnet.availableIpAddressCount; - // Math magic to find next power of 2 and based on that the subnetAddressPower - const subnetAddressPower = Fn.log(Fn.pow(2, Fn.ceil(Fn.log(availableIpv4, 2))), 2); - const subnetsMax = Op.sub(32, Op.add(subnetAddressPower, subnetMask)); - - const subnetIds = []; - for (let i = 1; i < 3; i++) { - const az = Fn.element(availabilityZones.names, i); - const subnetIndex = Op.add(subnetOffset, i); - const cidrBlock = Fn.cidrsubnet(vpc.cidrBlock, subnetsMax, Op.add(subnetIndex, i)); - - const subnet = new Subnet(this, `PublicSubnet${i}-${id}`, { - vpcId: vpc.id, - cidrBlock: cidrBlock, - availabilityZone: az, - mapPublicIpOnLaunch: true, - tags: { - Name: `public-subnet-${subnetIndex + 1}-${id}`, - }, - }); - - subnetIds.push(subnet.id); - - new RouteTableAssociation(this, `PublicSubnet${i}RouteTableAssociation-${id}`, { - subnetId: subnet.id, - routeTableId: publicRouteTable.id, - }); - } - - const kafkaTopicMqttSessions = new TerraformVariable(this, "kafka_topic_mqtt_sessions", { - type: "string", - description: 'The Kafka topic storing MQTT sessions, cleanup policy "compact"', - default: "mqtt-sessions", - }); - - const kafkaTopicMqttMessages = new TerraformVariable(this, "kafka_topic_mqtt_messages", { - type: "string", - description: 'The Kafka topic storing MQTT messages, cleanup policy "delete"', - default: "mqtt-messages", - }); - - const kafkaTopicMqttRetained = new TerraformVariable(this, "kafka_topic_mqtt_retained", { - type: "string", - description: 'The Kafka topic storing MQTT retained, cleanup policy "compact"', - default: "mqtt-retained", - }); - - const secretValue = Fn.jsondecode(secret.secretString); - const username = Fn.lookup(secretValue, "username"); - const password = Fn.lookup(secretValue, "password"); - - const bootstrapBrokers = [Fn.element(Fn.split(",", mskCluster.bootstrapBrokersSaslScram), 0)]; - - let zillaPlusRole; - if (!userVariables.createZillaPlusRole) { - const zillaPlusRoleVar = new TerraformVariable(this, "zilla_plus_role_name", { - type: "string", - description: "The role name assumed by Zilla Plus instances.", - }); - - zillaPlusRole = zillaPlusRoleVar.stringValue; - } else { - const iamRole = new IamRole(this, `zilla_plus_role-${id}`, { - name: `zilla_plus_role-${id}`, - assumeRolePolicy: JSON.stringify({ - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Principal: { - Service: "ec2.amazonaws.com", - }, - Action: "sts:AssumeRole", - }, - { - Effect: "Allow", - Principal: { - Service: "cloudformation.amazonaws.com", - }, - Action: "sts:AssumeRole", - }, - ], - }), - managedPolicyArns: [ - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", - "arn:aws:iam::aws:policy/AWSCertificateManagerReadOnly", - "arn:aws:iam::aws:policy/AWSGlueSchemaRegistryReadonlyAccess", - "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess", - ], - inlinePolicy: [ - { - name: "ZillaPlusSecretsManagerRead", - policy: JSON.stringify({ - Version: "2012-10-17", - Statement: [ - { - Sid: "VisualEditor0", - Effect: "Allow", - Action: [ - "acm-pca:GetCertificate", - "acm-pca:GetCertificateAuthorityCertificate", - "acm-pca:DescribeCertificateAuthority", - "tag:GetResources", - "secretsmanager:GetSecretValue", - "secretsmanager:DescribeSecret", - ], - Resource: ["*"], - }, - ], - }), - }, - ], - }); - - const iamInstanceProfile = new IamInstanceProfile(this, "zilla_plus_instance_profile", { - name: `zilla_plus_role-${id}`, - role: iamRole.name, - }); - - new IamRolePolicy(this, "ZillaPlusRolePolicy", { - role: iamRole.name, - policy: JSON.stringify({ - Version: "2012-10-17", - Statement: [ - { - Sid: "VisualEditor0", - Effect: "Allow", - Action: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], - Resource: ["arn:aws:secretsmanager:*:*:secret:*"], - }, - ], - }), - }); - - zillaPlusRole = iamInstanceProfile.name; - } - - const publicTcpPort = new TerraformVariable(this, "public_tcp_port", { - type: "number", - default: 8883, - description: "The public port number to be used by MQTT clients", - }); - - let zillaPlusSecurityGroups; - - if (!userVariables.createZillaPlusSecurityGroup) { - const zillaPlusSecurityGroupsVar = new TerraformVariable(this, "zilla_plus_security_groups", { - type: "list(string)", - description: "The security groups associated with Zilla Plus instances.", - }); - zillaPlusSecurityGroups = zillaPlusSecurityGroupsVar.listValue; - } else { - const zillaPlusSG = new SecurityGroup(this, `ZillaPlusSecurityGroup-${id}`, { - vpcId: vpc.id, - description: "Security group for Zilla Plus", - ingress: [ - { - fromPort: publicTcpPort.value, - toPort: publicTcpPort.value, - protocol: "tcp", - cidrBlocks: ["0.0.0.0/0"], - }, - ], - egress: [ - { - fromPort: 0, - toPort: 65535, - protocol: "tcp", - cidrBlocks: ["0.0.0.0/0"], - }, - ], - tags: { - Name: "zilla-plus-security-group", - }, - }); - zillaPlusSecurityGroups = [zillaPlusSG.id]; - } - - const zillaPlusCapacity = new TerraformVariable(this, "zilla_plus_capacity", { - type: "number", - default: 2, - description: "The initial number of Zilla Plus instances", - }); - - const publicTlsCertificateKey = new TerraformVariable(this, "public_tls_certificate_key", { - type: "string", - description: "TLS Certificate Private Key Secret ARN", - }); - // Validate that the Certificate Key exists - new DataAwsSecretsmanagerSecretVersion(this, "publicTlsCertificate", { - secretId: publicTlsCertificateKey.stringValue, - }); - - let keyName = ""; - - if (userVariables.sshKeyEnabled) { - const keyNameVar = new TerraformVariable(this, "zilla_plus_ssh_key", { - type: "string", - description: "Name of an existing EC2 KeyPair to enable SSH access to the instances", - }); - keyName = keyNameVar.stringValue; - } - - const instanceType = new TerraformVariable(this, "zilla_plus_instance_type", { - type: "string", - default: "t3.small", - description: "MSK Proxy EC2 instance type", - }); - instanceType.addValidation({ - condition: `${Fn.contains(instanceTypes.instanceTypes, instanceType.stringValue)}`, - errorMessage: "must be a valid EC2 instance type.", - }); - - const data: TemplateData = { - name: 'iot', - } - - if (!userVariables.cloudwatchDisabled) { - const defaultLogGroupName = `${id}-group`; - const defaultMetricNamespace = `${id}-namespace`; - - const cloudWatchLogsGroup = new TerraformVariable(this, `cloudwatch_logs_group-${id}`, { - type: "string", - description: "The Cloud Watch log group Zilla Plush should publish logs", - default: defaultLogGroupName, - }); - - const cloudWatchMetricsNamespace = new TerraformVariable(this, `cloudwatch_metrics_namespace-${id}`, { - type: "string", - description: "The Cloud Watch metrics namespace Zilla Plush should publish metrics", - default: defaultMetricNamespace, - }); - - new CloudwatchLogGroup(this, "loggroup", { - name: cloudWatchLogsGroup.stringValue, - }); - - data.cloudwatch = { - logs: { - group: cloudWatchLogsGroup.stringValue - }, - metrics: { - namespace: cloudWatchMetricsNamespace.stringValue - } - }; - } - - const ami = new dataAwsAmi.DataAwsAmi(this, "LatestAmi", { - mostRecent: true, - filter: [ - { - name: "product-code", - values: ["ca5mgk85pjtbyuhtfluzisgzy"], - }, - { - name: "is-public", - values: ["true"], - }, - ], - owners: ["679593333241"], - }); - - const nlb = new Lb(this, `NetworkLoadBalancer-${id}`, { - name: `nlb-${id}`, - loadBalancerType: "network", - internal: false, - subnets: subnetIds, - securityGroups: zillaPlusSecurityGroups, - enableCrossZoneLoadBalancing: true, - }); - - const nlbTargetGroup = new LbTargetGroup(this, `NLBTargetGroup-${id}`, { - name: `nlb-tg-${id}`, - port: publicTcpPort.value, - protocol: "TCP", - vpcId: vpc.id, - }); - - new LbListener(this, "NLBListener", { - loadBalancerArn: nlb.arn, - port: publicTcpPort.value, - protocol: "TCP", - defaultAction: [ - { - type: "forward", - targetGroupArn: nlbTargetGroup.arn, - }, - ], - }); - - const kafkaSaslUsername = Fn.join("", ["${{aws.secrets.", mskAccessCredentialsName.stringValue, "#username}}"]); - - const kafkaSaslPassword = Fn.join("", ["${{aws.secrets.", mskAccessCredentialsName.stringValue, "#password}}"]); - - const kafkaBootstrapServers = `['${Fn.join(`','`, Fn.split(",", mskCluster.bootstrapBrokersSaslScram))}']`; - - data.kafka = { - bootstrapServers: kafkaBootstrapServers, - sasl : { - username: kafkaSaslUsername, - password: kafkaSaslPassword - } - } - data.public = { - port: publicTcpPort.value, - tlsCertificateKey: publicTlsCertificateKey.stringValue - } - data.topics = { - sessions: kafkaTopicMqttSessions.stringValue, - messages: kafkaTopicMqttMessages.stringValue, - retained: kafkaTopicMqttRetained.stringValue - }; - - const yamlTemplate: string = fs.readFileSync('zilla.yaml.mustache', 'utf8'); - const renderedYaml: string = Mustache.render(yamlTemplate, data); - - const cfnHupConfContent = ` -[main] -stack=${id} -region=${region} - `; - - const cfnAutoReloaderConfContent = ` -[cfn-auto-reloader-hook] -triggers=post.update -path=Resources.MSKProxyLaunchTemplate.Metadata.AWS::CloudFormation::Init -action=/opt/aws/bin/cfn-init -v --stack ${id} --resource ZillaPlusLaunchTemplate --region ${region} -runas=root - `; - - let kafkaTopicCreationCommand = ""; - - if (!userVariables.mqttKafkaTopicCreationDisabled) { - kafkaTopicCreationCommand = ` -wget https://archive.apache.org/dist/kafka/3.5.1/kafka_2.13-3.5.1.tgz -tar -xzf kafka_2.13-3.5.1.tgz -cd kafka_2.13-3.5.1/libs -wget https://github.com/aws/aws-msk-iam-auth/releases/download/v1.1.1/aws-msk-iam-auth-1.1.1-all.jar -cd ../bin -cat <<'END_HELP'> client.properties -sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username='${username}' password='${password}'; -security.protocol=SASL_SSL -sasl.mechanism=SCRAM-SHA-512 -END_HELP -./kafka-topics.sh --create --bootstrap-server ${bootstrapBrokers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${kafkaTopicMqttSessions} --config 'cleanup.policy=compact' -./kafka-topics.sh --create --bootstrap-server ${bootstrapBrokers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${kafkaTopicMqttRetained} --config 'cleanup.policy=compact' -./kafka-topics.sh --create --bootstrap-server ${bootstrapBrokers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${kafkaTopicMqttMessages} - `; - } - - const userData = `#!/bin/bash -xe -yum update -y aws-cfn-bootstrap -cat <<'END_HELP' > /etc/zilla/zilla.yaml -${renderedYaml} -END_HELP - -chown ec2-user:ec2-user /etc/zilla/zilla.yaml - -mkdir /etc/cfn -cat < /etc/cfn/cfn-hup.conf -${cfnHupConfContent} -EOF - -chown root:root /etc/cfn/cfn-hup.conf -chmod 0400 /etc/cfn/cfn-hup.conf - -mkdir /etc/cfn/hooks.d -cat < /etc/cfn/hooks.d/cfn-auto-reloader.conf -${cfnAutoReloaderConfContent} -EOF - -chown root:root /etc/cfn/hooks.d/cfn-auto-reloader.conf -chmod 0400 /etc/cfn/hooks.d/cfn-auto-reloader.conf - -systemctl enable cfn-hup -systemctl start cfn-hup -systemctl enable amazon-ssm-agent -systemctl start amazon-ssm-agent -systemctl enable zilla-plus -systemctl start zilla-plus - -${kafkaTopicCreationCommand} - - `; - - const MSKProxyLaunchTemplate = new launchTemplate.LaunchTemplate(this, `ZillaPlusLaunchTemplate-${id}`, { - imageId: ami.imageId, - instanceType: instanceType.stringValue, - networkInterfaces: [ - { - associatePublicIpAddress: "true", - deviceIndex: 0, - securityGroups: zillaPlusSecurityGroups, - }, - ], - iamInstanceProfile: { - name: zillaPlusRole, - }, - keyName: keyName, - userData: Fn.base64encode(userData), - }); - - new autoscalingGroup.AutoscalingGroup(this, `zillaPlusGroup-${id}`, { - vpcZoneIdentifier: subnetIds, - launchTemplate: { - id: MSKProxyLaunchTemplate.id, - }, - minSize: 1, - maxSize: 5, - desiredCapacity: zillaPlusCapacity.numberValue, - targetGroupArns: [nlbTargetGroup.arn], - }); - - new TerraformOutput(this, "NetworkLoadBalancerOutput", { - description: "Public DNS name of newly created NLB for Public MSK Proxy", - value: nlb.dnsName, - }); - } -} +import { App } from "cdktf"; +import { ZillaPlusIotAndControlStack } from "./iot-ingest-and-control-stack"; const app = new App(); new ZillaPlusIotAndControlStack(app, "iot-ingest-and-control"); diff --git a/amazon-msk/cdktf/iot-ingest-and-control/terraform.tfvars.example b/amazon-msk/cdktf/iot-ingest-and-control/terraform.tfvars.example deleted file mode 100644 index 54c8d13..0000000 --- a/amazon-msk/cdktf/iot-ingest-and-control/terraform.tfvars.example +++ /dev/null @@ -1,14 +0,0 @@ -msk_cluster_name="" -msk_access_credentials_name="" -public_tls_certificate_key="" - -## optional -#kafka_topic_mqtt_sessions="" -#kafka_topic_mqtt_messages="" -#kafka_topic_mqtt_retained="" -#public_tcp_port= -#zilla_plus_security_groups=[""] -#zilla_plus_role="" -#zilla_plus_ssh_key="" -#cloudwatch_logs_group="" -#cloudwatch_metrics_namespace="" diff --git a/amazon-msk/cdktf/iot-ingest-and-control/variables.ts b/amazon-msk/cdktf/iot-ingest-and-control/variables.ts deleted file mode 100644 index 510ffea..0000000 --- a/amazon-msk/cdktf/iot-ingest-and-control/variables.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as dotenvx from "@dotenvx/dotenvx"; -import { Construct } from "constructs"; - -export class UserVariables extends Construct { - publicCertificateAuthority: boolean = false; - createZillaPlusRole: boolean = false; - createZillaPlusSecurityGroup: boolean = false; - sshKeyEnabled: boolean = false; - cloudwatchDisabled: boolean = false; - mqttKafkaTopicCreationDisabled: boolean = false; - igwId: string | undefined; - - constructor(scope: Construct, name: string) { - super(scope, name); - dotenvx.config({ quiet: true }); - - this.createZillaPlusRole = process.env.CREATE_ZILLA_PLUS_ROLE !== "false"; - this.createZillaPlusSecurityGroup = process.env.CREATE_ZILLA_PLUS_SECURITY_GROUP !== "false"; - this.sshKeyEnabled = process.env.SSH_KEY_ENABLED === "true"; - this.cloudwatchDisabled = process.env.CLOUDWATCH_DISABLED === "true"; - this.mqttKafkaTopicCreationDisabled = process.env.MQTT_KAFKA_TOPIC_CREATION_DISABLED === "true"; - this.igwId = process.env.IGW_ID; - } -} diff --git a/amazon-msk/cdktf/secure-public-access/.env.example b/amazon-msk/cdktf/secure-public-access/.env.example deleted file mode 100644 index 79313db..0000000 --- a/amazon-msk/cdktf/secure-public-access/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -CREATE_ZILLA_PLUS_ROLE=true -CREATE_ZILLA_PLUS_SECURITY_GROUP=true -SSH_KEY_ENABLED=false -CLOUDWATCH_DISABLED=false -#MSK_ACCESS_METHOD= - -## mTLS Specific Variables -#PUBLIC_CERTIFICATE_AUTHORITY= diff --git a/amazon-msk/cdktf/secure-public-access/README.md b/amazon-msk/cdktf/secure-public-access/README.md index 64e582e..62e8e49 100644 --- a/amazon-msk/cdktf/secure-public-access/README.md +++ b/amazon-msk/cdktf/secure-public-access/README.md @@ -25,15 +25,21 @@ This guide will help you gather the necessary AWS values required to configure a If you don't have an existing MSK cluster you can use our example MSK deployment with basic configuration and Unauthorized access. Follow the instructions inside the [example-cluster](../example-cluster/README.md) folder to deploy the example MSK cluster. Note the `mskClusterName` from the outputs as you'll need this later. You will need to set the [MSK client auth method](#msk-client-authentication-method) env var to `Unauthorized`. -## Required Terraform Variables +## Required CDKTF Context Variables -You can set these variable values in your `terraform.tfvars` file. To create a `.tfvars` from the example file run: +You can set these variables in your `context` in `cdktf.json` file under `zilla-plus` object. -```bash -cp terraform.tfvars.example terraform.tfvars +### `msk` related variables + +```json + "msk": + { + "cluster": "", + "clientAuthentication": "" + }, ``` -### `msk_cluster_name`: MSK Cluster Name +### `cluster`: MSK Cluster Name To get a list all MSK clusters run: @@ -42,9 +48,21 @@ aws kafka list-clusters --query 'ClusterInfoList[*].{Name:ClusterName, Arn:Clust ``` Use the `ClusterName` of your desired MSK cluster for this variable. -Set the desired client authentication method based on the MSK cluster setup, using `MSK_ACCESS_METHOD` environment variable. +Set the desired client authentication method based on the MSK cluster setup, using `clientAuthentication` variable. Allowed values are: `SASL/SCRAM`, `mTLS`, `Unauthorized`. + + +### `public` Zilla Plus variables -### `public_tls_certificate_key`: Public TLS Certificate Key +```json + "public": + { + "wildcardDNS": "", + "certificate": "", + "port": "" + } +``` + +#### `certificate`: Public TLS Certificate Key You need the ARN of either the Certificte Manager certificate or the Secrets Manager secret that contains your public TLS certificate private key. @@ -64,36 +82,36 @@ aws secretsmanager list-secrets --query 'SecretList[*].[Name,ARN]' --output tabl Find and note down the ARN of the secret that contains your public TLS certificate private key. -### `public_wildcard_dns`: Public Wildcard DNS +#### `wildcardDNS`: Public Wildcard DNS This variable defines the public wildcard DNS pattern for bootstrap servers to be used by Kafka clients. It should match the wildcard DNS of the public TLS certificate. -### `zilla_plus_capacity`: Zilla Plus Capacity +#### `port`: Public TCP Port -> Default: `2` +> Default: `9094` -This variable defines the initial number of Zilla Plus instances. +This variable defines the public port number to be used by Kafka clients. -### `zilla_plus_instance_type`: Zilla Plus EC2 Instance Type +### `capacity`: Zilla Plus Capacity -> Default: `t3.small` +> Default: `2` This variable defines the initial number of Zilla Plus instances. -### `public_port`: Public TCP Port +### `instanceType`: Zilla Plus EC2 Instance Type -> Default: `9094` +> Default: `t3.small` -This variable defines the public port number to be used by Kafka clients. +This variable defines the initial number of Zilla Plus instances. ### mTLS Specific Variables You only need to add these if you choose mTLS as client authentication method -#### `msk_certificate_authority_arn`: MSK Certificate Authority ARN +#### `certificateAuthorityArn`: MSK Certificate Authority ARN -This variable defines the ACM Private Certificate Authority ARN used to authorize clients connecting to the MSK cluster. +This variable defines the ACM Private Certificate Authority ARN used to authorize clients connecting to the MSK cluster. You can set this in the context variable in your `cdktf.json` file under `zilla-plus` object in the `msk` variables section. List all ACM Private Certificate Authorities: @@ -105,21 +123,11 @@ Note down the ARN of the ACM Private Certificate Authority you want to use. ## Optional Features -These features all have default values and can be configured using environment variables and terraform variables. If you don't plan to configure any of these features you can skip this section and go to the [Deploy stack using Terraform](#deploy-stack-using-terraform) section. - -### Environment Variables - -You can set these variable values in your runtime environment or with a `.env` file. If you don't plan on modifying any of the environment variable defaults you can skip this step. - -Create a `.env` file from the example file. - -```bash -cp .env.example .env -``` +These features all have default values and can be configured using cdk context variables. If you don't plan to configure any of these features you can skip this section and go to the [Deploy stack using Terraform](#deploy-stack-using-terraform) section. ### Internet Gateway ID -If you already have an Internet Gateway in the MSK's VPN it should be provided via the `IGW_ID` environment variable. If not set the deployment will attempt to create on in the VPC. +If you already have an Internet Gateway in the MSK's VPN it should be provided via the `igwId` context variable in your `cdktf.json` under `zilla-plus` object. If not set the deployment will attempt to create on in the VPC. To query the IGW_ID of your MSK's VPN use the following comman: ```bash @@ -127,17 +135,13 @@ VPC_ID=$(aws kafka describe-cluster --cluster-arn --query "Clu aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" --query "InternetGateways[0].InternetGatewayId" --output text ``` -### MSK Client Authentication Method - -To specify which client authentication method Zilla should use set the `MSK_ACCESS_METHOD` environment variable to the desired access method (mTLS, SASL/SCRAM or Unauthorized). - ### Public TLS Certificate via AWS Certificate Manager for Nitro Enclaves -By default Zilla Plus will assume TLS certificate coming from Secrets Manager. You can use Zilla Plus with TLS certificate via ACM. To enable this set `PUBLIC_TLS_CERTIFICATE_VIA_ACM` to `true`. +If you want to enable Zilla-plus Nitro Enclaves support all you have to do is provide the `public.certificate` context variable via ACM. ### Custom Zilla Plus Role -By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `CREATE_ZILLA_PLUS_ROLE` environment variable to `false` and adding `zilla_plus_role` to your `terraform.tfvars` file. +By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `roleName` context variable in your `cdktf.json` under `zilla-plus` object. List all IAM roles: @@ -149,7 +153,7 @@ Note down the role name `RoleName` of the desired IAM role. ### Custom Zilla Plus Security Groups -By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `CREATE_ZILLA_PLUS_SECURITY_GROUP` environment variable to `false` and adding `zilla_plus_security_groups` to your `terraform.tfvars` file. +By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `securityGroups` context variable in your `cdktf.json` under `zilla-plus` object. List all security groups: @@ -159,10 +163,10 @@ aws ec2 describe-security-groups --query 'SecurityGroups[*].[GroupId, GroupName] Note down the security group IDs (GroupId) of the desired security groups. -#### Separate Public Certificate Authority ARN +### Separate Public Certificate Authority ARN This variable defines the ACM Private Certificate Authority ARN used to authorize clients connecting to the Public Zilla Plus. -By default Zilla Plus will use the `msk_certificate_authority_arn` for the Public Certificate Authority. If you want to change this set `PUBLIC_CERTIFICATE_AUTHORITY` environment variable to `true` and define the `public_certificate_authority_arn` in your `terraform.tfvars` file. +By default Zilla Plus will use the `msk.certificateAuthorityArn` for the Public Certificate Authority. If you want to change this set `certificateAuthorityArn` context variable in your `cdktf.json` file under `zilla-plus` object in the `public` variables section. List all ACM Private Certificate Authorities: @@ -172,35 +176,50 @@ aws acm-pca list-certificate-authorities --query 'CertificateAuthorities[*].[Arn Note down the ARN of the ACM Private Certificate Authority you want to use. -### Disable CloudWatch Integration +### CloudWatch Integration + +```json + "cloudwatch": + { + "disabled": false, + "logs": + { + "group": "" + }, + "metrics": + { + "namespace": "" + } + } +``` -By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `CLOUDWATCH_DISABLED` environment variable to `true`. +By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `cloudwatch.disabled` context variable to `true`. You can create or use existing log groups and metric namespaces in CloudWatch. By default, the deployment creates a CloudWatch Log Groups and Custom Metrics Namespace. If you want to define your own, follow these steps. -#### List All CloudWatch Log Groups (cloudwatch_logs_group) +#### List All CloudWatch Log Groups ```bash aws logs describe-log-groups --query 'logGroups[*].[logGroupName]' --output table ``` This command will return a table listing the names of all the log groups in your CloudWatch. -In your `terraform.tfvars` file add the desired CloudWatch Logs Group for variable name `cloudwatch_logs_group` +In your `cdktf.json` file add the desired CloudWatch Logs Group for variable name `logs.group` under `zilla-plus` object in the `cloudwatch` variables section. -#### List All CloudWatch Custom Metric Namespaces (cloudwatch_metrics_namespace) +#### List All CloudWatch Custom Metric Namespaces ```bash aws cloudwatch list-metrics --query 'Metrics[*].Namespace' --output text | tr '\t' '\n' | sort | uniq | grep -v '^AWS' ``` -In your `terraform.tfvars` file add the desired CloudWatch Metrics Namespace for variable name `cloudwatch_metrics_namespace` +In your `cdktf.json` file add the desired CloudWatch Metrics Namespace for variable name `metrics.namespace` under `zilla-plus` object in the `cloudwatch` variables section. ### Enable SSH Access -To enable SSH access to the instances, set the `SSH_KEY_ENABLED` environment variable to `true`. You will also need the name of an existing EC2 KeyPair to set the `zilla_plus_ssh_key` terraform variable. +To enable SSH access to the instances you will need the name of an existing EC2 KeyPair to set the `sshKey` context variable under `zilla-plus` object. List all EC2 KeyPairs: @@ -248,12 +267,6 @@ This command will generate the necessary Terraform JSON configuration files in t After synthesizing the configuration you can use `terraform` to deploy zilla. -Move your `.tfvars` file into the the generated dir or you can manually enter these values when prompted, or use a .tfvars file to provide them. - -```bash -cp terraform.tfvars cdktf.out/stacks/secure-public-access/terraform.tfvars -``` - Initialize terraform. ```bash diff --git a/amazon-msk/cdktf/secure-public-access/__tests__/main-test.ts b/amazon-msk/cdktf/secure-public-access/__tests__/main-test.ts index f7085e2..d861480 100644 --- a/amazon-msk/cdktf/secure-public-access/__tests__/main-test.ts +++ b/amazon-msk/cdktf/secure-public-access/__tests__/main-test.ts @@ -1,6 +1,6 @@ import "cdktf/lib/testing/adapters/jest"; import { Testing } from "cdktf"; -import { ZillaPlusSecurePublicAccessStack } from "../main"; +import { ZillaPlusSecurePublicAccessStack } from "../secure-public-acces-stack"; import { CloudwatchLogGroup } from "@cdktf/provider-aws/lib/cloudwatch-log-group"; import { autoscalingGroup, launchTemplate } from "@cdktf/provider-aws"; import { LbTargetGroup } from "@cdktf/provider-aws/lib/lb-target-group"; @@ -11,7 +11,22 @@ describe("Zilla Plus Public Access Stack Test", () => { let output: string; beforeAll(() => { - const app = Testing.app(); + const app = Testing.app({ + context: { + "zilla-plus": + { + "msk": + { + "cluster": "test-cluster", + "clientAuthentication": "SASL/SCRAM" + }, + "public": + { + "certificate": "test-certificate", + "wildcardDNS": "*.example.aklivity.io" + } + } + }}); const stack = new ZillaPlusSecurePublicAccessStack(app, "test"); output = Testing.synth(stack); }); @@ -39,12 +54,8 @@ describe("Zilla Plus Public Access Stack Test", () => { }); it("should have cloudwatch group resource", async () => { - const app = Testing.app(); - const stack = new ZillaPlusSecurePublicAccessStack(app, "test"); - const output = Testing.synth(stack); - expect(output).toHaveResourceWithProperties(CloudwatchLogGroup, { - name: "${var.cloudwatch_logs_group}", + name: "test-group", }); }); @@ -52,7 +63,7 @@ describe("Zilla Plus Public Access Stack Test", () => { expect(output).toHaveResourceWithProperties(LbTargetGroup, { vpc_id: "${data.aws_vpc.Vpc.id}", name: "nlb-tg-test", - port: "${var.public_port}", + port: 9094, protocol: "TCP", }); }); @@ -79,7 +90,7 @@ describe("Zilla Plus Public Access Stack Test", () => { }, ], load_balancer_arn: "${aws_lb.NetworkLoadBalancer-test.arn}", - port: "${var.public_port}", + port: 9094, protocol: "TCP", }); }); @@ -90,8 +101,7 @@ describe("Zilla Plus Public Access Stack Test", () => { name: "${aws_iam_instance_profile.zilla_plus_instance_profile-test.name}", }, image_id: "${data.aws_ami.LatestAmi.image_id}", - instance_type: "${var.zilla_plus_instance_type}", - key_name: "", + instance_type: "t3.small", network_interfaces: [ { associate_public_ip_address: "true", diff --git a/amazon-msk/cdktf/secure-public-access/cdktf.json b/amazon-msk/cdktf/secure-public-access/cdktf.json index c75930d..ae626c2 100644 --- a/amazon-msk/cdktf/secure-public-access/cdktf.json +++ b/amazon-msk/cdktf/secure-public-access/cdktf.json @@ -8,6 +8,18 @@ ], "terraformModules": [], "context": { - + "zilla-plus": + { + "msk": + { + "cluster": "", + "clientAuthentication": "", + }, + "public": + { + "wildcardDNS": "", + "certificate": "" + } + } } } diff --git a/amazon-msk/cdktf/secure-public-access/main.ts b/amazon-msk/cdktf/secure-public-access/main.ts index 34f19a3..ace221c 100644 --- a/amazon-msk/cdktf/secure-public-access/main.ts +++ b/amazon-msk/cdktf/secure-public-access/main.ts @@ -1,642 +1,5 @@ -import { Construct } from "constructs"; -import { App, TerraformStack, TerraformOutput, TerraformVariable, Fn, Op } from "cdktf"; -import instanceTypes from "./instance-types"; -import { Lb } from "@cdktf/provider-aws/lib/lb"; -import { LbListener } from "@cdktf/provider-aws/lib/lb-listener"; -import { autoscalingGroup, dataAwsAmi, launchTemplate } from "@cdktf/provider-aws"; -import { LbTargetGroup } from "@cdktf/provider-aws/lib/lb-target-group"; -import { DataAwsAcmpcaCertificateAuthority } from "@cdktf/provider-aws/lib/data-aws-acmpca-certificate-authority"; -import { DataAwsSecretsmanagerSecretVersion } from "@cdktf/provider-aws/lib/data-aws-secretsmanager-secret-version"; -import { CloudwatchLogGroup } from "@cdktf/provider-aws/lib/cloudwatch-log-group"; -import { DataAwsMskCluster } from "@cdktf/provider-aws/lib/data-aws-msk-cluster"; -import { InternetGateway } from "@cdktf/provider-aws/lib/internet-gateway"; -import { Route } from "@cdktf/provider-aws/lib/route"; -import { RouteTable } from "@cdktf/provider-aws/lib/route-table"; -import { RouteTableAssociation } from "@cdktf/provider-aws/lib/route-table-association"; -import { Subnet } from "@cdktf/provider-aws/lib/subnet"; -import { IamRole } from "@cdktf/provider-aws/lib/iam-role"; -import { IamRolePolicy } from "@cdktf/provider-aws/lib/iam-role-policy"; -import { DataAwsMskBrokerNodes } from "@cdktf/provider-aws/lib/data-aws-msk-broker-nodes"; -import { DataAwsSubnet } from "@cdktf/provider-aws/lib/data-aws-subnet"; -import { DataAwsVpc } from "@cdktf/provider-aws/lib/data-aws-vpc"; -import { DataAwsRegion } from "@cdktf/provider-aws/lib/data-aws-region"; -import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group"; -import { DataAwsAvailabilityZones } from "@cdktf/provider-aws/lib/data-aws-availability-zones"; -import { DataAwsSubnets } from "@cdktf/provider-aws/lib/data-aws-subnets"; -import { IamInstanceProfile } from "@cdktf/provider-aws/lib/iam-instance-profile"; - - -import { UserVariables } from "./variables"; -import { AwsProvider } from "@cdktf/provider-aws/lib/provider"; -import { ec2EnclaveCertificateIamRoleAssociation } from "./.gen/providers/awscc" -import { AwsccProvider } from "./.gen/providers/awscc/provider"; -import Mustache = require("mustache"); -import fs = require("fs"); -import { DataAwsInternetGateway } from "@cdktf/provider-aws/lib/data-aws-internet-gateway"; - -interface TemplateData { - name: string; - useAcm: boolean; - cloudwatch?: object; - public?: object; - mTLS?: boolean; - externalHost?: string; - internalHost?: string; - msk?: object; -} - -export class ZillaPlusSecurePublicAccessStack extends TerraformStack { - constructor(scope: Construct, id: string) { - super(scope, id); - const userVariables = new UserVariables(this, "main"); - - const awsProvider = new AwsProvider(this, "AWS", { }); - new AwsccProvider(this, "AWSCC", { }); - - const region = new DataAwsRegion(this, "CurrentRegion", { - provider: awsProvider, - }); - - let mskPort; - let mskWildcardDNS; - let mskCertificateAuthority; - - const mskClusterName = new TerraformVariable(this, "msk_cluster_name", { - type: "string", - description: "The name of the MSK cluster", - }); - - const mskCluster = new DataAwsMskCluster(this, "MSKCluster", { - clusterName: mskClusterName.stringValue, - }); - - const mskClusterBrokerNodes = new DataAwsMskBrokerNodes(this, "MSKClusterBrokerNodes", { - clusterArn: mskCluster.arn, - }); - - const subnetId = mskClusterBrokerNodes.nodeInfoList.get(0).clientSubnet; - - const subnet = new DataAwsSubnet(this, "Subnet", { - id: subnetId, - }); - - const vpc = new DataAwsVpc(this, "Vpc", { - id: subnet.vpcId, - }); - - const subnets = new DataAwsSubnets(this, "PublicSubnets", { - filter: [ - { - name: "vpc-id", - values: [vpc.id], - }, - { - name: "mapPublicIpOnLaunch", - values: ["true"] - }, - ], - }); - - let igwId; - if (userVariables.igwId) - { - const existingIgw = new DataAwsInternetGateway(this, `ExistingInternetGateway-${id}`, { - filter: [ - { - name: "attachment.vpc-id", - values: [vpc.id], - }, - ], - }); - igwId = existingIgw.id; - } - else - { - const igw = new InternetGateway(this, `InternetGateway-${id}`, { - vpcId: vpc.id, - tags: { - Name: "my-igw", - }, - }); - igwId = igw.id; - } - - const publicRouteTable = new RouteTable(this, `PublicRouteTable-${id}`, { - vpcId: vpc.id, - tags: { - Name: "public-route-table", - }, - }); - - new Route(this, `PublicRoute-${id}`, { - routeTableId: publicRouteTable.id, - destinationCidrBlock: "0.0.0.0/0", - gatewayId: igwId, - }); - - const availabilityZones = new DataAwsAvailabilityZones(this, "AZs", {}); - const subnetOffset = subnets.ids.length; - const subnetMask = Fn.parseint(Fn.element(Fn.split("/", vpc.cidrBlock), 1), 10); - const availableIpv4 = subnet.availableIpAddressCount; - // Math magic to find next power of 2 and based on the subnetAddressPower - const subnetAddressPower = Fn.log(Fn.pow(2, Fn.ceil(Fn.log(availableIpv4, 2))), 2); - const subnetsMax = Op.sub(32, Op.add(subnetAddressPower, subnetMask)); - - const subnetIds = []; - for (let i = 1; i < 3; i++) { - const az = Fn.element(availabilityZones.names, i); - const subnetIndex = subnetOffset + i; - const cidrBlock = Fn.cidrsubnet(vpc.cidrBlock, subnetsMax, subnetIndex + i); - - const subnet = new Subnet(this, `PublicSubnet${i}-${id}`, { - vpcId: vpc.id, - cidrBlock: cidrBlock, - availabilityZone: az, - mapPublicIpOnLaunch: true, - tags: { - Name: `public-subnet-${subnetIndex + 1}-${id}`, - }, - }); - - subnetIds.push(subnet.id); - - new RouteTableAssociation(this, `PublicSubnet${i}RouteTableAssociation-${id}`, { - subnetId: subnet.id, - routeTableId: publicRouteTable.id, - }); - } - - let mskClientAuthentication = userVariables.mskClientAuthentication; - - const bootstrapServers = - mskClientAuthentication === "mTLS" - ? mskCluster.bootstrapBrokersTls - : mskClientAuthentication === "SASL/SCRAM" - ? mskCluster.bootstrapBrokersSaslScram - : mskCluster.bootstrapBrokers; - - const domainParts = Fn.split(":", Fn.element(Fn.split(",", bootstrapServers), 0)); - const serverAddress = Fn.element(domainParts, 0); - mskPort = Fn.element(domainParts, 1); - const addressParts = Fn.split(".", serverAddress); - const mskBootstrapCommonPart = Fn.join(".", Fn.slice(addressParts, 1, Fn.lengthOf(addressParts))); - mskWildcardDNS = Fn.format("*.%s", [mskBootstrapCommonPart]); - - const mTLSEnabled = mskClientAuthentication === "mTLS"; - const publicTlsCertificateViaAcm = userVariables.publicTlsCertificateViaAcm; - - const data: TemplateData = { - name: 'public', - useAcm: publicTlsCertificateViaAcm, - mTLS: mTLSEnabled, - public: {} - }; - - if (mTLSEnabled) { - // Seems like we can't get this from the MSK Cluster - const mskCertificateAuthorityVar = new TerraformVariable(this, "msk_certificate_authority_arn", { - type: "string", - description: "ACM Private Certificate Authority ARN used to authorize clients connecting to the MSK cluster", - }); - // Validate that the PCA exists - new DataAwsAcmpcaCertificateAuthority(this, "MSKCertificateAuthority", { - arn: mskCertificateAuthorityVar.stringValue, - }); - mskCertificateAuthority = mskCertificateAuthorityVar.stringValue; - - let publicCertificateAuthority = mskCertificateAuthority; - if (userVariables.publicCertificateAuthority) { - const publicCertificateAuthorityVar = new TerraformVariable(this, "public_certificate_authority_arn", { - type: "string", - description: - "ACM Private Certificate Authority ARN used to authorize clients connecting to the Public Zilla Plus", - default: mskCertificateAuthorityVar.stringValue, - }); - - // Validate that the PCA exists - new DataAwsAcmpcaCertificateAuthority(this, "publicCertificateAuthority", { - arn: publicCertificateAuthorityVar.stringValue, - }); - publicCertificateAuthority = publicCertificateAuthorityVar.stringValue; - } - data.public = { - certificateAuthority: publicCertificateAuthority - } - } - - const publicTlsCertificateKey = new TerraformVariable(this, "public_tls_certificate_key", { - type: "string", - description: "TLS Certificate SecretsManager or CertificateManager ARN", - }); - - let zillaPlusRole; - if (!userVariables.createZillaPlusRole) { - const zillaPlusRoleVar = new TerraformVariable(this, "zilla_plus_role_name", { - type: "string", - description: "The role name assumed by Zilla Plus instances.", - }); - - zillaPlusRole = zillaPlusRoleVar.stringValue; - } else { - const iamRole = new IamRole(this, `zilla_plus_role-${id}`, { - name: `zilla_plus_role-${id}`, - assumeRolePolicy: JSON.stringify({ - Version: "2012-10-17", - Statement: [ - { - Effect: "Allow", - Principal: { - Service: "ec2.amazonaws.com", - }, - Action: "sts:AssumeRole", - }, - { - Effect: "Allow", - Principal: { - Service: "cloudformation.amazonaws.com", - }, - Action: "sts:AssumeRole", - }, - ], - }), - managedPolicyArns: [ - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", - "arn:aws:iam::aws:policy/AWSCertificateManagerReadOnly", - "arn:aws:iam::aws:policy/AWSGlueSchemaRegistryReadonlyAccess", - "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess", - ], - inlinePolicy: [ - { - name: "CCProxySecretsManagerRead", - policy: JSON.stringify({ - Version: "2012-10-17", - Statement: [ - { - Sid: "VisualEditor0", - Effect: "Allow", - Action: [ - "acm-pca:GetCertificate", - "acm-pca:GetCertificateAuthorityCertificate", - "acm-pca:DescribeCertificateAuthority", - "tag:GetResources", - "secretsmanager:GetSecretValue", - "secretsmanager:DescribeSecret", - ], - Resource: [ - "arn:aws:secretsmanager:*:*:secret:wildcard.example.aklivity.io*", - "arn:aws:secretsmanager:*:*:secret:client-*", - "*", - ], - }, - ], - }), - }, - ], - }); - - const iamInstanceProfile = new IamInstanceProfile(this, `zilla_plus_instance_profile-${id}`, { - name: `zilla_plus_role-${id}`, - role: iamRole.name, - }); - - const iamPolicy = { - Version: "2012-10-17", - Statement: [ - { - Sid: "secretStatement", - Effect: "Allow", - Action: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], - Resource: ["arn:aws:secretsmanager:*:*:secret:*"], - }, - ], - }; - - if (publicTlsCertificateViaAcm) { - iamPolicy.Statement = iamPolicy.Statement.concat([ - { - "Sid": "s3Statement", - "Effect": "Allow", - "Action": [ "s3:GetObject" ], - "Resource": ["arn:aws:s3:::*/*"] - }, - { - "Sid": "kmsDecryptStatement", - "Effect": "Allow", - "Action": [ "kms:Decrypt" ], - "Resource": ["arn:aws:kms:*:*:key/*"] - }, - { - "Sid": "getRoleStatement", - "Effect": "Allow", - "Action": [ "iam:GetRole" ], - "Resource": [ `arn:aws:iam::*:role/${iamRole.name}` ] - }] - ); - - new ec2EnclaveCertificateIamRoleAssociation.Ec2EnclaveCertificateIamRoleAssociation(this, `ZillaPlusEnclaveIamRoleAssociation-${id}`, { - roleArn: iamRole.arn, - certificateArn: publicTlsCertificateKey.stringValue - }); - } - - new IamRolePolicy(this, `ZillaPlusRolePolicy-${id}`, { - role: iamRole.name, - policy: JSON.stringify(iamPolicy), - }); - - zillaPlusRole = iamInstanceProfile.name; - } - - let zillaPlusSecurityGroups; - - if (!userVariables.createZillaPlusSecurityGroup) { - const zillaPlusSecurityGroupsVar = new TerraformVariable(this, "zilla_plus_security_groups", { - type: "list(string)", - description: "The security groups associated with Zilla Plus instances.", - }); - zillaPlusSecurityGroups = zillaPlusSecurityGroupsVar.listValue; - } else { - const zillaPlusSG = new SecurityGroup(this, `ZillaPlusSecurityGroup-${id}`, { - vpcId: vpc.id, - description: "Security group for Zilla Plus", - ingress: [ - { - fromPort: 9092, - toPort: 9096, - protocol: "tcp", - cidrBlocks: ["0.0.0.0/0"], - }, - ], - egress: [ - { - fromPort: 0, - toPort: 65535, - protocol: "tcp", - cidrBlocks: ["0.0.0.0/0"], - }, - ], - tags: { - Name: "zilla-plus-security-group", - }, - }); - zillaPlusSecurityGroups = [zillaPlusSG.id]; - } - - const zillaPlusCapacity = new TerraformVariable(this, "zilla_plus_capacity", { - type: "number", - default: 2, - description: "The initial number of Zilla Plus instances", - }); - - const publicPort = new TerraformVariable(this, "public_port", { - type: "number", - default: 9094, - description: "The public port number to be used by Kafka clients", - }); - - const publicWildcardDNS = new TerraformVariable(this, "public_wildcard_dns", { - type: "string", - description: "The public wildcard DNS pattern for bootstrap servers to be used by Kafka clients", - }); - - if (!publicTlsCertificateViaAcm) { - // Validate that the Certificate Key exists - new DataAwsSecretsmanagerSecretVersion(this, "publicTlsCertificate", { - secretId: publicTlsCertificateKey.stringValue, - }); - } - - let keyName = ""; - - if (userVariables.sshKeyEnabled) { - const keyNameVar = new TerraformVariable(this, "zilla_plus_ssh_key", { - type: "string", - description: "Name of an existing EC2 KeyPair to enable SSH access to the instances", - }); - keyName = keyNameVar.stringValue; - } - - let acmYamlContent = ""; - let enclavesAcmServiceStart = ""; - - if (publicTlsCertificateViaAcm) { - acmYamlContent = ` -enclave: - cpu_count: 2 - memory_mib: 256 - -options: - sync_interval_secs: 600 - -tokens: - - label: acm-token-example - source: - Acm: - certificate_arn: '${publicTlsCertificateKey}' - refresh_interval_secs: 43200 - pin: 1234 -` - enclavesAcmServiceStart = ` -systemctl enable nitro-enclaves-acm.service -systemctl start nitro-enclaves-acm.service -` - } - - if (!userVariables.cloudwatchDisabled) { - const defaultLogGroupName = `${id}-group`; - const defaultMetricNamespace = `${id}-namespace`; - - const cloudWatchLogsGroup = new TerraformVariable(this, "cloudwatch_logs_group", { - type: "string", - description: "The Cloud Watch log group Zilla Plush should publish logs", - default: defaultLogGroupName, - }); - - const cloudWatchMetricsNamespace = new TerraformVariable(this, "cloudwatch_metrics_namespace", { - type: "string", - description: "The Cloud Watch metrics namespace Zilla Plush should publish metrics", - default: defaultMetricNamespace, - }); - - new CloudwatchLogGroup(this, `loggroup-${id}`, { - name: cloudWatchLogsGroup.stringValue, - }); - - - data.cloudwatch = { - logs: { - group: cloudWatchLogsGroup.stringValue - }, - metrics: { - namespace: cloudWatchMetricsNamespace.stringValue - } - }; - } - - const instanceType = new TerraformVariable(this, "zilla_plus_instance_type", { - type: "string", - default: publicTlsCertificateViaAcm ? "c6i.xlarge" : "t3.small", - description: "Zilla Plus EC2 instance type", - }); - instanceType.addValidation({ - condition: `${Fn.contains(instanceTypes.instanceTypes, instanceType.stringValue)}`, - errorMessage: "must be a valid EC2 instance type.", - }); - - let imageId = userVariables.zillaPlusAmi; - if (!imageId) - { - const ami = new dataAwsAmi.DataAwsAmi(this, "LatestAmi", { - mostRecent: true, - filter: [ - { - name: "product-code", - values: ["ca5mgk85pjtbyuhtfluzisgzy"], - }, - { - name: "is-public", - values: ["true"], - }, - ], - }); - imageId = ami.imageId; - } - - const nlb = new Lb(this, `NetworkLoadBalancer-${id}`, { - name: `nlb-${id}`, - loadBalancerType: "network", - internal: false, - subnets: subnetIds, - enableCrossZoneLoadBalancing: true, - }); - - const nlbTargetGroup = new LbTargetGroup(this, `NLBTargetGroup-${id}`, { - name: `nlb-tg-${id}`, - port: publicPort.value, - protocol: "TCP", - vpcId: vpc.id, - }); - - new LbListener(this, `NLBListener-${id}`, { - loadBalancerArn: nlb.arn, - port: publicPort.value, - protocol: "TCP", - defaultAction: [ - { - type: "forward", - targetGroupArn: nlbTargetGroup.arn, - }, - ], - }); - - const externalHost = ["b-#.", Fn.element(Fn.split("*.", publicWildcardDNS.stringValue), 1)].join(""); - - const internalHost = ["b-#.", Fn.element(Fn.split("*.", mskWildcardDNS), 1)].join(""); - - data.public = { - ...data.public, - port: publicPort.value, - tlsCertificateKey: publicTlsCertificateKey.stringValue, - wildcardDNS: publicWildcardDNS.stringValue - } - data.externalHost = externalHost; - data.internalHost = internalHost; - data.msk = { - port: mskPort, - wildcardDNS: mskWildcardDNS - } - const yamlTemplate: string = fs.readFileSync('zilla.yaml.mustache', 'utf8'); - const renderedYaml: string = Mustache.render(yamlTemplate, data); - - const cfnHupConfContent = ` -[main] -stack=${id} -region=${region} - `; - - const cfnAutoReloaderConfContent = ` -[cfn-auto-reloader-hook] -triggers=post.update -path=Resources.ZillaPlusLaunchTemplate.Metadata.AWS::CloudFormation::Init -action=/opt/aws/bin/cfn-init -v --stack ${id} --resource ZillaPlusLaunchTemplate --region ${region} -runas=root - `; - - const userData = `#!/bin/bash -xe -yum update -y aws-cfn-bootstrap -cat < /etc/zilla/zilla.yaml -${renderedYaml} -EOF - -cat < /etc/nitro_enclaves/acm.yaml -${acmYamlContent} -EOF - -chown ec2-user:ec2-user /etc/zilla/zilla.yaml - -mkdir /etc/cfn -cat < /etc/cfn/cfn-hup.conf -${cfnHupConfContent} -EOF - -chown root:root /etc/cfn/cfn-hup.conf -chmod 0400 /etc/cfn/cfn-hup.conf - -mkdir /etc/cfn/hooks.d -cat < /etc/cfn/hooks.d/cfn-auto-reloader.conf -${cfnAutoReloaderConfContent} -EOF - -chown root:root /etc/cfn/hooks.d/cfn-auto-reloader.conf -chmod 0400 /etc/cfn/hooks.d/cfn-auto-reloader.conf - -systemctl enable cfn-hup -systemctl start cfn-hup -systemctl enable amazon-ssm-agent -systemctl start amazon-ssm-agent -${enclavesAcmServiceStart} -systemctl enable zilla-plus -systemctl start zilla-plus - - `; - - const ZillaPlusLaunchTemplate = new launchTemplate.LaunchTemplate(this, `ZillaPlusLaunchTemplate-${id}`, { - imageId: imageId, - instanceType: instanceType.stringValue, - networkInterfaces: [ - { - associatePublicIpAddress: "true", - deviceIndex: 0, - securityGroups: zillaPlusSecurityGroups, - }, - ], - iamInstanceProfile: { - name: zillaPlusRole, - }, - enclaveOptions: { - enabled: publicTlsCertificateViaAcm - }, - keyName: keyName, - userData: Fn.base64encode(userData), - }); - - new autoscalingGroup.AutoscalingGroup(this, `ZillaPlusGroup-${id}`, { - vpcZoneIdentifier: subnetIds, - launchTemplate: { - id: ZillaPlusLaunchTemplate.id, - }, - minSize: 1, - maxSize: 5, - desiredCapacity: zillaPlusCapacity.numberValue, - targetGroupArns: [nlbTargetGroup.arn], - }); - - new TerraformOutput(this, "NetworkLoadBalancerOutput", { - description: "Public DNS name of newly created NLB for Zilla Plus", - value: nlb.dnsName, - }); - } -} +import { App } from "cdktf"; +import { ZillaPlusSecurePublicAccessStack } from "./secure-public-acces-stack"; const app = new App(); new ZillaPlusSecurePublicAccessStack(app, "secure-public-access"); diff --git a/amazon-msk/cdktf/secure-public-access/package-lock.json b/amazon-msk/cdktf/secure-public-access/package-lock.json index eb4c64a..84461d6 100644 --- a/amazon-msk/cdktf/secure-public-access/package-lock.json +++ b/amazon-msk/cdktf/secure-public-access/package-lock.json @@ -3912,10 +3912,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.13.tgz", + "integrity": "sha512-RNf+4dEeV69PIvyp++4IKM2vnLXtmp/JovfeQm5P5+qpKb6wHoH7INywLdZ7z+gVX46kgBP/fwJJvZYaHxtdyw==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } diff --git a/amazon-msk/cdktf/secure-public-access/secure-public-acces-stack.ts b/amazon-msk/cdktf/secure-public-access/secure-public-acces-stack.ts new file mode 100644 index 0000000..b7cddf1 --- /dev/null +++ b/amazon-msk/cdktf/secure-public-access/secure-public-acces-stack.ts @@ -0,0 +1,616 @@ +import { Construct, Node } from "constructs"; +import { TerraformStack, TerraformOutput, Fn, Op } from "cdktf"; +import { Lb } from "@cdktf/provider-aws/lib/lb"; +import { LbListener } from "@cdktf/provider-aws/lib/lb-listener"; +import { autoscalingGroup, dataAwsAmi, launchTemplate } from "@cdktf/provider-aws"; +import { LbTargetGroup } from "@cdktf/provider-aws/lib/lb-target-group"; +import { DataAwsAcmpcaCertificateAuthority } from "@cdktf/provider-aws/lib/data-aws-acmpca-certificate-authority"; +import { DataAwsSecretsmanagerSecretVersion } from "@cdktf/provider-aws/lib/data-aws-secretsmanager-secret-version"; +import { CloudwatchLogGroup } from "@cdktf/provider-aws/lib/cloudwatch-log-group"; +import { DataAwsMskCluster } from "@cdktf/provider-aws/lib/data-aws-msk-cluster"; +import { InternetGateway } from "@cdktf/provider-aws/lib/internet-gateway"; +import { Route } from "@cdktf/provider-aws/lib/route"; +import { RouteTable } from "@cdktf/provider-aws/lib/route-table"; +import { RouteTableAssociation } from "@cdktf/provider-aws/lib/route-table-association"; +import { Subnet } from "@cdktf/provider-aws/lib/subnet"; +import { IamRole } from "@cdktf/provider-aws/lib/iam-role"; +import { IamRolePolicy } from "@cdktf/provider-aws/lib/iam-role-policy"; +import { DataAwsMskBrokerNodes } from "@cdktf/provider-aws/lib/data-aws-msk-broker-nodes"; +import { DataAwsSubnet } from "@cdktf/provider-aws/lib/data-aws-subnet"; +import { DataAwsVpc } from "@cdktf/provider-aws/lib/data-aws-vpc"; +import { DataAwsRegion } from "@cdktf/provider-aws/lib/data-aws-region"; +import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group"; +import { DataAwsAvailabilityZones } from "@cdktf/provider-aws/lib/data-aws-availability-zones"; +import { DataAwsSubnets } from "@cdktf/provider-aws/lib/data-aws-subnets"; +import { IamInstanceProfile } from "@cdktf/provider-aws/lib/iam-instance-profile"; + +import { AwsProvider } from "@cdktf/provider-aws/lib/provider"; +import { ec2EnclaveCertificateIamRoleAssociation } from "./.gen/providers/awscc" +import { AwsccProvider } from "./.gen/providers/awscc/provider"; +import Mustache = require("mustache"); +import fs = require("fs"); +import { DataAwsInternetGateway } from "@cdktf/provider-aws/lib/data-aws-internet-gateway"; +import { CloudwatchLogStream } from "@cdktf/provider-aws/lib/cloudwatch-log-stream"; + +interface TemplateData { + name: string; + useAcm: boolean; + cloudwatch?: object; + public?: object; + mTLS?: boolean; + externalHost?: string; + internalHost?: string; + msk?: object; +} + +export class ZillaPlusSecurePublicAccessStack extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const awsProvider = new AwsProvider(this, "AWS", { }); + new AwsccProvider(this, "AWSCC", { }); + + const region = new DataAwsRegion(this, "CurrentRegion", { + provider: awsProvider, + }); + + const mandatoryVariables = [ + 'msk', + 'public' + ]; + + function validateContextKeys(node: object, keys: string[]): void { + const missingKeys = []; + if (node instanceof Node) { + missingKeys.push(...keys.filter((key) => !node.tryGetContext(key))); + } else if (typeof node === 'object' && node !== null) { + missingKeys.push(...keys.filter((key) => !(key in node))); + } + if (missingKeys.length > 0) { + throw new Error(`Missing required context variables: ${missingKeys.join(', ')}`); + } + } + + + let mskPort; + let mskWildcardDNS; + let mskCertificateAuthority; + + const zillaPlusContext = this.node.tryGetContext('zilla-plus'); + validateContextKeys(zillaPlusContext, mandatoryVariables); + const msk = zillaPlusContext.msk; + const mandatoryMSKVariables = [ + 'cluster', + 'clientAuthentication' + ]; + + validateContextKeys(msk, mandatoryMSKVariables); + const mskClusterName = msk.cluster; + const mskClientAuthentication = msk.clientAuthentication; + + const publicVar = zillaPlusContext.public; + const mandatoryPublicVariables = [ + 'certificate', + 'wildcardDNS' + ]; + validateContextKeys(publicVar, mandatoryPublicVariables); + const publicTlsCertificateKey = publicVar.certificate; + const publicWildcardDNS = publicVar.wildcardDNS; + + const mskCluster = new DataAwsMskCluster(this, "MSKCluster", { + clusterName: mskClusterName + }); + + const mskClusterBrokerNodes = new DataAwsMskBrokerNodes(this, "MSKClusterBrokerNodes", { + clusterArn: mskCluster.arn, + }); + + const subnetId = mskClusterBrokerNodes.nodeInfoList.get(0).clientSubnet; + + const subnet = new DataAwsSubnet(this, "Subnet", { + id: subnetId, + }); + + const vpc = new DataAwsVpc(this, "Vpc", { + id: subnet.vpcId, + }); + + const subnets = new DataAwsSubnets(this, "PublicSubnets", { + filter: [ + { + name: "vpc-id", + values: [vpc.id], + }, + { + name: "mapPublicIpOnLaunch", + values: ["true"] + }, + ], + }); + + let igwId = zillaPlusContext.igwId;; + if (!igwId) + { + const igw = new InternetGateway(this, `InternetGateway-${id}`, { + vpcId: vpc.id, + tags: { + Name: "my-igw", + }, + }); + igwId = igw.id; + } + else + { + const existingIgw = new DataAwsInternetGateway(this, `ExistingInternetGateway-${id}`, { + filter: [ + { + name: "attachment.vpc-id", + values: [vpc.id], + }, + ], + }); + igwId = existingIgw.id; + } + + const publicRouteTable = new RouteTable(this, `PublicRouteTable-${id}`, { + vpcId: vpc.id, + tags: { + Name: "public-route-table", + }, + }); + + new Route(this, `PublicRoute-${id}`, { + routeTableId: publicRouteTable.id, + destinationCidrBlock: "0.0.0.0/0", + gatewayId: igwId, + }); + + const availabilityZones = new DataAwsAvailabilityZones(this, "AZs", {}); + const subnetOffset = subnets.ids.length; + const subnetMask = Fn.parseint(Fn.element(Fn.split("/", vpc.cidrBlock), 1), 10); + const availableIpv4 = subnet.availableIpAddressCount; + // Math magic to find next power of 2 and based on the subnetAddressPower + const subnetAddressPower = Fn.log(Fn.pow(2, Fn.ceil(Fn.log(availableIpv4, 2))), 2); + const subnetsMax = Op.sub(32, Op.add(subnetAddressPower, subnetMask)); + + const subnetIds = []; + for (let i = 1; i < 3; i++) { + const az = Fn.element(availabilityZones.names, i); + const subnetIndex = subnetOffset + i; + const cidrBlock = Fn.cidrsubnet(vpc.cidrBlock, subnetsMax, subnetIndex + i); + + const subnet = new Subnet(this, `PublicSubnet${i}-${id}`, { + vpcId: vpc.id, + cidrBlock: cidrBlock, + availabilityZone: az, + mapPublicIpOnLaunch: true, + tags: { + Name: `public-subnet-${subnetIndex + 1}-${id}`, + }, + }); + + subnetIds.push(subnet.id); + + new RouteTableAssociation(this, `PublicSubnet${i}RouteTableAssociation-${id}`, { + subnetId: subnet.id, + routeTableId: publicRouteTable.id, + }); + } + + const bootstrapServers = + mskClientAuthentication === "mTLS" + ? mskCluster.bootstrapBrokersTls + : mskClientAuthentication === "SASL/SCRAM" + ? mskCluster.bootstrapBrokersSaslScram + : mskCluster.bootstrapBrokers; + + const domainParts = Fn.split(":", Fn.element(Fn.split(",", bootstrapServers), 0)); + const serverAddress = Fn.element(domainParts, 0); + mskPort = Fn.element(domainParts, 1); + const addressParts = Fn.split(".", serverAddress); + const mskBootstrapCommonPart = Fn.join(".", Fn.slice(addressParts, 1, Fn.lengthOf(addressParts))); + mskWildcardDNS = Fn.format("*.%s", [mskBootstrapCommonPart]); + + const mTLSEnabled = mskClientAuthentication === "mTLS"; + const publicTlsCertificateViaAcm: boolean = publicTlsCertificateKey.startsWith("arn:aws:acm"); + + const data: TemplateData = { + name: 'public', + useAcm: publicTlsCertificateViaAcm, + mTLS: mTLSEnabled, + public: {} + }; + + if (mTLSEnabled) { + validateContextKeys(msk, ['certificateAuthorityArn']); + const mskCertificateAuthorityArn = msk.certificateAuthorityArn; + const publicCertificateAuthority = publicVar.certificateAuthorityArn ?? mskCertificateAuthorityArn; + data.msk = { + certificateAuthority: mskCertificateAuthorityArn + } + // Validate that the PCA exists + new DataAwsAcmpcaCertificateAuthority(this, "MSKCertificateAuthority", { + arn: mskCertificateAuthorityArn + }); + mskCertificateAuthority = mskCertificateAuthority; + + // Validate that the PCA exists + new DataAwsAcmpcaCertificateAuthority(this, "publicCertificateAuthority", { + arn: publicCertificateAuthority, + }); + data.public = { + certificateAuthority: publicCertificateAuthority + } + } + + let zillaPlusRole = zillaPlusContext.roleName; + + if (!zillaPlusRole) { + const iamRole = new IamRole(this, `zilla_plus_role-${id}`, { + name: `zilla_plus_role-${id}`, + assumeRolePolicy: JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: { + Service: "ec2.amazonaws.com", + }, + Action: "sts:AssumeRole", + }, + { + Effect: "Allow", + Principal: { + Service: "cloudformation.amazonaws.com", + }, + Action: "sts:AssumeRole", + }, + ], + }), + managedPolicyArns: [ + "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", + "arn:aws:iam::aws:policy/AWSCertificateManagerReadOnly", + "arn:aws:iam::aws:policy/AWSGlueSchemaRegistryReadonlyAccess", + "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess", + ], + inlinePolicy: [ + { + name: "CCProxySecretsManagerRead", + policy: JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Sid: "VisualEditor0", + Effect: "Allow", + Action: [ + "acm-pca:GetCertificate", + "acm-pca:GetCertificateAuthorityCertificate", + "acm-pca:DescribeCertificateAuthority", + "tag:GetResources", + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + ], + Resource: [ + "arn:aws:secretsmanager:*:*:secret:wildcard.example.aklivity.io*", + "arn:aws:secretsmanager:*:*:secret:client-*", + "*", + ], + }, + ], + }), + }, + ], + }); + + const iamInstanceProfile = new IamInstanceProfile(this, `zilla_plus_instance_profile-${id}`, { + name: `zilla_plus_role-${id}`, + role: iamRole.name, + }); + + const iamPolicy = { + Version: "2012-10-17", + Statement: [ + { + Sid: "secretStatement", + Effect: "Allow", + Action: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], + Resource: ["arn:aws:secretsmanager:*:*:secret:*"], + }, + { + Sid: "cloudwatchStatement", + Effect: "Allow", + Action: ['logs:*', 'cloudwatch:GenerateQuery', 'cloudwatch:PutMetricData'], + Resource: ["*"], + }, + ], + }; + + if (publicTlsCertificateViaAcm) { + iamPolicy.Statement = iamPolicy.Statement.concat([ + { + "Sid": "s3Statement", + "Effect": "Allow", + "Action": [ "s3:GetObject" ], + "Resource": ["arn:aws:s3:::*/*"] + }, + { + "Sid": "kmsDecryptStatement", + "Effect": "Allow", + "Action": [ "kms:Decrypt" ], + "Resource": ["arn:aws:kms:*:*:key/*"] + }, + { + "Sid": "getRoleStatement", + "Effect": "Allow", + "Action": [ "iam:GetRole" ], + "Resource": [ `arn:aws:iam::*:role/${iamRole.name}` ] + }] + ); + + new ec2EnclaveCertificateIamRoleAssociation.Ec2EnclaveCertificateIamRoleAssociation(this, `ZillaPlusEnclaveIamRoleAssociation-${id}`, { + roleArn: iamRole.arn, + certificateArn: publicTlsCertificateKey + }); + } + + new IamRolePolicy(this, `ZillaPlusRolePolicy-${id}`, { + role: iamRole.name, + policy: JSON.stringify(iamPolicy), + }); + + zillaPlusRole = iamInstanceProfile.name; + } + + let zillaPlusSecurityGroups = zillaPlusContext.securityGroups; + + if (zillaPlusSecurityGroups) { + zillaPlusSecurityGroups = zillaPlusSecurityGroups.split(','); + } else { + const zillaPlusSG = new SecurityGroup(this, `ZillaPlusSecurityGroup-${id}`, { + vpcId: vpc.id, + description: "Security group for Zilla Plus", + ingress: [ + { + fromPort: 9092, + toPort: 9096, + protocol: "tcp", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + egress: [ + { + fromPort: 0, + toPort: 65535, + protocol: "tcp", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + tags: { + Name: "zilla-plus-security-group", + }, + }); + zillaPlusSecurityGroups = [zillaPlusSG.id]; + } + + const zillaPlusCapacity = zillaPlusContext.capacity ?? 2; + const publicPort = publicVar.port ?? 9094; + + if (!publicTlsCertificateViaAcm) { + // Validate that the Certificate Key exists + new DataAwsSecretsmanagerSecretVersion(this, "publicTlsCertificate", { + secretId: publicTlsCertificateKey, + }); + } + + let keyName = zillaPlusContext.sshKey; + let acmYamlContent = ''; + let enclavesAcmServiceStart = ''; + + if (publicTlsCertificateViaAcm) { + acmYamlContent = ` +enclave: + cpu_count: 2 + memory_mib: 256 + +options: + sync_interval_secs: 600 + +tokens: + - label: acm-token-example + source: + Acm: + certificate_arn: '${publicTlsCertificateKey}' + refresh_interval_secs: 43200 + pin: 1234 +` + enclavesAcmServiceStart = ` +systemctl enable nitro-enclaves-acm.service +systemctl start nitro-enclaves-acm.service +` + } + + const cloudwatch = zillaPlusContext.cloudwatch; + const cloudwatchDisabled = cloudwatch?.disabled ?? false; + + if (!cloudwatchDisabled) { + const defaultLogGroupName = `${id}-group`; + const defaultMetricNamespace = `${id}-namespace`; + + const logGroupName = cloudwatch?.logs?.group ?? defaultLogGroupName; + const metricNamespace = cloudwatch?.metrics?.namespace ?? defaultMetricNamespace; + + const cloudWatchLogGroup = new CloudwatchLogGroup(this, `loggroup-${id}`, { + name: logGroupName + }); + + new CloudwatchLogStream(this, `LogStream-${id}`, { + logGroupName: cloudWatchLogGroup.name, + name: 'events' + }); + + data.cloudwatch = { + logs: { + group: logGroupName, + }, + metrics: { + namespace: metricNamespace, + }, + }; + } + + const defaultInstanceType = publicTlsCertificateViaAcm ? 'c6i.xlarge' : 't3.small'; + const instanceType = zillaPlusContext.instanceType ?? defaultInstanceType; + + let imageId = zillaPlusContext.ami; + if (!imageId) { + const ami = new dataAwsAmi.DataAwsAmi(this, "LatestAmi", { + mostRecent: true, + filter: [ + { + name: "product-code", + values: ["ca5mgk85pjtbyuhtfluzisgzy"], + }, + { + name: "is-public", + values: ["true"], + }, + ], + }); + imageId = ami.imageId; + } + + const nlb = new Lb(this, `NetworkLoadBalancer-${id}`, { + name: `nlb-${id}`, + loadBalancerType: "network", + internal: false, + subnets: subnetIds, + enableCrossZoneLoadBalancing: true, + }); + + const nlbTargetGroup = new LbTargetGroup(this, `NLBTargetGroup-${id}`, { + name: `nlb-tg-${id}`, + port: publicPort, + protocol: "TCP", + vpcId: vpc.id, + }); + + new LbListener(this, `NLBListener-${id}`, { + loadBalancerArn: nlb.arn, + port: publicPort, + protocol: "TCP", + defaultAction: [ + { + type: "forward", + targetGroupArn: nlbTargetGroup.arn, + }, + ], + }); + + const externalHost = ["b-#.", publicWildcardDNS.split("*.")[1]].join(""); + const internalHost = ["b-#.", Fn.element(Fn.split("*.", mskWildcardDNS), 1)].join(""); + + data.public = { + ...data.public, + port: publicPort, + certificate: publicTlsCertificateKey, + wildcardDNS: publicWildcardDNS + } + data.externalHost = externalHost; + data.internalHost = internalHost; + data.msk = { + ...data.msk, + port: mskPort, + wildcardDNS: mskWildcardDNS + }; + const yamlTemplate: string = fs.readFileSync('zilla.yaml.mustache', 'utf8'); + const renderedYaml: string = Mustache.render(yamlTemplate, data); + + const cfnHupConfContent = ` +[main] +stack=${id} +region=${region} + `; + + const cfnAutoReloaderConfContent = ` +[cfn-auto-reloader-hook] +triggers=post.update +path=Resources.ZillaPlusLaunchTemplate.Metadata.AWS::CloudFormation::Init +action=/opt/aws/bin/cfn-init -v --stack ${id} --resource ZillaPlusLaunchTemplate --region ${region} +runas=root + `; + + const userData = `#!/bin/bash -xe +yum update -y aws-cfn-bootstrap +cat < /etc/zilla/zilla.yaml +${renderedYaml} +EOF + +cat < /etc/nitro_enclaves/acm.yaml +${acmYamlContent} +EOF + +chown ec2-user:ec2-user /etc/zilla/zilla.yaml + +mkdir /etc/cfn +cat < /etc/cfn/cfn-hup.conf +${cfnHupConfContent} +EOF + +chown root:root /etc/cfn/cfn-hup.conf +chmod 0400 /etc/cfn/cfn-hup.conf + +mkdir /etc/cfn/hooks.d +cat < /etc/cfn/hooks.d/cfn-auto-reloader.conf +${cfnAutoReloaderConfContent} +EOF + +chown root:root /etc/cfn/hooks.d/cfn-auto-reloader.conf +chmod 0400 /etc/cfn/hooks.d/cfn-auto-reloader.conf + +systemctl enable cfn-hup +systemctl start cfn-hup +systemctl enable amazon-ssm-agent +systemctl start amazon-ssm-agent +${enclavesAcmServiceStart} +systemctl enable zilla-plus +systemctl start zilla-plus + + `; + + const ZillaPlusLaunchTemplate = new launchTemplate.LaunchTemplate(this, `ZillaPlusLaunchTemplate-${id}`, { + imageId: imageId, + instanceType: instanceType, + networkInterfaces: [ + { + associatePublicIpAddress: "true", + deviceIndex: 0, + securityGroups: zillaPlusSecurityGroups, + }, + ], + iamInstanceProfile: { + name: zillaPlusRole, + }, + enclaveOptions: { + enabled: publicTlsCertificateViaAcm + }, + keyName: keyName, + userData: Fn.base64encode(userData), + }); + + new autoscalingGroup.AutoscalingGroup(this, `ZillaPlusGroup-${id}`, { + vpcZoneIdentifier: subnetIds, + launchTemplate: { + id: ZillaPlusLaunchTemplate.id, + }, + minSize: 1, + maxSize: 5, + desiredCapacity: zillaPlusCapacity, + targetGroupArns: [nlbTargetGroup.arn], + }); + + new TerraformOutput(this, "NetworkLoadBalancerOutput", { + description: "Public DNS name of newly created NLB for Zilla Plus", + value: nlb.dnsName, + }); + } +} diff --git a/amazon-msk/cdktf/secure-public-access/terraform.tfvars.example b/amazon-msk/cdktf/secure-public-access/terraform.tfvars.example deleted file mode 100644 index 8562852..0000000 --- a/amazon-msk/cdktf/secure-public-access/terraform.tfvars.example +++ /dev/null @@ -1,14 +0,0 @@ -msk_cluster_name="" -public_tls_certificate_key="" -public_wildcard_dns="" - -## mTLS Specific Variables -#msk_mtls_certificate_authority_arn="" - -## optional -#zilla_plus_security_groups=[""] -#zilla_plus_role="" -#public_mtls_certificate_authority_arn="" -#zilla_plus_ssh_key="" -#cloudwatch_logs_group="" -#cloudwatch_metrics_namespace="" diff --git a/amazon-msk/cdktf/secure-public-access/zilla.yaml.mustache b/amazon-msk/cdktf/secure-public-access/zilla.yaml.mustache index 359f2f5..c62c8f3 100644 --- a/amazon-msk/cdktf/secure-public-access/zilla.yaml.mustache +++ b/amazon-msk/cdktf/secure-public-access/zilla.yaml.mustache @@ -44,10 +44,10 @@ bindings: vault: secure options: keys: - - {{public.tlsCertificateKey}} + - {{{public.certificate}}} {{#mTLS}} trust: - - {{public.certificateAuthority}} + - {{{public.certificateAuthority}}} {{/mTLS}} telemetry: metrics: @@ -77,7 +77,7 @@ bindings: options: {{#mTLS}} signers: - - {{msk.certificateAuthority}} + - {{{msk.certificateAuthority}}} {{/mTLS}} trustcacerts: true telemetry: diff --git a/amazon-msk/cdktf/web-streaming/.env.example b/amazon-msk/cdktf/web-streaming/.env.example deleted file mode 100644 index f6b6b3f..0000000 --- a/amazon-msk/cdktf/web-streaming/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -CREATE_ZILLA_PLUS_ROLE=true -CREATE_ZILLA_PLUS_SECURITY_GROUP=true -CLOUDWATCH_DISABLED=false -GLUE_REGISTRY_ENABLED=false -KAFKA_TOPIC_CREATION_DISABLED=false -SSH_KEY_ENABLED=false -CLOUDWATCH_DISABLED=false -#CUSTOM_PATH= diff --git a/amazon-msk/cdktf/web-streaming/README.md b/amazon-msk/cdktf/web-streaming/README.md index 6c415e8..6b141a9 100644 --- a/amazon-msk/cdktf/web-streaming/README.md +++ b/amazon-msk/cdktf/web-streaming/README.md @@ -23,17 +23,23 @@ This guide will help you gather the necessary AWS values required to configure a ## (optional) Create an example MSK cluster -If you don't have an existing MSK cluster you can use our example MSK deployment with basic configuration and Unauthorized access. Follow the instructions inside the [example-cluster](../example-cluster/README.md) folder to deploy the example MSK cluster. Note the `mskClusterName` from the outputs as you'll need this later. +If you don't have an existing MSK cluster you can use our example MSK deployment with basic configuration and SASL/SCRAM access. Follow the instructions inside the [example-cluster](../example-cluster/README.md) folder to deploy the example MSK cluster. Note the `mskClusterName` from the outputs as you'll need this later. -## Required Terraform Variables +## Required CDKTF Context Variables -You can set these variable values in your `terraform.tfvars` file. To create a `.tfvars` from the example file run: +You can set these variables in your `context` in `cdktf.json` file under `zilla-plus` object. -```bash -cp terraform.tfvars.example terraform.tfvars +### `msk` related variables + +```json + "msk": + { + "cluster": "", + "credentials": "" + }, ``` -### `msk_cluster_name`: MSK Cluster Name +#### `cluster`: MSK Cluster Name To get a list all MSK clusters run: @@ -43,7 +49,7 @@ aws kafka list-clusters --query 'ClusterInfoList[*].[ClusterName,ClusterArn]' -- Use the `ClusterName` of your desired MSK cluster for this variable. -### `msk_credentials_secret_name`: MSK Credentials Secret Name +#### `credentials`: MSK Credentials Secret Name Provide the Secret Name that is associated with your MSK cluster. If you use our provided example cluster, there is already a secret associated with the cluster called `AmazonMSK_alice`. @@ -53,12 +59,30 @@ List all secrets ub Secrets Manager that can be associated with MSK: aws secretsmanager list-secrets --query "SecretList[?starts_with(Name, 'AmazonMSK_')].Name" --output table ``` +### `mappings`: Kafka Topic Mappings -### `kafka_topic`: Kafka Topic +```json + "mappings": + [ + {"topic": ""}, + {"topic": "", "path": ""} + ] +``` -This variable defines the Kafka topic exposed through REST and SSE. +This array variable defines the Kafka topics exposed through REST and SSE. If `path` is not specified, the topic will be exposed on `/` +To enable a custom path for the Kafka topic, set the `path` field to the path where the Kafka topic should be exposed. -### `public_tls_certificate_key`: Public TLS Certificate Key +### `public` Zilla Plus variables + +```json + "public": + { + "certificate": "", + "port": "" + } +``` + +#### `certificate`: Public TLS Certificate Key You need the ARN of the Secrets Manager secret that contains your public TLS certificate private key. @@ -70,41 +94,33 @@ aws secretsmanager list-secrets --query 'SecretList[*].[Name,ARN]' --output tabl Find and note down the ARN of the secret that contains your public TLS certificate private key. -### `zilla_plus_capacity`: Zilla Plus Capacity - -> Default: `2` -This variable defines the initial number of Zilla Plus instances. +#### `port`: Public TCP Port -### `zilla_plus_instance_type`: Zilla Plus EC2 Instance Type - -> Default: `t3.small` +> Default: `7143` -This variable defines the initial number of Zilla Plus instances. +This variable defines the public port number to be used by REST and SSE clients. -### `public_port`: Public TCP Port -> Default: `7143` +### `capacity`: Zilla Plus Capacity -This variable defines the public port number to be used by REST and SSE clients. +> Default: `2` -## Optional Features +This variable defines the initial number of Zilla Plus instances. -These features all have default values and can be configured using environment variables and terraform variables. If you don't plan to configure any of these features you can skip this section and go to the [Deploy stack using Terraform](#deploy-stack-using-terraform) section. +### `instanceType`: Zilla Plus EC2 Instance Type -### Environment Variables +> Default: `t3.small` -You can set these variable values in your runtime environment or with a `.env` file. If you don't plan on modifying any of the environment variable defaults you can skip this step. +This variable defines the initial number of Zilla Plus instances. -Create a `.env` file from the example file. +## Optional Features -```bash -cp .env.example .env -``` +These features all have default values and can be configured using cdk context variables. If you don't plan to configure any of these features you can skip this section and go to the [Deploy stack using Terraform](#deploy-stack-using-terraform) section. ### Internet Gateway ID -If you already have an Internet Gateway in the MSK's VPN it should be provided via the `IGW_ID` environment variable. If not set the deployment will attempt to create on in the VPC. +If you already have an Internet Gateway in the MSK's VPN it should be provided via the `igwId` context variable in your `cdktf.json` under `zilla-plus` object. If not set the deployment will attempt to create on in the VPC. To query the IGW_ID of your MSK's VPN use the following comman: ```bash @@ -113,13 +129,9 @@ VPC_ID=$(aws ec2 describe-subnets --subnet-ids $SUBNET_ID --query "Subnets[0].Vp aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" --query "InternetGateways[0].InternetGatewayId" --output text ``` -### Custom root Path - -To enable a custom path for the Kafka topic, set the environment variable CUSTOM_PATH to true. If enabled, you will need to provide the path where the Kafka topic should be exposed. Set `CUSTOM_PATH` environment variable to `true` to enable custom path support and adding `custom_path` to your `terraform.tfvars` file. - ### Custom Zilla Plus Role -By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `CREATE_ZILLA_PLUS_ROLE` environment variable to `false` and adding `zilla_plus_role` to your `terraform.tfvars` file. +By default the deployment creates the Zilla Plus Role with the necessary roles and policies. If you want, you can specify your own role by setting `roleName` context variable in your `cdktf.json` under `zilla-plus` object. List all IAM roles: @@ -131,7 +143,7 @@ Note down the role name `RoleName` of the desired IAM role. ### Custom Zilla Plus Security Groups -By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `CREATE_ZILLA_PLUS_SECURITY_GROUP` environment variable to `false` and adding `zilla_plus_security_groups` to your `terraform.tfvars` file. +By default the deployment creates the Zilla Plus Security Group with the necessary ports to be open. If you want, you can specify your own security group by setting `securityGroups` context variable in your `cdktf.json`. List all security groups: @@ -141,9 +153,24 @@ aws ec2 describe-security-groups --query 'SecurityGroups[*].[GroupId, GroupName] Note down the security group IDs (GroupId) of the desired security groups. -### Disable CloudWatch Integration +### CloudWatch Integration + +```json + "cloudwatch": + { + "disabled": false, + "logs": + { + "group": "" + }, + "metrics": + { + "namespace": "" + } + } +``` -By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `CLOUDWATCH_DISABLED` environment variable to `true`. +By default CloudWatch metrics and logging is enabled. To disable CloudWatch logging and metrics, set the `cloudwatch.disabled` context variable to `true`. You can create or use existing log groups and metric namespaces in CloudWatch. @@ -157,7 +184,8 @@ aws logs describe-log-groups --query 'logGroups[*].[logGroupName]' --output tabl ``` This command will return a table listing the names of all the log groups in your CloudWatch. -In your `terraform.tfvars` file add the desired CloudWatch Logs Group for variable name `cloudwatch_logs_group` +In your `cdktf.json` file add the desired CloudWatch Logs Group for variable name `logs.group` under `zilla-plus` object in the `cloudwatch` variables section. + #### List All CloudWatch Custom Metric Namespaces @@ -165,15 +193,23 @@ In your `terraform.tfvars` file add the desired CloudWatch Logs Group for variab aws cloudwatch list-metrics --query 'Metrics[*].Namespace' --output text | tr '\t' '\n' | sort | uniq | grep -v '^AWS' ``` -In your `terraform.tfvars` file add the desired CloudWatch Metrics Namespace for variable name `cloudwatch_metrics_namespace` +In your `cdktf.json` file add the desired CloudWatch Metrics Namespace for variable name `metrics.namespace` under `zilla-plus` object in the `cloudwatch` variables section. ### Enable JWT Access Tokens -To enable the JWT authentication and API access control, set the environment variable `JWT_ENABLED` to `true`. You will also need to set the JWT Issuer (`jwt_issuer`), JWT Audience (`jwt_audience`) and JWKS URL (`jwt_keys_url`) terraform variable. +To enable the JWT authentication and API access control, you need to provide the `jwt` context variable. You will also need to set the JWT Issuer (`issuer`), JWT Audience (`audience`) and JWKS URL (`keys_url`) context variable inside the `jwt` object. Example: + +```json + "jwt": { + "issuer" : "https://auth.example.com", + "audience": "https://api.example.com", + "keysUrl": "https://{yourDomain}/.well-known/jwks.json" + } +``` ### Enable Glue Schema Registry -To enable the Glue Schema Registry for schema fetching, set the environment variable `GLUE_REGISTRY_ENABLED` to `true`. You will also need the name of the Glue Registry to set the `glue_registry` terraform variable. +To enable the Glue Schema Registry for schema fetching, set the context variable `glueRegistry` to the name of the Glue Registry. 1. List all Glue Registries: @@ -185,7 +221,7 @@ Note down the Glue Registry name (RegistryName) you want to use. ### Enable SSH Access -To enable SSH access to the instances, set the `SSH_KEY_ENABLED` environment variable to `true`. You will also need the name of an existing EC2 KeyPair to set the `zilla_plus_ssh_key` terraform variable. +To enable SSH access to the instances you will need the name of an existing EC2 KeyPair to set the `sshKey` context variable. List all EC2 KeyPairs: @@ -223,12 +259,6 @@ This command will generate the necessary Terraform JSON configuration files in t After synthesizing the configuration you can use `terraform` to deploy zilla. -Move your `.tfvars` file into the the generated dir or you can manually enter these values when prompted, or use a .tfvars file to provide them. - -```bash -cp terraform.tfvars cdktf.out/stacks/web-streaming/terraform.tfvars -``` - Initialize terraform. ```bash diff --git a/amazon-msk/cdktf/web-streaming/__tests__/main-test.ts b/amazon-msk/cdktf/web-streaming/__tests__/main-test.ts index abd2cc1..822b329 100644 --- a/amazon-msk/cdktf/web-streaming/__tests__/main-test.ts +++ b/amazon-msk/cdktf/web-streaming/__tests__/main-test.ts @@ -1,17 +1,37 @@ import "cdktf/lib/testing/adapters/jest"; import { Testing } from "cdktf"; -import { ZillaPlusWebStreamingStack } from "../main"; import { CloudwatchLogGroup } from "@cdktf/provider-aws/lib/cloudwatch-log-group"; import { autoscalingGroup, launchTemplate } from "@cdktf/provider-aws"; import { LbTargetGroup } from "@cdktf/provider-aws/lib/lb-target-group"; import { LbListener } from "@cdktf/provider-aws/lib/lb-listener"; import { Lb } from "@cdktf/provider-aws/lib/lb"; +import { ZillaPlusWebStreamingStack } from "../web-streaming-stack"; describe("Zilla Plus Web Streaming Stack Test", () => { let output: string; beforeAll(() => { - const app = Testing.app(); + + const app = Testing.app({ + context: { + "zilla-plus": + { + "msk": + { + "cluster": "test-cluster", + "credentials": "test-credentials" + }, + "public": + { + "certificate": "test-certificate" + }, + "mappings": + [ + {"topic": "pets"} + ] + } + }}); + const stack = new ZillaPlusWebStreamingStack(app, "test"); output = Testing.synth(stack); }); @@ -20,7 +40,7 @@ describe("Zilla Plus Web Streaming Stack Test", () => { expect(output).toHaveResourceWithProperties( autoscalingGroup.AutoscalingGroup, { - desired_capacity: "${var.zilla_plus_capacity}", + desired_capacity: 2, launch_template: { id: "${aws_launch_template.ZillaPlusLaunchTemplate-test.id}", }, @@ -37,7 +57,7 @@ describe("Zilla Plus Web Streaming Stack Test", () => { it("should have cloudwatch group resource", async () => { expect(output).toHaveResourceWithProperties(CloudwatchLogGroup, { - name: "${var.cloudwatch_logs_group}", + name: "test-group", }); delete process.env.CLOUDWATCH_ENABLED; }); @@ -45,7 +65,7 @@ describe("Zilla Plus Web Streaming Stack Test", () => { it("should have load balancer target group", async () => { expect(output).toHaveResourceWithProperties(LbTargetGroup, { name: "nlb-tg-test", - port: "${var.public_tcp_port}", + port: 7143, protocol: "TCP", vpc_id: "${data.aws_vpc.Vpc.id}", }); @@ -74,7 +94,7 @@ describe("Zilla Plus Web Streaming Stack Test", () => { }, ], load_balancer_arn: "${aws_lb.NetworkLoadBalancer-test.arn}", - port: "${var.public_tcp_port}", + port: 7143, protocol: "TCP", }); }); @@ -85,8 +105,7 @@ describe("Zilla Plus Web Streaming Stack Test", () => { name: "${aws_iam_instance_profile.zilla_plus_instance_profile-test.name}", }, image_id: "${data.aws_ami.LatestAmi.image_id}", - instance_type: "${var.zilla_plus_instance_type}", - key_name: "", + instance_type: "t3.small", network_interfaces: [ { associate_public_ip_address: "true", diff --git a/amazon-msk/cdktf/web-streaming/cdktf.json b/amazon-msk/cdktf/web-streaming/cdktf.json index f64be95..4da57f1 100644 --- a/amazon-msk/cdktf/web-streaming/cdktf.json +++ b/amazon-msk/cdktf/web-streaming/cdktf.json @@ -6,6 +6,21 @@ "terraformProviders": [], "terraformModules": [], "context": { - + "zilla-plus": + { + "msk": + { + "cluster": "", + "credentials": "" + }, + "public": + { + "certificate": "" + }, + "mappings": + [ + {"topic": "pets"} + ] + } } } diff --git a/amazon-msk/cdktf/web-streaming/package-lock.json b/amazon-msk/cdktf/web-streaming/package-lock.json index ca97a28..b6a412d 100644 --- a/amazon-msk/cdktf/web-streaming/package-lock.json +++ b/amazon-msk/cdktf/web-streaming/package-lock.json @@ -10,7 +10,6 @@ "license": "MPL-2.0", "dependencies": { "@cdktf/provider-aws": "^19.27.0", - "@dotenvx/dotenvx": "^1.6.4", "cdktf": "^0.20.8", "constructs": "^10.3.0", "mustache": "^4.2.0" @@ -1088,15 +1087,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1119,104 +1109,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@dotenvx/dotenvx": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.7.0.tgz", - "integrity": "sha512-WPkSNS060+GwNoWHHBRjJxBR9KhsWyoNyevD8I2x8LffWPdbVvRJqJI72fF1H+KYpDWGBQ4xdX++xO2GS37XUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "chalk": "^4.1.2", - "commander": "^11.1.0", - "diff": "^5.2.0", - "dotenv": "^16.4.5", - "eciesjs": "^0.4.6", - "execa": "^5.1.1", - "fdir": "^6.1.1", - "ignore": "^5.3.0", - "object-treeify": "1.1.33", - "picomatch": "^3.0.1", - "which": "^4.0.0", - "winston": "^3.11.0", - "xxhashjs": "^0.2.2" - }, - "bin": { - "dotenvx": "src/cli/dotenvx.js", - "git-dotenvx": "src/cli/dotenvx.js" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/fdir": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.2.0.tgz", - "integrity": "sha512-9XaWcDl0riOX5j2kYfy0kKdg7skw3IY6kA4LFT8Tk2yF9UdrADUy8D6AJuBLtf7ISm/MksumwAHE3WVbMRyCLw==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@dotenvx/dotenvx/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, "node_modules/@inquirer/checkbox": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-1.5.2.tgz", @@ -2425,39 +2317,6 @@ "node": ">= 14.17.0" } }, - "node_modules/@noble/ciphers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", - "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz", - "integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2755,12 +2614,6 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -2903,6 +2756,7 @@ "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" }, @@ -3062,7 +2916,8 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true }, "node_modules/auto-bind": { "version": "4.0.0", @@ -4056,6 +3911,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4321,20 +4177,11 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, "node_modules/color-convert": { "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" }, @@ -4345,51 +4192,8 @@ "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==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/commonmark": { "version": "0.30.0", @@ -4531,6 +4335,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4540,12 +4345,6 @@ "node": ">= 8" } }, - "node_modules/cuint": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", - "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==", - "license": "MIT" - }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -4769,18 +4568,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/downlevel-dts": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/downlevel-dts/-/downlevel-dts-0.11.0.tgz", @@ -4816,20 +4603,6 @@ "dev": true, "license": "MIT" }, - "node_modules/eciesjs": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.7.tgz", - "integrity": "sha512-4JQahOkBdDy27jjW4q3FJQigHlcwZXx28sCtBQkBamF2XUdcNXrInpgrr8h205MtVIS0CMHufyIKGVjtjxQ2ZA==", - "license": "MIT", - "dependencies": { - "@noble/ciphers": "^0.5.3", - "@noble/curves": "^1.4.0", - "@noble/hashes": "^1.4.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -4869,12 +4642,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -4990,6 +4757,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -5161,12 +4929,6 @@ "pend": "~1.2.0" } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -5255,12 +5017,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -5434,6 +5190,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, "engines": { "node": ">=10" }, @@ -5545,6 +5302,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -5640,6 +5398,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, "engines": { "node": ">=10.17.0" } @@ -5678,15 +5437,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -5746,7 +5496,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/ini": { "version": "1.3.8", @@ -6223,6 +5974,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "engines": { "node": ">=8" }, @@ -6325,7 +6077,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -7484,12 +7237,6 @@ "node": ">=6" } }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -7662,23 +7409,6 @@ "node": ">=8.0" } }, - "node_modules/logform": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", - "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7753,7 +7483,8 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -7782,6 +7513,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "engines": { "node": ">=6" } @@ -7841,7 +7573,8 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/mustache": { "version": "4.2.0", @@ -7953,6 +7686,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, "dependencies": { "path-key": "^3.0.0" }, @@ -8020,15 +7754,6 @@ "node": ">= 0.4" } }, - "node_modules/object-treeify": { - "version": "1.1.33", - "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", - "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/object.assign": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", @@ -8064,19 +7789,11 @@ "wrappy": "1" } }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -8235,6 +7952,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -8657,6 +8375,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -8887,6 +8606,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -8903,15 +8623,6 @@ ], "license": "MIT" }, - "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -8994,6 +8705,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -9005,6 +8717,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -9059,7 +8772,8 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/simple-concat": { "version": "1.0.1", @@ -9108,21 +8822,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -9226,15 +8925,6 @@ "node": ">= 12.13.0" } }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -9306,6 +8996,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -9399,6 +9090,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, "engines": { "node": ">=6" } @@ -9419,6 +9111,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -9482,12 +9175,6 @@ "node": ">=8" } }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -9535,15 +9222,6 @@ "dev": true, "license": "MIT" }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, "node_modules/ts-jest": { "version": "29.2.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", @@ -9744,6 +9422,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -9807,6 +9486,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -9886,42 +9566,6 @@ "node": ">=8" } }, - "node_modules/winston": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", - "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.6.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.7.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", - "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", - "license": "MIT", - "dependencies": { - "logform": "^2.6.1", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", @@ -10040,15 +9684,6 @@ "url": "https://opencollective.com/xstate" } }, - "node_modules/xxhashjs": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", - "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", - "license": "MIT", - "dependencies": { - "cuint": "^0.2.2" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/amazon-msk/cdktf/web-streaming/package.json b/amazon-msk/cdktf/web-streaming/package.json index b25f509..626b91f 100644 --- a/amazon-msk/cdktf/web-streaming/package.json +++ b/amazon-msk/cdktf/web-streaming/package.json @@ -22,7 +22,6 @@ }, "dependencies": { "@cdktf/provider-aws": "^19.27.0", - "@dotenvx/dotenvx": "^1.6.4", "cdktf": "^0.20.8", "constructs": "^10.3.0", "mustache": "^4.2.0" diff --git a/amazon-msk/cdktf/web-streaming/terraform.tfvars.example b/amazon-msk/cdktf/web-streaming/terraform.tfvars.example deleted file mode 100644 index 1a9ddc9..0000000 --- a/amazon-msk/cdktf/web-streaming/terraform.tfvars.example +++ /dev/null @@ -1,16 +0,0 @@ -msk_cluster_name="" -msk_credentials_secret_name="" -kafka_topic="" -public_tls_certificate_key="" - -## optional -#public_tcp_port= -#zilla_plus_security_groups=[""] -#zilla_plus_role="" -#zilla_plus_ssh_key="" -#cloudwatch_logs_group="" -#cloudwatch_metrics_namespace="" -#glue_registry="" -#jwt_issuer="" -#jwt_audience="" -#jwt_keys_url="" diff --git a/amazon-msk/cdktf/web-streaming/tsconfig.json b/amazon-msk/cdktf/web-streaming/tsconfig.json index 27659f5..cf39f0d 100644 --- a/amazon-msk/cdktf/web-streaming/tsconfig.json +++ b/amazon-msk/cdktf/web-streaming/tsconfig.json @@ -32,4 +32,4 @@ "node_modules", "cdktf.out" ] -} \ No newline at end of file +} diff --git a/amazon-msk/cdktf/web-streaming/variables.ts b/amazon-msk/cdktf/web-streaming/variables.ts deleted file mode 100644 index e894383..0000000 --- a/amazon-msk/cdktf/web-streaming/variables.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as dotenvx from "@dotenvx/dotenvx"; -import { Construct } from "constructs"; - -export class UserVariables extends Construct { - customPath: boolean = false; - publicCertificateAuthority: boolean = false; - createZillaPlusRole: boolean = false; - createZillaPlusSecurityGroup: boolean = false; - sshKeyEnabled: boolean = false; - cloudwatchDisabled: boolean = false; - glueRegistryEnabled: boolean = false; - kafkaTopicCreationDisabled: boolean = false; - jwtEnabled: boolean = false; - igwId: string | undefined; - - constructor(scope: Construct, name: string) { - super(scope, name); - dotenvx.config({ quiet: true }); - - this.customPath = process.env.CUSTOM_PATH === "true"; - this.createZillaPlusRole = process.env.CREATE_ZILLA_PLUS_ROLE !== "false"; - this.createZillaPlusSecurityGroup = process.env.CREATE_ZILLA_PLUS_SECURITY_GROUP !== "false"; - this.sshKeyEnabled = process.env.SSH_KEY_ENABLED === "true"; - this.cloudwatchDisabled = process.env.CLOUDWATCH_DISABLED === "true"; - this.glueRegistryEnabled = process.env.GLUE_REGISTRY_ENABLED === "true"; - this.kafkaTopicCreationDisabled = process.env.KAFKA_TOPIC_CREATION_DISABLED === "true"; - this.jwtEnabled = process.env.JWT_ENABLED === "true"; - this.igwId = process.env.IGW_ID; - } -} diff --git a/amazon-msk/cdktf/web-streaming/main.ts b/amazon-msk/cdktf/web-streaming/web-streaming-stack.ts similarity index 66% rename from amazon-msk/cdktf/web-streaming/main.ts rename to amazon-msk/cdktf/web-streaming/web-streaming-stack.ts index c1625cc..fe35143 100644 --- a/amazon-msk/cdktf/web-streaming/main.ts +++ b/amazon-msk/cdktf/web-streaming/web-streaming-stack.ts @@ -1,5 +1,5 @@ -import { Construct } from "constructs"; -import { App, TerraformStack, TerraformOutput, TerraformVariable, Fn, Op } from "cdktf"; +import { Construct, Node } from "constructs"; +import { TerraformStack, TerraformOutput, Fn, Op } from "cdktf"; import { AwsProvider } from "@cdktf/provider-aws/lib/provider"; import { Lb } from "@cdktf/provider-aws/lib/lb"; import { LbListener } from "@cdktf/provider-aws/lib/lb-listener"; @@ -9,7 +9,6 @@ import { LbTargetGroup } from "@cdktf/provider-aws/lib/lb-target-group"; import { DataAwsSecretsmanagerSecretVersion } from "@cdktf/provider-aws/lib/data-aws-secretsmanager-secret-version"; import { CloudwatchLogGroup } from "@cdktf/provider-aws/lib/cloudwatch-log-group"; import { DataAwsMskCluster } from "@cdktf/provider-aws/lib/data-aws-msk-cluster"; -import instanceTypes from "./instance-types"; import { DataAwsAvailabilityZones } from "@cdktf/provider-aws/lib/data-aws-availability-zones"; import { DataAwsMskBrokerNodes } from "@cdktf/provider-aws/lib/data-aws-msk-broker-nodes"; import { DataAwsRegion } from "@cdktf/provider-aws/lib/data-aws-region"; @@ -25,26 +24,39 @@ import { IamInstanceProfile } from "@cdktf/provider-aws/lib/iam-instance-profile import { IamRole } from "@cdktf/provider-aws/lib/iam-role"; import { IamRolePolicy } from "@cdktf/provider-aws/lib/iam-role-policy"; import { SecurityGroup } from "@cdktf/provider-aws/lib/security-group"; -import { UserVariables } from "./variables"; import Mustache = require("mustache"); import fs = require("fs"); import { DataAwsInternetGateway } from "@cdktf/provider-aws/lib/data-aws-internet-gateway"; +import { CloudwatchLogStream } from "@cdktf/provider-aws/lib/cloudwatch-log-stream"; interface TemplateData { name: string; glue?: object; cloudwatch?: object; - path?: string; - topic?: string; + mappings?: Array; public?: object; kafka?: object; jwt?: object; } +function validateContextKeys(node: object, keys: string[]): void { + const missingKeys = []; + if (node instanceof Node) { + missingKeys.push(...keys.filter((key) => !node.tryGetContext(key))); + } else if (typeof node === 'object' && node !== null) { + missingKeys.push(...keys.filter((key) => !(key in node))); + } else { + var err =new Error(`Invalid node type. Must be either a constructs.Node or a JSON object.`); + throw err; + } + if (missingKeys.length > 0) { + throw new Error(`Missing required context variables: ${missingKeys.join(', ')}`); + } +} + export class ZillaPlusWebStreamingStack extends TerraformStack { constructor(scope: Construct, id: string) { super(scope, id); - const userVars = new UserVariables(this, "main"); const awsProvider = new AwsProvider(this, "AWS", {}); @@ -52,22 +64,47 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { provider: awsProvider, }); - const mskClusterName = new TerraformVariable(this, "msk_cluster_name", { - type: "string", - description: "The name of the MSK cluster", + const mandatoryVariables = [ + 'msk', + 'public', + 'mappings', + ]; + + const zillaPlusContext = this.node.tryGetContext('zilla-plus'); + validateContextKeys(zillaPlusContext, mandatoryVariables); + const msk = zillaPlusContext.msk; + const mandatoryMSKVariables = [ + 'cluster', + 'credentials' + ]; + + validateContextKeys(msk, mandatoryMSKVariables); + const mskClusterName = msk.cluster; + const mskCredentialsSecretName = msk.credentials; + + const publicVar = zillaPlusContext.public; + const mandatoryPublicVariables = [ + 'certificate', + ]; + validateContextKeys(publicVar, mandatoryPublicVariables); + + const publicTlsCertificateKey = publicVar.certificate; + const mappings = zillaPlusContext.mappings; + + mappings.forEach((mapping: { path: string; topic: string; }) => { + if (!mapping.path) { + mapping.path = `/${mapping.topic}`; + } }); - const mskCluster = new DataAwsMskCluster(this, "MSKCluster", { - clusterName: mskClusterName.stringValue, - }); + const kafkaTopics: string[] = mappings.map((mapping: { topic: any; }) => mapping.topic); - const mskCredentialsSecretName = new TerraformVariable(this, "msk_credentials_secret_name", { - type: "string", - description: "The MSK Credentials Secret Name with JSON properties; username, password", + const mskCluster = new DataAwsMskCluster(this, "MSKCluster", { + clusterName: mskClusterName, }); // Validate that the Credentials exists new DataAwsSecretsmanagerSecretVersion(this, "mskAccessCredentials", { - secretId: mskCredentialsSecretName.stringValue, + secretId: mskCredentialsSecretName, }); const mskClusterBrokerNodes = new DataAwsMskBrokerNodes(this, "MSKClusterBrokerNodes", { @@ -97,8 +134,18 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { ], }); - let igwId; - if (userVars.igwId) + let igwId = zillaPlusContext.igwId;; + if (!igwId) + { + const igw = new InternetGateway(this, `InternetGateway-${id}`, { + vpcId: vpc.id, + tags: { + Name: "my-igw", + }, + }); + igwId = igw.id; + } + else { const existingIgw = new DataAwsInternetGateway(this, `ExistingInternetGateway-${id}`, { filter: [ @@ -110,16 +157,6 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { }); igwId = existingIgw.id; } - else - { - const igw = new InternetGateway(this, `InternetGateway-${id}`, { - vpcId: vpc.id, - tags: { - Name: "my-igw", - }, - }); - igwId = igw.id; - } const publicRouteTable = new RouteTable(this, `PublicRouteTable-${id}`, { vpcId: vpc.id, @@ -166,33 +203,11 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { }); } - const topic = new TerraformVariable(this, "kafka_topic", { - type: "string", - description: "The Kafka topic exposed through REST and SSE", - }); - - let path = `/${topic.stringValue}`; - - if (userVars.customPath) { - const pathVar = new TerraformVariable(this, "custom_path", { - type: "string", - description: "The path the Kafka topic should be exposed to", - default: "", - }); - path = `/${pathVar.stringValue}`; - } - const bootstrapBrokers = [Fn.element(Fn.split(",", mskCluster.bootstrapBrokersSaslScram), 0)]; - let zillaPlusRole; - if (!userVars.createZillaPlusRole) { - const zillaPlusRoleVar = new TerraformVariable(this, "zilla_plus_role_name", { - type: "string", - description: "The role name assumed by Zilla Plus instances.", - }); + let zillaPlusRole = zillaPlusContext.roleName; - zillaPlusRole = zillaPlusRoleVar.stringValue; - } else { + if (!zillaPlusRole) { const iamRole = new IamRole(this, `zilla_plus_role-${id}`, { name: `zilla_plus_role-${id}`, assumeRolePolicy: JSON.stringify({ @@ -256,11 +271,17 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { Version: "2012-10-17", Statement: [ { - Sid: "VisualEditor0", + Sid: "secretStatement", Effect: "Allow", Action: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], Resource: ["arn:aws:secretsmanager:*:*:secret:*"], }, + { + Sid: "cloudwatchStatement", + Effect: "Allow", + Action: ['logs:*', 'cloudwatch:GenerateQuery', 'cloudwatch:PutMetricData'], + Resource: ["*"], + } ], }), }); @@ -268,34 +289,20 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { zillaPlusRole = iamInstanceProfile.name; } - const zillaPlusCapacity = new TerraformVariable(this, "zilla_plus_capacity", { - type: "number", - default: 2, - description: "The initial number of Zilla Plus instances", - }); - - const publicTcpPort = new TerraformVariable(this, "public_tcp_port", { - type: "number", - default: 7143, - description: "The public port number to be used by REST and SSE clients", - }); + const publicPort = publicVar.port ?? 7143; - let zillaPlusSecurityGroups; + let zillaPlusSecurityGroups = zillaPlusContext.securityGroups; - if (!userVars.createZillaPlusSecurityGroup) { - const zillaPlusSecurityGroupsVar = new TerraformVariable(this, "zilla_plus_security_groups", { - type: "list(string)", - description: "The security groups associated with Zilla Plus instances.", - }); - zillaPlusSecurityGroups = zillaPlusSecurityGroupsVar.listValue; + if (zillaPlusSecurityGroups) { + zillaPlusSecurityGroups = zillaPlusSecurityGroups.split(','); } else { const zillaPlusSG = new SecurityGroup(this, `ZillaPlusSecurityGroup-${id}`, { vpcId: vpc.id, description: "Security group for Zilla Plus", ingress: [ { - fromPort: publicTcpPort.value, - toPort: publicTcpPort.value, + fromPort: publicPort, + toPort: publicPort, protocol: "tcp", cidrBlocks: ["0.0.0.0/0"], }, @@ -315,117 +322,95 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { zillaPlusSecurityGroups = [zillaPlusSG.id]; } - const publicTlsCertificateKey = new TerraformVariable(this, "public_tls_certificate_key", { - type: "string", - description: "TLS Certificate Private Key Secret ARN", - }); + const zillaPlusCapacity = zillaPlusContext.capacity ?? 2; + const keyName = zillaPlusContext.sshKey; + const instanceType = zillaPlusContext.instanceType ?? 't3.small'; + // Validate that the Certificate Key exists new DataAwsSecretsmanagerSecretVersion(this, "publicTlsCertificate", { - secretId: publicTlsCertificateKey.stringValue, - }); - - let keyName = ""; - - if (userVars.sshKeyEnabled) { - const keyNameVar = new TerraformVariable(this, "zilla_plus_ssh_key", { - type: "string", - description: "Name of an existing EC2 KeyPair to enable SSH access to the instances", - }); - keyName = keyNameVar.stringValue; - } - - const instanceType = new TerraformVariable(this, "zilla_plus_instance_type", { - type: "string", - default: "t3.small", - description: "MSK Proxy EC2 instance type", - }); - instanceType.addValidation({ - condition: `${Fn.contains(instanceTypes.instanceTypes, instanceType.stringValue)}`, - errorMessage: "must be a valid EC2 instance type.", + secretId: publicTlsCertificateKey, }); const data: TemplateData = { name: 'web', } - if (userVars.jwtEnabled) { - const issuer = new TerraformVariable(this, "jwt_issuer", { - type: "string", - description: "A string that identifies the principal that issued the JWT", - }); - - const audience = new TerraformVariable(this, "jwt_audience", { - type: "string", - description: "A string that identifies the recipients that the JWT is intended fo", - }); - - const keysUrl = new TerraformVariable(this, "jwt_keys_url", { - type: "string", - description: "The JSON Web Key Set (JWKS) URL: set of keys containing the public keys used to verify any JSON Web Token (JWT)", - }); + const jwt = zillaPlusContext.jwt; + if (jwt) + { + const mandatoryJWTVariables = [ + 'issuer', + 'audience', + 'keysUrl' + ]; + validateContextKeys(jwt, mandatoryJWTVariables); + const issuer = jwt.issuer; + const audience = jwt.audience; + const keysUrl = jwt.keysUrl; data.jwt = { - issuer: issuer.stringValue, - audience: audience.stringValue, - keysUrl: keysUrl.stringValue + issuer: issuer, + audience: audience, + keysUrl: keysUrl } } - if (!userVars.cloudwatchDisabled) { + + const cloudwatch = zillaPlusContext.cloudwatch; + const cloudwatchDisabled = cloudwatch?.disabled ?? false; + + if (!cloudwatchDisabled) { const defaultLogGroupName = `${id}-group`; const defaultMetricNamespace = `${id}-namespace`; - const cloudWatchLogsGroup = new TerraformVariable(this, "cloudwatch_logs_group", { - type: "string", - description: "The Cloud Watch log group Zilla Plush should publish logs", - default: defaultLogGroupName, - }); + const logGroupName = cloudwatch?.logs?.group ?? defaultLogGroupName; + const metricNamespace = cloudwatch?.metrics?.namespace ?? defaultMetricNamespace; + - const cloudWatchMetricsNamespace = new TerraformVariable(this, "cloudwatch_metrics_namespace", { - type: "string", - description: "The Cloud Watch metrics namespace Zilla Plush should publish metrics", - default: defaultMetricNamespace, + + const cloudWatchLogGroup = new CloudwatchLogGroup(this, `loggroup-${id}`, { + name: logGroupName }); - new CloudwatchLogGroup(this, "loggroup", { - name: cloudWatchLogsGroup.stringValue, + new CloudwatchLogStream(this, `LogStream-${id}`, { + logGroupName: cloudWatchLogGroup.name, + name: 'events' }); data.cloudwatch = { logs: { - group: cloudWatchLogsGroup.stringValue + group: logGroupName }, metrics: { - namespace: cloudWatchMetricsNamespace.stringValue - } + namespace: metricNamespace + }, }; } - if (userVars.glueRegistryEnabled) { - const glueRegistry = new TerraformVariable(this, "glue_registry", { - type: "string", - description: "The Glue Registry to fetch the schemas from", - }); - + const glueRegistry = zillaPlusContext.glueRegistry; + if (glueRegistry) { data.glue = { - registry: glueRegistry.stringValue + registry: glueRegistry } } - const ami = new dataAwsAmi.DataAwsAmi(this, "LatestAmi", { - mostRecent: true, - filter: [ - { - name: "product-code", - values: ["ca5mgk85pjtbyuhtfluzisgzy"], - }, - { - name: "is-public", - values: ["true"], - }, - ], - owners: ["679593333241"], - }); + let imageId = zillaPlusContext.ami; + if (!imageId) { + const ami = new dataAwsAmi.DataAwsAmi(this, "LatestAmi", { + mostRecent: true, + filter: [ + { + name: "product-code", + values: ["ca5mgk85pjtbyuhtfluzisgzy"], + }, + { + name: "is-public", + values: ["true"], + }, + ] + }); + imageId = ami.imageId; + } const nlb = new Lb(this, `NetworkLoadBalancer-${id}`, { name: `nlb-${id}`, @@ -438,14 +423,14 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { const nlbTargetGroup = new LbTargetGroup(this, `NLBTargetGroup-${id}`, { name: `nlb-tg-${id}`, - port: publicTcpPort.value, + port: publicPort, protocol: "TCP", vpcId: vpc.id, }); new LbListener(this, `NLBListener-${id}`, { loadBalancerArn: nlb.arn, - port: publicTcpPort.value, + port: publicPort, protocol: "TCP", defaultAction: [ { @@ -454,11 +439,8 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { }, ], }); - - const kafkaSaslUsername = Fn.join("", ["${{aws.secrets.", mskCredentialsSecretName.stringValue, "#username}}"]); - - const kafkaSaslPassword = Fn.join("", ["${{aws.secrets.", mskCredentialsSecretName.stringValue, "#password}}"]); - + const kafkaSaslUsername = `\${{aws.secrets.${mskCredentialsSecretName}#username}}`; + const kafkaSaslPassword = `\${{aws.secrets.${mskCredentialsSecretName}#password}}`; const kafkaBootstrapServers = `['${Fn.join(`','`, Fn.split(",", mskCluster.bootstrapBrokersSaslScram))}']`; data.kafka = { @@ -469,11 +451,12 @@ export class ZillaPlusWebStreamingStack extends TerraformStack { } } data.public = { - port: publicTcpPort.value, - tlsCertificateKey: publicTlsCertificateKey.stringValue + port: publicPort, + certificate: publicTlsCertificateKey } - data.path = path; - data.topic = topic.stringValue; + data.mappings = mappings; + + const kafkaTopicCreationDisabled = zillaPlusContext.kafkaTopicCreationDisabled ?? false; const yamlTemplate: string = fs.readFileSync('zilla.yaml.mustache', 'utf8'); const renderedYaml: string = Mustache.render(yamlTemplate, data); @@ -492,16 +475,22 @@ action=/opt/aws/bin/cfn-init -v --stack ${id} --resource ZillaPlusLaunchTemplate runas=root `; + let topicsCommand = ""; + kafkaTopics.forEach((t: String) => { + topicsCommand = topicsCommand.concat(` +./kafka-topics.sh --create --if-not-exists --bootstrap-server ${bootstrapBrokers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${t} --config 'cleanup.policy=compact'`); + }); + let kafkaTopicCreationCommand = ""; - if (!userVars.kafkaTopicCreationDisabled) { + if (!kafkaTopicCreationDisabled) { kafkaTopicCreationCommand = ` wget https://archive.apache.org/dist/kafka/3.5.1/kafka_2.13-3.5.1.tgz tar -xzf kafka_2.13-3.5.1.tgz cd kafka_2.13-3.5.1/libs wget https://github.com/aws/aws-msk-iam-auth/releases/download/v1.1.1/aws-msk-iam-auth-1.1.1-all.jar cd ../bin -SECRET_STRING=$(aws secretsmanager get-secret-value --secret-id ${mskCredentialsSecretName.stringValue} --query SecretString --output text) +SECRET_STRING=$(aws secretsmanager get-secret-value --secret-id ${mskCredentialsSecretName} --query SecretString --output text) USERNAME=$(echo $SECRET_STRING | jq -r '.username') PASSWORD=$(echo $SECRET_STRING | jq -r '.password') @@ -510,8 +499,8 @@ sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule require security.protocol=SASL_SSL sasl.mechanism=SCRAM-SHA-512 EOF -./kafka-topics.sh --create --if-not-exists --bootstrap-server ${bootstrapBrokers} --command-config client.properties --replication-factor 2 --partitions 3 --topic ${topic.stringValue} --config 'cleanup.policy=compact' - `; +${topicsCommand} +`; } const userData = `#!/bin/bash -xe @@ -550,8 +539,8 @@ ${kafkaTopicCreationCommand} `; const ZillaPlusLaunchTemplate = new launchTemplate.LaunchTemplate(this, `ZillaPlusLaunchTemplate-${id}`, { - imageId: ami.imageId, - instanceType: instanceType.stringValue, + imageId: imageId, + instanceType: instanceType, networkInterfaces: [ { associatePublicIpAddress: "true", @@ -573,7 +562,7 @@ ${kafkaTopicCreationCommand} }, minSize: 1, maxSize: 5, - desiredCapacity: zillaPlusCapacity.numberValue, + desiredCapacity: zillaPlusCapacity, targetGroupArns: [nlbTargetGroup.arn], }); @@ -583,7 +572,3 @@ ${kafkaTopicCreationCommand} }); } } - -const app = new App(); -new ZillaPlusWebStreamingStack(app, "web-streaming"); -app.synth(); diff --git a/amazon-msk/cdktf/web-streaming/zilla.yaml.mustache b/amazon-msk/cdktf/web-streaming/zilla.yaml.mustache index 9128639..732ac91 100644 --- a/amazon-msk/cdktf/web-streaming/zilla.yaml.mustache +++ b/amazon-msk/cdktf/web-streaming/zilla.yaml.mustache @@ -7,9 +7,9 @@ guards: auth_jwt: type: jwt options: - issuer: {{jwt.issuer}} - audience: {{jwt.audience}} - keys: {{jwt.keysUrl}} + issuer: {{{jwt.issuer}}} + audience: {{{jwt.audience}}} + keys: {{{jwt.keysUrl}}} {{/jwt}} {{#glue}} catalogs: @@ -60,7 +60,7 @@ bindings: vault: secure options: keys: - - {{public.tlsCertificateKey}} + - {{public.certificate}} telemetry: metrics: - stream.* @@ -82,6 +82,7 @@ bindings: authorization: Bearer {credentials} {{/jwt}} routes: +{{#mappings}} - when: - headers: :path: {{{path}}} @@ -103,6 +104,7 @@ bindings: - http:stream {{/jwt}} exit: http_kafka_mapping +{{/mappings}} sse_server: type: sse kind: server @@ -117,11 +119,13 @@ bindings: metrics: - stream.* routes: +{{#mappings}} - when: - path: {{{path}}} exit: kafka_cache_client with: topic: {{topic}} +{{/mappings}} http_kafka_mapping: type: http-kafka kind: proxy @@ -129,6 +133,7 @@ bindings: metrics: - stream.* routes: +{{#mappings}} - when: - method: POST path: {{{path}}} @@ -171,12 +176,14 @@ bindings: topic: {{topic}} filters: - key: ${params.id} +{{/mappings}} kafka_cache_client: type: kafka kind: cache_client {{#glue}} options: topics: +{{#mappings}} - name: {{topic}} value: model: avro @@ -185,6 +192,7 @@ bindings: glue_catalog: - strategy: topic version: latest +{{/mappings}} {{/glue}} telemetry: metrics: @@ -195,9 +203,12 @@ bindings: kind: cache_server options: bootstrap: +{{#mappings}} - {{topic}} +{{/mappings}} {{#glue}} topics: +{{#mappings}} - name: {{topic}} value: model: avro @@ -206,6 +217,7 @@ bindings: glue_catalog: - strategy: topic version: latest +{{/mappings}} {{/glue}} telemetry: metrics: