Fix: Transform Failures With DnsRecord And Custom Provider
Understanding the Issue
Hey guys! Let's dive into a tricky issue where transforms fail when you're using a custom Pulumi provider alongside Cloudflare's DnsRecord
resource. Specifically, this problem surfaces when you have a Tagger
component within your custom provider that's designed to modify the tags of Cloudflare DnsRecord
resources. This setup works perfectly fine with version 5 of the Cloudflare provider, but things go south when you upgrade to version 6.0.0 or later. Let's break down what's happening and how to tackle it.
When you run pulumi preview
, you encounter an error that points to unresolved promises. The error message looks something like this:
pulumi:pulumi:Stack (pulumi-typescript-dev):
The Pulumi runtime detected that 72 promises were still active
at the time that the process exited. There are a few ways that this can occur:
* Not using `await` or `.then` on a Promise returned from a Pulumi API
* Introducing a cyclic dependency between two Pulumi Resources
* A bug in the Pulumi Runtime
Leaving promises active is probably not what you want. If you are unsure about
why you are seeing this message, re-run your program with the `PULUMI_DEBUG_PROMISE_LEAK
S`
environment variable. The Pulumi runtime will then print out additional
debug information about the leaked promises.
error: an unhandled error occurred: Program exited with non-zero exit code: 1
This error message can be a bit cryptic, but it essentially means that Pulumi detected unfinished asynchronous operations when the program exited. This often happens when promises aren't correctly handled, or there's an unexpected issue within the Pulumi runtime itself. The problem seems to stem from the interaction between the custom provider's transform and the Cloudflare provider, specifically with DnsRecord
resources.
Diving into the Provider Code
Let's examine the provider code snippet to pinpoint the problem area. The NewTagger
function in the custom provider is where the resource transform is registered. This transform is intended to modify resource properties, particularly tags, based on certain conditions. Here’s the relevant code block:
func NewTagger(ctx *pulumi.Context, name string, args *TaggerArgs, opts ...pulumi.ResourceOption) (*Tagger, error) {
component := &Tagger{}
err := ctx.RegisterComponentResource(p.GetTypeToken(ctx.Context()), name, component, opts...)
if err != nil {
return nil, err
}
err = component.build(ctx, args) // sets all the tags with sensible defaults
if err != nil {
return nil, err
}
ctx.RegisterResourceTransform(
func(_ context.Context, tArgs *pulumi.ResourceTransformArgs) *pulumi.ResourceTransformResult {
// this can be generalised by provider. For example:
// `if strings.HasPrefix(args.Type, "<provider>:") { ... }`
if strings.HasPrefix(tArgs.Type, "aws:") {
tArgs.Props["tags"] = component.aws(tArgs.Props["tags"])
return &pulumi.ResourceTransformResult{
Props: tArgs.Props,
Opts: tArgs.Opts,
}
} else if tArgs.Type == "cloudflare:index/dnsRecord:DnsRecord" {
tArgs.Props["tags"] = component.cloudflare(tArgs.Props["tags"])
return &pulumi.ResourceTransformResult{ // ERRORS HERE
Props: tArgs.Props,
Opts: tArgs.Opts,
}
}
return nil
},
)
return component, nil
}
The key part of this code is the ctx.RegisterResourceTransform
function. This function registers a transform that gets applied to resources during Pulumi's planning phase. The transform checks the resource type (tArgs.Type
) and, if it matches cloudflare:index/dnsRecord:DnsRecord
, it modifies the tags
property. The code then returns a ResourceTransformResult
with the modified properties. The error is specifically flagged at this return statement within the Cloudflare DnsRecord
block.
Analyzing a Sample Program
To further illustrate the issue, let's consider a sample Pulumi TypeScript program:
import * as pulumi from "@pulumi/pulumi"
import * as cloudflare from "@pulumi/cloudflare"
import * as custom from "@platform/example"
const tagger = new custom.config.Tagger('tagger', {
service: 'pulumi-test',
})
const opts: pulumi.ResourceOptions = {
parent: tagger,
}
async function create(opts: pulumi.ResourceOptions) {
const zone = cloudflare.getZone({ zoneId: process.env.CLOUDFLARE_ZONE_ID })
new cloudflare.Record('example', {
zoneId: await zone.then((zone) => zone.zoneId!),
name: '<subdomain_name>',
content: '<load_balancer_url>',
type: 'CNAME',
ttl: 300,
tags: ["CustomTag:some-value"],
}, opts)
}
create(opts)
This program defines a custom Tagger
component and uses it to tag a Cloudflare DnsRecord
. The create
function asynchronously retrieves a Cloudflare zone and then creates a DnsRecord
within that zone. The resource options (opts
) specify the Tagger
as the parent resource.
When this program is executed with pulumi preview
, it triggers the transform within the custom provider. The transform is supposed to add or modify tags on the DnsRecord
, but instead, it leads to the promise leak error. This suggests that the issue is likely related to how the transform interacts with the asynchronous nature of Pulumi's resource creation process, particularly when dealing with the Cloudflare provider version 6.0.0 and later.
Decoding the Log Output
Let's dissect the log output to get a clearer picture. The pulumi preview
command produces the following output:
$ pulumi preview
Previewing update (dev):
Type Name Plan Info
+ pulumi:pulumi:Stack pulumi-typescript-dev create 1 error; 9 messages
├─ pulumi:providers:example default_0_1_0 1 warning
+ └─ example:config:Tagger tagger create
Diagnostics:
pulumi:pulumi:Stack (pulumi-typescript-dev):
error: an unhandled error occurred: Program exited with non-zero exit code: 1
The Pulumi runtime detected that 72 promises were still active
at the time that the process exited. There are a few ways that this can occur:
* Not using `await` or `.then` on a Promise returned from a Pulumi API
* Introducing a cyclic dependency between two Pulumi Resources
* A bug in the Pulumi Runtime
Leaving promises active is probably not what you want. If you are unsure about
why you are seeing this message, re-run your program with the `PULUMI_DEBUG_PROMISE_LEAK
S`
environment variable. The Pulumi runtime will then print out additional
debug information about the leaked promises.
pulumi:providers:example (default_0_1_0):
warning: provider attempted to use __internal key that is reserved by the engine
Resources:
+ 2 to create
1 errored
The log output highlights a few key points:
- The
pulumi:pulumi:Stack
resource encounters an error and exits with a non-zero exit code. - The error is related to unresolved promises, indicating an issue with asynchronous operations.
- There's a warning from the custom provider (
pulumi:providers:example
) about using an internal key, which might be a red herring but could also hint at underlying compatibility issues. - One resource errored during the preview, confirming that the transform is indeed causing a problem.
When the PULUMI_DEBUG_PROMISE_LEAKS=true
environment variable is set, the logs become far more detailed. These logs can provide invaluable insights into the sequence of asynchronous operations and where exactly the promises are getting leaked. While the full logs might be too extensive to share directly in a post, they often pinpoint specific areas in the code where promises are not being correctly resolved or handled.
Affected Resources
The primary resource affected by this issue is the Cloudflare DnsRecord
. This resource is defined in the Cloudflare Pulumi provider and is used to manage DNS records within a Cloudflare zone. The transform, intended to modify the tags of this resource, instead triggers the promise leak error, preventing the resource from being correctly provisioned or updated.
Environment Details
To provide a comprehensive understanding, it's essential to consider the environment in which this issue occurs. The output of pulumi about
gives us a snapshot of the versions and configurations involved:
Version 3.203.0
Go Version go1.25.3
Go Compiler gc
Plugins
KIND NAME VERSION
resource aws 6.83.0
resource cloudflare 6.0.0
resource example 0.1.0
language nodejs 3.203.0
resource random 4.18.2
Host
OS ubuntu
Version 22.04
Arch x86_64
This project is written in nodejs: executable='/home/elena/.nvm/versions/node/v24.5.0/bin/no
de' version='v24.5.0'
Current Stack: organization/pulumi-typescript/dev
Found no resources associated with dev
Found no pending operations associated with dev
Backend
Name <redacted>
URL <redacted_s3_backend>
User elena
Organizations
Token type personal
Dependencies:
NAME VERSION
@pulumi/pulumi 3.181.0
@types/node 18.19.115
typescript 5.8.3
@platform/example 0.1.0
@pulumi/aws 6.83.0
@pulumi/cloudflare 6.0.0
@pulumi/random 4.18.2
ts-node 10.9.2
Pulumi locates its logs in /tmp by default
From this output, we can gather the following key details:
- Pulumi CLI version: 3.203.0
- Cloudflare provider version: 6.0.0
- Custom provider version: 0.1.0
- Node.js version: v24.5.0
- Operating system: Ubuntu 22.04
These details confirm that the issue is occurring with the Cloudflare provider version 6.0.0, which aligns with the initial problem description. The combination of these versions and the custom provider is likely contributing to the transform failure.
The Bigger Picture: Centralized Tagging
The underlying goal here is to implement a centralized tagging strategy. The custom provider attempts to tag resources from various providers, including AWS and Cloudflare. This approach aims to provide a unified way to manage tags across all resources in a stack. However, the interaction between the custom provider's transform and the Cloudflare provider's DnsRecord
resource is causing unexpected issues. This highlights the complexities of resource transforms, especially when dealing with asynchronous operations and different provider versions.
Potential Solutions and Next Steps
So, what can we do to fix this? Here are some potential avenues to explore:
-
Review Asynchronous Handling: The error message strongly suggests an issue with how promises are being handled. A thorough review of the custom provider's transform logic, particularly the parts dealing with asynchronous calls, is crucial. Ensure that all promises are correctly awaited or handled with
.then
and.catch
. -
Cloudflare Provider Version Compatibility: Since the issue surfaced with Cloudflare provider version 6.0.0, it might be worth investigating the changes introduced in this version. There could be breaking changes or new requirements that the custom provider's transform is not yet accounting for. Comparing the behavior with version 5 might offer some clues.
-
Resource Transform Mechanics: Resource transforms can be tricky, especially when they interact with resources from different providers. Double-check the transform logic to ensure it's correctly handling the resource properties and options. It might be necessary to adjust how the transform modifies the
DnsRecord
tags. -
Debugging with
PULUMI_DEBUG_PROMISE_LEAKS
: The detailed logs generated by settingPULUMI_DEBUG_PROMISE_LEAKS=true
are invaluable. Dive deep into these logs to trace the promise execution flow and identify the exact point where promises are being leaked. -
Simplify and Isolate: Try simplifying the transform logic or isolating the Cloudflare
DnsRecord
resource to see if the issue persists. This can help narrow down the problem area and make debugging more manageable. -
Check Pulumi Community and Documentation: Look for similar issues or discussions in the Pulumi community or documentation. It's possible that others have encountered the same problem and found a solution or workaround.
Contributing to the Solution
If you're facing this issue, don't hesitate to contribute! Here's how you can help:
- Vote on the issue: Add a 👍 reaction to the original issue to show your support and indicate that you're affected.
- Share your findings: If you've made any progress in debugging or have additional insights, share them in the comments.
- Contribute a fix: If you've identified a solution, consider submitting a pull request with the fix. Link your pull request in the comments.
By working together, we can get to the bottom of this and ensure smooth sailing with custom providers and Cloudflare resources. Let's roll up our sleeves and get this fixed!