packages/@aws-cdk/toolkit-lib/lib/api/environment/environment-resources.ts (157 lines of code) (raw):
import type { Environment } from '@aws-cdk/cx-api';
import { formatErrorMessage } from '../../util';
import type { SDK } from '../aws-auth/private';
import { IO, type IoHelper } from '../io/private';
import { Notices } from '../notices';
import { ToolkitError } from '../toolkit-error';
import { type EcrRepositoryInfo, ToolkitInfo } from '../toolkit-info';
/**
* Registry class for `EnvironmentResources`.
*
* The state management of this class is a bit non-standard. We want to cache
* data related to toolkit stacks and SSM parameters, but we are not in charge
* of ensuring caching of SDKs. Since `EnvironmentResources` needs an SDK to
* function, we treat it as an ephemeral class, and store the actual cached data
* in `EnvironmentResourcesRegistry`.
*/
export class EnvironmentResourcesRegistry {
private readonly cache = new Map<string, EnvironmentCache>();
constructor(private readonly toolkitStackName?: string) {
}
public for(resolvedEnvironment: Environment, sdk: SDK, ioHelper: IoHelper) {
const key = `${resolvedEnvironment.account}:${resolvedEnvironment.region}`;
let envCache = this.cache.get(key);
if (!envCache) {
envCache = emptyCache();
this.cache.set(key, envCache);
}
return new EnvironmentResources(resolvedEnvironment, sdk, ioHelper, envCache, this.toolkitStackName);
}
}
/**
* Interface with the account and region we're deploying into
*
* Manages lookups for bootstrapped resources, falling back to the legacy "CDK Toolkit"
* original bootstrap stack if necessary.
*
* The state management of this class is a bit non-standard. We want to cache
* data related to toolkit stacks and SSM parameters, but we are not in charge
* of ensuring caching of SDKs. Since `EnvironmentResources` needs an SDK to
* function, we treat it as an ephemeral class, and store the actual cached data
* in `EnvironmentResourcesRegistry`.
*/
export class EnvironmentResources {
constructor(
public readonly environment: Environment,
private readonly sdk: SDK,
private readonly ioHelper: IoHelper,
private readonly cache: EnvironmentCache,
private readonly toolkitStackName?: string,
) {
}
/**
* Look up the toolkit for a given environment, using a given SDK
*/
public async lookupToolkit() {
if (!this.cache.toolkitInfo) {
this.cache.toolkitInfo = await ToolkitInfo.lookup(this.environment, this.sdk, this.ioHelper, this.toolkitStackName);
}
return this.cache.toolkitInfo;
}
/**
* Validate that the bootstrap stack version matches or exceeds the expected version
*
* Use the SSM parameter name to read the version number if given, otherwise use the version
* discovered on the bootstrap stack.
*
* Pass in the SSM parameter name so we can cache the lookups an don't need to do the same
* lookup again and again for every artifact.
*/
public async validateVersion(expectedVersion: number | undefined, ssmParameterName: string | undefined) {
if (expectedVersion === undefined) {
// No requirement
return;
}
const defExpectedVersion = expectedVersion;
if (ssmParameterName !== undefined) {
try {
doValidate(await this.versionFromSsmParameter(ssmParameterName), this.environment);
return;
} catch (e: any) {
if (e.name !== 'AccessDeniedException') {
throw e;
}
// This is a fallback! The bootstrap template that goes along with this change introduces
// a new 'ssm:GetParameter' permission, but when run using the previous bootstrap template we
// won't have the permissions yet to read the version, so we won't be able to show the
// message telling the user they need to update! When we see an AccessDeniedException, fall
// back to the version we read from Stack Outputs; but ONLY if the version we discovered via
// outputs is legitimately an old version. If it's newer than that, something else must be broken,
// so let it fail as it would if we didn't have this fallback.
const bootstrapStack = await this.lookupToolkit();
if (bootstrapStack.found && bootstrapStack.version < BOOTSTRAP_TEMPLATE_VERSION_INTRODUCING_GETPARAMETER) {
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(
`Could not read SSM parameter ${ssmParameterName}: ${formatErrorMessage(e)}, falling back to version from ${bootstrapStack}`,
));
doValidate(bootstrapStack.version, this.environment);
return;
}
throw new ToolkitError(
`This CDK deployment requires bootstrap stack version '${expectedVersion}', but during the confirmation via SSM parameter ${ssmParameterName} the following error occurred: ${e}`,
);
}
}
// No SSM parameter
const bootstrapStack = await this.lookupToolkit();
doValidate(bootstrapStack.version, this.environment);
function doValidate(version: number, environment: Environment) {
const notices = Notices.get();
if (notices) {
// if `Notices` hasn't been initialized there is probably a good
// reason for it. handle gracefully.
notices.addBootstrappedEnvironment({ bootstrapStackVersion: version, environment });
}
if (defExpectedVersion > version) {
throw new ToolkitError(
`This CDK deployment requires bootstrap stack version '${expectedVersion}', found '${version}'. Please run 'cdk bootstrap'.`,
);
}
}
}
/**
* Read a version from an SSM parameter, cached
*/
public async versionFromSsmParameter(parameterName: string): Promise<number> {
const existing = this.cache.ssmParameters.get(parameterName);
if (existing !== undefined) {
return existing;
}
const ssm = this.sdk.ssm();
try {
const result = await ssm.getParameter({ Name: parameterName });
const asNumber = parseInt(`${result.Parameter?.Value}`, 10);
if (isNaN(asNumber)) {
throw new ToolkitError(`SSM parameter ${parameterName} not a number: ${result.Parameter?.Value}`);
}
this.cache.ssmParameters.set(parameterName, asNumber);
return asNumber;
} catch (e: any) {
if (e.name === 'ParameterNotFound') {
throw new ToolkitError(
`SSM parameter ${parameterName} not found. Has the environment been bootstrapped? Please run \'cdk bootstrap\' (see https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html)`,
);
}
throw e;
}
}
public async prepareEcrRepository(repositoryName: string): Promise<EcrRepositoryInfo> {
if (!this.sdk) {
throw new ToolkitError('ToolkitInfo needs to have been initialized with an sdk to call prepareEcrRepository');
}
const ecr = this.sdk.ecr();
// check if repo already exists
try {
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${repositoryName}: checking if ECR repository already exists`));
const describeResponse = await ecr.describeRepositories({
repositoryNames: [repositoryName],
});
const existingRepositoryUri = describeResponse.repositories![0]?.repositoryUri;
if (existingRepositoryUri) {
return { repositoryUri: existingRepositoryUri };
}
} catch (e: any) {
if (e.name !== 'RepositoryNotFoundException') {
throw e;
}
}
// create the repo (tag it so it will be easier to garbage collect in the future)
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${repositoryName}: creating ECR repository`));
const assetTag = { Key: 'awscdk:asset', Value: 'true' };
const response = await ecr.createRepository({
repositoryName,
tags: [assetTag],
});
const repositoryUri = response.repository?.repositoryUri;
if (!repositoryUri) {
throw new ToolkitError(`CreateRepository did not return a repository URI for ${repositoryUri}`);
}
// configure image scanning on push (helps in identifying software vulnerabilities, no additional charge)
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`${repositoryName}: enable image scanning`));
await ecr.putImageScanningConfiguration({
repositoryName,
imageScanningConfiguration: { scanOnPush: true },
});
return { repositoryUri };
}
}
export class NoBootstrapStackEnvironmentResources extends EnvironmentResources {
constructor(environment: Environment, sdk: SDK, ioHelper: IoHelper) {
super(environment, sdk, ioHelper, emptyCache());
}
/**
* Look up the toolkit for a given environment, using a given SDK
*/
public async lookupToolkit(): Promise<ToolkitInfo> {
throw new ToolkitError(
'Trying to perform an operation that requires a bootstrap stack; you should not see this error, this is a bug in the CDK CLI.',
);
}
}
/**
* Data that is cached on a per-environment level
*
* This cache may be shared between different instances of the `EnvironmentResources` class.
*/
interface EnvironmentCache {
readonly ssmParameters: Map<string, number>;
toolkitInfo?: ToolkitInfo;
}
function emptyCache(): EnvironmentCache {
return {
ssmParameters: new Map(),
toolkitInfo: undefined,
};
}
/**
* The bootstrap template version that introduced ssm:GetParameter
*/
const BOOTSTRAP_TEMPLATE_VERSION_INTRODUCING_GETPARAMETER = 5;