Repository: pulumi/pulumi-cdk
Pull request #243 (2024-11-21 22:45:53)

Improve the Ref intrinsic implementation
This change adds some tests and comments around the `Ref` intrinsic implementation and moves it to `intrinsics.ts`. The one change in behavior is that now `Ref` is able to consult Pulumi metadata from the `aws-native` provider. The metadata helps it resolve to the same values as it would in CloudFormation for resources that have non-standard `Ref` behavior.  The correctness of this process is up to the correctness of the metadata tables. Entries not found in the metadata tables default to using Pulumi `id` for most cases should do the right thing.

Fixes https://github.com/pulumi/pulumi-cdk/issues/173

Needs https://github.com/pulumi/pulumi-aws-native/pull/1836


Patch:
diff --git a/examples/fargate/index.ts b/examples/fargate/index.ts
index 62f33b4..59e85d3 100644
--- a/examples/fargate/index.ts
+++ b/examples/fargate/index.ts
@@ -6,7 +6,6 @@ import { Duration } from 'aws-cdk-lib';
 import * as ec2 from 'aws-cdk-lib/aws-ec2';
 import * as ecs from 'aws-cdk-lib/aws-ecs';
 import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
-import { CfnTargetGroup } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
 import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
 
 class FargateStack extends pulumicdk.Stack {
@@ -34,12 +33,8 @@ class FargateStack extends pulumicdk.Stack {
             },
         });
 
-        // workaround for https://github.com/pulumi/pulumi-cdk/issues/62
-        const cfnTargetGroup = fargateService.targetGroup.node.defaultChild as CfnTargetGroup;
-        cfnTargetGroup.overrideLogicalId('LBListenerTG');
-
         // Open port 80 inbound to IPs within VPC to allow network load balancer to connect to the service
