Skip to content

Commit b02f72e

Browse files
authored
fix(import): pass CDK asset zip paths as subprocess arguments (#2661)
1 parent 480ef02 commit b02f72e

2 files changed

Lines changed: 89 additions & 3 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { publishCdkAssets } from '../phase2-import';
2+
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
3+
import { unzipSync } from 'fflate';
4+
import * as fs from 'node:fs';
5+
import * as os from 'node:os';
6+
import * as path from 'node:path';
7+
import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
8+
9+
vi.mock('../../../aws/account', () => ({
10+
getCredentialProvider: () => ({
11+
accessKeyId: 'test',
12+
secretAccessKey: 'test',
13+
}),
14+
}));
15+
16+
describe('publishCdkAssets', () => {
17+
let assemblyDirectory: string;
18+
let sendSpy: MockInstance;
19+
20+
beforeEach(() => {
21+
assemblyDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'import-assets-'));
22+
sendSpy = vi.spyOn(S3Client.prototype, 'send').mockResolvedValue(undefined);
23+
});
24+
25+
afterEach(() => {
26+
vi.restoreAllMocks();
27+
fs.rmSync(assemblyDirectory, { recursive: true, force: true });
28+
});
29+
30+
function writeManifest(assetPath: string) {
31+
fs.writeFileSync(
32+
path.join(assemblyDirectory, 'stack.assets.json'),
33+
JSON.stringify({
34+
files: {
35+
asset: {
36+
source: { path: assetPath, packaging: 'zip' },
37+
destinations: {
38+
target: { bucketName: 'test-assets', objectKey: 'asset.zip', region: 'us-east-1' },
39+
},
40+
},
41+
},
42+
})
43+
);
44+
}
45+
46+
test.each([
47+
{ assetPath: 'asset', relativeAssembly: false },
48+
{ assetPath: 'asset with spaces', relativeAssembly: false },
49+
{ assetPath: 'asset & (copy)', relativeAssembly: false },
50+
{ assetPath: 'asset $(echo literal)', relativeAssembly: false },
51+
{ assetPath: 'asset `echo literal`', relativeAssembly: false },
52+
{ assetPath: 'asset with spaces', relativeAssembly: true },
53+
])(
54+
'uploads directory contents for $assetPath (relative assembly: $relativeAssembly)',
55+
async ({ assetPath, relativeAssembly }) => {
56+
const sourcePath = path.join(assemblyDirectory, assetPath);
57+
fs.mkdirSync(path.join(sourcePath, 'nested'), { recursive: true });
58+
fs.writeFileSync(path.join(sourcePath, 'main.py'), 'print("hello")\n');
59+
fs.writeFileSync(path.join(sourcePath, 'nested', 'data.txt'), 'asset data');
60+
writeManifest(assetPath);
61+
62+
await publishCdkAssets(
63+
relativeAssembly ? path.relative(process.cwd(), assemblyDirectory) : assemblyDirectory,
64+
'us-east-1'
65+
);
66+
67+
expect(sendSpy).toHaveBeenCalledTimes(1);
68+
const command = sendSpy.mock.calls[0]![0] as PutObjectCommand;
69+
expect(command).toBeInstanceOf(PutObjectCommand);
70+
expect(command.input).toMatchObject({ Bucket: 'test-assets', Key: 'asset.zip' });
71+
const files = unzipSync(command.input.Body as Buffer);
72+
expect(Buffer.from(files['main.py']!).toString()).toBe('print("hello")\n');
73+
expect(Buffer.from(files['nested/data.txt']!).toString()).toBe('asset data');
74+
expect(fs.existsSync(`${sourcePath}.zip`)).toBe(false);
75+
}
76+
);
77+
78+
test('rejects without uploading when zip fails', async () => {
79+
fs.mkdirSync(path.join(assemblyDirectory, 'empty-asset'));
80+
writeManifest('empty-asset');
81+
82+
await expect(publishCdkAssets(assemblyDirectory, 'us-east-1')).rejects.toThrow();
83+
84+
expect(sendSpy).not.toHaveBeenCalled();
85+
});
86+
});

‎src/cli/commands/import/phase2-import.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PollExhaustedError, PollTimeoutError } from '../../../lib/errors/types';
22
import { isThrottlingError, poll } from '../../../lib/utils/polling';
3+
import { runSubprocess } from '../../../lib/utils/subprocess';
34
import { getCredentialProvider } from '../../aws/account';
45
import type { CfnTemplate } from './template-utils';
56
import { buildImportTemplate } from './template-utils';
@@ -14,7 +15,6 @@ import {
1415
} from '@aws-sdk/client-cloudformation';
1516
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
1617
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
17-
import { execSync } from 'node:child_process';
1818
import * as fs from 'node:fs';
1919
import * as path from 'node:path';
2020

@@ -250,8 +250,8 @@ export async function publishCdkAssets(
250250
if (stat.isDirectory()) {
251251
if (asset.source.packaging === 'zip') {
252252
// Zip the directory contents
253-
const zipPath = `${sourcePath}.zip`;
254-
execSync(`cd "${sourcePath}" && zip -rq "${zipPath}" .`);
253+
const zipPath = path.resolve(`${sourcePath}.zip`);
254+
await runSubprocess('zip', ['-rq', zipPath, '.'], { cwd: sourcePath, stdio: 'pipe' });
255255
body = fs.readFileSync(zipPath);
256256
fs.unlinkSync(zipPath);
257257
} else {

0 commit comments

Comments
 (0)