|
| 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'; |
| 8 | + |
| 9 | +export interface IStackAssembly { |
| 10 | + /** |
| 11 | + * The directory this CloudAssembly was read from |
| 12 | + */ |
| 13 | + directory: string; |
| 14 | + |
| 15 | + /** |
| 16 | + * Select a single stack by its ID |
| 17 | + */ |
| 18 | + stackById(stackId: string): StackCollection; |
| 19 | +} |
| 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