Skip to content

Commit e0b98fd

Browse files
authoredDec 10, 2024··
Add r2 bucket cors command (list, set, delete) (#7382)
1 parent 8937b01 commit e0b98fd

File tree

5 files changed

+437
-0
lines changed

5 files changed

+437
-0
lines changed
 

‎.changeset/five-dryers-swim.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Added r2 bucket cors command to Wrangler including list, set, delete

‎packages/wrangler/src/__tests__/r2.test.ts

+151
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ describe("r2", () => {
9595
wrangler r2 bucket domain Manage custom domains for an R2 bucket
9696
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
9797
wrangler r2 bucket lifecycle Manage lifecycle rules for an R2 bucket
98+
wrangler r2 bucket cors Manage CORS configuration for an R2 bucket
9899
99100
GLOBAL FLAGS
100101
-c, --config Path to Wrangler configuration file [string]
@@ -132,6 +133,7 @@ describe("r2", () => {
132133
wrangler r2 bucket domain Manage custom domains for an R2 bucket
133134
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
134135
wrangler r2 bucket lifecycle Manage lifecycle rules for an R2 bucket
136+
wrangler r2 bucket cors Manage CORS configuration for an R2 bucket
135137
136138
GLOBAL FLAGS
137139
-c, --config Path to Wrangler configuration file [string]
@@ -2055,6 +2057,155 @@ describe("r2", () => {
20552057
});
20562058
});
20572059
});
2060+
describe("cors", () => {
2061+
const { setIsTTY } = useMockIsTTY();
2062+
mockAccountId();
2063+
mockApiToken();
2064+
describe("list", () => {
2065+
it("should list CORS rules when they exist", async () => {
2066+
const bucketName = "my-bucket";
2067+
const corsRules = [
2068+
{
2069+
allowed: {
2070+
origins: ["https://www.example.com"],
2071+
methods: ["GET", "PUT"],
2072+
headers: ["Content-Type", "Authorization"],
2073+
},
2074+
exposeHeaders: ["ETag", "Content-Length"],
2075+
maxAgeSeconds: 8640,
2076+
},
2077+
];
2078+
2079+
msw.use(
2080+
http.get(
2081+
"*/accounts/:accountId/r2/buckets/:bucketName/cors",
2082+
async ({ params }) => {
2083+
const { accountId, bucketName: bucketParam } = params;
2084+
expect(accountId).toEqual("some-account-id");
2085+
expect(bucketParam).toEqual(bucketName);
2086+
return HttpResponse.json(
2087+
createFetchResult({
2088+
rules: corsRules,
2089+
})
2090+
);
2091+
},
2092+
{ once: true }
2093+
)
2094+
);
2095+
await runWrangler(`r2 bucket cors list ${bucketName}`);
2096+
expect(std.out).toMatchInlineSnapshot(`
2097+
"Listing CORS rules for bucket 'my-bucket'...
2098+
allowed_origins: https://www.example.com
2099+
allowed_methods: GET, PUT
2100+
allowed_headers: Content-Type, Authorization
2101+
exposed_headers: ETag, Content-Length
2102+
max_age_seconds: 8640"
2103+
`);
2104+
});
2105+
});
2106+
describe("set", () => {
2107+
it("should set CORS configuration from a JSON file", async () => {
2108+
const bucketName = "my-bucket";
2109+
const filePath = "cors-configuration.json";
2110+
const corsRules = {
2111+
rules: [
2112+
{
2113+
allowed: {
2114+
origins: ["https://www.example.com"],
2115+
methods: ["GET", "PUT"],
2116+
headers: ["Content-Type", "Authorization"],
2117+
},
2118+
exposeHeaders: ["ETag", "Content-Length"],
2119+
maxAgeSeconds: 8640,
2120+
},
2121+
],
2122+
};
2123+
2124+
writeFileSync(filePath, JSON.stringify(corsRules));
2125+
2126+
setIsTTY(true);
2127+
mockConfirm({
2128+
text: `Are you sure you want to overwrite the existing CORS configuration for bucket '${bucketName}'?`,
2129+
result: true,
2130+
});
2131+
2132+
msw.use(
2133+
http.put(
2134+
"*/accounts/:accountId/r2/buckets/:bucketName/cors",
2135+
async ({ request, params }) => {
2136+
const { accountId, bucketName: bucketParam } = params;
2137+
expect(accountId).toEqual("some-account-id");
2138+
expect(bucketName).toEqual(bucketParam);
2139+
const requestBody = await request.json();
2140+
expect(requestBody).toEqual({
2141+
...corsRules,
2142+
});
2143+
return HttpResponse.json(createFetchResult({}));
2144+
},
2145+
{ once: true }
2146+
)
2147+
);
2148+
2149+
await runWrangler(
2150+
`r2 bucket cors set ${bucketName} --file ${filePath}`
2151+
);
2152+
expect(std.out).toMatchInlineSnapshot(`
2153+
"Setting CORS configuration (1 rules) for bucket 'my-bucket'...
2154+
✨ Set CORS configuration for bucket 'my-bucket'."
2155+
`);
2156+
});
2157+
});
2158+
describe("delete", () => {
2159+
it("should delete CORS configuration as expected", async () => {
2160+
const bucketName = "my-bucket";
2161+
const corsRules = {
2162+
rules: [
2163+
{
2164+
allowed: {
2165+
origins: ["https://www.example.com"],
2166+
methods: ["GET", "PUT"],
2167+
headers: ["Content-Type", "Authorization"],
2168+
},
2169+
exposeHeaders: ["ETag", "Content-Length"],
2170+
maxAgeSeconds: 8640,
2171+
},
2172+
],
2173+
};
2174+
setIsTTY(true);
2175+
mockConfirm({
2176+
text: `Are you sure you want to clear the existing CORS configuration for bucket '${bucketName}'?`,
2177+
result: true,
2178+
});
2179+
msw.use(
2180+
http.get(
2181+
"*/accounts/:accountId/r2/buckets/:bucketName/cors",
2182+
async ({ params }) => {
2183+
const { accountId, bucketName: bucketParam } = params;
2184+
expect(accountId).toEqual("some-account-id");
2185+
expect(bucketParam).toEqual(bucketName);
2186+
return HttpResponse.json(createFetchResult(corsRules));
2187+
},
2188+
{ once: true }
2189+
),
2190+
http.delete(
2191+
"*/accounts/:accountId/r2/buckets/:bucketName/cors",
2192+
async ({ params }) => {
2193+
const { accountId, bucketName: bucketParam } = params;
2194+
expect(accountId).toEqual("some-account-id");
2195+
expect(bucketName).toEqual(bucketParam);
2196+
return HttpResponse.json(createFetchResult({}));
2197+
},
2198+
{ once: true }
2199+
)
2200+
);
2201+
await runWrangler(`r2 bucket cors delete ${bucketName}`);
2202+
expect(std.out).toMatchInlineSnapshot(`
2203+
"Deleting the CORS configuration for bucket 'my-bucket'...
2204+
CORS configuration deleted for bucket 'my-bucket'."
2205+
`);
2206+
});
2207+
});
2208+
});
20582209
});
20592210

