I'm playing with component resources and looking a...
# typescript
l
I'm playing with component resources and looking at urn. Here's my little test app:
Copy code
class ExampleAppCloudResources extends pulumi.ComponentResource {
    randomResource: random.RandomString;
    anotherRandomResource: random.RandomString;
    public readonly secretString: pulumi.Output<string>;
    constructor(name: string, opts: pulumi.ComponentResourceOptions) {
        super("ExampleAppCloudResources", name, {}, opts);
        //console.log("ExampleAppCloudResources constructor");
        this.randomResource = new random.RandomString("random", {
            length: 16,
            special: false
        }, { parent: this });
        this.secretString = this.randomResource.result;

        this.anotherRandomResource = new random.RandomString("another-random", {
            length: 16,
            special: false
        }, { parent: this });
    }
}
Copy code
var exampleApp = new ExampleAppCloudResources("test", {});
var exampleApp2 = new ExampleAppCloudResources("test2", {});
export const secretString = exampleApp.secretString
I would guess pulumi would generate urns based on the component resource "type" and id, but it only generates it based on the type, causing collisions here. To me that seems like a strange design choice, it would make everything more robust with less chance of urn collisions if the entire "tree" was taken into account. Does anyone know how I can control this? I'd hade to have to add
{name}
as a prefix to every single resource in my component resource. Is there some global setting that can auto-prefix for me?
I guess I found my answer in the bottom here: https://github.com/pulumi/pulumi/issues/20573 Very unfortunate design choice
e
To me that seems like a strange design choice, it would make everything more robust with less chance of urn collisions if the entire "tree" was taken into account.
Yes šŸ˜ž a very unfortunate early decision that we're currently stuck with.
Does anyone know how I can control this? I'd hade to have to add
{name}
as a prefix to every single resource in my component resource. Is there some global setting that can auto-prefix for me?
Not currently. The advice is to just prefix your component resource name to every child resources name. We really want to change this but the amount of code that depends on the current format of URNs is vast
l
thanks! How about adding a project-level feature flag defaulting to the "Old" behavior so that at least greenfield projects would not have to suffer from this?
e
I'll give that a thought, not sure if the blast radius of this is constrained to single projects
l
I'm not sure if I understand but as I don't know much about the pulumi codebase there's probably nuances to that that I'm not able to see.
l
I came across this when I started testing out Pulumi and thankfully I caught it early enough into adoption that I was able to create a base component class that extends pulumi.ComponentResource that includes the name in the type, and all of our custom components extend from that
Copy code
super(`ebx:component:${type}/${name}`, name, args, pulumi.mergeOptions(opts, ourOpts))
This almost certainly breaks some assumptions within pulumi, but it has worked consistently for over a year now
ā¤ļø 1
e
As long as your names don't have dollars or colons in them you'll probably be ok with that
l
thanks @late-balloon-24601 that's very good input. I'd be interested in seeing what you put in the "ourOpts" - would you mind posting a more complete example? I was able to make this work by doing something similar:
Copy code
export class CustomComponentResource<TData = any> extends ComponentResource {
    constructor(type: string, name: string, args: Inputs, opts: ComponentResourceOptions) {
        let thisType = `${type}/${name}`
        super(thisType, name, args, opts);
    }
}

export class RandomComponentResource extends CustomComponentResource {
    constructor(name: string, args: {}, opts: pulumi.ComponentResourceOptions) {
        super("componentresource:customComponentResource", name, {}, opts)
        const myRandom1 = new random.RandomId("myRandom1", {byteLength: 8,}, {parent: this})
        const myRandom2 = new random.RandomId("myRandom2", {byteLength: 8,}, {parent: this})
    }
}


const random1 = new RandomComponentResource("random1", {}, {});
const random2 = new RandomComponentResource("random2", {}, {});
l
I can't provide much more than small snippets for business-reasons, but there's nothing super interesting in there, mostly just holdovers from older versions of Pulumi where I was doing sketchy things to the nodejs runtime to fix missing features at the time. I inject a
transformations
(not a
transforms
, as it didn't include a handle to the actual resource) so we could walk up the resource tree and dynamically build up parent/child metadata in the resource tags, and some
prefix
options from before Pulumi added customising name formatting. The only other addition that's still relevant is
noPassPropsToComponent
which stops properties from being passed to the underlying ComponentResource due to a recent change where props are saved into the ComoponentResource state and that breaks some of my stacks because of other ways I'm abusing pulumi šŸ˜› It's mostly just technical debt. I do have some other somewhat interesting additions which might give you some ideas though. My additions are actually all held within a Mixin function so they can be added to any resource, not just ComponentResources, but these are ComponentResource specific: • I defined an
@Output
decorator which allows you to mark class members that you actually care about, which are then returned as a filtered-down
.outputs
member, so you can keep all of the resources accessible for programmatic reasons on your ComponentResource class but avoid massive unreadable outputs if you export the whole thing. If you have components within components, it'll recursively clean up the tree and only extract bits that have the decorator. It stores the list of keys in a symbol on the class instance. • I have a reasonably cool
stackRef
static function that you can use as a helper to get correctly-typed outputs for a particular component from another stack.
Copy code
/**
     * Returns a proxy object with getters for each output of the referenced stack.
     * @param stackReference A reference to the stack that exports an instance of this component
     * @param instanceExportName Optionally, if the component is not the default export of the stack, specify the name of the export where the component is
     * @returns An object with getters for each output of the component
     */
    static stackRef<T extends Extended>(this: new (...args: any[]) => T, stackReference: StackRefLike, instanceExportName?: string): OutputifyProperties<ComponentOutputs<T>> {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
      const self = this
      return new Proxy({}, {
        get: function (target, prop: keyof typeof self) {
          if (instanceExportName) {
            return stackReference.getOutput(instanceExportName)[prop]
          } else {
            return stackReference.getOutput(prop)
          }
        },
      }) as OutputifyProperties<ComponentOutputs<T>>
    }
Example usage:
Copy code
const stackRef = new pulumi.StackReference('cb1-ref', { name: 'organization/shared-code-artifacts-eks/cb1' })
const cluster = MyEksCluster.stackRef(stackRef, 'cluster') // correctly typed with all of the outputs specified with the @Output decorator on my MyEksCluster class
(where MyEksCluster is a ComponentResource augmented with the .stackRef static method, exported from the above stack with the name 'cluster')
l
nice, thanks for all the info! Will parse this slowly šŸ™‚