pulumi/tests/integration/transformations/nodejs/simple/index.ts
Luke Hoban 893e51d0ce
Add Python resource transformations support (#3319)
Adds Python support for resource transformations aligned with the existing NodeJS support in #3174.

This PR also moves processing of transformations to earlier in the resource construction process (for both NodeJS and Python) to ensure that invariants established in the constructor cannot be violated by transformations. This change can technically be a breaking change, but given that (a) the transformations features was just released in 1.3.0 and (b) the cases where this is a breaking change are uncommon and unlikely to have been reliable anyway - it feels like a change we should make now.

Fixes #3283.
2019-10-14 19:35:00 -05:00

155 lines
5.6 KiB
TypeScript

// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
import * as pulumi from "@pulumi/pulumi";
const simpleProvider: pulumi.dynamic.ResourceProvider = {
async create(inputs: any) {
return {
id: "0",
outs: { output: "a", output2: "b" },
};
},
};
interface SimpleArgs {
input: pulumi.Input<string>;
optionalInput?: pulumi.Input<string>;
}
class SimpleResource extends pulumi.dynamic.Resource {
output: pulumi.Output<string>;
output2: pulumi.Output<string>;
constructor(name, args: SimpleArgs, opts?: pulumi.CustomResourceOptions) {
super(simpleProvider, name, { ...args, output: undefined, output2: undefined }, opts);
}
}
class MyComponent extends pulumi.ComponentResource {
child: SimpleResource;
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("my:component:MyComponent", name, {}, opts);
this.child = new SimpleResource(`${name}-child`, { input: "hello" }, {
parent: this,
additionalSecretOutputs: ["output2"],
});
this.registerOutputs({});
}
}
// Scenario #1 - apply a transformation to a CustomResource
const res1 = new SimpleResource("res1", { input: "hello" }, {
transformations: [
({ props, opts }) => {
console.log("res1 transformation");
return {
props: props,
opts: pulumi.mergeOptions(opts, { additionalSecretOutputs: ["output"] }),
};
},
],
});
// Scenario #2 - apply a transformation to a Component to transform it's children
const res2 = new MyComponent("res2", {
transformations: [
({ type, props, opts }) => {
console.log("res2 transformation");
if (type === "pulumi-nodejs:dynamic:Resource") {
return {
props: { optionalInput: "newDefault", ...props },
opts: pulumi.mergeOptions(opts, { additionalSecretOutputs: ["output"] }),
};
}
},
],
});
// Scenario #3 - apply a transformation to the Stack to transform all (future) resources in the stack
pulumi.runtime.registerStackTransformation(({ type, props, opts }) => {
console.log("stack transformation");
if (type === "pulumi-nodejs:dynamic:Resource") {
return {
props: { ...props, optionalInput: "stackDefault" },
opts: pulumi.mergeOptions(opts, { additionalSecretOutputs: ["output"] }),
};
}
});
const res3 = new SimpleResource("res3", { input: "hello" });
// Scenario #4 - transformations are applied in order of decreasing specificity
// 1. (not in this example) Child transformation
// 2. First parent transformation
// 3. Second parent transformation
// 4. Stack transformation
const res4 = new MyComponent("res4", {
transformations: [
({ type, props, opts }) => {
console.log("res4 transformation");
if (type === "pulumi-nodejs:dynamic:Resource") {
return {
props: { ...props, optionalInput: "default1" },
opts,
};
}
},
({ type, props, opts }) => {
console.log("res4 transformation 2");
if (type === "pulumi-nodejs:dynamic:Resource") {
return {
props: { ...props, optionalInput: "default2" },
opts,
};
}
},
],
});
// Scenario #5 - cross-resource transformations that inject dependencies on one resource into another.
class MyOtherComponent extends pulumi.ComponentResource {
child1: SimpleResource;
child2: SimpleResource;
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("my:component:MyComponent", name, {}, opts);
this.child1 = new SimpleResource(`${name}-child1`, { input: "hello" }, { parent: this });
this.child2 = new SimpleResource(`${name}-child2`, { input: "hello" }, { parent: this });
this.registerOutputs({});
}
}
const transformChild1DependsOnChild2: pulumi.ResourceTransformation = (() => {
// Create a promise that wil be resolved once we find child2. This is needed because we do not
// know what order we will see the resource registrations of child1 and child2.
let child2Found: (res: pulumi.Resource) => void;
const child2 = new Promise<pulumi.Resource>((res) => child2Found = res);
// Return a transformation which will rewrite child1 to depend on the promise for child2, and
// will resolve that promise when it finds child2.
return (args: pulumi.ResourceTransformationArgs) => {
if (args.name.endsWith("-child2")) {
// Resolve the child2 promise with the child2 resource.
child2Found(args.resource);
return undefined;
} else if (args.name.endsWith("-child1")) {
// Overwrite the `input` to child2 with a dependency on the `output2` from child1.
const child2Input = pulumi.output(args.props["input"]).apply(async (input) => {
if (input !== "hello") {
// Not strictly necessary - but shows we can confirm invariants we expect to be
// true.
throw new Error("unexpected input value");
}
return child2.then(c2Res => c2Res["output2"]);
});
// Finally - overwrite the input of child2.
return {
props: { ...args.props, input: child2Input },
opts: args.opts,
};
}
};
})();
const res5 = new MyOtherComponent("res5", {
transformations: [ transformChild1DependsOnChild2 ],
});