Been using Pulumi w/ Python for awhile. Finally de...
# python
g
Been using Pulumi w/ Python for awhile. Finally decided to join the community here on the Slack chan. I’m trying to figure out how to use an Output.concat as a key for a dictionary that is passed in as the
tags
attr for a resource Input.
Here is a snippet of what I’m attempting:
Copy code
tags = merge_dict(self.default_tags, {})
        tags["Name"] = self.cluster_randomized_name
That is a property accessor which simply pulls in the RandomPet:
Copy code
@property
    def cluster_randomized_name(self):
        if not self._cluster_randomized_name:
            self._cluster_randomized_name = pulumi_random.RandomPet(
                f"eks-cluster-{self.cluster_name}", prefix="eks"
            ).id

        return self._cluster_randomized_name
now, I go to create my resource:
Copy code
role = pulumi_aws.iam.Role(
            cluster_role_name_key,
            name=self.cluster_randomized_name,
            assume_role_policy=template_rendered(
                template_env=self.template_env,
                template_file="assume_role_policy_eks_service.json.j2",
            ),
            tags=tags,
            opts=pulumi.ResourceOptions(
                ignore_changes=["tags"] if import_id is not None else [],
                import_=import_id,
            ),
        )
I end up with this weird nugget
Copy code
File "/Users/thomasfarvour/.local/share/virtualenvs/pulumi-bYKQDQ7d/lib/python3.8/site-packages/google/protobuf/internal/well_known_types.py", line 731, in _SetStructValue
        struct_value.struct_value.update(value)
      File "/Users/thomasfarvour/.local/share/virtualenvs/pulumi-bYKQDQ7d/lib/python3.8/site-packages/google/protobuf/internal/well_known_types.py", line 805, in update
        _SetStructValue(self.fields[key], value)
    TypeError: <pulumi.output.Output object at 0x116a9b130> has type Output, but expected one of: bytes, unicode
    error: an unhandled error occurred: Program exited with non-zero exit code: 1

    Exception ignored in: <coroutine object Output.apply.<locals>.run at 0x116aafe40>
    Traceback (most recent call last):
      File "/Users/thomasfarvour/.local/share/virtualenvs/pulumi-bYKQDQ7d/lib/python3.8/site-packages/pulumi/output.py", line 209, in run
        result_resources.set_result(resources)
    UnboundLocalError: local variable 'resources' referenced before assignment
    Exception ignored in: <coroutine object Output.apply.<locals>.run at 0x116ab82c0>
    Traceback (most recent call last):
Is there a correct way to use dicts as Inputs or modify them before sending them into a resource as an Input[T]??
I have a similar issue in the subnet generation code as well :
Copy code
tags = {
            "Name": subnet_name_key,
            "tier": subnet_scope,
        }
        tags[
            pulumi.Output.concat("<http://kubernetes.io/cluster/|kubernetes.io/cluster/>", self.cluster_randomized_name)
        ] = "owned"
I guess.. I’m confused on how exactly one is able to create a dict outside the scope of a resource and feed it into an Input.. I mean, all the values are already known in the scope of the routine.
The docs show you how to do this for naked strings (Output.concat), but not complex types.
so I think my problem is that I’m trying to use it in the key (which I have to, since k8s / eks looks at the key of the dict as the tag, not the value) and I’m guessing pulumi doesn’t know how to deal with an Input[T] whose key is the value of an output/future…..???
if I swap the k/v around it works
s
did you try with .apply()?
1
r
You’ll need to create the entire dictionary within the apply. Something like this:
Copy code
def get_tags(subnet_name_key, subnet_scope, cluster_name):
    return {
        "Name": subnet_name_key,
        "tier": subnet_scope,
        f"<http://kubernetes.io/cluster/{cluster_name}|kubernetes.io/cluster/{cluster_name}>": "owned"
    }

tags = pulumi.Output
    .all(subnet_name_key, subnet_scope, self.cluster_randomized_name)
    .apply(lambda args: get_tags(args[0], args[1], args[2]))

