Skip to content

Commit

Permalink
Merge pull request #148 from PierreBeucher/core-split
Browse files Browse the repository at this point in the history
no prompt in core components
  • Loading branch information
PierreBeucher authored Feb 25, 2025
2 parents ad9a891 + ea375d1 commit b6b059f
Show file tree
Hide file tree
Showing 27 changed files with 197 additions and 218 deletions.
6 changes: 3 additions & 3 deletions ansible/requirements.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
roles:
- name: geerlingguy.docker
version: "7.3.0"
version: "7.4.5"
collections:
- name: community.docker
version: "3.10.4"
version: "4.3.1"
- name: community.general
version: "9.1.0"
version: "10.3.0"
- name: ansible.posix
version: "2.0.0"
11 changes: 9 additions & 2 deletions ansible/roles/prepare-install/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
# On Paperspace with Ubuntu 22.04, the apt source for Paperspace is buggy (invalid GPG key)
# causing subsequent failure on apt cache update
# removing it here to avoid issues
- name: Remove paperspace apt source (if any)
- name: Remove unwanted apt sources (if any)
become: true
file:
state: absent
path: /etc/apt/sources.list.d/paperspace.list
path: "{{ item }}"
loop:
# Paperspace, out of date, cause apt update failure
- /etc/apt/sources.list.d/paperspace.list

# Scaleway, out of date, cause apt update failure
- /etc/apt/sources.list.d/nvidia-docker.list
- /etc/apt/sources.list.d/cuda.list
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"bin": {
"cloudypad": "./dist/src/cli.js"
"cloudypad": "./dist/src/cli/main.js"
},
"author": "Pierre Beucher <[email protected]>",
"license": "ISC",
Expand Down
4 changes: 2 additions & 2 deletions src/core/cli/command.ts → src/cli/command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command, Option } from "@commander-js/extra-typings";
import { PUBLIC_IP_TYPE, PUBLIC_IP_TYPE_DYNAMIC, PUBLIC_IP_TYPE_STATIC } from "../const";
import { AnalyticsManager } from "../../tools/analytics/manager";
import { PUBLIC_IP_TYPE, PUBLIC_IP_TYPE_DYNAMIC, PUBLIC_IP_TYPE_STATIC } from "../core/const";
import { AnalyticsManager } from "../tools/analytics/manager";

//
// Common CLI Option each providers can re-use
Expand Down
40 changes: 30 additions & 10 deletions src/core/initializer.ts → src/cli/initializer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getLogger } from "../log/utils"
import { CLOUDYPAD_PROVIDER } from "./const"
import { CreateCliArgs } from "./cli/command"
import { InputPrompter, UserVoluntaryInterruptionError } from "./cli/prompter"
import { InstanceManagerBuilder } from "./manager-builder"
import { StateInitializer } from "./state/initializer"
import { CLOUDYPAD_PROVIDER } from "../core/const"
import { CreateCliArgs } from "./command"
import { InputPrompter, inputToHumanReadableString, UserVoluntaryInterruptionError } from "./prompter"
import { InstanceManagerBuilder } from "../core/manager-builder"
import { StateInitializer } from "../core/state/initializer"
import { confirm } from '@inquirer/prompts'
import { AnalyticsManager } from "../tools/analytics/manager"
import { CommonInstanceInput } from "./state/state"
import { CommonInstanceInput } from "../core/state/state"
import { InstanceManager } from "../core/manager"

export interface InstancerInitializerArgs {
provider: CLOUDYPAD_PROVIDER
Expand Down Expand Up @@ -90,17 +91,36 @@ export class InteractiveInstanceInitializer<A extends CreateCliArgs> {
return state
}

private async doProvisioning(manager: any, instanceName: string, autoApprove?: boolean) {
private async doProvisioning(manager: InstanceManager, instanceName: string, autoApprove?: boolean) {
this.logger.info(`Initializing ${instanceName}: provisioning...`)
this.analyticsEvent("create_instance_start_provision")


let confirmCreation: boolean
if(autoApprove){
confirmCreation = autoApprove
} else {

const inputs = await manager.getInputs()

confirmCreation = await confirm({
message: `You are about to provision instance ${instanceName} with the following details:\n` +
` ${inputToHumanReadableString(inputs)}` +
`\nDo you want to proceed?`,
default: true,
})
}

if (!confirmCreation) {
throw new Error(`Provision aborted for instance ${instanceName}.`);
}

await manager.provision({ autoApprove: autoApprove})

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

private async doConfiguration(manager: any, instanceName: string) {
private async doConfiguration(manager: InstanceManager, instanceName: string) {
this.analyticsEvent("create_instance_start_configure")
this.logger.info(`Initializing ${instanceName}: configuring...}`)

Expand All @@ -110,7 +130,7 @@ export class InteractiveInstanceInitializer<A extends CreateCliArgs> {
this.logger.info(`Initializing ${instanceName}: configuration done.}`)
}

private async doPairing(manager: any, instanceName: string, skipPairing: boolean, autoApprove: boolean) {
private async doPairing(manager: InstanceManager, instanceName: string, skipPairing: boolean, autoApprove: boolean) {

const doPair = skipPairing ? false : autoApprove ? true : await confirm({
message: `Your instance is almost ready ! Do you want to pair Moonlight now?`,
Expand Down
6 changes: 3 additions & 3 deletions src/cli.ts → src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
// - Initialize analytics client if enabled
//

import { ConfigManager } from "./core/config/manager"
import { ConfigManager } from "../core/config/manager"
import { buildProgram, shutdownAnalytics, cleanupAndExit, handleErrorAnalytics, logFullError } from "./program"
import { AnalyticsInitializer } from "./tools/analytics/initializer"
import { AnalyticsManager } from "./tools/analytics/manager"
import { AnalyticsInitializer } from "../tools/analytics/initializer"
import { AnalyticsManager } from "../tools/analytics/manager"

async function main(){
try {
Expand Down
31 changes: 22 additions & 9 deletions src/program.ts → src/cli/program.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Command } from '@commander-js/extra-typings';
import { getLogger, setLogVerbosity } from './log/utils';
import { InstanceManagerBuilder } from './core/manager-builder';
import { GcpCliCommandGenerator } from './providers/gcp/cli';
import { AzureCliCommandGenerator } from './providers/azure/cli';
import { AwsCliCommandGenerator } from './providers/aws/cli';
import { PaperspaceCliCommandGenerator } from './providers/paperspace/cli';
import { AnalyticsManager } from './tools/analytics/manager';
import { RUN_COMMAND_CONFIGURE, RUN_COMMAND_DESTROY, RUN_COMMAND_GET, RUN_COMMAND_LIST, RUN_COMMAND_PAIR, RUN_COMMAND_PROVISION, RUN_COMMAND_RESTART, RUN_COMMAND_START, RUN_COMMAND_STOP } from './tools/analytics/events';
import { CLOUDYPAD_VERSION } from './core/const';
import { getLogger, setLogVerbosity } from '../log/utils';
import { InstanceManagerBuilder } from '../core/manager-builder';
import { GcpCliCommandGenerator } from '../providers/gcp/cli';
import { AzureCliCommandGenerator } from '../providers/azure/cli';
import { AwsCliCommandGenerator } from '../providers/aws/cli';
import { PaperspaceCliCommandGenerator } from '../providers/paperspace/cli';
import { AnalyticsManager } from '../tools/analytics/manager';
import { RUN_COMMAND_CONFIGURE, RUN_COMMAND_DESTROY, RUN_COMMAND_GET, RUN_COMMAND_LIST, RUN_COMMAND_PAIR, RUN_COMMAND_PROVISION, RUN_COMMAND_RESTART, RUN_COMMAND_START, RUN_COMMAND_STOP } from '../tools/analytics/events';
import { CLOUDYPAD_VERSION } from '../core/const';
import { confirm } from '@inquirer/prompts';

const logger = getLogger("program")

Expand Down Expand Up @@ -220,6 +221,18 @@ export function buildProgram(){
try {
analyticsClient.sendEvent(RUN_COMMAND_DESTROY)

let approveDestroy: boolean | undefined = opts?.yes
if(approveDestroy === undefined){
approveDestroy = await confirm({
message: `You are about to destroy instance '${name}'. Please confirm:`,
default: false,
})
}

if (!approveDestroy) {
throw new Error('Destroy aborted.')
}

const m = await new InstanceManagerBuilder().buildInstanceManager(name)
await m.destroy({ autoApprove: opts.yes})

Expand Down
63 changes: 58 additions & 5 deletions src/core/cli/prompter.ts → src/cli/prompter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { PartialDeep } from "type-fest"
import { input, select, confirm, password } from '@inquirer/prompts';
import { ExitPromptError } from '@inquirer/core';
import lodash from 'lodash'
import { CommonInstanceInput } from "../state/state";
import { getLogger } from "../../log/utils";
import { PUBLIC_IP_TYPE, PUBLIC_IP_TYPE_DYNAMIC, PUBLIC_IP_TYPE_STATIC } from '../const';
import { CommonInstanceInput, CommonProvisionInputV1, CommonProvisionOutputV1 } from "../core/state/state";
import { getLogger } from "../log/utils";
import { PUBLIC_IP_TYPE, PUBLIC_IP_TYPE_DYNAMIC, PUBLIC_IP_TYPE_STATIC } from '../core/const';
import { CreateCliArgs } from './command';
import { StateLoader } from '../state/loader';
import { CostAlertOptions } from '../provisioner';
import { StateLoader } from '../core/state/loader';
import { CostAlertOptions, InstanceProvisionerArgs } from '../core/provisioner';
const { kebabCase } = lodash

export interface InputPrompter {
Expand Down Expand Up @@ -478,4 +478,57 @@ export function costAlertCliArgsIntoConfig(args: { costAlert?: boolean, costLimi

return undefined
}
}

/**
* Transform args into a human readable string, eg.
* { ssh: { key: '~/.ssh/id_ed25519', user: 'ubuntu' }, instanceName: 'my-instance' } into
* SSH Key: ~/.ssh/id_ed25519
* SSH User: ubuntu
* Instance Name: my-instance
*
*/
export function inputToHumanReadableString(args: CommonInstanceInput): string {

// Shamelessly generated by IA and edited/commented by hand
const humanReadableArgs = (obj?: any, parentKey?: string): string => {

if(obj === undefined){
return ""
}

return Object.keys(obj).map(key => {

// If parent key is not empty, add a dot between parent key and current key
// Otherwise, use current key (only for first level iteration)
const fullKey = parentKey ? `${parentKey} ${key}` : key;

// If value is an object, recursively transform it
if (typeof obj[key] === 'object' && obj[key] !== null && obj[key] !== undefined) {
return humanReadableArgs(obj[key], fullKey);
}

// Tranform original key into human readable key, with MAJ on first letter of each word
let humanReadableKey = fullKey.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())

if(humanReadableKey.startsWith("Ssh")){
humanReadableKey = humanReadableKey.replace("Ssh", "SSH")
}

if(humanReadableKey.startsWith("Ip")){
humanReadableKey = humanReadableKey.replace("Ip", "IP")
}

// Transform value into human readable value (don't transform raw type into string)
const humanReadableValue =
typeof obj[key] === 'boolean' ? obj[key] ? 'Yes' : 'No' :
obj[key] === undefined || obj[key] === null ? 'None' :
String(obj[key])
return `${humanReadableKey}: ${humanReadableValue}`;
}).join('\n ');
};

const provision = humanReadableArgs(args.configuration)
const configuration = humanReadableArgs(args.provision)
return `${provision}\n ${configuration}`
}
34 changes: 27 additions & 7 deletions src/core/updater.ts → src/cli/updater.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { PartialDeep } from "type-fest"
import { InstanceStateV1 } from "./state/state"
import { StateWriter } from "./state/writer"
import { InstanceStateV1 } from "../core/state/state"
import { StateWriter } from "../core/state/writer"
import { getLogger, Logger } from "../log/utils"
import { InstanceManagerBuilder } from "./manager-builder"
import { StateLoader } from "./state/loader"
import { UpdateCliArgs } from "./cli/command"
import { GenericStateParser } from "./state/parser"
import { AbstractInputPrompter } from "./cli/prompter"
import { InstanceManagerBuilder } from "../core/manager-builder"
import { StateLoader } from "../core/state/loader"
import { UpdateCliArgs } from "./command"
import { GenericStateParser } from "../core/state/parser"
import { AbstractInputPrompter, inputToHumanReadableString } from "./prompter"
import { confirm } from "@inquirer/prompts"
import * as lodash from "lodash"

export interface InstanceUpdaterArgs<ST extends InstanceStateV1, A extends UpdateCliArgs> {
Expand Down Expand Up @@ -82,6 +83,25 @@ export class InstanceUpdater<ST extends InstanceStateV1, A extends UpdateCliArgs

const manager = await new InstanceManagerBuilder().buildInstanceManager(instanceName)

const autoApprove = cliArgs.yes
let confirmCreation: boolean
if(autoApprove){
confirmCreation = autoApprove
} else {

const inputs = await manager.getInputs()
confirmCreation = await confirm({
message: `You are about to provision instance ${instanceName} with the following details:\n` +
` ${inputToHumanReadableString(inputs)}` +
`\nDo you want to proceed?`,
default: true,
})
}

if (!confirmCreation) {
throw new Error(`Provision aborted for instance ${instanceName}.`);
}

await manager.provision({ autoApprove: cliArgs.yes })
await manager.configure()
}
Expand Down
13 changes: 11 additions & 2 deletions src/core/manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InstanceStateV1 } from './state/state';
import { CommonInstanceInput, InstanceStateV1 } from './state/state';
import { DestroyOptions, InstanceProvisioner, InstanceProvisionOptions } from './provisioner';
import { InstanceConfigurator } from './configurator';
import { getLogger } from '../log/utils';
Expand Down Expand Up @@ -121,7 +121,7 @@ export interface InstanceManager {
pairSendPin(pin: string, retries?: number, retryDelay?: number): Promise<boolean>
getInstanceDetails(): Promise<CloudyPadInstanceDetails>
getStateJSON(): string

getInputs(): Promise<CommonInstanceInput>
}

export interface InstanceManagerArgs<ST extends InstanceStateV1> {
Expand Down Expand Up @@ -233,4 +233,13 @@ export class GenericInstanceManager<ST extends InstanceStateV1> implements Insta
public getStateJSON(){
return JSON.stringify(this.stateWriter.cloneState(), null, 2)
}

async getInputs(): Promise<CommonInstanceInput> {
const state = this.stateWriter.cloneState()
return {
instanceName: state.name,
provision: state.provision.input,
configuration: state.configuration.input
}
}
}
Loading

0 comments on commit b6b059f

Please sign in to comment.