A Docker-like CLI and HTTP API for managing headless VMs

format

+59 -57
+13 -13
src/subcommands/run.ts
··· 22 22 pulledImg 23 23 ? Effect.succeed(pulledImg) 24 24 : Effect.fail(new PullImageError({ cause: "Failed to pull image" })) 25 - ) 25 + ), 26 26 ); 27 - }) 27 + }), 28 28 ); 29 29 30 30 const createVolumeIfNeeded = ( 31 - image: Image 31 + image: Image, 32 32 ): Effect.Effect<[Image, Volume?], Error, never> => 33 33 parseFlags(Deno.args).flags.volume 34 34 ? Effect.gen(function* () { 35 - const volumeName = parseFlags(Deno.args).flags.volume as string; 36 - const volume = yield* getVolume(volumeName); 37 - if (volume) { 38 - return [image, volume]; 39 - } 40 - const newVolume = yield* createVolume(volumeName, image); 41 - return [image, newVolume]; 42 - }) 35 + const volumeName = parseFlags(Deno.args).flags.volume as string; 36 + const volume = yield* getVolume(volumeName); 37 + if (volume) { 38 + return [image, volume]; 39 + } 40 + const newVolume = yield* createVolume(volumeName, image); 41 + return [image, newVolume]; 42 + }) 43 43 : Effect.succeed([image]); 44 44 45 45 const runImage = ([image, volume]: [Image, Volume?]) => ··· 73 73 console.error(`Failed to run image: ${error.cause} ${image}`); 74 74 Deno.exit(1); 75 75 }) 76 - ) 77 - ) 76 + ), 77 + ), 78 78 ); 79 79 } 80 80
+46 -44
src/subcommands/start.ts
··· 17 17 }> {} 18 18 19 19 export class VmAlreadyRunningError extends Data.TaggedError( 20 - "VmAlreadyRunningError" 20 + "VmAlreadyRunningError", 21 21 )<{ 22 22 name: string; 23 23 }> {} ··· 31 31 getInstanceState(name), 32 32 Effect.flatMap((vm) => 33 33 vm ? Effect.succeed(vm) : Effect.fail(new VmNotFoundError({ name })) 34 - ) 34 + ), 35 35 ); 36 36 37 37 const logStarting = (vm: VirtualMachine) => ··· 44 44 export const setupFirmware = () => setupFirmwareFilesIfNeeded(); 45 45 46 46 export const buildQemuArgs = (vm: VirtualMachine, firmwareArgs: string[]) => { 47 - const qemu = 48 - Deno.build.arch === "aarch64" 49 - ? "qemu-system-aarch64" 50 - : "qemu-system-x86_64"; 47 + const qemu = Deno.build.arch === "aarch64" 48 + ? "qemu-system-aarch64" 49 + : "qemu-system-x86_64"; 51 50 52 51 let coreosArgs: string[] = Effect.runSync(setupCoreOSArgs(vm.drivePath)); 53 52 ··· 84 83 vm.drivePath && [ 85 84 "-drive", 86 85 `file=${vm.drivePath},format=${vm.diskFormat},if=virtio`, 87 - ] 86 + ], 88 87 ), 89 88 ...coreosArgs, 90 89 ...(vm.volume ? [] : ["-snapshot"]), ··· 100 99 export const startDetachedQemu = ( 101 100 name: string, 102 101 vm: VirtualMachine, 103 - qemuArgs: string[] 102 + qemuArgs: string[], 104 103 ) => { 105 - const qemu = 106 - Deno.build.arch === "aarch64" 107 - ? "qemu-system-aarch64" 108 - : "qemu-system-x86_64"; 104 + const qemu = Deno.build.arch === "aarch64" 105 + ? "qemu-system-aarch64" 106 + : "qemu-system-x86_64"; 109 107 110 108 const logPath = `${LOGS_DIR}/${vm.name}.log`; 111 109 112 110 const fullCommand = vm.bridge 113 - ? `sudo ${qemu} ${qemuArgs 111 + ? `sudo ${qemu} ${ 112 + qemuArgs 114 113 .slice(1) 115 - .join(" ")} >> "${logPath}" 2>&1 & echo $!` 114 + .join(" ") 115 + } >> "${logPath}" 2>&1 & echo $!` 116 116 : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 117 117 118 118 return Effect.tryPromise({ ··· 143 143 Effect.flatMap(({ qemuPid, logPath }) => 144 144 pipe( 145 145 updateInstanceState(name, "RUNNING", qemuPid), 146 - Effect.map(() => ({ vm, qemuPid, logPath })) 146 + Effect.map(() => ({ vm, qemuPid, logPath })), 147 147 ) 148 - ) 148 + ), 149 149 ); 150 150 }; 151 151 ··· 160 160 }) => 161 161 Effect.sync(() => { 162 162 console.log( 163 - `Virtual machine ${vm.name} started in background (PID: ${qemuPid})` 163 + `Virtual machine ${vm.name} started in background (PID: ${qemuPid})`, 164 164 ); 165 165 console.log(`Logs will be written to: ${logPath}`); 166 166 }); ··· 168 168 const startInteractiveQemu = ( 169 169 name: string, 170 170 vm: VirtualMachine, 171 - qemuArgs: string[] 171 + qemuArgs: string[], 172 172 ) => { 173 - const qemu = 174 - Deno.build.arch === "aarch64" 175 - ? "qemu-system-aarch64" 176 - : "qemu-system-x86_64"; 173 + const qemu = Deno.build.arch === "aarch64" 174 + ? "qemu-system-aarch64" 175 + : "qemu-system-x86_64"; 177 176 178 177 return Effect.tryPromise({ 179 178 try: async () => { ··· 209 208 }); 210 209 211 210 export const createVolumeIfNeeded = ( 212 - vm: VirtualMachine 211 + vm: VirtualMachine, 213 212 ): Effect.Effect<[VirtualMachine, Volume?], Error, never> => 214 213 Effect.gen(function* () { 215 214 const { flags } = parseFlags(Deno.args); ··· 224 223 225 224 if (!vm.drivePath) { 226 225 throw new Error( 227 - `Cannot create volume: Virtual machine ${vm.name} has no drivePath defined.` 226 + `Cannot create volume: Virtual machine ${vm.name} has no drivePath defined.`, 228 227 ); 229 228 } 230 229 ··· 267 266 diskFormat: volume ? "qcow2" : vm.diskFormat, 268 267 volume: volume?.path, 269 268 }, 270 - firmwareArgs 269 + firmwareArgs, 271 270 ) 272 271 ), 273 272 Effect.flatMap((qemuArgs) => ··· 277 276 startDetachedQemu(name, { ...vm, volume: volume?.path }, qemuArgs) 278 277 ), 279 278 Effect.tap(logDetachedSuccess), 280 - Effect.map(() => 0) // Exit code 0 279 + Effect.map(() => 0), // Exit code 0 281 280 ) 282 - ) 281 + ), 283 282 ) 284 283 ), 285 - Effect.catchAll(handleError) 284 + Effect.catchAll(handleError), 286 285 ); 287 286 288 287 const startInteractiveEffect = (name: string) => ··· 303 302 diskFormat: volume ? "qcow2" : vm.diskFormat, 304 303 volume: volume?.path, 305 304 }, 306 - firmwareArgs 305 + firmwareArgs, 307 306 ) 308 307 ), 309 308 Effect.flatMap((qemuArgs) => 310 309 startInteractiveQemu(name, { ...vm, volume: volume?.path }, qemuArgs) 311 310 ), 312 - Effect.map((status) => (status.success ? 0 : status.code || 1)) 311 + Effect.map((status) => (status.success ? 0 : status.code || 1)), 313 312 ) 314 313 ), 315 - Effect.catchAll(handleError) 314 + Effect.catchAll(handleError), 316 315 ); 317 316 318 317 export default async function (name: string, detach: boolean = false) { 319 318 const exitCode = await Effect.runPromise( 320 - detach ? startDetachedEffect(name) : startInteractiveEffect(name) 319 + detach ? startDetachedEffect(name) : startInteractiveEffect(name), 321 320 ); 322 321 323 322 if (detach) { ··· 331 330 const { flags } = parseFlags(Deno.args); 332 331 return { 333 332 ...vm, 334 - memory: 335 - flags.memory || flags.m ? String(flags.memory || flags.m) : vm.memory, 333 + memory: flags.memory || flags.m 334 + ? String(flags.memory || flags.m) 335 + : vm.memory, 336 336 cpus: flags.cpus || flags.C ? Number(flags.cpus || flags.C) : vm.cpus, 337 337 cpu: flags.cpu || flags.c ? String(flags.cpu || flags.c) : vm.cpu, 338 338 diskFormat: flags.diskFormat ? String(flags.diskFormat) : vm.diskFormat, 339 - portForward: 340 - flags.portForward || flags.p 341 - ? String(flags.portForward || flags.p) 342 - : vm.portForward, 343 - drivePath: 344 - flags.image || flags.i ? String(flags.image || flags.i) : vm.drivePath, 345 - bridge: 346 - flags.bridge || flags.b ? String(flags.bridge || flags.b) : vm.bridge, 347 - diskSize: 348 - flags.size || flags.s ? String(flags.size || flags.s) : vm.diskSize, 339 + portForward: flags.portForward || flags.p 340 + ? String(flags.portForward || flags.p) 341 + : vm.portForward, 342 + drivePath: flags.image || flags.i 343 + ? String(flags.image || flags.i) 344 + : vm.drivePath, 345 + bridge: flags.bridge || flags.b 346 + ? String(flags.bridge || flags.b) 347 + : vm.bridge, 348 + diskSize: flags.size || flags.s 349 + ? String(flags.size || flags.s) 350 + : vm.diskSize, 349 351 }; 350 352 }