A Docker-like CLI and HTTP API for managing headless VMs
1import { createId } from "@paralleldrive/cuid2";
2import { Effect, pipe } from "effect";
3import { Hono } from "hono";
4import type { VirtualMachine } from "../db.ts";
5import { ImageNotFoundError, VmNotFoundError } from "../errors.ts";
6import { deleteImage, getImage, listImages, saveImage } from "../images.ts";
7import { getInstanceState } from "../state.ts";
8import { du, extractTag } from "../utils.ts";
9import {
10 handleError,
11 parseCreateImageRequest,
12 parseParams,
13 presentation,
14} from "./utils.ts";
15
16const app = new Hono();
17
18const failIfNoVM = ([vm, tag]: [VirtualMachine | undefined, string]) =>
19 Effect.gen(function* () {
20 if (!vm) {
21 return yield* Effect.fail(new VmNotFoundError({ name: "unknown" }));
22 }
23 if (!vm.drivePath) {
24 return yield* Effect.fail(new ImageNotFoundError({ id: "unknown" }));
25 }
26
27 const size = yield* du(vm.drivePath);
28
29 return [vm, tag, size] as [VirtualMachine, string, number];
30 });
31
32app.get("/", (c) =>
33 Effect.runPromise(
34 pipe(
35 listImages(),
36 presentation(c),
37 Effect.catchAll((error) => handleError(error, c)),
38 ),
39 ));
40
41app.get("/:id", (c) =>
42 Effect.runPromise(
43 pipe(
44 parseParams(c),
45 Effect.flatMap(({ id }) => getImage(id)),
46 presentation(c),
47 Effect.catchAll((error) => handleError(error, c)),
48 ),
49 ));
50
51app.post("/", (c) =>
52 Effect.runPromise(
53 pipe(
54 parseCreateImageRequest(c),
55 Effect.flatMap(({ from, image }) =>
56 Effect.gen(function* () {
57 return yield* pipe(
58 Effect.all([getInstanceState(from), extractTag(image)]),
59 Effect.flatMap(failIfNoVM),
60 Effect.flatMap(([vm, tag, size]) =>
61 saveImage({
62 id: createId(),
63 repository: image.split(":")[0],
64 tag,
65 size,
66 path: vm.drivePath!,
67 format: vm.diskFormat,
68 })
69 ),
70 Effect.flatMap(() => getImage(image)),
71 );
72 })
73 ),
74 presentation(c),
75 Effect.catchAll((error) => handleError(error, c)),
76 ),
77 ));
78
79app.delete("/:id", (c) =>
80 Effect.runPromise(
81 pipe(
82 parseParams(c),
83 Effect.flatMap(({ id }) => deleteImage(id)),
84 presentation(c),
85 Effect.catchAll((error) => handleError(error, c)),
86 ),
87 ));
88
89export default app;