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

Refactor code for improved readability by adjusting formatting and removing unnecessary line breaks in run.ts and start.ts.

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