Skip to content

Commit 78f4726

Browse files
committed
fix: add process-level safety nets and XML parsing error handling to cleanup script
1 parent 47cdd09 commit 78f4726

File tree

1 file changed

+154
-54
lines changed

1 file changed

+154
-54
lines changed

packages/amplify-codegen-e2e-tests/src/cleanup-e2e-resources.ts

Lines changed: 154 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
/* eslint-disable spellcheck/spell-checker, camelcase, @typescript-eslint/no-explicit-any */
2+
3+
// Ultimate safety nets - cleanup must NEVER fail the build
4+
process.on('uncaughtException', (err) => {
5+
console.error('Uncaught exception in cleanup (non-fatal):', err.message);
6+
process.exit(0);
7+
});
8+
9+
process.on('unhandledRejection', (reason) => {
10+
console.error('Unhandled rejection in cleanup (non-fatal):', reason);
11+
process.exit(0);
12+
});
13+
214
import { CodeBuildClient, BatchGetBuildsCommand, Build } from '@aws-sdk/client-codebuild';
315
import { AccountClient, ListRegionsCommand, ListRegionsRequest } from '@aws-sdk/client-account';
416
import { AmplifyClient, ListAppsCommand, ListBackendEnvironmentsCommand, DeleteAppCommand } from '@aws-sdk/client-amplify';
@@ -154,6 +166,14 @@ const isNonJsonResponseError = (e: any): boolean => {
154166
return e instanceof SyntaxError || (e?.name === 'SyntaxError' && e?.message?.includes('Unexpected token'));
155167
};
156168

