Skip to content

Commit f0da28c

Browse files
authored
Patch cloudflare worker placement (#6421)
1 parent b7c9e71 commit f0da28c

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { CustomResourceOptions, Input, Output, dynamic } from "@pulumi/pulumi";
2+
import { cfFetch } from "../helpers/fetch.js";
3+
import { DEFAULT_ACCOUNT_ID } from "../account-id.js";
4+
5+
interface Inputs {
6+
accountId: string;
7+
scriptName: string;
8+
mode?: string;
9+
region?: string;
10+
host?: string;
11+
hostname?: string;
12+
}
13+
14+
export interface WorkerPlacementInputs {
15+
accountId?: Input<Inputs["accountId"]>;
16+
scriptName: Input<Inputs["scriptName"]>;
17+
mode?: Input<Inputs["mode"]>;
18+
region?: Input<Inputs["region"]>;
19+
host?: Input<Inputs["host"]>;
20+
hostname?: Input<Inputs["hostname"]>;
21+
}
22+
23+
export interface WorkerPlacement {
24+
scriptName: Output<string>;
25+
}
26+
27+
function buildPlacement(inputs: Inputs) {
28+
if (inputs.mode) return { mode: inputs.mode };
29+
if (inputs.region) return { region: inputs.region };
30+
if (inputs.host) return { host: inputs.host };
31+
if (inputs.hostname) return { hostname: inputs.hostname };
32+
return {};
33+
}
34+
35+
class Provider implements dynamic.ResourceProvider {
36+
async create(inputs: Inputs): Promise<dynamic.CreateResult> {
37+
await this.patch(inputs, buildPlacement(inputs));
38+
return {
39+
id: inputs.scriptName,
40+
outs: { scriptName: inputs.scriptName },
41+
};
42+
}
43+
44+
async update(
45+
id: string,
46+
olds: Inputs,
47+
news: Inputs,
48+
): Promise<dynamic.UpdateResult> {
49+
await this.patch(news, buildPlacement(news));
50+
return {
51+
outs: { scriptName: news.scriptName },
52+
};
53+
}
54+
55+
async delete(id: string, inputs: Inputs) {
56+
await this.patch(inputs, {});
57+
}
58+
59+
async patch(inputs: Inputs, placement: Partial<Record<string, string>>) {
60+
const accountId = inputs.accountId || DEFAULT_ACCOUNT_ID;
61+
const formData = new FormData();
62+
formData.append(
63+
"settings",
64+
new Blob([JSON.stringify({ placement })], {
65+
type: "application/json",
66+
}),
67+
);
68+
await cfFetch(
69+
`/accounts/${accountId}/workers/scripts/${inputs.scriptName}/settings`,
70+
{
71+
method: "PATCH",
72+
body: formData,
73+
},
74+
);
75+
}
76+
}
77+
78+
export class WorkerPlacement extends dynamic.Resource {
79+
constructor(
80+
name: string,
81+
args: WorkerPlacementInputs,
82+
opts?: CustomResourceOptions,
83+
) {
84+
super(
85+
new Provider(),
86+
`${name}.sst.cloudflare.WorkerPlacement`,
87+
{ ...args, scriptName: args.scriptName },
88+
opts,
89+
);
90+
}
91+
}

platform/src/components/cloudflare/worker.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { Loader } from "esbuild";
1313
import type { EsbuildOptions } from "../esbuild.js";
1414
import { Component, Transform, transform } from "../component";
1515
import { WorkerUrl } from "./providers/worker-url.js";
16+
import { WorkerPlacement } from "./providers/worker-placement.js";
1617
import { Link } from "../link.js";
1718
import type { Input } from "../input.js";
1819
import { ZoneLookup } from "./providers/zone-lookup.js";
@@ -181,6 +182,36 @@ export interface WorkerArgs {
181182
*/
182183
directory: Input<string>;
183184
}>;
185+
/**
186+
* Configure [placement](https://developers.cloudflare.com/workers/configuration/placement/)
187+
* for your Worker.
188+
*
189+
* @example
190+
*
191+
* #### Smart Placement
192+
* ```js
193+
* {
194+
* placement: {
195+
* mode: "smart"
196+
* }
197+
* }
198+
* ```
199+
*
200+
* #### Explicit region
201+
* ```js
202+
* {
203+
* placement: {
204+
* region: "aws:us-east-1"
205+
* }
206+
* }
207+
* ```
208+
*/
209+
placement?: Input<{
210+
mode?: Input<string>;
211+
region?: Input<string>;
212+
host?: Input<string>;
213+
hostname?: Input<string>;
214+
}>;
184215
/**
185216
* [Transform](/docs/components/#transform) how this component creates its underlying
186217
* resources.
@@ -261,6 +292,7 @@ export interface WorkerArgs {
261292
export class Worker extends Component implements Link.Linkable {
262293
private script: cf.WorkersScript;
263294
private workerUrl: WorkerUrl;
295+
private workerPlacement?: WorkerPlacement;
264296
private workerDomain?: cf.WorkerDomain;
265297

266298
constructor(name: string, args: WorkerArgs, opts?: ComponentResourceOptions) {
@@ -290,10 +322,12 @@ export class Worker extends Component implements Link.Linkable {
290322
const build = buildHandler();
291323
const script = createScript();
292324
const workerUrl = createWorkersUrl();
325+
const workerPlacement = createWorkerPlacement();
293326
const workerDomain = createWorkersDomain();
294327

295328
this.script = script;
296329
this.workerUrl = workerUrl;
330+
this.workerPlacement = workerPlacement;
297331
this.workerDomain = workerDomain;
298332

299333
all([dev, buildInput, script.scriptName]).apply(
@@ -539,6 +573,22 @@ export class Worker extends Component implements Link.Linkable {
539573
);
540574
}
541575

576+
// workaround: pulumi cloudflare provider marks placement as read-only,
577+
// so we use the CF API directly until upstream support lands
578+
function createWorkerPlacement() {
579+
if (!args.placement) return;
580+
581+
return new WorkerPlacement(
582+
`${name}Placement`,
583+
{
584+
accountId: DEFAULT_ACCOUNT_ID,
585+
scriptName: script.scriptName,
586+
...args.placement,
587+
},
588+
{ parent },
589+
);
590+
}
591+
542592
function createWorkersDomain() {
543593
if (!args.domain) return;
544594

0 commit comments

Comments
 (0)