From cef32c88ee75a84267c1007608c042deb220a30b Mon Sep 17 00:00:00 2001 From: Greg Brimble Date: Wed, 23 Oct 2024 12:02:55 -0400 Subject: [PATCH] Correctly apply Durable Object migrations for namespaced scripts (#7011) --- .changeset/gold-moose-dream.md | 5 + .../wrangler/src/__tests__/deploy.test.ts | 212 +++++++++++++++--- packages/wrangler/src/deploy/deploy.ts | 1 + packages/wrangler/src/durable.ts | 74 +++--- packages/wrangler/src/versions/upload.ts | 1 + 5 files changed, 233 insertions(+), 60 deletions(-) create mode 100644 .changeset/gold-moose-dream.md diff --git a/.changeset/gold-moose-dream.md b/.changeset/gold-moose-dream.md new file mode 100644 index 000000000000..839c9aaf05c4 --- /dev/null +++ b/.changeset/gold-moose-dream.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +fix: Correctly apply Durable Object migrations for namespaced scripts diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index 7a6f237ff21b..c745a64ae2bd 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -6534,6 +6534,109 @@ addEventListener('fetch', event => {});` `); }); }); + + describe("dispatch namespaces", () => { + it("should deploy all migrations on first deploy", async () => { + writeWranglerToml({ + durable_objects: { + bindings: [ + { name: "SOMENAME", class_name: "SomeClass" }, + { name: "SOMEOTHERNAME", class_name: "SomeOtherClass" }, + ], + }, + migrations: [ + { tag: "v1", new_classes: ["SomeClass"] }, + { tag: "v2", new_classes: ["SomeOtherClass"] }, + ], + }); + fs.writeFileSync( + "index.js", + `export class SomeClass{}; export class SomeOtherClass{}; export default {};` + ); + mockSubDomainRequest(); + mockServiceScriptData({ + dispatchNamespace: "test-namespace", + }); // no scripts at all + mockUploadWorkerRequest({ + expectedMigrations: { + new_tag: "v2", + steps: [ + { new_classes: ["SomeClass"] }, + { new_classes: ["SomeOtherClass"] }, + ], + }, + useOldUploadApi: true, + expectedDispatchNamespace: "test-namespace", + }); + + await runWrangler( + "deploy index.js --dispatch-namespace test-namespace" + ); + expect(std.out).toMatchInlineSnapshot(` + "Total Upload: xx KiB / gzip: xx KiB + Worker Startup Time: 100 ms + Your worker has access to the following bindings: + - Durable Objects: + - SOMENAME: SomeClass + - SOMEOTHERNAME: SomeOtherClass + Uploaded test-name (TIMINGS) + Dispatch Namespace: test-namespace + Current Version ID: Galaxy-Class" + `); + }); + + it("should use a script's current migration tag when publishing migrations", async () => { + writeWranglerToml({ + durable_objects: { + bindings: [ + { name: "SOMENAME", class_name: "SomeClass" }, + { name: "SOMEOTHERNAME", class_name: "SomeOtherClass" }, + ], + }, + migrations: [ + { tag: "v1", new_classes: ["SomeClass"] }, + { tag: "v2", new_classes: ["SomeOtherClass"] }, + ], + }); + fs.writeFileSync( + "index.js", + `export class SomeClass{}; export class SomeOtherClass{}; export default {};` + ); + mockSubDomainRequest(); + mockServiceScriptData({ + script: { id: "test-name", migration_tag: "v1" }, + dispatchNamespace: "test-namespace", + }); + mockUploadWorkerRequest({ + expectedMigrations: { + old_tag: "v1", + new_tag: "v2", + steps: [ + { + new_classes: ["SomeOtherClass"], + }, + ], + }, + useOldUploadApi: true, + expectedDispatchNamespace: "test-namespace", + }); + + await runWrangler( + "deploy index.js --dispatch-namespace test-namespace" + ); + expect(std.out).toMatchInlineSnapshot(` + "Total Upload: xx KiB / gzip: xx KiB + Worker Startup Time: 100 ms + Your worker has access to the following bindings: + - Durable Objects: + - SOMENAME: SomeClass + - SOMEOTHERNAME: SomeOtherClass + Uploaded test-name (TIMINGS) + Dispatch Namespace: test-namespace + Current Version ID: Galaxy-Class" + `); + }); + }); }); describe("tail consumers", () => { @@ -11817,13 +11920,14 @@ function mockServiceScriptData(options: { script?: DurableScriptInfo; scriptName?: string; env?: string; + dispatchNamespace?: string; }) { const { script } = options; - if (options.env) { + if (options.dispatchNamespace) { if (!script) { msw.use( http.get( - "*/accounts/:accountId/workers/services/:scriptName/environments/:envName", + "*/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName", () => { return HttpResponse.json({ success: false, @@ -11844,11 +11948,11 @@ function mockServiceScriptData(options: { } msw.use( http.get( - "*/accounts/:accountId/workers/services/:scriptName/environments/:envName", + "*/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName", ({ params }) => { expect(params.accountId).toEqual("some-account-id"); expect(params.scriptName).toEqual(options.scriptName || "test-name"); - expect(params.envName).toEqual(options.env); + expect(params.dispatchNamespace).toEqual(options.dispatchNamespace); return HttpResponse.json({ success: true, errors: [], @@ -11860,44 +11964,90 @@ function mockServiceScriptData(options: { ) ); } else { - if (!script) { + if (options.env) { + if (!script) { + msw.use( + http.get( + "*/accounts/:accountId/workers/services/:scriptName/environments/:envName", + () => { + return HttpResponse.json({ + success: false, + errors: [ + { + code: 10092, + message: "workers.api.error.environment_not_found", + }, + ], + messages: [], + result: null, + }); + }, + { once: true } + ) + ); + return; + } + msw.use( + http.get( + "*/accounts/:accountId/workers/services/:scriptName/environments/:envName", + ({ params }) => { + expect(params.accountId).toEqual("some-account-id"); + expect(params.scriptName).toEqual( + options.scriptName || "test-name" + ); + expect(params.envName).toEqual(options.env); + return HttpResponse.json({ + success: true, + errors: [], + messages: [], + result: { script }, + }); + }, + { once: true } + ) + ); + } else { + if (!script) { + msw.use( + http.get( + "*/accounts/:accountId/workers/services/:scriptName", + () => { + return HttpResponse.json({ + success: false, + errors: [ + { + code: 10090, + message: "workers.api.error.service_not_found", + }, + ], + messages: [], + result: null, + }); + }, + { once: true } + ) + ); + return; + } msw.use( http.get( "*/accounts/:accountId/workers/services/:scriptName", - () => { + ({ params }) => { + expect(params.accountId).toEqual("some-account-id"); + expect(params.scriptName).toEqual( + options.scriptName || "test-name" + ); return HttpResponse.json({ - success: false, - errors: [ - { - code: 10090, - message: "workers.api.error.service_not_found", - }, - ], + success: true, + errors: [], messages: [], - result: null, + result: { default_environment: { script } }, }); }, { once: true } ) ); - return; } - msw.use( - http.get( - "*/accounts/:accountId/workers/services/:scriptName", - ({ params }) => { - expect(params.accountId).toEqual("some-account-id"); - expect(params.scriptName).toEqual(options.scriptName || "test-name"); - return HttpResponse.json({ - success: true, - errors: [], - messages: [], - result: { default_environment: { script } }, - }); - }, - { once: true } - ) - ); } } diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index f8f708a9cdd2..5d9e44e214a8 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -632,6 +632,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m config, legacyEnv: props.legacyEnv, env: props.env, + dispatchNamespace: props.dispatchNamespace, }) : undefined; diff --git a/packages/wrangler/src/durable.ts b/packages/wrangler/src/durable.ts index d69185c5528e..9ac55f64b250 100644 --- a/packages/wrangler/src/durable.ts +++ b/packages/wrangler/src/durable.ts @@ -15,6 +15,7 @@ export async function getMigrationsToUpload( config: Config; legacyEnv: boolean | undefined; env: string | undefined; + dispatchNamespace: string | undefined; } ): Promise { const { config, accountId } = props; @@ -26,39 +27,42 @@ export async function getMigrationsToUpload( // get current migration tag type ScriptData = { id: string; migration_tag?: string }; let script: ScriptData | undefined; - if (!props.legacyEnv) { + if (props.dispatchNamespace) { try { - if (props.env) { - const scriptData = await fetchResult<{ - script: ScriptData; - }>( - `/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}` - ); - script = scriptData.script; - } else { - const scriptData = await fetchResult<{ - default_environment: { - script: ScriptData; - }; - }>(`/accounts/${accountId}/workers/services/${scriptName}`); - script = scriptData.default_environment.script; - } + const scriptData = await fetchResult<{ script: ScriptData }>( + `/accounts/${accountId}/workers/dispatch/namespaces/${props.dispatchNamespace}/scripts/${scriptName}` + ); + script = scriptData.script; } catch (err) { - if ( - ![ - 10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all - 10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet - ].includes((err as { code: number }).code) - ) { - throw err; - } - // else it's a 404, no script found, and we can proceed + suppressNotFoundError(err); } } else { - const scripts = await fetchResult( - `/accounts/${accountId}/workers/scripts` - ); - script = scripts.find(({ id }) => id === scriptName); + if (!props.legacyEnv) { + try { + if (props.env) { + const scriptData = await fetchResult<{ + script: ScriptData; + }>( + `/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}` + ); + script = scriptData.script; + } else { + const scriptData = await fetchResult<{ + default_environment: { + script: ScriptData; + }; + }>(`/accounts/${accountId}/workers/services/${scriptName}`); + script = scriptData.default_environment.script; + } + } catch (err) { + suppressNotFoundError(err); + } + } else { + const scripts = await fetchResult( + `/accounts/${accountId}/workers/scripts` + ); + script = scripts.find(({ id }) => id === scriptName); + } } if (script?.migration_tag) { @@ -100,3 +104,15 @@ export async function getMigrationsToUpload( } return migrations; } + +const suppressNotFoundError = (err: unknown) => { + if ( + ![ + 10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all + 10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet + ].includes((err as { code: number }).code) + ) { + throw err; + } + // else it's a 404, no script found, and we can proceed +}; diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 35779b27660d..8d5040bb1557 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -348,6 +348,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m config, legacyEnv: props.legacyEnv, env: props.env, + dispatchNamespace: undefined, }) : undefined;