Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/five-dryers-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": minor
---

Added r2 bucket cors command to Wrangler including list, set, delete
151 changes: 151 additions & 0 deletions packages/wrangler/src/__tests__/r2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe("r2", () => {
wrangler r2 bucket domain Manage custom domains for an R2 bucket
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
wrangler r2 bucket lifecycle Manage lifecycle rules for an R2 bucket
wrangler r2 bucket cors Manage CORS configuration for an R2 bucket

GLOBAL FLAGS
-c, --config Path to Wrangler configuration file [string]
Expand Down Expand Up @@ -132,6 +133,7 @@ describe("r2", () => {
wrangler r2 bucket domain Manage custom domains for an R2 bucket
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
wrangler r2 bucket lifecycle Manage lifecycle rules for an R2 bucket
wrangler r2 bucket cors Manage CORS configuration for an R2 bucket

GLOBAL FLAGS
-c, --config Path to Wrangler configuration file [string]
Expand Down Expand Up @@ -2055,6 +2057,155 @@ describe("r2", () => {
});
});
});
describe("cors", () => {
const { setIsTTY } = useMockIsTTY();
mockAccountId();
mockApiToken();
describe("list", () => {
it("should list CORS rules when they exist", async () => {
const bucketName = "my-bucket";
const corsRules = [
{
allowed: {
origins: ["https://www.example.com"],
methods: ["GET", "PUT"],
headers: ["Content-Type", "Authorization"],
},
exposeHeaders: ["ETag", "Content-Length"],
maxAgeSeconds: 8640,
},
];

msw.use(
http.get(
"*/accounts/:accountId/r2/buckets/:bucketName/cors",
async ({ params }) => {
const { accountId, bucketName: bucketParam } = params;
expect(accountId).toEqual("some-account-id");
expect(bucketParam).toEqual(bucketName);
return HttpResponse.json(
createFetchResult({
rules: corsRules,
})
);
},
{ once: true }
)
);
await runWrangler(`r2 bucket cors list ${bucketName}`);
expect(std.out).toMatchInlineSnapshot(`
"Listing CORS rules for bucket 'my-bucket'...
allowed_origins: https://www.example.com
allowed_methods: GET, PUT
allowed_headers: Content-Type, Authorization
exposed_headers: ETag, Content-Length
max_age_seconds: 8640"
`);
});
});
describe("set", () => {
it("should set CORS configuration from a JSON file", async () => {
const bucketName = "my-bucket";
const filePath = "cors-configuration.json";
const corsRules = {
rules: [
{
allowed: {
origins: ["https://www.example.com"],
methods: ["GET", "PUT"],
headers: ["Content-Type", "Authorization"],
},
exposeHeaders: ["ETag", "Content-Length"],
maxAgeSeconds: 8640,
},
],
};

writeFileSync(filePath, JSON.stringify(corsRules));

setIsTTY(true);
mockConfirm({
text: `Are you sure you want to overwrite the existing CORS configuration for bucket '${bucketName}'?`,
result: true,
});

msw.use(
http.put(
"*/accounts/:accountId/r2/buckets/:bucketName/cors",
async ({ request, params }) => {
const { accountId, bucketName: bucketParam } = params;
expect(accountId).toEqual("some-account-id");
expect(bucketName).toEqual(bucketParam);
const requestBody = await request.json();
expect(requestBody).toEqual({
...corsRules,
});
return HttpResponse.json(createFetchResult({}));
},
{ once: true }
)
);

await runWrangler(
`r2 bucket cors set ${bucketName} --file ${filePath}`
);
expect(std.out).toMatchInlineSnapshot(`
"Setting CORS configuration (1 rules) for bucket 'my-bucket'...
✨ Set CORS configuration for bucket 'my-bucket'."
`);
});
});
describe("delete", () => {
it("should delete CORS configuration as expected", async () => {
const bucketName = "my-bucket";
const corsRules = {
rules: [
{
allowed: {
origins: ["https://www.example.com"],
methods: ["GET", "PUT"],
headers: ["Content-Type", "Authorization"],
},
exposeHeaders: ["ETag", "Content-Length"],
maxAgeSeconds: 8640,
},
],
};
setIsTTY(true);
mockConfirm({
text: `Are you sure you want to clear the existing CORS configuration for bucket '${bucketName}'?`,
result: true,
});
msw.use(
http.get(
"*/accounts/:accountId/r2/buckets/:bucketName/cors",
async ({ params }) => {
const { accountId, bucketName: bucketParam } = params;
expect(accountId).toEqual("some-account-id");
expect(bucketParam).toEqual(bucketName);
return HttpResponse.json(createFetchResult(corsRules));
},
{ once: true }
),
http.delete(
"*/accounts/:accountId/r2/buckets/:bucketName/cors",
async ({ params }) => {
const { accountId, bucketName: bucketParam } = params;
expect(accountId).toEqual("some-account-id");
expect(bucketName).toEqual(bucketParam);
return HttpResponse.json(createFetchResult({}));
},
{ once: true }
)
);
await runWrangler(`r2 bucket cors delete ${bucketName}`);
expect(std.out).toMatchInlineSnapshot(`
"Deleting the CORS configuration for bucket 'my-bucket'...
CORS configuration deleted for bucket 'my-bucket'."
`);
});
});
});
});

