diff --git a/packages/aws-cdk/lib/commands/flags/operations.ts b/packages/aws-cdk/lib/commands/flags/operations.ts index 28a574cbe..b08d26476 100644 --- a/packages/aws-cdk/lib/commands/flags/operations.ts +++ b/packages/aws-cdk/lib/commands/flags/operations.ts @@ -43,6 +43,8 @@ export class FlagOperations { private allStacks: CloudFormationStackArtifact[]; private queue: PQueue; private baselineTempDir?: string; + private originalTempDir?: string; + private modifiedTempDir?: string; constructor( private readonly flags: FeatureFlag[], @@ -278,21 +280,26 @@ export class FlagOperations { const cdkJson = await JSON.parse(await fs.readFile(path.join(process.cwd(), 'cdk.json'), 'utf-8')); const app = cdkJson.app; + this.originalTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-original-')); const source = await this.toolkit.fromCdkApp(app, { contextStore: memoryContext, - outdir: fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-original-')), + outdir: this.originalTempDir, }); const updateObj = await this.buildUpdateObject(flagNames, params, baseContextValues); - if (!updateObj) return false; + if (!updateObj) { + await this.cleanupTempDirectories(); + return false; + } await memoryContext.update(updateObj); const cx = await this.toolkit.synth(source); const assembly = cx.cloudAssembly; + this.modifiedTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-temp-')); const modifiedSource = await this.toolkit.fromCdkApp(app, { contextStore: memoryContext, - outdir: fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-temp-')), + outdir: this.modifiedTempDir, }); const modifiedCx = await this.toolkit.synth(modifiedSource); @@ -378,10 +385,14 @@ export class FlagOperations { /** Removes temporary directories created during flag operations */ private async cleanupTempDirectories(): Promise { - const originalDir = path.join(process.cwd(), 'original'); - const tempDir = path.join(process.cwd(), 'temp'); - await fs.remove(originalDir); - await fs.remove(tempDir); + if (this.originalTempDir) { + await fs.remove(this.originalTempDir); + this.originalTempDir = undefined; + } + if (this.modifiedTempDir) { + await fs.remove(this.modifiedTempDir); + this.modifiedTempDir = undefined; + } } /** Actually modifies the cdk.json file with the new flag values */ diff --git a/packages/aws-cdk/test/commands/flag-operations.test.ts b/packages/aws-cdk/test/commands/flag-operations.test.ts index a52bb0b11..1297b768b 100644 --- a/packages/aws-cdk/test/commands/flag-operations.test.ts +++ b/packages/aws-cdk/test/commands/flag-operations.test.ts @@ -430,6 +430,66 @@ describe('processFlagsCommand', () => { requestResponseSpy.mockRestore(); }); + test('cleans up the real temp directories created during prototyping when the user cancels', async () => { + // Regression test: cleanupTempDirectories() used to delete + // `${cwd}/original` and `${cwd}/temp`, but the directories actually + // created by prototypeChanges() are `mkdtemp`-generated paths under + // os.tmpdir() (prefixed `cdk-original-`/`cdk-temp-`). Those paths were + // never stored anywhere, so every `cdk flags --set` invocation leaked + // two real synthesized-cloud-assembly directories into the OS temp dir. + const cdkJsonPath = await createCdkJsonFile(); + setupMockToolkitForPrototyping(mockToolkit); + + const tmpEntriesBefore = fs.readdirSync(os.tmpdir()); + + const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse'); + requestResponseSpy.mockResolvedValue(false); + + const options: FlagsOptions = { + FLAGNAME: ['@aws-cdk/core:testFlag'], + set: true, + value: 'true', + }; + + const flagOperations = new FlagCommandHandler(mockFlagsData, ioHelper, options, mockToolkit); + await expect(flagOperations.processFlagsCommand()).rejects.toThrow('Flag update cancelled'); + + const leakedEntries = fs.readdirSync(os.tmpdir()) + .filter(entry => !tmpEntriesBefore.includes(entry)) + .filter(entry => entry.startsWith('cdk-original-') || entry.startsWith('cdk-temp-')); + expect(leakedEntries).toEqual([]); + + await cleanupCdkJsonFile(cdkJsonPath); + requestResponseSpy.mockRestore(); + }); + + test('cleans up the real temp directories created during prototyping when the user accepts', async () => { + const cdkJsonPath = await createCdkJsonFile(); + setupMockToolkitForPrototyping(mockToolkit); + + const tmpEntriesBefore = fs.readdirSync(os.tmpdir()); + + const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse'); + requestResponseSpy.mockResolvedValue(true); + + const options: FlagsOptions = { + FLAGNAME: ['@aws-cdk/core:testFlag'], + set: true, + value: 'true', + }; + + const flagOperations = new FlagCommandHandler(mockFlagsData, ioHelper, options, mockToolkit); + await flagOperations.processFlagsCommand(); + + const leakedEntries = fs.readdirSync(os.tmpdir()) + .filter(entry => !tmpEntriesBefore.includes(entry)) + .filter(entry => entry.startsWith('cdk-original-') || entry.startsWith('cdk-temp-')); + expect(leakedEntries).toEqual([]); + + await cleanupCdkJsonFile(cdkJsonPath); + requestResponseSpy.mockRestore(); + }); + test('does not resynthesize when setting flag to same value as current context', async () => { const cdkJsonPath = await createCdkJsonFile({ '@aws-cdk/core:testFlag': true,