Skip to content

Commit

Permalink
feat(integ-runner): integ-runner --watch (#26087)
Browse files Browse the repository at this point in the history
This PR adds a new option `--watch` that runs a single integration test in watch mode. See README for more details

- Full deploy
![watch-demo1](https://github.com/aws/aws-cdk/assets/43035978/2c1af717-acec-4761-8a1e-2362f5c8bc89)

- Deploy without logs
![watch-demo2](https://github.com/aws/aws-cdk/assets/43035978/b859f1d3-634c-44dc-bd3d-54a0e48dc9a1)

- Deploy on file change
![watch-demo3](https://github.com/aws/aws-cdk/assets/43035978/b283ddd5-7e33-4c61-8477-12f25f4996ee)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
corymhall committed Jun 23, 2023
1 parent 3b6143b commit 1fe2f09
Show file tree
Hide file tree
Showing 16 changed files with 912 additions and 41 deletions.
55 changes: 53 additions & 2 deletions packages/@aws-cdk/cdk-cli-wrapper/lib/cdk-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DefaultCdkOptions, DeployOptions, DestroyOptions, SynthOptions, ListOptions, StackActivityProgress } from './commands';
import { exec } from './utils';
import { ChildProcess } from 'child_process';
import { DefaultCdkOptions, DeployOptions, DestroyOptions, SynthOptions, ListOptions, StackActivityProgress, HotswapMode } from './commands';
import { exec, watch } from './utils';

/**
* AWS CDK CLI operations
Expand Down Expand Up @@ -30,6 +31,11 @@ export interface ICdk {
* cdk synth fast
*/
synthFast(options: SynthFastOptions): void;

/**
* cdk watch
*/
watch(options: DeployOptions): ChildProcess;
}

/**
Expand Down Expand Up @@ -176,6 +182,7 @@ export class CdkCliWrapper implements ICdk {
...options.changeSetName ? ['--change-set-name', options.changeSetName] : [],
...options.toolkitStackName ? ['--toolkit-stack-name', options.toolkitStackName] : [],
...options.progress ? ['--progress', options.progress] : ['--progress', StackActivityProgress.EVENTS],
...options.deploymentMethod ? ['--method', options.deploymentMethod] : [],
...this.createDefaultArguments(options),
];

Expand All @@ -186,6 +193,50 @@ export class CdkCliWrapper implements ICdk {
});
}

public watch(options: DeployOptions): ChildProcess {
let hotswap: string;
switch (options.hotswap) {
case HotswapMode.FALL_BACK:
hotswap = '--hotswap-fallback';
break;
case HotswapMode.HOTSWAP_ONLY:
hotswap = '--hotswap';
break;
default:
hotswap = '--hotswap-fallback';
break;
}
const deployCommandArgs: string[] = [
'--watch',
...renderBooleanArg('ci', options.ci),
...renderBooleanArg('execute', options.execute),
...renderBooleanArg('exclusively', options.exclusively),
...renderBooleanArg('force', options.force),
...renderBooleanArg('previous-parameters', options.usePreviousParameters),
...renderBooleanArg('rollback', options.rollback),
...renderBooleanArg('staging', options.staging),
...renderBooleanArg('logs', options.traceLogs),
hotswap,
...options.reuseAssets ? renderArrayArg('--reuse-assets', options.reuseAssets) : [],
...options.notificationArns ? renderArrayArg('--notification-arns', options.notificationArns) : [],
...options.parameters ? renderMapArrayArg('--parameters', options.parameters) : [],
...options.outputsFile ? ['--outputs-file', options.outputsFile] : [],
...options.requireApproval ? ['--require-approval', options.requireApproval] : [],
...options.changeSetName ? ['--change-set-name', options.changeSetName] : [],
...options.toolkitStackName ? ['--toolkit-stack-name', options.toolkitStackName] : [],
...options.progress ? ['--progress', options.progress] : ['--progress', StackActivityProgress.EVENTS],
...options.deploymentMethod ? ['--method', options.deploymentMethod] : [],
...this.createDefaultArguments(options),
];

return watch([this.cdk, 'deploy', ...deployCommandArgs], {
cwd: this.directory,
verbose: this.showOutput,
env: this.env,
});

}

/**
* cdk destroy
*/
Expand Down
47 changes: 47 additions & 0 deletions packages/@aws-cdk/cdk-cli-wrapper/lib/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,53 @@ export interface DeployOptions extends DefaultCdkOptions {
* @default StackActivityProgress.EVENTS
*/
readonly progress?: StackActivityProgress;

/**
* Whether this 'deploy' command should actually delegate to the 'watch' command.
*
* @default false
*/
readonly watch?: boolean;

/**
* Whether to perform a 'hotswap' deployment.
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
* and update the affected resources like Lambda functions directly.
*
* @default - `HotswapMode.FALL_BACK` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments
*/
readonly hotswap?: HotswapMode;

/**
* Whether to show CloudWatch logs for hotswapped resources
* locally in the users terminal
*
* @default - false
*/
readonly traceLogs?: boolean;

/**
* Deployment method
*/
readonly deploymentMethod?: DeploymentMethod;
}
export type DeploymentMethod = 'direct' | 'change-set';

export enum HotswapMode {
/**
* Will fall back to CloudFormation when a non-hotswappable change is detected
*/
FALL_BACK = 'fall-back',

/**
* Will not fall back to CloudFormation when a non-hotswappable change is detected
*/
HOTSWAP_ONLY = 'hotswap-only',

/**
* Will not attempt to hotswap anything and instead go straight to CloudFormation
*/
FULL_DEPLOYMENT = 'full-deployment',
}

/**
Expand Down
21 changes: 20 additions & 1 deletion packages/@aws-cdk/cdk-cli-wrapper/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Helper functions for CDK Exec
import { spawnSync } from 'child_process';
import { spawn, spawnSync } from 'child_process';

/**
* Our own execute function which doesn't use shells and strings.
Expand Down Expand Up @@ -37,3 +37,22 @@ export function exec(commandLine: string[], options: { cwd?: string, json?: bool
throw new Error('Command output is not JSON');
}
}

/**
* For use with `cdk deploy --watch`
*/
export function watch(commandLine: string[], options: { cwd?: string, verbose?: boolean, env?: any } = { }) {
const proc = spawn(commandLine[0], commandLine.slice(1), {
stdio: ['ignore', 'pipe', options.verbose ? 'inherit' : 'pipe'], // inherit STDERR in verbose mode
env: {
...process.env,
...options.env,
},
cwd: options.cwd,
});
proc.on('error', (err: Error) => {
throw err;
});

return proc;
}
34 changes: 33 additions & 1 deletion packages/@aws-cdk/cdk-cli-wrapper/test/cdk-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as child_process from 'child_process';
import { CdkCliWrapper } from '../lib/cdk-wrapper';
import { RequireApproval, StackActivityProgress } from '../lib/commands';
let spawnSyncMock: jest.SpyInstance;
let spawnMock: jest.SpyInstance;

beforeEach(() => {
spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValue({
Expand All @@ -12,6 +13,11 @@ beforeEach(() => {
output: ['stdout', 'stderr'],
signal: null,
});
spawnMock = jest.spyOn(child_process, 'spawn').mockImplementation(jest.fn(() => {
return {
on: jest.fn(() => {}),
} as unknown as child_process.ChildProcess;
}));
});

afterEach(() => {
Expand Down Expand Up @@ -317,7 +323,33 @@ test('default synth', () => {
);
});

test('synth arguments', () => {
test('watch arguments', () => {
// WHEN
const cdk = new CdkCliWrapper({
directory: '/project',
env: {
KEY: 'value',
},
});
cdk.watch({
app: 'node bin/my-app.js',
stacks: ['test-stack1'],
});

// THEN
expect(spawnMock).toHaveBeenCalledWith(
expect.stringMatching(/cdk/),
['deploy', '--watch', '--hotswap-fallback', '--progress', 'events', '--app', 'node bin/my-app.js', 'test-stack1'],
expect.objectContaining({
env: expect.objectContaining({
KEY: 'value',
}),
cwd: '/project',
}),
);
});

test('destroy arguments', () => {
// WHEN
const cdk = new CdkCliWrapper({
directory: '/project',
Expand Down
54 changes: 54 additions & 0 deletions packages/@aws-cdk/integ-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ to be a self contained CDK app. The runner will execute the following for each f
- `--test-regex`
Detect integration test files matching this JavaScript regex pattern. If used multiple times, all files matching any one of the patterns are detected.

- `--watch`
Run a single integration test in watch mode. In watch mode the integ-runner
will not save any snapshots.

Use together with `--app` to fully customize how tests are run, or use with a single `--language` preset to change which files are detected for this language.
- `--language`
The language presets to use. You can discover and run tests written in multiple languages by passing this flag multiple times (`--language typescript --language python`). Defaults to all supported languages. Currently supported language presets are:
Expand Down Expand Up @@ -221,6 +225,56 @@ integ-runner --update-on-failed --disable-update-workflow integ.new-test.js

This is because for a new test we do not need to test the update workflow (there is nothing to update).

### watch

It can be useful to run an integration test in watch mode when you are iterating
on a specific test.

```console
integ-runner integ.new-test.js --watch
```

In watch mode the integ test will run similar to `cdk deploy --watch` with the
addition of also displaying the assertion results. By default the output will
only show the assertion results.

- To show the console output from watch run with `-v`
- To also stream the CloudWatch logs (i.e. `cdk deploy --watch --logs`) run with `-vv`

When running in watch mode most of the integ-runner functionality will be turned
off.

- Snapshots will not be created
- Update workflow will not be run
- Stacks will not be cleaned up (you must manually clean up the stacks)
- Only a single test can be run

Once you are done iterating using watch and want to create the snapshot you can
run the integ test like normal to create the snapshot and clean up the test.

#### cdk.context.json

cdk watch depends on a `cdk.context.json` file existing with a `watch` key. The
integ-runner will create a default `cdk.context.json` file if one does not
exist.

```json
{
"watch": {}
}
```

You can further edit this file after it is created and add additional `watch`
fields. For example:

```json
{
"watch": {
"include": ["**/*.js"]
}
}
```

### integ.json schema

See [@aws-cdk/cloud-assembly-schema/lib/integ-tests/schema.ts](../cloud-assembly-schema/lib/integ-tests/schema.ts)
Expand Down
Loading

0 comments on commit 1fe2f09

Please sign in to comment.