Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as path from 'path';
import { format } from 'util';
import * as cxapi from '@aws-cdk/cx-api';
import * as fs from 'fs-extra';
import type { SdkProvider } from '../aws-auth';
import type { Settings } from '../settings';

/**
* If we don't have region/account defined in context, we fall back to the default SDK behavior
* where region is retrieved from ~/.aws/config and account is based on default credentials provider
* chain and then STS is queried.
*
* This is done opportunistically: for example, if we can't access STS for some reason or the region
* is not configured, the context value will be 'null' and there could failures down the line. In
* some cases, synthesis does not require region/account information at all, so that might be perfectly
* fine in certain scenarios.
*
* @param context The context key/value bash.
*/
export async function prepareDefaultEnvironment(
aws: SdkProvider,
debugFn: (msg: string) => Promise<void>,
): Promise<{ [key: string]: string }> {
const env: { [key: string]: string } = { };

env[cxapi.DEFAULT_REGION_ENV] = aws.defaultRegion;
await debugFn(`Setting "${cxapi.DEFAULT_REGION_ENV}" environment variable to ${env[cxapi.DEFAULT_REGION_ENV]}`);

const accountId = (await aws.defaultAccount())?.accountId;
if (accountId) {
env[cxapi.DEFAULT_ACCOUNT_ENV] = accountId;
await debugFn(`Setting "${cxapi.DEFAULT_ACCOUNT_ENV}" environment variable to ${env[cxapi.DEFAULT_ACCOUNT_ENV]}`);
}

return env;
}

/**
* Settings related to synthesis are read from context.
* The merging of various configuration sources like cli args or cdk.json has already happened.
* We now need to set the final values to the context.
*/
export async function prepareContext(
settings: Settings,
context: {[key: string]: any},
env: { [key: string]: string | undefined},
debugFn: (msg: string) => Promise<void>,
) {
const debugMode: boolean = settings.get(['debug']) ?? true;
if (debugMode) {
env.CDK_DEBUG = 'true';
}

const pathMetadata: boolean = settings.get(['pathMetadata']) ?? true;
if (pathMetadata) {
context[cxapi.PATH_METADATA_ENABLE_CONTEXT] = true;
}

const assetMetadata: boolean = settings.get(['assetMetadata']) ?? true;
if (assetMetadata) {
context[cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT] = true;
}

const versionReporting: boolean = settings.get(['versionReporting']) ?? true;
if (versionReporting) {
context[cxapi.ANALYTICS_REPORTING_ENABLED_CONTEXT] = true;
}
// We need to keep on doing this for framework version from before this flag was deprecated.
if (!versionReporting) {
context['aws:cdk:disable-version-reporting'] = true;
}

const stagingEnabled = settings.get(['staging']) ?? true;
if (!stagingEnabled) {
context[cxapi.DISABLE_ASSET_STAGING_CONTEXT] = true;
}

const bundlingStacks = settings.get(['bundlingStacks']) ?? ['**'];
context[cxapi.BUNDLING_STACKS] = bundlingStacks;

await debugFn(format('context:', context));

return context;
}

export function spaceAvailableForContext(env: { [key: string]: string }, limit: number) {
const size = (value: string) => value != null ? Buffer.byteLength(value) : 0;

const usedSpace = Object.entries(env)
.map(([k, v]) => k === cxapi.CONTEXT_ENV ? size(k) : size(k) + size(v))
.reduce((a, b) => a + b, 0);

return Math.max(0, limit - usedSpace);
}

/**
* Guess the executable from the command-line argument
*
* Only do this if the file is NOT marked as executable. If it is,
* we'll defer to the shebang inside the file itself.
*
* If we're on Windows, we ALWAYS take the handler, since it's hard to
* verify if registry associations have or have not been set up for this
* file type, so we'll assume the worst and take control.
*/
export async function guessExecutable(app: string, debugFn: (msg: string) => Promise<void>) {
const commandLine = appToArray(app);
if (commandLine.length === 1) {
let fstat;

try {
fstat = await fs.stat(commandLine[0]);
} catch {
await debugFn(`Not a file: '${commandLine[0]}'. Using '${commandLine}' as command-line`);
return commandLine;
}

// eslint-disable-next-line no-bitwise
const isExecutable = (fstat.mode & fs.constants.X_OK) !== 0;
const isWindows = process.platform === 'win32';

const handler = EXTENSION_MAP.get(path.extname(commandLine[0]));
if (handler && (!isExecutable || isWindows)) {
return handler(commandLine[0]);
}
}
return commandLine;
}

/**
* Mapping of extensions to command-line generators
*/
const EXTENSION_MAP = new Map<string, CommandGenerator>([
['.js', executeNode],
]);

type CommandGenerator = (file: string) => string[];

/**
* Execute the given file with the same 'node' process as is running the current process
*/
function executeNode(scriptFile: string): string[] {
return [process.execPath, scriptFile];
}

