Skip to content

Commit

Permalink
Add new transforms docs (#11851)
Browse files Browse the repository at this point in the history
Co-authored-by: Fraser Waters <fraser@pulumi.com>
  • Loading branch information
justinvp and Frassle authored Jul 15, 2024
1 parent d3b8117 commit a22d806
Show file tree
Hide file tree
Showing 5 changed files with 470 additions and 3 deletions.
3 changes: 2 additions & 1 deletion content/docs/concepts/options/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ All resource constructors accept an options argument that provide the following
- [providers](/docs/concepts/options/providers/): pass a set of [explicitly configured providers](/docs/concepts/resources/providers/#explicit-provider-configuration). These are used if provider is not given, and are passed to child resources.
- [replaceOnChanges](/docs/concepts/options/replaceonchanges/): declare that changes to certain properties should be treated as forcing a replacement.
- [retainOnDelete](/docs/concepts/options/retainondelete/): if true the resource will be retained in the backing cloud provider during a Pulumi delete operation.
- [transformations](/docs/concepts/options/transformations/): dynamically transform a resource’s properties on the fly.
- [transformations](/docs/concepts/options/transformations/): dynamically transform a resource’s properties on the fly. Prefer `transforms` if possible. `transformations` will be deprecated in the future in favor of `transforms`.
- [transforms](/docs/concepts/options/transforms/): dynamically transform a resource’s properties on the fly.
- [version](/docs/concepts/options/version/): pass a provider plugin version that should be used when operating on a resource.
6 changes: 4 additions & 2 deletions content/docs/concepts/options/parent.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,12 @@ Previewing update (dev):

Child resources inherit default values for many other resource options from their `parent`, including:

* [`provider`](/docs/concepts/options/provider): The provider instance used to construct a resource is inherited from it's parent, unless explicitly overridden by the child resource. The parent itself may have inherited the global [default provider](../providers/#default-provider-configuration) if no resource in the parent chain specified a provider instance for the corresponding provider type.
* [`provider`](/docs/concepts/options/provider): The provider instance used to construct a resource is inherited from its parent, unless explicitly overridden by the child resource. The parent itself may have inherited the global [default provider](../providers/#default-provider-configuration) if no resource in the parent chain specified a provider instance for the corresponding provider type.

* [`aliases`](/docs/concepts/options/aliases): Aliases applied to a parent are applied to all child resources, so that changing the type of a parent resource correctly changes the qualified type of a child resource, and changing the name of a parent resource correctly changes the name prefix of child resources.

* [`protect`](/docs/concepts/options/protect): A protected parent will protect all children. This ensures that if a parent is marked as protected, none of it's children will be deleted ahead of the attempt to delete the parent failing.

* [`transformations`](/docs/concepts/options/transformations): Transformations applied to a parent will run on the parent and on all child resources. This allows a transformation to be applied to a component to intercept and modify any resources created by it's children. As a special case, [Stack transformations](/docs/concepts/options/transformations#stack-transformations) will be applied to *all* resources (since all resources ultimately are parented directly or indirectly by the root stack resource).
* [`transformations`](/docs/concepts/options/transformations): Transformations applied to a parent will run on the parent and on all child resources. This allows a transformation to be applied to a component to intercept and modify any resources created by its children. As a special case, [Stack transformations](/docs/concepts/options/transformations#stack-transformations) will be applied to *all* resources (since all resources ultimately are parented directly or indirectly by the root stack resource). Prefer `transforms` if possible. `transformations` will be deprecated in the future in favor of `transforms`.

* [`transforms`](/docs/concepts/options/transforms): Transforms applied to a parent will run on the parent and on all child resources. This allows a transform to be applied to a component to intercept and modify any resources created by its children. As a special case, [Stack transforms](/docs/concepts/options/transforms#stack-transforms) will be applied to *all* resources (since all resources ultimately are parented directly or indirectly by the root stack resource).
203 changes: 203 additions & 0 deletions content/docs/concepts/options/transformations.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ The `transformations` resource option provides a list of transformations to appl

Each transformation is a callback that gets invoked by the Pulumi runtime. It receives the resource type, name, input properties, resource options, and the resource instance object itself. The callback returns a new set of resource input properties and resource options that will be used to construct the resource instead of the original values.

{{% notes type="warning" %}}
Note that Transformations will be deprecated in the future in favor of [Transforms](/docs/concepts/options/transforms).

Transforms support modifying child resources of packaged components (such as those in [awsx](/registry/packages/awsx) and [eks](/registry/packages/eks)) whereas Transformations do not.

See [Migrating from Transformations to Transforms](#migrating-from-transformations-to-transforms) below for guidance on how to migrate from Transformations to Transforms.
{{% /notes %}}

This example looks for all VPC and Subnet resources inside of a component’s child hierarchy and adds an option to ignore any changes for tags properties (perhaps because we manage all VPC and Subnet tags outside of Pulumi):

{{< chooser language "javascript,typescript,python,go,csharp,java,yaml" >}}
Expand Down Expand Up @@ -252,3 +260,198 @@ Pulumi.withOptions(stackOptions).run(ctx -> {
{{% /choosable %}}
{{< /chooser >}}
## Migrating from Transformations to Transforms
Transformations will be deprecated in the future in favor of the more capable [Transforms](/docs/concepts/options/transforms) APIs. While the Transforms APIs are similar to Transformations, there are some differences in both API signatures and runtime behavior to be aware of. When moving from Transformations to Transforms you will need to update your transform code to handle the differences.
Summary of key differences:
- [**No resource object**](#no-resource-object): There is no `Resource` object passed to transform functions. Most of the information you could have retrieved from that object is presented on the transform arguments directly, such as the type of the resource.
- [**No typed args classes**](#no-typed-args-classes): In the old transformation system the transform function is called with the same values that are passed to the resource constructor. This means that in languages like Go, C#, and Python, you could typecast the arguments to the typed args struct/class. The new transform system works over the wire protocol, allowing it to run for resources created in other processes, but it means the properties object you get is closer to the raw protocol than the typed arguments you might expect. Objects are represented as dictionaries/maps with camelCase keys (e.g. in Python, access properties with camelCase keys like `environmentVariables` instead of snake_case keys like `environment_variables`). Property names in resource options are also camelCase.
- [**Natively Async**](#natively-async): The new transform API has been designed from the start with async support in mind. In all applicable languages the transform functions support returning a Promise/Task so you can use standard `await` operators for async calls in the transform. In Node.js and Python, returning a Promise/Awaitable is optional.
### No Resource Object
There is no `Resource` object passed to transform functions. Most of the information you could have retrieved from that object is presented on the transform arguments directly.
This mostly impacts C# transforms. With transformations, you would have to call `args.Resource.GetResourceType()` to get the type of the resource:
```csharp
args =>
{
if (args.Resource.GetResourceType() == "random:index/randomString:RandomString")
{
var resultOpts = CustomResourceOptions.Merge((CustomResourceOptions)args.Options,
new CustomResourceOptions {AdditionalSecretOutputs = {"length"}});
return new ResourceTransformationResult(resultArgs, resultOpts);
}
return null;
}
```
With transforms, this is now available as `args.Type`:
```csharp
async (args, _) =>
{
if (args.Type == "random:index/randomString:RandomString")
{
var resultOpts = CustomResourceOptions.Merge((CustomResourceOptions)args.Options,
new CustomResourceOptions {AdditionalSecretOutputs = {"length"}});
return new ResourceTransformResult(resultArgs, resultOpts);
}
return null;
}
```
### No Typed Args Classes
In the old transform system the transform function was called with the same values passed to the resource constructor. This meant that in languages like Go, C#, and Python you could typecast to the typed arguments struct/class.
The new transform system works over the wire protocol, allowing it to run for resources created in other
processes, but it means the properties object you get is closer to the raw protocol than the typed arguments
you might expect. Objects are represented as dictionaries/maps with camelCase keys (e.g. in Python, access properties with camelCase keys like `environmentVariables` instead of snake_case keys like `environment_variables`). Property names in resource options are also camelCase.
{{< chooser language "python,go,csharp" >}}
{{% choosable language python %}}
Here's a Python example of the old system:
```python
def transformation(args: pulumi.ResourceTransformationArgs) -> pulumi.ResourceTransformationResult | None:
if args.type_ == "random:index/randomString:RandomString":
props = { **args.props }
length = pulumi.Output.from_input(props["length"])
props["length"] = length.apply(lambda v: v * 2)
props["special"] = True
props["override_special"] = "/@£$"
return pulumi.ResourceTransformationResult(props=props, opts=args.opts)
```
Here's the example with the new system:
```python
def transform(args: pulumi.ResourceTransformArgs) -> pulumi.ResourceTransformResult | None:
if args.type_ == "random:index/randomString:RandomString":
props = { **args.props }
length = pulumi.Output.from_input(props["length"])
props["length"] = length.apply(lambda v: v * 2)
props["special"] = True
props["overrideSpecial"] = "/@£$"
return pulumi.ResourceTransformResult(props=props, opts=args.opts)
```
Note in the new system, we specify the property name as `overrideSpecial` instead of `override_special`.
{{% /choosable %}}
{{% choosable language go %}}
In this Go example the old system would have given us a typed `RandomStringArgs` structure with the `Length` field
being a `pulumi.IntInput`:
```go
func(_ context.Context, args *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
if args.Type == "random:index/randomString:RandomString" {
var props *random.RandomStringArgs
if args.Props == nil {
props = &random.RandomStringArgs{}
} else {
props = args.Props.(*random.RandomStringArgs)
}
props.Length = props.Length.ToIntOutput().ApplyT(func(v int) int { return v * 2 }).(pulumi.IntOutput)
props.Special = pulumi.Bool(true)
props.OverrideSpecial = pulumi.String("/@£$")

return &pulumi.ResourceTransformationResult{
Props: props,
Opts: args.Opts,
}
}
return nil
}
```
The new system is just a map where we have to know the key is `"length"` and the value is a `Float64Input`:
```go
func(_ context.Context, args *pulumi.ResourceTransformArgs) *pulumi.ResourceTransformResult {
if args.Type == "random:index/randomString:RandomString" {
props := args.Props
if props == nil {
props = pulumi.Map{}
}
length := args.Props["length"].(pulumi.Float64Input).ToFloat64Output()
props["length"] = length.ApplyT(func(v float64) float64 { return v * 2 })
props["special"] = pulumi.Bool(true)
props["overrideSpecial"] = pulumi.String("/@£$")

return &pulumi.ResourceTransformResult{
Props: props,
Opts: args.Opts,
}
}
return nil
},
```
You might ask why `Float64Input` instead of at least `IntInput`. This is because the wire protocol doesn't
actually have support for integers (being JSON based), so on receiving the untyped properties the transform
callback can only go on their JSON values and all numbers are 64-bit floats.
{{% /choosable %}}
{{% choosable language csharp %}}
In this C# example, the old system would have given us a typed `RandomStringArgs` class with the `Length` field being an `Input<int>`:
```csharp
args =>
{
if (args.Resource.GetResourceType() == "random:index/randomString:RandomString")
{
var props = (RandomStringArgs)args.Args;
props.Length = props.Length.Apply(v => v * 2);
props.Special = true;
props.OverrideSpecial = "/@£$";
return new ResourceTransformationResult(props, args.Options);
}
return null;
}
```
The new system is just a dictionary where we have to know the key is `"length"` and the value is a `Input<double>`:
```csharp
(args, _) =>
{
if (args.Type == "random:index/randomString:RandomString")
{
var props = args.Args;
var length = (double)props["length"]! * 2;
props = props.SetItem("length", length);
props = props.SetItem("special", true);
props = props.SetItem("overrideSpecial", "/@£$");
return new ResourceTransformResult(props, args.Options);
}
return null;
}
```
{{% /choosable %}}
{{< /chooser >}}
### Natively Async
The new transform API has been designed from the start with async support in mind.
In all applicable languages the transform functions support returning a Promise/Task so you can use standard `await` operators for async calls in the transform.
For Go, we pass a `context.Context` in as the first argument for the transform function. This is to indicate its async nature, but also allows you access to the async context for tracing/logging/cancellation.
For .NET, we pass a `System.Threading.CancellationToken` as the last argument of the transform function. This allows you to handle cancellation if needed.
For Node.js and Python, the transform function can optionally be `async` and return a `Promise`/`Awaitable`.
Loading

0 comments on commit a22d806

Please sign in to comment.