-        fargateService.service.connections.securityGroups[0].addIngressRule(
+        fargateService.service.connections.allowFrom(
             ec2.Peer.ipv4(vpc.vpcCidrBlock),
             ec2.Port.tcp(80),
             'allow http inbound from vpc',
diff --git a/integration/cloudfront/index.ts b/integration/cloudfront/index.ts
index e248402..48f253a 100644
--- a/integration/cloudfront/index.ts
+++ b/integration/cloudfront/index.ts
@@ -89,6 +89,7 @@ class CloudFrontStack extends pulumicdk.Stack {
 
         const stream = new kinesis.Stream(this, 'stream', {
             encryption: kinesis.StreamEncryption.UNENCRYPTED,
+            removalPolicy: RemovalPolicy.DESTROY,
         });
         const realtimeLogConfig = new cloudfront.RealtimeLogConfig(this, 'realtimeLog', {
             endPoints: [cloudfront.Endpoint.fromKinesisStream(stream)],
diff --git a/integration/kinesis/index.ts b/integration/kinesis/index.ts
index 0289a73..32aed55 100644
--- a/integration/kinesis/index.ts
+++ b/integration/kinesis/index.ts
@@ -1,7 +1,7 @@
 import * as pulumi from '@pulumi/pulumi';
 import * as kinesis from 'aws-cdk-lib/aws-kinesis';
 import * as pulumicdk from '@pulumi/cdk';
-import { Duration } from 'aws-cdk-lib/core';
+import { Duration, RemovalPolicy } from 'aws-cdk-lib/core';
 
 class KinesisStack extends pulumicdk.Stack {
     kinesisStreamName: pulumi.Output<string>;
@@ -12,7 +12,8 @@ class KinesisStack extends pulumicdk.Stack {
         const kStream = new kinesis.Stream(this, 'my-stream', {
             shardCount: 3,
             retentionPeriod: Duration.hours(24),
-        })
+            removalPolicy: RemovalPolicy.DESTROY,
+        });
 
         this.kinesisStreamName = this.asOutput(kStream.streamName);
     }
diff --git a/integration/logs/index.ts b/integration/logs/index.ts
index 5a4871d..2f34cd8 100644
--- a/integration/logs/index.ts
+++ b/integration/logs/index.ts
@@ -3,6 +3,7 @@ import * as iam from 'aws-cdk-lib/aws-iam';
 import * as logs_destinations from 'aws-cdk-lib/aws-logs-destinations';
 import * as logs from 'aws-cdk-lib/aws-logs';
 import * as pulumicdk from '@pulumi/cdk';
+import { RemovalPolicy } from 'aws-cdk-lib/core';
 
 class LogsStack extends pulumicdk.Stack {
     constructor(app: pulumicdk.App, id: string, options?: pulumicdk.StackOptions) {
@@ -44,6 +45,7 @@ class LogsStack extends pulumicdk.Stack {
         });
         const stream = new kinesis.Stream(this, 'stream', {
             encryption: kinesis.StreamEncryption.UNENCRYPTED,
+            removalPolicy: RemovalPolicy.DESTROY,
         });
         logGroup2.addSubscriptionFilter('cdk-filter', {
             destination: new logs_destinations.KinesisDestination(stream),
diff --git a/src/cfn.ts b/src/cfn.ts
index 544b608..37b9237 100644
--- a/src/cfn.ts
+++ b/src/cfn.ts
@@ -14,11 +14,26 @@
 
 import { CfnDeletionPolicy } from 'aws-cdk-lib/core';
 
+/**
+ * Represents a CF parameter declaration from the Parameters template section.
+ *
+ * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
+ */
 export interface CloudFormationParameter {
+    /**
+     * DataType such as 'String'.
+     */
     readonly Type: string;
+
     readonly Default?: any;
 }
 
+export type CloudFormationParameterLogicalId = string;
+
+export interface CloudFormationParameterWithId extends CloudFormationParameter {
+    id: CloudFormationParameterLogicalId;
+}
+
 export interface CloudFormationResource {
     readonly Type: string;
     readonly Properties: any;
diff --git a/src/converters/app-converter.ts b/src/converters/app-converter.ts
index f69ae39..30125be 100644
--- a/src/converters/app-converter.ts
+++ b/src/converters/app-converter.ts
@@ -25,6 +25,9 @@ import { getPartition } from '@pulumi/aws-native/getPartition';
 import { mapToCustomResource } from '../custom-resource-mapping';
 import { processSecretsManagerReferenceValue } from './secrets-manager-dynamic';
 import * as intrinsics from './intrinsics';
+import { CloudFormationParameter, CloudFormationParameterLogicalId, CloudFormationParameterWithId } from '../cfn';
+import { Metadata, PulumiResource } from '../pulumi-metadata';
+import { PulumiProvider } from '../types';
 
 /**
  * AppConverter will convert all CDK resources into Pulumi resources.
@@ -92,7 +95,7 @@ export class AppConverter {
  * StackConverter converts all of the resources in a CDK stack to Pulumi resources
  */
 export class StackConverter extends ArtifactConverter implements intrinsics.IntrinsicContext {
-    readonly parameters = new Map<string, any>();
+    readonly parameters = new Map<CloudFormationParameterLogicalId, any>();
     readonly resources = new Map<string, Mapping<pulumi.Resource>>();
     readonly constructs = new Map<ConstructInfo, pulumi.Resource>();
     private readonly cdkStack: cdk.Stack;
@@ -377,7 +380,7 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
 
         const ref = obj.Ref;
         if (ref) {
-            return this.resolveRef(ref);
+            return intrinsics.ref.evaluate(this, [ref]);
         }
 
         const keys = Object.keys(obj);
@@ -417,8 +420,15 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
         return obj?.Ref === 'AWS::NoValue';
     }
 
-    private resolveOutput(repr: OutputRepr): pulumi.Output<any> {
-        return OutputMap.instance().lookupOutput(repr)!;
+    /**
+     * @internal
+     */
+    public resolveOutput(repr: OutputRepr): pulumi.Output<any> {
+        const result = OutputMap.instance().lookupOutput(repr);
+        if (result === undefined) {
+            throw new Error(`@pulumi/pulumi-cdk internal failure: unable to resolveOutput ${repr.PulumiOutput}`);
+        }
+        return result;
     }
 
     private resolveIntrinsic(fn: string, params: any) {
@@ -475,7 +485,8 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
                     const [template, _vars] =
                         typeof params === 'string' ? [params, undefined] : [params[0] as string, params[1]];
 
-                    const parts: string[] = [];
+                    // parts may contain pulumi.Output values.
+                    const parts: any[] = [];
                     for (const part of parseSub(template)) {
                         parts.push(part.str);
 
@@ -483,7 +494,7 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
                             if (part.ref.attr !== undefined) {
                                 parts.push(this.resolveAtt(part.ref.id, part.ref.attr!));
                             } else {
-                                parts.push(this.resolveRef(part.ref.id));
+                                parts.push(intrinsics.ref.evaluate(this, [part.ref.id]));
                             }
                         }
                     }
@@ -557,47 +568,6 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
         }
     }
 
-    private resolveRef(target: any): any {
-        if (typeof target !== 'string') {
-            return this.resolveOutput(<OutputRepr>target);
-        }
-
-        switch (target) {
-            case 'AWS::AccountId':
-                return getAccountId({ parent: this.app.component }).then((r) => r.accountId);
-            case 'AWS::NoValue':
-                return undefined;
-            case 'AWS::Partition':
-                return getPartition({ parent: this.app.component }).then((p) => p.partition);
-            case 'AWS::Region':
-                return getRegion({ parent: this.app.component }).then((r) => r.region);
-            case 'AWS::URLSuffix':
-                return getUrlSuffix({ parent: this.app.component }).then((r) => r.urlSuffix);
-            case 'AWS::NotificationARNs':
-            case 'AWS::StackId':
-            case 'AWS::StackName':
-                // These are typically used in things like names or descriptions so I think
-                // the stack node id is a good substitute.
-                return this.cdkStack.node.id;
-        }
-
-        const mapping = this.lookup(target);
-        if ((<any>mapping).value !== undefined) {
-            return (<any>mapping).value;
-        }
-        // Due to https://github.com/pulumi/pulumi-cdk/issues/173 we have some
-        // resource which we have to special case the `id` attribute. The `Resource.id`
-        // will not contain the correct value
-        const map = <Mapping<pulumi.Resource>>mapping;
-        if (map.attributes && 'id' in map.attributes) {
-            return map.attributes.id;
-        } else if (aws.cloudformation.CustomResourceEmulator.isInstance(map.resource)) {
-            // Custom resources have a `physicalResourceId` that is used for Ref
-            return map.resource.physicalResourceId;
-        }
-        return (<pulumi.CustomResource>(<Mapping<pulumi.Resource>>mapping).resource).id;
-    }
-
     private lookup(logicalId: string): Mapping<pulumi.Resource> | { value: any } {
         const targetParameter = this.parameters.get(logicalId);
         if (targetParameter !== undefined) {
@@ -666,4 +636,46 @@ export class StackConverter extends ArtifactConverter implements intrinsics.Intr
     apply<T, U>(result: intrinsics.Result<T>, fn: (value: U) => intrinsics.Result<U>): intrinsics.Result<U> {
         return lift(fn, result);
     }
+
+    findParameter(parameterLogicalId: CloudFormationParameterLogicalId): CloudFormationParameterWithId | undefined {
+        const p: CloudFormationParameter | undefined = (this.stack.parameters || {})[parameterLogicalId];
+        return p ? { ...p, id: parameterLogicalId } : undefined;
+    }
+
+    evaluateParameter(param: CloudFormationParameterWithId): intrinsics.Result<any> {
+        const value = this.parameters.get(param.id);
+        if (value === undefined) {
+            throw new Error(`No value for the CloudFormation "${param.id}" parameter`);
+        }
+        return value;
+    }
+
+    findResourceMapping(resourceLogicalID: string): Mapping<pulumi.Resource> | undefined {
+        return this.resources.get(resourceLogicalID);
+    }
+
+    tryFindResource(cfnType: string): PulumiResource | undefined {
+        const m = new Metadata(PulumiProvider.AWS_NATIVE);
+        return m.tryFindResource(cfnType);
+    }
+
+    getStackNodeId(): intrinsics.Result<string> {
+        return this.cdkStack.node.id;
+    }
+
+    getAccountId(): intrinsics.Result<string> {
+        return getAccountId({ parent: this.app.component }).then((r) => r.accountId);
+    }
+
+    getRegion(): intrinsics.Result<string> {
+        return getRegion({ parent: this.app.component }).then((r) => r.region);
+    }
+
+    getPartition(): intrinsics.Result<string> {
+        return getPartition({ parent: this.app.component }).then((p) => p.partition);
+    }
+
+    getURLSuffix(): intrinsics.Result<string> {
+        return getUrlSuffix({ parent: this.app.component }).then((r) => r.urlSuffix);
+    }
 }
diff --git a/src/converters/intrinsics.ts b/src/converters/intrinsics.ts
index bcd0529..68ddf87 100644
--- a/src/converters/intrinsics.ts
+++ b/src/converters/intrinsics.ts
@@ -12,7 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import * as aws from '@pulumi/aws-native';
 import * as equal from 'fast-deep-equal';
+import * as pulumi from '@pulumi/pulumi';
+import { debug } from '@pulumi/pulumi/log';
+import { CloudFormationParameterWithId } from '../cfn';
+import { Mapping } from '../types';
+import { PulumiResource } from '../pulumi-metadata';
+import { toSdkName } from '../naming';
+import { OutputRepr, isOutputReprInstance } from '../output-map';
 
 /**
  * Models a CF Intrinsic Function.
@@ -84,6 +92,26 @@ export interface IntrinsicContext {
      */
     evaluate(expression: Expression): Result<any>;
 
+    /**
+     * Resolves a logical parameter ID to a parameter, or indicates that no such parameter is defined on the template.
+     */
+    findParameter(parameterLogicalID: string): CloudFormationParameterWithId | undefined;
+
+    /**
+     * Resolves a logical resource ID to a Mapping.
+     */
+    findResourceMapping(resourceLogicalID: string): Mapping<pulumi.Resource> | undefined;
+
+    /**
+     * Find the current value of a given Cf parameter.
+     */
+    evaluateParameter(param: CloudFormationParameterWithId): Result<any>;
+
+    /**
+     * Find the value of an `OutputRepr`.
+     */
+    resolveOutput(repr: OutputRepr): Result<any>;
+
     /**
      * If result succeeds, use its value to call `fn` and proceed with what it returns.
      *
@@ -99,7 +127,41 @@ export interface IntrinsicContext {
     /**
      * Succeed with a given value.
      */
-    succeed<T>(r: T): Result<T>;
+    succeed<T>(r: pulumi.Input<T>): Result<T>;
+
+    /**
+     * Pulumi metadata source that may inform the intrinsic evaluation.
+     */
+    tryFindResource(cfnType: string): PulumiResource | undefined;
+
+    /**
+     * Gets the CDK Stack Node ID.
+     */
+    getStackNodeId(): Result<string>;
+
+    /**
+     * The AWS account ID.
+     */
+    getAccountId(): Result<string>;
+
+    /**
+     * The AWS Region.
+     */
+    getRegion(): Result<string>;
+
+    /**
+     * The AWS partition.
+     */
+    getPartition(): Result<string>;
+
+    /**
+     * The URL suffix.
+     *
+     * Quoting the docs: "The suffix is typically amazonaws.com, but might differ by Region".
+     *
+     * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html#cfn-pseudo-param-urlsuffix
+     */
+    getURLSuffix(): Result<string>;
 }
 
 /**
@@ -251,6 +313,131 @@ export const fnEquals: Intrinsic = {
     },
 };
 
+/**
+ * Ref intrinsic resolves pseudo-parameters, parameter logical IDs or resource logical IDs to their values.
+ *
+ * If the argument to a Ref intrinsic is not a string literal, it may be another CF expression with intrinsic functions
+ * that needs to be evaluated first.
+ *
+ * See also:
+ *
+ * - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html
+ */
+export const ref: Intrinsic = {
+    name: 'Ref',
+    evaluate: (ctx: IntrinsicContext, params: Expression[]): Result<any> => {
+        if (params.length != 1) {
+            return ctx.fail(`Ref intrinsic expects exactly 1 param, got ${params.length}`);
+        }
+        const param = params[0];
+
+        // Although not part of the CF spec, Output values are passed through CDK tokens as Ref structures; therefore
+        // Pulumi Ref intrinsic receives them and has to handle them.
+        if (isOutputReprInstance(param)) {
+            return ctx.resolveOutput(<OutputRepr>param);
+        }
+
+        // Unless the parameter is a literal string, it may be another expression.
+        //
+        // CF docs: "When the AWS::LanguageExtensions transform is used, you can use intrinsic functions..".
+        if (typeof param !== 'string') {
+            const s = ctx.apply(ctx.evaluate(param), (p) => mustBeString(ctx, p));
+            return ctx.apply(s, (name) => evaluateRef(ctx, name));
+        }
+        return evaluateRef(ctx, param);
+    },
+};
+
+/**
+ * See `ref`.
+ */
+function evaluateRef(ctx: IntrinsicContext, param: string): Result<any> {
+    // Handle pseudo-parameters.
+    // See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html
+    switch (param) {
+        case 'AWS::AccountId':
+            return ctx.getAccountId();
+        case 'AWS::NoValue':
+            return ctx.succeed(undefined);
+        case 'AWS::Partition':
+            return ctx.getPartition();
+        case 'AWS::Region':
+            return ctx.getRegion();
+        case 'AWS::URLSuffix':
+            return ctx.getURLSuffix();
+        case 'AWS::NotificationARNs':
+            return ctx.fail('AWS::NotificationARNs pseudo-parameter is not yet supported in pulumi-cdk');
+        case 'AWS::StackId':
+        case 'AWS::StackName': {
+            // TODO[pulumi/pulumi-cdk#246]: these pseudo-parameters are typically used in things like names or descriptions
+            // so it should be safe to substitute with a stack node ID for most applications.
+            const stackNodeId = ctx.getStackNodeId();
+            debug(`pulumi-cdk is replacing a Ref to a CF pseudo-parameter ${param} with the stack node ID`);
+            return stackNodeId;
+        }
+    }
+
+    // Handle Cf template parameters.
+    const cfParam = ctx.findParameter(param);
+    if (cfParam !== undefined) {
+        return ctx.evaluateParameter(cfParam);
+    }
+
+    // Handle references to resources.
+    const map = ctx.findResourceMapping(param);
+    if (map !== undefined) {
+        if (map.attributes && 'id' in map.attributes) {
+            // Users may override the `id` in a custom-supplied mapping, respect this.
+            return ctx.succeed(map.attributes.id);
+        }
+        if (aws.cloudformation.CustomResourceEmulator.isInstance(map.resource)) {
+            // Custom resources have a `physicalResourceId` that is used for Ref
+            return ctx.succeed(map.resource.physicalResourceId);
+        }
+        const resMeta = ctx.tryFindResource(map.resourceType);
+
+        // If there is no metadata to suggest otherwise, assume that we can use the Pulumi id which typically will be
+        // the primaryIdentifier from CloudControl.
+        if (resMeta === undefined || !resMeta.cfRef || resMeta.cfRef.notSupportedYet) {
+            const cr = <pulumi.CustomResource>map.resource; // assume we have a custom resource.
+            return ctx.succeed(cr.id);
+        }
+
+        // Respect metadata if it suggests Ref is not supported.
+        if (resMeta.cfRef.notSupported) {
+            return ctx.fail(`Ref intrinsic is not supported for the ${map.resourceType} resource type`);
+        }
+
+        // At this point metadata should indicate which properties to extract from the resource to compute the ref.
+        const propNames: string[] = (resMeta.cfRef.properties || [])
+            .concat(resMeta.cfRef.property ? [resMeta.cfRef.property] : [])
+            .map((x) => toSdkName(x));
+
+        const propValues: any[] = [];
+        for (const p of propNames) {
+            if (!Object.prototype.hasOwnProperty.call(map.resource, p)) {
+                return ctx.fail(`Pulumi metadata notes a property "${p}" but no such property was found on a resource`);
+            }
+            propValues.push((<any>map.resource)[p]);
+        }
+
+        const delim: string = resMeta.cfRef!.delimiter || '|';
+
+        return ctx.apply(ctx.succeed(propValues), (resolvedValues) => {
+            let i = 0;
+            for (const v of resolvedValues) {
+                if (typeof v !== 'string') {
+                    return ctx.fail(`Expected property "${propNames[i]}" to resolve to a string, got ${typeof v}`);
+                }
+                i++;
+            }
+            return ctx.succeed(resolvedValues.join(delim));
+        });
+    }
+
+    return ctx.fail(`Ref intrinsic unable to resolve ${param}: not a known logical resource or parameter reference`);
+}
+
 /**
  * Recognize forms such as {"Condition" : "SomeOtherCondition"}. If recognized, returns the conditionName.
  */
@@ -285,6 +472,14 @@ function mustBeBoolean(ctx: IntrinsicContext, r: any): Result<boolean> {
     }
 }
 
+function mustBeString(ctx: IntrinsicContext, r: any): Result<string> {
+    if (typeof r === 'string') {
+        return ctx.succeed(r);
+    } else {
+        return ctx.fail(`Expected a string, got ${typeof r}`);
+    }
+}
+
 function evaluateCondition(ctx: IntrinsicContext, conditionName: string): Result<boolean> {
     const conditionExpr = ctx.findCondition(conditionName);
     if (conditionExpr === undefined) {
diff --git a/src/output-map.ts b/src/output-map.ts
index 575c009..f3d25a6 100644
--- a/src/output-map.ts
+++ b/src/output-map.ts
@@ -15,15 +15,51 @@ import * as pulumi from '@pulumi/pulumi';
 
 const glob = global as any;
 
+/**
+ * A serializable reference to an output.
+ *
+ * @internal
+ */
 export interface OutputRef {
+    /**
+     * The name of this field has to be `Ref` so that `Token.asString` CDK functionality can be called on an `OutputRef`
+     * and it can travel through the CDK internals. An alternative to this special encoding could be implementing CDK
+     * `IResolvable` on these values.
+     */
     Ref: OutputRepr;
 }
 
+/**
+ * See `OutputRef`.
+ *
+ * @internal
+ */
 export interface OutputRepr {
+    /**
+     * An arbitrary integer identifying the output.
+     */
     PulumiOutput: number;
 }
 
+/**
+ * Recognize if something is an `OutputRepr`.
+ *
+ * @internal
+ */
+export function isOutputReprInstance(x: any): boolean {
+    return typeof x === 'object' && Object.prototype.hasOwnProperty.call(x, 'PulumiOutput');
+}
+
+/**
+ * Stores Pulumi Output values in memory so that they can be encoded into serializable `OutputRef` values with unique
+ * integers for CDK interop.
+ *
+ * @internal
+ */
 export class OutputMap {
+    /**
+     * Get the global instance.
+     */
     public static instance(): OutputMap {
         if (glob.__pulumiOutputMap === undefined) {
             glob.__pulumiOutputMap = new OutputMap();
@@ -34,12 +70,18 @@ export class OutputMap {
     private readonly outputMap = new Map<number, pulumi.Output<any>>();
     private outputId = 0;
 
+    /**
+     * Stores a reference to a Pulumi Output in the map and returns a serializable reference.
+     */
     public registerOutput(o: pulumi.Output<any>): OutputRef {
         const id = this.outputId++;
         this.outputMap.set(id, o);
         return { Ref: { PulumiOutput: id } };
     }
 
+    /**
+     * Tries to look up an output reference in the map and find the original value.
+     */
     public lookupOutput(o: OutputRepr): pulumi.Output<any> | undefined {
         return this.outputMap.get(o.PulumiOutput);
     }
diff --git a/src/pulumi-metadata.ts b/src/pulumi-metadata.ts
index 4a4fdd4..2699c9b 100644
--- a/src/pulumi-metadata.ts
+++ b/src/pulumi-metadata.ts
@@ -45,11 +45,22 @@ export class Metadata {
      * @throws UnknownCfnType if the resource is not found
      */
     public findResource(cfnType: string): PulumiResource {
+        const r = this.tryFindResource(cfnType);
+        if (r === undefined) {
+            throw new UnknownCfnType(cfnType);
+        }
+        return r;
+    }
+
+    /**
+     * Non-throwing version of `findResource`.
+     */
+    public tryFindResource(cfnType: string): PulumiResource | undefined {
         const pType = typeToken(cfnType);
         if (pType in this.pulumiMetadata.resources) {
             return this.pulumiMetadata.resources[pType];
         }
-        throw new UnknownCfnType(cfnType);
+        return undefined;
     }
 
     public types(): { [key: string]: PulumiType } {
@@ -120,11 +131,51 @@ export interface PulumiProperty extends PulumiPropertyItems {
     items?: PulumiPropertyItems;
 }
 
+/**
+ * The schema for an individual resource.
+ */
 export interface PulumiResource {
+    cfRef?: CfRefBehavior;
     inputs: { [key: string]: PulumiProperty };
     outputs: { [key: string]: PulumiProperty };
 }
 
+/**
+ * Metadata predicting the behavior of CF Ref intrinsic for a given resource.
+ *
+ * @internal
+ */
+export interface CfRefBehavior {
+    /**
+     * If set, indicates that Ref will return the value of the given Resource property directly.
+     *
+     * The property name is a CF name such as "GroupId".
+     */
+    property?: string;
+
+    /**
+     * If set, indicates that Ref will return a string value obtained by joining several Resource properties with a
+     * delimiter, typically "|".
+     */
+    properties?: string[];
+
+    /**
+     * Delimiter for `properties`, typically "|".
+     */
+    delimiter?: string;
+
+    /**
+     * If set, Ref is not supported for this resource in CF.
+     */
+    notSupported?: boolean;
+
+    /**
+     * If set, Ref is supported in CF but this metadata is not yet available in the Pulumi aws-native provider but might
+     * be added in a later version.
+     */
+    notSupportedYet?: boolean;
+}
+
 /**
  * If a property is a JSON type then we need provide
  * the value as is, without further processing.
diff --git a/yarn.lock b/yarn.lock
index 0e28c17..9fcdec9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -884,26 +884,26 @@
   resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
   integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
 
-"@opentelemetry/context-async-hooks@1.27.0":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.27.0.tgz#a18c288ac586f5385d156003d67851465b34fb73"
-  integrity sha512-CdZ3qmHCwNhFAzjTgHqrDQ44Qxcpz43cVxZRhOs+Ns/79ug+Mr84Bkb626bkJLkA3+BLimA5YAEVRlJC6pFb7g==
+"@opentelemetry/context-async-hooks@1.28.0":
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.28.0.tgz#287afda2b75cb226f70d433244c3ef6f6dd8abdd"
+  integrity sha512-igcl4Ve+F1N2063PJUkesk/GkYyuGIWinYkSyAFTnIj3gzrOgvOA4k747XNdL47HRRL1w/qh7UW8NDuxOLvKFA==
 
-"@opentelemetry/core@1.27.0":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.27.0.tgz#9f1701a654ab01abcebb12931b418f3393b94b75"
-  integrity sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==
+"@opentelemetry/core@1.28.0":
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.28.0.tgz#e97290a3e36c59480ffb2287fe2713c66749274c"
+  integrity sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==
   dependencies:
     "@opentelemetry/semantic-conventions" "1.27.0"
 
 "@opentelemetry/exporter-zipkin@^1.25":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.27.0.tgz#3ed984643797545c34ceb320276ed65d0df24e77"
-  integrity sha512-eGMY3s4QprspFZojqsuQyQpWNFpo+oNVE/aosTbtvAlrJBAlvXcwwsOROOHOd8Y9lkU4i0FpQW482rcXkgwCSw==
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.28.0.tgz#6e5ca7f43f79d0e3240ca7029f8dbc43af64c1b7"
+  integrity sha512-AMwr3eGXaPEH7gk8yhcUcen31VXy1yU5VJETu0pCfGpggGCYmhm0FKgYBpL5/vlIgQJWU/sW2vIjCL7aSilpKg==
   dependencies:
-    "@opentelemetry/core" "1.27.0"
-    "@opentelemetry/resources" "1.27.0"
-    "@opentelemetry/sdk-trace-base" "1.27.0"
+    "@opentelemetry/core" "1.28.0"
+    "@opentelemetry/resources" "1.28.0"
+    "@opentelemetry/sdk-trace-base" "1.28.0"
     "@opentelemetry/semantic-conventions" "1.27.0"
 
 "@opentelemetry/instrumentation-grpc@^0.52":
@@ -926,47 +926,47 @@
     semver "^7.5.2"
     shimmer "^1.2.1"
 
-"@opentelemetry/propagator-b3@1.27.0":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.27.0.tgz#6433b3fb2486548e94af7af37ab8b7932f198597"
-  integrity sha512-pTsko3gnMioe3FeWcwTQR3omo5C35tYsKKwjgTCTVCgd3EOWL9BZrMfgLBmszrwXABDfUrlAEFN/0W0FfQGynQ==
+"@opentelemetry/propagator-b3@1.28.0":
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.28.0.tgz#5e4c87c2097a18f88a5a4d19a57350f55517bb8a"
+  integrity sha512-Q7HVDIMwhN5RxL4bECMT4BdbyYSAKkC6U/RGn4NpO/cbqP6ZRg+BS7fPo/pGZi2w8AHfpIGQFXQmE8d2PC5xxQ==
   dependencies:
-    "@opentelemetry/core" "1.27.0"
+    "@opentelemetry/core" "1.28.0"
 
-"@opentelemetry/propagator-jaeger@1.27.0":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.27.0.tgz#8387d64805bd8edf72ed3267cf759c7887220201"
-  integrity sha512-EI1bbK0wn0yIuKlc2Qv2LKBRw6LiUWevrjCF80fn/rlaB+7StAi8Y5s8DBqAYNpY7v1q86+NjU18v7hj2ejU3A==
+"@opentelemetry/propagator-jaeger@1.28.0":
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.28.0.tgz#427158c3d0f8ea77c205c7b9cde09091c7f25ae3"
+  integrity sha512-wKJ94+s8467CnIRgoSRh0yXm/te0QMOwTq9J01PfG/RzYZvlvN8aRisN2oZ9SznB45dDGnMj3BhUlchSA9cEKA==
   dependencies:
-    "@opentelemetry/core" "1.27.0"
+    "@opentelemetry/core" "1.28.0"
 
-"@opentelemetry/resources@1.27.0", "@opentelemetry/resources@^1.25":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.27.0.tgz#1f91c270eb95be32f3511e9e6624c1c0f993c4ac"
-  integrity sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==
+"@opentelemetry/resources@1.28.0", "@opentelemetry/resources@^1.25":
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.28.0.tgz#c8c27ae7559c817f9d117f1bf96d76f893fb29f5"
+  integrity sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==
   dependencies:
-    "@opentelemetry/core" "1.27.0"
+    "@opentelemetry/core" "1.28.0"
     "@opentelemetry/semantic-conventions" "1.27.0"
 
-"@opentelemetry/sdk-trace-base@1.27.0", "@opentelemetry/sdk-trace-base@^1.25":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.27.0.tgz#2276e4cd0d701a8faba77382b2938853a0907b54"
-  integrity sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==
+"@opentelemetry/sdk-trace-base@1.28.0", "@opentelemetry/sdk-trace-base@^1.25":
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.28.0.tgz#6195dc8cd78bd74394cf54c67c5cbd8d1528516c"
+  integrity sha512-ceUVWuCpIao7Y5xE02Xs3nQi0tOGmMea17ecBdwtCvdo9ekmO+ijc9RFDgfifMl7XCBf41zne/1POM3LqSTZDA==
   dependencies:
-    "@opentelemetry/core" "1.27.0"
-    "@opentelemetry/resources" "1.27.0"
+    "@opentelemetry/core" "1.28.0"
+    "@opentelemetry/resources" "1.28.0"
     "@opentelemetry/semantic-conventions" "1.27.0"
 
 "@opentelemetry/sdk-trace-node@^1.25":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.27.0.tgz#ee890a1fedba6a2a313190ada80e5077865a8684"
-  integrity sha512-dWZp/dVGdUEfRBjBq2BgNuBlFqHCxyyMc8FsN0NX15X07mxSUO0SZRLyK/fdAVrde8nqFI/FEdMH4rgU9fqJfQ==
-  dependencies:
-    "@opentelemetry/context-async-hooks" "1.27.0"
-    "@opentelemetry/core" "1.27.0"
-    "@opentelemetry/propagator-b3" "1.27.0"
-    "@opentelemetry/propagator-jaeger" "1.27.0"
-    "@opentelemetry/sdk-trace-base" "1.27.0"
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.28.0.tgz#e3384802e12c689bb7a84809a732d091a2fbda09"
+  integrity sha512-N0sYfYXvHpP0FNIyc+UfhLnLSTOuZLytV0qQVrDWIlABeD/DWJIGttS7nYeR14gQLXch0M1DW8zm3VeN6Opwtg==
+  dependencies:
+    "@opentelemetry/context-async-hooks" "1.28.0"
+    "@opentelemetry/core" "1.28.0"
+    "@opentelemetry/propagator-b3" "1.28.0"
+    "@opentelemetry/propagator-jaeger" "1.28.0"
+    "@opentelemetry/sdk-trace-base" "1.28.0"
     semver "^7.5.2"
 
 "@opentelemetry/semantic-conventions@1.25.1":
@@ -974,11 +974,16 @@
   resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz#0deecb386197c5e9c2c28f2f89f51fb8ae9f145e"
   integrity sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==
 
-"@opentelemetry/semantic-conventions@1.27.0", "@opentelemetry/semantic-conventions@^1.25":
+"@opentelemetry/semantic-conventions@1.27.0":
   version "1.27.0"
   resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz#1a857dcc95a5ab30122e04417148211e6f945e6c"
   integrity sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==
 
+"@opentelemetry/semantic-conventions@^1.25":
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6"
+  integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==
+
 "@pkgjs/parseargs@^0.11.0":
   version "0.11.0"
   resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
@@ -1045,9 +1050,9 @@
     "@pulumi/pulumi" "^3.136.0"
 
 "@pulumi/aws@^6.56.0":
-  version "6.57.0"
-  resolved "https://registry.yarnpkg.com/@pulumi/aws/-/aws-6.57.0.tgz#3f41e2645f3ff0d41c2952b5bef0532d7b5a7b09"
-  integrity sha512-cFwb7EbbfWntByGuyNmQAZaTXyJu1Wxcuw8aWsBS5xF/C/28iYUooRFXWT37sjYbFtYqTDYGG+LSsg+WKl1yHw==
+  version "6.60.0"
+  resolved "https://registry.yarnpkg.com/@pulumi/aws/-/aws-6.60.0.tgz#23944ecbd76b9847e6c018f76abc3e604471b27a"
+  integrity sha512-TUQngWDlRq41dyOhHwVdbLJuMqbxfS+BMJDznJUYb3ajr2KlSkMXFbO3d+jF7U9eTGP95+VfppRj2yQz/rSMrA==
   dependencies:
     "@pulumi/pulumi" "^3.136.0"
     builtin-modules "3.0.0"
@@ -1413,9 +1418,9 @@
     "@types/node" "*"
 
 "@types/node@*", "@types/node@>=13.7.0":
-  version "22.9.0"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365"
-  integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==
+  version "22.9.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.2.tgz#51e58f2bf29cc38f529dacbb0eafca890464138e"
+  integrity sha512-wwuxAVEbsRvDD9x7buvAl7DyQ7Oj+va/d/Veug7higYzp9MF0CINbfWTBgDFMpcVwcdUiYuNmX2KfnvY3N70mw==
   dependencies:
     undici-types "~6.19.8"
 
@@ -2323,9 +2328,9 @@ create-require@^1.1.0:
   integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
 
 cross-spawn@^7.0.0, cross-spawn@^7.0.3:
-  version "7.0.5"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
-  integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+  integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
   dependencies:
     path-key "^3.1.0"
     shebang-command "^2.0.0"


Test patch:
diff --git a/tests/converters/intrinsics.test.ts b/tests/converters/intrinsics.test.ts
index f8a66ab..6e71aac 100644
--- a/tests/converters/intrinsics.test.ts
+++ b/tests/converters/intrinsics.test.ts
@@ -1,4 +1,13 @@
+import * as aws from '@pulumi/aws-native';
+import * as pulumi from '@pulumi/pulumi';
 import * as intrinsics from '../../src/converters/intrinsics';
+import {
+    CloudFormationParameter,
+    CloudFormationParameterLogicalId,
+    CloudFormationParameterWithId
+} from '../../src/cfn';
+import { Mapping } from '../../src/types';
+import { PulumiResource } from '../../src/pulumi-metadata';
 
 describe('Fn::If', () => {
     test('picks true', async () => {
@@ -102,7 +111,6 @@ describe('Fn::And', () => {
     });
 })
 
-
 describe('Fn::Not', () => {
     test('inverts false', async () => {
         const tc = new TestContext({});
@@ -167,6 +175,160 @@ describe('Fn::Equals', () => {
     });
 })
 
+describe('Ref', () => {
+    test('resolves a parameter by its logical ID', async () => {
+        const tc = new TestContext({parameters: {
+            'MyParam': {id: 'MyParam', Type: 'String', Default: 'MyParamValue'}
+        }});
+        const result = runIntrinsic(intrinsics.ref, tc, ['MyParam']);
+        expect(result).toEqual(ok('MyParamValue'));
+    });
+
+    test('respects "id" resource mapping provided by the user', async () => {
+        const tc = new TestContext({resources: {
+            'MyRes': {
+                resource: <any>{},
+                resourceType: 'AWS::S3::Bucket',
+                attributes: {
+                    'id': 'myID'
+                },
+            },
+        }});
+        const result = runIntrinsic(intrinsics.ref, tc, ['MyRes']);
+        expect(result).toEqual(ok('myID'));
+    });
+
+    test('resolves a CustomResource to its physical ID', async () => {
+        const tc = new TestContext({resources: {
+            'MyRes': {
+                resource: <any>{
+                    __pulumiType: (<any>aws.cloudformation.CustomResourceEmulator).__pulumiType,
+                    physicalResourceId: 'physicalID',
+                },
+                resourceType: 'AWS::CloudFormation::CustomResource',
+            },
+        }});
+        const result = runIntrinsic(intrinsics.ref, tc, ['MyRes']);
+        expect(result).toEqual(ok('physicalID'));
+    });
+
+    test('fails if Pulumi metadata indicates Ref is not supported', async () => {
+        const tc = new TestContext({
+            resources: {
+                'MyRes': {
+                    resource: <any>{},
+                    resourceType: 'AWS::S3::Bucket',
+                },
+            },
+            pulumiMetadata: {
+                'AWS::S3::Bucket': {
+                    inputs: {},
+                    outputs: {},
+                    cfRef: {
+                        notSupported: true,
+                    }
+                },
+            }
+        });
+        const result = runIntrinsic(intrinsics.ref, tc, ['MyRes']);
+        expect(result).toEqual(failed('Ref intrinsic is not supported for the AWS::S3::Bucket resource type'));
+    });
+
+    test('resolves to a property value indicated by Pulumi metadata', async () => {
+        const tc = new TestContext({
+            resources: {
+                'MyRes': {
+                    resource: <any>{stageName: 'my-stage'},
+                    resourceType: 'AWS::ApiGateway::Stage',
+                },
+            },
+            pulumiMetadata: {
+                'AWS::ApiGateway::Stage': {
+                    inputs: {},
+                    outputs: {},
+                    cfRef: {
+                        property: 'StageName',
+                    }
+                },
+            }
+        });
+        const result = runIntrinsic(intrinsics.ref, tc, ['MyRes']);
+        expect(result).toEqual(ok('my-stage'));
+    });
+
+    test('resolves to a join of several property values indicated by Pulumi metadata', async () => {
+        const tc = new TestContext({
+            resources: {
+                'MyRes': {
+                    resource: <any>{roleName: 'my-role', policyName: 'my-policy'},
+                    resourceType: 'AWS::IAM::RolePolicy',
+                },
+            },
+            pulumiMetadata: {
+                'AWS::IAM::RolePolicy': {
+                    inputs: {},
+                    outputs: {},
+                    cfRef: {
+                        properties: ['PolicyName', 'RoleName'],
+                        delimiter: '|',
+                    }
+                },
+            }
+        });
+        const result = runIntrinsic(intrinsics.ref, tc, ['MyRes']);
+        expect(result).toEqual(ok('my-policy|my-role'));
+    });
+
+    test('fails if called with an ID that does not resolve', async () => {
+        const tc = new TestContext({});
+        const result = runIntrinsic(intrinsics.ref, tc, ['MyParam']);
+        expect(result).toEqual(failed('Ref intrinsic unable to resolve MyParam: not a known logical resource or parameter reference'));
+    });
+
+    test('evaluates inner expressions before resolving', async () => {
+        const tc = new TestContext({
+            parameters: {
+                'MyParam': {id: 'MyParam', Type: 'String', Default: 'MyParamValue'}
+            },
+            conditions: {
+                'MyCondition': true,
+            },
+        });
+        const result = runIntrinsic(intrinsics.ref, tc, [{'Fn::If': ['MyCondition', 'MyParam', 'MyParam2']}]);
+        expect(result).toEqual(ok('MyParamValue'));
+    });
+
+    test('resolves pseudo-parameters', async () => {
+        const stackNodeId = 'stackNodeId';
+        const tc = new TestContext({
+            parameters: {
+                'MyParam': {id: 'MyParam', Type: 'String', Default: 'MyParamValue'}
+            },
+            conditions: {
+                'MyCondition': true,
+            },
+            accountId: '012345678901',
+            region: 'us-west-2',
+            partition: 'aws-us-gov',
+            urlSuffix: 'amazonaws.com.cn',
+            stackNodeId: stackNodeId,
+        });
+
+        expect(runIntrinsic(intrinsics.ref, tc, ['AWS::AccountId'])).toEqual(ok('012345678901'));
+        expect(runIntrinsic(intrinsics.ref, tc, ['AWS::Region'])).toEqual(ok('us-west-2'));
+        expect(runIntrinsic(intrinsics.ref, tc, ['AWS::Partition'])).toEqual(ok('aws-us-gov'));
+        expect(runIntrinsic(intrinsics.ref, tc, ['AWS::URLSuffix'])).toEqual(ok('amazonaws.com.cn'));
+        expect(runIntrinsic(intrinsics.ref, tc, ['AWS::NoValue'])).toEqual(ok(undefined));
+
+        expect(runIntrinsic(intrinsics.ref, tc, ['AWS::NotificationARNs']))
+            .toEqual(failed('AWS::NotificationARNs pseudo-parameter is not yet supported in pulumi-cdk'));
+
+        // These are approximations; testing the current behavior for completeness sake.
+        expect(runIntrinsic(intrinsics.ref, tc, ['AWS::StackId'])).toEqual(ok(stackNodeId));
+        expect(runIntrinsic(intrinsics.ref, tc, ['AWS::StackName'])).toEqual(ok(stackNodeId));
+    });
+})
+
 function runIntrinsic(fn: intrinsics.Intrinsic, tc: TestContext, args: intrinsics.Expression[]): TestResult<any> {
     const result: TestResult<any> = <any>(fn.evaluate(tc, args));
     return result;
@@ -185,23 +347,84 @@ function failed<T>(errorMessage: string): TestResult<T> {
 }
 
 class TestContext implements intrinsics.IntrinsicContext {
+    accountId: string;
+    region: string;
+    partition: string;
+    urlSuffix: string;
+    stackNodeId: string;
     conditions: { [id: string]: intrinsics.Expression };
+    parameters: { [id: CloudFormationParameterLogicalId]: CloudFormationParameterWithId };
+    resources: { [id: string]: Mapping<pulumi.Resource> };
+    pulumiMetadata: { [cfnType: string]: PulumiResource };
+
+    constructor(args: {
+        accountId?: string;
+        region?: string;
+        partition?: string;
+        urlSuffix?: string;
+        stackNodeId?: string;
+        conditions?: { [id: string]: intrinsics.Expression },
+        parameters?: { [id: CloudFormationParameterLogicalId]: CloudFormationParameterWithId },
+        resources?: { [id: string]: Mapping<pulumi.Resource> },
+        pulumiMetadata?: { [cfnType: string]: PulumiResource },
+    }) {
+        this.stackNodeId = args.stackNodeId || '';
+        this.accountId = args.accountId || '';
+        this.partition = args.partition || '';
+        this.region = args.region || '';
+        this.urlSuffix = args.urlSuffix || '';
+        this.conditions = args.conditions || {};
+        this.parameters = args.parameters || {};
+        this.resources = args.resources || {};
+        this.pulumiMetadata = args.pulumiMetadata || {};
+    }
 
-    constructor(args: {conditions?: { [id: string]: intrinsics.Expression }}) {
-        if (args.conditions) {
-            this.conditions = args.conditions;
-        } else {
-            this.conditions = {};
+    tryFindResource(cfnType: string): PulumiResource|undefined {
+        if (this.pulumiMetadata.hasOwnProperty(cfnType)) {
+            return this.pulumiMetadata[cfnType];
+        }
+    };
+
+    findParameter(parameterLogicalID: string): CloudFormationParameterWithId | undefined {
+        if (this.parameters.hasOwnProperty(parameterLogicalID)) {
+            return this.parameters[parameterLogicalID];
         }
     }
 
+    evaluateParameter(param: CloudFormationParameter): intrinsics.Result<any> {
+        // Simplistic but sufficient for this test suite.
+        return this.succeed(param.Default!);
+    }
+
     findCondition(conditionName: string): intrinsics.Expression|undefined {
         if (this.conditions.hasOwnProperty(conditionName)) {
             return this.conditions[conditionName];
         }
     }
 
+    findResourceMapping(resourceLogicalID: string): Mapping<pulumi.Resource> | undefined {
+        if (this.resources.hasOwnProperty(resourceLogicalID)) {
+            return this.resources[resourceLogicalID];
+        }
+    }
+
     evaluate(expression: intrinsics.Expression): intrinsics.Result<any> {
+        // Evaluate known heuristics.
+        const known = [intrinsics.fnAnd,
+                       intrinsics.fnEquals,
+                       intrinsics.fnIf,
+                       intrinsics.fnNot,
+                       intrinsics.fnOr,
+                       intrinsics.ref];
+        if (typeof expression === 'object' && Object.keys(expression).length == 1) {
+            for (const k of known) {
+                if (k.name === Object.keys(expression)[0]) {
+                    const args = expression[k.name];
+                    return k.evaluate(this, args)
+                }
+            }
+        }
+
         // Self-evaluate the expression. This is very incomplete.
         const result: TestResult<any> = {'ok': true, value: expression};
         return result;
@@ -225,4 +448,24 @@ class TestContext implements intrinsics.IntrinsicContext {
         const result: TestResult<any> = {'ok': true, value: r};
         return result;
     }
+
+    getAccountId(): intrinsics.Result<string> {
+        return this.succeed(this.accountId);
+    }
+
+    getRegion(): intrinsics.Result<string> {
+        return this.succeed(this.region);
+    }
+
+    getPartition(): intrinsics.Result<string> {
+        return this.succeed(this.partition);
+    }
+
+    getURLSuffix(): intrinsics.Result<string> {
+        return this.succeed(this.urlSuffix);
+    }
+
+    getStackNodeId(): intrinsics.Result<string> {
+        return this.succeed(this.stackNodeId);
+    }
 }