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

refactor(cli): docs, doctor, context commands do not use configuration directly anymore #32578

Merged
merged 10 commits into from
Dec 20, 2024
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ component_management:
# https://docs.codecov.com/docs/ignoring-paths
ignore:
- packages/aws-cdk/lib/parse-command-line-arguments.ts # this file is generated and some lines cannot be tested
- packages/aws-cdk/lib/cli.ts # we integ test this file
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/cli-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,7 +1224,7 @@ export interface ContextOptions {
/**
* Clear all context
*
* @default - undefined
* @default - false
*/
readonly clear?: boolean;
}
Expand Down
23 changes: 12 additions & 11 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { Deployments } from '../lib/api/deployments';
import { PluginHost } from '../lib/api/plugin';
import { ToolkitInfo } from '../lib/api/toolkit-info';
import { CdkToolkit, AssetBuildTime } from '../lib/cdk-toolkit';
import { realHandler as context } from '../lib/commands/context';
import { realHandler as docs } from '../lib/commands/docs';
import { realHandler as doctor } from '../lib/commands/doctor';
import { contextHandler as context } from '../lib/commands/context';
import { docs } from '../lib/commands/docs';
import { doctor } from '../lib/commands/doctor';
import { getMigrateScanType } from '../lib/commands/migrate';
import { cliInit, printAvailableTemplates } from '../lib/init';
import { data, debug, error, print, setCI, setLogLevel, LogLevel } from '../lib/logging';
Expand All @@ -37,7 +37,6 @@ if (!process.stdout.isTTY) {
}

export async function exec(args: string[], synthesizer?: Synthesizer): Promise<number | void> {

const argv = await parseCommandLineArguments(args);

// if one -v, log at a DEBUG level
Expand Down Expand Up @@ -154,9 +153,6 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
throw new Error(`First argument should be a string. Got: ${cmd} (${typeof(cmd)})`);
}

// Bundle up global objects so the commands have access to them
const commandOptions = { args: argv, configuration, aws: sdkProvider };

try {
return await main(cmd, argv);
} finally {
Expand All @@ -174,7 +170,6 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
await notices.refresh();
notices.display();
}

}

