Skip to content

Commit

Permalink
no more deprecated
Browse files Browse the repository at this point in the history
  • Loading branch information
comcalvi committed Nov 11, 2022
1 parent 50393c7 commit ad0fdaa
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 108 deletions.
22 changes: 15 additions & 7 deletions packages/@aws-cdk/core/lib/private/refs.ts
Expand Up @@ -193,15 +193,23 @@ function findAllReferences(root: IConstruct) {
function createImportValue(reference: Reference): Intrinsic {
const exportingStack = Stack.of(reference.target);

const importExpr = exportingStack.exportListOrStringValue(reference);
let importExpr: string | string[];

if (Array.isArray(importExpr)) {
// I happen to know this returns a Fn.split() which implements Intrinsic.
return Tokenization.reverseList(importExpr) as Intrinsic;
}
try {
importExpr = exportingStack.exportValue(reference);

// I happen to know this returns a Fn.importValue() which implements Intrinsic.
return Tokenization.reverseCompleteString(importExpr) as Intrinsic;
} catch (e) {
try {
importExpr = exportingStack.exportListValue(reference);

// I happen to know this returns a Fn.importValue() which implements Intrinsic.
return Tokenization.reverseCompleteString(importExpr) as Intrinsic;
// I happen to know this returns a Fn.split() which implements Intrinsic.
return Tokenization.reverseList(importExpr) as Intrinsic;
} catch (e) {
throw e;
}
}
}

/**
Expand Down
173 changes: 72 additions & 101 deletions packages/@aws-cdk/core/lib/stack.ts
Expand Up @@ -925,118 +925,26 @@ export class Stack extends Construct implements ITaggable {
* - You are now free to remove the `bucket` resource from `producerStack`.
* - Don't forget to remove the `exportValue()` call as well.
* - Deploy again (this time only the `producerStack` will be changed -- the bucket will be deleted).
*
* @deprecated Use `exportListOrStringValue`
*/
public exportValue(exportedValue: any, options: ExportValueOptions = {}): string {
const importExpr = this.exportListOrStringValue(exportedValue, options);
if (typeof importExpr !== 'string') {
throw new Error('Tried to export a list with `exportValue()`. Please use `exportListOrStringValue()` instead.');
throw new Error('Tried to export a list with `exportValue()`. Please use `exportListValue()` instead.');
}
return importExpr;
}

/**
* Create a CloudFormation Export for a value
*
* Returns a string or string list representing the corresponding `Fn.importValue()`
* expression for this Export. If the value is a string list then the export is wrapped
* with an `Fn::Join` and the import is wrapped with an `Fn::split`, since CloudFormation
* requires any exported value evaluate to a string. You can control the name for the export by
* passing the `name` option.
*
* If you don't supply a value for `name`, the value you're exporting must be
* a Resource attribute (for example: `bucket.bucketName`) and it will be
* given the same name as the automatic cross-stack reference that would be created
* if you used the attribute in another Stack.
*
* One of the uses for this method is to *remove* the relationship between
* two Stacks established by automatic cross-stack references. It will
* temporarily ensure that the CloudFormation Export still exists while you
* remove the reference from the consuming stack. After that, you can remove
* the resource and the manual export.
*
* ## Example
*
* Here is how the process works. Let's say there are two stacks,
* `producerStack` and `consumerStack`, and `producerStack` has a bucket
* called `bucket`, which is referenced by `consumerStack` (perhaps because
* an AWS Lambda Function writes into it, or something like that).
*
* It is not safe to remove `producerStack.bucket` because as the bucket is being
* deleted, `consumerStack` might still be using it.
*
* Instead, the process takes two deployments:
*
* ### Deployment 1: break the relationship
*
* - Make sure `consumerStack` no longer references `bucket.bucketName` (maybe the consumer
* stack now uses its own bucket, or it writes to an AWS DynamoDB table, or maybe you just
* remove the Lambda Function altogether).
* - In the `ProducerStack` class, call `this.exportValue(this.bucket.bucketName)`. This
* will make sure the CloudFormation Export continues to exist while the relationship
* between the two stacks is being broken.
* - Deploy (this will effectively only change the `consumerStack`, but it's safe to deploy both).
*
* ### Deployment 2: remove the bucket resource
*
* - You are now free to remove the `bucket` resource from `producerStack`.
* - Don't forget to remove the `exportValue()` call as well.
* - Deploy again (this time only the `producerStack` will be changed -- the bucket will be deleted).
* identical to `exportValue` except that the export will be wrapped in an `Fn::Join`
* and then import will be wrapped in an `Fn::Split`, since CloudFormation exports
* are required to be strings.
*/
public exportListOrStringValue(exportedValue: any, options: ExportValueOptions = {}) {
if (options.name) {
new CfnOutput(this, `Export${options.name}`, {
value: exportedValue,
exportName: options.name,
});
return Fn.importValue(options.name);
}

const resolvable = Tokenization.reverse(exportedValue);
if (!resolvable || !Reference.isReference(resolvable)) {
throw new Error('exportValue: either supply \'name\' or make sure to export a resource attribute (like \'bucket.bucketName\')');
}

// "teleport" the value here, in case it comes from a nested stack. This will also
// ensure the value is from our own scope.
const exportable = getExportable(this, resolvable);

// Ensure a singleton "Exports" scoping Construct
// This mostly exists to trigger LogicalID munging, which would be
// disabled if we parented constructs directly under Stack.
// Also it nicely prevents likely construct name clashes
const exportsScope = getCreateExportsScope(this);

// Ensure a singleton CfnOutput for this value
const resolved = this.resolve(exportable);
const id = 'Output' + JSON.stringify(resolved);
const exportName = generateExportName(exportsScope, id);

if (Token.isUnresolved(exportName)) {
throw new Error(`unresolved token in generated export name: ${JSON.stringify(this.resolve(exportName))}`);
}

const exportIsAList = isExportAList(exportedValue, resolved);
const delimiter = 'CDK-determined-super-magic-delimiter';

// if it's a list, export an Fn::Join expression
// and import an Fn::Split expression,
// since CloudFormation Outputs can only be strings
// (string lists are invalid)
const valueToExport = exportIsAList ?
Fn.join(delimiter, Token.asList(exportable))
: Token.asString(exportable);

const output = exportsScope.node.tryFindChild(id) as CfnOutput;
if (!output) {
new CfnOutput(exportsScope, id, { value: valueToExport, exportName });
public exportListValue(exportedValue: any, options: ExportValueOptions = {}): string[] {
const importExpr = this.exportListOrStringValue(exportedValue, options);
if (typeof importExpr === 'string') {
throw new Error('Tried to export a string with `exportListValue()`. Please use `exportValue()` instead.');
}

// we don't use `Fn.importListValue()` since this array is a CFN attribute, and we don't know how long this attribute is
return exportIsAList ?
Fn.split(delimiter, Fn.importValue(exportName))
: Fn.importValue(exportName);
return importExpr;
}

/**
Expand Down Expand Up @@ -1264,6 +1172,69 @@ export class Stack extends Construct implements ITaggable {
return makeStackName(ids);
}

/**
* Create a CloudFormation Export for a value
*
* Returns a string or string list representing the corresponding `Fn.importValue()`
* expression for this Export. If the value is a string list then the export is wrapped
* with an `Fn::Join` and the import is wrapped with an `Fn::split`, since CloudFormation
* requires any exported value evaluate to a string.
*/
private exportListOrStringValue(exportedValue: any, options: ExportValueOptions = {}) {
if (options.name) {
new CfnOutput(this, `Export${options.name}`, {
value: exportedValue,
exportName: options.name,
});
return Fn.importValue(options.name);
}

const resolvable = Tokenization.reverse(exportedValue);
if (!resolvable || !Reference.isReference(resolvable)) {
throw new Error('exportValue: either supply \'name\' or make sure to export a resource attribute (like \'bucket.bucketName\')');
}

// "teleport" the value here, in case it comes from a nested stack. This will also
// ensure the value is from our own scope.
const exportable = getExportable(this, resolvable);

// Ensure a singleton "Exports" scoping Construct
// This mostly exists to trigger LogicalID munging, which would be
// disabled if we parented constructs directly under Stack.
// Also it nicely prevents likely construct name clashes
const exportsScope = getCreateExportsScope(this);

// Ensure a singleton CfnOutput for this value
const resolved = this.resolve(exportable);
const id = 'Output' + JSON.stringify(resolved);
const exportName = generateExportName(exportsScope, id);

if (Token.isUnresolved(exportName)) {
throw new Error(`unresolved token in generated export name: ${JSON.stringify(this.resolve(exportName))}`);
}

const exportIsAList = isExportAList(exportedValue, resolved);
const delimiter = 'CDK-determined-super-magic-delimiter';

// if it's a list, export an Fn::Join expression
// and import an Fn::Split expression,
// since CloudFormation Outputs can only be strings
// (string lists are invalid)
const valueToExport = exportIsAList ?
Fn.join(delimiter, Token.asList(exportable))
: Token.asString(exportable);

const output = exportsScope.node.tryFindChild(id) as CfnOutput;
if (!output) {
new CfnOutput(exportsScope, id, { value: valueToExport, exportName });
}

// we don't use `Fn.importListValue()` since this array is a CFN attribute, and we don't know how long this attribute is
return exportIsAList ?
Fn.split(delimiter, Fn.importValue(exportName))
: Fn.importValue(exportName);
}

/**
* Indicates whether the stack requires bundling or not
*/
Expand Down

0 comments on commit ad0fdaa

Please sign in to comment.