important-elephant-42212
02/14/2025, 7:58 AMimport * as tailscale from "@pulumi/tailscale";
import * as aws from "@pulumi/aws";
const tn_key = new tailscale.TailnetKey("tailnet-key", {
});
new aws.ec2.Instance(`instance1`, {
tags: {
Name: "slartibartfast"
},
instanceType: "t3.small",
ami: "ami-01e2093820bf84df1",
// NOTE the .apply() is being set on the Input<string> at the top level
userData: tn_key.key.apply(v => `
#!/bin/sh
curl -fsSL <https://tailscale.com/install.sh> | sh
tailscale up --auth-key=${v}
`),
})
But for a more complex one, where the key needs to be outputted first, the response is always Calling [toString] on an [Output<T>] is not supported. ...
.
e.g.
import * as tailscale from "@pulumi/tailscale";
import * as aws from "@pulumi/aws";
const tn_key2 = new tailscale.TailnetKey("tailnet-key", {
});
new aws.ec2.Instance(`instance2`, {
tags: {
Name: "slartibartfast2"
},
instanceType: "t3.small",
ami: "ami-01e2093820bf84df1",
// NOTE the .apply() is inside the string interpolation
userData: `
#!/bin/sh
curl -fsSL <https://tailscale.com/install.sh> | sh
tailscale up --auth-key=${tn_key2.key.apply(v => v)}
`,
})
Please don't respond is you are simply going to suggest use apply or pulumi.interpolate.
The code above is complete, ie. with run if you put it in a index.ts.
If you are not 110% sure you answer will solve the problem, please run it on with pulumi up -d
.
If get ailscale up --auth-key=Calling [toString] on an [Output<T>] is not supported....
, then you have not solved the problem.
I have tried many alternatives (multiple versions of creating a custom ComponentResource, putting the tn_key in the dependsOn opts, ...)
My guess the issue has something to do how pulumi manages dependancies, but I could really do with some expert help. (@big-piano-35669)
Thanks in advance
Garyquick-house-41860
02/14/2025, 10:01 AMapply
) inside string interpolation. At the time the string interpolation is done the output isn't resolved yet (you can think of it like a promise).
I know you explicitly asked for other options, but variations of apply
(i.e. pulumi.all
, pulumi.interpolate
) or turning the Output
into a promise is what you'll need to do in order to await the result.
I'm trying to understand what you're referring to with "But for a more complex one, where the key needs to be outputted first [...]". Can you show an example of what you're trying to achieve conceptually?little-cartoon-10569
02/15/2025, 3:18 AMuserData: `
#!/bin/sh
curl -fsSL <https://tailscale.com/install.sh> | sh
tailscale up --auth-key=${tn_key2.key.apply(v => v)}
`
The interpolation must happen inside the apply:
userData: tn_key2.key.apply(v => {
return `#!/bin/sh
curl -fsSL <https://tailscale.com/install.sh> | sh tailscale up --auth-key=${v}`;
})
important-elephant-42212
02/17/2025, 4:33 AMapply
) looks like the a nested apply is required.
Here is a simple example.
import * as random from "@pulumi/random";
const r1 = new random.RandomString("random1", {
length: 16,
});
const r2 = new random.RandomString("random2", {
length: 16,
});
class BootScript {
lines: string[] = [];
sh(line: string) {
this.lines.push(line);
}
compile(): string {
return this.lines.join("\n")
}
}
const bs0 = new BootScript()
const bs1 = r1.result.apply(v => {
bs0.sh(`R1=${v}`)
return bs0
})
const bs2 = bs1.apply(bs => {
return r2.result.apply(v => {
bs.sh(`R2=${v}`)
return bs
})
})
export const data = bs2.apply(v => v.compile())
little-cartoon-10569
02/17/2025, 4:35 AMpulumi.all()
for that. That essentially groups multiple outputs into a single output array containing all the values.
https://www.pulumi.com/docs/iac/concepts/inputs-outputs/all/important-elephant-42212
02/17/2025, 1:37 PMall
can’t be used if the applies are in different places in the code base.late-balloon-24601
02/17/2025, 4:02 PM.apply
returns an Output
. You can't embed Outputs in strings. Using an apply
inside a string will never work because of this.
Taking the example from your original post, your ${tn_key2.key.apply(v => v)}
is essentially doing nothing, as it's converting an Output into an Output and then embedding it into a string that isn't being interpolated by Pulumi, so Node tries to interpolate it, hence the error.
I'm pretty sure the answer actually is just pulumi.interpolate
in that case. Note that you will not be able to see it in -debug' or preview because, as it's an output, it hasn't resolved yet.
import * as aws from "@pulumi/aws"
import * as pulumi from "@pulumi/pulumi"
import * as tailscale from "@pulumi/tailscale"
const tn_key2 = new tailscale.TailnetKey("tailnet-key", {
});
new aws.ec2.Instance(`instance2`, {
tags: {
Name: "slartibartfast2"
},
instanceType: "t3.small",
ami: "ami-01e2093820bf84df1",
// wrapped the entire userdata in a pulumi.interpolate, now Outputs inside the string will be resolved
userData: pulumi.interpolate`
#!/bin/sh
curl -fsSL <https://tailscale.com/install.sh> | sh
tailscale up --auth-key=${tn_key2.key}
`,
})
late-balloon-24601
02/17/2025, 4:18 PM.compile()
. In my experience, if you're trying to .apply
inside a .apply
, you need to refactor a little.
I'm having a little bit of trouble parsing the requirements here, but if I've understood correctly:
import * as random from "@pulumi/random";
import * as pulumi from "@pulumi/pulumi";
const r1 = new random.RandomString("random1", {
length: 16,
});
const r2 = new random.RandomString("random2", {
length: 16,
});
class BootScript {
lines: pulumi.Output<string>[] = [];
sh(line: pulumi.Output<string>) {
this.lines.push(line);
}
compile(): pulumi.Output<string> {
return pulumi.all(this.lines).apply(lines => lines.join("\n"));
}
}
const bs0 = new BootScript()
const bs1 = r1.result.apply(v => {
return `R1=${v}`
})
bs0.sh(bs1)
// I assume you wanted to wait for bs1 first here so they resolved in the correct order in the array. That won't be an issue here
const bs2 = r2.result.apply(v => {
return `R2=${v}`
})
bs0.sh(bs2)
export const data = bs0.compile()
Side note: It's very easy to end up with race conditions with this pattern, and just have pulumi hang and not make any progress during a deployment. There are ways to define the equivalent of CDK's 'lazy' variables, but it makes subtle and infuriating bugs. I have a fairly complex 'policy' class that makes use of that to have multiple components contribute to a policy and then collapse them all into optimised statements at the end, and the amount of times I've forgotten to call .freeze()
and just had Pulumi hang forever is ridiculouslittle-cartoon-10569
02/17/2025, 5:56 PMall()
can be applied. Any time nested `apply()`s might be needed, all()
cam be used instead.important-elephant-42212
02/17/2025, 10:31 PMthe amount of times I've forgotten to callWhat isand just had Pulumi hang forever is ridiculous.freeze()
freeze
?important-elephant-42212
02/17/2025, 11:43 PMimport * as random from "@pulumi/random";
import * as pulumi from "@pulumi/pulumi";
const r1 = new random.RandomString("random1", {
length: 16,
});
const r2 = new random.RandomString("random2", {
length: 16,
});
class BootScript {
lines: pulumi.Output<string>[] = [];
sh(line: string): void;
sh(line: pulumi.Output<string>): void;
sh(line: pulumi.Output<string> | string): void {
switch (typeof line) {
case "string":
this.lines.push(pulumi.output(line));
break
case "object":
this.lines.push(line);
break
}
}
compile(): pulumi.Output<string> {
// could used all instead of output
return pulumi.output(this.lines).apply(v => v.join("\n"))
}
}
const bs = new BootScript()
bs.sh("#!/bin/sh")
// apply or interpolate
bs.sh(r1.result.apply(v => `R1=${v}`))
bs.sh(pulumi.interpolate`R2=${r2.result}`)
export const data = bs.compile()
late-balloon-24601
02/18/2025, 8:42 PM.freeze()
was my equivalent to your .compile()
, except mine is pure evil because I made it so you could add more statements to a policy after you'd already used that policy in a resource, as long as you hadn't called freeze yet. Don't do that 😁important-elephant-42212
02/20/2025, 2:11 AMexcept mine is pure evil because I made it so you could add more statements to a policy after you'd already used that policy in a resource@late-balloon-24601 apologies is I'm stating the obvious, sounds like a fluent api might save you. e.g.
interface BSBuilder {
sh(line: pulumi.Output<string> | string): BSBuilder;
// remove if compile can't be called multiple times.
compile(): pulumi.Output<string>
freeze(): BSFrozen
}
interface BSFrozen {
compile(): pulumi.Output<string>
}
class BootScript implements BSBuilder {
lines: pulumi.Output<string>[] = [];
sh(line: pulumi.Output<string> | string): BSBuilder {
switch (typeof line) {
case "string":
this.lines.push(pulumi.output(line));
break
case "object":
this.lines.push(line);
break
}
return this
}
freeze(): BSFrozen {
return this
}
compile(): pulumi.Output<string> {
return pulumi.output(this.lines).apply(v => v.join("\n"))
}
}