Skip to content

Commit

Permalink
Added location option for the Wrangler R2 bucket create command (#7092)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonesphillip authored Oct 29, 2024
1 parent 8ca4b32 commit 038fdd9
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 85 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-pens-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": minor
---

Added location hint option for the Wrangler R2 bucket create command
70 changes: 47 additions & 23 deletions packages/wrangler/src/__tests__/r2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ describe("r2", () => {
-v, --version Show version number [boolean]
OPTIONS
-J, --jurisdiction The jurisdiction where the new bucket will be created [string]
-s, --storage-class The default storage class for objects uploaded to this bucket [string]"
--location The optional location hint that determines geographic placement of the R2 bucket [string] [choices: \\"weur\\", \\"eeur\\", \\"apac\\", \\"wnam\\", \\"enam\\"]
-s, --storage-class The default storage class for objects uploaded to this bucket [string]
-J, --jurisdiction The jurisdiction where the new bucket will be created [string]"
`);
expect(std.err).toMatchInlineSnapshot(`
"X [ERROR] Not enough non-option arguments: got 0, need at least 1
Expand All @@ -221,24 +222,25 @@ describe("r2", () => {
`[Error: Unknown arguments: def, ghi]`
);
expect(std.out).toMatchInlineSnapshot(`
"
wrangler r2 bucket create <name>
"
wrangler r2 bucket create <name>
Create a new R2 bucket
Create a new R2 bucket
POSITIONALS
name The name of the new bucket [string] [required]
POSITIONALS
name The name of the new bucket [string] [required]
GLOBAL FLAGS
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
-c, --config Path to .toml configuration file [string]
-e, --env Environment to use for operations and .env files [string]
-h, --help Show help [boolean]
-v, --version Show version number [boolean]
GLOBAL FLAGS
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
-c, --config Path to .toml configuration file [string]
-e, --env Environment to use for operations and .env files [string]
-h, --help Show help [boolean]
-v, --version Show version number [boolean]
OPTIONS
-J, --jurisdiction The jurisdiction where the new bucket will be created [string]
-s, --storage-class The default storage class for objects uploaded to this bucket [string]"
OPTIONS
--location The optional location hint that determines geographic placement of the R2 bucket [string] [choices: \\"weur\\", \\"eeur\\", \\"apac\\", \\"wnam\\", \\"enam\\"]
-s, --storage-class The default storage class for objects uploaded to this bucket [string]
-J, --jurisdiction The jurisdiction where the new bucket will be created [string]"
`);
expect(std.err).toMatchInlineSnapshot(`
"X [ERROR] Unknown arguments: def, ghi
Expand All @@ -262,8 +264,8 @@ describe("r2", () => {
);
await runWrangler("r2 bucket create testBucket");
expect(std.out).toMatchInlineSnapshot(`
"Creating bucket testBucket with default storage class set to Standard.
Created bucket testBucket with default storage class set to Standard."
"Creating bucket 'testBucket'...
Created bucket 'testBucket' with default storage class of Standard."
`);
});

Expand All @@ -283,16 +285,16 @@ describe("r2", () => {
);
await runWrangler("r2 bucket create testBucket -J eu");
expect(std.out).toMatchInlineSnapshot(`
"Creating bucket testBucket (eu) with default storage class set to Standard.
Created bucket testBucket (eu) with default storage class set to Standard."
"Creating bucket 'testBucket (eu)'...
Created bucket 'testBucket (eu)' with default storage class of Standard."
`);
});

it("should create a bucket with the expected default storage class", async () => {
await runWrangler("r2 bucket create testBucket -s InfrequentAccess");
expect(std.out).toMatchInlineSnapshot(`
"Creating bucket testBucket with default storage class set to InfrequentAccess.
Created bucket testBucket with default storage class set to InfrequentAccess."
"Creating bucket 'testBucket'...
Created bucket 'testBucket' with default storage class of InfrequentAccess."
`);
});

Expand All @@ -303,7 +305,7 @@ describe("r2", () => {
`[APIError: A request to the Cloudflare API (/accounts/some-account-id/r2/buckets) failed.]`
);
expect(std.out).toMatchInlineSnapshot(`
"Creating bucket testBucket with default storage class set to Foo.
"Creating bucket 'testBucket'...
X [ERROR] A request to the Cloudflare API (/accounts/some-account-id/r2/buckets) failed.
Expand All @@ -315,6 +317,28 @@ describe("r2", () => {
"
`);
});
it("should create a bucket with the expected location hint", async () => {
msw.use(
http.post(
"*/accounts/:accountId/r2/buckets",
async ({ request, params }) => {
const { accountId } = params;
expect(accountId).toEqual("some-account-id");
expect(await request.json()).toEqual({
name: "testBucket",
locationHint: "weur",
});
return HttpResponse.json(createFetchResult({}));
},
{ once: true }
)
);
await runWrangler("r2 bucket create testBucket --location weur");
expect(std.out).toMatchInlineSnapshot(`
"Creating bucket 'testBucket'...
✅ Created bucket 'testBucket' with location hint weur and default storage class of Standard."
`);
});
});

describe("update", () => {
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/r2/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
* The maximum file size we can upload using the V4 API.
*/
export const MAX_UPLOAD_SIZE = 300 * 1024 * 1024;
export const LOCATION_CHOICES = ["weur", "eeur", "apac", "wnam", "enam"];
76 changes: 76 additions & 0 deletions packages/wrangler/src/r2/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { printWranglerBanner } from "..";
import { readConfig } from "../config";
import { UserError } from "../errors";
import { logger } from "../logger";
import * as metrics from "../metrics";
import { requireAuth } from "../user";
import { LOCATION_CHOICES } from "./constants";
import { createR2Bucket, isValidR2BucketName } from "./helpers";
import type {
CommonYargsArgv,
StrictYargsOptionsToInterface,
} from "../yargs-types";

export function Options(yargs: CommonYargsArgv) {
return yargs
.positional("name", {
describe: "The name of the new bucket",
type: "string",
demandOption: true,
})
.option("location", {
describe:
"The optional location hint that determines geographic placement of the R2 bucket",
choices: LOCATION_CHOICES,
requiresArg: true,
type: "string",
})
.option("storage-class", {
describe: "The default storage class for objects uploaded to this bucket",
alias: "s",
requiresArg: false,
type: "string",
})
.option("jurisdiction", {
describe: "The jurisdiction where the new bucket will be created",
alias: "J",
requiresArg: true,
type: "string",
});
}

type HandlerOptions = StrictYargsOptionsToInterface<typeof Options>;
export async function Handler(args: HandlerOptions) {
await printWranglerBanner();
const config = readConfig(args.config, args);
const accountId = await requireAuth(config);
const { name, location, storageClass, jurisdiction } = args;

if (!isValidR2BucketName(name)) {
throw new UserError(
`The bucket name "${name}" is invalid. Bucket names can only have alphanumeric and - characters.`
);
}

if (jurisdiction && location) {
throw new UserError(
"Provide either a jurisdiction or location hint - not both."
);
}

let fullBucketName = `${name}`;
if (jurisdiction !== undefined) {
fullBucketName += ` (${jurisdiction})`;
}

logger.log(`Creating bucket '${fullBucketName}'...`);
await createR2Bucket(accountId, name, location, jurisdiction, storageClass);
logger.log(
`✅ Created bucket '${fullBucketName}' with${
location ? ` location hint ${location} and` : ``
} default storage class of ${storageClass ? storageClass : `Standard`}.`
);
await metrics.sendMetricsEvent("create r2 bucket", {
sendMetrics: config.send_metrics,
});
}
2 changes: 2 additions & 0 deletions packages/wrangler/src/r2/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export async function listR2Buckets(
export async function createR2Bucket(
accountId: string,
bucketName: string,
location?: string,
jurisdiction?: string,
storageClass?: string
): Promise<void> {
Expand All @@ -60,6 +61,7 @@ export async function createR2Bucket(
body: JSON.stringify({
name: bucketName,
...(storageClass !== undefined && { storageClass }),
...(location !== undefined && { locationHint: location }),
}),
headers,
});
Expand Down
65 changes: 3 additions & 62 deletions packages/wrangler/src/r2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ import { logger } from "../logger";
import * as metrics from "../metrics";
import { requireAuth } from "../user";
import { MAX_UPLOAD_SIZE } from "./constants";
import * as Create from "./create";
import {
bucketAndKeyFromObjectPath,
createR2Bucket,
deleteR2Bucket,
deleteR2Object,
getR2Object,
isValidR2BucketName,
listR2Buckets,
putR2Object,
updateR2BucketStorageClass,
Expand Down Expand Up @@ -434,66 +433,8 @@ export function r2(r2Yargs: CommonYargsArgv, subHelp: SubHelp) {
r2BucketYargs.command(
"create <name>",
"Create a new R2 bucket",
(yargs) => {
return yargs
.positional("name", {
describe: "The name of the new bucket",
type: "string",
demandOption: true,
})
.option("jurisdiction", {
describe: "The jurisdiction where the new bucket will be created",
alias: "J",
requiresArg: true,
type: "string",
})
.option("storage-class", {
describe:
"The default storage class for objects uploaded to this bucket",
alias: "s",
requiresArg: false,
type: "string",
});
},
async (args) => {
await printWranglerBanner();

if (!isValidR2BucketName(args.name)) {
throw new CommandLineArgsError(
`The bucket name "${args.name}" is invalid. Bucket names can only have alphanumeric and - characters.`
);
}

const config = readConfig(args.config, args);

const accountId = await requireAuth(config);

let fullBucketName = `${args.name}`;
if (args.jurisdiction !== undefined) {
fullBucketName += ` (${args.jurisdiction})`;
}

let defaultStorageClass = ` with default storage class set to `;
if (args.storageClass !== undefined) {
defaultStorageClass += args.storageClass;
} else {
defaultStorageClass += "Standard";
}

logger.log(
`Creating bucket ${fullBucketName}${defaultStorageClass}.`
);
await createR2Bucket(
accountId,
args.name,
args.jurisdiction,
args.storageClass
);
logger.log(`Created bucket ${fullBucketName}${defaultStorageClass}.`);
await metrics.sendMetricsEvent("create r2 bucket", {
sendMetrics: config.send_metrics,
});
}
Create.Options,
Create.Handler
);

r2BucketYargs.command("update", "Update bucket state", (updateYargs) => {
Expand Down

0 comments on commit 038fdd9

Please sign in to comment.