subnet = pulumi_aws.ec2.Subnet(
    subnet_name_key,
    cidr_block=params["cidr_block"],
    vpc_id=self.vpc_id,
    tags=tags,
    opts=pulumi.ResourceOptions(
        ignore_changes=["tags"] if import_id is not None else [],
        import_=import_id,
    ),
)
g
Yup I figured this was going to be the end result. It works though! https://pulumi-community.slack.com/archives/CDE799L1M/p1619829888205700 Thank you for confirming>!
r
It works though!
Indeed! It works because you are creating the dictionary within the
apply
just as my suggestion does. And yes, there are many ways of doing the same thing, but there is a reason that the recommendation not to create resources within your apply exists. You are free to ignore the recommendation, but it can lead to unexpected behavior 😄
g
Right, I actually moved the resource creation outside the scope and just returned the tags Output.. since the dict is mutable in all threads, I only needed to modify it inside the callback, instead of making the whole resource.
I still have some situations where I am just creating the resource inside the callback because I have to do a comparison against a string that just happens to occur outside of the thread.. theres’ probably a better more correct way to do this (like creating a custom resource that extends an existing one to do the “comparison” logic with an Input[T])?
example:
Copy code
def subnet_route_table_assoc_1(
            rtb, rtb_id, rtb_tags, subnet_params, subnet_resource
        ):
            if subnet_params["zone"] == rtb_tags["zone"]:
                rta_name = f"vpc-{self.default_vpc_name}-rta-{rtb_id}-{subnet_params['cidr_block']}"
                pulumi_aws.ec2.RouteTableAssociation(
                    rta_name,
                    route_table_id=rtb_id,
                    subnet_id=subnet_resource.id,
                    opts=pulumi.ResourceOptions(parent=rtb, depends_on=[rtb]),
                )

        def subnet_route_table_assoc(rtbs, subnet_params, subnet_resource):
            for rtb in rtbs.values():
                pulumi.Output.all(
                    rtb, rtb.id, rtb.tags, subnet_params, subnet_resource
                ).apply(lambda args1: subnet_route_table_assoc_1(*args1))

        for subnet in self.in_private_subnets:
            self._cluster_private_subnets[subnet["cidr_block"]] = self.create_subnet(
                params=subnet,
                is_public=False,
            )

            # Associate the route table for the appropriate zone and scope to this
            # if it is passed in.

            pulumi.Output.all(
                self.private_route_tables,
                subnet,
                self.cluster_private_subnets[subnet["cidr_block"]],
            ).apply(lambda args: subnet_route_table_assoc(*args))
it’s a bit spaghetti, but I can’t think of any other way other than extending the RouteTableAssociation resource to implement my own comparator logic against an Input[T].. ??? maybe that is the right approach though.
the purpose of this code is to answer the question: “Don’t create this resource unless this thing is truthy outside of the scope of the callback.”
the “named zone” isn’t part of the .get() attribute of a RouteTable or Subnet so it’s difficult to do this outside that scope
I had to do something similar and ugly in terraform back in the day..
iterate the route tables to determine if it has a tag, and its value == the same value of the subnet’s zone tag or whatever because the AWS API doesn’t give you either of this information from the 2 resources themself.
so like I said, maybe the correct approach is creating some sort of dummy resource that does the comparison to determine if it should even create itself.. I dunno I couldnt/ find any good pulumi docs on it… so I just am creating the RTA in the callback despite the docs saying not to.
maybe a nice flag on pulumi.CustomResource would be a opts for
should_create
and allow that to be a future..
just a thought…
that way it would be easy to placeholder a resource from even getting created based on a future value. without having to do it in the callback.
the Future output could simply be a bool that is computed in the callback, returned, and sent to the
should_create
pulumi opt/input[T]?
TBH under the covers I bet that’s how terraform handled this with the
count
directive… since the entire HCL was declarative, there was no if/else block — thus the resource had to physically exist in the code in the main thread, but didn’t necessarily get created unless the future count was truthy… so I guess, TL;DR, this is the type of flag I’m looking for on a pulumi resource.. woudl be very handy! and maybe it exists already and I’m unaware of it?