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
64 changes: 6 additions & 58 deletions src/lib/buckets/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import enquirer from 'enquirer';
import { getArgumentSpec, buildPromptChoices } from '../../utils/specs.js';
import { StorageClass, createBucket } from '@tigrisdata/storage';
import { getStorageConfig } from '../../auth/s3-client';
import {
parseLocations,
multiRegionChoices,
singleRegionChoices,
} from '../../utils/locations.js';
import { parseLocations, promptLocations } from '../../utils/locations.js';
import type { BucketLocations } from '@tigrisdata/storage';
import {
printStart,
Expand Down Expand Up @@ -116,59 +112,11 @@ export default async function create(options: Record<string, unknown>) {
defaultTier = responses.defaultTier;
enableSnapshots = responses.enableSnapshots;

// Location selection: type first, then region(s) based on type
const { locationType } = await prompt<{ locationType: string }>({
type: 'select',
name: 'locationType',
message: 'Location type:',
choices: [
{ name: 'global', message: 'Global' },
{ name: 'multi', message: 'Multi-region (USA or Europe)' },
{ name: 'dual', message: 'Dual region' },
{ name: 'single', message: 'Single region' },
],
});

if (locationType === 'global') {
parsedLocations = { type: 'global' };
} else if (locationType === 'multi') {
const { region } = await prompt<{ region: string }>({
type: 'select',
name: 'region',
message: 'Multi-region:',
choices: multiRegionChoices.map((c) => ({
name: c.value,
message: c.name,
})),
});
parsedLocations = parseLocations(region);
} else if (locationType === 'single') {
const { region } = await prompt<{ region: string }>({
type: 'select',
name: 'region',
message: 'Region:',
choices: singleRegionChoices.map((c) => ({
name: c.value,
message: c.name,
})),
});
parsedLocations = parseLocations(region);
} else {
const { regions } = await prompt<{ regions: string[] }>({
type: 'multiselect',
name: 'regions',
message:
'Press space key to select regions (multiple supported) and enter to confirm:',
choices: singleRegionChoices.map((c) => ({
name: c.value,
message: c.name,
})),
} as Parameters<typeof prompt>[0]);
if (regions.length < 2) {
printFailure(context, 'Dual region requires at least two regions');
process.exit(1);
}
parsedLocations = parseLocations(regions);
try {
parsedLocations = await promptLocations();
} catch (err) {
printFailure(context, (err as Error).message);
process.exit(1);
}
}

Expand Down
59 changes: 59 additions & 0 deletions src/lib/buckets/set-locations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getOption } from '../../utils/options.js';
import { getStorageConfig } from '../../auth/s3-client.js';
import { getSelectedOrganization } from '../../auth/storage.js';
import { updateBucket } from '@tigrisdata/storage';
import type { BucketLocations } from '@tigrisdata/storage';
import { parseLocations, promptLocations } from '../../utils/locations.js';
import {
printStart,
printSuccess,
printFailure,
msg,
} from '../../utils/messages.js';

const context = msg('buckets', 'set-locations');

export default async function setLocations(options: Record<string, unknown>) {
printStart(context);

const name = getOption<string>(options, ['name']);
const locations = getOption<string | string[]>(options, ['locations']);

if (!name) {
printFailure(context, 'Bucket name is required');
process.exit(1);
}

let parsedLocations: BucketLocations;
if (locations !== undefined) {
parsedLocations = parseLocations(locations);
} else {
try {
parsedLocations = await promptLocations();
} catch (err) {
printFailure(context, (err as Error).message);
process.exit(1);
}
}

const config = await getStorageConfig();
const selectedOrg = getSelectedOrganization();
const finalConfig = {
...config,
...(selectedOrg && !config.organizationId
? { organizationId: selectedOrg }
: {}),
};

const { error } = await updateBucket(name, {
locations: parsedLocations,
config: finalConfig,
});

if (error) {
printFailure(context, error.message);
process.exit(1);
}

printSuccess(context, { name });
}
23 changes: 23 additions & 0 deletions src/specs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,29 @@ commands:
- name: disable
description: Disable TTL on the bucket
type: flag
# set-locations
- name: set-locations
description: Set the data locations for a bucket
examples:
- "tigris buckets set-locations my-bucket --locations iad"
- "tigris buckets set-locations my-bucket --locations iad,fra"
- "tigris buckets set-locations my-bucket --locations global"
messages:
onStart: 'Updating bucket locations...'
onSuccess: 'Locations updated for bucket {{name}}'
onFailure: 'Failed to update bucket locations'
arguments:
- name: name
description: Name of the bucket
type: positional
required: true
examples:
- my-bucket
- name: locations
description: Bucket location
alias: l
options: *location_options
multiple: true
# set-migration
- name: set-migration
description: Configure data migration from an external S3-compatible source bucket. Tigris will pull objects on demand from the source
Expand Down
113 changes: 100 additions & 13 deletions src/utils/locations.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,117 @@
import type { BucketLocations } from '@tigrisdata/storage';
import enquirer from 'enquirer';

const { prompt } = enquirer;

type Multi = Extract<BucketLocations, { type: 'multi' }>;
type Single = Extract<BucketLocations, { type: 'single' }>;
type Dual = Extract<BucketLocations, { type: 'dual' }>;

const multiRegions: string[] = ['usa', 'eur'];

export const multiRegionChoices = [
const multiRegionChoices = [
{ name: 'USA', value: 'usa' },
{ name: 'Europe', value: 'eur' },
];

export const singleRegionChoices = [
{ name: 'Amsterdam, Netherlands', value: 'ams' },
{ name: 'Frankfurt, Germany', value: 'fra' },
{ name: 'Sao Paulo, Brazil', value: 'gru' },
{ name: 'Ashburn, Virginia (US)', value: 'iad' },
{ name: 'Johannesburg, South Africa', value: 'jnb' },
{ name: 'London, United Kingdom', value: 'lhr' },
{ name: 'Tokyo, Japan', value: 'nrt' },
{ name: 'Chicago, Illinois (US)', value: 'ord' },
{ name: 'Singapore, Singapore', value: 'sin' },
{ name: 'San Jose, California (US)', value: 'sjc' },
{ name: 'Sydney, Australia', value: 'syd' },
const singleRegionChoices = [
{ name: 'Amsterdam, Netherlands (AMS)', value: 'ams' },
{ name: 'Frankfurt, Germany (FRA)', value: 'fra' },
{ name: 'Sao Paulo, Brazil (GRU)', value: 'gru' },
{ name: 'Ashburn, Virginia (IAD)', value: 'iad' },
{ name: 'Johannesburg, South Africa (JNB)', value: 'jnb' },
{ name: 'London, United Kingdom (LHR)', value: 'lhr' },
{ name: 'Tokyo, Japan (NRT)', value: 'nrt' },
{ name: 'Chicago, Illinois (ORD)', value: 'ord' },
{ name: 'Singapore, Singapore (SIN)', value: 'sin' },
{ name: 'San Jose, California (SJC)', value: 'sjc' },
{ name: 'Sydney, Australia (SYD)', value: 'syd' },
];

async function promptRegion(
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
locationType: string
): Promise<BucketLocations | null> {
try {
if (locationType === 'multi') {
const { region } = await prompt<{ region: string }>({
type: 'select',
name: 'region',
message: 'Multi-region:',
choices: multiRegionChoices.map((c) => ({
name: c.value,
message: c.name,
})),
});
return parseLocations(region);
}

if (locationType === 'single') {
const { region } = await prompt<{ region: string }>({
type: 'select',
name: 'region',
message: 'Region:',
choices: singleRegionChoices.map((c) => ({
name: c.value,
message: c.name,
})),
});
return parseLocations(region);
}

// dual
const { regions } = await prompt<{ regions: string[] }>({
type: 'multiselect',
name: 'regions',
message:
'Press space key to select regions (multiple supported) and enter to confirm:',
choices: singleRegionChoices.map((c) => ({
name: c.value,
message: c.name,
})),
} as Parameters<typeof prompt>[0]);

if (regions.length < 2) {
throw new Error('Dual region requires at least two regions');
}

return parseLocations(regions);
} catch (err) {
// User pressed Escape — return null to go back
if (err === '') {
return null;
}

throw err;
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}

export async function promptLocations(): Promise<BucketLocations> {
let locationType: string;
try {
({ locationType } = await prompt<{ locationType: string }>({
type: 'select',
name: 'locationType',
message: 'Location type:',
choices: [
{ name: 'global', message: 'Global' },
{ name: 'multi', message: 'Multi-region (USA or Europe)' },
{ name: 'dual', message: 'Dual region' },
{ name: 'single', message: 'Single region' },
],
}));
} catch {
throw new Error('Location selection cancelled');
}

if (locationType === 'global') {
return { type: 'global' };
}

const result = await promptRegion(locationType);
// Escape was pressed in sub-menu — go back to location type
return result ?? promptLocations();
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

export function parseLocations(input: string | string[]): BucketLocations {
const values = (Array.isArray(input) ? input : [input])
.flatMap((v) => v.split(','))
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"moduleDetection": "force"
},
"include": ["src"],
"exclude": ["src/cli-binary.ts", "src/specs-embedded.ts", "src/command-registry.ts"]
"exclude": ["src/cli-binary.ts", "src/command-registry.ts"]
}
Loading