169+
const isXmlParsingError = (e: any): boolean => {
170+
const message = e?.message || '';
171+
return message.includes('Entity expansion limit exceeded') ||
172+
message.includes('Deserialization error') ||
173+
message.includes('entity expansion') ||
174+
(e?.name === 'Error' && message.includes('fast-xml-parser'));
175+
};
176+
157177
const isNetworkError = (e: any): boolean => {
158178
const code = e?.code || e?.name || '';
159179
return ['ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EAI_AGAIN', 'NetworkingError', 'TimeoutError'].includes(code)
@@ -196,29 +216,45 @@ const testRoleStalenessFilter = (resource: Role): boolean => {
196216
* Get all S3 buckets in the account, and filter down to the ones we consider stale.
197217
*/
198218
const getOrphanS3TestBuckets = async (account: AWSAccountInfo): Promise<S3BucketInfo[]> => {
199-
const s3Client = new S3Client(getAWSConfig(account));
200-
const listBucketResponse = await s3Client.send(new ListBucketsCommand({}));
201-
const staleBuckets = listBucketResponse.Buckets.filter(testBucketStalenessFilter);
202-
const bucketInfos = await Promise.all(
203-
staleBuckets.map(async (staleBucket): Promise<S3BucketInfo> => {
204-
const region = await getBucketRegion(account, staleBucket.Name);
205-
return {
206-
name: staleBucket.Name,
207-
region,
208-
};
209-
}),
210-
);
211-
return bucketInfos;
219+
try {
220+
const s3Client = new S3Client(getAWSConfig(account));
221+
const listBucketResponse = await s3Client.send(new ListBucketsCommand({}));
222+
const staleBuckets = listBucketResponse.Buckets.filter(testBucketStalenessFilter);
223+
const bucketInfos = await Promise.all(
224+
staleBuckets.map(async (staleBucket): Promise<S3BucketInfo> => {
225+
const region = await getBucketRegion(account, staleBucket.Name);
226+
return {
227+
name: staleBucket.Name,
228+
region,
229+
};
230+
}),
231+
);
232+
return bucketInfos;
233+
} catch (e) {
234+
if (isXmlParsingError(e)) {
235+
console.log(`XML parsing error listing orphan S3 buckets (non-fatal, skipping): ${e.message}`);
236+
return [];
237+
}
238+
throw e;
239+
}
212240
};
213241

214242
/**
215243
* Get all iam roles in the account, and filter down to the ones we consider stale.
216244
*/
217245
const getOrphanTestIamRoles = async (account: AWSAccountInfo): Promise<IamRoleInfo[]> => {
218-
const iamClient = new IAMClient(getAWSConfig(account));
219-
const listRoleResponse = await iamClient.send(new ListRolesCommand({MaxItems: 1000}));
220-
const staleRoles = listRoleResponse.Roles.filter(testRoleStalenessFilter);
221-
return staleRoles.map(it => ({ name: it.RoleName }));
246+
try {
247+
const iamClient = new IAMClient(getAWSConfig(account));
248+
const listRoleResponse = await iamClient.send(new ListRolesCommand({MaxItems: 1000}));
249+
const staleRoles = listRoleResponse.Roles.filter(testRoleStalenessFilter);
250+
return staleRoles.map(it => ({ name: it.RoleName }));
251+
} catch (e) {
252+
if (isXmlParsingError(e)) {
253+
console.log(`XML parsing error listing IAM roles (non-fatal, skipping): ${e.message}`);
254+
return [];
255+
}
256+
throw e;
257+
}
222258
};
223259

224260
/**
@@ -240,26 +276,34 @@ const getOrphanTestIamRoles = async (account: AWSAccountInfo): Promise<IamRoleIn
240276
* @returns Promise<string[]> a list of AWS regions enabled by the account
241277
*/
242278
const getRegionsEnabled = async (accountInfo: AWSAccountInfo): Promise<string[]> => {
243-
// Specify service region to avoid possible endpoint unavailable error
244-
const account = new AccountClient(getAWSConfig(accountInfo, 'us-east-1'));
279+
try {
280+
// Specify service region to avoid possible endpoint unavailable error
281+
const account = new AccountClient(getAWSConfig(accountInfo, 'us-east-1'));
245282

246-
const enabledRegions: string[] = [];
247-
let nextToken: string | undefined = undefined;
283+
const enabledRegions: string[] = [];
284+
let nextToken: string | undefined = undefined;
248285

249-
do {
250-
const input: ListRegionsRequest = {
251-
RegionOptStatusContains: ['ENABLED', 'ENABLED_BY_DEFAULT'],
252-
NextToken: nextToken,
253-
};
286+
do {
287+
const input: ListRegionsRequest = {
288+
RegionOptStatusContains: ['ENABLED', 'ENABLED_BY_DEFAULT'],
289+
NextToken: nextToken,
290+
};
254291

255-
const response = await account.send(new ListRegionsCommand(input));
256-
nextToken = response.NextToken;
292+
const response = await account.send(new ListRegionsCommand(input));
293+
nextToken = response.NextToken;
257294

258-
enabledRegions.push(...response.Regions.map(r => r.RegionName).filter(Boolean));
259-
} while (nextToken);
295+
enabledRegions.push(...response.Regions.map(r => r.RegionName).filter(Boolean));
296+
} while (nextToken);
260297

261-
console.log('All enabled regions fetched: ', enabledRegions);
262-
return enabledRegions;
298+
console.log('All enabled regions fetched: ', enabledRegions);
299+
return enabledRegions;
300+
} catch (e) {
301+
if (isXmlParsingError(e)) {
302+
console.log(`XML parsing error listing regions (non-fatal, using default test regions): ${e.message}`);
303+
return AWS_REGIONS_TO_RUN_TESTS;
304+
}
305+
throw e;
306+
}
263307
};
264308

265309
/**
@@ -277,7 +321,17 @@ const getAmplifyApps = async (account: AWSAccountInfo, region: string, regionsEn
277321
return [];
278322
}
279323

280-
const amplifyApps = await amplifyClient.send(new ListAppsCommand({ maxResults: 50 })); // keeping it to 50 as max supported is 50
324+
let amplifyApps;
325+
try {
326+
amplifyApps = await amplifyClient.send(new ListAppsCommand({ maxResults: 50 }));
327+
} catch (e) {
328+
if (isXmlParsingError(e)) {
329+
console.log(`XML parsing error listing Amplify apps in ${region} (non-fatal, skipping): ${e.message}`);
330+
return [];
331+
}
332+
throw e;
333+
}
334+
281335
const result: AmplifyAppInfo[] = [];
282336
for (const app of amplifyApps.apps) {
283337
const backends: Record<string, StackInfo> = {};
@@ -349,6 +403,10 @@ const getStackDetails = async (stackName: string, account: AWSAccountInfo, regio
349403
console.log(`Stack ${stackName} does not exist in ${region}. Skipping.`);
350404
return;
351405
}
406+
if (isXmlParsingError(e)) {
407+
console.log(`XML parsing error describing stack ${stackName} in ${region} (non-fatal, skipping): ${e.message}`);
408+
return;
409+
}
352410
throw e;
353411
}
354412
};
@@ -361,19 +419,28 @@ const getStacks = async (account: AWSAccountInfo, region: string, regionsEnabled
361419
return [];
362420
}
363421

364-
const stacks = await cfnClient.send(new ListStacksCommand({
365-
StackStatusFilter: [
366-
'CREATE_COMPLETE',
367-
'ROLLBACK_FAILED',
368-
'DELETE_FAILED',
369-
'UPDATE_COMPLETE',
370-
'UPDATE_ROLLBACK_FAILED',
371-
'UPDATE_ROLLBACK_COMPLETE',
372-
'IMPORT_COMPLETE',
373-
'IMPORT_ROLLBACK_FAILED',
374-
'IMPORT_ROLLBACK_COMPLETE',
375-
],
376-
}));
422+
let stacks;
423+
try {
424+
stacks = await cfnClient.send(new ListStacksCommand({
425+
StackStatusFilter: [
426+
'CREATE_COMPLETE',
427+
'ROLLBACK_FAILED',
428+
'DELETE_FAILED',
429+
'UPDATE_COMPLETE',
430+
'UPDATE_ROLLBACK_FAILED',
431+
'UPDATE_ROLLBACK_COMPLETE',
432+
'IMPORT_COMPLETE',
433+
'IMPORT_ROLLBACK_FAILED',
434+
'IMPORT_ROLLBACK_COMPLETE',
435+
],
436+
}));
437+
} catch (e) {
438+
if (isXmlParsingError(e)) {
439+
console.log(`XML parsing error listing stacks in ${region} (non-fatal, skipping): ${e.message}`);
440+
return [];
441+
}
442+
throw e;
443+
}
377444

378445
// We are interested in only the root stacks that are deployed by amplify-cli
379446
const rootStacks = stacks.StackSummaries.filter(stack => !stack.RootId);
@@ -430,7 +497,16 @@ const getBucketRegion = async (account: AWSAccountInfo, bucketName: string): Pro
430497
const getS3Buckets = async (account: AWSAccountInfo): Promise<S3BucketInfo[]> => {
431498
const awsConfig = getAWSConfig(account);
432499
const s3Client = new S3Client(awsConfig);
433-
const buckets = await s3Client.send(new ListBucketsCommand({}));
500+
let buckets;
501+
try {
502+
buckets = await s3Client.send(new ListBucketsCommand({}));
503+
} catch (e) {
504+
if (isXmlParsingError(e)) {
505+
console.log(`XML parsing error listing S3 buckets (non-fatal, skipping): ${e.message}`);
506+
return [];
507+
}
508+
throw e;
509+
}
434510
const result: S3BucketInfo[] = [];
435511
for (const bucket of buckets.Buckets) {
436512
let region: string | undefined;
@@ -460,6 +536,8 @@ const getS3Buckets = async (account: AWSAccountInfo): Promise<S3BucketInfo[]> =>
460536
console.error(`Skipping processing ${account.accountId}, bucket ${bucket.Name}`, e);
461537
} else if (isNonJsonResponseError(e)) {
462538
console.warn(`Received non-JSON response for bucket ${bucket.Name}. Skipping.`, e.message);
539+
} else if (isXmlParsingError(e)) {
540+
console.log(`XML parsing error for bucket ${bucket.Name} (non-fatal, skipping): ${e.message}`);
463541
} else {
464542
throw e;
465543
}
@@ -625,9 +703,17 @@ const deleteAttachedRolePolicies = async (
625703
accountIndex: number,
626704
roleName: string,
627705
): Promise<void> => {
628-
const iamClient = new IAMClient(getAWSConfig(account));
629-
const rolePolicies = await iamClient.send(new ListAttachedRolePoliciesCommand({ RoleName: roleName }));
630-
await Promise.all(rolePolicies.AttachedPolicies.map(policy => detachIamAttachedRolePolicy(account, accountIndex, roleName, policy)));
706+
try {
707+
const iamClient = new IAMClient(getAWSConfig(account));
708+
const rolePolicies = await iamClient.send(new ListAttachedRolePoliciesCommand({ RoleName: roleName }));
709+
await Promise.all(rolePolicies.AttachedPolicies.map(policy => detachIamAttachedRolePolicy(account, accountIndex, roleName, policy)));
710+
} catch (e) {
711+
if (isXmlParsingError(e)) {
712+
console.log(`${generateAccountInfo(account, accountIndex)} XML parsing error listing attached policies for ${roleName} (non-fatal, skipping): ${e.message}`);
713+
return;
714+
}
715+
throw e;
716+
}
631717
};
632718

633719
const detachIamAttachedRolePolicy = async (
@@ -653,9 +739,17 @@ const deleteRolePolicies = async (
653739
accountIndex: number,
654740
roleName: string,
655741
): Promise<void> => {
656-
const iamClient = new IAMClient(getAWSConfig(account));
657-
const rolePolicies = await iamClient.send(new ListRolePoliciesCommand({ RoleName: roleName }));
658-
await Promise.all(rolePolicies.PolicyNames.map(policy => deleteIamRolePolicy(account, accountIndex, roleName, policy)));
742+
try {
743+
const iamClient = new IAMClient(getAWSConfig(account));
744+
const rolePolicies = await iamClient.send(new ListRolePoliciesCommand({ RoleName: roleName }));
745+
await Promise.all(rolePolicies.PolicyNames.map(policy => deleteIamRolePolicy(account, accountIndex, roleName, policy)));
746+
} catch (e) {
747+
if (isXmlParsingError(e)) {
748+
console.log(`${generateAccountInfo(account, accountIndex)} XML parsing error listing policies for ${roleName} (non-fatal, skipping): ${e.message}`);
749+
return;
750+
}
751+
throw e;
752+
}
659753
};
660754

661755
const deleteIamRolePolicy = async (
@@ -904,6 +998,11 @@ const cleanupAccount = async (account: AWSAccountInfo, accountIndex: number, fil
904998
summary.skippedReason = 'network error';
905999
return;
9061000
}
1001+
if (isXmlParsingError(e)) {
1002+
console.warn(`${generateAccountInfo(account, accountIndex)} XML parsing error encountered. Skipping this account.`, e.message);
1003+
summary.skippedReason = 'XML parsing error';
1004+
return;
1005+
}
9071006
console.error(`${generateAccountInfo(account, accountIndex)} Cleanup failed with unexpected error:`, e);
9081007
summary.skippedReason = 'unexpected error';
9091008
}
@@ -999,9 +1098,10 @@ const printCleanupSummary = (): void => {
9991098

10001099
cleanup()
10011100
.catch((e) => {
1002-
console.log(`Cleanup encountered an error but completing gracefully: ${e.message}`);
1101+
console.error('Top-level cleanup error (non-fatal):', e?.message || e);
10031102
})
10041103
.finally(() => {
10051104
printCleanupSummary();
1105+
console.log('Cleanup complete. Exiting with code 0.');
10061106
process.exit(0);
10071107
});

0 commit comments

Comments
 (0)