|
1 | 1 | import type { ParameterDeclaration } from '@aws-sdk/client-cloudformation'; |
| 2 | +import { minimatch } from 'minimatch'; |
2 | 3 | import { ToolkitError } from '../../toolkit/toolkit-error'; |
3 | 4 | import type { ICloudFormationClient } from '../aws-auth/private'; |
4 | 5 | import type { IoHelper } from '../io/private'; |
@@ -36,40 +37,72 @@ async function paginateSdkCall(cb: (nextToken?: string) => Promise<string | unde |
36 | 37 | * - stacks in DELETE_COMPLETE or DELETE_IN_PROGRESS stage |
37 | 38 | * - stacks that are using a different bootstrap qualifier |
38 | 39 | */ |
39 | | -async function fetchAllStackTemplates(cfn: ICloudFormationClient, ioHelper: IoHelper, qualifier?: string) { |
| 40 | +async function fetchAllStackTemplates( |
| 41 | + cfn: ICloudFormationClient, |
| 42 | + ioHelper: IoHelper, |
| 43 | + qualifier?: string, |
| 44 | + ignoreNonCdkStacks?: string[], |
| 45 | + skipUnauthorizedStacks?: boolean, |
| 46 | +) { |
40 | 47 | const stackNames: string[] = []; |
41 | | - await paginateSdkCall(async (nextToken) => { |
42 | | - const stacks = await cfn.listStacks({ NextToken: nextToken }); |
43 | | - |
44 | | - // We ignore stacks with these statuses because their assets are no longer live |
45 | | - const ignoredStatues = ['CREATE_FAILED', 'DELETE_COMPLETE', 'DELETE_IN_PROGRESS', 'DELETE_FAILED', 'REVIEW_IN_PROGRESS']; |
46 | | - stackNames.push( |
47 | | - ...(stacks.StackSummaries ?? []) |
48 | | - .filter((s: any) => !ignoredStatues.includes(s.StackStatus)) |
49 | | - .map((s: any) => s.StackId ?? s.StackName), |
50 | | - ); |
| 48 | + // Handle the error if the user is unable to list stacks due to insufficient permissions |
| 49 | + try { |
| 50 | + await paginateSdkCall(async (nextToken) => { |
| 51 | + const stacks = await cfn.listStacks({ NextToken: nextToken }); |
| 52 | + |
| 53 | + // We ignore stacks with these statuses because their assets are no longer live |
| 54 | + const ignoredStatues = ['CREATE_FAILED', 'DELETE_COMPLETE', 'DELETE_IN_PROGRESS', 'DELETE_FAILED', 'REVIEW_IN_PROGRESS']; |
| 55 | + stackNames.push( |
| 56 | + ...(stacks.StackSummaries ?? []) |
| 57 | + .filter((s: any) => !ignoredStatues.includes(s.StackStatus)) |
| 58 | + .map((s: any) => s.StackId ?? s.StackName), |
| 59 | + ); |
51 | 60 |
|
52 | | - return stacks.NextToken; |
53 | | - }); |
| 61 | + return stacks.NextToken; |
| 62 | + }); |
| 63 | + } catch (error: any) { |
| 64 | + if (error.name == 'AccessDenied' && skipUnauthorizedStacks) { |
| 65 | + await ioHelper.defaults.warn('Skipping all stack processing due to AccessDenied on listStacks'); |
| 66 | + return []; |
| 67 | + } |
| 68 | + throw error; |
| 69 | + } |
54 | 70 |
|
55 | 71 | await ioHelper.defaults.debug(`Parsing through ${stackNames.length} stacks`); |
56 | 72 |
|
57 | 73 | const templates: string[] = []; |
58 | 74 | for (const stack of stackNames) { |
59 | | - let summary; |
60 | | - summary = await cfn.getTemplateSummary({ |
61 | | - StackName: stack, |
62 | | - }); |
63 | | - |
64 | | - if (bootstrapFilter(summary.Parameters, qualifier)) { |
65 | | - // This stack is definitely bootstrapped to a different qualifier so we can safely ignore it |
66 | | - continue; |
67 | | - } else { |
68 | | - const template = await cfn.getTemplate({ |
| 75 | + // Skip stacks matching ignore patterns |
| 76 | + if (ignoreNonCdkStacks && ignoreNonCdkStacks.length > 0) { |
| 77 | + const shouldSkip = ignoreNonCdkStacks.some(pattern => minimatch(stack, pattern)); |
| 78 | + if (shouldSkip) { |
| 79 | + await ioHelper.defaults.debug(`Skipping stack ${stack}`); |
| 80 | + continue; |
| 81 | + } |
| 82 | + } |
| 83 | + // Allows users to skip a stack if they can't access it instead of failing |
| 84 | + try { |
| 85 | + let summary; |
| 86 | + summary = await cfn.getTemplateSummary({ |
69 | 87 | StackName: stack, |
70 | 88 | }); |
71 | 89 |
|
72 | | - templates.push((template.TemplateBody ?? '') + JSON.stringify(summary?.Parameters)); |
| 90 | + if (bootstrapFilter(summary.Parameters, qualifier)) { |
| 91 | + // This stack is definitely bootstrapped to a different qualifier so we can safely ignore it |
| 92 | + continue; |
| 93 | + } else { |
| 94 | + const template = await cfn.getTemplate({ |
| 95 | + StackName: stack, |
| 96 | + }); |
| 97 | + |
| 98 | + templates.push((template.TemplateBody ?? '') + JSON.stringify(summary?.Parameters)); |
| 99 | + } |
| 100 | + } catch (error: any) { |
| 101 | + if (error.name === 'AccessDenied' && skipUnauthorizedStacks) { |
| 102 | + await ioHelper.defaults.warn(`Skipping stack ${stack} due to AccessDenied error`); |
| 103 | + continue; |
| 104 | + } |
| 105 | + throw error; |
73 | 106 | } |
74 | 107 | } |
75 | 108 |
|
@@ -102,11 +135,19 @@ export interface RefreshStacksProps { |
102 | 135 | readonly ioHelper: IoHelper; |
103 | 136 | readonly activeAssets: ActiveAssetCache; |
104 | 137 | readonly qualifier?: string; |
| 138 | + readonly ignoreNonCdkStacks?: string[]; |
| 139 | + readonly skipUnauthorizedStacks?: boolean; |
105 | 140 | } |
106 | 141 |
|
107 | 142 | export async function refreshStacks(props: RefreshStacksProps) { |
108 | 143 | try { |
109 | | - const stacks = await fetchAllStackTemplates(props.cfn, props.ioHelper, props.qualifier); |
| 144 | + const stacks = await fetchAllStackTemplates( |
| 145 | + props.cfn, |
| 146 | + props.ioHelper, |
| 147 | + props.qualifier, |
| 148 | + props.ignoreNonCdkStacks, |
| 149 | + props.skipUnauthorizedStacks, |
| 150 | + ); |
110 | 151 | for (const stack of stacks) { |
111 | 152 | props.activeAssets.rememberStack(stack); |
112 | 153 | } |
@@ -138,6 +179,16 @@ export interface BackgroundStackRefreshProps { |
138 | 179 | * Stack bootstrap qualifier |
139 | 180 | */ |
140 | 181 | readonly qualifier?: string; |
| 182 | + |
| 183 | + /** |
| 184 | + * Stack names to ignore during garbage collection |
| 185 | + */ |
| 186 | + readonly ignoreNonCdkStacks?: string[]; |
| 187 | + |
| 188 | + /** |
| 189 | + * Skip stacks that return AccessDenied errors |
| 190 | + */ |
| 191 | + readonly skipUnauthorizedStacks?: boolean; |
141 | 192 | } |
142 | 193 |
|
143 | 194 | /** |
@@ -166,6 +217,8 @@ export class BackgroundStackRefresh { |
166 | 217 | ioHelper: this.props.ioHelper, |
167 | 218 | activeAssets: this.props.activeAssets, |
168 | 219 | qualifier: this.props.qualifier, |
| 220 | + ignoreNonCdkStacks: this.props.ignoreNonCdkStacks, |
| 221 | + skipUnauthorizedStacks: this.props.skipUnauthorizedStacks, |
169 | 222 | }); |
170 | 223 | this.justRefreshedStacks(); |
171 | 224 |
|
|
0 commit comments