Skip to content

Commit

Permalink
Merge pull request #52 from livecycle/refactor-profiles
Browse files Browse the repository at this point in the history
- telemetry: add profile name, type
- change default s3 bucket name from `preview` to `preevy`
- refactor profiles a bit: 
  - remove redundant code 
  - rearrange files 
  - rename to more descriptive names
  - snapshotStore is the backend, store is the friendly frontend
  - introduced \`FileBackedSnapshotter\` as an abstraction for tar
  - added tar dirty check
- `profile import` - added smarter default alias
- `profile rm` - remove `current` pointer when removing the current profile
  • Loading branch information
Roy Razon authored Apr 16, 2023
2 parents e456417 + cd6aeb9 commit de0b7b8
Show file tree
Hide file tree
Showing 21 changed files with 349 additions and 344 deletions.
22 changes: 5 additions & 17 deletions packages/cli/src/base-command.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Command, Flags, Interfaces, settings as oclifSettings } from '@oclif/core'
import path from 'path'
import { ProfileConfig, profileConfig } from './lib/profile'
import { createStore } from './lib/store'
import { realFs } from './lib/store/fs'
import { s3fs } from './lib/store/s3'
import { tarSnapshotter } from './lib/store/tar'
import { LocalProfilesConfig, localProfilesConfig } from './lib/profile'
import { commandLogger, Logger, LogLevel, logLevels } from './log'

// eslint-disable-next-line no-use-before-define
Expand Down Expand Up @@ -67,20 +63,12 @@ abstract class BaseCommand<T extends typeof Command=typeof Command> extends Comm
this.stdErrLogger.info(message, ...args)
}

#profileConfig: ProfileConfig | undefined
get profileConfig(): ProfileConfig {
#profileConfig: LocalProfilesConfig | undefined
get profileConfig(): LocalProfilesConfig {
if (!this.#profileConfig) {
const root = path.join(this.config.dataDir, 'v2')
this.debug('init profile config at:', root)
this.#profileConfig = profileConfig(root, async location => {
const { protocol, hostname } = new URL(location)
if (protocol === 'local:') {
return createStore(realFs(path.join(root, 'profiles', hostname)), tarSnapshotter())
} if (protocol === 's3:') {
return createStore(await s3fs(location), tarSnapshotter())
}
throw new Error(`Unsupported blob prefix: ${protocol}`)
})
this.logger.debug('init profile config at:', root)
this.#profileConfig = localProfilesConfig(root)
}

return this.#profileConfig
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import inquirer from 'inquirer'
import { pickBy } from 'lodash'
import BaseCommand from '../../base-command'
import { DriverFlagName, DriverName, machineDrivers } from '../../lib/machine'
import { suggestDefaultUrl } from '../../lib/store/s3'
import { suggestDefaultUrl } from '../../lib/store/fs/s3'

export default class Init extends BaseCommand {
static description = 'Initialize or import a new profile'
Expand Down
19 changes: 15 additions & 4 deletions packages/cli/src/commands/profile/import.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { Args, Flags, ux } from '@oclif/core'
import { find, range, map } from 'iter-tools-es'
import BaseCommand from '../../base-command'
import { onProfileChange } from '../../profile-command'
import { LocalProfilesConfig } from '../../lib/profile'

const DEFAULT_ALIAS_PREFIX = 'default'

const defaultAlias = async (profileConfig: LocalProfilesConfig) => {
const profiles = new Set((await profileConfig.list()).map(l => l.alias))
return find(
(alias: string) => !profiles.has(alias),
map(suffix => (suffix ? `${DEFAULT_ALIAS_PREFIX}${suffix + 1}` : DEFAULT_ALIAS_PREFIX), range()),
) as string
}

// eslint-disable-next-line no-use-before-define
export default class ImportProfile extends BaseCommand<typeof ImportProfile> {
Expand All @@ -10,7 +22,6 @@ export default class ImportProfile extends BaseCommand<typeof ImportProfile> {
name: Flags.string({
description: 'name of the profile',
required: false,
default: 'default',
}),
}

Expand All @@ -26,10 +37,10 @@ export default class ImportProfile extends BaseCommand<typeof ImportProfile> {
static enableJsonFlag = true

async run(): Promise<void> {
const alias = this.flags.name
const alias = this.flags.name ?? await defaultAlias(this.profileConfig)

const { info } = await this.profileConfig.importExisting(alias, this.args.location)
onProfileChange(info)
ux.info(`Profile ${info.id} imported successfully`)
onProfileChange(info, alias, this.args.location)
ux.info(`Profile ${info.id} imported successfully as ${alias}`)
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/lib/machine/driver/driver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Profile } from '../../profile/types'
import { Profile } from '../../profile/profile'
import { SSHKeyConfig } from '../../ssh/keypair'

export type Machine = {
Expand Down
129 changes: 129 additions & 0 deletions packages/cli/src/lib/profile/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import path from 'path'
import { localFs } from '../store/fs/local'
import { fsFromUrl, store, tarSnapshot } from '../store'
import { ProfileStore, profileStore } from './store'
import { Profile } from './profile'

type ProfileListing = {
alias: string
id: string
location: string
}

type ProfileList = {
current: string | undefined
profiles: Record<string, Omit<ProfileListing, 'alias'>>
}

const profileListFileName = 'profileList.json'

export const localProfilesConfig = (localDir: string) => {
const localStore = localFs(localDir)
const tarSnapshotFromUrl = async (
url: string,
) => store(async dir => tarSnapshot(await fsFromUrl(url, path.join(localDir, 'profiles')), dir))

async function readProfileList(): Promise<ProfileList> {
const data = await localStore.read(profileListFileName)
if (!data) {
const initData = { current: undefined, profiles: {} }
await localStore.write(profileListFileName, JSON.stringify(initData))
return initData
}
return JSON.parse(data.toString())
}

return {
async current() {
const { profiles, current: currentAlias } = await readProfileList()
const current = currentAlias && profiles[currentAlias]
if (!current) {
return undefined
}
return {
alias: currentAlias,
id: current.id,
location: current.location,
}
},
async setCurrent(alias: string) {
const list = await readProfileList()
if (!list.profiles[alias]) {
throw new Error(`Profile ${alias} doesn't exists`)
}
list.current = alias
await localStore.write(profileListFileName, JSON.stringify(list))
},
async list(): Promise<ProfileListing[]> {
return Object.entries((await readProfileList()).profiles).map(([alias, profile]) => ({ alias, ...profile }))
},
async get(alias: string) {
const { profiles } = await readProfileList()
const locationUrl = profiles[alias]?.location
if (!locationUrl) {
throw new Error(`Profile ${alias} not found`)
}
const tarSnapshotStore = await tarSnapshotFromUrl(locationUrl)
const profileInfo = await profileStore(tarSnapshotStore).info()
return {
info: profileInfo,
store: tarSnapshotStore,
}
},
async delete(alias: string) {
const list = await readProfileList()
if (!list.profiles[alias]) {
throw new Error(`Profile ${alias} does not exist`)
}
delete list.profiles[alias]
if (list.current === alias) {
list.current = undefined
}
await localStore.write(profileListFileName, JSON.stringify(list))
},
async importExisting(alias: string, location: string) {
const list = await readProfileList()
if (list.profiles[alias]) {
throw new Error(`Profile ${alias} already exists`)
}
const tarSnapshotStore = await tarSnapshotFromUrl(location)
const info = await profileStore(tarSnapshotStore).info()
list.profiles[alias] = {
id: info.id,
location,
}
list.current = alias
await localStore.write(profileListFileName, JSON.stringify(list))
return {
info,
store: tarSnapshotStore,
}
},
async create(alias: string, location: string, profile: Omit<Profile, 'id'>, init: (store: ProfileStore) => Promise<void>) {
const list = await readProfileList()
if (list.profiles[alias]) {
throw new Error(`Profile ${alias} already exists`)
}
const id = `${alias}-${Math.random().toString(36).substring(2, 9)}`
const tar = await tarSnapshotFromUrl(location)
const pStore = profileStore(tar)
await pStore.init({ id, ...profile })
list.profiles[alias] = {
id,
location,
}
list.current = alias
await init(pStore)
await localStore.write(profileListFileName, JSON.stringify(list))
return {
info: {
id,
...profile,
},
store: tar,
}
},
}
}

export type LocalProfilesConfig = ReturnType<typeof localProfilesConfig>
4 changes: 2 additions & 2 deletions packages/cli/src/lib/profile/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './store'
export * from './types'
export * from './profileConfig'
export * from './profile'
export * from './config'
File renamed without changes.
126 changes: 0 additions & 126 deletions packages/cli/src/lib/profile/profileConfig.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/cli/src/lib/profile/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path'
import { parseKey } from '@preevy/common'
import { Profile } from './types'
import { Profile } from './profile'
import { Store } from '../store'

export const profileStore = (store: Store) => {
Expand Down
Loading

0 comments on commit de0b7b8

Please sign in to comment.