Skip to content

Commit

Permalink
Handle pulumi.Output recursively when translating CDK properties to P…
Browse files Browse the repository at this point in the history
…ulumi (#228)

`pulumi.Output` values seem to be able to reach the `normalize` step of
translating properties from a CDK notation to a Pulumi notation and
making sure the casing is right. This may be happening in particular
because CF intrinsic evaluator may emit Output values. With the fix, the
`normalize` step can deeply recur into these eventual values and make
sure that the property casing is correctly handled regardless of whether
a value is an eventual or not.

This seems to resolve an issue I have been running into when testing a
Kinesis Stream.
  • Loading branch information
t0yv0 authored Nov 19, 2024
1 parent 88cac59 commit be3a4f2
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 5 deletions.
15 changes: 12 additions & 3 deletions src/interop.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016-2022, Pulumi Corporation.
// Copyright 2016-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,18 +38,27 @@ export function firstToLower(str: string) {
export function normalize(value: any, cfnType?: string, pulumiProvider?: PulumiProvider): any {
if (!value) return value;

if (value instanceof Promise) {
return pulumi.output(value).apply(v => normalize(v, cfnType, pulumiProvider));
}

if (pulumi.Output.isInstance(value)) {
return value.apply(v => normalize(v, cfnType, pulumiProvider));
}

if (Array.isArray(value)) {
const result: any[] = [];
for (let i = 0; i < value.length; i++) {
result[i] = normalize(value[i], cfnType);
result[i] = normalize(value[i], cfnType, pulumiProvider);
}
return result;
}

if (typeof value !== 'object' || pulumi.Output.isInstance(value) || value instanceof Promise) {
if (typeof value !== 'object') {
return value;
}

// The remaining case is the object type, representing either Maps or Object types with known field types in Pulumi.
const result: any = {};
Object.entries(value).forEach(([k, v]) => {
result[toSdkName(k)] = normalizeObject([k], v, cfnType, pulumiProvider);
Expand Down
29 changes: 27 additions & 2 deletions src/pulumi-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
// Copyright 2016-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as path from 'path';
import * as pulumi from '@pulumi/pulumi';
import { toSdkName, typeToken } from './naming';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pulumiMetadata = require(path.join(__dirname, '../schemas/aws-native-metadata.json'));
import { PulumiProvider } from './types';
import { debug } from '@pulumi/pulumi/log';
import * as pulumi from '@pulumi/pulumi';


export class UnknownCfnType extends Error {
constructor(cfnType: string) {
Expand Down Expand Up @@ -269,6 +284,15 @@ export function getNativeType(
*/
export function normalizeObject(key: string[], value: any, cfnType?: string, pulumiProvider?: PulumiProvider): any {
if (!value) return value;

if (value instanceof Promise) {
return pulumi.output(value).apply(v => normalizeObject(key, v, cfnType, pulumiProvider));
}

if (pulumi.Output.isInstance(value)) {
return value.apply(v => normalizeObject(key, v, cfnType, pulumiProvider));
}

if (Array.isArray(value)) {
const result: any[] = [];
for (let i = 0; i < value.length; i++) {
Expand All @@ -277,10 +301,11 @@ export function normalizeObject(key: string[], value: any, cfnType?: string, pul
return result;
}

if (typeof value !== 'object' || pulumi.Output.isInstance(value) || value instanceof Promise) {
if (typeof value !== 'object') {
return value;
}

// The remaining case is the actual object type.
const result: any = {};
if (cfnType) {
try {
Expand Down
28 changes: 28 additions & 0 deletions tests/interop.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { normalize } from '../src/interop';
import * as pulumi from '@pulumi/pulumi';

beforeEach(() => {
jest.resetAllMocks();
Expand Down Expand Up @@ -339,4 +340,31 @@ describe('normalize', () => {
},
});
});

test('normalize changes the case of nested properties through an Output eventual type', async () => {
const normalized = normalize({
'StreamEncryption': pulumi.output({
EncryptionType: 'KMS',
KeyId: 'alias/aws/kinesis'
})
});

const finalValue = await awaitOutput(pulumi.output(normalized));

expect(finalValue).toEqual({
streamEncryption: {
encryptionType: 'KMS',
keyId: 'alias/aws/kinesis',
}
});
});
});

function awaitOutput<T>(out: pulumi.Output<T>): Promise<T> {
return new Promise((resolve, _reject) => {
out.apply(v => {
resolve(v);
return v;
})
});
}

0 comments on commit be3a4f2

Please sign in to comment.