Skip to content

Commit

Permalink
Merge pull request #87 from PierreBeucher/update-command
Browse files Browse the repository at this point in the history
Update command
  • Loading branch information
PierreBeucher authored Dec 30, 2024
2 parents 0b8013f + 4147aa8 commit 0141496
Show file tree
Hide file tree
Showing 46 changed files with 1,479 additions and 608 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ tmp/
pulumi/aws/Pulumi.*.yaml
**/node_modules
**/dist
result
result

# Unit test tmp dir
test/unit/core/state/TMP-v0-root-data-dir
20 changes: 12 additions & 8 deletions src/configurators/ansible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,33 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as yaml from 'js-yaml';
import { CommonProvisionInputV1, CommonProvisionOutputV1 } from '../core/state/state';
import { InstanceConfigurator } from '../core/configurator';
import { CommonConfigurationInputV1, CommonProvisionInputV1, CommonProvisionOutputV1, InstanceStateV1 } from '../core/state/state';
import { AbstractInstanceConfigurator } from '../core/configurator';
import { getLogger, Logger } from '../log/utils';
import { AnsibleClient } from '../tools/ansible';

export interface AnsibleConfiguratorArgs {
instanceName: string
commonInput: CommonProvisionInputV1
commonOutput: CommonProvisionOutputV1
provisionInput: CommonProvisionInputV1
provisionOutput: CommonProvisionOutputV1
configurationInput: CommonConfigurationInputV1
additionalAnsibleArgs?: string[]
}