20602211
describe("r2 object", () => {

‎packages/wrangler/src/index.ts

+22
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ import {
9999
r2BucketUpdateNamespace,
100100
r2BucketUpdateStorageClassCommand,
101101
} from "./r2/bucket";
102+
import {
103+
r2BucketCORSDeleteCommand,
104+
r2BucketCORSListCommand,
105+
r2BucketCORSNamespace,
106+
r2BucketCORSSetCommand,
107+
} from "./r2/cors";
102108
import {
103109
r2BucketDomainAddCommand,
104110
r2BucketDomainListCommand,
@@ -829,6 +835,22 @@ export function createCLIParser(argv: string[]) {
829835
command: "wrangler r2 bucket lifecycle set",
830836
definition: r2BucketLifecycleSetCommand,
831837
},
838+
{
839+
command: "wrangler r2 bucket cors",
840+
definition: r2BucketCORSNamespace,
841+
},
842+
{
843+
command: "wrangler r2 bucket cors delete",
844+
definition: r2BucketCORSDeleteCommand,
845+
},
846+
{
847+
command: "wrangler r2 bucket cors list",
848+
definition: r2BucketCORSListCommand,
849+
},
850+
{
851+
command: "wrangler r2 bucket cors set",
852+
definition: r2BucketCORSSetCommand,
853+
},
832854
]);
833855
registry.registerNamespace("r2");
834856

‎packages/wrangler/src/r2/cors.ts

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import path from "node:path";
2+
import { createCommand, createNamespace } from "../core/create-command";
3+
import { confirm } from "../dialogs";
4+
import { UserError } from "../errors";
5+
import { logger } from "../logger";
6+
import { parseJSON, readFileSync } from "../parse";
7+
import { requireAuth } from "../user";
8+
import formatLabelledValues from "../utils/render-labelled-values";
9+
import {
10+
deleteCORSPolicy,
11+
getCORSPolicy,
12+
putCORSPolicy,
13+
tableFromCORSPolicyResponse,
14+
} from "./helpers";
15+
import type { CORSRule } from "./helpers";
16+
17+
export const r2BucketCORSNamespace = createNamespace({
18+
metadata: {
19+
description: "Manage CORS configuration for an R2 bucket",
20+
status: "stable",
21+
owner: "Product: R2",
22+
},
23+
});
24+
25+
export const r2BucketCORSListCommand = createCommand({
26+
metadata: {
27+
description: "List the CORS rules for an R2 bucket",
28+
status: "stable",
29+
owner: "Product: R2",
30+
},
31+
positionalArgs: ["bucket"],
32+
args: {
33+
bucket: {
34+
describe: "The name of the R2 bucket to list the CORS rules for",
35+
type: "string",
36+
demandOption: true,
37+
},
38+
jurisdiction: {
39+
describe: "The jurisdiction where the bucket exists",
40+
alias: "J",
41+
requiresArg: true,
42+
type: "string",
43+
},
44+
},
45+
async handler({ bucket, jurisdiction }, { config }) {
46+
const accountId = await requireAuth(config);
47+
48+
logger.log(`Listing CORS rules for bucket '${bucket}'...`);
49+
const corsPolicy = await getCORSPolicy(accountId, bucket, jurisdiction);
50+
51+
if (corsPolicy.length === 0) {
52+
logger.log(
53+
`There is no CORS configuration defined for bucket '${bucket}'.`
54+
);
55+
} else {
56+
const tableOutput = tableFromCORSPolicyResponse(corsPolicy);
57+
logger.log(tableOutput.map((x) => formatLabelledValues(x)).join("\n\n"));
58+
}
59+
},
60+
});
61+
62+
export const r2BucketCORSSetCommand = createCommand({
63+
metadata: {
64+
description: "Set the CORS configuration for an R2 bucket from a JSON file",
65+
status: "stable",
66+
owner: "Product: R2",
67+
},
68+
positionalArgs: ["bucket"],
69+
args: {
70+
bucket: {
71+
describe: "The name of the R2 bucket to set the CORS configuration for",
72+
type: "string",
73+
demandOption: true,
74+
},
75+
file: {
76+
describe: "Path to the JSON file containing the CORS configuration",
77+
type: "string",
78+
demandOption: true,
79+
requiresArg: true,
80+
},
81+
jurisdiction: {
82+
describe: "The jurisdiction where the bucket exists",
83+
alias: "J",
84+
requiresArg: true,
85+
type: "string",
86+
},
87+
force: {
88+
describe: "Skip confirmation",
89+
type: "boolean",
90+
alias: "y",
91+
default: false,
92+
},
93+
},
94+
async handler({ bucket, file, jurisdiction, force }, { config }) {
95+
const accountId = await requireAuth(config);
96+
97+
const jsonFilePath = path.resolve(file);
98+
99+
const corsConfig = parseJSON<{ rules: CORSRule[] }>(
100+
readFileSync(jsonFilePath),
101+
jsonFilePath
102+
);
103+
104+
if (!corsConfig.rules || !Array.isArray(corsConfig.rules)) {
105+
throw new UserError(
106+
`The CORS configuration file must contain a 'rules' array as expected by the request body of the CORS API: ` +
107+
`https://developers.cloudflare.com/api/operations/r2-put-bucket-cors-policy`
108+
);
109+
}
110+
111+
if (!force) {
112+
const confirmedRemoval = await confirm(
113+
`Are you sure you want to overwrite the existing CORS configuration for bucket '${bucket}'?`
114+
);
115+
if (!confirmedRemoval) {
116+
logger.log("Set cancelled.");
117+
return;
118+
}
119+
}
120+
121+
logger.log(
122+
`Setting CORS configuration (${corsConfig.rules.length} rules) for bucket '${bucket}'...`
123+
);
124+
await putCORSPolicy(accountId, bucket, corsConfig.rules, jurisdiction);
125+
logger.log(`✨ Set CORS configuration for bucket '${bucket}'.`);
126+
},
127+
});
128+
129+
export const r2BucketCORSDeleteCommand = createCommand({
130+
metadata: {
131+
description: "Clear the CORS configuration for an R2 bucket",
132+
status: "stable",
133+
owner: "Product: R2",
134+
},
135+
positionalArgs: ["bucket"],
136+
args: {
137+
bucket: {
138+
describe:
139+
"The name of the R2 bucket to delete the CORS configuration for",
140+
type: "string",
141+
demandOption: true,
142+
},
143+
jurisdiction: {
144+
describe: "The jurisdiction where the bucket exists",
145+
alias: "J",
146+
requiresArg: true,
147+
type: "string",
148+
},
149+
force: {
150+
describe: "Skip confirmation",
151+
type: "boolean",
152+
alias: "y",
153+
default: false,
154+
},
155+
},
156+
async handler({ bucket, jurisdiction, force }, { config }) {
157+
const accountId = await requireAuth(config);
158+
159+
if (!force) {
160+
const confirmedRemoval = await confirm(
161+
`Are you sure you want to clear the existing CORS configuration for bucket '${bucket}'?`
162+
);
163+
if (!confirmedRemoval) {
164+
logger.log("Set cancelled.");
165+
return;
166+
}
167+
}
168+
169+
logger.log(`Deleting the CORS configuration for bucket '${bucket}'...`);
170+
await deleteCORSPolicy(accountId, bucket, jurisdiction);
171+
logger.log(`CORS configuration deleted for bucket '${bucket}'.`);
172+
},
173+
});

0 commit comments

Comments
 (0)
Please sign in to comment.