Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added location option for the Wrangler R2 bucket create command #7092

Merged
merged 2 commits into from
Oct 29, 2024
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/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,
jonesphillip marked this conversation as resolved.
Show resolved Hide resolved
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
Loading