Skip to content

Commit

Permalink
kube driver: move to StatefulSet (#401)
Browse files Browse the repository at this point in the history
new environments now use a StatefulSet definition which [dynamically provisions](https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/) a persistent volume. Cluster must have a default StorageClass defined.
  • Loading branch information
Roy Razon authored Jan 22, 2024
1 parent f727940 commit 73d6317
Show file tree
Hide file tree
Showing 26 changed files with 830 additions and 432 deletions.
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

0 comments on commit 73d6317

Please sign in to comment.