1- import type { StackCollection } from './stack-collection' ;
1+ import type * as cxapi from '@aws-cdk/cx-api' ;
2+ import * as chalk from 'chalk' ;
3+ import { minimatch } from 'minimatch' ;
4+ import { StackCollection } from './stack-collection' ;
5+ import { flatten } from '../../util' ;
6+ import { IO } from '../io/private' ;
7+ import type { IoHelper } from '../io/private/io-helper' ;
28
39export interface IStackAssembly {
410 /**
@@ -11,3 +17,170 @@ export interface IStackAssembly {
1117 */
1218 stackById ( stackId : string ) : StackCollection ;
1319}
20+
21+ /**
22+ * When selecting stacks, what other stacks to include because of dependencies
23+ */
24+ export enum ExtendedStackSelection {
25+ /**
26+ * Don't select any extra stacks
27+ */
28+ None ,
29+
30+ /**
31+ * Include stacks that this stack depends on
32+ */
33+ Upstream ,
34+
35+ /**
36+ * Include stacks that depend on this stack
37+ */
38+ Downstream ,
39+ }
40+
41+ /**
42+ * A single Cloud Assembly and the operations we do on it to deploy the artifacts inside
43+ */
44+ export abstract class BaseStackAssembly implements IStackAssembly {
45+ /**
46+ * Sanitize a list of stack match patterns
47+ */
48+ protected static sanitizePatterns ( patterns : string [ ] ) : string [ ] {
49+ let sanitized = patterns . filter ( s => s != null ) ; // filter null/undefined
50+ sanitized = [ ...new Set ( sanitized ) ] ; // make them unique
51+ return sanitized ;
52+ }
53+
54+ /**
55+ * The directory this CloudAssembly was read from
56+ */
57+ public readonly directory : string ;
58+
59+ /**
60+ * The IoHelper used for messaging
61+ */
62+ protected readonly ioHelper : IoHelper ;
63+
64+ constructor ( public readonly assembly : cxapi . CloudAssembly , ioHelper : IoHelper ) {
65+ this . directory = assembly . directory ;
66+ this . ioHelper = ioHelper ;
67+ }
68+
69+ /**
70+ * Select a single stack by its ID
71+ */
72+ public stackById ( stackId : string ) {
73+ return new StackCollection ( this , [ this . assembly . getStackArtifact ( stackId ) ] ) ;
74+ }
75+
76+ protected async selectMatchingStacks (
77+ stacks : cxapi . CloudFormationStackArtifact [ ] ,
78+ patterns : string [ ] ,
79+ extend : ExtendedStackSelection = ExtendedStackSelection . None ,
80+ ) : Promise < StackCollection > {
81+ const matchingPattern = ( pattern : string ) => ( stack : cxapi . CloudFormationStackArtifact ) => minimatch ( stack . hierarchicalId , pattern ) ;
82+ const matchedStacks = flatten ( patterns . map ( pattern => stacks . filter ( matchingPattern ( pattern ) ) ) ) ;
83+
84+ return this . extendStacks ( matchedStacks , stacks , extend ) ;
85+ }
86+
87+ protected async extendStacks (
88+ matched : cxapi . CloudFormationStackArtifact [ ] ,
89+ all : cxapi . CloudFormationStackArtifact [ ] ,
90+ extend : ExtendedStackSelection = ExtendedStackSelection . None ,
91+ ) {
92+ const allStacks = new Map < string , cxapi . CloudFormationStackArtifact > ( ) ;
93+ for ( const stack of all ) {
94+ allStacks . set ( stack . hierarchicalId , stack ) ;
95+ }
96+
97+ const index = indexByHierarchicalId ( matched ) ;
98+
99+ switch ( extend ) {
100+ case ExtendedStackSelection . Downstream :
101+ await includeDownstreamStacks ( this . ioHelper , index , allStacks ) ;
102+ break ;
103+ case ExtendedStackSelection . Upstream :
104+ await includeUpstreamStacks ( this . ioHelper , index , allStacks ) ;
105+ break ;
106+ }
107+
108+ // Filter original array because it is in the right order
109+ const selectedList = all . filter ( s => index . has ( s . hierarchicalId ) ) ;
110+
111+ return new StackCollection ( this , selectedList ) ;
112+ }
113+ }
114+
115+ function indexByHierarchicalId ( stacks : cxapi . CloudFormationStackArtifact [ ] ) : Map < string , cxapi . CloudFormationStackArtifact > {
116+ const result = new Map < string , cxapi . CloudFormationStackArtifact > ( ) ;
117+
118+ for ( const stack of stacks ) {
119+ result . set ( stack . hierarchicalId , stack ) ;
120+ }
121+
122+ return result ;
123+ }
124+
125+ /**
126+ * Calculate the transitive closure of stack dependents.
127+ *
128+ * Modifies `selectedStacks` in-place.
129+ */
130+ async function includeDownstreamStacks (
131+ ioHelper : IoHelper ,
132+ selectedStacks : Map < string , cxapi . CloudFormationStackArtifact > ,
133+ allStacks : Map < string , cxapi . CloudFormationStackArtifact > ,
134+ ) {
135+ const added = new Array < string > ( ) ;
136+
137+ let madeProgress ;
138+ do {
139+ madeProgress = false ;
140+
141+ for ( const [ id , stack ] of allStacks ) {
142+ // Select this stack if it's not selected yet AND it depends on a stack that's in the selected set
143+ if ( ! selectedStacks . has ( id ) && ( stack . dependencies || [ ] ) . some ( dep => selectedStacks . has ( dep . id ) ) ) {
144+ selectedStacks . set ( id , stack ) ;
145+ added . push ( id ) ;
146+ madeProgress = true ;
147+ }
148+ }
149+ } while ( madeProgress ) ;
150+
151+ if ( added . length > 0 ) {
152+ await ioHelper . notify ( IO . DEFAULT_ASSEMBLY_INFO . msg ( `Including depending stacks: ${ chalk . bold ( added . join ( ', ' ) ) } ` ) ) ;
153+ }
154+ }
155+
156+ /**
157+ * Calculate the transitive closure of stack dependencies.
158+ *
159+ * Modifies `selectedStacks` in-place.
160+ */
161+ async function includeUpstreamStacks (
162+ ioHelper : IoHelper ,
163+ selectedStacks : Map < string , cxapi . CloudFormationStackArtifact > ,
164+ allStacks : Map < string , cxapi . CloudFormationStackArtifact > ,
165+ ) {
166+ const added = new Array < string > ( ) ;
167+ let madeProgress = true ;
168+ while ( madeProgress ) {
169+ madeProgress = false ;
170+
171+ for ( const stack of selectedStacks . values ( ) ) {
172+ // Select an additional stack if it's not selected yet and a dependency of a selected stack (and exists, obviously)
173+ for ( const dependencyId of stack . dependencies . map ( x => x . manifest . displayName ?? x . id ) ) {
174+ if ( ! selectedStacks . has ( dependencyId ) && allStacks . has ( dependencyId ) ) {
175+ added . push ( dependencyId ) ;
176+ selectedStacks . set ( dependencyId , allStacks . get ( dependencyId ) ! ) ;
177+ madeProgress = true ;
178+ }
179+ }
180+ }
181+ }
182+
183+ if ( added . length > 0 ) {
184+ await ioHelper . notify ( IO . DEFAULT_ASSEMBLY_INFO . msg ( `Including dependency stacks: ${ chalk . bold ( added . join ( ', ' ) ) } ` ) ) ;
185+ }
186+ }
0 commit comments