Hi, I'm trying to get a little bit better understa...
# typescript
r
Hi, I'm trying to get a little bit better understanding of how dynamic providers work. I noticed the following behavior and I'd like to develop a clear explanation for it. I have created a
Cluster
resource representing a Kafka cluster in Confluent Cloud:
Copy code
class Cluster extends pulumi.dynamic.Resource {
    public readonly endpoint!: pulumi.Output<string>;

    constructor(
        name: string,
        props: IClusterInputs,
        opts?: pulumi.CustomResourceOptions,
    ) {
        super(new ClusterProvider(), name, props, opts);
    }
}
and I've written a dynamic provider for this resource:
Copy code
class ClusterProvider implements pulumi.dynamic.ResourceProvider {
    async create(
        inputs: IClusterProviderInputs
    ): Promise<pulumi.dynamic.CreateResult> {
        const cmd = `kafka cluster create ${inputs.clusterName}`
            + ` --cloud aws`
            + ` --region ${inputs.region}`
            + ` --availability ${inputs.availability}`
            + ` --type ${inputs.clusterType}`
            + ` --environment ${inputs.environmentId}`;
        const result: IClusterCreateResult = await runConfluentCliCommand(cmd, true);
        return {
            id: result.id,
            outs: { // implements IClusterCreateOutputs
                endpoint: result.endpoint,
                environmentId: inputs.environmentId,
            },
        };
    }

    async update(
        _id: string,
        olds: IClusterProviderInputs,
        _news: IClusterProviderInputs
    ): Promise<pulumi.dynamic.UpdateResult> {
        return {outs: olds}; // FIXME: implement a real update method
    }

    async delete(
        id: string,
        props: IClusterCreateOutputs
    ): Promise<void> {
        const cmd = `kafka cluster delete ${id} --environment ${props.environmentId}`;
        return await runConfluentCliCommand(cmd, false);
    }
}
Then I construct a
Cluster
instance and
export
an object as a stack output:
Copy code
const cluster = new Cluster("MyCluster", { ... });

export let stackOutputs = {
    cluster_id: cluster.id,
    cluster_endpoint: cluster.endpoint,
};
When I spin up this stack and look at the stack outputs, the
cluster_id
is present but the
cluster_endpoint
is absent. I was able to determine that the
cluster.endpoint
value was
undefined
. To "fix" this, I did the following:
Copy code
export let stackOutputs = {
    cluster_id: cluster.id,
    cluster_endpoint: pulumi.output(cluster.endpoint),
};
Now the
cluster_endpoint
shows up in my stack outputs!
Actually I'm not so sure... So, my question is: Why did wrapping this dynamic resource class member reference in a
pulumi.output(..)
cause it to be reified to a non
undefined
value? What is actually going on here? A corollary question: Why did I not have to also do this for the
id
? EDIT: I was a little confused, actually. My
pulumi.output(..)
edit did not, in fact, cause anything to show up in the stack output. The correct fix was to "initialize" the
endpoint
with an
undefined
in the call to
super(..)
in the
Cluster
constructor (see thread).
m
I think that I can answer this, though the answer for the
endpoint
bit isn’t particularly satisfying.
The constructor for the
pulumi.Resource
superclass needs to see all potential properties in the
props
bag, not just input properties. If the names of the output properties are not present in that bag, the
Resource
machinery can’t determine which output properties to expect.
So I think that if you change your code to this, things should work as you expect:
Copy code
class Cluster extends pulumi.dynamic.Resource {
    public readonly endpoint!: pulumi.Output<string>;

    constructor(
        name: string,
        props: IClusterInputs,
        opts?: pulumi.CustomResourceOptions,
    ) {
        super(new ClusterProvider(), name, { endpoint: undefined, ...props }, opts);
    }
}
The reason
pulumi.output(cluster.endpoint)
changed things is that it turned an
undefined
value into a resolved (but defined!)
pulumi.Output
value whose inner value is
undefined
.
w.r.t. `id`: you didn’t have to do anything special there because
id
is always defined by the SDK.
r
Oh, yeah I think you're right. Why do we have to "initialize"
endpoint
like that in the dynamic resource constructor?
m
I think that it’s because without that initialization, it’s not possible for the constructor to know what the set of output properties is for the resource. IIRC there are problems with attempting to iterate over the properties defined on
this
(e.g. properties that are present on every object shouldn’t be considered output properties), but my JS/TS expertise may be failing me here (cc @microscopic-pilot-97530).
r
Got it, that makes sense. Thanks.