Skip to content

ZeroTrustTunnelCloudflared` delete reports success but tunnel is not deleted from Cloudflare #1440

Description

@forge-it

Describe what happened

Provider version: pulumi-cloudflare 6.4.1
Pulumi version: 3.181.0
Python version: 3.13

Description:

When replacing a ZeroTrustTunnelCloudflared resource with delete_before_replace=True, Pulumi reports the deletion as successful but the tunnel remains in Cloudflare. The subsequent create operation fails with a 409 Conflict error because the tunnel with that name still exists.

Steps to reproduce:

  1. Import an existing Cloudflare tunnel using import_ option
  2. Remove the import_ option and change a force-new property (e.g., tunnel_secret or config_src)
  3. Set delete_before_replace=True in ResourceOptions
  4. Run pulumi up

Expected behavior:

Pulumi should delete the tunnel from Cloudflare, then create a new one.

Actual behavior:

-- cloudflare:index:ZeroTrustTunnelCloudflared forgeit-infra deleted original (0.81s)
+- cloudflare:index:ZeroTrustTunnelCloudflared forgeit-infra replacing [diff: +configSrc,tunnelSecret]
++ cloudflare:index:ZeroTrustTunnelCloudflared forgeit-infra creating replacement error: 409 Conflict {"errors":[{"code":1013,"message":"You already have a tunnel with this name..."}]}

The tunnel was never deleted from Cloudflare despite Pulumi reporting "deleted original (0.81s)". Manual deletion via dashboard was required.

Possible causes:

  • The Cloudflare API delete call may be failing silently (no error propagation)
  • The tunnel may have active connections or DNS routes preventing deletion, but no error is surfaced
  • The provider may not be checking the API response status

Sample program

# __main__.py
import os
import pulumi
from pulumi import ResourceOptions
from pulumi_cloudflare import Provider, ZeroTrustTunnelCloudflared

CLOUDFLARE_ACCOUNT_ID = os.environ["CLOUDFLARE_ACCOUNT_ID"]
CLOUDFLARE_API_TOKEN = os.environ["CLOUDFLARE_API_TOKEN"]
CLOUDFLARE_TUNNEL_SECRET = "c2VjcmV0LXRlc3QtdmFsdWUtMTIzNDU2Nzg5MA=="  # base64 encoded

# Change this to "step2" after first successful run to trigger the bug
STEP = os.environ.get("STEP", "step1")

provider = Provider(
    "cloudflare-provider",
    api_token=CLOUDFLARE_API_TOKEN,
)

if STEP == "step1":
    # Step 1: Create the tunnel normally
    tunnel = ZeroTrustTunnelCloudflared(
        "test-tunnel",
        account_id=CLOUDFLARE_ACCOUNT_ID,
        name="pulumi-bug-repro-tunnel",
        tunnel_secret=CLOUDFLARE_TUNNEL_SECRET,
        config_src="cloudflare",
        opts=ResourceOptions(
            provider=provider,
            delete_before_replace=True,
        ),
    )
else:
    # Step 2: Change tunnel_secret to trigger replacement
    tunnel = ZeroTrustTunnelCloudflared(
        "test-tunnel",
        account_id=CLOUDFLARE_ACCOUNT_ID,
        name="pulumi-bug-repro-tunnel",
        tunnel_secret="bmV3LXNlY3JldC12YWx1ZS1hYmNkZWZnaGlqaw==",  # different secret
        config_src="cloudflare",
        opts=ResourceOptions(
            provider=provider,
            delete_before_replace=True,
        ),
    )

pulumi.export("tunnel_id", tunnel.id)

Log output

No response

Affected Resource(s)

No response

Output of pulumi about

Run export PATH=$PATH:${HOME}/.pulumi/bin

CLI
Version 3.184.0
Go Version go1.24.5
Go Compiler gc
Host
OS ubuntu
Version 24.04
Arch x86_64
This project is written in python: executable='/usr/bin/python3' version='3.12.3'
Backend
Name x
URL file:///var/pulumi-state/x_workspace
User github-runner
Organizations
Token type personal
Pulumi locates its logs in /tmp by default

Additional context

No response

Contributing

Vote on this issue by adding a 👍 reaction.
To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting-feedbackBlocked on input from the authorkind/bugSome behavior is incorrect or out of spec

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions