Skip to content

Commit

Permalink
add profile cp command (#304)
Browse files Browse the repository at this point in the history
fixes #231
  • Loading branch information
Roy Razon authored Oct 26, 2023
1 parent a5a100b commit 65eae06
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 159 deletions.
2 changes: 1 addition & 1 deletion packages/cli-common/src/lib/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EOL } from 'os'

export const code = (c: string) => chalk.bold(c)

export const codeList = (c: string[]) => c.map(code).join(', ')
export const codeList = (c: string[] | readonly string[]) => c.map(code).join(', ')

export const command = ({ bin }: Pick<Config, 'bin'>, ...args: string[]) => code(`${bin} ${args.join(' ')}`)

Expand Down
91 changes: 3 additions & 88 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { Flags, Args, ux } from '@oclif/core'
import inquirer from 'inquirer'
import confirm from '@inquirer/confirm'
import { defaultBucketName as gsDefaultBucketName, defaultProjectId as defaultGceProjectId } from '@preevy/driver-gce'
import { defaultBucketName as s3DefaultBucketName, AWS_REGIONS, awsUtils } from '@preevy/driver-lightsail'
import { BaseCommand, text } from '@preevy/cli-common'
import { EOL } from 'os'
import { Flag } from '@oclif/core/lib/interfaces'
import { DriverName, formatDriverFlagsToArgs, machineDrivers } from '../drivers'
import { loadProfileConfig } from '../profile-command'
import ambientAwsAccountId = awsUtils.ambientAccountId
import { chooseFs, chooseFsType } from '../fs'