describe("r2 object", () => {
Expand Down
22 changes: 22 additions & 0 deletions packages/wrangler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ import {
r2BucketUpdateNamespace,
r2BucketUpdateStorageClassCommand,
} from "./r2/bucket";
import {
r2BucketCORSDeleteCommand,
r2BucketCORSListCommand,
r2BucketCORSNamespace,
r2BucketCORSSetCommand,
} from "./r2/cors";
import {
r2BucketDomainAddCommand,
r2BucketDomainListCommand,
Expand Down Expand Up @@ -829,6 +835,22 @@ export function createCLIParser(argv: string[]) {
command: "wrangler r2 bucket lifecycle set",
definition: r2BucketLifecycleSetCommand,
},
{
command: "wrangler r2 bucket cors",
definition: r2BucketCORSNamespace,
},
{
command: "wrangler r2 bucket cors delete",
definition: r2BucketCORSDeleteCommand,
},
{
command: "wrangler r2 bucket cors list",
definition: r2BucketCORSListCommand,
},
{
command: "wrangler r2 bucket cors set",
definition: r2BucketCORSSetCommand,
},
]);
registry.registerNamespace("r2");

Expand Down
173 changes: 173 additions & 0 deletions packages/wrangler/src/r2/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import path from "node:path";
import { createCommand, createNamespace } from "../core/create-command";
import { confirm } from "../dialogs";
import { UserError } from "../errors";
import { logger } from "../logger";
import { parseJSON, readFileSync } from "../parse";
import { requireAuth } from "../user";
import formatLabelledValues from "../utils/render-labelled-values";
import {
deleteCORSPolicy,
getCORSPolicy,
putCORSPolicy,
tableFromCORSPolicyResponse,
} from "./helpers";
import type { CORSRule } from "./helpers";

export const r2BucketCORSNamespace = createNamespace({
metadata: {
description: "Manage CORS configuration for an R2 bucket",
status: "stable",
owner: "Product: R2",
},
});

export const r2BucketCORSListCommand = createCommand({
metadata: {
description: "List the CORS rules for an R2 bucket",
status: "stable",
owner: "Product: R2",
},
positionalArgs: ["bucket"],
args: {
bucket: {
describe: "The name of the R2 bucket to list the CORS rules for",
type: "string",
demandOption: true,
},
jurisdiction: {
describe: "The jurisdiction where the bucket exists",
alias: "J",
requiresArg: true,
type: "string",
},
},
async handler({ bucket, jurisdiction }, { config }) {
const accountId = await requireAuth(config);

logger.log(`Listing CORS rules for bucket '${bucket}'...`);
const corsPolicy = await getCORSPolicy(accountId, bucket, jurisdiction);

if (corsPolicy.length === 0) {
logger.log(
`There is no CORS configuration defined for bucket '${bucket}'.`
);
} else {
const tableOutput = tableFromCORSPolicyResponse(corsPolicy);
logger.log(tableOutput.map((x) => formatLabelledValues(x)).join("\n\n"));
}
},
});

export const r2BucketCORSSetCommand = createCommand({
metadata: {
description: "Set the CORS configuration for an R2 bucket from a JSON file",
status: "stable",
owner: "Product: R2",
},
positionalArgs: ["bucket"],
args: {
bucket: {
describe: "The name of the R2 bucket to set the CORS configuration for",
type: "string",
demandOption: true,
},
file: {
describe: "Path to the JSON file containing the CORS configuration",
type: "string",
demandOption: true,
requiresArg: true,
},
jurisdiction: {
describe: "The jurisdiction where the bucket exists",
alias: "J",
requiresArg: true,
type: "string",
},
force: {
describe: "Skip confirmation",
type: "boolean",
alias: "y",
default: false,
},
},
async handler({ bucket, file, jurisdiction, force }, { config }) {
const accountId = await requireAuth(config);

const jsonFilePath = path.resolve(file);

const corsConfig = parseJSON<{ rules: CORSRule[] }>(
readFileSync(jsonFilePath),
jsonFilePath
);

if (!corsConfig.rules || !Array.isArray(corsConfig.rules)) {
throw new UserError(
`The CORS configuration file must contain a 'rules' array as expected by the request body of the CORS API: ` +
`https://developers.cloudflare.com/api/operations/r2-put-bucket-cors-policy`
);
}

if (!force) {
const confirmedRemoval = await confirm(
`Are you sure you want to overwrite the existing CORS configuration for bucket '${bucket}'?`
);
if (!confirmedRemoval) {
logger.log("Set cancelled.");
return;
}
}

logger.log(
`Setting CORS configuration (${corsConfig.rules.length} rules) for bucket '${bucket}'...`
);
await putCORSPolicy(accountId, bucket, corsConfig.rules, jurisdiction);
logger.log(`✨ Set CORS configuration for bucket '${bucket}'.`);
},
});

export const r2BucketCORSDeleteCommand = createCommand({
metadata: {
description: "Clear the CORS configuration for an R2 bucket",
status: "stable",
owner: "Product: R2",
},
positionalArgs: ["bucket"],
args: {
bucket: {
describe:
"The name of the R2 bucket to delete the CORS configuration for",
type: "string",
demandOption: true,
},
jurisdiction: {
describe: "The jurisdiction where the bucket exists",
alias: "J",
requiresArg: true,
type: "string",
},
force: {
describe: "Skip confirmation",
type: "boolean",
alias: "y",
default: false,
},
},
async handler({ bucket, jurisdiction, force }, { config }) {
const accountId = await requireAuth(config);

if (!force) {
const confirmedRemoval = await confirm(
`Are you sure you want to clear the existing CORS configuration for bucket '${bucket}'?`
);
if (!confirmedRemoval) {
logger.log("Set cancelled.");
return;
}
}

logger.log(`Deleting the CORS configuration for bucket '${bucket}'...`);
await deleteCORSPolicy(accountId, bucket, jurisdiction);
logger.log(`CORS configuration deleted for bucket '${bucket}'.`);
},
});
Loading
Loading