diff --git a/examples/s3-object-lambda/src/s3-object-lambda-stack.ts b/examples/s3-object-lambda/src/s3-object-lambda-stack.ts index 758ec23a..84398f3d 100644 --- a/examples/s3-object-lambda/src/s3-object-lambda-stack.ts +++ b/examples/s3-object-lambda/src/s3-object-lambda-stack.ts @@ -1,11 +1,11 @@ import * as pulumi from '@pulumi/pulumi'; +import * as aws from '@pulumi/aws-native'; import * as pulumicdk from '@pulumi/cdk'; import * as cdk from 'aws-cdk-lib'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as s3ObjectLambda from 'aws-cdk-lib/aws-s3objectlambda'; -import { Construct } from 'constructs'; // configurable variables const S3_ACCESS_POINT_NAME = 'example-test-ap'; @@ -74,23 +74,27 @@ export class S3ObjectLambdaStack extends pulumicdk.Stack { const policyStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:GetObject'], - principals: [new iam.ArnPrincipal(retrieveTransformedObjectLambda.role?.roleArn)], - resources: [`${accessPoint}/object/*`], + principals: [ + new iam.ArnPrincipal(this.asOutput(retrieveTransformedObjectLambda.role?.roleArn) as unknown as string), + ], + resources: [this.asOutput(`${accessPoint}/object/*`) as unknown as string], }); policyStatement.sid = 'AllowLambdaToUseAccessPoint'; policyDoc.addStatements(policyStatement); - new s3.CfnAccessPoint(this, 'exampleBucketAP', { - bucket: bucket.bucketName, + const ap = new aws.s3.AccessPoint('exampleBucketAP', { + // CDK property can be passed to a Pulumi resource + bucket: this.asOutput(bucket.bucketName), name: S3_ACCESS_POINT_NAME, - policy: policyDoc, + policy: policyDoc.toJSON(), }); // Access point to receive GET request and use lambda to process objects const objectLambdaAP = new s3ObjectLambda.CfnAccessPoint(this, 's3ObjectLambdaAP', { name: OBJECT_LAMBDA_ACCESS_POINT_NAME, objectLambdaConfiguration: { - supportingAccessPoint: accessPoint, + // a pulumi resource property can be passed to a cdk resource + supportingAccessPoint: pulumicdk.asString(ap.arn), transformationConfigurations: [ { actions: ['GetObject'], diff --git a/package.json b/package.json index 3baa494e..5df2c484 100644 --- a/package.json +++ b/package.json @@ -21,40 +21,46 @@ } }, "resolutions": { - "wrap-ansi": "7.0.0", - "string-width": "4.1.0" + "wrap-ansi": "7.0.0", + "string-width": "4.1.0" }, "devDependencies": { "@aws-cdk/aws-apprunner-alpha": "2.20.0-alpha.0", "@pulumi/aws": "^6.32.0", + "@pulumi/aws-native": "0.121.0", "@pulumi/docker": "^4.5.0", "@pulumi/pulumi": "^3.117.0", "@types/archiver": "^6.0.2", + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.2", + "@types/mock-fs": "^4.13.4", "@types/node": "^20.12.13", "aws-cdk-lib": "2.149.0", "constructs": "^10.0.111", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "typescript-eslint": "^7.16.1", "jest": "^29.5.0", "jest-junit": "^15", + "mock-fs": "^5.3.0", "prettier": "^2.6.2", "ts-jest": "^29.1.0", "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "typescript-eslint": "^7.16.1" }, "peerDependencies": { "@pulumi/aws": "^6.32.0", + "@pulumi/aws-native": "^0.121.0", "@pulumi/docker": "^4.5.0", "@pulumi/pulumi": "^3.117.0", "aws-cdk-lib": "^2.20.0", "constructs": "^10.0.111" }, "dependencies": { - "@pulumi/aws-native": "0.121.0", "@types/glob": "^8.1.0", - "archiver": "^7.0.1" + "archiver": "^7.0.1", + "cdk-assets": "^2.154.8", + "fs-extra": "^11.2.0" }, "scripts": { "set-version": "sed -i.bak -e \"s/\\${VERSION}/$(pulumictl get version --language javascript)/g\" package.json && rm package.json.bak", diff --git a/src/assembly/index.ts b/src/assembly/index.ts new file mode 100644 index 00000000..2b258e28 --- /dev/null +++ b/src/assembly/index.ts @@ -0,0 +1,3 @@ +export * from './stack'; +export * from './types'; +export * from './manifest'; diff --git a/src/assembly/manifest.ts b/src/assembly/manifest.ts new file mode 100644 index 00000000..479ee2d3 --- /dev/null +++ b/src/assembly/manifest.ts @@ -0,0 +1,174 @@ +import * as path from 'path'; +import { AssemblyManifest, Manifest, ArtifactType, ArtifactMetadataEntryType } from '@aws-cdk/cloud-assembly-schema'; +import * as fs from 'fs-extra'; +import { CloudFormationTemplate } from '../cfn'; +import { ArtifactManifest, AssetManifestProperties, LogicalIdMetadataEntry } from 'aws-cdk-lib/cloud-assembly-schema'; +import { AssetManifest, DockerImageManifestEntry, FileManifestEntry } from 'cdk-assets'; +import { StackManifest } from './stack'; +import { ConstructTree, StackAsset, StackMetadata } from './types'; +import { warn } from '@pulumi/pulumi/log'; + +/** + * Reads a Cloud Assembly manifest + */ +export class AssemblyManifestReader { + private static readonly DEFAULT_FILENAME = 'manifest.json'; + + /** + * Reads a Cloud Assembly manifest from a file or a directory + * If the given filePath is a directory then it will look for + * a file within the directory with the DEFAULT_FILENAME + */ + public static fromDirectory(dir: string): AssemblyManifestReader { + const filePath = path.join(dir, AssemblyManifestReader.DEFAULT_FILENAME); + try { + fs.statSync(dir); + const obj = Manifest.loadAssemblyManifest(filePath); + return new AssemblyManifestReader(dir, obj); + } catch (e: any) { + throw new Error(`Cannot read manifest at '${filePath}': ${e}`); + } + } + + /** + * The directory where the manifest was found + */ + public readonly directory: string; + + private readonly _stackManifests = new Map(); + private readonly tree: ConstructTree; + + constructor(directory: string, private readonly manifest: AssemblyManifest) { + this.directory = directory; + try { + const fullTree = fs.readJsonSync(path.resolve(this.directory, 'tree.json')); + if (!fullTree.tree || !fullTree.tree.children) { + throw new Error(`Invalid tree.json found ${JSON.stringify(fullTree)}`); + } + this.tree = fullTree.tree; + this.renderStackManifests(); + } catch (e) { + throw new Error(`Could not process CDK Cloud Assembly directory: ${e}`); + } + } + + /** + * Renders the StackManifests for all the stacks in the CloudAssembly + * - Finds all CloudFormation stacks in the assembly + * - Reads the stack template files + * - Creates a metadata map of constructPath to logicalId for all resources in the stack + * - Finds all assets that the stack depends on + */ + private renderStackManifests() { + for (const [artifactId, artifact] of Object.entries(this.manifest.artifacts ?? {})) { + if (artifact.type === ArtifactType.AWS_CLOUDFORMATION_STACK) { + if (!artifact.properties || !('templateFile' in artifact.properties)) { + throw new Error('Invalid CloudFormation artifact. Cannot find the template file'); + } + const templateFile = artifact.properties.templateFile; + + let template: CloudFormationTemplate; + try { + template = fs.readJSONSync(path.resolve(this.directory, templateFile)); + } catch (e) { + throw new Error(`Failed to read CloudFormation template at path: ${templateFile}: ${e}`); + } + + const metadata = this.getMetadata(artifact); + + const assets = this.getAssetsForStack(artifactId); + if (!this.tree.children) { + throw new Error('Invalid tree.json found'); + } + const stackTree = this.tree.children[artifactId]; + const stackManifest = new StackManifest( + this.directory, + artifactId, + templateFile, + metadata, + stackTree, + template, + assets, + ); + this._stackManifests.set(artifactId, stackManifest); + } + } + } + + /** + * Creates a metadata map of constructPath to logicalId for all resources in the stack + * + * @param artifact - The manifest containing the stack metadata + * @returns The StackMetadata lookup table + */ + private getMetadata(artifact: ArtifactManifest): StackMetadata { + const metadata: StackMetadata = {}; + for (const [metadataId, metadataEntry] of Object.entries(artifact.metadata ?? {})) { + metadataEntry.forEach((meta) => { + if (meta.type === ArtifactMetadataEntryType.LOGICAL_ID) { + // For some reason the metadata entry prefixes the path with a `/` + const path = metadataId.startsWith('/') ? metadataId.substring(1) : metadataId; + metadata[path] = meta.data as LogicalIdMetadataEntry; + } + }); + } + return metadata; + } + + /** + * Get the stacks from the Cloud Assembly + * + * @returns List of CloudFormationStackArtifacts available in the Cloud Assembly + */ + public get stackManifests(): StackManifest[] { + return Array.from(this._stackManifests.values()); + } + + /** + * Return a list of assets for a given stack + * + * @param stackId - The artifactId of the stack to find assets for + * @returns a list of `StackAsset` for the given stack + */ + private getAssetsForStack(stackId: string): StackAsset[] { + const assets: (FileManifestEntry | DockerImageManifestEntry)[] = []; + for (const artifact of Object.values(this.manifest.artifacts ?? {})) { + if ( + artifact.type === ArtifactType.ASSET_MANIFEST && + (artifact.properties as AssetManifestProperties)?.file === `${stackId}.assets.json` + ) { + assets.push(...this.assetsFromAssetManifest(artifact)); + } + } + return assets; + } + + /** + * Get a list of assets from the asset manifest. + * + * @param artifact - An ArtifactManifest to extract individual assets from + * @returns a list of file and docker assets found in the manifest + */ + private assetsFromAssetManifest(artifact: ArtifactManifest): StackAsset[] { + const assets: (FileManifestEntry | DockerImageManifestEntry)[] = []; + const fileName = (artifact.properties as AssetManifestProperties).file; + const assetManifest = AssetManifest.fromFile(path.join(this.directory, fileName)); + assetManifest.entries.forEach((entry) => { + if (entry.type === 'file') { + const source = (entry as FileManifestEntry).source; + // This will ignore template assets + if (source.path && source.path.startsWith('asset.')) { + assets.push(entry as FileManifestEntry); + } + } else if (entry.type === 'docker-image') { + const source = (entry as DockerImageManifestEntry).source; + if (source.directory && source.directory.startsWith('asset.')) { + assets.push(entry as DockerImageManifestEntry); + } + } else { + warn(`found unexpected asset type: ${entry.type}`); + } + }); + return assets; + } +} diff --git a/src/assembly/stack.ts b/src/assembly/stack.ts new file mode 100644 index 00000000..8c4abffd --- /dev/null +++ b/src/assembly/stack.ts @@ -0,0 +1,162 @@ +import * as path from 'path'; +import { DestinationIdentifier, FileManifestEntry } from 'cdk-assets'; +import { CloudFormationParameter, CloudFormationResource, CloudFormationTemplate } from '../cfn'; +import { ConstructTree, StackAsset, StackMetadata } from './types'; +import { FileAssetPackaging, FileDestination } from 'aws-cdk-lib/cloud-assembly-schema'; + +/** + * FileAssetManifest represents a CDK File asset. + * It is a helper class that is used to better represent a file asset + * in a way that this library requires + */ +export class FileAssetManifest { + /** + * The destination of the file asset (i.e. where the file needs to be published) + */ + public readonly destination: FileDestination; + + /** + * The destination id + */ + public readonly id: DestinationIdentifier; + + /** + * Absolute path to the asset + */ + public readonly path: string; + public readonly packaging: FileAssetPackaging; + + /** + * @param directory - The directory in which the file manifest is found + * @param asset - The file asset + */ + constructor(directory: string, asset: FileManifestEntry) { + this.destination = asset.destination; + this.id = asset.id; + if (asset.source.executable) { + throw new Error(`file assets produced by commands are not yet supported`); + } + this.path = path.join(directory, asset.source.path!); + this.packaging = asset.source.packaging ?? FileAssetPackaging.FILE; + } +} + +/** + * StackManifest represents a single Stack that needs to be converted + * It contains all the necessary information for this library to fully convert + * the resources and assets in the stack to pulumi resources + */ +export class StackManifest { + /** + * The artifactId / stackId of the stack + */ + public id: string; + + /** + * The construct tree for the stack + */ + public readonly constructTree: ConstructTree; + + /** + * The relative path to the stack template file + */ + public readonly templatePath: string; + + /** + * The Outputs from the CFN Stack + */ + public readonly outputs?: { [id: string]: any }; + + /** + * The Parameters from the CFN Stack + */ + public readonly parameters?: { [id: string]: CloudFormationParameter }; + + /** + * Map of resource logicalId to CloudFormation template resource fragment + */ + private readonly resources: { [logicalId: string]: CloudFormationResource }; + + /** + * + */ + private readonly metadata: StackMetadata; + private readonly assets: StackAsset[]; + private readonly directory: string; + constructor( + directory: string, + id: string, + templatePath: string, + metadata: StackMetadata, + tree: ConstructTree, + template: CloudFormationTemplate, + assets: StackAsset[], + ) { + this.directory = directory; + this.assets = assets; + this.outputs = template.Outputs; + this.parameters = template.Parameters; + this.metadata = metadata; + this.templatePath = templatePath; + this.id = id; + this.constructTree = tree; + if (!template.Resources) { + throw new Error('CloudFormation template has no resources!'); + } + this.resources = template.Resources; + } + + public get fileAssets(): FileAssetManifest[] { + return this.assets + .filter((asset) => asset.type === 'file') + .flatMap((asset) => new FileAssetManifest(this.directory, asset)); + } + + // TODO: implement docker assets + // public get dockerAssets(): DockerAssetManifest[] { + // + // } + + /** + * Get the CloudFormation logicalId for the CFN resource at the given Construct path + * + * @param path - The construct path + * @returns the logicalId of the resource + * @throws error if the construct path does not relate to a CFN resource with a logicalId + */ + public logicalIdForPath(path: string): string { + if (path in this.metadata) { + return this.metadata[path]; + } + throw new Error(`Could not find logicalId for path ${path}`); + } + + /** + * Get the CloudFormation template fragment of the resource with the given + * logicalId + * + * @param logicalId - The CFN LogicalId of the resource + * @returns The resource portion of the CFN template + */ + public resourceWithLogicalId(logicalId: string): CloudFormationResource { + if (logicalId in this.resources) { + return this.resources[logicalId]; + } + throw new Error(`Could not find resource with logicalId '${logicalId}'`); + } + + /** + * Get the CloudFormation template fragment of the resource with the given + * CDK construct path + * + * @param path - The construct path to find the CFN Resource for + * @returns The resource portion of the CFN template + */ + public resourceWithPath(path: string): CloudFormationResource { + const logicalId = this.logicalIdForPath(path); + if (logicalId && logicalId in this.resources) { + return this.resources[logicalId]; + } + throw new Error(`Could not find resource with logicalId '${logicalId}'`); + } +} diff --git a/src/assembly/types.ts b/src/assembly/types.ts new file mode 100644 index 00000000..f930422f --- /dev/null +++ b/src/assembly/types.ts @@ -0,0 +1,15 @@ +import { ConstructInfo } from 'aws-cdk-lib/core/lib/private/runtime-info'; +import { Node } from 'aws-cdk-lib/core/lib/private/tree-metadata'; +import { DockerImageManifestEntry, FileManifestEntry } from 'cdk-assets'; + +export type StackAsset = FileManifestEntry | DockerImageManifestEntry; + +/** + * Map of CDK construct path to logicalId + */ +export type StackMetadata = { [path: string]: string }; + +/** + * ConstructTree is a tree of the current CDK construct + */ +export type ConstructTree = Node; diff --git a/src/aws-resource-mappings.ts b/src/aws-resource-mappings.ts index 7af6c7dd..0a89b0b5 100644 --- a/src/aws-resource-mappings.ts +++ b/src/aws-resource-mappings.ts @@ -14,7 +14,6 @@ import * as pulumi from '@pulumi/pulumi'; import * as aws from '@pulumi/aws'; -import { CfnElement } from 'aws-cdk-lib'; import { ResourceMapping, normalize } from './interop'; function maybe(v: T | undefined, fn: (t: T) => U): U | undefined { @@ -43,7 +42,6 @@ function tags(tags: pulumi.Input[]> | undefined): AwsTags * Any resource that does not currently exist in CCAPI can be mapped to an aws classic resource. */ export function mapToAwsResource( - element: CfnElement, logicalId: string, typeName: string, rawProps: any, diff --git a/src/cfn-resource-mappings.ts b/src/cfn-resource-mappings.ts index adf1ec09..09aa207e 100644 --- a/src/cfn-resource-mappings.ts +++ b/src/cfn-resource-mappings.ts @@ -14,13 +14,13 @@ import * as pulumi from '@pulumi/pulumi'; import { s3 } from '@pulumi/aws-native'; -import { CfnElement, Token, Reference, Tokenization } from 'aws-cdk-lib'; import { CfnResource, ResourceMapping, normalize } from './interop'; import { debug } from '@pulumi/pulumi/log'; import { toSdkName } from './naming'; +import { Metadata } from './pulumi-metadata'; +import { PulumiProvider } from './types'; export function mapToCfnResource( - element: CfnElement, logicalId: string, typeName: string, rawProps: any, @@ -34,20 +34,12 @@ export function mapToCfnResource( // lowercase letters. return new s3.Bucket(logicalId.toLowerCase(), props, options); default: { - // Scrape the attributes off of the construct. - // - // NOTE: this relies on CfnReference setting the reference's display name to the literal attribute name. - const attributes = Object.values(element) - .filter(Token.isUnresolved) - .flatMap((v) => { - if (typeof v === 'string') { - return Tokenization.reverseString(v).tokens; - } - return [Tokenization.reverse(v)]; - }) - .filter(Reference.isReference) - .filter((ref) => ref.target === element) - .map((ref) => attributePropertyName(ref.displayName)); + // When creating a generic `CfnResource` we don't have any information on the + // attributes attached to the resource. We need to populate them by looking up the + // `output` in the metadata + const metadata = new Metadata(PulumiProvider.AWS_NATIVE); + const resource = metadata.findResource(typeName); + const attributes = Object.keys(resource.outputs); return new CfnResource(logicalId, typeName, props, attributes, options); } diff --git a/src/converters/app-converter.ts b/src/converters/app-converter.ts new file mode 100644 index 00000000..f4b5a36c --- /dev/null +++ b/src/converters/app-converter.ts @@ -0,0 +1,396 @@ +import * as pulumi from '@pulumi/pulumi'; +import { AssemblyManifestReader, StackManifest } from '../assembly'; +import { ConstructInfo, GraphBuilder } from '../graph'; +import { StackComponentResource, lift, Mapping } from '../types'; +import { ArtifactConverter, FileAssetManifestConverter } from './artifact-converter'; +import { CdkConstruct, ResourceMapping } from '../interop'; +import { debug } from '@pulumi/pulumi/log'; +import { + cidr, + getAccountId, + getAzs, + getRegion, + getSsmParameterList, + getSsmParameterString, + getUrlSuffix, +} from '@pulumi/aws-native'; +import { mapToAwsResource } from '../aws-resource-mappings'; +import { attributePropertyName, mapToCfnResource } from '../cfn-resource-mappings'; +import { CloudFormationResource, getDependsOn } from '../cfn'; +import { OutputMap, OutputRepr } from '../output-map'; +import { parseSub } from '../sub'; +import { getPartition } from '@pulumi/aws-native/getPartition'; + +/** + * AppConverter will convert all CDK resources into Pulumi resources. + */ +export class AppConverter { + // Map of stack artifactId to StackConverter + public readonly stacks = new Map(); + + public readonly manifestReader: AssemblyManifestReader; + + constructor(readonly host: StackComponentResource) { + this.manifestReader = AssemblyManifestReader.fromDirectory(host.assemblyDir); + } + + convert() { + for (const stackManifest of this.manifestReader.stackManifests) { + const stackConverter = new StackConverter(this.host, stackManifest); + this.stacks.set(stackManifest.id, stackConverter); + this.convertStackManifest(stackManifest); + } + } + + private convertStackManifest(artifact: StackManifest): void { + const dependencies = new Set(); + for (const file of artifact.fileAssets) { + const converter = new FileAssetManifestConverter(this.host, file); + converter.convert(); + dependencies.add(converter); + } + + // TODO add docker asset converter + // for (const image of artifact.dockerAssets) { + // } + + const stackConverter = this.stacks.get(artifact.id); + if (!stackConverter) { + throw new Error(`missing CDK Stack for artifact ${artifact.id}`); + } + stackConverter.convert(dependencies); + } +} + +/** + * StackConverter converts all of the resources in a CDK stack to Pulumi resources + */ +export class StackConverter extends ArtifactConverter { + readonly parameters = new Map(); + readonly resources = new Map>(); + readonly constructs = new Map(); + + constructor(host: StackComponentResource, readonly stack: StackManifest) { + super(host); + } + + public convert(dependencies: Set) { + const dependencyGraphNodes = GraphBuilder.build(this.stack); + + // process parameters first because resources will reference them + for (const [logicalId, value] of Object.entries(this.stack.parameters ?? {})) { + this.mapParameter(logicalId, value.Type, value.Default); + } + + for (const n of dependencyGraphNodes) { + if (n.construct.id === this.stack.id) { + const stackResource = new CdkConstruct( + `${this.stackComponent.name}/${n.construct.path}`, + n.construct.id, + { + parent: this.stackComponent, + // NOTE: Currently we make the stack depend on all the assets and then all resources + // have the parent as the stack. This means we deploy all assets before we deploy any resources + // we might be able better and have individual resources depend on individual assets, but CDK + // doesn't track asset dependencies at that level + dependsOn: this.stackDependsOn(dependencies), + }, + ); + this.constructs.set(n.construct, stackResource); + continue; + } + + if (!n.construct.parent || !this.constructs.has(n.construct.parent)) { + throw new Error(`Construct at path ${n.construct.path} should be created in the scope of a Stack`); + } + const parent = this.constructs.get(n.construct.parent)!; + if (n.resource && n.logicalId) { + const cfn = n.resource; + debug(`Processing node with template: ${JSON.stringify(cfn)}`); + debug(`Creating resource for ${n.logicalId}`); + const props = this.processIntrinsics(cfn.Properties); + const options = this.processOptions(cfn, parent); + + const mapped = this.mapResource(n.logicalId, cfn.Type, props, options); + const resource = pulumi.Resource.isInstance(mapped) ? mapped : mapped.resource; + const attributes = pulumi.Resource.isInstance(mapped) ? undefined : mapped.attributes; + this.resources.set(n.logicalId, { resource, attributes, resourceType: cfn.Type }); + this.constructs.set(n.construct, resource); + + debug(`Done creating resource for ${n.logicalId}`); + // TODO: process template conditions + // for (const [conditionId, condition] of Object.entries(cfn.Conditions || {})) { + // // Do something with the condition + // } + } else { + const r = new CdkConstruct(`${this.stackComponent.name}/${n.construct.path}`, n.construct.type, { + parent, + }); + this.constructs.set(n.construct, r); + } + } + + // Register the outputs as outputs of the component resource. + for (const [outputId, args] of Object.entries(this.stack.outputs ?? {})) { + this.stackComponent.registerOutput(outputId, this.processIntrinsics(args.Value)); + } + + for (let i = dependencyGraphNodes.length - 1; i >= 0; i--) { + const n = dependencyGraphNodes[i]; + if (!n.resource) { + (this.constructs.get(n.construct)!).done(); + } + } + } + + private stackDependsOn(dependencies: Set): pulumi.Resource[] { + const dependsOn: pulumi.Resource[] = []; + for (const d of dependencies) { + if (d instanceof FileAssetManifestConverter) { + this.resources.set(d.id, { resource: d.file, resourceType: d.resourceType }); + dependsOn.push(d.file); + } + // TODO: handle docker images + } + return dependsOn; + } + + private mapParameter(logicalId: string, typeName: string, defaultValue: any | undefined) { + // TODO: support arbitrary parameters? + + if (!typeName.startsWith('AWS::SSM::Parameter::')) { + throw new Error(`unsupported parameter ${logicalId} of type ${typeName}`); + } + if (defaultValue === undefined) { + throw new Error(`unsupported parameter ${logicalId} with no default value`); + } + + function parameterValue(parent: pulumi.Resource): any { + const key = defaultValue; + const paramType = typeName.slice('AWS::SSM::Parameter::'.length); + if (paramType.startsWith('Value<')) { + const type = paramType.slice('Value<'.length); + if (type.startsWith('List<') || type === 'CommaDelimitedList>') { + return getSsmParameterList({ name: key }, { parent }).then((v) => v.value); + } + return getSsmParameterString({ name: key }, { parent }).then((v) => v.value); + } + return key; + } + + this.parameters.set(logicalId, parameterValue(this.stackComponent)); + } + + private mapResource( + logicalId: string, + typeName: string, + props: any, + options: pulumi.ResourceOptions, + ): ResourceMapping { + if (this.stackComponent.options?.remapCloudControlResource !== undefined) { + const res = this.stackComponent.options.remapCloudControlResource(logicalId, typeName, props, options); + if (res !== undefined) { + debug(`remapped ${logicalId}`); + return res; + } + } + + const awsMapping = mapToAwsResource(logicalId, typeName, props, options); + if (awsMapping !== undefined) { + debug(`mapped ${logicalId} to classic AWS resource(s)`); + return awsMapping; + } + + const cfnMapping = mapToCfnResource(logicalId, typeName, props, options); + debug(`mapped ${logicalId} to native AWS resource(s)`); + return cfnMapping; + } + + private processOptions(resource: CloudFormationResource, parent: pulumi.Resource): pulumi.ResourceOptions { + const dependsOn = getDependsOn(resource); + return { + parent: parent, + dependsOn: dependsOn !== undefined ? dependsOn.map((id) => this.resources.get(id)!.resource) : undefined, + }; + } + + /** @internal */ + asOutputValue(v: T): T { + const value = this.stackComponent.stack.resolve(v); + return this.processIntrinsics(value) as T; + } + + private processIntrinsics(obj: any): any { + try { + debug(`Processing intrinsics for ${JSON.stringify(obj)}`); + } catch { + // just don't log + } + if (typeof obj === 'string') { + return obj; + } + + if (typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.filter((x) => !this.isNoValue(x)).map((x) => this.processIntrinsics(x)); + } + + const ref = obj.Ref; + if (ref) { + return this.resolveRef(ref); + } + + const keys = Object.keys(obj); + if (keys.length == 1 && keys[0]?.startsWith('Fn::')) { + return this.resolveIntrinsic(keys[0], obj[keys[0]]); + } + + return Object.entries(obj) + .filter(([_, v]) => !this.isNoValue(v)) + .reduce((result, [k, v]) => ({ ...result, [k]: this.processIntrinsics(v) }), {}); + } + + private isNoValue(obj: any): boolean { + return obj?.Ref === 'AWS::NoValue'; + } + + private resolveOutput(repr: OutputRepr): pulumi.Output { + return OutputMap.instance().lookupOutput(repr)!; + } + + private resolveIntrinsic(fn: string, params: any) { + switch (fn) { + case 'Fn::GetAtt': { + debug(`Fn::GetAtt(${params[0]}, ${params[1]})`); + return this.resolveAtt(params[0], params[1]); + } + + case 'Fn::Join': + return lift(([delim, strings]) => strings.join(delim), this.processIntrinsics(params)); + + case 'Fn::Select': + return lift(([index, list]) => list[index], this.processIntrinsics(params)); + + case 'Fn::Split': + return lift(([delim, str]) => str.split(delim), this.processIntrinsics(params)); + + case 'Fn::Base64': + return lift((str) => Buffer.from(str).toString('base64'), this.processIntrinsics(params)); + + case 'Fn::Cidr': + return lift( + ([ipBlock, count, cidrBits]) => + cidr({ + ipBlock, + count, + cidrBits, + }).then((r) => r.subnets), + this.processIntrinsics(params), + ); + + case 'Fn::GetAZs': + return lift(([region]) => getAzs({ region }).then((r) => r.azs), this.processIntrinsics(params)); + + case 'Fn::Sub': + return lift((params) => { + const [template, vars] = + typeof params === 'string' ? [params, undefined] : [params[0] as string, params[1]]; + + const parts: string[] = []; + for (const part of parseSub(template)) { + parts.push(part.str); + + if (part.ref !== undefined) { + if (part.ref.attr !== undefined) { + parts.push(this.resolveAtt(part.ref.id, part.ref.attr!)); + } else { + parts.push(this.resolveRef(part.ref.id)); + } + } + } + + return lift((parts) => parts.map((v: any) => v.toString()).join(''), parts); + }, this.processIntrinsics(params)); + + case 'Fn::Transform': { + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-macros.html + throw new Error('Fn::Transform is not supported – Cfn Template Macros are not supported yet'); + } + + case 'Fn::ImportValue': { + // TODO: support cross cfn stack references? + // This is related to the Export Name from outputs https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html + // We might revisit this once the CDKTF supports cross stack references + throw new Error(`Fn::ImportValue is not yet supported.`); + } + + default: + throw new Error(`unsupported intrinsic function ${fn} (params: ${JSON.stringify(params)})`); + } + } + + private resolveRef(target: any): any { + if (typeof target !== 'string') { + return this.resolveOutput(target); + } + + switch (target) { + case 'AWS::AccountId': + return getAccountId({ parent: this.stackComponent }).then((r) => r.accountId); + case 'AWS::NoValue': + return undefined; + case 'AWS::Partition': + return getPartition({ parent: this.stackComponent }).then((p) => p.partition); + case 'AWS::Region': + return getRegion({ parent: this.stackComponent }).then((r) => r.region); + case 'AWS::URLSuffix': + return getUrlSuffix({ parent: this.stackComponent }).then((r) => r.urlSuffix); + case 'AWS::NotificationARNs': + case 'AWS::StackId': + case 'AWS::StackName': + // Can't support these + throw new Error(`reference to unsupported pseudo parameter ${target}`); + } + + const mapping = this.lookup(target); + if ((mapping).value !== undefined) { + return (mapping).value; + } + return ((>mapping).resource).id; + } + + private lookup(logicalId: string): Mapping | { value: any } { + const targetParameter = this.parameters.get(logicalId); + if (targetParameter !== undefined) { + return { value: targetParameter }; + } + const targetMapping = this.resources.get(logicalId); + if (targetMapping !== undefined) { + return targetMapping; + } + throw new Error(`missing reference for ${logicalId}`); + } + + private resolveAtt(logicalId: string, attribute: string) { + const mapping = >this.lookup(logicalId); + + debug( + `Resource: ${logicalId} - resourceType: ${mapping.resourceType} - ${Object.getOwnPropertyNames( + mapping.resource, + )}`, + ); + + // If this resource has explicit attribute mappings, those mappings will use PascalCase, not camelCase. + const propertyName = mapping.attributes !== undefined ? attribute : attributePropertyName(attribute); + + const descs = Object.getOwnPropertyDescriptors(mapping.attributes || mapping.resource); + const d = descs[propertyName]; + if (!d) { + throw new Error(`No property ${propertyName} for attribute ${attribute} on resource ${logicalId}`); + } + return d.value; + } +} diff --git a/src/converters/artifact-converter.ts b/src/converters/artifact-converter.ts new file mode 100644 index 00000000..224b5a7c --- /dev/null +++ b/src/converters/artifact-converter.ts @@ -0,0 +1,91 @@ +import * as aws from '@pulumi/aws'; +import * as cx from 'aws-cdk-lib/cx-api'; +import { getAccountId, getPartition, getRegion } from '@pulumi/aws-native'; +import { FileAssetManifest } from '../assembly'; +import { FileAssetPackaging } from 'aws-cdk-lib/cloud-assembly-schema'; +import { zipDirectory } from '../zip'; +import { StackComponentResource } from '../types'; + +/** + * ArtifactConverter + */ +export abstract class ArtifactConverter { + constructor(protected readonly stackComponent: StackComponentResource) {} + + /** + * Takes a string and resolves any CDK environment placeholders (e.g. accountId, region, partition) + * + * @param s - The string that contains the placeholders to replace + * @returns The string with the placeholders fully resolved + */ + protected resolvePlaceholders(s: string): Promise { + const host = this.stackComponent; + return cx.EnvironmentPlaceholders.replaceAsync(s, { + async region(): Promise { + return getRegion({ parent: host }).then((r) => r.region); + }, + + async accountId(): Promise { + return getAccountId({ parent: host }).then((r) => r.accountId); + }, + + async partition(): Promise { + return getPartition({ parent: host }).then((p) => p.partition); + }, + }); + } +} + +/** + * FileAssetManifestConverter handles converting CDK assets into Pulumi resources + */ +export class FileAssetManifestConverter extends ArtifactConverter { + private _file?: aws.s3.BucketObjectv2; + public _id?: string; + public resourceType: string = 'aws:s3:BucketObjectv2'; + + constructor(host: StackComponentResource, readonly manifest: FileAssetManifest) { + super(host); + } + + public get id(): string { + if (!this._id) { + throw new Error('must call convert before accessing file'); + } + return this._id; + } + + /** + * @returns the underlying bucket object pulumi resource + */ + public get file(): aws.s3.BucketObjectv2 { + if (!this._file) { + throw new Error('must call convert before accessing file'); + } + return this._file; + } + + /** + * Converts a CDK file asset into a Pulumi aws.s3.BucketObjectv2 resource + */ + public convert(): void { + const name = this.manifest.id.assetId; + const id = this.manifest.id.destinationId; + this._id = `${this.stackComponent.name}/${name}/${id}`; + + const outputPath = + this.manifest.packaging === FileAssetPackaging.FILE + ? Promise.resolve(this.manifest.path) + : zipDirectory(this.manifest.path, this.manifest.path + '.zip'); + + this._file = new aws.s3.BucketObjectv2( + this._id, + { + source: outputPath, + bucket: this.resolvePlaceholders(this.manifest.destination.bucketName), + key: this.resolvePlaceholders(this.manifest.destination.objectKey), + }, + { parent: this.stackComponent }, + ); + } +} diff --git a/src/graph.ts b/src/graph.ts index 5b5728b5..31b2a5e8 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -11,35 +11,148 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - import { debug } from '@pulumi/pulumi/log'; -import { Stack, CfnElement, Token } from 'aws-cdk-lib'; -import { Construct, ConstructOrder } from 'constructs'; -import { CloudFormationTemplate } from './cfn'; +import { CloudFormationResource } from './cfn'; import { parseSub } from './sub'; +import { ConstructTree, StackManifest } from './assembly'; + +/** + * Represents a CDK Construct + */ +export interface ConstructInfo { + /** + * The construct path + */ + path: string; + + /** + * The node id of the construct + */ + id: string; + + /** + * The CloudFormation resource type + * + * This will only be set if this is the construct for cfn resource + */ + type?: string; + + /** + * The attributes of the construct + */ + attributes?: { [key: string]: any }; + + /** + * The parent construct (i.e. scope) + * Will be undefined for the construct representing the `Stack` + */ + parent?: ConstructInfo; +} export interface GraphNode { incomingEdges: Set; outgoingEdges: Set; - construct: Construct; - template?: CloudFormationTemplate; + /** + * The CFN LogicalID. + * + * This will only be set if this node represents a CloudFormation resource. + * It will not be set for wrapper constructs + */ + logicalId?: string; + + /** + * The info on the Construct this node represents + */ + construct: ConstructInfo; + + /** + * The CloudFormation resource data for the resource represented by this node. + * This will only be set if this node represents a cfn resource (not a wrapper construct) + */ + resource?: CloudFormationResource; +} + +/** + * Get the 'type' from the CFN Type + * `AWS::S3::Bucket` => `Bucket` + * + * @param cfnType - The CloudFormation type (i.e. AWS::S3::Bucket) + * @returns The resource type (i.e. Bucket) + */ +function typeFromCfn(cfnType: string): string { + const typeParts = cfnType.split('::'); + if (typeParts.length !== 3) { + throw new Error(`Expected cfn type in format 'AWS::Service::Resource', got ${cfnType}`); + } + return typeParts[2]; +} + +function typeFromFqn(fqn: string): string { + const fqnParts = fqn.split('.'); + return fqnParts[fqnParts.length - 1]; } export class GraphBuilder { - constructNodes: Map; + // Allows for easy access to the GraphNode of a specific Construct + constructNodes: Map; + // Map of resource logicalId to GraphNode. Allows for easy lookup by logicalId cfnElementNodes: Map; - constructor(private readonly stack: Stack) { - this.constructNodes = new Map(); + constructor(private readonly stack: StackManifest) { + this.constructNodes = new Map(); this.cfnElementNodes = new Map(); } // build constructs a dependency graph from the adapter and returns its nodes sorted in topological order. - public static build(stack: Stack): GraphNode[] { + public static build(stack: StackManifest): GraphNode[] { const b = new GraphBuilder(stack); return b._build(); } + /** + * Recursively parses the construct tree to create: + * - constructNodes + * - cfnElementNodes + * + * @param tree - The construct tree of the current construct being parsed + * @param parent - The parent construct of the construct currently being parsed + */ + private parseTree(tree: ConstructTree, parent?: ConstructInfo) { + const construct: ConstructInfo = { + parent, + id: tree.id, + path: tree.path, + type: tree.constructInfo ? typeFromFqn(tree.constructInfo.fqn) : tree.id, + attributes: tree.attributes, + }; + const node: GraphNode = { + incomingEdges: new Set(), + outgoingEdges: new Set(), + construct, + }; + if (tree.attributes && 'aws:cdk:cloudformation:type' in tree.attributes) { + const cfnType = tree.attributes['aws:cdk:cloudformation:type'] as string; + const logicalId = this.stack.logicalIdForPath(tree.path); + const resource = this.stack.resourceWithLogicalId(logicalId); + const typ = typeFromCfn(cfnType); + node.construct.type = typ; + construct.type = typ; + if (resource.Type === cfnType) { + node.resource = resource; + node.logicalId = logicalId; + this.cfnElementNodes.set(logicalId, node); + } else { + throw new Error( + `Something went wrong: resourceType ${resource.Type} does not equal CfnType ${cfnType}`, + ); + } + } + this.constructNodes.set(construct, node); + if (tree.children) { + Object.values(tree.children).forEach((child) => this.parseTree(child, construct)); + } + } + private _build(): GraphNode[] { // passes // 1. collect all constructs into a map from construct name to DAG node, converting CFN elements to fragments @@ -49,41 +162,28 @@ export class GraphBuilder { // Create graph nodes and associate them with constructs and CFN logical IDs. // // NOTE: this doesn't handle cross-stack references. We'll likely need to do so, at least for nested stacks. - for (const construct of this.stack.node.findAll(ConstructOrder.POSTORDER)) { - const template = CfnElement.isCfnElement(construct) - ? (this.stack.resolve((construct as any)._toCloudFormation()) as CloudFormationTemplate) - : undefined; - - const node = { - incomingEdges: new Set(), - outgoingEdges: new Set(), - construct, - template, - }; - - this.constructNodes.set(construct, node); - if (CfnElement.isCfnElement(construct)) { - const logicalId = this.stack.resolve(construct.logicalId); - debug(`adding node for ${logicalId}`); - this.cfnElementNodes.set(logicalId, node); - - for (const [logicalId, r] of Object.entries(template!.Resources || {})) { - debug(`adding node for ${logicalId}`); - this.cfnElementNodes.set(logicalId, node); - } - } - } + this.parseTree(this.stack.constructTree); - // Add dependency edges. for (const [construct, node] of this.constructNodes) { - if (construct.node.scope !== undefined && !Stack.isStack(construct)) { - const parentNode = this.constructNodes.get(construct.node.scope)!; + // No parent means this is the construct that represents the `Stack` + if (construct.parent !== undefined) { + const parentNode = this.constructNodes.get(construct.parent)!; node.outgoingEdges.add(parentNode); parentNode.incomingEdges.add(node); } - if (node.template !== undefined) { - this.addEdgesForTemplate(node.template); + // Then this is the construct representing the CFN resource (i.e. not a wrapper construct) + if (node.resource && node.logicalId) { + const source = this.cfnElementNodes.get(node.logicalId!)!; + this.addEdgesForCfnResource(node.resource, source); + + const dependsOn = + typeof node.resource.DependsOn === 'string' ? [node.resource.DependsOn] : node.resource.DependsOn; + if (dependsOn !== undefined) { + for (const target of dependsOn) { + this.addEdgeForRef(target, source); + } + } } } @@ -97,7 +197,7 @@ export class GraphBuilder { visited.add(node); // If this is a non-CFN construct with no incoming edges, ignore it. - if (!CfnElement.isCfnElement(node.construct) && node.incomingEdges.size == 0) { + if (!node.resource && node.incomingEdges.size == 0) { return; } @@ -114,27 +214,11 @@ export class GraphBuilder { return sorted; } - private addEdgesForTemplate(template: CloudFormationTemplate) { - for (const [logicalId, value] of Object.entries(template.Resources || {})) { - const source = this.cfnElementNodes.get(logicalId)!; - this.addEdgesForFragment(value, source); - - const dependsOn = typeof value.DependsOn === 'string' ? [value.DependsOn] : value.DependsOn; - if (dependsOn !== undefined) { - for (const target of dependsOn) { - this.addEdgeForRef(target, source); - } - } - } - } - - private addEdgesForFragment(obj: any, source: GraphNode): void { + private addEdgesForCfnResource(obj: any, source: GraphNode): void { + // Since we are processing the final CloudFormation template, strings will always + // be the fully resolved value if (typeof obj === 'string') { - if (!Token.isUnresolved(obj)) { - return; - } - console.warn(`unresolved token ${obj}`); - obj = this.stack.resolve(obj); + return; } if (typeof obj !== 'object') { @@ -142,7 +226,7 @@ export class GraphBuilder { } if (Array.isArray(obj)) { - obj.map((x) => this.addEdgesForFragment(x, source)); + obj.map((x) => this.addEdgesForCfnResource(x, source)); return; } @@ -159,7 +243,7 @@ export class GraphBuilder { } for (const v of Object.values(obj)) { - this.addEdgesForFragment(v, source); + this.addEdgesForCfnResource(v, source); } } @@ -192,7 +276,7 @@ export class GraphBuilder { const [template, vars] = typeof params === 'string' ? [params, undefined] : [params[0] as string, params[1]]; - this.addEdgesForFragment(vars, source); + this.addEdgesForCfnResource(vars, source); for (const part of parseSub(template).filter((p) => p.ref !== undefined)) { this.addEdgeForRef(part.ref!.id, source); @@ -200,7 +284,7 @@ export class GraphBuilder { } break; default: - this.addEdgesForFragment(params, source); + this.addEdgesForCfnResource(params, source); break; } } diff --git a/src/index.ts b/src/index.ts index e5670ec1..2fa2557c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,4 +17,4 @@ export * from './output'; import * as interop from './interop'; export { interop }; -export * from './types'; +export { StackOptions } from './types'; diff --git a/src/interop.ts b/src/interop.ts index aee332c5..b2158969 100644 --- a/src/interop.ts +++ b/src/interop.ts @@ -18,6 +18,7 @@ import { IConstruct } from 'constructs'; import { normalizeObject } from './pulumi-metadata'; import { toSdkName, typeToken } from './naming'; import { PulumiProvider } from './types'; +import { ConstructInfo } from './graph'; export function firstToLower(str: string) { return str.replace(/\w\S*/g, function (txt) { @@ -96,9 +97,9 @@ export function getFqn(construct: IConstruct): string | undefined { } export class CdkConstruct extends pulumi.ComponentResource { - constructor(name: string | undefined, construct: IConstruct, options?: pulumi.ComponentResourceOptions) { - const constructType = construct.constructor.name || 'Construct'; - const constructName = name || construct.node.path; + constructor(name: string, type?: string, options?: pulumi.ComponentResourceOptions) { + const constructType = type ?? 'Construct'; + const constructName = name; super(`cdk:construct:${constructType}`, constructName, {}, options); } diff --git a/src/output-map.ts b/src/output-map.ts index efc8cc62..575c009e 100644 --- a/src/output-map.ts +++ b/src/output-map.ts @@ -11,8 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -import { Token, IResolvable } from 'aws-cdk-lib'; import * as pulumi from '@pulumi/pulumi'; const glob = global as any; diff --git a/src/pulumi-metadata.ts b/src/pulumi-metadata.ts index 1753809a..018f85c6 100644 --- a/src/pulumi-metadata.ts +++ b/src/pulumi-metadata.ts @@ -97,6 +97,7 @@ export interface PulumiProperty extends PulumiPropertyItems { export interface PulumiResource { inputs: { [key: string]: PulumiProperty }; + outputs: { [key: string]: PulumiProperty }; } /** diff --git a/src/stack.ts b/src/stack.ts index fda9d226..b919748e 100644 --- a/src/stack.ts +++ b/src/stack.ts @@ -11,87 +11,38 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -import * as path from 'path'; import * as cdk from 'aws-cdk-lib'; import * as cx from 'aws-cdk-lib/cx-api'; -import * as cloud_assembly from 'aws-cdk-lib/cloud-assembly-schema'; import * as pulumi from '@pulumi/pulumi'; -import * as aws from '@pulumi/aws'; -import * as docker from '@pulumi/docker'; -import { - cidr, - getAccountId, - getPartition, - getAzs, - getRegion, - getSsmParameterList, - getSsmParameterString, - getUrlSuffix, -} from '@pulumi/aws-native'; import { debug } from '@pulumi/pulumi/log'; -import { CfnElement, Token } from 'aws-cdk-lib'; -import { IConstruct } from 'constructs'; -import { mapToAwsResource } from './aws-resource-mappings'; -import { CloudFormationResource, getDependsOn } from './cfn'; -import { attributePropertyName, mapToCfnResource } from './cfn-resource-mappings'; -import { GraphBuilder } from './graph'; -import { CdkConstruct, JSII_RUNTIME_SYMBOL, ResourceMapping, getFqn } from './interop'; -import { OutputRepr, OutputMap } from './output-map'; -import { parseSub } from './sub'; -import { zipDirectory } from './zip'; - -/** - * Options specific to the Stack component. - */ -export interface StackOptions extends pulumi.ComponentResourceOptions { - /** - * Specify the CDK Stack properties to asociate with the stack. - */ - props?: cdk.StackProps; - - /** - * Defines a mapping to override and/or provide an implementation for a CloudFormation resource - * type that is not (yet) implemented in the AWS Cloud Control API (and thus not yet available in - * the Pulumi AWS Native provider). Pulumi code can override this method to provide a custom mapping - * of CloudFormation elements and their properties into Pulumi CustomResources, commonly by using the - * AWS Classic provider to implement the missing resource. - * - * @param element The full CloudFormation element object being mapped. - * @param logicalId The logical ID of the resource being mapped. - * @param typeName The CloudFormation type name of the resource being mapped. - * @param props The bag of input properties to the CloudFormation resource being mapped. - * @param options The set of Pulumi ResourceOptions to apply to the resource being mapped. - * @returns An object containing one or more logical IDs mapped to Pulumi resources that must be - * created to implement the mapped CloudFormation resource, or else undefined if no mapping is - * implemented. - */ - remapCloudControlResource?( - element: CfnElement, - logicalId: string, - typeName: string, - props: any, - options: pulumi.ResourceOptions, - ): ResourceMapping | undefined; -} +import { StackComponentResource, StackOptions } from './types'; +import { AppConverter, StackConverter } from './converters/app-converter'; -class StackComponent extends pulumi.ComponentResource { +class StackComponent extends pulumi.ComponentResource implements StackComponentResource { /** @internal */ name: string; /** @internal */ converter: AppConverter; - constructor(private readonly stack: Stack) { + /** @internal */ + assemblyDir: string; + + options?: StackOptions; + + constructor(public readonly stack: Stack) { super('cdk:index:Stack', stack.node.id, {}, stack.options); + this.options = stack.options; this.name = stack.node.id; const assembly = stack.app.synth(); + this.assemblyDir = assembly.directory; + debug(`ASSEMBLY_DIR: ${this.assemblyDir}`); debug(JSON.stringify(debugAssembly(assembly))); - this.converter = new AppConverter(this, stack.app, assembly, stack.options || {}); + this.converter = new AppConverter(this); this.converter.convert(); this.registerOutputs(stack.outputs); @@ -210,537 +161,6 @@ export class Stack extends cdk.Stack { } } -type Mapping = { - resource: T; - resourceType: string; - attributes?: { [name: string]: pulumi.Input }; -}; - -class AppConverter { - readonly stacks = new Map(); - readonly stackTemplates = new Set(); - readonly s3Assets = new Map(); - - constructor( - readonly host: StackComponent, - readonly app: cdk.App, - readonly assembly: cx.CloudAssembly, - readonly options: StackOptions, - ) {} - - convert() { - // Build a lookup table for the app's stacks. - for (const construct of this.app.node.findAll()) { - if (cdk.Stack.isStack(construct)) { - const artifact = this.assembly.getStackArtifact(construct.artifactId); - const stack = new StackConverter(this, construct, artifact); - this.stacks.set(construct.artifactId, stack); - this.stackTemplates.add(artifact.templateFullPath); - debug(`${artifact.templateFullPath} is a stack template`); - - for (const asset of stack.findS3Assets()) { - debug(`${path.join(this.assembly.directory, asset.assetPath)} -> ${asset.node.path}`); - this.s3Assets.set(path.join(this.assembly.directory, asset.assetPath), asset); - } - } - } - - // Process stack artifacts in dependency order. - const done = new Map(); - for (const stack of this.assembly.artifacts.filter( - (a) => a.manifest.type === cloud_assembly.ArtifactType.AWS_CLOUDFORMATION_STACK, - )) { - this.convertArtifact(stack, done); - } - } - - private convertArtifact( - artifact: cx.CloudArtifact, - done: Map, - ): ArtifactConverter | undefined { - if (done.has(artifact)) { - return done.get(artifact)!; - } - - const dependencies = new Set(); - for (const d of artifact.dependencies) { - const c = this.convertArtifact(d, done); - if (c !== undefined) { - debug(`${artifact.id} depends on ${d.id}`); - dependencies.add(c); - } - } - - switch (artifact.manifest.type) { - case cloud_assembly.ArtifactType.ASSET_MANIFEST: - return this.convertAssetManifest(artifact as cx.AssetManifestArtifact, dependencies); - case cloud_assembly.ArtifactType.AWS_CLOUDFORMATION_STACK: - return this.convertStack(artifact as cx.CloudFormationStackArtifact, dependencies); - default: - debug(`attempting to convert artifact ${artifact.id} with unsupported type ${artifact.manifest.type}`); - return undefined; - } - } - - private convertStack( - artifact: cx.CloudFormationStackArtifact, - dependencies: Set, - ): StackConverter { - const stack = this.stacks.get(artifact.id); - if (stack === undefined) { - throw new Error(`missing CDK Stack for artifact ${artifact.id}`); - } - stack.convert(dependencies); - return stack; - } - - private convertAssetManifest( - artifact: cx.AssetManifestArtifact, - dependencies: Set, - ): ArtifactConverter { - const converter = new AssetManifestConverter(this, cloud_assembly.Manifest.loadAssetManifest(artifact.file)); - converter.convert(); - return converter; - } -} - -class ArtifactConverter { - constructor(readonly app: AppConverter) {} -} - -class AssetManifestConverter extends ArtifactConverter { - public readonly files = new Map(); - public readonly dockerImages = new Map(); - - constructor(app: AppConverter, readonly manifest: cloud_assembly.AssetManifest) { - super(app); - } - - public convert() { - for (const [id, file] of Object.entries(this.manifest.files || {})) { - this.convertFile(id, file); - } - - for (const [id, image] of Object.entries(this.manifest.dockerImages || {})) { - this.convertDockerImage(id, image); - } - } - - private convertFile(id: string, asset: cloud_assembly.FileAsset) { - if (asset.source.executable !== undefined) { - throw new Error(`file assets produced by commands are not yet supported`); - } - - const inputPath = path.join(this.app.assembly.directory, asset.source.path!); - if (this.app.stackTemplates.has(inputPath)) { - // Ignore stack templates. - return; - } - - const s3Asset = this.app.s3Assets.get(inputPath); - const name = s3Asset?.node.path || id; - - const outputPath = - asset.source.packaging === cloud_assembly.FileAssetPackaging.FILE - ? Promise.resolve(inputPath) - : zipDirectory(inputPath, inputPath + '.zip'); - - const objects = Object.entries(asset.destinations).map( - ([destId, d]) => - new aws.s3.BucketObjectv2( - `${this.app.host.name}/${name}/${destId}`, - { - source: outputPath, - bucket: this.resolvePlaceholders(d.bucketName), - key: this.resolvePlaceholders(d.objectKey), - }, - { parent: this.app.host }, - ), - ); - - this.files.set(id, objects); - } - - private convertDockerImage(id: string, asset: cloud_assembly.DockerImageAsset) { - debug('TODO: convert docker image asset'); - } - - private resolvePlaceholders(s: string): Promise { - const app = this.app; - return cx.EnvironmentPlaceholders.replaceAsync(s, { - async region(): Promise { - return getRegion({ parent: app.host }).then((r) => r.region); - }, - - async accountId(): Promise { - return getAccountId({ parent: app.host }).then((r) => r.accountId); - }, - - async partition(): Promise { - return getPartition({ parent: app.host }).then((p) => p.partition); - }, - }); - } -} - -const s3AssetFqn = (cdk.aws_s3_assets.Asset)[JSII_RUNTIME_SYMBOL]?.fqn; - -function isS3Asset(construct: IConstruct): construct is cdk.aws_s3_assets.Asset { - return s3AssetFqn !== undefined && getFqn(construct) === s3AssetFqn; -} - -class StackConverter extends ArtifactConverter { - readonly parameters = new Map(); - readonly resources = new Map>(); - readonly constructs = new Map(); - stackResource!: CdkConstruct; - - constructor(app: AppConverter, readonly stack: cdk.Stack, readonly artifact: cx.CloudFormationStackArtifact) { - super(app); - } - - public findS3Assets(): cdk.aws_s3_assets.Asset[] { - return [...this.stack.node.findAll().filter(isS3Asset)]; - } - - public convert(dependencies: Set) { - const dependencyGraphNodes = GraphBuilder.build(this.stack); - for (const n of dependencyGraphNodes) { - if (n.construct === this.stack) { - this.stackResource = new CdkConstruct(`${this.app.host.name}/${n.construct.node.path}`, n.construct, { - parent: this.app.host, - dependsOn: this.stackDependsOn(dependencies), - }); - this.constructs.set(n.construct, this.stackResource); - continue; - } - - const parent = this.constructs.get(n.construct.node.scope!)!; - if (CfnElement.isCfnElement(n.construct)) { - const cfn = n.template!; - debug(`Processing node with template: ${JSON.stringify(cfn)}`); - for (const [logicalId, value] of Object.entries(cfn.Parameters || {})) { - this.mapParameter(n.construct, logicalId, value.Type, value.Default); - } - for (const [logicalId, value] of Object.entries(cfn.Resources || {})) { - debug(`Creating resource for ${logicalId}`); - const props = this.processIntrinsics(value.Properties); - const options = this.processOptions(value, parent); - - const mapped = this.mapResource(n.construct, logicalId, value.Type, props, options); - const resource = pulumi.Resource.isInstance(mapped) ? mapped : mapped.resource; - const attributes = pulumi.Resource.isInstance(mapped) ? undefined : mapped.attributes; - this.resources.set(logicalId, { resource, attributes, resourceType: value.Type }); - this.constructs.set(n.construct, resource); - - debug(`Done creating resource for ${logicalId}`); - } - for (const [conditionId, condition] of Object.entries(cfn.Conditions || {})) { - // Do something with the condition - } - // Register the outputs as outputs of the component resource. - for (const [outputId, args] of Object.entries(cfn.Outputs || {})) { - this.app.host.registerOutput(outputId, this.processIntrinsics(args.Value)); - } - } else { - const r = new CdkConstruct(`${this.app.host.name}/${n.construct.node.path}`, n.construct, { - parent, - }); - this.constructs.set(n.construct, r); - } - } - - for (let i = dependencyGraphNodes.length - 1; i >= 0; i--) { - const n = dependencyGraphNodes[i]; - if (!CfnElement.isCfnElement(n.construct)) { - (this.constructs.get(n.construct)!).done(); - } - } - } - - private stackDependsOn(dependencies: Set): pulumi.Resource[] { - const dependsOn: pulumi.Resource[] = []; - for (const d of dependencies) { - if (d instanceof AssetManifestConverter) { - for (const objects of d.files.values()) { - dependsOn.push(...objects); - } - for (const images of d.dockerImages.values()) { - dependsOn.push(...images); - } - } - } - return dependsOn; - } - - private mapParameter(element: CfnElement, logicalId: string, typeName: string, defaultValue: any | undefined) { - // TODO: support arbitrary parameters? - - if (!typeName.startsWith('AWS::SSM::Parameter::')) { - throw new Error(`unsupported parameter ${logicalId} of type ${typeName}`); - } - if (defaultValue === undefined) { - throw new Error(`unsupported parameter ${logicalId} with no default value`); - } - - function parameterValue(parent: pulumi.Resource): any { - const key = defaultValue; - const paramType = typeName.slice('AWS::SSM::Parameter::'.length); - if (paramType.startsWith('Value<')) { - const type = paramType.slice('Value<'.length); - if (type.startsWith('List<') || type === 'CommaDelimitedList>') { - return getSsmParameterList({ name: key }, { parent }).then((v) => v.value); - } - return getSsmParameterString({ name: key }, { parent }).then((v) => v.value); - } - return key; - } - - this.parameters.set(logicalId, parameterValue(this.app.host)); - } - - private mapResource( - element: CfnElement, - logicalId: string, - typeName: string, - props: any, - options: pulumi.ResourceOptions, - ): ResourceMapping { - if (this.app.options.remapCloudControlResource !== undefined) { - const res = this.app.options.remapCloudControlResource(element, logicalId, typeName, props, options); - if (res !== undefined) { - debug(`remapped ${logicalId}`); - return res; - } - } - - const awsMapping = mapToAwsResource(element, logicalId, typeName, props, options); - if (awsMapping !== undefined) { - debug(`mapped ${logicalId} to classic AWS resource(s)`); - return awsMapping; - } - - return mapToCfnResource(element, logicalId, typeName, props, options); - } - - private processOptions(resource: CloudFormationResource, parent: pulumi.Resource): pulumi.ResourceOptions { - const dependsOn = getDependsOn(resource); - return { - parent: parent, - dependsOn: dependsOn !== undefined ? dependsOn.map((id) => this.resources.get(id)!.resource) : undefined, - }; - } - - /** @internal */ - asOutputValue(v: T): T { - return this.processIntrinsics(this.stack.resolve(v)) as T; - } - - private processIntrinsics(obj: any): any { - debug(`Processing intrinsics for ${JSON.stringify(obj)}`); - if (typeof obj === 'string') { - if (Token.isUnresolved(obj)) { - debug(`Unresolved: ${JSON.stringify(obj)}`); - return this.stack.resolve(obj); - } - return obj; - } - - if (typeof obj !== 'object') { - return obj; - } - - if (Array.isArray(obj)) { - return obj.filter((x) => !this.isNoValue(x)).map((x) => this.processIntrinsics(x)); - } - - const ref = obj.Ref; - if (ref) { - return this.resolveRef(ref); - } - - const keys = Object.keys(obj); - if (keys.length == 1 && keys[0]?.startsWith('Fn::')) { - return this.resolveIntrinsic(keys[0], obj[keys[0]]); - } - - return Object.entries(obj) - .filter(([_, v]) => !this.isNoValue(v)) - .reduce((result, [k, v]) => ({ ...result, [k]: this.processIntrinsics(v) }), {}); - } - - private isNoValue(obj: any): boolean { - return obj?.Ref === 'AWS::NoValue'; - } - - private resolveOutput(repr: OutputRepr): pulumi.Output { - return OutputMap.instance().lookupOutput(repr)!; - } - - private resolveIntrinsic(fn: string, params: any) { - switch (fn) { - case 'Fn::GetAtt': { - debug(`Fn::GetAtt(${params[0]}, ${params[1]})`); - return this.resolveAtt(params[0], params[1]); - } - - case 'Fn::Join': - return lift(([delim, strings]) => strings.join(delim), this.processIntrinsics(params)); - - case 'Fn::Select': - return lift(([index, list]) => list[index], this.processIntrinsics(params)); - - case 'Fn::Split': - return lift(([delim, str]) => str.split(delim), this.processIntrinsics(params)); - - case 'Fn::Base64': - return lift(([str]) => Buffer.from(str).toString('base64'), this.processIntrinsics(params)); - - case 'Fn::Cidr': - return lift( - ([ipBlock, count, cidrBits]) => - cidr({ - ipBlock, - count, - cidrBits, - }).then((r) => r.subnets), - this.processIntrinsics(params), - ); - - case 'Fn::GetAZs': - return lift(([region]) => getAzs({ region }).then((r) => r.azs), this.processIntrinsics(params)); - - case 'Fn::Sub': - return lift((params) => { - const [template, vars] = - typeof params === 'string' ? [params, undefined] : [params[0] as string, params[1]]; - - const parts: string[] = []; - for (const part of parseSub(template)) { - parts.push(part.str); - - if (part.ref !== undefined) { - if (part.ref.attr !== undefined) { - parts.push(this.resolveAtt(part.ref.id, part.ref.attr!)); - } else { - parts.push(this.resolveRef(part.ref.id)); - } - } - } - - return lift((parts) => parts.map((v: any) => v.toString()).join(''), parts); - }, this.processIntrinsics(params)); - - case 'Fn::Transform': { - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-macros.html - throw new Error('Fn::Transform is not supported – Cfn Template Macros are not supported yet'); - } - - case 'Fn::ImportValue': { - // TODO: support cross cfn stack references? - // This is related to the Export Name from outputs https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html - // We might revisit this once the CDKTF supports cross stack references - throw new Error(`Fn::ImportValue is not yet supported.`); - } - - default: - throw new Error(`unsupported intrinsic function ${fn} (params: ${JSON.stringify(params)})`); - } - } - - private resolveRef(target: any): any { - if (typeof target !== 'string') { - return this.resolveOutput(target); - } - - switch (target) { - case 'AWS::AccountId': - return getAccountId({ parent: this.app.host }).then((r) => r.accountId); - case 'AWS::NoValue': - return undefined; - case 'AWS::Partition': - // TODO: this is tricky b/c it seems to be context-dependent. From the docs: - // - // Returns the partition that the resource is in. For standard AWS Regions, the partition is aws. - // For resources in other partitions, the partition is aws-partitionname. - // - // For now, just return 'aws'. In the future, we may need to keep track of the type of the resource - // we're walking and then ask the provider via an invoke. - return 'aws'; - case 'AWS::Region': - return getRegion({ parent: this.app.host }).then((r) => r.region); - case 'AWS::URLSuffix': - return getUrlSuffix({ parent: this.app.host }).then((r) => r.urlSuffix); - case 'AWS::NotificationARNs': - case 'AWS::StackId': - case 'AWS::StackName': - // Can't support these - throw new Error(`reference to unsupported pseudo parameter ${target}`); - } - - const mapping = this.lookup(target); - if ((mapping).value !== undefined) { - return (mapping).value; - } - return ((>mapping).resource).id; - } - - private lookup(logicalId: string): Mapping | { value: any } { - const targetParameter = this.parameters.get(logicalId); - if (targetParameter !== undefined) { - return { value: targetParameter }; - } - const targetMapping = this.resources.get(logicalId); - if (targetMapping !== undefined) { - return targetMapping; - } - throw new Error(`missing reference for ${logicalId}`); - } - - private resolveAtt(logicalId: string, attribute: string) { - const mapping = >this.lookup(logicalId); - - debug( - `Resource: ${logicalId} - resourceType: ${mapping.resourceType} - ${Object.getOwnPropertyNames( - mapping.resource, - )}`, - ); - - // If this resource has explicit attribute mappings, those mappings will use PascalCase, not camelCase. - const propertyName = mapping.attributes !== undefined ? attribute : attributePropertyName(attribute); - - const descs = Object.getOwnPropertyDescriptors(mapping.attributes || mapping.resource); - const d = descs[propertyName]; - if (!d) { - throw new Error(`No property ${propertyName} for attribute ${attribute} on resource ${logicalId}`); - } - return d.value; - } -} - -function containsEventuals(v: any): boolean { - if (typeof v !== 'object') { - return false; - } - - if (v instanceof Promise || pulumi.Output.isInstance(v)) { - return true; - } - - if (Array.isArray(v)) { - return v.some((e) => containsEventuals(e)); - } - - return Object.values(v).some((e) => containsEventuals(e)); -} - -function lift(f: (args: any) => any, args: any): any { - if (!containsEventuals(args)) { - return f(args); - } - return pulumi.all(args).apply(f); -} - function debugAssembly(assembly: cx.CloudAssembly): any { return { version: assembly.version, diff --git a/src/types.ts b/src/types.ts index 5f239ce4..0cb3ccaa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,38 @@ +import * as pulumi from '@pulumi/pulumi'; +import { Stack, StackProps } from 'aws-cdk-lib/core'; +import { ResourceMapping } from './interop'; +/** + * Options specific to the Stack component. + */ +export interface StackOptions extends pulumi.ComponentResourceOptions { + /** + * Specify the CDK Stack properties to asociate with the stack. + */ + props?: StackProps; + + /** + * Defines a mapping to override and/or provide an implementation for a CloudFormation resource + * type that is not (yet) implemented in the AWS Cloud Control API (and thus not yet available in + * the Pulumi AWS Native provider). Pulumi code can override this method to provide a custom mapping + * of CloudFormation elements and their properties into Pulumi CustomResources, commonly by using the + * AWS Classic provider to implement the missing resource. + * + * @param logicalId The logical ID of the resource being mapped. + * @param typeName The CloudFormation type name of the resource being mapped. + * @param props The bag of input properties to the CloudFormation resource being mapped. + * @param options The set of Pulumi ResourceOptions to apply to the resource being mapped. + * @returns An object containing one or more logical IDs mapped to Pulumi resources that must be + * created to implement the mapped CloudFormation resource, or else undefined if no mapping is + * implemented. + */ + remapCloudControlResource?( + logicalId: string, + typeName: string, + props: any, + options: pulumi.ResourceOptions, + ): ResourceMapping | undefined; +} + /** * The pulumi provider to read the schema from */ @@ -5,3 +40,66 @@ export enum PulumiProvider { // We currently only support aws-native provider resources AWS_NATIVE = 'aws-native', } + +/** + * StackComponentResource is the underlying pulumi ComponentResource for each pulumicdk.Stack + * This exists because pulumicdk.Stack needs to extend cdk.Stack, but we also want it to represent a + * pulumi ComponentResource so we create this `StackComponentResource` to hold the pulumi logic + */ +export abstract class StackComponentResource extends pulumi.ComponentResource { + public abstract name: string; + + /** + * The directory to which cdk synthesizes the CloudAssembly + */ + public abstract assemblyDir: string; + + /** + * The Stack that creates this component + */ + public abstract stack: Stack; + + /** + * Any stack options that are supplied by the user + * @internal + */ + public abstract options?: StackOptions; + + /** + * Register pulumi outputs to the stack + * @internal + */ + abstract registerOutput(outputId: string, output: any): void; + + constructor(id: string, options?: pulumi.ComponentResourceOptions) { + super('cdk:index:Stack', id, {}, options); + } +} +export type Mapping = { + resource: T; + resourceType: string; + attributes?: { [name: string]: pulumi.Input }; +}; + +export function containsEventuals(v: any): boolean { + if (typeof v !== 'object') { + return false; + } + + if (v instanceof Promise || pulumi.Output.isInstance(v)) { + return true; + } + + if (Array.isArray(v)) { + return v.some((e) => containsEventuals(e)); + } + + return Object.values(v).some((e) => containsEventuals(e)); +} + +export function lift(f: (args: any) => any, args: any): any { + if (!containsEventuals(args)) { + return f(args); + } + return pulumi.all(args).apply(f); +} diff --git a/tests/assembly/manifest.test.ts b/tests/assembly/manifest.test.ts new file mode 100644 index 00000000..3b33dbbe --- /dev/null +++ b/tests/assembly/manifest.test.ts @@ -0,0 +1,167 @@ +import * as path from 'path'; +import * as mockfs from 'mock-fs'; +import { AssemblyManifestReader } from '../../src/assembly'; + +describe('cloud assembly manifest reader', () => { + const manifestFile = '/tmp/foo/bar/does/not/exist/manifest.json'; + const manifestStack = '/tmp/foo/bar/does/not/exist/test-stack.template.json'; + const manifestTree = '/tmp/foo/bar/does/not/exist/tree.json'; + const manifestAssets = '/tmp/foo/bar/does/not/exist/test-stack.assets.json'; + beforeEach(() => { + mockfs({ + // Recursively loads all node_modules + node_modules: mockfs.load(path.resolve(__dirname, '../../node_modules')), + [manifestAssets]: JSON.stringify({ + version: '36.0.0', + files: { + abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44: { + source: { + path: 'asset.abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + packaging: 'zip', + }, + destinations: { + 'current_account-current_region': { + bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + objectKey: 'abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44.zip', + }, + }, + }, + cd12352cc95113284dfa6575f1d74d8dea52dddcaa2f46fa695b33b59c1b4579: { + source: { + path: 'stack.template.json', + packaging: 'file', + }, + destinations: { + 'current_account-current_region': { + bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + objectKey: 'cd12352cc95113284dfa6575f1d74d8dea52dddcaa2f46fa695b33b59c1b4579.json', + }, + }, + }, + }, + dockerImages: {}, + }), + [manifestTree]: JSON.stringify({ + version: 'tree-0.1', + tree: { + id: 'App', + path: '', + children: { + 'test-stack': { + id: 'test-stack', + path: 'test-stack', + }, + }, + }, + }), + [manifestStack]: JSON.stringify({ + Resources: { + MyFunction1ServiceRole9852B06B: { + Type: 'AWS::IAM::Role', + Properties: {}, + }, + MyFunction12A744C2E: { + Type: 'AWS::Lambda::Function', + Properties: {}, + }, + }, + }), + [manifestFile]: JSON.stringify({ + version: '17.0.0', + artifacts: { + 'test-stack.assets': { + type: 'cdk:asset-manifest', + properties: { + file: 'test-stack.assets.json', + }, + }, + Tree: { + type: 'cdk:tree', + properties: { + file: 'tree.json', + }, + }, + 'test-stack': { + type: 'aws:cloudformation:stack', + environment: 'aws://unknown-account/unknown-region', + properties: { + templateFile: 'test-stack.template.json', + validateOnSynth: false, + }, + metadata: { + '/test-stack/MyFunction1/ServiceRole/Resource': [ + { + type: 'aws:cdk:logicalId', + data: 'MyFunction1ServiceRole9852B06B', + }, + ], + '/test-stack/MyFunction1/Resource': [ + { + type: 'aws:cdk:logicalId', + data: 'MyFunction12A744C2E', + }, + ], + }, + displayName: 'test-stack', + }, + }, + }), + }); + }); + + afterEach(() => { + mockfs.restore(); + }); + + test('throws if manifest file not found', () => { + expect(() => { + AssemblyManifestReader.fromDirectory('some-other-file'); + }).toThrow(/Cannot read manifest at 'some-other-file\/manifest.json'/); + }); + + test('can read manifest from path', () => { + expect(() => { + AssemblyManifestReader.fromDirectory(path.dirname(manifestFile)); + }).not.toThrow(); + }); + + test('fromPath sets directory correctly', () => { + const manifest = AssemblyManifestReader.fromDirectory(path.dirname(manifestFile)); + expect(manifest.directory).toEqual('/tmp/foo/bar/does/not/exist'); + }); + + test('can get stacks from manifest', () => { + const manifest = AssemblyManifestReader.fromDirectory(path.dirname(manifestFile)); + + expect(manifest.stackManifests[0]).toEqual({ + assets: expect.anything(), + constructTree: { id: 'test-stack', path: 'test-stack' }, + directory: '/tmp/foo/bar/does/not/exist', + id: 'test-stack', + metadata: { + 'test-stack/MyFunction1/Resource': 'MyFunction12A744C2E', + 'test-stack/MyFunction1/ServiceRole/Resource': 'MyFunction1ServiceRole9852B06B', + }, + outputs: undefined, + parameters: undefined, + resources: { + MyFunction12A744C2E: { Properties: {}, Type: 'AWS::Lambda::Function' }, + MyFunction1ServiceRole9852B06B: { Properties: {}, Type: 'AWS::IAM::Role' }, + }, + templatePath: 'test-stack.template.json', + }); + expect(manifest.stackManifests[0].fileAssets.length).toEqual(1); + expect(manifest.stackManifests[0].fileAssets[0]).toEqual({ + destination: { + bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + objectKey: 'abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44.zip', + }, + id: { + assetId: 'abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + destinationId: 'current_account-current_region', + }, + packaging: 'zip', + path: '/tmp/foo/bar/does/not/exist/asset.abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + }); + }); +}); diff --git a/tests/assembly/stack.test.ts b/tests/assembly/stack.test.ts new file mode 100644 index 00000000..fd7d53ef --- /dev/null +++ b/tests/assembly/stack.test.ts @@ -0,0 +1,140 @@ +import { StackManifest } from '../../src/assembly'; + +describe('StackManifest', () => { + test('Throws if template has no resources', () => { + expect(() => { + new StackManifest('dir', 'id', 'path', {}, { id: 'id', path: 'path' }, {}, []); + }).toThrow(/CloudFormation template has no resources/); + }); + + test('get file assets', () => { + const stack = new StackManifest( + 'dir', + 'id', + 'path', + {}, + { id: 'id', path: 'path' }, + { + Resources: { SomeResource: { Type: 'sometype', Properties: {} } }, + }, + [ + { + id: { + assetId: 'asset', + destinationId: 'dest', + }, + type: 'file', + source: { path: 'somepath' }, + destination: { objectKey: 'abc', bucketName: 'bucket' }, + genericSource: {}, + genericDestination: {}, + }, + { + id: { + assetId: 'asset2', + destinationId: 'dest2', + }, + type: 'docker-image', + source: {}, + destination: { imageTag: 'tag', repositoryName: 'repop' }, + genericSource: {}, + genericDestination: {}, + }, + ], + ); + expect(stack.fileAssets.length).toEqual(1); + expect(stack.fileAssets[0]).toEqual({ + destination: { + bucketName: 'bucket', + objectKey: 'abc', + }, + id: { + assetId: 'asset', + destinationId: 'dest', + }, + packaging: 'file', + path: 'dir/somepath', + }); + }); + + test('can get logicalId for path', () => { + const stack = new StackManifest( + 'dir', + 'id', + 'path', + { + 'stack/bucket': 'SomeBucket', + }, + { + id: 'id', + path: 'path', + }, + { + Resources: { + SomeBucket: { + Type: 'AWS::S3::Bucket', + Properties: {}, + }, + }, + }, + [], + ); + expect(stack.logicalIdForPath('stack/bucket')).toEqual('SomeBucket'); + }); + + test('can get resource for path', () => { + const stack = new StackManifest( + 'dir', + 'id', + 'path', + { + 'stack/bucket': 'SomeBucket', + }, + { + id: 'id', + path: 'path', + }, + { + Resources: { + SomeBucket: { + Type: 'AWS::S3::Bucket', + Properties: { Key: 'Value' }, + }, + }, + }, + [], + ); + expect(stack.resourceWithPath('stack/bucket')).toEqual({ + Type: 'AWS::S3::Bucket', + Properties: { Key: 'Value' }, + }); + }); + + test('can get resource for logicalId', () => { + const stack = new StackManifest( + 'dir', + 'id', + 'path', + { + 'stack/bucket': 'SomeBucket', + }, + { + id: 'id', + path: 'path', + }, + { + Resources: { + SomeBucket: { + Type: 'AWS::S3::Bucket', + Properties: { Key: 'Value' }, + }, + }, + }, + [], + ); + expect(stack.resourceWithLogicalId('SomeBucket')).toEqual({ + Type: 'AWS::S3::Bucket', + Properties: { Key: 'Value' }, + }); + }); +}); diff --git a/tests/aws-resource-mappings.test.ts b/tests/aws-resource-mappings.test.ts index 670ae3ea..47af0c70 100644 --- a/tests/aws-resource-mappings.test.ts +++ b/tests/aws-resource-mappings.test.ts @@ -1,6 +1,5 @@ import { CfnResource, Stack } from 'aws-cdk-lib/core'; import { mapToAwsResource } from '../src/aws-resource-mappings'; -import { setMocks } from './mocks'; import * as aws from '@pulumi/aws'; jest.mock('@pulumi/aws', () => { @@ -31,9 +30,7 @@ afterEach(() => { jest.resetAllMocks(); }); -beforeAll(() => { - setMocks(); -}); +beforeAll(() => {}); describe('AWS Resource Mappings', () => { test('maps iam.Policy', () => { @@ -56,16 +53,7 @@ describe('AWS Resource Mappings', () => { Users: ['my-user'], }; // WHEN - mapToAwsResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToAwsResource(logicalId, cfnType, cfnProps, {}); // THEN expect(aws.iam.Policy).toHaveBeenCalledWith( logicalId, @@ -131,16 +119,7 @@ describe('AWS Resource Mappings', () => { TlsConfig: { ServerNameToVerify: 'example.com' }, }; // WHEN - mapToAwsResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToAwsResource(logicalId, cfnType, cfnProps, {}); // THEN expect(aws.apigatewayv2.Integration).toHaveBeenCalledWith( logicalId, diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 10b1228a..85128db5 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -16,8 +16,45 @@ import * as pulumi from '@pulumi/pulumi'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { Stack } from '../src/stack'; import { Construct } from 'constructs'; -import * as mocks from './mocks'; import * as output from '../src/output'; +import { MockCallArgs, MockResourceArgs } from '@pulumi/pulumi/runtime'; + +function arn(service: string, type: string): string { + const [region, account] = service === 's3' ? ['', ''] : ['us-west-2', '123456789012']; + return `arn:aws:${service}:${region}:${account}:${type}`; +} + +function setMocks() { + pulumi.runtime.setMocks({ + call: (args: MockCallArgs) => { + return {}; + }, + newResource: (args: MockResourceArgs): { id: string; state: any } => { + switch (args.type) { + case 'cdk:index:Stack': + return { id: '', state: {} }; + case 'cdk:construct:TestStack': + return { id: '', state: {} }; + case 'cdk:construct:teststack': + return { id: '', state: {} }; + case 'cdk:index:Component': + return { id: '', state: {} }; + case 'cdk:construct:Bucket': + return { id: '', state: {} }; + case 'aws-native:s3:Bucket': + return { + id: args.name, + state: { + ...args.inputs, + arn: arn('s3', args.inputs['bucketName']), + }, + }; + default: + throw new Error(`unrecognized resource type ${args.type}`); + } + }, + }); +} function testStack(fn: (scope: Construct) => void, done: any) { class TestStack extends Stack { @@ -36,7 +73,7 @@ function testStack(fn: (scope: Construct) => void, done: any) { describe('Basic tests', () => { beforeEach(() => { - mocks.setMocks(); + setMocks(); }); test('Checking single resource registration', (done) => { testStack((adapter) => { diff --git a/tests/cfn-resource-mappings.test.ts b/tests/cfn-resource-mappings.test.ts index dd2e4d5c..2bf1dac2 100644 --- a/tests/cfn-resource-mappings.test.ts +++ b/tests/cfn-resource-mappings.test.ts @@ -1,7 +1,5 @@ -import { CfnResource, Stack } from 'aws-cdk-lib/core'; import { CustomResource } from '@pulumi/pulumi'; import { mapToCfnResource } from '../src/cfn-resource-mappings'; -import { setMocks } from './mocks'; import * as aws from '@pulumi/aws-native'; jest.mock('@pulumi/pulumi', () => { @@ -45,9 +43,7 @@ afterEach(() => { jest.resetAllMocks(); }); -beforeAll(() => { - setMocks(); -}); +beforeAll(() => {}); describe('Cfn Resource Mappings', () => { test('lowercase s3.Bucket name', () => { @@ -56,16 +52,7 @@ describe('Cfn Resource Mappings', () => { const logicalId = 'My-resource'; const cfnProps = {}; // WHEN - mapToCfnResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToCfnResource(logicalId, cfnType, cfnProps, {}); // THEN expect(aws.s3.Bucket).toHaveBeenCalledWith('my-resource', {}, {}); }); @@ -90,16 +77,7 @@ describe('Cfn Resource Mappings', () => { }, }; // WHEN - mapToCfnResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToCfnResource(logicalId, cfnType, cfnProps, {}); // THEN expect(CustomResource).toHaveBeenCalledWith( 'aws-native:s3objectlambda:AccessPoint', @@ -135,16 +113,7 @@ describe('Cfn Resource Mappings', () => { }, }; // WHEN - mapToCfnResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToCfnResource(logicalId, cfnType, cfnProps, {}); // THEN expect(CustomResource).toHaveBeenCalledWith( 'aws-native:lambda:Function', @@ -197,16 +166,7 @@ describe('Cfn Resource Mappings', () => { ], }; // WHEN - mapToCfnResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToCfnResource(logicalId, cfnType, cfnProps, {}); // THEN expect(CustomResource).toHaveBeenCalledWith( 'aws-native:iam:Role', @@ -267,16 +227,7 @@ describe('Cfn Resource Mappings', () => { }, }; // WHEN - mapToCfnResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToCfnResource(logicalId, cfnType, cfnProps, {}); // THEN expect(CustomResource).toHaveBeenCalledWith( 'aws-native:s3:AccessPoint', @@ -308,16 +259,7 @@ describe('Cfn Resource Mappings', () => { CidrBlock: '10.0.0.0/16', }; // WHEN - mapToCfnResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToCfnResource(logicalId, cfnType, cfnProps, {}); // THEN expect(CustomResource).toHaveBeenCalledWith( 'aws-native:ec2:Vpc', @@ -339,16 +281,7 @@ describe('Cfn Resource Mappings', () => { const cfnProps = {}; // WHEN - mapToCfnResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToCfnResource(logicalId, cfnType, cfnProps, {}); // THEN expect(CustomResource).toHaveBeenCalledWith(pulumiType, logicalId, {}, {}); @@ -374,16 +307,7 @@ describe('Cfn Resource Mappings', () => { EnableECSManagedTags: true, }; // WHEN - mapToCfnResource( - new CfnResource(new Stack(), logicalId, { - type: cfnType, - properties: cfnProps, - }), - logicalId, - cfnType, - cfnProps, - {}, - ); + mapToCfnResource(logicalId, cfnType, cfnProps, {}); // THEN expect(CustomResource).toHaveBeenCalledWith( 'aws-native:ecs:Service', diff --git a/tests/converters/app-converter.test.ts b/tests/converters/app-converter.test.ts new file mode 100644 index 00000000..3d72cda8 --- /dev/null +++ b/tests/converters/app-converter.test.ts @@ -0,0 +1,308 @@ +import { AppConverter, StackConverter } from '../../src/converters/app-converter'; +import { Stack } from 'aws-cdk-lib/core'; +import { StackComponentResource, StackOptions } from '../../src/types'; +import * as path from 'path'; +import * as mockfs from 'mock-fs'; +import * as pulumi from '@pulumi/pulumi'; +import { MockCallArgs, MockResourceArgs } from '@pulumi/pulumi/runtime'; +import { Bucket, BucketPolicy } from '@pulumi/aws-native/s3'; +import { createStackManifest } from '../utils'; + +// Convert a pulumi.Output to a promise of the same type. +function promiseOf(output: pulumi.Output): Promise { + return new Promise((resolve) => output.apply(resolve)); +} + +function setMocks() { + pulumi.runtime.setMocks( + { + call: (args: MockCallArgs): { [id: string]: any } => { + switch (args.token) { + case 'aws-native:index:getAccountId': + return { + accountId: '12345678910', + }; + case 'aws-native:index:getRegion': + return { + region: 'us-east-2', + }; + case 'aws-native:index:getPartition': + return { + partition: 'aws', + }; + case 'aws-native:index:getAzs': + return { + azs: ['us-east-1a', 'us-east-1b'], + }; + default: + return {}; + } + }, + newResource: (args: MockResourceArgs): { id: string; state: any } => { + return { + id: args.name + '_id', + state: { + ...args.inputs, + arn: args.name + '_arn', + }, + }; + }, + }, + 'project', + 'stack', + false, + ); +} + +class MockStackComponent extends StackComponentResource { + public readonly name = 'stack'; + public readonly assemblyDir: string; + public stack: Stack; + public options?: StackOptions | undefined; + constructor(dir: string) { + super('stack'); + this.assemblyDir = dir; + this.registerOutputs(); + } + + registerOutput(outputId: string, output: any): void {} +} + +describe('App Converter', () => { + const manifestFile = '/tmp/foo/bar/does/not/exist/manifest.json'; + const manifestStack = '/tmp/foo/bar/does/not/exist/test-stack.template.json'; + const manifestTree = '/tmp/foo/bar/does/not/exist/tree.json'; + const manifestAssets = '/tmp/foo/bar/does/not/exist/test-stack.assets.json'; + beforeEach(() => { + mockfs({ + // Recursively loads all node_modules + node_modules: mockfs.load(path.resolve(__dirname, '../../node_modules')), + [manifestAssets]: JSON.stringify({ + version: '36.0.0', + files: { + abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44: { + source: { + path: 'asset.abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + packaging: 'file', + }, + destinations: { + 'current_account-current_region': { + bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + objectKey: 'abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + }, + }, + }, + }, + dockerImages: {}, + }), + [manifestTree]: JSON.stringify({ + version: 'tree-0.1', + tree: { + id: 'App', + path: '', + children: { + 'test-stack': { + id: 'test-stack', + path: 'test-stack', + children: { + 'example-bucket': { + id: 'example-bucket', + path: 'test-stack/example-bucket', + children: { + Resource: { + id: 'Resource', + path: 'test-stack/example-bucket/Resource', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::S3::Bucket', + }, + constructInfo: { + fqn: 'aws-cdk-lib.aws_s3.CfnBucket', + version: '2.149.0', + }, + }, + Policy: { + id: 'Policy', + path: 'test-stack/example-bucket/Policy', + children: { + Resource: { + id: 'Resource', + path: 'test-stack/example-bucket/Policy/Resource', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::S3::BucketPolicy', + }, + constructInfo: { + fqn: 'aws-cdk-lib.aws_s3.CfnBucketPolicy', + version: '2.149.0', + }, + }, + }, + constructInfo: { + fqn: 'aws-cdk-lib.aws_s3.BucketPolicy', + version: '2.149.0', + }, + }, + }, + constructInfo: { + fqn: 'aws-cdk-lib.aws_s3.Bucket', + version: '2.149.0', + }, + }, + }, + }, + }, + }, + }), + [manifestStack]: JSON.stringify({ + Resources: { + examplebucketC9DFA43E: { + Type: 'AWS::S3::Bucket', + Properties: {}, + }, + examplebucketPolicyE09B485E: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'examplebucketC9DFA43E', + }, + }, + }, + }, + }), + [manifestFile]: JSON.stringify({ + version: '17.0.0', + artifacts: { + 'test-stack.assets': { + type: 'cdk:asset-manifest', + properties: { + file: 'test-stack.assets.json', + }, + }, + Tree: { + type: 'cdk:tree', + properties: { + file: 'tree.json', + }, + }, + 'test-stack': { + type: 'aws:cloudformation:stack', + environment: 'aws://unknown-account/unknown-region', + properties: { + templateFile: 'test-stack.template.json', + validateOnSynth: false, + }, + metadata: { + '/test-stack/example-bucket/Resource': [ + { + type: 'aws:cdk:logicalId', + data: 'examplebucketC9DFA43E', + }, + ], + '/test-stack/example-bucket/Policy/Resource': [ + { + type: 'aws:cdk:logicalId', + data: 'examplebucketPolicyE09B485E', + }, + ], + }, + displayName: 'test-stack', + }, + }, + }), + }); + }); + + afterEach(() => { + mockfs.restore(); + }); + test('can convert', async () => { + setMocks(); + const mockStackComponent = new MockStackComponent('/tmp/foo/bar/does/not/exist'); + const converter = new AppConverter(mockStackComponent); + converter.convert(); + const stacks = Array.from(converter.stacks.values()); + + const resourceMap: { [key: string]: pulumi.Resource } = {}; + const urnPromises = stacks.flatMap((stack) => { + const resources = Array.from(stack.resources.values()); + resources.forEach((res) => (resourceMap[res.resourceType] = res.resource)); + return resources.flatMap((res) => promiseOf(res.resource.urn)); + }); + const urns = await Promise.all(urnPromises); + expect(urns).toEqual([ + 'urn:pulumi:stack::project::cdk:index:Stack$aws:s3/bucketObjectv2:BucketObjectv2::stack/abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44/current_account-current_region', + createUrn('Bucket', 'examplebucketc9dfa43e'), + createUrn('BucketPolicy', 'examplebucketPolicyE09B485E'), + ]); + const bucket = resourceMap['AWS::S3::BucketPolicy'] as BucketPolicy; + const bucketName = await promiseOf(bucket.bucket); + expect(bucketName).toEqual('examplebucketc9dfa43e_id'); + }); + + test.each([ + ['ref', createStackManifest({ Bucket: { Ref: 'resource1' } }), 'resource1_id'], + [ + 'GetAtt', + createStackManifest({ + Bucket: { 'Fn::GetAtt': ['resource1', 'Arn'] }, + }), + 'resource1_arn', + ], + [ + 'Join-Ref', + createStackManifest({ + Bucket: { 'Fn::Join': ['', ['arn:', { Ref: 'resource1' }]] }, + }), + 'arn:resource1_id', + ], + [ + 'Split-Select-Ref', + createStackManifest({ + Bucket: { 'Fn::Select': ['1', { 'Fn::Split': ['_', { Ref: 'resource1' }] }] }, + }), + 'id', + ], + [ + 'Base64-Ref', + createStackManifest({ + Bucket: { 'Fn::Base64': { Ref: 'resource1' } }, + }), + Buffer.from('resource1_id').toString('base64'), + ], + [ + 'GetAZs-Select-Ref', + createStackManifest({ + Bucket: { 'Fn::Select': ['1', { 'Fn::GetAZs': 'us-east-1' }] }, + }), + 'us-east-1b', + ], + [ + 'Sub-Ref', + createStackManifest({ + Bucket: { 'Fn::Sub': 'www.${resource1}-${AWS::Region}-${AWS::AccountId}' }, + }), + 'www.resource1_id-us-east-2-12345678910', + ], + ])( + 'intrinsics %s', + async (_name, stackManifest, expected) => { + setMocks(); + const mockStackComponent = new MockStackComponent('/tmp/foo/bar/does/not/exist'); + const converter = new StackConverter(mockStackComponent, stackManifest); + converter.convert(new Set()); + const promises = Array.from(converter.resources.values()).flatMap((res) => promiseOf(res.resource.urn)); + await Promise.all(promises); + const bucket = converter.resources.get('resource1'); + expect(bucket).toBeDefined(); + const policy = converter.resources.get('resource2'); + expect(policy).toBeDefined(); + const policyResource = policy!.resource as BucketPolicy; + const policyBucket = await promiseOf(policyResource.bucket); + expect(policyBucket).toEqual(expected); + }, + 10_000, + ); +}); + +function createUrn(resource: string, logicalId: string): string { + return `urn:pulumi:stack::project::cdk:construct:${resource}$aws-native:s3:${resource}::${logicalId}`; +} diff --git a/tests/converters/artifact-converter.test.ts b/tests/converters/artifact-converter.test.ts new file mode 100644 index 00000000..8a8f039f --- /dev/null +++ b/tests/converters/artifact-converter.test.ts @@ -0,0 +1,100 @@ +import { FileAssetPackaging, Stack } from 'aws-cdk-lib/core'; +import { FileAssetManifestConverter } from '../../src/converters/artifact-converter'; +import { StackComponentResource, StackOptions } from '../../src/types'; +import { FileAssetManifest } from '../../src/assembly'; +import * as pulumi from '@pulumi/pulumi'; +import { MockCallArgs, MockResourceArgs } from '@pulumi/pulumi/runtime'; + +function setMocks(assertFn: (args: MockResourceArgs) => void) { + pulumi.runtime.setMocks( + { + call: (args: MockCallArgs): { [id: string]: any } => { + switch (args.token) { + case 'aws-native:index:getAccountId': + return { + accountId: '12345678910', + }; + case 'aws-native:index:getRegion': + return { + region: 'us-east-2', + }; + case 'aws-native:index:getPartition': + return { + partition: 'aws', + }; + default: + return {}; + } + }, + newResource: (args: MockResourceArgs): { id: string; state: any } => { + switch (args.type) { + case 'cdk:index:stack': + return { id: '', state: {} }; + default: + assertFn(args); + return { + id: args.name + '_id', + state: args.inputs, + }; + } + }, + }, + 'project', + 'stack', + false, + ); +} + +class MockStackComponent extends StackComponentResource { + public readonly name = 'stack'; + public readonly assemblyDir: string = 'dir'; + public stack: Stack; + public options?: StackOptions | undefined; + constructor() { + super('stack'); + this.registerOutputs(); + } + + registerOutput(outputId: string, output: any): void {} +} + +describe('Artifact Converters', () => { + test('can convert file artifacts', (done) => { + setMocks((args) => { + if (args.type === 'aws:s3/bucketObjectv2:BucketObjectv2') { + expect(args.id).toEqual(''); + expect(args.name).toEqual( + 'stack/abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44/current_account-current_region', + ); + expect(args.inputs).toEqual({ + bucket: 'cdk-hnb659fds-assets-12345678910-us-east-2', + key: 'abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + source: 'dir/asset.abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + }); + } + }); + const mockStackComponent = new MockStackComponent(); + const converter = new FileAssetManifestConverter( + mockStackComponent, + new FileAssetManifest('dir', { + genericDestination: undefined, + genericSource: undefined, + destination: { + bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + objectKey: 'abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + }, + source: { + path: 'asset.abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + packaging: FileAssetPackaging.FILE, + }, + type: 'file', + id: { + destinationId: 'current_account-current_region', + assetId: 'abe4e2f4fcc1aaaf53db4829c23a5cf08795d36cce0f68a3321c1c8d728fec44', + }, + }), + ); + converter.convert(); + mockStackComponent.urn.apply(() => done()); + }); +}); diff --git a/tests/graph.test.ts b/tests/graph.test.ts index 9112bbbc..1ef803ad 100644 --- a/tests/graph.test.ts +++ b/tests/graph.test.ts @@ -12,241 +12,230 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as autoscaling from 'aws-cdk-lib/aws-autoscaling'; -import * as ec2 from 'aws-cdk-lib/aws-ec2'; -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as s3 from 'aws-cdk-lib/aws-s3'; -import { Service, Source } from '@aws-cdk/aws-apprunner-alpha'; -import { App, Stack, Aspects } from 'aws-cdk-lib'; import { GraphBuilder, GraphNode } from '../src/graph'; -import { Construct } from 'constructs'; - -function testGraph(fn: (scope: Construct) => void, expected: (string | RegExp)[], done: any) { - const app = new App(); - const stack = new GraphTester(app, 'graphtest', fn); - app.synth(); - const sortedPaths = stack.nodes.map((n) => n.construct.node.path); - - expect(sortedPaths.length).toEqual(expected.length); - for (let i = 0; i < sortedPaths.length; i++) { - const [actualPath, expectedPath] = [sortedPaths[i], expected[i]]; - if (typeof expectedPath === 'string') { - expect(actualPath).toEqual(expectedPath); - } else { - expect(actualPath).toMatch(expectedPath); - } - } - - done(); -} - -class GraphTester extends Stack { - public nodes: GraphNode[] = []; - - constructor(scope: Construct, id: string, fn: (scope: Construct) => void) { - super(undefined, id); - - Aspects.of(scope).add({ - visit: (node) => { - if (node === scope) { - this.nodes = GraphBuilder.build(this); - } +import { StackManifest } from '../src/assembly'; +import { createStackManifest } from './utils'; + +const nodes = GraphBuilder.build( + new StackManifest( + 'test', + 'stack', + 'test/stack', + { + 'stack/example-bucket/Resource': 'examplebucketC9DFA43E', + 'stack/example-bucket/Policy/Resource': 'examplebucketPolicyE09B485E', + }, + { + path: 'stack', + id: 'stack', + children: { + 'example-bucket': { + id: 'example-bucket', + path: 'stack/example-bucket', + children: { + Resource: { + id: 'Resource', + path: 'stack/example-bucket/Resource', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::S3::Bucket', + }, + constructInfo: { + fqn: 'aws-cdk-lib.aws_s3.CfnBucket', + version: '2.149.0', + }, + }, + Policy: { + id: 'Policy', + path: 'stack/example-bucket/Policy', + children: { + Resource: { + id: 'Resource', + path: 'stack/example-bucket/Policy/Resource', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::S3::BucketPolicy', + }, + constructInfo: { + fqn: 'aws-cdk-lib.aws_s3.CfnBucketPolicy', + version: '2.149.0', + }, + }, + }, + constructInfo: { + fqn: 'aws-cdk-lib.aws_s3.BucketPolicy', + version: '2.149.0', + }, + }, + }, + constructInfo: { + fqn: 'aws-cdk-lib.aws_s3.Bucket', + version: '2.149.0', + }, + }, }, - }); - - fn(this); - } -} + constructInfo: { + fqn: 'aws-cdk-lib.Stack', + version: '2.149.0', + }, + }, + { + Resources: { + examplebucketC9DFA43E: { + Type: 'AWS::S3::Bucket', + Properties: {}, + }, + examplebucketPolicyE09B485E: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'examplebucketC9DFA43E', + }, + }, + }, + }, + }, + [], + ), +); describe('Graph tests', () => { - test('Test sort for single resource', (done) => { - testGraph( - (stack) => { - new s3.Bucket(stack, 'MyFirstBucket', { versioned: true }); + test.each([ + [ + nodes, + 'stack', + { + construct: { + path: 'stack', + id: 'stack', + type: 'Stack', + parent: undefined, + }, + logicalId: undefined, + resource: undefined, + incomingEdges: ['stack/example-bucket'], + outgoingEdges: [], }, - ['graphtest', 'graphtest/MyFirstBucket', 'graphtest/MyFirstBucket/Resource'], - done, - ); - }); - - test('Test sort for ALB example', (done) => { - testGraph( - (stack) => { - const vpc = new ec2.Vpc(stack, 'VPC'); - - const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { - vpc, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO), - machineImage: new ec2.AmazonLinuxImage(), - }); - - const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { - vpc, - internetFacing: true, - }); - - const listener = lb.addListener('Listener', { - port: 80, - }); - - listener.addTargets('Target', { - port: 80, - targets: [asg], - }); - - listener.connections.allowDefaultPortFromAnyIpv4('Open to the world'); - - asg.scaleOnRequestCount('AModestLoad', { - targetRequestsPerMinute: 60, - }); + ], + [ + nodes, + 'stack/example-bucket', + { + construct: { + parent: 'stack', + path: 'stack/example-bucket', + id: 'example-bucket', + type: 'Bucket', + }, + logicalId: undefined, + resource: undefined, + incomingEdges: ['stack/example-bucket/Resource', 'stack/example-bucket/Policy'], + outgoingEdges: ['stack'], }, - [ - 'graphtest', - 'graphtest/VPC', - 'graphtest/VPC/Resource', - 'graphtest/VPC/PublicSubnet1', - 'graphtest/VPC/PublicSubnet1/Subnet', - 'graphtest/VPC/PublicSubnet1/RouteTable', - 'graphtest/VPC/PublicSubnet1/RouteTableAssociation', - 'graphtest/VPC/IGW', - 'graphtest/VPC/PublicSubnet1/DefaultRoute', - 'graphtest/VPC/PublicSubnet1/EIP', - 'graphtest/VPC/PublicSubnet1/NATGateway', - 'graphtest/VPC/PublicSubnet2', - 'graphtest/VPC/PublicSubnet2/Subnet', - 'graphtest/VPC/PublicSubnet2/RouteTable', - 'graphtest/VPC/PublicSubnet2/RouteTableAssociation', - 'graphtest/VPC/PublicSubnet2/DefaultRoute', - 'graphtest/VPC/PublicSubnet2/EIP', - 'graphtest/VPC/PublicSubnet2/NATGateway', - 'graphtest/VPC/PrivateSubnet1', - 'graphtest/VPC/PrivateSubnet1/Subnet', - 'graphtest/VPC/PrivateSubnet1/RouteTable', - 'graphtest/VPC/PrivateSubnet1/RouteTableAssociation', - 'graphtest/VPC/PrivateSubnet1/DefaultRoute', - 'graphtest/VPC/PrivateSubnet2', - 'graphtest/VPC/PrivateSubnet2/Subnet', - 'graphtest/VPC/PrivateSubnet2/RouteTable', - 'graphtest/VPC/PrivateSubnet2/RouteTableAssociation', - 'graphtest/VPC/PrivateSubnet2/DefaultRoute', - 'graphtest/VPC/VPCGW', - 'graphtest/ASG', - 'graphtest/ASG/InstanceSecurityGroup', - 'graphtest/ASG/InstanceSecurityGroup/Resource', - 'graphtest/LB', - 'graphtest/LB/SecurityGroup', - 'graphtest/LB/SecurityGroup/Resource', - /graphtest\/ASG\/InstanceSecurityGroup\/from graphtestLBSecurityGroup[A-Z0-9]+:80/, - 'graphtest/ASG/InstanceRole', - 'graphtest/ASG/InstanceRole/Resource', - 'graphtest/ASG/InstanceProfile', - /graphtest\/SsmParameterValue:.*/, - 'graphtest/ASG/LaunchConfig', - 'graphtest/LB/Listener', - 'graphtest/LB/Listener/TargetGroup', - 'graphtest/LB/Listener/TargetGroup/Resource', - 'graphtest/ASG/ASG', - 'graphtest/ASG/ScalingPolicyAModestLoad', - 'graphtest/LB/Resource', - 'graphtest/LB/Listener/Resource', - 'graphtest/ASG/ScalingPolicyAModestLoad/Resource', - /graphtest\/LB\/SecurityGroup\/to graphtestASGInstanceSecurityGroup[A-Z0-9]+:80/, - ], - done, - ); - }); - - test('Test sort for appsvc example', (done) => { - testGraph( - (stack) => { - const cluster = new ecs.CfnCluster(stack, 'clusterstack'); - - const role = new iam.Role(stack, 'taskexecrole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - - new elbv2.CfnListener(stack, 'web', { - loadBalancerArn: 'dummy-alb-arn', - port: 80, - protocol: 'HTTP', - defaultActions: [ - { - type: 'forward', - targetGroupArn: 'dummy-target-group-arn', - }, - ], - }); - - const taskDefinition = new ecs.CfnTaskDefinition(stack, 'apptask', { - family: 'fargate-task-definition', - cpu: '256', - memory: '512', - networkMode: 'awsvpc', - requiresCompatibilities: ['FARGATE'], - executionRoleArn: role.roleArn, - containerDefinitions: [ - { - name: 'my-app', - image: 'nginx', - portMappings: [ - { - containerPort: 80, - hostPort: 80, - protocol: 'tcp', - }, - ], - }, - ], - }); - new ecs.CfnService(stack, 'appsvc', { - serviceName: 'app-svc-cloud-api', - cluster: cluster.attrArn, - desiredCount: 1, - launchType: 'FARGATE', - taskDefinition: taskDefinition.attrTaskDefinitionArn, - networkConfiguration: { - awsvpcConfiguration: { - assignPublicIp: 'ENABLED', - subnets: ['dummy-subnet-id-0', 'dummy-subnet-id-1'], - securityGroups: ['dummy-security-group-id'], + ], + [ + nodes, + 'stack/example-bucket/Resource', + { + construct: { + parent: 'example-bucket', + path: 'stack/example-bucket/Resource', + id: 'Resource', + type: 'Bucket', + }, + resource: { + Type: 'AWS::S3::Bucket', + Properties: {}, + }, + logicalId: 'examplebucketC9DFA43E', + incomingEdges: ['stack/example-bucket/Policy/Resource'], + outgoingEdges: ['stack/example-bucket'], + }, + ], + [ + nodes, + 'stack/example-bucket/Policy', + { + construct: { + parent: 'example-bucket', + path: 'stack/example-bucket/Policy', + id: 'Policy', + type: 'BucketPolicy', + }, + logicalId: undefined, + resource: undefined, + incomingEdges: ['stack/example-bucket/Policy/Resource'], + outgoingEdges: ['stack/example-bucket'], + }, + ], + [ + nodes, + 'stack/example-bucket/Policy/Resource', + { + construct: { + parent: 'Policy', + path: 'stack/example-bucket/Policy/Resource', + id: 'Resource', + type: 'BucketPolicy', + }, + resource: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { + Ref: 'examplebucketC9DFA43E', }, }, - loadBalancers: [ - { - targetGroupArn: 'dummy-target-group-arn', - containerName: 'my-app', - containerPort: 80, - }, - ], - }); + }, + logicalId: 'examplebucketPolicyE09B485E', + incomingEdges: [], + outgoingEdges: ['stack/example-bucket/Policy', 'stack/example-bucket/Resource'], }, - [ - 'graphtest', - 'graphtest/clusterstack', - 'graphtest/taskexecrole', - 'graphtest/taskexecrole/Resource', - 'graphtest/web', - 'graphtest/apptask', - 'graphtest/appsvc', - ], - done, - ); + ], + ])('Parses the graph correctly', (graph, path, expected) => { + const actual = graph.find((node) => node.construct.path === path); + expect(actual).toBeDefined(); + expect(actual!.logicalId).toEqual(expected.logicalId); + expect(actual!.resource).toEqual(expected.resource); + expect(actual!.construct.parent?.id).toEqual(expected.construct.parent); + expect(edgesToArray(actual!.incomingEdges)).toEqual(expected.incomingEdges); + expect(edgesToArray(actual!.outgoingEdges)).toEqual(expected.outgoingEdges); }); - test('Test sort for apprunner example', (done) => { - testGraph( - (stack) => { - const service = new Service(stack, 'service', { - source: Source.fromEcrPublic({ - imageConfiguration: { port: 8000 }, - imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest', - }), - }); - }, - ['graphtest', 'graphtest/service', 'graphtest/service/Resource'], - done, - ); + test.each([ + ['dependsOn', createStackManifest({}, {}, ['resource1'])], + [ + 'ref', + createStackManifest({ + SomeProp: { Ref: 'resource1' }, + }), + ], + [ + 'GetAtt', + createStackManifest({ + SomeProp: { 'Fn::GetAtt': ['resource1', 'Arn'] }, + }), + ], + [ + 'Sub-Ref', + createStackManifest({ + SomeProp: { 'Fn::Sub': ['www.${Domain}', { Domain: { Ref: 'resource1' } }] }, + }), + ], + [ + 'Sub-GetAtt', + createStackManifest({ + SomeProp: { 'Fn::Sub': ['www.${Domain}', { Domain: { 'Fn::GetAtt': ['resource1', 'Arn'] } }] }, + }), + ], + ])('adds edge for %s', (_name, stackManifest) => { + const graph = GraphBuilder.build(stackManifest); + expect(graph[1].construct.path).toEqual('stack/resource-1'); + expect(edgesToArray(graph[1].incomingEdges)).toEqual(['stack/resource-2']); + expect(edgesToArray(graph[1].outgoingEdges)).toEqual(['stack']); + expect(graph[2].construct.path).toEqual('stack/resource-2'); + expect(edgesToArray(graph[2].incomingEdges)).toEqual([]); + expect(edgesToArray(graph[2].outgoingEdges)).toEqual(['stack', 'stack/resource-1']); }); }); + +function edgesToArray(edges: Set): string[] { + return Array.from(edges).flatMap((value) => value.construct.path); +} diff --git a/tests/mocks.ts b/tests/mocks.ts deleted file mode 100644 index 36790329..00000000 --- a/tests/mocks.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2016-2022, Pulumi Corporation. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import * as pulumi from '@pulumi/pulumi'; -import { MockCallArgs, MockResourceArgs } from '@pulumi/pulumi/runtime'; - -function arn(service: string, type: string): string { - const [region, account] = service === 's3' ? ['', ''] : ['us-west-2', '123456789012']; - return `arn:aws:${service}:${region}:${account}:${type}`; -} - -export function setMocks() { - pulumi.runtime.setMocks({ - call: (args: MockCallArgs) => { - return {}; - }, - newResource: (args: MockResourceArgs): { id: string; state: any } => { - switch (args.type) { - case 'cdk:index:Stack': - return { id: '', state: {} }; - case 'cdk:construct:TestStack': - return { id: '', state: {} }; - case 'cdk:index:Component': - return { id: '', state: {} }; - case 'cdk:construct:Bucket': - return { id: '', state: {} }; - case 'aws-native:s3:Bucket': - return { - id: args.name, - state: { - ...args.inputs, - arn: arn('s3', args.inputs['bucketName']), - }, - }; - default: - throw new Error(`unrecognized resource type ${args.type}`); - } - }, - }); -} diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 00000000..92c8dd2e --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,53 @@ +import { StackManifest } from '../src/assembly'; + +export function createStackManifest( + resource2Props: any, + resource1Props?: any, + resource2DependsOn?: string | string[], + resource1DependsOn?: string | string[], +): StackManifest { + return new StackManifest( + 'dir', + 'stack', + 'template', + { + 'stack/resource-1': 'resource1', + 'stack/resource-2': 'resource2', + }, + { + path: 'stack', + id: 'stack', + children: { + 'resource-1': { + id: 'resource-1', + path: 'stack/resource-1', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::S3::Bucket', + }, + }, + 'resource-2': { + id: 'resource-2', + path: 'stack/resource-2', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::S3::BucketPolicy', + }, + }, + }, + }, + { + Resources: { + resource1: { + Type: 'AWS::S3::Bucket', + Properties: resource1Props ?? {}, + DependsOn: resource1DependsOn, + }, + resource2: { + Type: 'AWS::S3::BucketPolicy', + Properties: resource2Props, + DependsOn: resource2DependsOn, + }, + }, + }, + [], + ); +} diff --git a/yarn.lock b/yarn.lock index a478ea45..7c54b8c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,21 @@ resolved "https://registry.yarnpkg.com/@aws-cdk/aws-apprunner-alpha/-/aws-apprunner-alpha-2.20.0-alpha.0.tgz#66ae8b2795281bf46163872f450d9163cf4beb39" integrity sha512-Eno+FXxa7k0Irx9ssl0ML44rlBg2THo8WMqxO3dKZpAZeZbfd8s8T3/UjP1Fq22TCKn+psDJ+wiUAd9r/BI2ig== +"@aws-cdk/cloud-assembly-schema@^38.0.1": + version "38.0.1" + resolved "https://registry.yarnpkg.com/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz#cdf4684ae8778459e039cd44082ea644a3504ca9" + integrity sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w== + dependencies: + jsonschema "^1.4.1" + semver "^7.6.3" + +"@aws-cdk/cx-api@^2.160.0": + version "2.160.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/cx-api/-/cx-api-2.160.0.tgz#08d4599690a39768bb944c411f1141166e313b59" + integrity sha512-ujXT/UoUDquCwxJ14jkRzIFeMabMyLATWP32Jv0WJjWpxrGJCa+Lua+CByOyikC1QeSVxq8pZcrx0jjYyG0qzw== + dependencies: + semver "^7.6.3" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" @@ -1241,6 +1256,14 @@ "@types/node" "*" "@types/responselike" "^1.0.0" +"@types/fs-extra@^11.0.4": + version "11.0.4" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45" + integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== + dependencies: + "@types/jsonfile" "*" + "@types/node" "*" + "@types/glob@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" @@ -1293,6 +1316,13 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jsonfile@*": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702" + integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== + dependencies: + "@types/node" "*" + "@types/keyv@^3.1.4": version "3.1.4" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" @@ -1305,6 +1335,13 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== +"@types/mock-fs@^4.13.4": + version "4.13.4" + resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.4.tgz#e73edb4b4889d44d23f1ea02d6eebe50aa30b09a" + integrity sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@>=13.7.0", "@types/node@^20.12.13": version "20.14.11" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b" @@ -1562,6 +1599,38 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== + dependencies: + glob "^7.1.4" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^2.0.0" + +archiver-utils@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-3.0.4.tgz#a0d201f1cf8fce7af3b5a05aea0a337329e96ec7" + integrity sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw== + dependencies: + glob "^7.2.3" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + archiver-utils@^5.0.0, archiver-utils@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" @@ -1575,6 +1644,19 @@ archiver-utils@^5.0.0, archiver-utils@^5.0.2: normalize-path "^3.0.0" readable-stream "^4.0.0" +archiver@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.2.tgz#99991d5957e53bd0303a392979276ac4ddccf3b0" + integrity sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw== + dependencies: + archiver-utils "^2.1.0" + async "^3.2.4" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.1.2" + tar-stream "^2.2.0" + zip-stream "^4.1.0" + archiver@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" @@ -1620,6 +1702,13 @@ async@^3.2.3, async@^3.2.4: resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + aws-cdk-lib@2.149.0: version "2.149.0" resolved "https://registry.yarnpkg.com/aws-cdk-lib/-/aws-cdk-lib-2.149.0.tgz#5f13a6b2c222f6a1db66be6a58129a67845bf6e8" @@ -1640,6 +1729,22 @@ aws-cdk-lib@2.149.0: table "^6.8.2" yaml "1.10.2" +aws-sdk@^2.1691.0: + version "2.1691.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1691.0.tgz#9d6ccdcbae03c806fc62667b76eb3e33e5294dcc" + integrity sha512-/F2YC+DlsY3UBM2Bdnh5RLHOPNibS/+IcjUuhP8XuctyrN+MlL+fWDAiela32LTDk7hMy4rx8MTgvbJ+0blO5g== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.16.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + util "^0.12.4" + uuid "8.0.0" + xml2js "0.6.2" + b4a@^1.6.4: version "1.6.6" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.6.tgz#a4cc349a3851987c3c4ac2d7785c18744f6da9ba" @@ -1715,7 +1820,7 @@ bare-events@^2.2.0: resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.4.2.tgz#3140cca7a0e11d49b3edc5041ab560659fd8e1f8" integrity sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q== -base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1730,6 +1835,15 @@ bin-links@^4.0.4: read-cmd-shim "^4.0.0" write-file-atomic "^5.0.0" +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1776,6 +1890,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + buffer-crc32@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" @@ -1786,6 +1905,23 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -1835,6 +1971,17 @@ cacheable-request@^7.0.2: normalize-url "^6.0.1" responselike "^2.0.0" +call-bind@^1.0.2, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1860,6 +2007,19 @@ case@1.6.3: resolved "https://registry.yarnpkg.com/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9" integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ== +cdk-assets@^2.154.8: + version "2.154.8" + resolved "https://registry.yarnpkg.com/cdk-assets/-/cdk-assets-2.154.8.tgz#58cccc1ff4f040173f615c400c565eaf6665be90" + integrity sha512-mjqDhWDnf0NciVO2Q+eqDFmbhGa42OgudEN/V8D2YNzgBrt4Lk/ogF3z4mKPM6yAucQ8tslYh0JzWuO4wS29hg== + dependencies: + "@aws-cdk/cloud-assembly-schema" "^38.0.1" + "@aws-cdk/cx-api" "^2.160.0" + archiver "^5.3.2" + aws-sdk "^2.1691.0" + glob "^7.2.3" + mime "^2.6.0" + yargs "^16.2.0" + chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1902,6 +2062,15 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -1962,6 +2131,16 @@ common-ancestor-path@^1.0.1: resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w== +compress-commons@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.2.tgz#6542e59cb63e1f46a8b21b0e06f9a32e4c8b06df" + integrity sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.2" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + compress-commons@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" @@ -1998,6 +2177,14 @@ crc-32@^1.2.0: resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== +crc32-stream@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.3.tgz#85dd677eb78fa7cad1ba17cc506a597d41fc6f33" + integrity sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + crc32-stream@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" @@ -2072,6 +2259,15 @@ defer-to-connect@^2.0.0: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -2130,7 +2326,7 @@ encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2154,6 +2350,18 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + escalade@^3.1.1, escalade@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -2279,6 +2487,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== + events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -2434,6 +2647,13 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + foreground-child@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" @@ -2442,6 +2662,11 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@^11.2.0: version "11.2.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" @@ -2490,6 +2715,17 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -2533,7 +2769,7 @@ glob@^10.0.0, glob@^10.2.2, glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.1.3, glob@^7.1.4: +glob@^7.1.3, glob@^7.1.4, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2574,6 +2810,13 @@ google-protobuf@^3.5.0: resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.4.tgz#2f933e8b6e5e9f8edde66b7be0024b68f77da6c9" integrity sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + got@^11.8.6: version "11.8.6" resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" @@ -2611,7 +2854,31 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -hasown@^2.0.2: +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -2671,7 +2938,12 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.2.1: +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -2732,7 +3004,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2755,11 +3027,24 @@ ip-address@^9.0.5: jsbn "1.1.0" sprintf-js "^1.1.3" +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-callable@^1.1.3: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-core-module@^2.13.0: version "2.15.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" @@ -2782,6 +3067,13 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -2809,7 +3101,14 @@ is-stream@^2.0.0, is-stream@^2.0.1: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -isarray@~1.0.0: +is-typed-array@^1.1.3: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== @@ -3264,6 +3563,11 @@ jest@^29.5.0: import-local "^3.0.2" jest-cli "^29.7.0" +jmespath@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" + integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3426,6 +3730,26 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA== + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -3441,6 +3765,11 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== + lodash@^4.17.15: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -3535,7 +3864,7 @@ mime-types@^2.1.35: dependencies: mime-db "1.52.0" -mime@^2.0.0: +mime@^2.0.0, mime@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -3650,6 +3979,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mock-fs@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.3.0.tgz#7dfc95ce5528aff8e10fa117161b91d8129e0e9e" + integrity sha512-IMvz1X+RF7vf+ur7qUenXMR7/FSKSIqS3HqFHXcyNI7G0FbpFO8L5lfsUJhl+bhK1AiulVHWKUSxebWauPA+xQ== + module-details-from-path@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" @@ -4000,6 +4334,11 @@ pkg-dir@^7.0.0: dependencies: find-up "^6.3.0" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-selector-parser@^6.0.10: version "6.1.1" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz#5be94b277b8955904476a2400260002ce6c56e38" @@ -4104,6 +4443,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== + punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -4114,6 +4458,11 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -4147,7 +4496,7 @@ read-package-json-fast@^3.0.0, read-package-json-fast@^3.0.2: json-parse-even-better-errors "^3.0.0" npm-normalize-package-bin "^3.0.0" -readable-stream@^2.0.5: +readable-stream@^2.0.0, readable-stream@^2.0.5: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -4160,6 +4509,15 @@ readable-stream@^2.0.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^4.0.0: version "4.5.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" @@ -4279,6 +4637,16 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== + +sax@>=0.6.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + semver@^5.4.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -4289,11 +4657,23 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2: +semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -4482,7 +4862,7 @@ string-width@4.1.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2. is-fullwidth-code-point "^3.0.0" strip-ansi "^5.2.0" -string_decoder@^1.3.0: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -4576,6 +4956,17 @@ table@^6.8.2: string-width "^4.2.3" strip-ansi "^6.0.1" +tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar-stream@^3.0.0: version "3.1.7" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" @@ -4773,11 +5164,35 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@^1.0.2, util-deprecate@~1.0.1: +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +util@^0.12.4: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + +uuid@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -4822,6 +5237,17 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +which-typed-array@^1.1.14, which-typed-array@^1.1.2: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -4880,11 +5306,24 @@ write-file-atomic@^5.0.0: imurmurhash "^0.1.4" signal-exit "^4.0.1" +xml2js@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" + integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -4905,11 +5344,29 @@ yaml@1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" @@ -4938,6 +5395,15 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== +zip-stream@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.1.tgz#1337fe974dbaffd2fa9a1ba09662a66932bd7135" + integrity sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ== + dependencies: + archiver-utils "^3.0.4" + compress-commons "^4.1.2" + readable-stream "^3.6.0" + zip-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb"