Skip to content

Commit

Permalink
CLI options
Browse files Browse the repository at this point in the history
  • Loading branch information
comcalvi committed Dec 13, 2023
1 parent 7c6287b commit da00e1a
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 149 deletions.
141 changes: 2 additions & 139 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as cxapi from '@aws-cdk/cx-api';
import type { CloudFormation } from 'aws-sdk';
import * as chalk from 'chalk';
import * as fs from 'fs-extra';
import * as uuid from 'uuid';
import { ISDK, SdkProvider } from './aws-auth';
import { EnvironmentResources } from './environment-resources';
Expand All @@ -13,18 +12,12 @@ import {
waitForStackDeploy, waitForStackDelete, ParameterValues, ParameterChanges, ResourcesToImport,
} from './util/cloudformation';
import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
import { TemplateBodyParameter, makeBodyParameter } from './util/template-body-parameter';
import { addMetadataAssetsToManifest } from '../assets';
import { Tag } from '../cdk-toolkit';
import { debug, error, print, warning } from '../logging';
import { toYAML } from '../serialize';
import { debug, print, warning } from '../logging';
import { AssetManifestBuilder } from '../util/asset-manifest-builder';
import { publishAssets } from '../util/asset-publishing';
import { contentHash } from '../util/content-hash';

export type TemplateBodyParameter = {
TemplateBody?: string
TemplateURL?: string
};

export interface DeployStackResult {
readonly noOp: boolean;
Expand Down Expand Up @@ -233,8 +226,6 @@ export interface ChangeSetDeploymentMethod {
readonly changeSetName?: string;
}

const LARGE_TEMPLATE_SIZE_KB = 50;

export async function deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
const stackArtifact = options.stack;

Expand Down Expand Up @@ -559,100 +550,6 @@ class FullCloudFormationDeployment {
}
}

/**
* Prepares the body parameter for +CreateChangeSet+.
*
* If the template is small enough to be inlined into the API call, just return
* it immediately.
*
* Otherwise, add it to the asset manifest to get uploaded to the staging
* bucket and return its coordinates. If there is no staging bucket, an error
* is thrown.
*
* @param stack the synthesized stack that provides the CloudFormation template
* @param toolkitInfo information about the toolkit stack
*/
export async function makeBodyParameter(
stack: cxapi.CloudFormationStackArtifact,
resolvedEnvironment: cxapi.Environment,
assetManifest: AssetManifestBuilder,
resources: EnvironmentResources,
sdk: ISDK,
overrideTemplate?: any,
): Promise<TemplateBodyParameter> {

// If the template has already been uploaded to S3, just use it from there.
if (stack.stackTemplateAssetObjectUrl && !overrideTemplate) {
return { TemplateURL: restUrlFromManifest(stack.stackTemplateAssetObjectUrl, resolvedEnvironment, sdk) };
}

// Otherwise, pass via API call (if small) or upload here (if large)
const templateJson = toYAML(overrideTemplate ?? stack.template);

if (templateJson.length <= LARGE_TEMPLATE_SIZE_KB * 1024) {
return { TemplateBody: templateJson };
}

const toolkitInfo = await resources.lookupToolkit();
if (!toolkitInfo.found) {
error(
`The template for stack "${stack.displayName}" is ${Math.round(templateJson.length / 1024)}KiB. ` +
`Templates larger than ${LARGE_TEMPLATE_SIZE_KB}KiB must be uploaded to S3.\n` +
'Run the following command in order to setup an S3 bucket in this environment, and then re-deploy:\n\n',
chalk.blue(`\t$ cdk bootstrap ${resolvedEnvironment.name}\n`));

throw new Error('Template too large to deploy ("cdk bootstrap" is required)');
}

const templateHash = contentHash(templateJson);
const key = `cdk/${stack.id}/${templateHash}.yml`;

let templateFile = stack.templateFile;
if (overrideTemplate) {
// Add a variant of this template
templateFile = `${stack.templateFile}-${templateHash}.yaml`;
await fs.writeFile(templateFile, templateJson, { encoding: 'utf-8' });
}

assetManifest.addFileAsset(templateHash, {
path: templateFile,
}, {
bucketName: toolkitInfo.bucketName,
objectKey: key,
});

const templateURL = `${toolkitInfo.bucketUrl}/${key}`;
debug('Storing template in S3 at:', templateURL);
return { TemplateURL: templateURL };
}

/**
* Prepare a body parameter for CFN, performing the upload
*
* Return it as-is if it is small enough to pass in the API call,
* upload to S3 and return the coordinates if it is not.
*/
export async function makeBodyParameterAndUpload(
stack: cxapi.CloudFormationStackArtifact,
resolvedEnvironment: cxapi.Environment,
resources: EnvironmentResources,
sdkProvider: SdkProvider,
sdk: ISDK,
overrideTemplate?: any): Promise<TemplateBodyParameter> {

// We don't have access to the actual asset manifest here, so pretend that the
// stack doesn't have a pre-published URL.
const forceUploadStack = Object.create(stack, {
stackTemplateAssetObjectUrl: { value: undefined },
});

const builder = new AssetManifestBuilder();
const bodyparam = await makeBodyParameter(forceUploadStack, resolvedEnvironment, builder, resources, sdk, overrideTemplate);
const manifest = builder.toManifest(stack.assembly.directory);
await publishAssets(manifest, sdkProvider, resolvedEnvironment, { quiet: true });
return bodyparam;
}