export class AnsibleConfigurator implements InstanceConfigurator {
export class AnsibleConfigurator<ST extends InstanceStateV1> extends AbstractInstanceConfigurator<ST> {

protected readonly logger: Logger
private readonly args: AnsibleConfiguratorArgs

constructor(args: AnsibleConfiguratorArgs){
super()
this.args = args
this.logger = getLogger(args.instanceName)
}

async configure() {
async doConfigure() {

const ssh = this.args.commonInput.ssh
const ssh = this.args.provisionInput.ssh

this.logger.debug(`Running Ansible configuration`)

Expand All @@ -39,7 +41,7 @@ export class AnsibleConfigurator implements InstanceConfigurator {
all: {
hosts: {
[this.args.instanceName]: {
ansible_host: this.args.commonOutput.host,
ansible_host: this.args.provisionOutput.host,
ansible_user: ssh.user,
ansible_ssh_private_key_file: ssh.privateKeyPath,
wolf_instance_name: this.args.instanceName
Expand All @@ -59,5 +61,7 @@ export class AnsibleConfigurator implements InstanceConfigurator {

const ansible = new AnsibleClient()
await ansible.runAnsible(inventoryPath, playbookPath, this.args.additionalAnsibleArgs ?? [])

return {}
}
}
12 changes: 11 additions & 1 deletion src/core/configurator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { CommonConfigurationOutputV1, InstanceStateV1 } from "./state/state";

/**
* Configurator are responsible to configure an instance after provisioning,
* such as installing drivers and system packages.
*/
export interface InstanceConfigurator {
configure(): Promise<void>
configure(): Promise<CommonConfigurationOutputV1>
}

export abstract class AbstractInstanceConfigurator<ST extends InstanceStateV1> implements InstanceConfigurator {
configure(): Promise<NonNullable<ST["configuration"]["output"]>> {
return this.doConfigure()
}

abstract doConfigure(): Promise<NonNullable<ST["configuration"]["output"]>>
}
47 changes: 43 additions & 4 deletions src/core/initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { CreateCliArgs } from "./input/cli"
import { InputPrompter } from "./input/prompter"
import { InstanceManagerBuilder } from "./manager-builder"
import { StateInitializer } from "./state/initializer"
import { confirm } from '@inquirer/prompts';
import { StateLoader } from "./state/loader"

export interface InstancerInitializerArgs {
provider: CLOUDYPAD_PROVIDER
Expand Down Expand Up @@ -44,16 +46,53 @@ export class InteractiveInstanceInitializer {

const input = await this.inputPrompter.completeCliInput(cliArgs)

const loader = new StateLoader()
if(await loader.instanceExists(input.instanceName) && !cliArgs.overwriteExisting){
const confirmAlreadyExists = await confirm({
message: `Instance ${input.instanceName} already exists. Do you want to overwrite existing instance config?`,
default: false,
})

if (!confirmAlreadyExists) {
throw new Error("Won't overwrite existing instance. Initialization aborted.")
}
}

const state = await new StateInitializer({
input: input,
provider: this.provider,
overwriteExisting: cliArgs.overwriteExisting
}).initializeState()

const manager = new InstanceManagerBuilder().buildManagerForState(state)
await manager.initialize({
autoApprove: cliArgs.yes
const manager = await new InstanceManagerBuilder().buildInstanceManager(state.name)
const instanceName = state.name
const autoApprove = cliArgs.yes

this.logger.info(`Initializing ${instanceName}: provisioning...`)

await manager.provision({ autoApprove: autoApprove})

this.logger.info(`Initializing ${instanceName}: provision done.}`)

this.logger.info(`Initializing ${instanceName}: configuring...}`)

await manager.configure()

this.logger.info(`Initializing ${instanceName}: configuration done.}`)

const doPair = autoApprove ? true : await confirm({
message: `Your instance is almost ready ! Do you want to pair Moonlight now?`,
default: true,
})

if (doPair) {
this.logger.info(`Initializing ${instanceName}: pairing...}`)

await manager.pair()

this.logger.info(`Initializing ${instanceName}: pairing done.}`)
} else {
this.logger.info(`Initializing ${instanceName}: pairing skipped.}`)
}

if(!options?.skipPostInitInfo){
console.info("")
Expand Down
26 changes: 25 additions & 1 deletion src/core/input/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@ import { PUBLIC_IP_TYPE, PUBLIC_IP_TYPE_DYNAMIC, PUBLIC_IP_TYPE_STATIC } from ".

//
// Common CLI Option each providers can re-use
///
//

/**
* Arguments any Provider can take as parameter for create command
*/
export interface CreateCliArgs {
name?: string
privateSshKey?: string
yes?: boolean // auto approve
overwriteExisting?: boolean
}

/**
* Arguments any Provider can take as parameter for update command
*/
export type UpdateCliArgs = Omit<CreateCliArgs, "name" | "privateSshKey">


export const CLI_OPTION_INSTANCE_NAME = new Option('--name <name>', 'Instance name')
export const CLI_OPTION_PRIVATE_SSH_KEY = new Option('--private-ssh-key <path>', 'Path to private SSH key to use to connect to instance')
export const CLI_OPTION_AUTO_APPROVE = new Option('--yes', 'Do not prompt for approval, automatically approve and continue')
Expand Down Expand Up @@ -39,11 +48,26 @@ export abstract class CliCommandGenerator {
.addOption(CLI_OPTION_OVERWRITE_EXISTING)
}

/**
* Create a base 'update' command for a given provider name with possibilities to chain with additional options.
*/
protected getBaseUpdateCommand(provider: string){
return new Command(provider)
.description(`Update an existing Cloudy Pad instance using ${provider} provider.`)
.requiredOption('--name <name>', 'Instance name')
.addOption(CLI_OPTION_AUTO_APPROVE)
}

/**
* Build a 'create' Command for Commander CLI using provided Command
*/
abstract buildCreateCommand(): Command<[]>

/**
* Build an 'update' Command for Commander CLI using provided Command
*/
abstract buildUpdateCommand(): Command<[]>

}

export function parsePublicIpType(value: string): PUBLIC_IP_TYPE {
Expand Down
15 changes: 14 additions & 1 deletion src/core/input/prompter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,20 @@ export abstract class AbstractInputPrompter<A extends CreateCliArgs, I extends C
/**
* Transform CLI arguments into known Input interface
*/
abstract cliArgsIntoInput(cliArgs: A): PartialDeep<I>
cliArgsIntoInput(cliArgs: A): PartialDeep<I> {
this.logger.debug(`Parsing CLI args ${JSON.stringify(cliArgs)} into Input interface...`)

const result = this.doTransformCliArgsIntoInput(cliArgs)

this.logger.debug(`Parsed CLI args ${JSON.stringify(cliArgs)} into ${JSON.stringify(input)}`)

return result
}

/**
* Transform CLI arguments into known Input interface
*/
protected abstract doTransformCliArgsIntoInput(cliArgs: A): PartialDeep<I>

async completeCliInput(cliArgs: A): Promise<I> {
const partialInput = this.cliArgsIntoInput(cliArgs)
Expand Down
127 changes: 72 additions & 55 deletions src/core/manager-builder.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,104 @@
import { CLOUDYPAD_PROVIDER_AWS, CLOUDYPAD_PROVIDER_AZURE, CLOUDYPAD_PROVIDER_GCP, CLOUDYPAD_PROVIDER_PAPERSPACE } from './const';
import { getLogger } from '../log/utils';
import { StateManager } from './state/manager';
import { AwsInstanceStateV1 } from '../providers/aws/state';
import { AwsSubManagerFactory } from '../providers/aws/factory';
import { InstanceStateV1 } from './state/state';
import { AzureInstanceStateV1 } from '../providers/azure/state';
import { PaperspaceInstanceStateV1 } from '../providers/paperspace/state';
import { GcpInstanceStateV1 } from '../providers/gcp/state';
import { GcpSubManagerFactory } from '../providers/gcp/factory';
import { AzureSubManagerFactory } from '../providers/azure/factory';
import { PaperspaceSubManagerFactory } from '../providers/paperspace/factory';
import { InstanceManager } from './manager';
import { GenericInstanceManager, InstanceManager } from './manager';
import { StateLoader } from './state/loader';
import { StateParser } from './state/parser';
import { StateWriter } from './state/writer';
import { InstanceStateV1 } from './state/state';
import { StateMigrator } from './state/migrator';
import { AwsInstanceStateV1 } from '../providers/aws/state';
import { AzureInstanceStateV1 } from '../providers/azure/state';
import { GcpInstanceStateV1 } from '../providers/gcp/state';
import { PaperspaceInstanceStateV1 } from '../providers/paperspace/state';
import { InstanceUpdater } from './updater';

export class InstanceManagerBuilder {

private static readonly logger = getLogger(InstanceManagerBuilder.name)

private readonly stateManager: StateManager

private readonly stateParser = new StateParser()

constructor() {
this.stateManager = StateManager.default()
getAllInstances(): string[] {
return new StateLoader().listInstances()
}

getAllInstances(): string[] {
return StateManager.default().listInstances()
private async loadAndMigrateState(instanceName: string): Promise<InstanceStateV1>{
await new StateMigrator().ensureInstanceStateV1(instanceName)

const state = await new StateLoader().loadInstanceStateSafe(instanceName)
return state
}

/**
* Build an InstanceManager for a instance. Load instance state from disk
* and create a related InstanceManager.
*/
async buildManagerForInstance(name: string): Promise<InstanceManager>{
const state = await StateManager.default().loadInstanceStateSafe(name)
return this.buildManagerForState(state)
private parseAwsState(rawState: InstanceStateV1): AwsInstanceStateV1 {
return new StateParser().parseAwsStateV1(rawState)
}

private parseAzureState(rawState: InstanceStateV1): AzureInstanceStateV1 {
return new StateParser().parseAzureStateV1(rawState)
}

private parseGcpState(rawState: InstanceStateV1): GcpInstanceStateV1 {
return new StateParser().parseGcpStateV1(rawState)
}

private parsePaperspaceState(rawState: InstanceStateV1): PaperspaceInstanceStateV1 {
return new StateParser().parsePaperspaceStateV1(rawState)
}

async buildInstanceManager(name: string): Promise<InstanceManager>{
const state = await this.loadAndMigrateState(name)

buildManagerForState(state: InstanceStateV1) {
if (state.provision.provider === CLOUDYPAD_PROVIDER_AWS) {
const awsState: AwsInstanceStateV1 = this.stateParser.parseAwsStateV1(state)
return this.buildAwsInstanceManager(awsState)
return new GenericInstanceManager({
stateWriter: new StateWriter({ state: this.parseAwsState(state)}),
factory: new AwsSubManagerFactory()
})
} else if (state.provision.provider === CLOUDYPAD_PROVIDER_GCP) {
const gcpState: GcpInstanceStateV1 = this.stateParser.parseGcpStateV1(state)
return this.buildGcpInstanceManager(gcpState)
return new GenericInstanceManager({
stateWriter: new StateWriter({ state: this.parseGcpState(state)}),
factory: new GcpSubManagerFactory()
})
} else if (state.provision.provider === CLOUDYPAD_PROVIDER_AZURE) {
const azureState: AzureInstanceStateV1 = this.stateParser.parseAzureStateV1(state)
return this.buildAzureInstanceManager(azureState)
return new GenericInstanceManager({
stateWriter: new StateWriter({ state: this.parseAzureState(state)}),
factory: new AzureSubManagerFactory()
})
} else if (state.provision.provider === CLOUDYPAD_PROVIDER_PAPERSPACE) {
const paperspaceState: PaperspaceInstanceStateV1 = this.stateParser.parsePaperspaceStateV1(state)
return this.buildPaperspaceInstanceManager(paperspaceState)
return new GenericInstanceManager({
stateWriter: new StateWriter({ state: this.parsePaperspaceState(state)}),
factory: new PaperspaceSubManagerFactory()
})
} else {
throw new Error(`Unknown provider '${state.provision.provider}' in state: ${JSON.stringify(state)}`)
}
}
}

buildAwsInstanceManager(state: AwsInstanceStateV1){
return new InstanceManager({
state: state,
factory: new AwsSubManagerFactory()
})
async buildAwsInstanceUpdater(instanceName: string): Promise<InstanceUpdater<AwsInstanceStateV1>> {
const rawState = await this.loadAndMigrateState(instanceName)
const stateWriter = new StateWriter({ state: this.parseAwsState(rawState) })
return new InstanceUpdater({ stateWriter: stateWriter })
}

buildGcpInstanceManager(state: GcpInstanceStateV1){
return new InstanceManager({
state: state,
factory: new GcpSubManagerFactory()
})

async buildGcpInstanceUpdater(instanceName: string): Promise<InstanceUpdater<GcpInstanceStateV1>> {
const rawState = await this.loadAndMigrateState(instanceName)
const stateWriter = new StateWriter({ state: this.parseGcpState(rawState) })
return new InstanceUpdater({ stateWriter: stateWriter })
}

buildAzureInstanceManager(state: AzureInstanceStateV1){
return new InstanceManager({
state: state,
factory: new AzureSubManagerFactory()
})

async buildAzureInstanceUpdater(instanceName: string): Promise<InstanceUpdater<AzureInstanceStateV1>> {
const rawState = await this.loadAndMigrateState(instanceName)
const stateWriter = new StateWriter({ state: this.parseAzureState(rawState) })
return new InstanceUpdater({ stateWriter: stateWriter })
}

buildPaperspaceInstanceManager(state: PaperspaceInstanceStateV1){
return new InstanceManager({
state: state,
factory: new PaperspaceSubManagerFactory()
})

async buildPaperspaceInstanceUpdater(instanceName: string): Promise<InstanceUpdater<PaperspaceInstanceStateV1>> {
const rawState = await this.loadAndMigrateState(instanceName)
const stateWriter = new StateWriter({ state: this.parsePaperspaceState(rawState) })
return new InstanceUpdater({ stateWriter: stateWriter })
}



}
Loading

0 comments on commit 0141496

Please sign in to comment.