/**
* Make sure the 'app' is an array
*
* If it's a string, split on spaces as a trivial way of tokenizing the command line.
*/
function appToArray(app: any) {
return typeof app === 'string' ? app.split(' ') : app;
}
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './environment';
export * from './stack-assembly';
export * from './stack-collection';
export * from './stack-selector';
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import type * as cxapi from '@aws-cdk/cx-api';
import * as chalk from 'chalk';
import { minimatch } from 'minimatch';
import { StackCollection } from './stack-collection';
import { flatten } from '../../util';
import { IO } from '../io/private';
import type { IoHelper } from '../io/private/io-helper';

export interface IStackAssembly {
/**
* The directory this CloudAssembly was read from
*/
directory: string;

/**
* Select a single stack by its ID
*/
stackById(stackId: string): StackCollection;
}

/**
* 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 abstract class BaseStackAssembly implements IStackAssembly {
/**
* Sanitize a list of stack match patterns
*/
protected static sanitizePatterns(patterns: string[]): string[] {
let sanitized = patterns.filter(s => s != null); // filter null/undefined
sanitized = [...new Set(sanitized)]; // make them unique
return sanitized;
}

/**
* The directory this CloudAssembly was read from
*/
public readonly directory: string;

/**
* The IoHelper used for messaging
*/
protected readonly ioHelper: IoHelper;

constructor(public readonly assembly: cxapi.CloudAssembly, ioHelper: IoHelper) {
this.directory = assembly.directory;
this.ioHelper = ioHelper;
}

/**
* Select a single stack by its ID
*/
public stackById(stackId: string) {
return new StackCollection(this, [this.assembly.getStackArtifact(stackId)]);
}

protected async selectMatchingStacks(
stacks: cxapi.CloudFormationStackArtifact[],
patterns: string[],
extend: ExtendedStackSelection = ExtendedStackSelection.None,
): Promise<StackCollection> {
const matchingPattern = (pattern: string) => (stack: cxapi.CloudFormationStackArtifact) => minimatch(stack.hierarchicalId, pattern);
const matchedStacks = flatten(patterns.map(pattern => stacks.filter(matchingPattern(pattern))));

return this.extendStacks(matchedStacks, stacks, extend);
}

protected async extendStacks(
matched: cxapi.CloudFormationStackArtifact[],
all: cxapi.CloudFormationStackArtifact[],
extend: ExtendedStackSelection = ExtendedStackSelection.None,
) {
const allStacks = new Map<string, cxapi.CloudFormationStackArtifact>();
for (const stack of all) {
allStacks.set(stack.hierarchicalId, stack);
}

const index = indexByHierarchicalId(matched);

switch (extend) {
case ExtendedStackSelection.Downstream:
await includeDownstreamStacks(this.ioHelper, index, allStacks);
break;
case ExtendedStackSelection.Upstream:
await includeUpstreamStacks(this.ioHelper, index, allStacks);
break;
}

// Filter original array because it is in the right order
const selectedList = all.filter(s => index.has(s.hierarchicalId));

return new StackCollection(this, selectedList);
}
}

function indexByHierarchicalId(stacks: cxapi.CloudFormationStackArtifact[]): Map<string, cxapi.CloudFormationStackArtifact> {
const result = new Map<string, cxapi.CloudFormationStackArtifact>();

for (const stack of stacks) {
result.set(stack.hierarchicalId, stack);
}

return result;
}

/**
* Calculate the transitive closure of stack dependents.
*
* Modifies `selectedStacks` in-place.
*/
async function includeDownstreamStacks(
ioHelper: IoHelper,
selectedStacks: Map<string, cxapi.CloudFormationStackArtifact>,
allStacks: Map<string, cxapi.CloudFormationStackArtifact>,
) {
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) {
await ioHelper.notify(IO.DEFAULT_ASSEMBLY_INFO.msg(`Including depending stacks: ${chalk.bold(added.join(', '))}`));
}
}

/**
* Calculate the transitive closure of stack dependencies.
*
* Modifies `selectedStacks` in-place.
*/
async function includeUpstreamStacks(
ioHelper: IoHelper,
selectedStacks: Map<string, cxapi.CloudFormationStackArtifact>,
allStacks: Map<string, cxapi.CloudFormationStackArtifact>,
) {
const added = new Array<string>();
let madeProgress = true;
while (madeProgress) {
madeProgress = false;

for (const stack of selectedStacks.values()) {
// 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.manifest.displayName ?? x.id)) {
if (!selectedStacks.has(dependencyId) && allStacks.has(dependencyId)) {
added.push(dependencyId);
selectedStacks.set(dependencyId, allStacks.get(dependencyId)!);
madeProgress = true;
}
}
}
}

if (added.length > 0) {
await ioHelper.notify(IO.DEFAULT_ASSEMBLY_INFO.msg(`Including dependency stacks: ${chalk.bold(added.join(', '))}`));
}
}
Loading
Loading