const chooseDriver = async () => (
await inquirer.prompt<{ driver: DriverName }>([
Expand All @@ -26,89 +24,6 @@ const chooseDriver = async () => (
])
).driver

const locationTypes = ['local', 's3', 'gs'] as const
type LocationType = typeof locationTypes[number]

const chooseLocationType = async () => (
await inquirer.prompt<{ locationType: LocationType }>([
{
type: 'list',
name: 'locationType',
message: 'Where do you want to store the profile?',
default: 'local',
choices: [
{ value: 'local', name: 'local file' },
{ value: 's3', name: 'AWS S3' },
{ value: 'gs', name: 'Google Cloud Storage' },
],
},
])
).locationType

type LocationFactory = (opts: {
profileAlias: string
driver: DriverName
driverFlags: Record<string, unknown>
}) => Promise<`${string}://${string}`>

const chooseLocation: Record<LocationType, LocationFactory> = {
local: async ({ profileAlias }: { profileAlias: string }) => `local://${profileAlias}`,
s3: async ({ profileAlias, driver, driverFlags }: {
profileAlias: string
driver: DriverName
driverFlags: Record<string, unknown>
}) => {
// eslint-disable-next-line no-use-before-define
const { region, bucket } = await inquirer.prompt<{ region: string; bucket: string }>([
{
type: 'list',
name: 'region',
message: 'S3 bucket region',
choices: AWS_REGIONS,
default: driver === 'lightsail' ? driverFlags.region as string : 'us-east-1',
},
{
type: 'input',
name: 'bucket',
message: 'Bucket name',
default: async (
answers: Record<string, unknown>
) => {
const accountId = await ambientAwsAccountId(answers.region as string)
return accountId ? s3DefaultBucketName({ profileAlias, accountId }) : undefined
},
},
])

return `s3://${bucket}?region=${region}`
},
gs: async ({ profileAlias, driver, driverFlags }: {
profileAlias: string
driver: DriverName
driverFlags: Record<string, unknown>
}) => {
// eslint-disable-next-line no-use-before-define
const { project, bucket } = await inquirer.prompt<{ project: string; bucket: string }>([
{
type: 'input',
name: 'project',
message: 'Google Cloud project',
default: driver === 'gce' ? driverFlags['project-id'] : defaultGceProjectId(),
},
{
type: 'input',
name: 'bucket',
message: 'Bucket name',
default: (
answers: Record<string, unknown>,
) => gsDefaultBucketName({ profileAlias, project: answers.project as string }),
},
])

return `gs://${bucket}?project=${project}`
},
}

export default class Init extends BaseCommand {
static description = 'Initialize or import a new profile'

Expand Down Expand Up @@ -154,9 +69,9 @@ export default class Init extends BaseCommand {
const driverAnswers = await inquirer.prompt<Record<string, unknown>>(await driverStatic.questions())
const driverFlags = await driverStatic.flagsFromAnswers(driverAnswers) as Record<string, unknown>

const locationType = await chooseLocationType()
const locationType = await chooseFsType()

const location = await chooseLocation[locationType]({ profileAlias, driver, driverFlags })
const location = await chooseFs[locationType]({ profileAlias, driver: { name: driver, flags: driverFlags } })

await this.config.runCommand('profile:create', [
'--use',
Expand Down
102 changes: 102 additions & 0 deletions packages/cli/src/commands/profile/cp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Flags, ux } from '@oclif/core'
import inquirer from 'inquirer'
import { BaseCommand, text } from '@preevy/cli-common'
import { LocalProfilesConfig } from '@preevy/core'
import { loadProfileConfig } from '../../profile-command'
import { FsType, chooseFs, chooseFsType, fsTypes, isFsType } from '../../fs'
import { machineDrivers } from '../../drivers'

const validateFsType = (fsType: string) => {
if (!isFsType(fsType)) {
throw new Error(`Unsupported storage type: ${text.code(fsType)}. Supported types: ${text.codeList(fsTypes as readonly string[])}`)
}
return fsType
}

const chooseTargetAlias = async (defaultAlias: string) => (
await inquirer.prompt<{ targetAlias: string }>([
{
type: 'input',
name: 'targetAlias',
message: 'Target profile name',
default: defaultAlias,
},
])
).targetAlias

// eslint-disable-next-line no-use-before-define
export default class CopyProfile extends BaseCommand<typeof CopyProfile> {
static description = 'Copy a profile'

static enableJsonFlag = true

static flags = {
profile: Flags.string({
description: 'Source profile name, defaults to the current profile',
required: false,
}),
// eslint-disable-next-line no-restricted-globals
'target-location': Flags.custom<{ location: string; fsType: FsType }>({
description: 'Target profile location URL',
required: false,
exclusive: ['target-storage'],
parse: async location => {
let url: URL
try {
url = new URL(location)
} catch (e) {
throw new Error(`Invalid URL: ${text.code(location)}`, { cause: e })
}
return { location, fsType: validateFsType(url.protocol.replace(':', '')) }
},
})(),
'target-storage': Flags.custom<FsType>({
description: 'Target profile storage type',
required: false,
options: [...fsTypes],
})(),
'target-name': Flags.string({
description: 'Target profile name',
required: false,
}),
use: Flags.boolean({
description: 'Mark the new profile as the current profile',
required: false,
}),
}

async source(profileConfig: LocalProfilesConfig): Promise<{ alias: string; location: string }> {
if (this.flags.profile) {
const { location } = await profileConfig.get(this.flags.profile)
return { alias: this.flags.profile, location }
}
const result = await profileConfig.current()
if (!result) {
throw new Error(`No current profile, specify the source alias with ${text.code(`--${CopyProfile.flags.profile.name}`)}`)
}
ux.info(`Copying current profile ${text.code(result.alias)} from ${text.code(result.location)}`)
return result
}

async target(source: { alias: string }): Promise<{ location: string; alias: string }> {
const { 'target-location': targetLocation, 'target-storage': targetStorage } = this.flags
const fsType = targetLocation?.fsType ?? targetStorage ?? await chooseFsType()
const alias = this.flags['target-name'] ?? await chooseTargetAlias(`${source.alias}-${fsType}`)
return { alias, location: targetLocation?.location ?? await chooseFs[fsType]({ profileAlias: alias }) }
}

async run(): Promise<unknown> {
const profileConfig = loadProfileConfig(this.config)
const source = await this.source(profileConfig)
const target = await this.target(source)
await profileConfig.copy(source, target, Object.keys(machineDrivers))

ux.info(text.success(`Profile ${text.code(source.alias)} copied to ${text.code(target.location)} as ${text.code(target.alias)}`))

if (this.flags.use) {
await profileConfig.setCurrent(target.alias)
}

return { source, target }
}
}
6 changes: 3 additions & 3 deletions packages/cli/src/commands/profile/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ export default class CreateProfile extends ProfileCommand<typeof CreateProfile>
...machineCreationflagsForAllDrivers,
driver: DriverCommand.baseFlags.driver,
use: Flags.boolean({
description: 'use the new profile',
description: 'Mark the new profile as the current profile',
required: false,
}),
}

static args = {
name: Args.string({
description: 'name of the new profile',
description: 'Name of the new profile',
required: true,
}),
url: Args.string({
description: 'url of the new profile store',
description: 'URL of the new profile',
required: true,
}),
}
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/profile/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ export default class ImportProfile extends BaseCommand<typeof ImportProfile> {

static flags = {
name: Flags.string({
description: 'name of the profile',
description: 'Name of the profile',
required: false,
}),
use: Flags.boolean({
description: 'use the imported profile',
description: 'Mark the new profile as the current profile',
required: false,
}),
}

static args = {
location: Args.string({
description: 'location of the profile',
description: 'URL of the profile',
required: true,
}),
}
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/commands/profile/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ import ProfileCommand from '../../profile-command'
export default class ListProfile extends ProfileCommand<typeof ListProfile> {
static description = 'Lists profiles'

static strict = false

static enableJsonFlag = true

async run(): Promise<unknown> {
const currentProfile = await this.profileConfig.current()
const profiles = await this.profileConfig.list()

if (this.flags.json) {
return profiles
return {
profiles: Object.fromEntries(profiles.map(({ alias, ...rest }) => [alias, rest])),
current: currentProfile?.alias,
}
}

ux.table(profiles, {
alias: {
header: 'Alias',
Expand Down
14 changes: 6 additions & 8 deletions packages/cli/src/commands/profile/use.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Args, ux } from '@oclif/core'
import ProfileCommand from '../../profile-command'
import { BaseCommand, text } from '@preevy/cli-common'
import { loadProfileConfig } from '../../profile-command'

// eslint-disable-next-line no-use-before-define
export default class UseProfile extends ProfileCommand<typeof UseProfile> {
export default class UseProfile extends BaseCommand<typeof UseProfile> {
static description = 'Set current profile'

static args = {
Expand All @@ -12,14 +13,11 @@ export default class UseProfile extends ProfileCommand<typeof UseProfile> {
}),
}

static strict = false

static enableJsonFlag = true

async run(): Promise<unknown> {
const alias = this.args.name
await this.profileConfig.setCurrent(alias)
ux.info(`Profile ${alias} is now being used`)
const profileConfig = loadProfileConfig(this.config)
await profileConfig.setCurrent(alias)
ux.info(text.success(`Profile ${text.code(alias)} is now being used`))
return undefined
}
}
Loading

0 comments on commit 65eae06

Please sign in to comment.