async function main(command: string, args: any): Promise<number | void> {
Expand Down Expand Up @@ -207,13 +202,19 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n

switch (command) {
case 'context':
return context(commandOptions);
return context({
context: configuration.context,
clear: argv.clear,
json: argv.json,
force: argv.force,
reset: argv.reset,
});

case 'docs':
return docs(commandOptions);
return docs({ browser: configuration.settings.get(['browser']) });

case 'doctor':
return doctor(commandOptions);
return doctor();

case 'ls':
case 'list':
Expand Down
26 changes: 0 additions & 26 deletions packages/aws-cdk/lib/command-api.ts

This file was deleted.

64 changes: 52 additions & 12 deletions packages/aws-cdk/lib/commands/context.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,64 @@
import * as chalk from 'chalk';
import { minimatch } from 'minimatch';
import * as version from '../../lib/version';
import { CommandOptions } from '../command-api';
import { print, error, warning } from '../logging';
import { Context, PROJECT_CONFIG, PROJECT_CONTEXT, USER_DEFAULTS } from '../settings';
import { renderTable } from '../util';

export async function realHandler(options: CommandOptions): Promise<number> {
const { configuration, args } = options;
if (args.clear) {
configuration.context.clear();
await configuration.saveContext();
/**
* Options for the context command
*/
export interface ContextOptions {
/**
* The context object sourced from all context locations
*/
context: Context;

/**
* The context key (or its index) to reset
*
* @default undefined
*/
reset?: string;

/**
* Ignore missing key error
*
* @default false
*/
force?: boolean;

/**
* Clear all context
*
* @default false
*/
clear?: boolean;

/**
* Use JSON output instead of YAML when templates are printed to STDOUT
*
* @default false
*/
json?: boolean;
}

export async function contextHandler(options: ContextOptions): Promise<number> {
if (options.clear) {
options.context.clear();
await options.context.save(PROJECT_CONTEXT);
print('All context values cleared.');
} else if (args.reset) {
invalidateContext(configuration.context, args.reset as string, args.force as boolean);
await configuration.saveContext();
} else if (options.reset) {
invalidateContext(options.context, options.reset, options.force ?? false);
await options.context.save(PROJECT_CONTEXT);
} else {
// List -- support '--json' flag
if (args.json) {
const contextValues = configuration.context.all;
if (options.json) {
/* istanbul ignore next */
const contextValues = options.context.all;
process.stdout.write(JSON.stringify(contextValues, undefined, 2));
} else {
listContext(configuration.context);
listContext(options.context);
}
}
await version.displayVersionMessage();
Expand Down Expand Up @@ -105,13 +142,15 @@ function invalidateContext(context: Context, key: string, force: boolean) {
throw new Error(`No context value matching key: ${key}`);
}
}

function printUnset(unset: string[]) {
if (unset.length === 0) return;
print('The following matched context values reset. They will be refreshed on next synthesis');
unset.forEach((match) => {
print(' %s', match);
});
}

function printReadonly(readonly: string[]) {
if (readonly.length === 0) return;
warning('The following matched context values could not be reset through the CLI');
Expand All @@ -121,6 +160,7 @@ function printReadonly(readonly: string[]) {
print('');
print('This usually means they are configured in %s or %s', chalk.blue(PROJECT_CONFIG), chalk.blue(USER_DEFAULTS));
}

function keysByExpression(context: Context, expression: string) {
return context.keys.filter(minimatch.filter(expression));
}
Expand Down
15 changes: 12 additions & 3 deletions packages/aws-cdk/lib/commands/docs.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import * as childProcess from 'child_process';
import * as chalk from 'chalk';
import { debug, print, warning } from '../../lib/logging';
import { CommandOptions } from '../command-api';

export const command = 'docs';
export const describe = 'Opens the reference documentation in a browser';
export const aliases = ['doc'];

export async function realHandler(options: CommandOptions): Promise<number> {
/**
* Options for the docs command
*/
export interface DocsOptions {
/**
* The command to use to open the browser
*/
browser: string;
}

export async function docs(options: DocsOptions): Promise<number> {
const url = 'https://docs.aws.amazon.com/cdk/api/v2/';
print(chalk.green(url));
const browserCommand = (options.args.browser as string).replace(/%u/g, url);
const browserCommand = (options.browser).replace(/%u/g, url);
debug(`Opening documentation ${chalk.green(browserCommand)}`);
return new Promise<number>((resolve, _reject) => {
childProcess.exec(browserCommand, (err, stdout, stderr) => {
Expand Down
3 changes: 1 addition & 2 deletions packages/aws-cdk/lib/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import * as cxapi from '@aws-cdk/cx-api';
import * as chalk from 'chalk';
import { print } from '../../lib/logging';
import * as version from '../../lib/version';
import { CommandOptions } from '../command-api';

export async function realHandler(_options: CommandOptions): Promise<number> {
export async function doctor(): Promise<number> {
let exitStatus: number = 0;
for (const verification of verifications) {
if (!await verification()) {
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,9 @@ export async function makeConfig(): Promise<CliConfig> {
context: {
description: 'Manage cached context values',
options: {
reset: { alias: 'e', desc: 'The context key (or its index) to reset', type: 'string', requiresArg: true },
reset: { alias: 'e', desc: 'The context key (or its index) to reset', type: 'string', requiresArg: true, default: undefined },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: we should probably change the codegen to always add an explicit default if none is provied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

force: { alias: 'f', desc: 'Ignore missing key error', type: 'boolean', default: false },
clear: { desc: 'Clear all context', type: 'boolean' },
clear: { desc: 'Clear all context', type: 'boolean', default: false },
},
},
docs: {
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ export function parseCommandLineArguments(args: Array<string>): any {
desc: 'The context key (or its index) to reset',
type: 'string',
requiresArg: true,
default: undefined,
})
.option('force', {
alias: 'f',
Expand All @@ -752,6 +753,7 @@ export function parseCommandLineArguments(args: Array<string>): any {
.option('clear', {
desc: 'Clear all context',
type: 'boolean',
default: false,
})
)
.command(['docs', 'doc'], 'Opens the reference documentation in a browser', (yargs: Argv) =>
Expand Down
49 changes: 42 additions & 7 deletions packages/aws-cdk/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class Configuration {
return this._projectConfig;
}

private get projectContext() {
public get projectContext() {
if (!this._projectContext) {
throw new Error('#load has not been called yet!');
}
Expand All @@ -121,12 +121,12 @@ export class Configuration {
}

const contextSources = [
this.commandLineContext,
this.projectConfig.subSettings([CONTEXT_KEY]).makeReadOnly(),
this.projectContext,
{ bag: this.commandLineContext },
{ fileName: PROJECT_CONFIG, bag: this.projectConfig.subSettings([CONTEXT_KEY]).makeReadOnly() },
{ fileName: PROJECT_CONTEXT, bag: this.projectContext },
];
if (readUserContext) {
contextSources.push(userConfig.subSettings([CONTEXT_KEY]).makeReadOnly());
contextSources.push({ fileName: USER_DEFAULTS, bag: userConfig.subSettings([CONTEXT_KEY]).makeReadOnly() });
}

this.context = new Context(...contextSources);
Expand Down Expand Up @@ -166,6 +166,19 @@ async function loadAndLog(fileName: string): Promise<Settings> {
return ret;
}

interface ContextBag {
/**
* The file name of the context. Will be used to potentially
* save new context back to the original file.
*/
fileName?: string;

/**
* The context values.
*/
bag: Settings;
}

/**
* Class that supports overlaying property bags
*
Expand All @@ -176,9 +189,11 @@ async function loadAndLog(fileName: string): Promise<Settings> {
*/
export class Context {
private readonly bags: Settings[];
private readonly fileNames: (string|undefined)[];

constructor(...bags: Settings[]) {
this.bags = bags.length > 0 ? bags : [new Settings()];
constructor(...bags: ContextBag[]) {
this.bags = bags.length > 0 ? bags.map(b => b.bag) : [new Settings()];
this.fileNames = bags.length > 0 ? bags.map(b => b.fileName) : ['default'];
}

public get keys(): string[] {
Expand Down Expand Up @@ -227,6 +242,26 @@ export class Context {
this.unset(key);
}
}

/**
* Save a specific context file
*/
public async save(fileName: string): Promise<this> {
const index = this.fileNames.indexOf(fileName);

// File not found, don't do anything in this scenario
if (index === -1) {
return this;
}

const bag = this.bags[index];
if (bag.readOnly) {
throw new Error(`Context file ${fileName} is read only!`);
}

await bag.save(fileName);
return this;
}
}

/**
Expand Down
12 changes: 3 additions & 9 deletions packages/aws-cdk/test/cdk-docs.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
import * as child_process from 'child_process';
import { mocked } from 'jest-mock';
import { CommandHandler } from '../lib/command-api';
import { realHandler } from '../lib/commands/docs';
const argv = {
browser: 'echo %u',
commandHandler: undefined as (CommandHandler | undefined),
};
import { docs } from '../lib/commands/docs';

// eslint-disable-next-line no-console
console.log = jest.fn();
jest.mock('child_process');

describe('`cdk docs`', () => {

test('exits with 0 when everything is OK', async () => {
const mockChildProcessExec: any = (_: string, cb: (err?: Error, stdout?: string, stderr?: string) => void) => cb();
mocked(child_process.exec).mockImplementation(mockChildProcessExec);

const result = await realHandler({ args: argv } as any);
const result = await docs({ browser: 'echo %u' });
expect(result).toBe(0);
});

test('exits with 0 when opening the browser fails', async () => {
const mockChildProcessExec: any = (_: string, cb: (err: Error, stdout?: string, stderr?: string) => void) => cb(new Error('TEST'));
mocked(child_process.exec).mockImplementation(mockChildProcessExec);

const result = await realHandler({ args: argv } as any);
const result = await docs({ browser: 'echo %u' });
expect(result).toBe(0);
});
});
Loading
Loading