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): glob-style key matching to context --reset #19840

Merged
merged 2 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 70 additions & 10 deletions packages/aws-cdk/lib/commands/context.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as chalk from 'chalk';
import * as minimatch from 'minimatch';
import * as version from '../../lib/version';
import { CommandOptions } from '../command-api';
import { print } from '../logging';
import { Context, PROJECT_CONFIG } from '../settings';
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();
Expand Down Expand Up @@ -48,9 +48,8 @@ function listContext(context: Context) {
const jsonWithoutNewlines = JSON.stringify(context.all[key], undefined, 2).replace(/\s+/g, ' ');
data.push([i, key, jsonWithoutNewlines]);
}

print(`Context found in ${chalk.blue(PROJECT_CONFIG)}:\n`);

print('Context found in %s:', chalk.blue(PROJECT_CONFIG));
print('');
print(renderTable(data, process.stdout.columns));

// eslint-disable-next-line max-len
Expand All @@ -63,14 +62,75 @@ function invalidateContext(context: Context, key: string) {
// was a number and we fully parsed it.
key = keyByNumber(context, i);
}

// Unset!
if (context.has(key)) {
context.unset(key);
print(`Context value ${chalk.blue(key)} reset. It will be refreshed on next synthesis`);
} else {
print(`No context value with key ${chalk.blue(key)}`);
// check if the value was actually unset.
if (!context.has(key)) {
print('Context value %s reset. It will be refreshed on next synthesis', chalk.blue(key));
return;
}

// Value must be in readonly bag
error('Only context values specified in %s can be reset through the CLI', chalk.blue(PROJECT_CONTEXT));
throw new Error(`Cannot reset readonly context value with key: ${key}`);
}

// check if value is expression matching keys
const matches = keysByExpression(context, key);

if (matches.length > 0) {

matches.forEach((match) => {
context.unset(match);
});

const { unset, readonly } = getUnsetAndReadonly(context, matches);

// output the reset values
printUnset(unset);

// warn about values not reset
printReadonly(readonly);

// throw when none of the matches were reset
if (unset.length === 0) {
throw new Error('None of the matched context values could be reset');
}
return;
}

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');
readonly.forEach((match) => {
print(' %s', match);
});
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));
}

function getUnsetAndReadonly(context: Context, matches: string[]) {
return matches.reduce<{ unset: string[], readonly: string[] }>((acc, match) => {
if (context.has(match)) {
acc.readonly.push(match);
} else {
acc.unset.push(match);
}
return acc;
}, { unset: [], readonly: [] });
}

function keyByNumber(context: Context, n: number) {
Expand Down
249 changes: 203 additions & 46 deletions packages/aws-cdk/test/commands/context-command.test.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,221 @@
import { realHandler } from '../../lib/commands/context';
import { Configuration } from '../../lib/settings';
import { Configuration, Settings, Context } from '../../lib/settings';

test('context list', async() => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');
describe('context --list', () => {
test('runs', async() => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');

expect(configuration.context.all).toEqual({
foo: 'bar',
});
expect(configuration.context.all).toEqual({
foo: 'bar',
});

// WHEN
await realHandler({
configuration,
args: {},
} as any);
// WHEN
await realHandler({
configuration,
args: {},
} as any);
});
});

test('context reset can remove a context key', async () => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');
configuration.context.set('baz', 'quux');
describe('context --reset', () => {
test('can remove a context key', async () => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');
configuration.context.set('baz', 'quux');

expect(configuration.context.all).toEqual({
foo: 'bar',
baz: 'quux',
});

expect(configuration.context.all).toEqual({
foo: 'bar',
baz: 'quux',
// WHEN
await realHandler({
configuration,
args: { reset: 'foo' },
} as any);

// THEN
expect(configuration.context.all).toEqual({
baz: 'quux',
});
});

// WHEN
await realHandler({
configuration,
args: { reset: 'foo' },
} as any);
test('can remove a context key using number', async () => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');
configuration.context.set('baz', 'quux');

expect(configuration.context.all).toEqual({
foo: 'bar',
baz: 'quux',
});

// THEN
expect(configuration.context.all).toEqual({
baz: 'quux',
// WHEN
await realHandler({
configuration,
args: { reset: '1' },
} as any);

// THEN
expect(configuration.context.all).toEqual({
foo: 'bar',
});
});
});

test('context reset can remove a context key using number', async () => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');
configuration.context.set('baz', 'quux');

expect(configuration.context.all).toEqual({
foo: 'bar',
baz: 'quux',
test('can reset matched pattern', async () => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');
configuration.context.set('match-a', 'baz');
configuration.context.set('match-b', 'qux');

expect(configuration.context.all).toEqual({
'foo': 'bar',
'match-a': 'baz',
'match-b': 'qux',
});

// WHEN
await realHandler({
configuration,
args: { reset: 'match-*' },
} as any);

// THEN
expect(configuration.context.all).toEqual({
foo: 'bar',
});
});

// WHEN
await realHandler({
configuration,
args: { reset: '1' },
} as any);

// THEN
expect(configuration.context.all).toEqual({
foo: 'bar',
test('prefers an exact match', async () => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');
configuration.context.set('fo*', 'baz');

expect(configuration.context.all).toEqual({
'foo': 'bar',
'fo*': 'baz',
});

// WHEN
await realHandler({
configuration,
args: { reset: 'fo*' },
} as any);

// THEN
expect(configuration.context.all).toEqual({
foo: 'bar',
});
});


test('doesn\'t throw when at least one match is reset', async () => {
// GIVEN
const configuration = new Configuration();
const readOnlySettings = new Settings({
'foo': 'bar',
'match-a': 'baz',
}, true);
configuration.context = new Context(readOnlySettings, new Settings());
configuration.context.set('match-b', 'quux');

// When
await expect(realHandler({
configuration,
args: { reset: 'match-*' },
} as any));

// Then
expect(configuration.context.all).toEqual({
'foo': 'bar',
'match-a': 'baz',
});
});

test('throws when key not found', async () => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');

expect(configuration.context.all).toEqual({
foo: 'bar',
});

// THEN
await expect(realHandler({
configuration,
args: { reset: 'baz' },
} as any)).rejects.toThrow(/No context value matching key/);
});


test('throws when no key of index found', async () => {
// GIVEN
const configuration = new Configuration();
configuration.context.set('foo', 'bar');

expect(configuration.context.all).toEqual({
foo: 'bar',
});

// THEN
await expect(realHandler({
configuration,
args: { reset: '2' },
} as any)).rejects.toThrow(/No context key with number/);
});


test('throws when resetting read-only values', async () => {
// GIVEN
const configuration = new Configuration();
const readOnlySettings = new Settings({
foo: 'bar',
}, true);
configuration.context = new Context(readOnlySettings);

expect(configuration.context.all).toEqual({
foo: 'bar',
});

// THEN
await expect(realHandler({
configuration,
args: { reset: 'foo' },
} as any)).rejects.toThrow(/Cannot reset readonly context value with key/);
});


test('throws when no matches could be reset', async () => {
// GIVEN
const configuration = new Configuration();
const readOnlySettings = new Settings({
'foo': 'bar',
'match-a': 'baz',
'match-b': 'quux',
}, true);
configuration.context = new Context(readOnlySettings);

expect(configuration.context.all).toEqual({
'foo': 'bar',
'match-a': 'baz',
'match-b': 'quux',
});

// THEN
await expect(realHandler({
configuration,
args: { reset: 'match-*' },
} as any)).rejects.toThrow(/None of the matched context values could be reset/);
});

});