From d288b66fbb7bff6260a4a2c9652ba80baf75eb9b Mon Sep 17 00:00:00 2001 From: Andre Kurait Date: Thu, 19 Sep 2024 22:24:41 -0500 Subject: [PATCH 1/2] CDK Sonar Fixes Signed-off-by: Andre Kurait --- .../lib/common-utilities.ts | 35 +++++-------------- .../lib/lambda/acm-cert-importer-handler.ts | 23 ++++++------ .../lib/migration-services-yaml.ts | 7 ---- .../lib/network-stack.ts | 2 +- .../lib/opensearch-domain-stack.ts | 22 ++++++------ .../lib/service-stacks/capture-proxy-stack.ts | 2 +- .../service-stacks/migration-console-stack.ts | 2 +- .../migration-otel-collector-sidecar.ts | 4 +-- .../service-stacks/migration-service-core.ts | 9 +++-- .../reindex-from-snapshot-stack.ts | 6 ++-- .../lib/stack-composer.ts | 24 ++++++------- .../test/test-utils.ts | 1 - 12 files changed, 56 insertions(+), 81 deletions(-) diff --git a/deployment/cdk/opensearch-service-migration/lib/common-utilities.ts b/deployment/cdk/opensearch-service-migration/lib/common-utilities.ts index 2ce5358bd..452b59d6d 100644 --- a/deployment/cdk/opensearch-service-migration/lib/common-utilities.ts +++ b/deployment/cdk/opensearch-service-migration/lib/common-utilities.ts @@ -2,8 +2,6 @@ import {Effect, PolicyStatement, Role, ServicePrincipal} from "aws-cdk-lib/aws-i import {Construct} from "constructs"; import {CpuArchitecture} from "aws-cdk-lib/aws-ecs"; import {RemovalPolicy} from "aws-cdk-lib"; -import { IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2"; -import { ICertificate } from "aws-cdk-lib/aws-certificatemanager"; import { IStringParameter, StringParameter } from "aws-cdk-lib/aws-ssm"; import * as forge from 'node-forge'; import * as yargs from 'yargs'; @@ -211,20 +209,18 @@ export function createDefaultECSTaskRole(scope: Construct, serviceName: string): } export function validateFargateCpuArch(cpuArch?: string): CpuArchitecture { - const desiredArch = cpuArch ? cpuArch : process.arch + const desiredArch = cpuArch ?? process.arch const desiredArchUpper = desiredArch.toUpperCase() if (desiredArchUpper === "X86_64" || desiredArchUpper === "X64") { return CpuArchitecture.X86_64 } else if (desiredArchUpper === "ARM64") { return CpuArchitecture.ARM64 - } else { - if (cpuArch) { - throw new Error(`Unknown Fargate cpu architecture provided: ${desiredArch}`) - } - else { - throw new Error(`Unsupported process cpu architecture detected: ${desiredArch}, CDK requires X64 or ARM64 for Docker image compatability`) - } + } else if (cpuArch) { + throw new Error(`Unknown Fargate cpu architecture provided: ${desiredArch}`) + } + else { + throw new Error(`Unsupported process cpu architecture detected: ${desiredArch}, CDK requires X64 or ARM64 for Docker image compatability`) } } @@ -235,21 +231,6 @@ export function parseRemovalPolicy(optionName: string, policyNameString?: string } return policy } - - -export type ALBConfig = NewALBListenerConfig; - -export interface NewALBListenerConfig { - alb: IApplicationLoadBalancer, - albListenerCert: ICertificate, - albListenerPort?: number, -} - -export function isNewALBListenerConfig(config: ALBConfig): config is NewALBListenerConfig { - const parsed = config as NewALBListenerConfig; - return parsed.alb !== undefined && parsed.albListenerCert !== undefined; -} - export function hashStringSHA256(message: string): string { const md = forge.md.sha256.create(); md.update(message); @@ -317,7 +298,7 @@ export enum MigrationSSMParameter { } -export class ClusterNoAuth {}; +export class ClusterNoAuth {} export class ClusterSigV4Auth { region?: string; @@ -443,4 +424,4 @@ export function parseClusterDefinition(json: any): ClusterYaml { throw new Error(`Invalid auth type when parsing cluster definition: ${json.auth.type}`) } return new ClusterYaml({endpoint, version, auth}) -} \ No newline at end of file +} diff --git a/deployment/cdk/opensearch-service-migration/lib/lambda/acm-cert-importer-handler.ts b/deployment/cdk/opensearch-service-migration/lib/lambda/acm-cert-importer-handler.ts index 4763f960d..1cf7157e3 100644 --- a/deployment/cdk/opensearch-service-migration/lib/lambda/acm-cert-importer-handler.ts +++ b/deployment/cdk/opensearch-service-migration/lib/lambda/acm-cert-importer-handler.ts @@ -15,23 +15,26 @@ export const handler = async (event: CloudFormationCustomResourceEvent, context: try { switch (event.RequestType) { - case 'Create': + case 'Create': { const { certificate, privateKey, certificateChain } = await generateSelfSignedCertificate(); const certificateArn = await importCertificate(certificate, privateKey, certificateChain); console.log(`Certificate imported with ARN: ${certificateArn}`); responseData = { CertificateArn: certificateArn }; physicalResourceId = certificateArn; break; - case 'Update': + } + case 'Update': { // No update logic needed, return existing physical resource id physicalResourceId = event.PhysicalResourceId; - break; - case 'Delete': + break; + } + case 'Delete': { const arn = event.PhysicalResourceId; await deleteCertificate(arn); responseData = { CertificateArn: arn }; physicalResourceId = arn; break; + } } return await sendResponse(event, context, 'SUCCESS', responseData, physicalResourceId); @@ -45,7 +48,7 @@ async function generateSelfSignedCertificate(): Promise<{ certificate: string, p return new Promise((resolve, reject) => { const keys = forge.pki.rsa.generateKeyPair(2048); const cert = forge.pki.createCertificate(); - + cert.publicKey = keys.publicKey; cert.serialNumber = '01'; cert.validity.notBefore = new Date(Date.UTC(1970, 0, 1, 0, 0, 0)); @@ -54,10 +57,10 @@ async function generateSelfSignedCertificate(): Promise<{ certificate: string, p name: 'commonName', value: 'localhost' }]; - + cert.setSubject(attrs); cert.setIssuer(attrs); - + cert.setExtensions([{ name: 'basicConstraints', cA: true @@ -78,7 +81,7 @@ async function generateSelfSignedCertificate(): Promise<{ certificate: string, p clientAuth: true },]); cert.sign(keys.privateKey, forge.md.sha384.create()); - + const pemCert = forge.pki.certificateToPem(cert); const pemKey = forge.pki.privateKeyToPem(keys.privateKey); @@ -165,7 +168,7 @@ async function sendResponse(event: CloudFormationCustomResourceEvent, context: C }); }); - request.on('error', (error) => { + request.on('error', (error: Error) => { console.error('sendResponse Error:', error); reject(error); }); @@ -173,4 +176,4 @@ async function sendResponse(event: CloudFormationCustomResourceEvent, context: C request.write(responseBody); request.end(); }); -} \ No newline at end of file +} diff --git a/deployment/cdk/opensearch-service-migration/lib/migration-services-yaml.ts b/deployment/cdk/opensearch-service-migration/lib/migration-services-yaml.ts index 5b0dcc23c..bb492892f 100644 --- a/deployment/cdk/opensearch-service-migration/lib/migration-services-yaml.ts +++ b/deployment/cdk/opensearch-service-migration/lib/migration-services-yaml.ts @@ -110,13 +110,6 @@ export class MetadataMigrationYaml { otel_endpoint: string = ''; source_cluster_version?: string; } - -export class MSKYaml { -} - -export class StandardKafkaYaml { -} - export class KafkaYaml { broker_endpoints: string = ''; msk?: string | null; diff --git a/deployment/cdk/opensearch-service-migration/lib/network-stack.ts b/deployment/cdk/opensearch-service-migration/lib/network-stack.ts index 20f1939ba..579a284bd 100644 --- a/deployment/cdk/opensearch-service-migration/lib/network-stack.ts +++ b/deployment/cdk/opensearch-service-migration/lib/network-stack.ts @@ -110,7 +110,7 @@ export class NetworkStack extends Stack { this.vpc = new Vpc(this, 'domainVPC', { // IP space should be customized for use cases that have specific IP range needs ipAddresses: IpAddresses.cidr('10.0.0.0/16'), - maxAzs: zoneCount ? zoneCount : 2, + maxAzs: zoneCount ?? 2, subnetConfiguration: [ // Outbound internet access for private subnets require a NAT Gateway which must live in // a public subnet diff --git a/deployment/cdk/opensearch-service-migration/lib/opensearch-domain-stack.ts b/deployment/cdk/opensearch-service-migration/lib/opensearch-domain-stack.ts index 84f9cbb8d..52210d277 100644 --- a/deployment/cdk/opensearch-service-migration/lib/opensearch-domain-stack.ts +++ b/deployment/cdk/opensearch-service-migration/lib/opensearch-domain-stack.ts @@ -15,8 +15,14 @@ import {ILogGroup, LogGroup} from "aws-cdk-lib/aws-logs"; import {ISecret, Secret} from "aws-cdk-lib/aws-secretsmanager"; import {StackPropsExt} from "./stack-composer"; import { ClusterYaml } from "./migration-services-yaml"; -import { ClusterAuth, ClusterBasicAuth, ClusterNoAuth } from "./common-utilities" -import { MigrationSSMParameter, createMigrationStringParameter, getMigrationStringParameterValue } from "./common-utilities"; +import { + ClusterAuth, + ClusterBasicAuth, + ClusterNoAuth, + MigrationSSMParameter, + createMigrationStringParameter, + getMigrationStringParameterValue +} from "./common-utilities"; export interface OpensearchDomainStackProps extends StackPropsExt { @@ -56,8 +62,6 @@ export interface OpensearchDomainStackProps extends StackPropsExt { } -export const osClusterEndpointParameterName = "osClusterEndpoint"; - export class OpenSearchDomainStack extends Stack { targetClusterYaml: ClusterYaml; @@ -99,8 +103,7 @@ export class OpenSearchDomainStack extends Stack { } createSSMParameters(domain: Domain, adminUserName: string|undefined, adminUserSecret: ISecret|undefined, stage: string, deployId: string) { - const endpointParameter = osClusterEndpointParameterName - const endpointSSM = createMigrationStringParameter(this, `https://${domain.domainEndpoint}:443`, { + createMigrationStringParameter(this, `https://${domain.domainEndpoint}:443`, { parameter: MigrationSSMParameter.OS_CLUSTER_ENDPOINT, defaultDeployId: deployId, stage, @@ -108,10 +111,10 @@ export class OpenSearchDomainStack extends Stack { if (domain.masterUserPassword && !adminUserSecret) { console.log(`An OpenSearch domain fine-grained access control user was configured without an existing Secrets Manager secret, will not create SSM Parameter: /migration/${stage}/${deployId}/osUserAndSecret`) } else if (domain.masterUserPassword && adminUserSecret) { - const secretSSM = createMigrationStringParameter(this, `${adminUserName} ${adminUserSecret.secretArn}`, { + createMigrationStringParameter(this, `${adminUserName} ${adminUserSecret.secretArn}`, { parameter: MigrationSSMParameter.OS_USER_AND_SECRET_ARN, defaultDeployId: deployId, - stage, + stage, }); } } @@ -138,12 +141,11 @@ export class OpenSearchDomainStack extends Stack { const appLG: ILogGroup|undefined = props.appLogGroup && props.appLogEnabled ? LogGroup.fromLogGroupArn(this, "appLogGroup", props.appLogGroup) : undefined - const domainAccessSecurityGroupParameter = props.domainAccessSecurityGroupParameter ?? "osAccessSecurityGroupId" const defaultOSClusterAccessGroup = SecurityGroup.fromSecurityGroupId(this, "defaultDomainAccessSG", getMigrationStringParameterValue(this, { ...props, parameter: MigrationSSMParameter.OS_ACCESS_SECURITY_GROUP_ID, })); - + let adminUserSecret: ISecret|undefined = props.fineGrainedManagerUserSecretManagerKeyARN ? Secret.fromSecretCompleteArn(this, "managerSecret", props.fineGrainedManagerUserSecretManagerKeyARN) : undefined // Map objects from props diff --git a/deployment/cdk/opensearch-service-migration/lib/service-stacks/capture-proxy-stack.ts b/deployment/cdk/opensearch-service-migration/lib/service-stacks/capture-proxy-stack.ts index d5eb90a21..49e57a212 100644 --- a/deployment/cdk/opensearch-service-migration/lib/service-stacks/capture-proxy-stack.ts +++ b/deployment/cdk/opensearch-service-migration/lib/service-stacks/capture-proxy-stack.ts @@ -89,7 +89,7 @@ export class CaptureProxyStack extends MigrationServiceCore { constructor(scope: Construct, id: string, props: CaptureProxyProps) { super(scope, id, props) - const serviceName = props.serviceName || "capture-proxy"; + const serviceName = props.serviceName ?? "capture-proxy"; let securityGroupConfigs = [ { id: "serviceSG", param: MigrationSSMParameter.SERVICE_SECURITY_GROUP_ID }, diff --git a/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-console-stack.ts b/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-console-stack.ts index 96135d343..348f6fdcb 100644 --- a/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-console-stack.ts +++ b/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-console-stack.ts @@ -1,6 +1,6 @@ import {StackPropsExt} from "../stack-composer"; import {IVpc, SecurityGroup} from "aws-cdk-lib/aws-ec2"; -import {CpuArchitecture, MountPoint, PortMapping, Protocol, Volume} from "aws-cdk-lib/aws-ecs"; +import {CpuArchitecture, PortMapping, Protocol} from "aws-cdk-lib/aws-ecs"; import {Construct} from "constructs"; import {join} from "path"; import {Effect, PolicyStatement, Role, ServicePrincipal} from "aws-cdk-lib/aws-iam"; diff --git a/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-otel-collector-sidecar.ts b/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-otel-collector-sidecar.ts index 029a20eaa..a23d44daa 100644 --- a/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-otel-collector-sidecar.ts +++ b/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-otel-collector-sidecar.ts @@ -13,8 +13,8 @@ import { DockerImageAsset } from "aws-cdk-lib/aws-ecr-assets"; import { join } from "path"; export class OtelCollectorSidecar { - public static OTEL_CONTAINER_PORT = 4317; - public static OTEL_CONTAINER_HEALTHCHECK_PORT = 13133; + public static readonly OTEL_CONTAINER_PORT = 4317; + public static readonly OTEL_CONTAINER_HEALTHCHECK_PORT = 13133; static getOtelLocalhostEndpoint() { return "http://localhost:" + OtelCollectorSidecar.OTEL_CONTAINER_PORT; diff --git a/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-service-core.ts b/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-service-core.ts index 432e57c0c..945888162 100644 --- a/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-service-core.ts +++ b/deployment/cdk/opensearch-service-migration/lib/service-stacks/migration-service-core.ts @@ -1,7 +1,6 @@ import {StackPropsExt} from "../stack-composer"; import {ISecurityGroup, IVpc, SubnetType} from "aws-cdk-lib/aws-ec2"; import { - CfnService as FargateCfnService, Cluster, ContainerImage, CpuArchitecture, FargateService, @@ -140,8 +139,8 @@ export class MigrationServiceCore extends Stack { let startupPeriodSeconds = 30; // Add a separate container to monitor and fail healthcheck after a given maxUptime const maxUptimeContainer = serviceTaskDef.addContainer("MaxUptimeContainer", { - image: ContainerImage.fromRegistry("public.ecr.aws/amazonlinux/amazonlinux:2023-minimal"), - memoryLimitMiB: 64, + image: ContainerImage.fromRegistry("public.ecr.aws/amazonlinux/amazonlinux:2023-minimal"), + memoryLimitMiB: 64, entryPoint: [ "/bin/sh", "-c", @@ -185,10 +184,10 @@ export class MigrationServiceCore extends Stack { securityGroups: props.securityGroups, vpcSubnets: props.vpc.selectSubnets({subnetType: SubnetType.PRIVATE_WITH_EGRESS}), }); - + if (props.targetGroups) { props.targetGroups.filter(tg => tg !== undefined).forEach(tg => tg.addTarget(fargateService)); } } -} \ No newline at end of file +} diff --git a/deployment/cdk/opensearch-service-migration/lib/service-stacks/reindex-from-snapshot-stack.ts b/deployment/cdk/opensearch-service-migration/lib/service-stacks/reindex-from-snapshot-stack.ts index b42f3dde5..d453ebe77 100644 --- a/deployment/cdk/opensearch-service-migration/lib/service-stacks/reindex-from-snapshot-stack.ts +++ b/deployment/cdk/opensearch-service-migration/lib/service-stacks/reindex-from-snapshot-stack.ts @@ -79,9 +79,9 @@ export class ReindexFromSnapshotStack extends MigrationServiceCore { let targetPassword = ""; let targetPasswordArn = ""; if (props.clusterAuthDetails.basicAuth) { - targetUser = props.clusterAuthDetails.basicAuth.username, - targetPassword = props.clusterAuthDetails.basicAuth.password || "", - targetPasswordArn = props.clusterAuthDetails.basicAuth.password_from_secret_arn || "" + targetUser = props.clusterAuthDetails.basicAuth.username + targetPassword = props.clusterAuthDetails.basicAuth.password ?? "" + targetPasswordArn = props.clusterAuthDetails.basicAuth.password_from_secret_arn ?? "" }; const sharedLogFileSystem = new SharedLogFileSystem(this, props.stage, props.defaultDeployId); const openSearchPolicy = createOpenSearchIAMAccessPolicy(this.partition, this.region, this.account); diff --git a/deployment/cdk/opensearch-service-migration/lib/stack-composer.ts b/deployment/cdk/opensearch-service-migration/lib/stack-composer.ts index a96b3b739..085257af3 100644 --- a/deployment/cdk/opensearch-service-migration/lib/stack-composer.ts +++ b/deployment/cdk/opensearch-service-migration/lib/stack-composer.ts @@ -79,10 +79,10 @@ export class StackComposer { private getEngineVersion(engineVersionString: string) : EngineVersion { let version: EngineVersion - if (engineVersionString && engineVersionString.startsWith("OS_")) { + if (engineVersionString?.startsWith("OS_")) { // Will accept a period delimited version string (i.e. 1.3) and return a proper EngineVersion version = EngineVersion.openSearch(engineVersionString.substring(3)) - } else if (engineVersionString && engineVersionString.startsWith("ES_")) { + } else if (engineVersionString?.startsWith("ES_")) { version = EngineVersion.elasticsearch(engineVersionString.substring(3)) } else { throw new Error(`Engine version (${engineVersionString}) is not present or does not match the expected format, i.e. OS_1.3 or ES_7.9`) @@ -224,7 +224,7 @@ export class StackComposer { "auth": {"type": "none"} } } - const sourceClusterDisabled = sourceClusterDefinition?.disabled ? true : false + const sourceClusterDisabled = !!sourceClusterDefinition?.disabled const sourceCluster = (sourceClusterDefinition && !sourceClusterDisabled) ? parseClusterDefinition(sourceClusterDefinition) : undefined const sourceClusterEndpoint = sourceCluster?.endpoint @@ -263,7 +263,7 @@ export class StackComposer { } const targetClusterAuth = targetCluster?.auth - const targetVersion = this.getEngineVersion(targetCluster?.version || engineVersion) + const targetVersion = this.getEngineVersion(targetCluster?.version ?? engineVersion) const requiredFields: { [key: string]: any; } = {"stage":stage} for (let key in requiredFields) { @@ -277,12 +277,12 @@ export class StackComposer { if (stage.length > 15) { throw new Error(`Maximum allowed stage name length is 15 characters but received ${stage}`) } - const clusterDomainName = domainName ? domainName : `os-cluster-${stage}` + const clusterDomainName = domainName ?? `os-cluster-${stage}` let preexistingOrContainerTargetEndpoint if (targetCluster && osContainerServiceEnabled) { throw new Error("The following options are mutually exclusive as only one target cluster can be specified for a given deployment: [targetCluster, osContainerServiceEnabled]") } else if (targetCluster || osContainerServiceEnabled) { - preexistingOrContainerTargetEndpoint = targetCluster?.endpoint || "https://opensearch:9200" + preexistingOrContainerTargetEndpoint = targetCluster?.endpoint ?? "https://opensearch:9200" } const fargateCpuArch = validateFargateCpuArch(defaultFargateCpuArch) @@ -309,14 +309,14 @@ export class StackComposer { trafficReplayerCustomUserAgent = `${props.customReplayerUserAgent};${trafficReplayerUserAgentSuffix}` } else { - trafficReplayerCustomUserAgent = trafficReplayerUserAgentSuffix ? trafficReplayerUserAgentSuffix : props.customReplayerUserAgent + trafficReplayerCustomUserAgent = trafficReplayerUserAgentSuffix ?? props.customReplayerUserAgent } if (sourceClusterDisabled && (sourceCluster || captureProxyESServiceEnabled || elasticsearchServiceEnabled || captureProxyServiceEnabled)) { throw new Error("A source cluster must be specified by one of: [sourceCluster, captureProxyESServiceEnabled, elasticsearchServiceEnabled, captureProxyServiceEnabled]"); } - const deployId = addOnMigrationDeployId ? addOnMigrationDeployId : defaultDeployId + const deployId = addOnMigrationDeployId ?? defaultDeployId // If enabled re-use existing VPC and/or associated resources or create new let networkStack: NetworkStack|undefined @@ -392,10 +392,8 @@ export class StackComposer { this.addDependentStacks(openSearchStack, [networkStack]) this.stacks.push(openSearchStack) servicesYaml.target_cluster = openSearchStack.targetClusterYaml; - } else { - if (targetCluster) { - servicesYaml.target_cluster = targetCluster - } + } else if (targetCluster) { + servicesYaml.target_cluster = targetCluster } let migrationStack @@ -436,7 +434,7 @@ export class StackComposer { this.addDependentStacks(osContainerStack, [migrationStack]) this.stacks.push(osContainerStack) servicesYaml.target_cluster = new ClusterYaml({ - endpoint: preexistingOrContainerTargetEndpoint || "", + endpoint: preexistingOrContainerTargetEndpoint ?? "", auth: new ClusterAuth({noAuth: new ClusterNoAuth()}) }) } diff --git a/deployment/cdk/opensearch-service-migration/test/test-utils.ts b/deployment/cdk/opensearch-service-migration/test/test-utils.ts index dea702f3d..7784ef6ce 100644 --- a/deployment/cdk/opensearch-service-migration/test/test-utils.ts +++ b/deployment/cdk/opensearch-service-migration/test/test-utils.ts @@ -1,4 +1,3 @@ -import {Construct} from "constructs"; import {StackComposer} from "../lib/stack-composer"; import {App} from "aws-cdk-lib"; From e9ed46bfa44ee979cb2062e08907b60d0f4c2149 Mon Sep 17 00:00:00 2001 From: Andre Kurait Date: Thu, 19 Sep 2024 23:59:32 -0500 Subject: [PATCH 2/2] Add cdk tests for app and acm-cert Signed-off-by: Andre Kurait --- .../opensearch-service-migration/bin/app.ts | 27 +-- .../bin/createApp.ts | 28 +++ .../test/createApp.test.ts | 77 ++++++++ .../lambda/acm-cert-importer-handler.test.ts | 177 ++++++++++++++++++ 4 files changed, 285 insertions(+), 24 deletions(-) create mode 100644 deployment/cdk/opensearch-service-migration/bin/createApp.ts create mode 100644 deployment/cdk/opensearch-service-migration/test/createApp.test.ts create mode 100644 deployment/cdk/opensearch-service-migration/test/lambda/acm-cert-importer-handler.test.ts diff --git a/deployment/cdk/opensearch-service-migration/bin/app.ts b/deployment/cdk/opensearch-service-migration/bin/app.ts index 74391be2e..1af7660f8 100644 --- a/deployment/cdk/opensearch-service-migration/bin/app.ts +++ b/deployment/cdk/opensearch-service-migration/bin/app.ts @@ -1,27 +1,6 @@ #!/usr/bin/env node import 'source-map-support/register'; -import {readFileSync} from 'fs'; -import {App, Tags} from 'aws-cdk-lib'; -import {StackComposer} from "../lib/stack-composer"; +import { createApp } from './createApp'; -const app = new App(); -const versionFile = readFileSync('../../../VERSION', 'utf-8') -// Remove any blank newlines because this would be an invalid tag value -const version = versionFile.replace(/\n/g, ''); -Tags.of(app).add("migration_deployment", version) -const account = process.env.CDK_DEFAULT_ACCOUNT -const region = process.env.CDK_DEFAULT_REGION -// Environment setting to allow providing an existing AWS AppRegistry application ARN which each created CDK stack -// from this CDK app will be added to. -const migrationsAppRegistryARN = process.env.MIGRATIONS_APP_REGISTRY_ARN -if (migrationsAppRegistryARN) { - console.info(`App Registry mode is enabled for CFN stack tracking. Will attempt to import the App Registry application from the MIGRATIONS_APP_REGISTRY_ARN env variable of ${migrationsAppRegistryARN} and looking in the configured region of ${region}`) -} -const customReplayerUserAgent = process.env.CUSTOM_REPLAYER_USER_AGENT - -new StackComposer(app, { - migrationsAppRegistryARN: migrationsAppRegistryARN, - customReplayerUserAgent: customReplayerUserAgent, - migrationsSolutionVersion: version, - env: { account: account, region: region } -}); \ No newline at end of file +const app = createApp(); +app.synth(); diff --git a/deployment/cdk/opensearch-service-migration/bin/createApp.ts b/deployment/cdk/opensearch-service-migration/bin/createApp.ts new file mode 100644 index 000000000..69440e010 --- /dev/null +++ b/deployment/cdk/opensearch-service-migration/bin/createApp.ts @@ -0,0 +1,28 @@ +import { App, Tags } from 'aws-cdk-lib'; +import { readFileSync } from 'fs'; +import { StackComposer } from "../lib/stack-composer"; + +export function createApp(): App { + const app = new App(); + const versionFile = readFileSync('../../../VERSION', 'utf-8'); + const version = versionFile.replace(/\n/g, ''); + Tags.of(app).add("migration_deployment", version); + + const account = process.env.CDK_DEFAULT_ACCOUNT; + const region = process.env.CDK_DEFAULT_REGION; + const migrationsAppRegistryARN = process.env.MIGRATIONS_APP_REGISTRY_ARN; + const customReplayerUserAgent = process.env.CUSTOM_REPLAYER_USER_AGENT; + + if (migrationsAppRegistryARN) { + console.info(`App Registry mode is enabled for CFN stack tracking. Will attempt to import the App Registry application from the MIGRATIONS_APP_REGISTRY_ARN env variable of ${migrationsAppRegistryARN} and looking in the configured region of ${region}`); + } + + new StackComposer(app, { + migrationsAppRegistryARN: migrationsAppRegistryARN, + customReplayerUserAgent: customReplayerUserAgent, + migrationsSolutionVersion: version, + env: { account: account, region: region } + }); + + return app; +} diff --git a/deployment/cdk/opensearch-service-migration/test/createApp.test.ts b/deployment/cdk/opensearch-service-migration/test/createApp.test.ts new file mode 100644 index 000000000..9d8906f71 --- /dev/null +++ b/deployment/cdk/opensearch-service-migration/test/createApp.test.ts @@ -0,0 +1,77 @@ +import { App, Tags } from 'aws-cdk-lib'; +import { createApp } from '../bin/createApp'; +import { StackComposer } from '../lib/stack-composer'; + +jest.mock('fs', () => ({ + readFileSync: jest.fn().mockReturnValue('1.0.0\n'), +})); + +jest.mock('aws-cdk-lib', () => ({ + App: jest.fn().mockImplementation(() => ({ + node: { + tryGetContext: jest.fn(), + }, + })), + Tags: { + of: jest.fn().mockReturnValue({ + add: jest.fn(), + }), + }, + Stack: class MockStack {}, +})); + +jest.mock('../lib/stack-composer'); + +describe('createApp', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.clearAllMocks(); + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it('should create an App instance with correct configuration', () => { + // Set up environment variables + process.env.CDK_DEFAULT_ACCOUNT = 'test-account'; + process.env.CDK_DEFAULT_REGION = 'test-region'; + process.env.MIGRATIONS_APP_REGISTRY_ARN = 'test-arn'; + process.env.CUSTOM_REPLAYER_USER_AGENT = 'test-user-agent'; + + const consoleSpy = jest.spyOn(console, 'info').mockImplementation(); + const mockAddTag = jest.fn(); + Tags.of = jest.fn().mockReturnValue({ add: mockAddTag }); + + const app = createApp(); + + // Verify App creation + expect(App).toHaveBeenCalled(); + + // Verify tag addition + expect(mockAddTag).toHaveBeenCalledWith('migration_deployment', '1.0.0'); + + // Verify StackComposer creation + expect(StackComposer).toHaveBeenCalledWith( + expect.any(Object), + { + migrationsAppRegistryARN: 'test-arn', + customReplayerUserAgent: 'test-user-agent', + migrationsSolutionVersion: '1.0.0', + env: { account: 'test-account', region: 'test-region' }, + } + ); + + // Verify console log + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('App Registry mode is enabled for CFN stack tracking') + ); + + // Verify app is returned + expect(app).toBeDefined(); + + consoleSpy.mockRestore(); + }); +}); \ No newline at end of file diff --git a/deployment/cdk/opensearch-service-migration/test/lambda/acm-cert-importer-handler.test.ts b/deployment/cdk/opensearch-service-migration/test/lambda/acm-cert-importer-handler.test.ts new file mode 100644 index 000000000..1e1baaca3 --- /dev/null +++ b/deployment/cdk/opensearch-service-migration/test/lambda/acm-cert-importer-handler.test.ts @@ -0,0 +1,177 @@ +import { handler } from '../../lib/lambda/acm-cert-importer-handler'; +import { CloudFormationCustomResourceCreateEvent, CloudFormationCustomResourceUpdateEvent, CloudFormationCustomResourceDeleteEvent, Context } from 'aws-lambda'; +import { ACMClient, ImportCertificateCommand, DeleteCertificateCommand } from '@aws-sdk/client-acm'; +import * as forge from 'node-forge'; +import * as https from 'https'; + +jest.mock('@aws-sdk/client-acm'); +jest.mock('node-forge'); +jest.mock('https'); + +describe('ACM Certificate Importer Handler', () => { + let mockContext: Context; + + beforeEach(() => { + mockContext = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'mockFunctionName', + functionVersion: 'mockFunctionVersion', + invokedFunctionArn: 'mockInvokedFunctionArn', + memoryLimitInMB: '128', + awsRequestId: 'mockAwsRequestId', + logGroupName: 'mockLogGroupName', + logStreamName: 'mockLogStreamName', + getRemainingTimeInMillis: jest.fn(), + done: jest.fn(), + fail: jest.fn(), + succeed: jest.fn() + }; + + process.env.AWS_REGION = 'us-west-2'; + + (https.request as jest.Mock).mockImplementation((options, callback) => { + const mockResponse = { + statusCode: 200, + statusMessage: 'OK', + }; + callback(mockResponse); + return { + on: jest.fn(), + write: jest.fn(), + end: jest.fn(), + }; + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('Create: should generate and import a self-signed certificate', async () => { + const mockEvent: CloudFormationCustomResourceCreateEvent = { + RequestType: 'Create', + ServiceToken: 'mockServiceToken', + ResponseURL: 'https://mockurl.com', + StackId: 'mockStackId', + RequestId: 'mockRequestId', + LogicalResourceId: 'mockLogicalResourceId', + ResourceType: 'Custom::ACMCertificateImporter', + ResourceProperties: { + ServiceToken: 'mockServiceToken' + } + }; + + const mockCertificate = 'mockCertificate'; + const mockPrivateKey = 'mockPrivateKey'; + const mockCertificateChain = 'mockCertificateChain'; + const mockCertificateArn = 'arn:aws:acm:us-west-2:123456789012:certificate/mock-certificate-id'; + + (forge.pki.rsa.generateKeyPair as jest.Mock).mockReturnValue({ + publicKey: 'mockPublicKey', + privateKey: 'mockPrivateKey' + }); + + const mockCert = { + publicKey: 'mockPublicKey', + serialNumber: '01', + validity: { + notBefore: new Date(), + notAfter: new Date() + }, + setSubject: jest.fn(), + setIssuer: jest.fn(), + setExtensions: jest.fn(), + sign: jest.fn() + }; + (forge.pki.createCertificate as jest.Mock).mockReturnValue(mockCert); + + (forge.pki.certificateToPem as jest.Mock).mockReturnValue(mockCertificate); + (forge.pki.privateKeyToPem as jest.Mock).mockReturnValue(mockPrivateKey); + + const mockSendFn = jest.fn().mockResolvedValue({ CertificateArn: mockCertificateArn }); + (ACMClient as jest.Mock).mockImplementation(() => ({ + send: mockSendFn + })); + + const result = await handler(mockEvent, mockContext); + + expect(result.Status).toBe('SUCCESS'); + expect(result.PhysicalResourceId).toBe(mockCertificateArn); + expect(result.Data).toEqual({ CertificateArn: mockCertificateArn }); + expect(mockSendFn).toHaveBeenCalledWith(expect.any(ImportCertificateCommand)); + + expect(forge.pki.rsa.generateKeyPair).toHaveBeenCalledWith(2048); + expect(mockCert.setSubject).toHaveBeenCalledWith([{ name: 'commonName', value: 'localhost' }]); + expect(mockCert.setIssuer).toHaveBeenCalledWith([{ name: 'commonName', value: 'localhost' }]); + expect(mockCert.setExtensions).toHaveBeenCalledWith(expect.arrayContaining([ + { name: 'basicConstraints', cA: true }, + { name: 'subjectAltName', altNames: [{ type: 2, value: 'localhost' }] }, + { name: 'keyUsage', keyCertSign: true, digitalSignature: true, keyEncipherment: true }, + { name: 'extKeyUsage', serverAuth: true, clientAuth: true } + ])); + expect(mockCert.sign).toHaveBeenCalledWith(expect.anything(), forge.md.sha1.create()); + + // Wait for any pending promises to resolve + await new Promise(process.nextTick); + }); + + test('Update: should return the existing physical resource id', async () => { + const mockEvent: CloudFormationCustomResourceUpdateEvent = { + RequestType: 'Update', + ServiceToken: 'mockServiceToken', + ResponseURL: 'https://mockurl.com', + StackId: 'mockStackId', + RequestId: 'mockRequestId', + LogicalResourceId: 'mockLogicalResourceId', + PhysicalResourceId: 'existingArn', + ResourceType: 'Custom::ACMCertificateImporter', + ResourceProperties: { + ServiceToken: 'mockServiceToken' + }, + OldResourceProperties: {} + }; + + const result = await handler(mockEvent, mockContext); + + expect(result.Status).toBe('SUCCESS'); + expect(result.PhysicalResourceId).toBe('existingArn'); + expect(result.Data).toEqual({}); + expect(ACMClient).not.toHaveBeenCalled(); + + // Wait for any pending promises to resolve + await new Promise(process.nextTick); + }); + + test('Delete: should delete the certificate', async () => { + const mockEvent: CloudFormationCustomResourceDeleteEvent = { + RequestType: 'Delete', + ServiceToken: 'mockServiceToken', + ResponseURL: 'https://mockurl.com', + StackId: 'mockStackId', + RequestId: 'mockRequestId', + LogicalResourceId: 'mockLogicalResourceId', + PhysicalResourceId: 'arnToDelete', + ResourceType: 'Custom::ACMCertificateImporter', + ResourceProperties: { + ServiceToken: 'mockServiceToken' + } + }; + + const mockSendFn = jest.fn().mockResolvedValue({}); + (ACMClient as jest.Mock).mockImplementation(() => ({ + send: mockSendFn + })); + + const result = await handler(mockEvent, mockContext); + + expect(result.Status).toBe('SUCCESS'); + expect(result.PhysicalResourceId).toBe('arnToDelete'); + expect(result.Data).toEqual({ CertificateArn: 'arnToDelete' }); + expect(mockSendFn).toHaveBeenCalledWith(expect.any(DeleteCertificateCommand)); + expect(mockSendFn).toHaveBeenCalledTimes(1); + expect(DeleteCertificateCommand).toHaveBeenCalledWith({ CertificateArn: 'arnToDelete' }); + + // Wait for any pending promises to resolve + await new Promise(process.nextTick); + }); +}); \ No newline at end of file