A Docker-like CLI and HTTP API for managing headless VMs
at main 4.6 kB view raw
1import { Effect } from "effect"; 2import type { Context } from "hono"; 3import type { Image, Volume } from "../db.ts"; 4import { 5 type CommandError, 6 ImageNotFoundError, 7 ParseRequestError, 8 RemoveRunningVmError, 9 StopCommandError, 10 VmAlreadyRunningError, 11 VmNotFoundError, 12} from "../errors.ts"; 13import { 14 MachineParamsSchema, 15 NewImageSchema, 16 NewMachineSchema, 17 NewVolumeSchema, 18} from "../types.ts"; 19import { createVolume, getVolume } from "../volumes.ts"; 20import type { FileSystemError, XorrisoError } from "../xorriso.ts"; 21 22export const parseQueryParams = (c: Context) => Effect.succeed(c.req.query()); 23 24export const parseParams = (c: Context) => Effect.succeed(c.req.param()); 25 26const convertBigIntToNumber = (obj: unknown): unknown => { 27 if (typeof obj === "bigint") { 28 return Number(obj); 29 } 30 if (Array.isArray(obj)) { 31 return obj.map(convertBigIntToNumber); 32 } 33 if (obj !== null && typeof obj === "object") { 34 return Object.fromEntries( 35 Object.entries(obj).map(([key, value]) => [ 36 key, 37 convertBigIntToNumber(value), 38 ]), 39 ); 40 } 41 return obj; 42}; 43 44export const presentation = (c: Context) => 45 Effect.flatMap((data) => Effect.succeed(c.json(convertBigIntToNumber(data)))); 46 47export const handleError = ( 48 error: 49 | VmNotFoundError 50 | StopCommandError 51 | CommandError 52 | ParseRequestError 53 | VmAlreadyRunningError 54 | ImageNotFoundError 55 | RemoveRunningVmError 56 | FileSystemError 57 | XorrisoError 58 | Error, 59 c: Context, 60) => 61 Effect.sync(() => { 62 if (error instanceof VmNotFoundError) { 63 return c.json({ message: "VM not found", code: "VM_NOT_FOUND" }, 404); 64 } 65 if (error instanceof StopCommandError) { 66 return c.json( 67 { 68 message: error.message || `Failed to stop VM ${error.vmName}`, 69 code: "STOP_COMMAND_ERROR", 70 }, 71 500, 72 ); 73 } 74 75 if (error instanceof ParseRequestError) { 76 return c.json( 77 { 78 message: error.message || "Failed to parse request body", 79 code: "PARSE_BODY_ERROR", 80 }, 81 400, 82 ); 83 } 84 85 if (error instanceof VmAlreadyRunningError) { 86 return c.json( 87 { 88 message: `VM ${error.name} is already running`, 89 code: "VM_ALREADY_RUNNING", 90 }, 91 400, 92 ); 93 } 94 95 if (error instanceof ImageNotFoundError) { 96 return c.json( 97 { 98 message: `Image ${error.id} not found`, 99 code: "IMAGE_NOT_FOUND", 100 }, 101 404, 102 ); 103 } 104 105 if (error instanceof RemoveRunningVmError) { 106 return c.json( 107 { 108 message: 109 `Cannot remove running VM with ID ${error.id}. Please stop it first.`, 110 code: "REMOVE_RUNNING_VM_ERROR", 111 }, 112 400, 113 ); 114 } 115 116 return c.json( 117 { message: error instanceof Error ? error.message : String(error) }, 118 500, 119 ); 120 }); 121 122export const parseStartRequest = (c: Context) => 123 Effect.tryPromise({ 124 try: async () => { 125 const body = await c.req.json(); 126 return MachineParamsSchema.parse(body); 127 }, 128 catch: (error) => 129 new ParseRequestError({ 130 cause: error, 131 message: error instanceof Error ? error.message : String(error), 132 }), 133 }); 134 135export const parseCreateMachineRequest = (c: Context) => 136 Effect.tryPromise({ 137 try: async () => { 138 const body = await c.req.json(); 139 return NewMachineSchema.parse(body); 140 }, 141 catch: (error) => 142 new ParseRequestError({ 143 cause: error, 144 message: error instanceof Error ? error.message : String(error), 145 }), 146 }); 147 148export const parseCreateImageRequest = (c: Context) => 149 Effect.tryPromise({ 150 try: async () => { 151 const body = await c.req.json(); 152 return NewImageSchema.parse(body); 153 }, 154 catch: (error) => 155 new ParseRequestError({ 156 cause: error, 157 message: error instanceof Error ? error.message : String(error), 158 }), 159 }); 160 161export const createVolumeIfNeeded = ( 162 image: Image, 163 volumeName: string, 164 size?: string, 165): Effect.Effect<Volume, Error, never> => 166 Effect.gen(function* () { 167 const volume = yield* getVolume(volumeName); 168 if (volume) { 169 return volume; 170 } 171 172 return yield* createVolume(volumeName, image, size); 173 }); 174 175export const parseCreateVolumeRequest = (c: Context) => 176 Effect.tryPromise({ 177 try: async () => { 178 const body = await c.req.json(); 179 return NewVolumeSchema.parse(body); 180 }, 181 catch: (error) => 182 new ParseRequestError({ 183 cause: error, 184 message: error instanceof Error ? error.message : String(error), 185 }), 186 });