Skip to content

Commit 75b75f3

Browse files
authored
containers: Throw a better error when there is no containers scope in the API token (#9731)
1 parent fdbc9f6 commit 75b75f3

File tree

7 files changed

+98
-8
lines changed

7 files changed

+98
-8
lines changed

.changeset/busy-planets-accept.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
containers: Check for container scopes before running a container command to give a better error

packages/wrangler/src/__tests__/cloudchamber/build.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ describe("buildAndMaybePush", () => {
4040
runInTempDir();
4141
mockApiToken();
4242
mockAccountId();
43-
mockAccount();
4443

4544
beforeEach(() => {
4645
vi.clearAllMocks();
4746
vi.mocked(dockerImageInspect).mockResolvedValue("53387881 2 []");
4847
mkdirSync("./container-context");
4948

5049
writeFileSync("./container-context/Dockerfile", dockerfile);
50+
mockAccount();
5151
});
5252
afterEach(() => {
5353
vi.clearAllMocks();

packages/wrangler/src/__tests__/cloudchamber/utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from "node:fs";
22
import * as TOML from "@iarna/toml";
33
import { http, HttpResponse } from "msw";
4+
import * as user from "../../user";
45
import { msw } from "../helpers/msw";
56
import type { CloudchamberConfig } from "../../config/environment";
67

@@ -17,6 +18,9 @@ export function setWranglerConfig(cloudchamber: CloudchamberConfig) {
1718
}
1819

1920
export function mockAccount() {
21+
const spy = vi.spyOn(user, "getScopes");
22+
spy.mockImplementationOnce(() => ["cloudchamber:write", "containers:write"]);
23+
2024
msw.use(
2125
http.get(
2226
"*/me",
@@ -33,7 +37,10 @@ export function mockAccount() {
3337
);
3438
}
3539

36-
export function mockAccountV4() {
40+
export function mockAccountV4(scopes: user.Scope[] = ["containers:write"]) {
41+
const spy = vi.spyOn(user, "getScopes");
42+
spy.mockImplementationOnce(() => scopes);
43+
3744
msw.use(
3845
http.get(
3946
"*/me",

packages/wrangler/src/__tests__/containers/info.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { http, HttpResponse } from "msw";
22
import patchConsole from "patch-console";
3+
import * as user from "../../user";
34
import { mockAccount, setWranglerConfig } from "../cloudchamber/utils";
45
import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
56
import { mockConsoleMethods } from "../helpers/mock-console";
@@ -45,6 +46,19 @@ describe("containers info", () => {
4546
`);
4647
});
4748

49+
it("should show the correct authentication error", async () => {
50+
const spy = vi.spyOn(user, "getScopes");
51+
spy.mockReset();
52+
spy.mockImplementationOnce(() => []);
53+
setIsTTY(false);
54+
setWranglerConfig({});
55+
await expect(
56+
runWrangler("containers info asdf")
57+
).rejects.toThrowErrorMatchingInlineSnapshot(
58+
`[Error: You need 'containers:write', try logging in again or creating an appropiate API token]`
59+
);
60+
});
61+
4862
it("should show a single container when given an ID (json)", async () => {
4963
setIsTTY(false);
5064
setWranglerConfig({});

packages/wrangler/src/__tests__/deploy.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9155,6 +9155,56 @@ addEventListener('fetch', event => {});`
91559155
expect(std.warn).toMatchInlineSnapshot(`""`);
91569156
});
91579157

9158+
it("should error when no scope for containers", async () => {
9159+
mockContainersAccount([]);
9160+
writeWranglerConfig({
9161+
durable_objects: {
9162+
bindings: [
9163+
{
9164+
name: "EXAMPLE_DO_BINDING",
9165+
class_name: "ExampleDurableObject",
9166+
},
9167+
],
9168+
},
9169+
containers: [
9170+
{
9171+
image: "docker.io/hello:world",
9172+
name: "my-container",
9173+
instances: 10,
9174+
class_name: "ExampleDurableObject",
9175+
},
9176+
],
9177+
migrations: [
9178+
{ tag: "v1", new_sqlite_classes: ["ExampleDurableObject"] },
9179+
],
9180+
});
9181+
fs.writeFileSync(
9182+
"index.js",
9183+
`export class ExampleDurableObject {}; export default{};`
9184+
);
9185+
mockSubDomainRequest();
9186+
mockLegacyScriptData({
9187+
scripts: [{ id: "test-name", migration_tag: "v1" }],
9188+
});
9189+
mockUploadWorkerRequest({
9190+
expectedBindings: [
9191+
{
9192+
class_name: "ExampleDurableObject",
9193+
name: "EXAMPLE_DO_BINDING",
9194+
type: "durable_object_namespace",
9195+
},
9196+
],
9197+
useOldUploadApi: true,
9198+
expectedContainers: [{ class_name: "ExampleDurableObject" }],
9199+
});
9200+
9201+
await expect(
9202+
runWrangler("deploy index.js")
9203+
).rejects.toThrowErrorMatchingInlineSnapshot(
9204+
`[Error: You need 'containers:write', try logging in again or creating an appropiate API token]`
9205+
);
9206+
});
9207+
91589208
it("should support durable objects and D1", async () => {
91599209
writeWranglerConfig({
91609210
main: "index.js",

packages/wrangler/src/cloudchamber/common.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { getCloudflareApiBaseUrl } from "../environment-variables/misc-variables
1717
import { UserError } from "../errors";
1818
import { isNonInteractiveOrCI } from "../is-interactive";
1919
import { logger } from "../logger";
20-
import { requireApiToken, requireAuth } from "../user";
20+
import { getScopes, printScopes, requireApiToken, requireAuth } from "../user";
2121
import { printWranglerBanner } from "../wrangler-banner";
2222
import { parseByteSize } from "./../parse";
2323
import { wrap } from "./helpers/wrap";
@@ -200,6 +200,15 @@ export async function fillOpenAPIConfiguration(
200200

201201
const accountId = await requireAuth(config);
202202
const auth = requireApiToken();
203+
const scopes = getScopes();
204+
if (!scopes?.includes(scope)) {
205+
logger.error(`You don't have '${scope}' in your list of scopes`);
206+
printScopes(scopes ?? []);
207+
throw new UserError(
208+
`You need '${scope}', try logging in again or creating an appropiate API token`
209+
);
210+
}
211+
203212
addAuthorizationHeaderIfUnspecified(headers, auth);
204213
addUserAgent(headers);
205214

packages/wrangler/src/user/user.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,11 +1206,7 @@ export async function logout(): Promise<void> {
12061206

12071207
export function listScopes(message = "💁 Available scopes:"): void {
12081208
logger.log(message);
1209-
const data = DefaultScopeKeys.map((scope: Scope) => ({
1210-
Scope: scope,
1211-
Description: DefaultScopes[scope],
1212-
}));
1213-
logger.table(data);
1209+
printScopes(DefaultScopeKeys);
12141210
// TODO: maybe a good idea to show usage here
12151211
}
12161212

@@ -1324,6 +1320,15 @@ export function getScopes(): Scope[] | undefined {
13241320
return LocalState.scopes;
13251321
}
13261322

1323+
export function printScopes(scopes: Scope[]) {
1324+
const data = scopes.map((scope: Scope) => ({
1325+
Scope: scope,
1326+
Description: DefaultScopes[scope],
1327+
}));
1328+
1329+
logger.table(data);
1330+
}
1331+
13271332
/**
13281333
* Make a request to the Cloudflare OAuth endpoint to get a token.
13291334
*

0 commit comments

Comments
 (0)