-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathcommands.ts
More file actions
267 lines (238 loc) · 9.95 KB
/
Copy pathcommands.ts
File metadata and controls
267 lines (238 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import * as path from 'path';
import * as fs from 'fs';
import { copySync, moveSync, readFileSync, writeFileSync } from 'fs-extra';
import {
addApiWithoutSchema,
amplifyPush,
getProjectMeta,
getScriptRunnerPath,
initJSProjectWithProfile,
nspawn as spawn,
sleep,
updateApiSchema,
} from 'amplify-category-api-e2e-core';
import { DynamoDBClient, DeleteTableCommand, ListTablesCommand, UpdateTableCommand } from '@aws-sdk/client-dynamodb';
/**
* Retrieve the path to the `npx` executable for interacting with the aws-cdk cli.
* @returns the local `npx` executable path.
*/
const getNpxPath = (): string => (process.platform === 'win32' ? getScriptRunnerPath().replace('node.exe', 'npx.cmd') : 'npx');
export type CdkConstruct = 'GraphqlApi' | 'Data';
const cdkConstructToPackagedConstructDirectory: Record<CdkConstruct, string> = {
GraphqlApi: path.join(__dirname, '..', '..', 'amplify-graphql-api-construct', 'dist', 'js'),
Data: path.join(__dirname, '..', '..', 'amplify-data-construct', 'dist', 'js'),
};
/**
* Try and retrieve the locally packaged construct path, and throw an error if not found.
* @returns path to the packaged construct for testing.
*/
const getPackagedConstructPath = (cdkConstruct: CdkConstruct): string => {
const packagedConstructDirectory = cdkConstructToPackagedConstructDirectory[cdkConstruct];
const packagedConstructTarballs = fs.readdirSync(packagedConstructDirectory).filter((fileName) => fileName.match(/\.tgz/));
if (packagedConstructTarballs.length !== 1) {
throw new Error('Construct packaged tarball not found');
}
return path.join(packagedConstructDirectory, packagedConstructTarballs[0]);
};
/**
* Copy the backend snapshot into the generated app location.
*/
const copyTemplateDirectory = (projectPath: string, templatePath: string): void => {
const binDir = path.join(projectPath, 'bin');
copySync(templatePath, binDir, { overwrite: true });
moveSync(path.join(binDir, 'app.ts'), path.join(binDir, `${path.basename(projectPath)}.ts`), { overwrite: true });
};
/**
* Adds additional values to cdk context persisted in cdk.json file.
*/
const appendToCDKContext = (projectPath: string, additionalContext: Record<string, string>): void => {
const cdkJsonPath = path.join(projectPath, 'cdk.json');
const cdkJson = JSON.parse(readFileSync(cdkJsonPath, 'utf-8'));
if (!cdkJson.context) {
cdkJson.context = {};
}
Object.entries(additionalContext).forEach(([contextKey, contextValue]) => {
cdkJson.context[contextKey] = contextValue;
});
writeFileSync(cdkJsonPath, JSON.stringify(cdkJson, null, 2));
};
export type InitCDKProjectProps = {
construct?: CdkConstruct;
cdkContext?: Record<string, string>;
cdkVersion?: string;
additionalDependencies?: Array<string>;
};
/**
* Initialize a CDK project in the cwd using a reference backend `app.ts` file, and optional cdkVersion specified.
* @param cwd the directory to initialize the CDK project in
* @param templatePath path to the project to overwrite the cdk sample code with
* @param props additional properties to configure the test app setup.
* @returns a promise which resolves to the stack name
*/
export const initCDKProject = async (cwd: string, templatePath: string, props?: InitCDKProjectProps): Promise<string> => {
const { cdkVersion = '2.187.0', additionalDependencies = [] } = props ?? {};
await spawn(getNpxPath(), ['cdk', 'init', 'app', '--language', 'typescript'], {
cwd,
stripColors: true,
// npx cdk does not work on verdaccio
env: {
npm_config_registry: 'https://registry.npmjs.org/',
},
})
.sendYes()
.runAsync();
if (props?.cdkContext) {
appendToCDKContext(cwd, props.cdkContext);
}
copyTemplateDirectory(cwd, templatePath);
const deps = [getPackagedConstructPath(props?.construct ?? 'GraphqlApi'), `aws-cdk-lib@${cdkVersion}`, ...additionalDependencies];
await spawn('npm', ['install', ...deps], { cwd, stripColors: true }).runAsync();
return JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8')).name.replace(/_/g, '-');
};
export type CdkDeployProps = {
/**
* Amount of time to wait with no output from CDK before failing.
*/
timeoutMs?: number;
/**
* Amount of time to wait after deployment before returning. This allows time for certain resources to propagate and finalize.
*/
postDeployWaitMs?: number;
};
/**
* Execute `cdk deploy` on the project to push to the cloud.
* @param cwd the cwd of the cdk project
* @param option additional option to pass into the deployment
* @returns the generated outputs file as a JSON object
*/
export const cdkDeploy = async (cwd: string, option: string, props?: CdkDeployProps): Promise<any> => {
// The CodegenAssets BucketDeployment resource takes a while. Set the timeout to 10m account for that. (Note that this is the "no output
// timeout"--the overall deployment is still allowed to take longer than 10m)
const noOutputTimeout = props?.timeoutMs ?? 10 * 60 * 1000;
const commandOptions = {
cwd,
stripColors: true,
// npx cdk does not work on verdaccio
env: { npm_config_registry: 'https://registry.npmjs.org/' },
noOutputTimeout,
};
await spawn(
getNpxPath(),
['cdk', 'deploy', '--outputs-file', 'outputs.json', '--require-approval', 'never', option],
commandOptions,
).runAsync();
if (props?.postDeployWaitMs) {
console.log(`Waiting for ${props.postDeployWaitMs} ms to let resources propagate and finalize`);
await sleep(props.postDeployWaitMs);
}
return JSON.parse(readFileSync(path.join(cwd, 'outputs.json'), 'utf8'));
};
/**
* Execute `cdk destroy` in the project directory to tear down test stacks.
* @param cwd the directory of the cdk project
* @param option option to pass into the destroy command, e.g. the stack name.
* @returns a promise which resolves after teardown of the stack
*/
export const cdkDestroy = async (cwd: string, option: string): Promise<void> => {
return spawn(getNpxPath(), ['cdk', 'destroy', '--force', option], { cwd, stripColors: true }).runAsync();
};
/**
* Helper function to update the cdk app code by a given directory path containing the new `app.ts`
* @param cwd cdk app project root
* @param templatePath updated cdk app code directory path. The new `app.ts` should be defined under this directory
*/
export const updateCDKAppWithTemplate = (cwd: string, templatePath: string): void => {
const binDir = path.join(cwd, 'bin');
copySync(templatePath, binDir, { overwrite: true });
moveSync(path.join(binDir, 'app.ts'), path.join(binDir, `${path.basename(cwd)}.ts`), { overwrite: true });
};
/**
* Helper function to create a gen 1 project with for migration.
*
* @param name project name
* @param projRoot project root directory
* @param schema schema file to use
*/
export const createGen1ProjectForMigration = async (
name: string,
projRoot: string,
schema: string,
): Promise<{
GraphQLAPIEndpointOutput: string;
GraphQLAPIKeyOutput: string;
DataSourceMappingOutput: string;
}> => {
await initJSProjectWithProfile(projRoot, { name });
await addApiWithoutSchema(projRoot, { transformerVersion: 2 });
await updateApiSchema(projRoot, name, schema);
await amplifyPush(projRoot);
// The test should do a second push after enabling the feature flag to start the migration
// TODO: GEN1_GEN2_MIGRATION
// The Gen 1 CLI has not released this feature flag yet
// In the meantime, manually create the data source mapping
// restore this block when the feature flag is released
// Start block
/*
addFeatureFlag(projRoot, 'graphqltransformer', 'enablegen2migration', true);
await amplifyPushForce(projRoot);
*/
// End block
const meta = getProjectMeta(projRoot);
const { output } = meta.api[name];
const {
GraphQLAPIEndpointOutput,
GraphQLAPIKeyOutput,
GraphQLAPIIdOutput,
// TODO: GEN1_GEN2_MIGRATION
// get DataSourceMappingOutput from output when feature flag is released
// uncomment the line below
// DataSourceMappingOutput,
} = output;
// TODO: GEN1_GEN2_MIGRATION
// Construct the DataSourceMappingOutput with the AWS SDK
// Set table deletion protection to true for all tables
// Remove this block when the feature flag is released
// Start block
const client = new DynamoDBClient({ region: process.env.CLI_REGION || 'us-west-2' });
const tables = [];
let ExclusiveStartTableName;
do {
const command = new ListTablesCommand({ ExclusiveStartTableName });
const response = await client.send(command);
ExclusiveStartTableName = response.LastEvaluatedTableName;
tables.push(...response.TableNames);
} while (ExclusiveStartTableName);
const tablesForApi = tables
// filter all tables by the API ID
.filter((tableName) => tableName.includes(GraphQLAPIIdOutput));
const tableNameMapping = tablesForApi
// extract the model name from the table name and create the mapping
.map((tableName) => [tableName.match(/(^.*?)-/)[1], tableName]);
const DataSourceMappingOutput = JSON.stringify(Object.fromEntries(tableNameMapping));
// set deletion protection to true for all tables
await Promise.allSettled(
tablesForApi.map((tableName) => client.send(new UpdateTableCommand({ TableName: tableName, DeletionProtectionEnabled: true }))),
);
// End block
return {
GraphQLAPIEndpointOutput,
GraphQLAPIKeyOutput,
DataSourceMappingOutput,
};
};
/**
* Helper function to delete DDB tables.
* Used to delete tables set to retain on delete.
* @param tableNames table names to delete
*/
export const deleteDDBTables = async (tableNames: string[]): Promise<void> => {
const client = new DynamoDBClient({ region: process.env.CLI_REGION || 'us-west-2' });
// TODO: GEN1_GEN2_MIGRATION
// disable deletion protection before deleting the tables
// start block
await Promise.allSettled(
tableNames.map((tableName) => client.send(new UpdateTableCommand({ TableName: tableName, DeletionProtectionEnabled: false }))),
);
// end block
await Promise.allSettled(tableNames.map((tableName) => client.send(new DeleteTableCommand({ TableName: tableName }))));
};