https://pulumi.com logo
Title
a

agreeable-tomato-43927

06/23/2021, 2:50 PM
hello! Is there any way to pass an output into aws.s3.BucketObject source? Examples:
const bucketObject = new aws.s3.BucketObject("index.html", {
    acl: "public-read",
    contentType: "text/html",
    bucket: bucket,
    source: new pulumi.asset.FileAsset(pulumi.interpolate`${yarnBuild.path}/index.html`)
});
☝️this fails because FileAsset don't accept an output
const bucketObject = new aws.s3.BucketObject("index.html", {
    acl: "public-read",
    contentType: "text/html",
    bucket: bucket,
    source: pulumi.all([yarnBuild.path]).apply(([p]) => new pulumi.asset.FileAsset(`${p}/index.html`))
});
☝️this fails because p is undefined...
Some context: I'd like to deploy a full-stack app with Pulumi and I need to do the following: 1. Deploy backend (dynamoDB + API Gateway) 2. Build the frontend locally using the API URL from step 1 3. Deploy frontend (S3+Cloudfront) In order to do step 2 I created a custom Resource and Resource provider that gets the API URL as input, builds the frontend locally using yarn and outputs the local path of the static assets. I want to pass this output (
yarnBuild.path
) as an input to the BucketObject
b

brave-planet-10645

06/23/2021, 3:06 PM
You're close. I think you want something like
pulumi
    .all([yarnBuild.path])
    .apply(([p]) => new aws.s3.BucketObject("index.html", {
        acl: "public-read",
        contentType: "text/html",
        bucket: bucket,
        source: new pulumi.asset.FileAsset(`${p}/index.html`)
    }));
b

bored-oyster-3147

06/23/2021, 3:25 PM
Worth noting that .apply(...) documentation says not to declare new resources within an apply
b

brave-planet-10645

06/23/2021, 3:28 PM
Yes, the best practice is that you don't. If you do, you might find that the resources created there don't show up in the preview
a

agreeable-tomato-43927

06/23/2021, 3:33 PM
Hi @brave-planet-10645 I just tried your solution but the preview fails with:
error: Error: failed to register new resource index.html [aws:s3/bucketObject:BucketObject]: 2 UNKNOWN: failed to compute asset hash: failed to open asset file 'undefined/index.html': open undefined/index.html: no such file or directory
Then I tried to run with:
pulumi
    .all([yarnBuild.path])
    .apply(([p]) => p && new aws.s3.BucketObject("index.html", {
        acl: "public-read",
        contentType: "text/html",
        bucket: bucket,
        source: new pulumi.asset.FileAsset(`${p}/index.html`)
    }));
Now the deploy works but it doesn't create the BucketObject (after the deploy, there's an empty S3 bucket)
b

brave-planet-10645

06/23/2021, 3:35 PM
That's probably because you've got the
p && new...
in there. I'm not sure that this would work. Where are you getting yarnBuild.path from?
a

agreeable-tomato-43927

06/23/2021, 3:38 PM
if I don't put
p && new...
the preview fails as p is undefined. yarnBuild is a custom dynamic provider:
export interface YarnResourceInputs {
    apiUrl: pulumi.Input<string>;
}
interface YarnResourceProviderInputs {
    apiUrl: string;
}
interface YarnResourceProviderOutputs {
    path: string;
}

const YarnResourceProvider: pulumi.dynamic.ResourceProvider = {
    async create(inputs: YarnResourceProviderInputs): Promise<pulumi.dynamic.CreateResult> {
       //build frontend with "yarn build"
 console.log('YarnResourceProvider:create ', inputs.apiUrl)
        return { id: "123", outs: { path: './dist' }};
    },
    async diff(id: string, olds: YarnResourceProviderInputs, news: YarnResourceProviderInputs): Promise<pulumi.dynamic.DiffResult> {
        return {changes: true, replaces: ['apiUrl']}
    }
}

class YarnResource extends pulumi.dynamic.Resource {
    public readonly path!: pulumi.Output<string>;
    constructor(name: string, props: YarnResourceInputs, opts?: pulumi.CustomResourceOptions) {
        super(YarnResourceProvider, name, props, opts);
    }
}
then:
const yarnBuild = new YarnResource('yarn-build', {apiUrl: api.url})
api.url
comes from the backend resource
b

bored-oyster-3147

06/23/2021, 3:52 PM
I don't understand why we would do
pulumi.all(...)
for a single output since that is meant to be used to get multiple outputs into the same apply delegate. Why not just
yarnBuild.path.apply(p => new aws.s3.BucketObject(...));
Also it doesn't look like
YarnResource
is setting and/or registering
path
as an output?
b

brave-planet-10645

06/23/2021, 4:03 PM
So I think you're both wrong ( 😉 )... this is a dynamic resource which I don't think uses outputs... so Joshua: outputs don't need to be registered and Eric: you don't need to use outputs here
💡 1
b

bored-oyster-3147

06/23/2021, 4:05 PM
Gotcha - Haven't had the chance to use dynamic resources yet 🙂
a

agreeable-tomato-43927

06/23/2021, 4:05 PM
@bored-oyster-3147 I tried
yarnBuild.path.apply(...)
but it fails in the preview run as
yarnBuild.path == undefined
b

