Skip to content

Commit 5de2b9a

Browse files
authored
Add containers {info, list, delete} subcommands (#8673)
1 parent 5777b33 commit 5de2b9a

File tree

8 files changed

+614
-1
lines changed

8 files changed

+614
-1
lines changed

.changeset/huge-kings-switch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Add containers {info, list, delete} subcommands.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { http, HttpResponse } from "msw";
2+
import patchConsole from "patch-console";
3+
import { mockAccount, setWranglerConfig } from "../cloudchamber/utils";
4+
import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
5+
import { mockConsoleMethods } from "../helpers/mock-console";
6+
import { useMockIsTTY } from "../helpers/mock-istty";
7+
import { msw } from "../helpers/msw";
8+
import { runWrangler } from "../helpers/run-wrangler";
9+
10+
describe("containers delete", () => {
11+
const std = mockConsoleMethods();
12+
const { setIsTTY } = useMockIsTTY();
13+
14+
mockAccountId();
15+
mockApiToken();
16+
beforeEach(mockAccount);
17+
18+
afterEach(() => {
19+
patchConsole(() => {});
20+
msw.resetHandlers();
21+
});
22+
23+
it("should help", async () => {
24+
await runWrangler("containers delete --help");
25+
expect(std.err).toMatchInlineSnapshot(`""`);
26+
expect(std.out).toMatchInlineSnapshot(`
27+
"wrangler containers delete [ID]
28+
29+
delete a container
30+
31+
POSITIONALS
32+
ID id of the containers to delete [string]
33+
34+
GLOBAL FLAGS
35+
-c, --config Path to Wrangler configuration file [string]
36+
--cwd Run as if Wrangler was started in the specified directory instead of the current working directory [string]
37+
-e, --env Environment to use for operations, and for selecting .env and .dev.vars files [string]
38+
-h, --help Show help [boolean]
39+
-v, --version Show version number [boolean]
40+
41+
OPTIONS
42+
--json Return output as clean JSON [boolean] [default: false]"
43+
`);
44+
});
45+
46+
it("should delete container (json)", async () => {
47+
setIsTTY(false);
48+
setWranglerConfig({});
49+
msw.use(
50+
http.delete(
51+
"*/applications/:id",
52+
async ({ request }) => {
53+
expect(await request.text()).toEqual("");
54+
return new HttpResponse("{}");
55+
},
56+
{ once: true }
57+
)
58+
);
59+
await runWrangler("containers delete --json asdf");
60+
expect(std.err).toMatchInlineSnapshot(`""`);
61+
expect(std.out).toMatchInlineSnapshot(`"\\"{}\\""`);
62+
});
63+
64+
it("should error when trying to delete a non-existant container (json)", async () => {
65+
setIsTTY(false);
66+
setWranglerConfig({});
67+
msw.use(
68+
http.delete(
69+
"*/applications/*",
70+
async ({ request }) => {
71+
expect(await request.text()).toEqual("");
72+
return new HttpResponse(JSON.stringify({ error: "Not Found" }), {
73+
status: 404,
74+
});
75+
},
76+
{ once: true }
77+
)
78+
);
79+
expect(std.err).toMatchInlineSnapshot(`""`);
80+
await runWrangler("containers delete --json nope");
81+
expect(std.out).toMatchInlineSnapshot(
82+
`"\\"{/\\"error/\\":/\\"Not Found/\\"}\\""`
83+
);
84+
});
85+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { http, HttpResponse } from "msw";
2+
import patchConsole from "patch-console";
3+
import { mockAccount, setWranglerConfig } from "../cloudchamber/utils";
4+
import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
5+
import { mockConsoleMethods } from "../helpers/mock-console";
6+
import { useMockIsTTY } from "../helpers/mock-istty";
7+
import { msw } from "../helpers/msw";
8+
import { runWrangler } from "../helpers/run-wrangler";
9+
10+
const MOCK_APPLICATION_SINGLE = `{"id":"asdf","created_at":"2025-02-14T18:03:13.268999936Z","account_id":"test-account","name":"app-test","version":1,"configuration":{"image":"registry.test.cfdata.org/test-app:v1","network":{"mode":"private"}},"scheduling_policy":"regional","instances":2,"jobs":false,"constraints":{"region":"WNAM"},"durable_objects":{"namespace_id":"test-id"},"health":{"instances":{"healthy":2,"failed":0,"scheduling":0,"starting":0}}}`;
11+
12+
describe("containers info", () => {
13+
const std = mockConsoleMethods();
14+
const { setIsTTY } = useMockIsTTY();
15+
16+
mockAccountId();
17+
mockApiToken();
18+
beforeEach(mockAccount);
19+
20+
afterEach(() => {
21+
patchConsole(() => {});
22+
msw.resetHandlers();
23+
});
24+
25+
it("should help", async () => {
26+
await runWrangler("containers info --help");
27+
expect(std.err).toMatchInlineSnapshot(`""`);
28+
expect(std.out).toMatchInlineSnapshot(`
29+
"wrangler containers info [ID]
30+
31+
get information about a specific container
32+
33+
POSITIONALS
34+
ID id of the containers to view [string]
35+
36+
GLOBAL FLAGS
37+
-c, --config Path to Wrangler configuration file [string]
38+
--cwd Run as if Wrangler was started in the specified directory instead of the current working directory [string]
39+
-e, --env Environment to use for operations, and for selecting .env and .dev.vars files [string]
40+
-h, --help Show help [boolean]
41+
-v, --version Show version number [boolean]
42+
43+
OPTIONS
44+
--json Return output as clean JSON [boolean] [default: false]"
45+
`);
46+
});
47+
48+
it("should show a single container when given an ID (json)", async () => {
49+
setIsTTY(false);
50+
setWranglerConfig({});
51+
msw.use(
52+
http.get(
53+
"*/applications/asdf",
54+
async ({ request }) => {
55+
expect(await request.text()).toEqual("");
56+
return HttpResponse.json(MOCK_APPLICATION_SINGLE);
57+
},
58+
{ once: true }
59+
)
60+
);
61+
expect(std.err).toMatchInlineSnapshot(`""`);
62+
await runWrangler("containers info asdf");
63+
expect(std.out).toMatchInlineSnapshot(`"{}"`);
64+
});
65+
66+
it("should error when not given an ID", async () => {
67+
await expect(
68+
runWrangler("containers info")
69+
).rejects.toThrowErrorMatchingInlineSnapshot(
70+
`[Error: You must provide an ID. Use 'wrangler containers list\` to view your containers.]`
71+
);
72+
expect(std.err).toMatchInlineSnapshot(`
73+
"X [ERROR] You must provide an ID. Use 'wrangler containers list\` to view your containers.
74+
75+
"
76+
`);
77+
});
78+
});
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { http, HttpResponse } from "msw";
2+
import patchConsole from "patch-console";
3+
import { mockAccount, setWranglerConfig } from "../cloudchamber/utils";
4+
import { mockAccountId, mockApiToken } from "../helpers/mock-account-id";
5+
import { MOCK_APPLICATIONS } from "../helpers/mock-cloudchamber";
6+
import { mockConsoleMethods } from "../helpers/mock-console";
7+
import { useMockIsTTY } from "../helpers/mock-istty";
8+
import { msw } from "../helpers/msw";
9+
import { runWrangler } from "../helpers/run-wrangler";
10+
11+
describe("containers list", () => {
12+
const std = mockConsoleMethods();
13+
const { setIsTTY } = useMockIsTTY();
14+
15+
mockAccountId();
16+
mockApiToken();
17+
beforeEach(mockAccount);
18+
19+
afterEach(() => {
20+
patchConsole(() => {});
21+
msw.resetHandlers();
22+
});
23+
24+
it("should help", async () => {
25+
await runWrangler("containers list --help");
26+
expect(std.err).toMatchInlineSnapshot(`""`);
27+
expect(std.out).toMatchInlineSnapshot(`
28+
"wrangler containers list
29+
30+
list containers
31+
32+
GLOBAL FLAGS
33+
-c, --config Path to Wrangler configuration file [string]
34+
--cwd Run as if Wrangler was started in the specified directory instead of the current working directory [string]
35+
-e, --env Environment to use for operations, and for selecting .env and .dev.vars files [string]
36+
-h, --help Show help [boolean]
37+
-v, --version Show version number [boolean]
38+
39+
OPTIONS
40+
--json Return output as clean JSON [boolean] [default: false]"
41+
`);
42+
});
43+
44+
it("should list containers (json)", async () => {
45+
setIsTTY(false);
46+
setWranglerConfig({});
47+
msw.use(
48+
http.get(
49+
"*/applications",
50+
async ({ request }) => {
51+
expect(await request.text()).toEqual("");
52+
return HttpResponse.json(MOCK_APPLICATIONS);
53+
},
54+
{ once: true }
55+
)
56+
);
57+
expect(std.err).toMatchInlineSnapshot(`""`);
58+
await runWrangler("containers list");
59+
expect(std.out).toMatchInlineSnapshot(`
60+
"[
61+
{
62+
\\"id\\": \\"asdf-2\\",
63+
\\"created_at\\": \\"123\\",
64+
\\"account_id\\": \\"test-account\\",
65+
\\"name\\": \\"Test-app\\",
66+
\\"configuration\\": {
67+
\\"image\\": \\"test-registry.cfdata.org/test-app:v1\\",
68+
\\"network\\": {
69+
\\"mode\\": \\"private\\"
70+
}
71+
},
72+
\\"scheduling_policy\\": \\"regional\\",
73+
\\"instances\\": 2,
74+
\\"jobs\\": false,
75+
\\"constraints\\": {
76+
\\"region\\": \\"WNAM\\"
77+
}
78+
},
79+
{
80+
\\"id\\": \\"asdf-1\\",
81+
\\"created_at\\": \\"123\\",
82+
\\"account_id\\": \\"test-account\\",
83+
\\"name\\": \\"Test-app\\",
84+
\\"configuration\\": {
85+
\\"image\\": \\"test-registry.cfdata.org/test-app:v10\\",
86+
\\"network\\": {
87+
\\"mode\\": \\"private\\"
88+
}
89+
},
90+
\\"scheduling_policy\\": \\"regional\\",
91+
\\"instances\\": 10,
92+
\\"jobs\\": false,
93+
\\"constraints\\": {
94+
\\"region\\": \\"WNAM\\"
95+
}
96+
},
97+
{
98+
\\"id\\": \\"asdf-3\\",
99+
\\"created_at\\": \\"123\\",
100+
\\"account_id\\": \\"test-account\\",
101+
\\"name\\": \\"Test-app\\",
102+
\\"configuration\\": {
103+
\\"image\\": \\"test-registry.cfdata.org/test-app:v2\\",
104+
\\"network\\": {
105+
\\"mode\\": \\"private\\"
106+
}
107+
},
108+
\\"scheduling_policy\\": \\"regional\\",
109+
\\"instances\\": 2,
110+
\\"jobs\\": false,
111+
\\"constraints\\": {
112+
\\"region\\": \\"WNAM\\"
113+
}
114+
}
115+
]"
116+
`);
117+
});
118+
});

packages/wrangler/src/__tests__/helpers/mock-cloudchamber.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {
2+
ContainerNetworkMode,
23
DeploymentType,
34
NodeGroup,
45
PlacementStatusHealth,
6+
SchedulingPolicy,
57
} from "../../cloudchamber/client";
68
import type {
9+
Application,
710
DeploymentV2,
811
PlacementWithEvents,
912
} from "../../cloudchamber/client";
@@ -181,3 +184,54 @@ export const MOCK_PLACEMENTS: PlacementWithEvents[] = [
181184
status: { health: PlacementStatusHealth.RUNNING },
182185
},
183186
];
187+
188+
export const MOCK_APPLICATIONS: Application[] = [
189+
{
190+
id: "asdf-2",
191+
created_at: "123",
192+
account_id: "test-account",
193+
name: "Test-app",
194+
configuration: {
195+
image: "test-registry.cfdata.org/test-app:v1",
196+
network: {
197+
mode: ContainerNetworkMode.PRIVATE,
198+
},
199+
},
200+
scheduling_policy: SchedulingPolicy.REGIONAL,
201+
instances: 2,
202+
jobs: false,
203+
constraints: { region: "WNAM" },
204+
},
205+
{
206+
id: "asdf-1",
207+
created_at: "123",
208+
account_id: "test-account",
209+
name: "Test-app",
210+
configuration: {
211+
image: "test-registry.cfdata.org/test-app:v10",
212+
network: {
213+
mode: ContainerNetworkMode.PRIVATE,
214+
},
215+
},
216+
scheduling_policy: SchedulingPolicy.REGIONAL,
217+
instances: 10,
218+
jobs: false,
219+
constraints: { region: "WNAM" },
220+
},
221+
{
222+
id: "asdf-3",
223+
created_at: "123",
224+
account_id: "test-account",
225+
name: "Test-app",
226+
configuration: {
227+
image: "test-registry.cfdata.org/test-app:v2",
228+
network: {
229+
mode: ContainerNetworkMode.PRIVATE,
230+
},
231+
},
232+
scheduling_policy: SchedulingPolicy.REGIONAL,
233+
instances: 2,
234+
jobs: false,
235+
constraints: { region: "WNAM" },
236+
},
237+
];

packages/wrangler/src/cloudchamber/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function handleFailure<
109109
await fillOpenAPIConfiguration(config, args.json);
110110
await cb(args, config);
111111
} catch (err) {
112-
if (!args.json) {
112+
if (!args.json || !isNonInteractiveOrCI()) {
113113
throw err;
114114
}
115115

0 commit comments

Comments
 (0)