export interface DestroyStackOptions {
/**
* The stack to be destroyed
Expand Down Expand Up @@ -783,40 +680,6 @@ function compareTags(a: Tag[], b: Tag[]): boolean {
return true;
}

/**
* Format an S3 URL in the manifest for use with CloudFormation
*
* Replaces environment placeholders (which this field may contain),
* and reformats s3://.../... urls into S3 REST URLs (which CloudFormation
* expects)
*/
function restUrlFromManifest(url: string, environment: cxapi.Environment, sdk: ISDK): string {
const doNotUseMarker = '**DONOTUSE**';
// This URL may contain placeholders, so still substitute those.
url = cxapi.EnvironmentPlaceholders.replace(url, {
accountId: environment.account,
region: environment.region,
partition: doNotUseMarker,
});

// Yes, this is extremely crude, but we don't actually need this so I'm not inclined to spend
// a lot of effort trying to thread the right value to this location.
if (url.indexOf(doNotUseMarker) > -1) {
throw new Error('Cannot use \'${AWS::Partition}\' in the \'stackTemplateAssetObjectUrl\' field');
}

const s3Url = url.match(/s3:\/\/([^/]+)\/(.*)$/);
if (!s3Url) { return url; }

// We need to pass an 'https://s3.REGION.amazonaws.com[.cn]/bucket/object' URL to CloudFormation, but we
// got an 's3://bucket/object' URL instead. Construct the rest API URL here.
const bucketName = s3Url[1];
const objectKey = s3Url[2];

const urlSuffix: string = sdk.getEndpointSuffix(environment.region);
return `https://s3.${environment.region}.${urlSuffix}/${bucketName}/${objectKey}`;
}

function suffixWithErrors(msg: string, errors?: string[]) {
return errors && errors.length > 0
? `${msg}: ${errors.join(', ')}`
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk/lib/api/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { AssetManifest, IManifestEntry } from 'cdk-assets';
import { Mode } from './aws-auth/credentials';
import { ISDK } from './aws-auth/sdk';
import { CredentialsOptions, SdkForEnvironment, SdkProvider } from './aws-auth/sdk-provider';
import { deployStack, DeployStackResult, destroyStack, makeBodyParameterAndUpload, DeploymentMethod } from './deploy-stack';
import { deployStack, DeployStackResult, destroyStack, DeploymentMethod } from './deploy-stack';
import { EnvironmentResources, EnvironmentResourcesRegistry } from './environment-resources';
import { HotswapMode } from './hotswap/common';
import { loadCurrentTemplateWithNestedStacks, loadCurrentTemplate, flattenNestedStackNames, TemplateWithNestedStackCount } from './nested-stack-helpers';
import { CloudFormationStack, Template, ResourcesToImport, ResourceIdentifierSummaries } from './util/cloudformation';
import { StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
import { replaceEnvPlaceholders } from './util/placeholders';
import { makeBodyParameterAndUpload } from './util/template-body-parameter';
import { Tag } from '../cdk-toolkit';
import { debug, warning } from '../logging';
import { buildAssets, publishAssets, BuildAssetsOptions, PublishAssetsOptions, PublishingAws, EVENT_TO_LOGGER } from '../util/asset-publishing';
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/util/cloudformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { SSMPARAM_NO_INVALIDATE } from '@aws-cdk/cx-api';
import * as cxapi from '@aws-cdk/cx-api';
import { CloudFormation } from 'aws-sdk';
import { StackStatus } from './cloudformation/stack-status';
import { makeBodyParameterAndUpload, TemplateBodyParameter } from './template-body-parameter';
import { debug } from '../../logging';
import { deserializeStructure } from '../../serialize';
import { TemplateBodyParameter, makeBodyParameterAndUpload } from '../deploy-stack';
import { Deployments } from '../deployments';

export type Template = {
Expand Down
146 changes: 146 additions & 0 deletions packages/aws-cdk/lib/api/util/template-body-parameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import * as cxapi from '@aws-cdk/cx-api';
import * as chalk from 'chalk';
import * as fs from 'fs-extra';
import { debug, error } from '../../logging';
import { toYAML } from '../../serialize';
import { AssetManifestBuilder } from '../../util/asset-manifest-builder';
import { publishAssets } from '../../util/asset-publishing';
import { contentHash } from '../../util/content-hash';
import { ISDK, SdkProvider } from '../aws-auth';
import { EnvironmentResources } from '../environment-resources';

export type TemplateBodyParameter = {
TemplateBody?: string
TemplateURL?: string
};

const LARGE_TEMPLATE_SIZE_KB = 50;

/**
* Prepares the body parameter for +CreateChangeSet+.
*
* If the template is small enough to be inlined into the API call, just return
* it immediately.
*
* Otherwise, add it to the asset manifest to get uploaded to the staging
* bucket and return its coordinates. If there is no staging bucket, an error
* is thrown.
*
* @param stack the synthesized stack that provides the CloudFormation template
* @param toolkitInfo information about the toolkit stack
*/
export async function makeBodyParameter(
stack: cxapi.CloudFormationStackArtifact,
resolvedEnvironment: cxapi.Environment,
assetManifest: AssetManifestBuilder,
resources: EnvironmentResources,
sdk: ISDK,
overrideTemplate?: any,
): Promise<TemplateBodyParameter> {

// If the template has already been uploaded to S3, just use it from there.
if (stack.stackTemplateAssetObjectUrl && !overrideTemplate) {
return { TemplateURL: restUrlFromManifest(stack.stackTemplateAssetObjectUrl, resolvedEnvironment, sdk) };
}

// Otherwise, pass via API call (if small) or upload here (if large)
const templateJson = toYAML(overrideTemplate ?? stack.template);

if (templateJson.length <= LARGE_TEMPLATE_SIZE_KB * 1024) {
return { TemplateBody: templateJson };
}

const toolkitInfo = await resources.lookupToolkit();
if (!toolkitInfo.found) {
error(
`The template for stack "${stack.displayName}" is ${Math.round(templateJson.length / 1024)}KiB. ` +
`Templates larger than ${LARGE_TEMPLATE_SIZE_KB}KiB must be uploaded to S3.\n` +
'Run the following command in order to setup an S3 bucket in this environment, and then re-deploy:\n\n',
chalk.blue(`\t$ cdk bootstrap ${resolvedEnvironment.name}\n`));

throw new Error('Template too large to deploy ("cdk bootstrap" is required)');
}

const templateHash = contentHash(templateJson);
const key = `cdk/${stack.id}/${templateHash}.yml`;

let templateFile = stack.templateFile;
if (overrideTemplate) {
// Add a variant of this template
templateFile = `${stack.templateFile}-${templateHash}.yaml`;
await fs.writeFile(templateFile, templateJson, { encoding: 'utf-8' });
}

assetManifest.addFileAsset(templateHash, {
path: templateFile,
}, {
bucketName: toolkitInfo.bucketName,
objectKey: key,
});

const templateURL = `${toolkitInfo.bucketUrl}/${key}`;
debug('Storing template in S3 at:', templateURL);
return { TemplateURL: templateURL };
}

/**
* Prepare a body parameter for CFN, performing the upload
*
* Return it as-is if it is small enough to pass in the API call,
* upload to S3 and return the coordinates if it is not.
*/
export async function makeBodyParameterAndUpload(
stack: cxapi.CloudFormationStackArtifact,
resolvedEnvironment: cxapi.Environment,
resources: EnvironmentResources,
sdkProvider: SdkProvider,
sdk: ISDK,
overrideTemplate?: any): Promise<TemplateBodyParameter> {

// We don't have access to the actual asset manifest here, so pretend that the
// stack doesn't have a pre-published URL.
const forceUploadStack = Object.create(stack, {
stackTemplateAssetObjectUrl: { value: undefined },
});

const builder = new AssetManifestBuilder();
const bodyparam = await makeBodyParameter(forceUploadStack, resolvedEnvironment, builder, resources, sdk, overrideTemplate);
const manifest = builder.toManifest(stack.assembly.directory);
await publishAssets(manifest, sdkProvider, resolvedEnvironment, { quiet: true });

return bodyparam;
}

/**
* Format an S3 URL in the manifest for use with CloudFormation
*
* Replaces environment placeholders (which this field may contain),
* and reformats s3://.../... urls into S3 REST URLs (which CloudFormation
* expects)
*/
function restUrlFromManifest(url: string, environment: cxapi.Environment, sdk: ISDK): string {
const doNotUseMarker = '**DONOTUSE**';
// This URL may contain placeholders, so still substitute those.
url = cxapi.EnvironmentPlaceholders.replace(url, {
accountId: environment.account,
region: environment.region,
partition: doNotUseMarker,
});

// Yes, this is extremely crude, but we don't actually need this so I'm not inclined to spend
// a lot of effort trying to thread the right value to this location.
if (url.indexOf(doNotUseMarker) > -1) {
throw new Error('Cannot use \'${AWS::Partition}\' in the \'stackTemplateAssetObjectUrl\' field');
}

const s3Url = url.match(/s3:\/\/([^/]+)\/(.*)$/);
if (!s3Url) { return url; }

// We need to pass an 'https://s3.REGION.amazonaws.com[.cn]/bucket/object' URL to CloudFormation, but we
// got an 's3://bucket/object' URL instead. Construct the rest API URL here.
const bucketName = s3Url[1];
const objectKey = s3Url[2];

const urlSuffix: string = sdk.getEndpointSuffix(environment.region);
return `https://s3.${environment.region}.${urlSuffix}/${bucketName}/${objectKey}`;
}
Loading

0 comments on commit da00e1a

Please sign in to comment.