pulumi: Dynamic resources in Python don't work outside of __main__

Instantiating a dynamic resource in Python outside of the __main__ module fails. This was filed as #4100 but that issue was incorrectly closed, in my opinion! The repro there is totally legit and I am copy/pasting it below.

Steps to reproduce

Run pulumi up on this program:

# __main__.py
import custom_resource

# custom_resource.py
import pulumi
import pulumi.dynamic


class CustomDynamicResourceProvider(
    pulumi.dynamic.ResourceProvider,
):
    pass


pulumi.dynamic.Resource(
    provider=CustomDynamicResourceProvider(),
    name='my-resource',
    props={},
)

It will incorrectly fail:

Previewing update (dev)

View Live: https://app.pulumi.com/benesch/pulumi-4100/dev/previews/b9c38f39-280d-40d2-9bed-e6c5ccd7636c

     Type                               Name             Plan       Info
 +   pulumi:pulumi:Stack                pulumi-4100-dev  create     
     └─ pulumi-python:dynamic:Resource  my-resource                 1 error
 
Diagnostics:
  pulumi-python:dynamic:Resource (my-resource):
    error: Exception calling application: Program run without the Pulumi engine available; re-run using the `pulumi` CLI

I’ve put this together in a Git repository, if that’s easier: https://github.com/benesch/pulumi-4100

As far as I can tell, all the dynamic resource examples in the pulumi/examples repository do so from __main__ so this bug is never observed.

cc @leezen @ilaif

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Comments: 15 (12 by maintainers)

Commits related to this issue

Most upvoted comments

Is there a workaround for this issue? Currently hitting this issue. It happens even if it’s written inside __main__

import binascii
import os

import pulumi
from godaddypy import Client, Account
from pulumi import Input, Output
from pulumi.dynamic import *


def get_client() -> Client:
    config = pulumi.Config()
    api_key = config.require_secret("godaddy_key")
    api_secret = config.require_secret("godaddy_secret")
    godaddyClient = Client()

    def setupClient(self, args):
        account = Account(args[0], api_secret=args[1])
        godaddyClient = Client(account)

    pulumi.Output.all(api_key, api_secret).all(setupClient)

    return godaddyClient


class GodaddyInputs(object):
    domain: Input[str]
    value: Input[str]
    name: Input[str]
    ttl: Input[str]
    ttype: Input[int]

    def __init__(self, domain, value, name, ttl, ttype):
        self.domain = domain
        self.value = value
        self.name = name
        self.ttl = ttl
        self.ttype = ttype


class GoDaddyProvider(ResourceProvider):

    def has_record(self, args):
        domain = args["domain"]
        name = args["name"]
        ttype = args["ttype"]

        records = get_client().get_records(domain)

        found = False

        for record in records:
            if record.get("name") is not None and record.get("type") is not None:
                if record["name"] == name and record["type"] == ttype:
                    found = True
                    break

        return found

    def create(self, args):

        if not self.has_record(args):
            get_client(). \
                add_record(args["domain"],
                           {
                               'data': args["value"],
                               'name': args["name"],
                               'ttl': args["ttl"],
                               'type': args["ttype"]
                           })

        return CreateResult("schema-" + binascii.b2a_hex(os.urandom(16)).decode("utf-8"), outs=args)

    def delete(self, id, args):
        if self.has_record(args):
            get_client().delete_records(args["domain"], args["name"])

    def diff(self, id, old_inputs, new_inputs):

        replaces = []
        if (old_inputs["domain"] != new_inputs["domain"]): replaces.append("domain")
        if (old_inputs["ttype"] != new_inputs["ttype"]): replaces.append("ttype")
        if (old_inputs["value"] != new_inputs["value"]): replaces.append("value")
        if (old_inputs["name"] != new_inputs["name"]): replaces.append("name")

        return DiffResult(
            changes=old_inputs != new_inputs,
            replaces=replaces,
            stables=None,
            delete_before_replace=True)

    def update(self, id, old_inputs, new_inputs):
        return UpdateResult(outs={**new_inputs})


class GoDaddy(Resource):
    domain: Output[str]
    value: Output[str]
    name: Output[str]
    ttl: Output[str]
    ttype: Output[int]

    def __init__(self, resource_name: str, args: GodaddyInputs, opts=None):
        super().__init__(GoDaddyProvider(), resource_name, vars(args), opts)