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

kube driver: move to StatefulSet #401

Merged
merged 1 commit into from
Jan 22, 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
21 changes: 16 additions & 5 deletions packages/cli-common/src/commands/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
LogLevel, Logger, logLevels, ComposeModel, ProcessError, telemetryEmitter,
} from '@preevy/core'
import { asyncReduce } from 'iter-tools-es'
import { ParsingToken } from '@oclif/core/lib/interfaces/parser.js'
import { ArgOutput, FlagOutput, Input, ParserOutput, ParsingToken } from '@oclif/core/lib/interfaces/parser.js'
import { mergeWith } from 'lodash-es'
import { commandLogger } from '../lib/log.js'
import { composeFlags, pluginFlags } from '../lib/common-flags/index.js'
import { PreevyConfig } from '../../../core/src/config.js'
Expand Down Expand Up @@ -90,17 +91,27 @@ abstract class BaseCommand<T extends typeof Command=typeof Command> extends Comm
return result
}

public async init(): Promise<void> {
await super.init()
const { args, flags, raw } = await this.parse({
protected async reparse<
F extends FlagOutput,
B extends FlagOutput,
A extends ArgOutput>(
options?: Input<F, B, A>,
argv?: string[],
): Promise<ParserOutput<F, B, A>> {
return await this.parse(mergeWith({
flags: this.ctor.flags,
baseFlags: {
...this.ctor.baseFlags,
...this.ctor.enableJsonFlag ? jsonFlags : {},
},
args: this.ctor.args,
strict: false,
})
}, options, argv))
}

public async init(): Promise<void> {
await super.init()
const { args, flags, raw } = await this.reparse()
this.args = args as Args<T>
this.flags = flags as Flags<T>
if (this.flags.debug) {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/dist
node_modules
/scripts
/tmp
10 changes: 7 additions & 3 deletions packages/cli/src/commands/down.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Flags } from '@oclif/core'
import { findEnvId, machineResourceType, withSpinner } from '@preevy/core'
import DriverCommand from '../driver-command.js'
import MachineCreationDriverCommand from '../machine-creation-driver-command.js'
import { envIdFlags } from '../common-flags.js'

// eslint-disable-next-line no-use-before-define
export default class Down extends DriverCommand<typeof Down> {
export default class Down extends MachineCreationDriverCommand<typeof Down> {
static description = 'Delete preview environments'

static flags = {
Expand All @@ -28,6 +28,7 @@ export default class Down extends DriverCommand<typeof Down> {
const log = this.logger
const { flags } = this
const driver = await this.driver()
const machineCreationDriver = await this.machineCreationDriver()

const envId = await findEnvId({
log,
Expand All @@ -45,7 +46,10 @@ export default class Down extends DriverCommand<typeof Down> {
}

await withSpinner(async () => {
await driver.deleteResources(flags.wait, { type: machineResourceType, providerId: machine.providerId })
await machineCreationDriver.deleteResources(
flags.wait,
{ type: machineResourceType, providerId: machine.providerId },
)
}, { opPrefix: `Deleting ${driver.friendlyName} machine ${machine.providerId} for environment ${envId}` })

await Promise.all(
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default class Init extends BaseCommand {

const driver = await chooseDriver()
const driverStatic = machineDrivers[driver]
const driverFlags = await driverStatic.inquireFlags()
const driverFlags = await driverStatic.inquireFlags({ log: this.logger })

ux.info(text.recommendation('To use Preevy in a CI flow, select a remote storage for your profile.'))
const locationType = await chooseFsType(({ driver }))
Expand Down
9 changes: 5 additions & 4 deletions packages/cli/src/commands/purge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Flags, ux } from '@oclif/core'
import { asyncFilter, asyncToArray } from 'iter-tools-es'
import { groupBy, partition } from 'lodash-es'
import { MachineResource, isPartialMachine, machineResourceType } from '@preevy/core'
import DriverCommand from '../driver-command.js'
import MachineCreationDriverCommand from '../machine-creation-driver-command.js'
import { carefulBooleanPrompt } from '../prompt.js'

const isMachineResource = (r: { type: string }): r is MachineResource => r.type === machineResourceType
Expand Down Expand Up @@ -35,7 +35,7 @@ const confirmPurge = async (
}

// eslint-disable-next-line no-use-before-define
export default class Purge extends DriverCommand<typeof Purge> {
export default class Purge extends MachineCreationDriverCommand<typeof Purge> {
static description = 'Delete all cloud provider machines and potentially other resources'

static flags = {
Expand Down Expand Up @@ -68,6 +68,7 @@ export default class Purge extends DriverCommand<typeof Purge> {
const { flags } = this

const driver = await this.driver()
const creationDriver = await this.machineCreationDriver()
const resourcePlurals: Record<string, string> = { [machineResourceType]: 'machines', ...driver.resourcePlurals }
const driverResourceTypes = new Set(Object.keys(resourcePlurals))

Expand All @@ -83,7 +84,7 @@ export default class Purge extends DriverCommand<typeof Purge> {
const allResources = await asyncToArray(
asyncFilter(
({ type }) => flags.all || flags.type.includes(type),
driver.listDeletableResources(),
creationDriver.listDeletableResources(),
),
)

Expand All @@ -106,7 +107,7 @@ export default class Purge extends DriverCommand<typeof Purge> {
return undefined
}

await driver.deleteResources(flags.wait, ...allResources)
await creationDriver.deleteResources(flags.wait, ...allResources)

if (flags.json) {
return allResources
Expand Down
34 changes: 21 additions & 13 deletions packages/cli/src/driver-command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Command, Flags, Interfaces } from '@oclif/core'
import { MachineConnection, MachineDriver, isPartialMachine, profileStore } from '@preevy/core'
import { pickBy } from 'lodash-es'
import { DriverFlags, DriverName, FlagType, flagsForAllDrivers, machineDrivers, removeDriverPrefix } from './drivers.js'
import { mapValues, pickBy } from 'lodash-es'
import { Flag } from '@oclif/core/lib/interfaces'
import { DriverFlags, DriverName, FlagType, addDriverPrefix, flagsForAllDrivers, machineDrivers, removeDriverPrefix } from './drivers.js'
import ProfileCommand from './profile-command.js'

// eslint-disable-next-line no-use-before-define
Expand Down Expand Up @@ -40,18 +41,25 @@ abstract class DriverCommand<T extends typeof Command> extends ProfileCommand<T>
driver: Name,
type: Type
): Promise<DriverFlags<DriverName, Type>> {
const driverFlagNames = Object.keys(machineDrivers[driver][type])
const flagDefaults = pickBy(
{
...await profileStore(this.store).ref.defaultDriverFlags(driver),
...this.preevyConfig?.drivers?.[driver] ?? {},
},
(_v, k) => driverFlagNames.includes(k),
) as DriverFlags<DriverName, Type>
return {
...flagDefaults,
...removeDriverPrefix<DriverFlags<DriverName, Type>>(driver, this.flags),
const driverFlags = machineDrivers[driver][type]
const flagDefaults = {
...await profileStore(this.store).ref.defaultDriverFlags(driver),
...this.preevyConfig?.drivers?.[driver] ?? {},
}

const flagDefsWithDefaults = addDriverPrefix(driver, mapValues(
driverFlags,
(v: Flag<unknown>, k) => Object.assign(v, { default: flagDefaults[k] ?? v.default }),
)) as Record<string, Flag<unknown>>

const { flags: parsedFlags } = await this.reparse({ flags: flagDefsWithDefaults })

const driverFlagNamesWithPrefix = new Set(Object.keys(driverFlags).map(k => `${driver}-${k}`))

const parsedDriverFlags = pickBy(parsedFlags, (_v, k) => driverFlagNamesWithPrefix.has(k))

const result = removeDriverPrefix(driver, parsedDriverFlags) as DriverFlags<DriverName, Type>
return result
}

#driver: MachineDriver | undefined
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/hooks/init/load-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const wrappedHook: OclifHook<'init'> = async function wrappedHook(...args) {
await initHook.call(this, ...args)
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`warning: failed to init context: ${e}`)
console.warn(`warning: failed to init context: ${(e as Error).stack || e}`)
telemetryEmitter().capture('plugin-init-error', { error: e })
await telemetryEmitter().flush()
process.exit(1)
Expand Down
22 changes: 14 additions & 8 deletions packages/core/src/driver/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export type MachineConnection = Disposable & {

export type MachineDriver<
Machine extends MachineBase = MachineBase,
ResourceType extends string = string
> = {
customizationScripts?: string[]
friendlyName: string
Expand All @@ -33,8 +32,6 @@ export type MachineDriver<
) => Promise<{ code: number } | { signal: string }>

listMachines: () => AsyncIterableIterator<Machine | PartialMachine>
listDeletableResources: () => AsyncIterableIterator<Resource<ResourceType>>
deleteResources: (wait: boolean, ...resource: Resource<string>[]) => Promise<void>
machineStatusCommand: (machine: MachineBase) => Promise<MachineStatusCommand | undefined>
}

Expand All @@ -43,7 +40,10 @@ export type MachineCreationResult<Machine extends MachineBase = MachineBase> = {
result: Promise<{ machine: Machine; connection: MachineConnection }>
}

export type MachineCreationDriver<Machine extends MachineBase = MachineBase> = {
export type MachineCreationDriver<
Machine extends MachineBase = MachineBase,
ResourceType extends string = string,
> = {
metadata: Record<string, unknown>

createMachine: (args: {
Expand All @@ -54,24 +54,30 @@ export type MachineCreationDriver<Machine extends MachineBase = MachineBase> = {
getMachineAndSpecDiff: (
args: { envId: string },
) => Promise<(Machine & { specDiff: SpecDiffItem[] }) | PartialMachine | undefined>

listDeletableResources: () => AsyncIterableIterator<Resource<ResourceType>>
deleteResources: (wait: boolean, ...resource: Resource<string>[]) => Promise<void>
}

export type MachineDriverFactory<
Flags,
Machine extends MachineBase = MachineBase,
ResourceType extends string = string
> = ({ flags, profile, store, log, debug }: {
flags: Flags
profile: Profile
store: Store
log: Logger
debug: boolean
}) => MachineDriver<Machine, ResourceType>
}) => MachineDriver<Machine>

export type MachineCreationDriverFactory<Flags, Machine extends MachineBase> = ({ flags, profile, store, log, debug }: {
export type MachineCreationDriverFactory<
Flags,
Machine extends MachineBase,
ResourceType extends string = string,
> = ({ flags, profile, store, log, debug }: {
flags: Flags
profile: Profile
store: Store
log: Logger
debug: boolean
}) => MachineCreationDriver<Machine>
}) => MachineCreationDriver<Machine, ResourceType>
5 changes: 4 additions & 1 deletion packages/core/src/driver/machine-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ const ensureBareMachine = async ({
return await withSpinner(async spinner => {
if (existingMachine && recreating) {
spinner.text = 'Deleting machine'
await machineDriver.deleteResources(false, { type: machineResourceType, providerId: existingMachine.providerId })
await machineCreationDriver.deleteResources(
false,
{ type: machineResourceType, providerId: existingMachine.providerId },
)
}
spinner.text = 'Checking for existing snapshot'
const machineCreation = await machineCreationDriver.createMachine({ envId })
Expand Down
83 changes: 38 additions & 45 deletions packages/driver-azure/src/driver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,48 +87,32 @@ const machineFromVm = (
}
}

const listMachines = ({ client: cl }: { client: Client }) => asyncMap(
rg => cl.getInstanceByRg(rg.name as string).then(vm => {
if (vm) {
return machineFromVm(vm)
}
return {
type: machineResourceType,
providerId: rg.name as string,
envId: rg.tags?.[AzureCustomTags.ENV_ID] as string,
error: 'VM creation is incomplete',
}
}),
cl.listResourceGroups()
)

const machineDriver = (
{ store, client: cl }: DriverContext,
): MachineDriver<SshMachine, ResourceType> => {
const listMachines = () => asyncMap(
rg => cl.getInstanceByRg(rg.name as string).then(vm => {
if (vm) {
return machineFromVm(vm)
}
return {
type: machineResourceType,
providerId: rg.name as string,
envId: rg.tags?.[AzureCustomTags.ENV_ID] as string,
error: 'VM creation is incomplete',
}
}),
cl.listResourceGroups()
)

return ({
customizationScripts: CUSTOMIZE_BARE_MACHINE,
friendlyName: 'Microsoft Azure',
getMachine: async ({ envId }) => await cl.getInstance(envId).then(vm => machineFromVm(vm)),

listMachines,
listDeletableResources: listMachines,

deleteResources: async (wait, ...resources) => {
await Promise.all(resources.map(({ type, providerId }) => {
if (type === machineResourceType) {
return cl.deleteResourcesResourceGroup(providerId, wait)
}
throw new Error(`Unknown resource type "${type}"`)
}))
},

resourcePlurals: {},

...sshDriver({ getSshKey: () => getStoredSshKey(store, SSH_KEYPAIR_ALIAS) }),

machineStatusCommand: async () => machineStatusNodeExporterCommand,
})
}
): MachineDriver<SshMachine> => ({
customizationScripts: CUSTOMIZE_BARE_MACHINE,
friendlyName: 'Microsoft Azure',
getMachine: async ({ envId }) => await cl.getInstance(envId).then(vm => machineFromVm(vm)),
listMachines: () => listMachines({ client: cl }),
resourcePlurals: {},
...sshDriver({ getSshKey: () => getStoredSshKey(store, SSH_KEYPAIR_ALIAS) }),
machineStatusCommand: async () => machineStatusNodeExporterCommand,
})

const flags = {
region: Flags.string({
Expand All @@ -143,7 +127,7 @@ const flags = {

type FlagTypes = Omit<Interfaces.InferredFlags<typeof flags>, 'json'>

const inquireFlags = async () => {
const inquireFlags = async ({ log: _log }: { log: Logger }) => {
const region = await inquirerAutoComplete<string>({
message: flags.region.description as string,
source: async input => REGIONS.filter(r => !input || r.includes(input.toLowerCase())).map(value => ({ value })),
Expand Down Expand Up @@ -191,7 +175,7 @@ type MachineCreationContext = DriverContext & {

const machineCreationDriver = (
{ client: cl, vmSize, store, log, debug, metadata }: MachineCreationContext,
): MachineCreationDriver<SshMachine> => {
): MachineCreationDriver<SshMachine, ResourceType> => {
const ssh = sshDriver({ getSshKey: () => getStoredSshKey(store, SSH_KEYPAIR_ALIAS) })

return {
Expand Down Expand Up @@ -241,13 +225,21 @@ const machineCreationDriver = (
: [],
})
},
listDeletableResources: () => listMachines({ client: cl }),
deleteResources: async (wait, ...resources) => {
await Promise.all(resources.map(({ type, providerId }) => {
if (type === machineResourceType) {
return cl.deleteResourcesResourceGroup(providerId, wait)
}
throw new Error(`Unknown resource type "${type}"`)
}))
},
}
}

const factory: MachineDriverFactory<
Interfaces.InferredFlags<typeof flags>,
SshMachine,
ResourceType
SshMachine
> = ({ flags: f, profile: { id: profileId }, store, log, debug }) => machineDriver({
client: createClient({
...contextFromFlags(f),
Expand All @@ -267,7 +259,8 @@ const machineCreationContextFromFlags = (

const machineCreationFactory: MachineCreationDriverFactory<
MachineCreationFlagTypes,
SshMachine
SshMachine,
ResourceType
> = ({ flags: f, profile: { id: profileId }, store, log, debug }) => {
const c = machineCreationContextFromFlags(f)
return machineCreationDriver({
Expand Down
Loading
Loading