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

add profile cp command #304

Merged
merged 4 commits into from
Oct 26, 2023
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
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 @@ -3,7 +3,7 @@ import chalk from 'chalk'

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 @@ -150,9 +65,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
Loading