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

fix volume issue in machine API

+80 -64
+80 -64
src/api/machines.ts
··· 1 - import { Hono } from "hono"; 1 + import { createId } from "@paralleldrive/cuid2"; 2 2 import { Data, Effect, pipe } from "effect"; 3 - import { 4 - createVolumeIfNeeded, 5 - handleError, 6 - parseCreateMachineRequest, 7 - parseParams, 8 - parseQueryParams, 9 - parseStartRequest, 10 - presentation, 11 - } from "./utils.ts"; 3 + import { Hono } from "hono"; 4 + import Moniker from "moniker"; 5 + import { getImage } from "../images.ts"; 12 6 import { DEFAULT_VERSION, getInstanceState } from "../mod.ts"; 7 + import { generateRandomMacAddress } from "../network.ts"; 13 8 import { 14 9 listInstances, 15 10 removeInstanceState, 16 11 saveInstanceState, 17 12 } from "../state.ts"; 18 - import { findVm, killProcess, updateToStopped } from "../subcommands/stop.ts"; 19 13 import { 20 14 buildQemuArgs, 21 15 createLogsDir, ··· 23 17 setupFirmware, 24 18 startDetachedQemu, 25 19 } from "../subcommands/start.ts"; 20 + import { findVm, killProcess, updateToStopped } from "../subcommands/stop.ts"; 26 21 import type { NewMachine } from "../types.ts"; 27 - import { createId } from "@paralleldrive/cuid2"; 28 - import { generateRandomMacAddress } from "../network.ts"; 29 - import Moniker from "moniker"; 30 - import { getImage } from "../images.ts"; 22 + import { getVolume } from "../volumes.ts"; 23 + import { 24 + createVolumeIfNeeded, 25 + handleError, 26 + parseCreateMachineRequest, 27 + parseParams, 28 + parseQueryParams, 29 + parseStartRequest, 30 + presentation, 31 + } from "./utils.ts"; 31 32 32 33 export class ImageNotFoundError extends Data.TaggedError("ImageNotFoundError")<{ 33 34 id: string; 34 35 }> {} 35 36 36 37 export class RemoveRunningVmError extends Data.TaggedError( 37 - "RemoveRunningVmError", 38 + "RemoveRunningVmError" 38 39 )<{ 39 40 id: string; 40 41 }> {} ··· 46 47 pipe( 47 48 parseQueryParams(c), 48 49 Effect.flatMap((params) => 49 - listInstances( 50 - params.all === "true" || params.all === "1", 51 - ) 50 + listInstances(params.all === "true" || params.all === "1") 52 51 ), 53 - presentation(c), 54 - ), 55 - )); 52 + presentation(c) 53 + ) 54 + ) 55 + ); 56 56 57 57 app.post("/", (c) => 58 58 Effect.runPromise( ··· 63 63 const image = yield* getImage(params.image); 64 64 if (!image) { 65 65 return yield* Effect.fail( 66 - new ImageNotFoundError({ id: params.image }), 66 + new ImageNotFoundError({ id: params.image }) 67 67 ); 68 68 } 69 69 ··· 97 97 }) 98 98 ), 99 99 presentation(c), 100 - Effect.catchAll((error) => handleError(error, c)), 101 - ), 102 - )); 100 + Effect.catchAll((error) => handleError(error, c)) 101 + ) 102 + ) 103 + ); 103 104 104 105 app.get("/:id", (c) => 105 106 Effect.runPromise( 106 107 pipe( 107 108 parseParams(c), 108 109 Effect.flatMap(({ id }) => getInstanceState(id)), 109 - presentation(c), 110 - ), 111 - )); 110 + presentation(c) 111 + ) 112 + ) 113 + ); 112 114 113 115 app.delete("/:id", (c) => 114 116 Effect.runPromise( ··· 127 129 }) 128 130 ), 129 131 presentation(c), 130 - Effect.catchAll((error) => handleError(error, c)), 131 - ), 132 - )); 132 + Effect.catchAll((error) => handleError(error, c)) 133 + ) 134 + ) 135 + ); 133 136 134 137 app.post("/:id/start", (c) => 135 138 Effect.runPromise( 136 139 pipe( 137 140 Effect.all([parseParams(c), parseStartRequest(c)]), 138 - Effect.flatMap(( 139 - [{ id }, startRequest], 140 - ) => Effect.all([findVm(id), Effect.succeed(startRequest)])), 141 + Effect.flatMap(([{ id }, startRequest]) => 142 + Effect.all([findVm(id), Effect.succeed(startRequest)]) 143 + ), 141 144 Effect.flatMap(([vm, startRequest]) => 142 145 Effect.gen(function* () { 143 146 yield* failIfVMRunning(vm); 147 + const volume = yield* getVolume(vm.drivePath || ""); 144 148 const firmwareArgs = yield* setupFirmware(); 145 - const qemuArgs = yield* buildQemuArgs({ 146 - ...vm, 147 - cpu: String(startRequest.cpu ?? vm.cpu), 148 - cpus: startRequest.cpus ?? vm.cpus, 149 - memory: startRequest.memory ?? vm.memory, 150 - portForward: startRequest.portForward 151 - ? startRequest.portForward.join(",") 152 - : vm.portForward, 153 - }, firmwareArgs); 149 + vm.volume = volume ? volume.path : undefined; 150 + const qemuArgs = yield* buildQemuArgs( 151 + { 152 + ...vm, 153 + cpu: String(startRequest.cpu ?? vm.cpu), 154 + cpus: startRequest.cpus ?? vm.cpus, 155 + memory: startRequest.memory ?? vm.memory, 156 + portForward: startRequest.portForward 157 + ? startRequest.portForward.join(",") 158 + : vm.portForward, 159 + }, 160 + firmwareArgs 161 + ); 154 162 yield* createLogsDir(); 155 163 yield* startDetachedQemu(vm.id, vm, qemuArgs); 156 164 return { ...vm, status: "RUNNING" }; 157 165 }) 158 166 ), 159 167 presentation(c), 160 - Effect.catchAll((error) => handleError(error, c)), 161 - ), 162 - )); 168 + Effect.catchAll((error) => handleError(error, c)) 169 + ) 170 + ) 171 + ); 163 172 164 173 app.post("/:id/stop", (c) => 165 174 Effect.runPromise( ··· 169 178 Effect.flatMap(killProcess), 170 179 Effect.flatMap(updateToStopped), 171 180 presentation(c), 172 - Effect.catchAll((error) => handleError(error, c)), 173 - ), 174 - )); 181 + Effect.catchAll((error) => handleError(error, c)) 182 + ) 183 + ) 184 + ); 175 185 176 186 app.post("/:id/restart", (c) => 177 187 Effect.runPromise( ··· 181 191 Effect.flatMap(killProcess), 182 192 Effect.flatMap(updateToStopped), 183 193 Effect.flatMap(() => Effect.all([parseParams(c), parseStartRequest(c)])), 184 - Effect.flatMap(( 185 - [{ id }, startRequest], 186 - ) => Effect.all([findVm(id), Effect.succeed(startRequest)])), 194 + Effect.flatMap(([{ id }, startRequest]) => 195 + Effect.all([findVm(id), Effect.succeed(startRequest)]) 196 + ), 187 197 Effect.flatMap(([vm, startRequest]) => 188 198 Effect.gen(function* () { 189 199 const firmwareArgs = yield* setupFirmware(); 190 - const qemuArgs = yield* buildQemuArgs({ 191 - ...vm, 192 - cpu: String(startRequest.cpus ?? vm.cpu), 193 - memory: startRequest.memory ?? vm.memory, 194 - portForward: startRequest.portForward 195 - ? startRequest.portForward.join(",") 196 - : vm.portForward, 197 - }, firmwareArgs); 200 + const volume = yield* getVolume(vm.drivePath || ""); 201 + vm.volume = volume ? volume.path : undefined; 202 + const qemuArgs = yield* buildQemuArgs( 203 + { 204 + ...vm, 205 + cpu: String(startRequest.cpus ?? vm.cpu), 206 + memory: startRequest.memory ?? vm.memory, 207 + portForward: startRequest.portForward 208 + ? startRequest.portForward.join(",") 209 + : vm.portForward, 210 + }, 211 + firmwareArgs 212 + ); 198 213 yield* createLogsDir(); 199 214 yield* startDetachedQemu(vm.id, vm, qemuArgs); 200 215 return { ...vm, status: "RUNNING" }; 201 216 }) 202 217 ), 203 218 presentation(c), 204 - Effect.catchAll((error) => handleError(error, c)), 205 - ), 206 - )); 219 + Effect.catchAll((error) => handleError(error, c)) 220 + ) 221 + ) 222 + ); 207 223 208 224 export default app;