Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): introduce the 'watch' command #17240

Merged
merged 1 commit into from Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/aws-cdk/README.md
Expand Up @@ -372,6 +372,69 @@ For this reason, only use it for development purposes.
**⚠ Note #2**: This command is considered experimental,
and might have breaking changes in the future.

### `cdk watch`

The `watch` command is similar to `deploy`,
eladb marked this conversation as resolved.
Show resolved Hide resolved
but instead of being a one-shot operation,
the command continuously monitors the files of the project,
and triggers a deployment whenever it detects any changes:

```console
$ cdk watch DevelopmentStack
Detected change to 'lambda-code/index.js' (type: change). Triggering 'cdk deploy'
DevelopmentStack: deploying...

✅ DevelopmentStack

^C
```

To end a `cdk watch` session, interrupt the process by pressing Ctrl+C.

What files are observed is determined by the `"watch"` setting in your `cdk.json` file.
It has two sub-keys, `"include"` and `"exclude"`, each of which can be either a single string, or an array of strings.
Each entry is interpreted as a path relative to the location of the `cdk.json` file.
Globs, both `*` and `**`, are allowed to be used.
Example:

```json
{
"app": "mvn -e -q compile exec:java",
"watch": {
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
"include": "src/main/**",
"exclude": "target/*"
}
}
```

The default for `"include"` is `"**/*"`
(which means all files and directories in the root of the project),
and `"exclude"` is optional
(note that we always ignore files and directories starting with `.`,
the CDK output directory, and the `node_modules` directory),
so the minimal settings to enable `watch` are `"watch": {}`.

If either your CDK code, or application code, needs a build step before being deployed,
`watch` works with the `"build"` key in the `cdk.json` file,
for example:

```json
{
"app": "mvn -e -q exec:java",
"build": "mvn package",
"watch": {
"include": "src/main/**",
"exclude": "target/*"
}
}
```

Note that `watch` by default uses hotswap deployments (see above for details) --
to turn them off, pass the `--no-hotswap` option when invoking it.

**Note**: This command is considered experimental,
and might have breaking changes in the future.

### `cdk destroy`

Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were
Expand Down
59 changes: 59 additions & 0 deletions packages/aws-cdk/bin/cdk.ts
Expand Up @@ -118,6 +118,45 @@ async function parseCommandLineArguments() {
'which skips CloudFormation and updates the resources directly, ' +
'and falls back to a full deployment if that is not possible. ' +
'Do not use this in production environments',
})
.option('watch', {
type: 'boolean',
desc: 'Continuously observe the project files, ' +
'and deploy the given stack(s) automatically when changes are detected. ' +
'Implies --hotswap by default',
}),
)
.command('watch [STACKS..]', "Shortcut for 'deploy --watch'", yargs => yargs
// I'm fairly certain none of these options, present for 'deploy', make sense for 'watch':
// .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' })
eladb marked this conversation as resolved.
Show resolved Hide resolved
// .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined })
// @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment
// .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true })
// .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
// These options, however, are more subtle - I could be convinced some of these should also be available for 'watch':
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
// .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' })
// .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} })
// .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' })
// .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true })
// .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true })
.option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] })
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' })
.option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' })
.option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false })
.option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' })
.option('rollback', {
type: 'boolean',
desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " +
'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail',
})
// same hack for -R as above in 'deploy'
.option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true)
.option('hotswap', {
type: 'boolean',
desc: "Attempts to perform a 'hotswap' deployment, " +
'which skips CloudFormation and updates the resources directly, ' +
'and falls back to a full deployment if that is not possible. ' +
"'true' by default, use --no-hotswap to turn off",
}),
)
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
Expand Down Expand Up @@ -335,6 +374,26 @@ async function initCommandLine() {
ci: args.ci,
rollback: configuration.settings.get(['rollback']),
hotswap: args.hotswap,
watch: args.watch,
});

case 'watch':
return cli.watch({
selector,
// parameters: parameterMap,
// usePreviousParameters: args['previous-parameters'],
// outputsFile: configuration.settings.get(['outputsFile']),
// requireApproval: configuration.settings.get(['requireApproval']),
// notificationArns: args.notificationArns,
exclusively: args.exclusively,
toolkitStackName,
roleArn: args.roleArn,
reuseAssets: args['build-exclude'],
changeSetName: args.changeSetName,
force: args.force,
progress: configuration.settings.get(['progress']),
rollback: configuration.settings.get(['rollback']),
hotswap: args.hotswap,
});

case 'destroy':
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/cloudformation-deployments.ts
Expand Up @@ -142,7 +142,7 @@ export interface DeployStackOptions {
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
* and update the affected resources like Lambda functions directly.
*
* @default - false (do not perform a 'hotswap' deployment)
* @default - false for regular deployments, true for 'watch' deployments
*/
readonly hotswap?: boolean;
}
Expand Down
10 changes: 7 additions & 3 deletions packages/aws-cdk/lib/api/cxapp/cloud-executable.ts
Expand Up @@ -46,10 +46,14 @@ export class CloudExecutable {
}

/**
* Synthesize a set of stacks
* Synthesize a set of stacks.
*
* @param cacheCloudAssembly whether to cache the Cloud Assembly after it has been first synthesized.
* This is 'true' by default, and only set to 'false' for 'cdk watch',
* which needs to re-synthesize the Assembly each time it detects a change to the project files
*/
public async synthesize(): Promise<CloudAssembly> {
if (!this._cloudAssembly) {
public async synthesize(cacheCloudAssembly: boolean = true): Promise<CloudAssembly> {
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
if (!this._cloudAssembly || !cacheCloudAssembly) {
this._cloudAssembly = await this.doSynthesize();
}
return this._cloudAssembly;
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/deploy-stack.ts
Expand Up @@ -179,7 +179,7 @@ export interface DeployStackOptions {
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
* and update the affected resources like Lambda functions directly.
*
* @default - false (do not perform a 'hotswap' deployment)
* @default - false for regular deployments, true for 'watch' deployments
*/
readonly hotswap?: boolean;
}
Expand Down