bored-oyster-3147

06/23/2021, 4:05 PM
Well that's because you're not setting it, right?
a

agreeable-tomato-43927

06/23/2021, 4:06 PM
yes, in the
YarnResourceProvider
create function
b

bored-oyster-3147

06/23/2021, 4:07 PM
but I don't think that sets
YarnResource.path
. You need to set it in your
YarnResource
constructor after calling the base constructor. And @brave-planet-10645 is saying that it can be
string
instead of
Output<string>
a

agreeable-tomato-43927

06/23/2021, 4:07 PM
@brave-planet-10645 I don't get why you say I don't need outputs here. The
YarnResourceProvider
is supposed to run "yarn build", grab the path of the build and return it as an output to be used by the bucket resource
b

brave-planet-10645

06/23/2021, 4:08 PM
Does it actually convert the string into an
Output<string>
?
a

agreeable-tomato-43927

06/23/2021, 4:09 PM
so far I'm just mocking it with:
async create(inputs: YarnResourceProviderInputs): Promise<pulumi.dynamic.CreateResult> {
       //build frontend with "yarn build"
        return { id: "123", outs: { path: './dist' }};
    },
I'm using examples from your docs. Do I need to convert the
'./dist'
part to an output object? I thought Pulumi did this automatically
b

brave-planet-10645

06/23/2021, 4:14 PM
I would expect Pulumi to do that automatically. I'll have a play with resource providers tomorrow morning and see what I can find out. This looks like we need some better examples in the examples repo as well
b

bored-oyster-3147

06/23/2021, 4:17 PM
Ok I just scanned the docs for this and you're right about not needing to set that property explicitly, I'm sorry. There is some magic happening to set that from your
CreateResult
. Not a fan of magic implementations like that but my bad for confusing the discussion I did notice that your code differs from the documentation example slightly on 1 point:
...

interface MyResourceProviderOutputs {
    myNumberOutput: number;
    myStringOutput: string;
}

class MyResourceProvider implements pulumi.dynamic.ResourceProvider {
    async create(inputs: MyResourceProviderInputs): Promise<pulumi.dynamic.CreateResult> {
        ...
        // Values are for an example only.
        return { id: "...", outs: { myNumberOutput: 12, myStringOutput: "some value" }};
    }
}

export class MyResource extends pulumi.dynamic.Resource {
    public readonly myStringOutput!: pulumi.Output<string>;
    public readonly myNumberOutput!: pulumi.Output<number>;

    constructor(name: string, props: MyResourceInputs, opts?: pulumi.CustomResourceOptions) {
        super(myprovider, name, { myStringOutput: undefined, myNumberOutput: undefined, ...props }, opts);
    }
}
Maybe that object expansion in the
super
call is important? They are doing something similar in all of the other SDK examples. Maybe your
super
call should be
super(YarnResourceProvider, name, { path: undefined, ...props }, opts);
a

agreeable-tomato-43927

06/23/2021, 4:52 PM
Hi @bored-oyster-3147 I don't think that makes any difference
b

bored-oyster-3147

06/23/2021, 4:55 PM
Weird to be in the documentation then. Why would they use object expansion to add null output properties to the input obj? very weird
b

brave-planet-10645

06/24/2021, 9:23 AM
So I've taken a look, and I think Joshua is correct here. You do need to expand the object. So using Eric's original code and adding the expanded object:
import * as pulumi from "@pulumi/pulumi";

export interface YarnResourceInputs {
    apiUrl: pulumi.Input<string>;
}
interface YarnResourceProviderInputs {
    apiUrl: string;
}
interface YarnResourceProviderOutputs {
    path: string;
}
const YarnResourceProvider: pulumi.dynamic.ResourceProvider = {
    async create(inputs: YarnResourceProviderInputs): Promise<pulumi.dynamic.CreateResult> {
       //build frontend with "yarn build"
//  console.log('YarnResourceProvider:create ', inputs.apiUrl)
        return { id: "123", outs: { path: './dist' }};
    },
    async diff(id: string, olds: YarnResourceProviderInputs, news: YarnResourceProviderInputs): Promise<pulumi.dynamic.DiffResult> {
        return {changes: true, replaces: ['apiUrl']}
    }
}
export class YarnResource extends pulumi.dynamic.Resource {
    public readonly path!: pulumi.Output<string>;
    constructor(name: string, props: YarnResourceInputs, opts?: pulumi.CustomResourceOptions) {
        super(YarnResourceProvider, name, {path: undefined, ...props}, opts);
    }
}
And then updating the program slightly so the bucketobject isn't created in the apply, but the fileasset, you get something like this:
import * as pulumi from "@pulumi/pulumi";
import * as provider from "./provider";
import * as aws from "@pulumi/aws";

const yarn = new provider.YarnResource("yarn", {
    apiUrl: "<http://thing.com|thing.com>"
});

export const path = yarn.path;

const bucket = new aws.s3.Bucket("bucket");

const file = yarn.path.apply(x => new pulumi.asset.FileAsset(x));

const bucketObject = new aws.s3.BucketObject("object", {
    source: file,
    bucket
})
Give that a go
a

agreeable-tomato-43927

06/24/2021, 6:03 PM
Thanks @brave-planet-10645! That worked 😃
🎉 1