Skip to content

Commit

Permalink
feat!: add cdk-exec --all (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
misterjoshua authored Feb 20, 2022
1 parent cc1b8ce commit 8c5b724
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 107 deletions.
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ Lambdas and State Machines in AWS.

```
$ cdk-exec integ-cdk-exec/Function --input '{"succeed":true}'
✨ Executing integ-cdk-exec-Function76856677-k5ehIzbG2T6S
⚡ Executing integ-cdk-exec/Function/Resource (integ-cdk-exec-Function76856677-k5ehIzbG2T6S)
✨ Final status of integ-cdk-exec/Function/Resource
Output:
{
Expand Down Expand Up @@ -76,7 +79,10 @@ $ cdk synth --output cdk.out

```
$ cdk-exec integ-cdk-exec/StateMachine --input '{"succeed":true}'
✨ Executing arn:aws:states:REGION:000000000000:stateMachine:StateMachine2E01A3A5-8z4XHXAvT3qq
⚡ Executing integ-cdk-exec/StateMachine/Resource (arn:aws:states:REGION:000000000000:stateMachine:StateMachine2E01A3A5-8z4XHXAvT3qq)
✨ Final status of integ-cdk-exec/StateMachine/Resource
Output:
{
Expand All @@ -90,7 +96,10 @@ Output:

```
$ cdk-exec integ-cdk-exec/Function --input '{"succeed":true}'
✨ Executing integ-cdk-exec-Function76856677-k5ehIzbG2T6S
⚡ Executing integ-cdk-exec/Function/Resource (integ-cdk-exec-Function76856677-k5ehIzbG2T6S)
✨ Final status of integ-cdk-exec/Function/Resource (integ-cdk-exec-Function76856677-k5ehIzbG2T6S)
Output:
{
Expand All @@ -105,7 +114,10 @@ Output:

```
$ cdk-exec --app path/to/cdkout integ-cdk-exec/Function --input '{"json":"here"}'
✨ Executing integ-cdk-exec-Function76856677-k5ehIzbG2T6S
⚡ Executing integ-cdk-exec/Function/Resource (integ-cdk-exec-Function76856677-k5ehIzbG2T6S)
✨ Final status of integ-cdk-exec/Function/Resource
Output:
{
Expand All @@ -125,8 +137,7 @@ is also a convenient shortcut when your app has only one executable resource.
For example, if you have only one function or state machine in a stack, you
can type `cdk-exec my-stack` and your resource will be found. If your entire
app has only one executable resource, you can run `cdk-exec` without arguments
to run it. However, if you provide an ambiguous path, matching more than one
supported resource, you will receive an error message.
to run it.

**Path metadata**

Expand Down
90 changes: 69 additions & 21 deletions src/cli/cdk-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as cxapi from 'aws-cdk-lib/cx-api';
import chalk from 'chalk';
import * as yargs from 'yargs';
import { AwsSdk } from '../aws-sdk';
import { AmbiguousPathError, Executor } from '../executor';
import { Executor } from '../executor';
import { MetadataMatch } from '../find-matching-resources';

async function main(): Promise<number> {
const args: any = yargs
Expand All @@ -12,10 +13,24 @@ async function main(): Promise<number> {
type: 'string',
description: 'Path to executable construct resource',
})
.option('app', { type: 'string', alias: 'a', default: 'cdk.out' })
.option('app', {
type: 'string',
alias: 'a',
default: 'cdk.out',
description: 'Path to your `cdk.out` cloud assembly directory',
})
.option('all', {
type: 'boolean',
description: 'Execute all matching resources',
})
.option('metadata', {
type: 'array',
alias: 'm',
description: 'Match resources with the given metadata key or key=value',
})
.option('input', {
type: 'string',
desc: 'Execute with custom JSON input',
description: 'Execute with custom JSON input',
}))
.argv;

Expand All @@ -25,6 +40,8 @@ async function main(): Promise<number> {
return cdkExec({
constructPath: args.path,
app: args.app,
all: args.all,
metadata: args.metadata ? new MetadataMatch(args.metadata) : undefined,
input: args.input,
});
}
Expand All @@ -35,50 +52,81 @@ export interface CdkExecOptions {
*/
readonly app: string;

/**
* Execute all matches rather than erroring on ambiguity
*/
readonly all: string;

/**
* Path of the construct to execute.
*/
readonly constructPath?: string;

/**
* Match records with the given metadata
*/
readonly metadata?: MetadataMatch;

/**
* Execution input.
*/
readonly input?: string;
}

export async function cdkExec(options: CdkExecOptions): Promise<number> {
const assembly = new cxapi.CloudAssembly(options.app);

try {
const executor = await Executor.find({
const assembly = new cxapi.CloudAssembly(options.app);

const executors = await Executor.find({
assembly,
constructPath: options.constructPath,
metadata: options.metadata,
sdk: new AwsSdk(),
});

if (!executor) {
if (executors.length === 0) {
console.log('❌ Could not find a construct at the provided path');
return 1;
}

console.log('✨ Executing %s', executor.physicalResourceId);
const result = await executor.execute(options.input);

if (result.output) {
console.log('\nOutput:\n%s', chalk.cyan(JSON.stringify(result.output, null, 2)));
if (!options.all && executors.length > 1) {
console.log('\n❌ Matched multiple resources: %s', executors.map(e => e.constructPath).join(', '));
return 1;
}

if (result.error) {
console.log('\n❌ Execution failed: %s', result.error);
return 1;
const executorResults = await Promise.all(
executors.map(async (executor) => {
console.log('⚡ Executing %s (%s)', executor.constructPath, executor.physicalResourceId);
const result = await executor.execute(options.input);
return {
executor,
...result,
};
}),
);

let error = false;
for (const result of executorResults) {
console.log('\n\n✨ Final status of %s', result.executor.constructPath);
if (result.output) {
console.log('\nOutput:\n%s', chalk.cyan(JSON.stringify(result.output, null, 2)));
}

if (result.error) {
error = true;
console.log('\n❌ Execution failed: %s', result.error);
} else {
console.log('\n✅ Execution succeeded');
}
}

console.log('\n✅ Execution succeeded');
return 0;
return error ? 1 : 0;
} catch (e) {
if (e instanceof AmbiguousPathError) {
console.log('\n❌ Matched multiple resources: %s', e.matchingPaths.join(', '));
return 1;
if (e instanceof Error) {
if (e.stack && /new CloudAssembly/.test(e.stack)) {
console.log('\n❌ AWS CDK lib error: %s', e.message);
return 1;
}
}

throw e;
Expand All @@ -92,4 +140,4 @@ main()
.catch(e => {
console.error(e);
process.exit(1);
});
});
96 changes: 63 additions & 33 deletions src/executor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as cxapi from 'aws-cdk-lib/cx-api';
import * as AWS from 'aws-sdk';
import { IAwsSdk, LazyListStackResources } from './aws-sdk';
import { findMatchingResources, MatchingResource } from './find-matching-resources';
import { findMatchingResources, MatchingResource, MetadataMatch } from './find-matching-resources';

const STATE_MACHINE_TYPE = 'AWS::StepFunctions::StateMachine';
const LAMBDA_TYPE = 'AWS::Lambda::Function';
Expand All @@ -10,6 +10,16 @@ const LAMBDA_TYPE = 'AWS::Lambda::Function';
* Options for `StateMachineExecutor`
*/
export interface ExecutorOptions {
/**
* The construct path of the matching resource.
*/
readonly constructPath: string;

/**
* The logical id of the matching resource.
*/
readonly logicalResourceId: string;

/**
* The physical resource of the resource to execute.
*/
Expand All @@ -23,14 +33,18 @@ export abstract class Executor {
/**
* Find an executor
*/
static async find(options: FindExecutorOptions): Promise<Executor | undefined> {
return findExecutor(options);
static async find(options: FindExecutorOptions): Promise<Executor[]> {
return findExecutors(options);
}

readonly constructPath: string;
readonly physicalResourceId: string;
readonly logicalResourceId: string;

protected constructor(options: ExecutorOptions) {
this.physicalResourceId = options.physicalResourceId;
this.constructPath = options.constructPath;
this.logicalResourceId = options.logicalResourceId;
}

/**
Expand Down Expand Up @@ -61,6 +75,11 @@ export interface FindExecutorOptions {
*/
readonly constructPath?: string;

/**
* Metadata of the resources to match.
*/
readonly metadata?: MetadataMatch;

/**
* AWS SDK
*/
Expand Down Expand Up @@ -200,51 +219,62 @@ function getLambdaErrorMessage(output: any) {
/**
* Finds an executor.
*/
async function findExecutor(options: FindExecutorOptions): Promise<Executor | undefined> {
const { assembly, constructPath, sdk } = options;
async function findExecutors(options: FindExecutorOptions): Promise<Executor[]> {
const { assembly, constructPath, sdk, metadata } = options;

const matchingResources = findMatchingResources({
assembly,
constructPath,
metadata,
types: [
STATE_MACHINE_TYPE,
LAMBDA_TYPE,
],
});

if (matchingResources.length === 0) {
return;
}
const lazyListStackResources: Record<string, LazyListStackResources> = {};
function getLazyListStackResources(matchingResource: MatchingResource) {
if (!lazyListStackResources[matchingResource.stackName]) {
lazyListStackResources[matchingResource.stackName] = new LazyListStackResources(sdk, matchingResource.stackName);
}

if (matchingResources.length > 1) {
throw new AmbiguousPathError(matchingResources);
return lazyListStackResources[matchingResource.stackName];
}

const [matchingResource] = matchingResources;
const listStackResources = new LazyListStackResources(sdk, matchingResource.stackName);
const stackResource = (await listStackResources.listStackResources())
.find(sr => sr.LogicalResourceId === matchingResource.logicalId);
return Promise.all(
matchingResources.map(async (matchingResource) => {
// Cache lazy lists
const listStackResources = getLazyListStackResources(matchingResource);

if (!stackResource || !stackResource.PhysicalResourceId) {
throw new Error(`Could not find the physical resource id for ${constructPath}`);
}
const stackResource = (await listStackResources.listStackResources())
.find(sr => sr.LogicalResourceId === matchingResource.logicalResourceId);

switch (stackResource.ResourceType) {
case STATE_MACHINE_TYPE:
return new StateMachineExecutor({
physicalResourceId: stackResource.PhysicalResourceId,
stepFunctions: sdk.stepFunctions(),
});

case LAMBDA_TYPE:
return new LambdaFunctionExecutor({
physicalResourceId: stackResource.PhysicalResourceId,
lambda: sdk.lambda(),
});

default:
throw new Error(`Unsupported resource type ${stackResource.ResourceType}`);
}
if (!stackResource || !stackResource.PhysicalResourceId) {
throw new Error(`Could not find the physical resource id for ${constructPath}`);
}

switch (stackResource.ResourceType) {
case STATE_MACHINE_TYPE:
return new StateMachineExecutor({
constructPath: matchingResource.constructPath,
logicalResourceId: matchingResource.logicalResourceId,
physicalResourceId: stackResource.PhysicalResourceId,
stepFunctions: sdk.stepFunctions(),
});

case LAMBDA_TYPE:
return new LambdaFunctionExecutor({
constructPath: matchingResource.constructPath,
logicalResourceId: matchingResource.logicalResourceId,
physicalResourceId: stackResource.PhysicalResourceId,
lambda: sdk.lambda(),
});

default:
throw new Error(`Unsupported resource type ${stackResource.ResourceType}`);
}
}),
);
}

/**
Expand Down
Loading

0 comments on commit 8c5b724

Please sign in to comment.