packages/ros-cdk-cli/lib/api/cloud-assembly.ts (182 lines of code) (raw):

import * as cxapi from '@alicloud/ros-cdk-cxapi'; import * as colors from 'colors/safe'; import * as minimatch from 'minimatch'; import { error, print, warning } from '../logging'; export enum DefaultSelection { /** * Returns an empty selection in case there are no selectors. */ None = 'none', /** * If the app includes a single stack, returns it. Otherwise throws an exception. * This behavior is used by "deploy". */ OnlySingle = 'single', /** * If no selectors are provided, returns all stacks in the app. */ AllStacks = 'all', } export interface SelectStacksOptions { /** * Extend the selection to upstread/downstream stacks * @default ExtendedStackSelection.None only select the specified stacks. */ extend?: ExtendedStackSelection; /** * The behavior if if no selectors are privided. */ defaultBehavior: DefaultSelection; } /** * When selecting stacks, what other stacks to include because of dependencies */ export enum ExtendedStackSelection { /** * Don't select any extra stacks */ None, /** * Include stacks that this stack depends on */ Upstream, /** * Include stacks that depend on this stack */ Downstream, } /** * A single Cloud Assembly and the operations we do on it to deploy the artifacts inside */ export class CloudAssembly { /** * The directory this CloudAssembly was read from */ public readonly directory: string; constructor(public readonly assembly: cxapi.CloudAssembly) { this.directory = assembly.directory; } public async selectStacks(selectors: string[], options: SelectStacksOptions): Promise<StackCollection> { selectors = selectors.filter((s) => s != null); // filter null/undefined const stacks = this.assembly.stacks; if (stacks.length === 0) { throw new Error('This app contains no stacks'); } if (selectors.length === 0) { switch (options.defaultBehavior) { case DefaultSelection.AllStacks: return new StackCollection(this, stacks); case DefaultSelection.None: return new StackCollection(this, []); case DefaultSelection.OnlySingle: if (stacks.length === 1) { return new StackCollection(this, stacks); } else { throw new Error( 'Since this app includes more than a single stack, specify which stacks to use (wildcards are supported)\n' + `Stacks: ${stacks.map((x) => x.id).join(' ')}`, ); } default: throw new Error(`invalid default behavior: ${options.defaultBehavior}`); } } const allStacks = new Map<string, cxapi.RosStackArtifact>(); for (const stack of stacks) { allStacks.set(stack.id, stack); } // For every selector argument, pick stacks from the list. const selectedStacks = new Map<string, cxapi.RosStackArtifact>(); for (const pattern of selectors) { let found = false; for (const stack of stacks) { if (minimatch(stack.id, pattern) && !selectedStacks.has(stack.id)) { selectedStacks.set(stack.id, stack); found = true; } } if (!found) { throw new Error(`No stack found matching '${pattern}'. Use "list" to print manifest`); } } const extend = options.extend || ExtendedStackSelection.None; switch (extend) { case ExtendedStackSelection.Downstream: includeDownstreamStacks(selectedStacks, allStacks); break; case ExtendedStackSelection.Upstream: includeUpstreamStacks(selectedStacks, allStacks); break; } // Filter original array because it is in the right order const selectedList = stacks.filter((s) => selectedStacks.has(s.id)); return new StackCollection(this, selectedList); } /** * Select a single stack by its ID */ public stackById(stackId: string) { return new StackCollection(this, [this.assembly.getStackArtifact(stackId)]); } } /** * A collection of stacks and related artifacts * * In practice, not all artifacts in the CloudAssembly are created equal; * stacks can be selected independently, but other artifacts such as asset * bundles cannot. */ export class StackCollection { constructor(public readonly assembly: CloudAssembly, public readonly stackArtifacts: cxapi.RosStackArtifact[]) {} public get stackCount() { return this.stackArtifacts.length; } public get firstStack() { if (this.stackCount < 1) { throw new Error('StackCollection contains no stack artifacts (trying to access the first one)'); } return this.stackArtifacts[0]; } public get stackIds(): string[] { return this.stackArtifacts.map((s) => s.id); } public reversed() { const arts = [...this.stackArtifacts]; arts.reverse(); return new StackCollection(this.assembly, arts); } public processMetadataMessages(options: MetadataMessageOptions = {}) { let warnings = false; let errors = false; for (const stack of this.stackArtifacts) { for (const message of stack.messages) { switch (message.level) { case cxapi.SynthesisMessageLevel.WARNING: warnings = true; printMessage(warning, 'Warning', message.id, message.entry); break; case cxapi.SynthesisMessageLevel.ERROR: errors = true; printMessage(error, 'Error', message.id, message.entry); break; case cxapi.SynthesisMessageLevel.INFO: printMessage(print, 'Info', message.id, message.entry); break; } } } if (errors && !options.ignoreErrors) { throw new Error('Found errors'); } if (options.strict && warnings) { throw new Error('Found warnings (--strict mode)'); } function printMessage(logFn: (s: string) => void, prefix: string, id: string, entry: cxapi.MetadataEntry) { logFn(`[${prefix} at ${id}] ${entry.data}`); if (options.verbose && entry.trace) { logFn(` ${entry.trace.join('\n ')}`); } } } } export interface MetadataMessageOptions { /** * Whether to be verbose * * @default false */ verbose?: boolean; /** * Don't stop on error metadata * * @default false */ ignoreErrors?: boolean; /** * Treat warnings in metadata as errors * * @default false */ strict?: boolean; } /** * Calculate the transitive closure of stack dependents. * * Modifies `selectedStacks` in-place. */ function includeDownstreamStacks( selectedStacks: Map<string, cxapi.RosStackArtifact>, allStacks: Map<string, cxapi.RosStackArtifact>, ) { const added = new Array<string>(); let madeProgress; do { madeProgress = false; for (const [id, stack] of allStacks) { // Select this stack if it's not selected yet AND it depends on a stack that's in the selected set if (!selectedStacks.has(id) && (stack.dependencies || []).some((dep) => selectedStacks.has(dep.id))) { selectedStacks.set(id, stack); added.push(id); madeProgress = true; } } } while (madeProgress); if (added.length > 0) { print('Including depending stacks: %s', colors.bold(added.join(', '))); } } /** * Calculate the transitive closure of stack dependencies. * * Modifies `selectedStacks` in-place. */ function includeUpstreamStacks( selectedStacks: Map<string, cxapi.RosStackArtifact>, allStacks: Map<string, cxapi.RosStackArtifact>, ) { const added = new Array<string>(); let madeProgress = true; while (madeProgress) { madeProgress = false; for (const stack of selectedStacks.values()) { if (stack.dependencies === undefined) continue; // Select an additional stack if it's not selected yet and a dependency of a selected stack (and exists, obviously) for (const dependencyId of stack.dependencies.map((x) => x.id)) { if (!selectedStacks.has(dependencyId) && allStacks.has(dependencyId)) { added.push(dependencyId); selectedStacks.set(dependencyId, allStacks.get(dependencyId)!); madeProgress = true; } } } } if (added.length > 0) { print('Including dependency stacks: %s', colors.bold(added.join(', '))); } }