Skip to content

Commit 5128946

Browse files
vivian12300rix0rrr
andauthored
fix(cli): flags command shows unnecessary flags, not all table rows have vertical lines (#796)
When a user runs `cdk flags`, a table is displayed with information about their feature flags. This PR omits the intentionally unconfigured feature flags from the table and makes the table more visually appealing. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Co-authored-by: Rico Hermans <[email protected]>
1 parent ad1a7ce commit 5128946

File tree

4 files changed

+165
-64
lines changed

4 files changed

+165
-64
lines changed

packages/@aws-cdk/cloudformation-diff/lib/format-table.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import * as table from 'table';
77
*
88
* First row is considered the table header.
99
*/
10-
export function formatTable(cells: string[][], columns: number | undefined): string {
10+
export function formatTable(cells: string[][], columns: number | undefined, noHorizontalLines?: boolean): string {
1111
return table.table(cells, {
1212
border: TABLE_BORDER_CHARACTERS,
1313
columns: buildColumnConfig(columns !== undefined ? calculateColumnWidths(cells, columns) : undefined),
1414
drawHorizontalLine: (line) => {
1515
// Numbering like this: [line 0] [header = row[0]] [line 1] [row 1] [line 2] [content 2] [line 3]
16+
if (noHorizontalLines) {
17+
return line < 2 || line === cells.length;
18+
}
1619
return (line < 2 || line === cells.length) || lineBetween(cells[line - 1], cells[line]);
1720
},
1821
}).trimRight();

packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,29 +1276,46 @@ export class Toolkit extends CloudAssemblySourceBuilder {
12761276
/**
12771277
* Retrieve feature flag information from the cloud assembly
12781278
*/
1279-
12801279
public async flags(cx: ICloudAssemblySource): Promise<FeatureFlag[]> {
12811280
this.requireUnstableFeature('flags');
12821281

12831282
const ioHelper = asIoHelper(this.ioHost, 'flags');
12841283
await using assembly = await assemblyFromSource(ioHelper, cx);
1285-
const artifacts = assembly.cloudAssembly.manifest.artifacts;
1284+
const artifacts = Object.values(assembly.cloudAssembly.manifest.artifacts ?? {});
1285+
const featureFlagReports = artifacts.filter(a => a.type === ArtifactType.FEATURE_FLAG_REPORT);
1286+
1287+
const flags = featureFlagReports.flatMap(report => {
1288+
const properties = report.properties as FeatureFlagReportProperties;
1289+
const moduleName = properties.module;
1290+
1291+
const flagsWithUnconfiguredBehavesLike = Object.entries(properties.flags)
1292+
.filter(([_, flagInfo]) => flagInfo.unconfiguredBehavesLike != undefined);
12861293

1287-
return Object.values(artifacts!)
1288-
.filter(a => a.type === ArtifactType.FEATURE_FLAG_REPORT)
1289-
.flatMap(report => {
1290-
const properties = report.properties as FeatureFlagReportProperties;
1291-
const moduleName = properties.module;
1294+
const shouldIncludeUnconfiguredBehavesLike = flagsWithUnconfiguredBehavesLike.length > 0;
12921295

1293-
return Object.entries(properties.flags).map(([flagName, flagInfo]) => ({
1296+
return Object.entries(properties.flags).map(([flagName, flagInfo]) => {
1297+
const baseFlag = {
12941298
module: moduleName,
12951299
name: flagName,
12961300
recommendedValue: flagInfo.recommendedValue,
12971301
userValue: flagInfo.userValue ?? undefined,
12981302
explanation: flagInfo.explanation ?? '',
1299-
unconfiguredBehavesLike: flagInfo.unconfiguredBehavesLike,
1300-
}));
1303+
};
1304+
1305+
if (shouldIncludeUnconfiguredBehavesLike) {
1306+
return {
1307+
...baseFlag,
1308+
unconfiguredBehavesLike: {
1309+
v2: flagInfo.unconfiguredBehavesLike?.v2 ?? false,
1310+
},
1311+
};
1312+
}
1313+
1314+
return baseFlag;
13011315
});
1316+
});
1317+
1318+
return flags;
13021319
}
13031320

13041321
private requireUnstableFeature(requestedFeature: UnstableFeature) {

packages/aws-cdk/lib/commands/flag-operations.ts

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as path from 'path';
2+
import { formatTable } from '@aws-cdk/cloudformation-diff';
23
import type { FeatureFlag, Toolkit } from '@aws-cdk/toolkit-lib';
34
import { CdkAppMultiContext, MemoryContext, DiffMethod } from '@aws-cdk/toolkit-lib';
45
import * as chalk from 'chalk';
@@ -92,7 +93,7 @@ export async function handleFlags(flagData: FeatureFlag[], ioHelper: IoHelper, o
9293
default: true,
9394
unconfigured: true,
9495
};
95-
await setMultipleFlags(params);
96+
await setMultipleFlagsIfSupported(params);
9697
} else if (answer == FlagsMenuOptions.MODIFY_SPECIFIC_FLAG) {
9798
await setFlag(params, true);
9899
} else if (answer == FlagsMenuOptions.EXIT) {
@@ -172,8 +173,7 @@ export async function handleFlags(flagData: FeatureFlag[], ioHelper: IoHelper, o
172173
}
173174

174175
if (options.set && options.all && options.default) {
175-
await setMultipleFlags(params);
176-
return;
176+
await setMultipleFlagsIfSupported(params);
177177
}
178178

179179
if (options.set && options.unconfigured && options.recommended) {
@@ -182,9 +182,20 @@ export async function handleFlags(flagData: FeatureFlag[], ioHelper: IoHelper, o
182182
}
183183

184184
if (options.set && options.unconfigured && options.default) {
185+
await setMultipleFlagsIfSupported(params);
186+
}
187+
}
188+
189+
/**
190+
* Sets flag configurations to default values if `unconfiguredBehavesLike` is populated
191+
*/
192+
async function setMultipleFlagsIfSupported(params: FlagOperationsParams) {
193+
const { flagData, ioHelper } = params;
194+
if (flagData[0].unconfiguredBehavesLike) {
185195
await setMultipleFlags(params);
186196
return;
187197
}
198+
await ioHelper.defaults.error('The --default options are not compatible with the AWS CDK library used by your application. Please upgrade to 2.212.0 or above.');
188199
}
189200

190201
async function setFlag(params: FlagOperationsParams, interactive?: boolean) {
@@ -378,38 +389,6 @@ async function modifyValues(params: FlagOperationsParams, flagNames: string[]):
378389
await fs.writeFile(cdkJsonPath, JSON.stringify(cdkJson, null, 2), 'utf-8');
379390
}
380391

381-
function formatTable(headers: string[], rows: string[][]): string {
382-
const columnWidths = [
383-
Math.max(headers[0].length, ...rows.map(row => row[0].length)),
384-
Math.max(headers[1].length, ...rows.map(row => row[1].length)),
385-
Math.max(headers[2].length, ...rows.map(row => row[2].length)),
386-
];
387-
388-
const createSeparator = () => {
389-
return '+' + columnWidths.map(width => '-'.repeat(width + 2)).join('+') + '+';
390-
};
391-
392-
const formatRow = (values: string[]) => {
393-
return '|' + values.map((value, i) => ` ${value.padEnd(columnWidths[i])} `).join('|') + '|';
394-
};
395-
396-
const separator = createSeparator();
397-
let table = separator + '\n';
398-
table += formatRow(headers) + '\n';
399-
table += separator + '\n';
400-
401-
rows.forEach(row => {
402-
if (row[1] === '' && row[2] === '') {
403-
table += ` ${row[0].padEnd(columnWidths[0])} \n`;
404-
} else {
405-
table += formatRow(row) + '\n';
406-
}
407-
});
408-
409-
table += separator;
410-
return table;
411-
}
412-
413392
function getFlagSortOrder(flag: FeatureFlag): number {
414393
if (flag.userValue === undefined) {
415394
return 3;
@@ -421,9 +400,9 @@ function getFlagSortOrder(flag: FeatureFlag): number {
421400
}
422401

423402
async function displayFlagTable(flags: FeatureFlag[], ioHelper: IoHelper): Promise<void> {
424-
const headers = ['Feature Flag Name', 'Recommended Value', 'User Value'];
403+
const filteredFlags = flags.filter(flag => flag.unconfiguredBehavesLike?.v2 !== flag.recommendedValue);
425404

426-
const sortedFlags = [...flags].sort((a, b) => {
405+
const sortedFlags = [...filteredFlags].sort((a, b) => {
427406
const orderA = getFlagSortOrder(a);
428407
const orderB = getFlagSortOrder(b);
429408

@@ -437,6 +416,7 @@ async function displayFlagTable(flags: FeatureFlag[], ioHelper: IoHelper): Promi
437416
});
438417

439418
const rows: string[][] = [];
419+
rows.push(['Feature Flag Name', 'Recommended Value', 'User Value']);
440420
let currentModule = '';
441421

442422
sortedFlags.forEach((flag) => {
@@ -445,13 +425,13 @@ async function displayFlagTable(flags: FeatureFlag[], ioHelper: IoHelper): Promi
445425
currentModule = flag.module;
446426
}
447427
rows.push([
448-
flag.name,
428+
` ${flag.name}`,
449429
String(flag.recommendedValue),
450430
flag.userValue === undefined ? '<unset>' : String(flag.userValue),
451431
]);
452432
});
453433

454-
const formattedTable = formatTable(headers, rows);
434+
const formattedTable = formatTable(rows, undefined, true);
455435
await ioHelper.defaults.info(formattedTable);
456436
}
457437

packages/aws-cdk/test/commands/flag-operations.test.ts

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ const mockFlagsData: FeatureFlag[] = [
4242
userValue: 'true',
4343
explanation: 'Flag that matches recommendation',
4444
},
45+
{
46+
module: 'different-module',
47+
name: '@aws-cdk/core:anotherMatchingFlag',
48+
recommendedValue: 'true',
49+
userValue: 'true',
50+
explanation: 'Flag that matches recommendation',
51+
unconfiguredBehavesLike: { v2: 'true' },
52+
},
4553
];
4654

4755
function createMockToolkit(): jest.Mocked<Toolkit> {
@@ -121,8 +129,8 @@ describe('displayFlags', () => {
121129
await displayFlags(params);
122130

123131
const plainTextOutput = output();
124-
expect(plainTextOutput).toContain('@aws-cdk/core:testFlag');
125-
expect(plainTextOutput).toContain('@aws-cdk/s3:anotherFlag');
132+
expect(plainTextOutput).toContain(' @aws-cdk/core:testFlag');
133+
expect(plainTextOutput).toContain(' @aws-cdk/s3:anotherFlag');
126134
});
127135

128136
test('handles null user values correctly', async () => {
@@ -181,6 +189,19 @@ describe('displayFlags', () => {
181189
expect(plainTextOutput).toContain('different-module');
182190
});
183191

192+
test('does not display flag when unconfigured behavior is the same as recommended behavior', async () => {
193+
const params = {
194+
flagData: mockFlagsData,
195+
toolkit: mockToolkit,
196+
ioHelper,
197+
all: true,
198+
};
199+
await displayFlags(params);
200+
201+
const plainTextOutput = output();
202+
expect(plainTextOutput).not.toContain(' @aws-cdk/core:anotherMatchingFlag');
203+
});
204+
184205
test('displays single flag details when only one substring match is found', async () => {
185206
const params = {
186207
flagData: mockFlagsData,
@@ -221,9 +242,10 @@ describe('displayFlags', () => {
221242
await displayFlags(params);
222243

223244
const plainTextOutput = output();
224-
expect(plainTextOutput).toContain('@aws-cdk/core:testFlag');
225-
expect(plainTextOutput).toContain('@aws-cdk/s3:anotherFlag');
226-
expect(plainTextOutput).toContain('@aws-cdk/core:matchingFlag');
245+
expect(plainTextOutput).toContain(' @aws-cdk/core:testFlag');
246+
expect(plainTextOutput).toContain(' @aws-cdk/s3:anotherFlag');
247+
expect(plainTextOutput).toContain(' @aws-cdk/core:matchingFlag');
248+
expect(plainTextOutput).not.toContain(' @aws-cdk/core:anothermatchingFlag');
227249
});
228250

229251
test('returns all matching flags if user enters multiple substrings', async () => {
@@ -236,9 +258,10 @@ describe('displayFlags', () => {
236258
await displayFlags(params);
237259

238260
const plainTextOutput = output();
239-
expect(plainTextOutput).toContain('@aws-cdk/core:testFlag');
240-
expect(plainTextOutput).toContain('@aws-cdk/core:matchingFlag');
241-
expect(plainTextOutput).not.toContain('@aws-cdk/s3:anotherFlag');
261+
expect(plainTextOutput).toContain(' @aws-cdk/core:testFlag');
262+
expect(plainTextOutput).toContain(' @aws-cdk/core:matchingFlag');
263+
expect(plainTextOutput).not.toContain(' @aws-cdk/s3:anotherFlag');
264+
expect(plainTextOutput).not.toContain(' @aws-cdk/core:anothermatchingFlag');
242265
});
243266
});
244267

@@ -264,8 +287,8 @@ describe('handleFlags', () => {
264287
await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);
265288

266289
const plainTextOutput = output();
267-
expect(plainTextOutput).toContain('@aws-cdk/core:testFlag');
268-
expect(plainTextOutput).toContain('@aws-cdk/s3:anotherFlag');
290+
expect(plainTextOutput).toContain(' @aws-cdk/core:testFlag');
291+
expect(plainTextOutput).toContain(' @aws-cdk/s3:anotherFlag');
269292
});
270293

271294
test('displays only differing flags when no specific options are provided', async () => {
@@ -275,9 +298,9 @@ describe('handleFlags', () => {
275298
await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);
276299

277300
const plainTextOutput = output();
278-
expect(plainTextOutput).toContain('@aws-cdk/core:testFlag');
279-
expect(plainTextOutput).toContain('@aws-cdk/s3:anotherFlag');
280-
expect(plainTextOutput).not.toContain('@aws-cdk/core:matchingFlag');
301+
expect(plainTextOutput).toContain(' @aws-cdk/core:testFlag');
302+
expect(plainTextOutput).toContain(' @aws-cdk/s3:anotherFlag');
303+
expect(plainTextOutput).not.toContain(' @aws-cdk/core:matchingFlag');
281304
});
282305

283306
test('handles flag not found for specific flag query', async () => {
@@ -560,6 +583,65 @@ describe('modifyValues', () => {
560583
});
561584
});
562585

586+
describe('checkDefaultBehavior', () => {
587+
test('calls setMultipleFlags when unconfiguredBehavesLike is present', async () => {
588+
const flagsWithUnconfiguredBehavior: FeatureFlag[] = [
589+
{
590+
module: 'aws-cdk-lib',
591+
name: '@aws-cdk/core:testFlag',
592+
recommendedValue: 'true',
593+
userValue: undefined,
594+
explanation: 'Test flag',
595+
unconfiguredBehavesLike: { v2: 'true' },
596+
},
597+
];
598+
599+
const cdkJsonPath = await createCdkJsonFile({});
600+
setupMockToolkitForPrototyping(mockToolkit);
601+
602+
const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse');
603+
requestResponseSpy.mockResolvedValue(true);
604+
605+
const options: FlagsOptions = {
606+
set: true,
607+
all: true,
608+
default: true,
609+
};
610+
611+
await handleFlags(flagsWithUnconfiguredBehavior, ioHelper, options, mockToolkit);
612+
613+
expect(mockToolkit.fromCdkApp).toHaveBeenCalled();
614+
expect(mockToolkit.synth).toHaveBeenCalled();
615+
616+
await cleanupCdkJsonFile(cdkJsonPath);
617+
requestResponseSpy.mockRestore();
618+
});
619+
620+
test('shows error when unconfiguredBehavesLike is not present', async () => {
621+
const flagsWithoutUnconfiguredBehavior: FeatureFlag[] = [
622+
{
623+
module: 'aws-cdk-lib',
624+
name: '@aws-cdk/core:testFlag',
625+
recommendedValue: 'true',
626+
userValue: undefined,
627+
explanation: 'Test flag',
628+
},
629+
];
630+
631+
const options: FlagsOptions = {
632+
set: true,
633+
all: true,
634+
default: true,
635+
};
636+
637+
await handleFlags(flagsWithoutUnconfiguredBehavior, ioHelper, options, mockToolkit);
638+
639+
const plainTextOutput = output();
640+
expect(plainTextOutput).toContain('The --default options are not compatible with the AWS CDK library used by your application.');
641+
expect(mockToolkit.fromCdkApp).not.toHaveBeenCalled();
642+
});
643+
});
644+
563645
describe('interactive prompts lead to the correct function calls', () => {
564646
beforeEach(() => {
565647
setupMockToolkitForPrototyping(mockToolkit);
@@ -642,11 +724,30 @@ describe('interactive prompts lead to the correct function calls', () => {
642724
const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse');
643725
requestResponseSpy.mockResolvedValue(true);
644726

727+
const flagsWithUnconfiguredBehavior: FeatureFlag[] = [
728+
{
729+
module: 'aws-cdk-lib',
730+
name: '@aws-cdk/core:testFlag',
731+
recommendedValue: 'true',
732+
userValue: 'false',
733+
explanation: 'Test flag for unit tests',
734+
unconfiguredBehavesLike: { v2: 'true' },
735+
},
736+
{
737+
module: 'aws-cdk-lib',
738+
name: '@aws-cdk/s3:anotherFlag',
739+
recommendedValue: 'false',
740+
userValue: undefined,
741+
explanation: 'Another test flag',
742+
unconfiguredBehavesLike: { v2: 'false' },
743+
},
744+
];
745+
645746
const options: FlagsOptions = {
646747
interactive: true,
647748
};
648749

649-
await handleFlags(mockFlagsData, ioHelper, options, mockToolkit);
750+
await handleFlags(flagsWithUnconfiguredBehavior, ioHelper, options, mockToolkit);
650751

651752
expect(mockToolkit.fromCdkApp).toHaveBeenCalledTimes(2);
652753
expect(mockToolkit.synth).toHaveBeenCalledTimes(2);

0 commit comments

Comments
 (0)