From 75a73da136e7e8b7652b99321da348dbbc4f0d4c Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Sat, 22 Nov 2025 13:37:34 +0300 Subject: [PATCH] feat: refactor error handling and improve image management functionality --- main.ts | 2 +- src/api/images.ts | 75 ++++++++++++++++++++---- src/api/machines.ts | 13 +---- src/api/utils.ts | 51 ++++++++++++---- src/api/volumes.ts | 20 +++---- src/config.ts | 7 +-- src/errors.ts | 115 +++++++++++++++++++++++++++++++++++++ src/images.ts | 46 +++++++-------- src/network.ts | 15 +---- src/oras.ts | 27 ++------- src/state.ts | 53 ++++++----------- src/subcommands/inspect.ts | 17 ++---- src/subcommands/logs.ts | 17 +----- src/subcommands/ps.ts | 7 +-- src/subcommands/restart.ts | 15 +---- src/subcommands/rm.ts | 11 +--- src/subcommands/run.ts | 3 +- src/subcommands/start.ts | 21 ++----- src/subcommands/stop.ts | 35 ++++------- src/subcommands/volume.ts | 8 +-- src/types.ts | 16 +++++- src/utils.ts | 25 +++----- src/volumes.ts | 7 +-- 23 files changed, 338 insertions(+), 268 deletions(-) create mode 100644 src/errors.ts diff --git a/main.ts b/main.ts index b07c34a..7642eae 100755 --- a/main.ts +++ b/main.ts @@ -9,6 +9,7 @@ import { Effect, pipe } from "effect"; import pkg from "./deno.json" with { type: "json" }; import { initVmFile, mergeConfig, parseVmFile } from "./src/config.ts"; import { CONFIG_FILE_NAME } from "./src/constants.ts"; +import { NoSuchFileError } from "./src/errors.ts"; import { getImage } from "./src/images.ts"; import { constructCoreOSImageURL } from "./src/mod.ts"; import { createBridgeNetworkIfNeeded } from "./src/network.ts"; @@ -46,7 +47,6 @@ import { extractXz, fileExists, isValidISOurl, - NoSuchFileError, type Options, runQemu, } from "./src/utils.ts"; diff --git a/src/api/images.ts b/src/api/images.ts index b707f20..46405f5 100644 --- a/src/api/images.ts +++ b/src/api/images.ts @@ -1,15 +1,40 @@ -import { Hono } from "hono"; +import { createId } from "@paralleldrive/cuid2"; import { Effect, pipe } from "effect"; -import { parseParams, presentation } from "./utils.ts"; -import { getImage, listImages } from "../images.ts"; +import { Hono } from "hono"; +import type { VirtualMachine } from "../db.ts"; +import { ImageNotFoundError, VmNotFoundError } from "../errors.ts"; +import { deleteImage, getImage, listImages, saveImage } from "../images.ts"; +import { getInstanceState } from "../state.ts"; +import { du, extractTag } from "../utils.ts"; +import { + handleError, + parseCreateImageRequest, + parseParams, + presentation, +} from "./utils.ts"; const app = new Hono(); +const failIfNoVM = ([vm, tag]: [VirtualMachine | undefined, string]) => + Effect.gen(function* () { + if (!vm) { + return yield* Effect.fail(new VmNotFoundError({ name: "unknown" })); + } + if (!vm.drivePath) { + return yield* Effect.fail(new ImageNotFoundError({ id: "unknown" })); + } + + const size = yield* du(vm.drivePath); + + return [vm, tag, size] as [VirtualMachine, string, number]; + }); + app.get("/", (c) => Effect.runPromise( pipe( listImages(), presentation(c), + Effect.catchAll((error) => handleError(error, c)), ), )); @@ -19,16 +44,46 @@ app.get("/:id", (c) => parseParams(c), Effect.flatMap(({ id }) => getImage(id)), presentation(c), + Effect.catchAll((error) => handleError(error, c)), ), )); -app.post("/", (c) => { - return c.json({ message: "New image created" }); -}); +app.post("/", (c) => + Effect.runPromise( + pipe( + parseCreateImageRequest(c), + Effect.flatMap(({ from, image }) => + Effect.gen(function* () { + return yield* pipe( + Effect.all([getInstanceState(from), extractTag(image)]), + Effect.flatMap(failIfNoVM), + Effect.flatMap(([vm, tag, size]) => + saveImage({ + id: createId(), + repository: image.split(":")[0], + tag, + size, + path: vm.drivePath!, + format: vm.diskFormat, + }) + ), + Effect.flatMap(() => getImage(image)), + ); + }) + ), + presentation(c), + Effect.catchAll((error) => handleError(error, c)), + ), + )); -app.delete("/:id", (c) => { - const { id } = c.req.param(); - return c.json({ message: `Image with ID ${id} deleted` }); -}); +app.delete("/:id", (c) => + Effect.runPromise( + pipe( + parseParams(c), + Effect.flatMap(({ id }) => deleteImage(id)), + presentation(c), + Effect.catchAll((error) => handleError(error, c)), + ), + )); export default app; diff --git a/src/api/machines.ts b/src/api/machines.ts index 38ec9ad..60d481f 100644 --- a/src/api/machines.ts +++ b/src/api/machines.ts @@ -1,9 +1,10 @@ import _ from "@es-toolkit/es-toolkit/compat"; import { createId } from "@paralleldrive/cuid2"; -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import { Hono } from "hono"; import Moniker from "moniker"; import { SEED_DIR } from "../constants.ts"; +import { ImageNotFoundError, RemoveRunningVmError } from "../errors.ts"; import { getImage } from "../images.ts"; import { DEFAULT_VERSION, getInstanceState } from "../mod.ts"; import { generateRandomMacAddress } from "../network.ts"; @@ -33,16 +34,6 @@ import { presentation, } from "./utils.ts"; -export class ImageNotFoundError extends Data.TaggedError("ImageNotFoundError")<{ - id: string; -}> {} - -export class RemoveRunningVmError extends Data.TaggedError( - "RemoveRunningVmError", -)<{ - id: string; -}> {} - const app = new Hono(); app.get("/", (c) => diff --git a/src/api/utils.ts b/src/api/utils.ts index 7bfb2e2..2b7c519 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -1,32 +1,48 @@ -import { Data, Effect } from "effect"; +import { Effect } from "effect"; import type { Context } from "hono"; import type { Image, Volume } from "../db.ts"; -import { VmAlreadyRunningError } from "../subcommands/start.ts"; import { type CommandError, + ImageNotFoundError, + ParseRequestError, + RemoveRunningVmError, StopCommandError, + VmAlreadyRunningError, VmNotFoundError, -} from "../subcommands/stop.ts"; +} from "../errors.ts"; import { MachineParamsSchema, + NewImageSchema, NewMachineSchema, NewVolumeSchema, } from "../types.ts"; import { createVolume, getVolume } from "../volumes.ts"; -import { FileSystemError, XorrisoError } from "../xorriso.ts"; -import { ImageNotFoundError, RemoveRunningVmError } from "./machines.ts"; +import type { FileSystemError, XorrisoError } from "../xorriso.ts"; export const parseQueryParams = (c: Context) => Effect.succeed(c.req.query()); export const parseParams = (c: Context) => Effect.succeed(c.req.param()); -export const presentation = (c: Context) => - Effect.flatMap((data) => Effect.succeed(c.json(data))); +const convertBigIntToNumber = (obj: unknown): unknown => { + if (typeof obj === "bigint") { + return Number(obj); + } + if (Array.isArray(obj)) { + return obj.map(convertBigIntToNumber); + } + if (obj !== null && typeof obj === "object") { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [ + key, + convertBigIntToNumber(value), + ]), + ); + } + return obj; +}; -export class ParseRequestError extends Data.TaggedError("ParseRequestError")<{ - cause?: unknown; - message: string; -}> {} +export const presentation = (c: Context) => + Effect.flatMap((data) => Effect.succeed(c.json(convertBigIntToNumber(data)))); export const handleError = ( error: @@ -129,6 +145,19 @@ export const parseCreateMachineRequest = (c: Context) => }), }); +export const parseCreateImageRequest = (c: Context) => + Effect.tryPromise({ + try: async () => { + const body = await c.req.json(); + return NewImageSchema.parse(body); + }, + catch: (error) => + new ParseRequestError({ + cause: error, + message: error instanceof Error ? error.message : String(error), + }), + }); + export const createVolumeIfNeeded = ( image: Image, volumeName: string, diff --git a/src/api/volumes.ts b/src/api/volumes.ts index 3971782..cf195a5 100644 --- a/src/api/volumes.ts +++ b/src/api/volumes.ts @@ -1,5 +1,10 @@ -import { Hono } from "hono"; import { Effect, pipe } from "effect"; +import { Hono } from "hono"; +import { ImageNotFoundError } from "../errors.ts"; +import { getImage } from "../images.ts"; +import { listVolumes } from "../mod.ts"; +import type { NewVolume } from "../types.ts"; +import { deleteVolume, getVolume } from "../volumes.ts"; import { createVolumeIfNeeded, handleError, @@ -7,21 +12,10 @@ import { parseParams, presentation, } from "./utils.ts"; -import { listVolumes } from "../mod.ts"; -import { deleteVolume, getVolume } from "../volumes.ts"; -import type { NewVolume } from "../types.ts"; -import { getImage } from "../images.ts"; -import { ImageNotFoundError } from "./machines.ts"; const app = new Hono(); -app.get("/", (c) => - Effect.runPromise( - pipe( - listVolumes(), - presentation(c), - ), - )); +app.get("/", (c) => Effect.runPromise(pipe(listVolumes(), presentation(c)))); app.get("/:id", (c) => Effect.runPromise( diff --git a/src/config.ts b/src/config.ts index 19aefc6..5b4f358 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,8 +2,9 @@ import { parseFlags } from "@cliffy/flags"; import _ from "@es-toolkit/es-toolkit/compat"; import * as toml from "@std/toml"; import z from "@zod/zod"; -import { Data, Effect } from "effect"; +import { Effect } from "effect"; import { UBUNTU_ISO_URL } from "./constants.ts"; +import { VmConfigError } from "./errors.ts"; import type { Options } from "./utils.ts"; export const VmConfigSchema = z.object({ @@ -34,10 +35,6 @@ export const VmConfigSchema = z.object({ export type VmConfig = z.infer; -class VmConfigError extends Data.TaggedError("VmConfigError")<{ - cause?: string; -}> {} - export const initVmFile = ( path: string, ): Effect.Effect => diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..db6abbb --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,115 @@ +import { Data } from "effect"; + +// API Errors +export class ImageNotFoundError extends Data.TaggedError("ImageNotFoundError")<{ + id: string; +}> {} + +export class RemoveRunningVmError extends Data.TaggedError( + "RemoveRunningVmError", +)<{ + id: string; +}> {} + +export class ParseRequestError extends Data.TaggedError("ParseRequestError")<{ + cause?: unknown; + message: string; +}> {} + +// Config Errors +export class VmConfigError extends Data.TaggedError("VmConfigError")<{ + cause?: string; +}> {} + +// Volume Errors +export class VolumeError extends Data.TaggedError("VolumeError")<{ + message?: unknown; +}> {} + +// ORAS/Image Registry Errors +export class PushImageError extends Data.TaggedError("PushImageError")<{ + cause?: unknown; +}> {} + +export class PullImageError extends Data.TaggedError("PullImageError")<{ + cause?: unknown; +}> {} + +export class CreateDirectoryError extends Data.TaggedError( + "CreateDirectoryError", +)<{ + cause?: unknown; +}> {} + +export class ImageAlreadyPulledError extends Data.TaggedError( + "ImageAlreadyPulledError", +)<{ + name: string; +}> {} + +// Database Errors +export class DbError extends Data.TaggedError("DatabaseError")<{ + message?: string; + cause?: unknown; +}> {} + +export class DbQueryError extends Data.TaggedError("DbQueryError")<{ + cause?: unknown; +}> {} + +// Network Errors +export class NetworkError extends Data.TaggedError("NetworkError")<{ + cause?: unknown; +}> {} + +export class BridgeSetupError extends Data.TaggedError("BridgeSetupError")<{ + cause?: unknown; +}> {} + +// VM Operation Errors +export class VmNotFoundError extends Data.TaggedError("VmNotFoundError")<{ + name: string; +}> {} + +export class VmAlreadyRunningError extends Data.TaggedError( + "VmAlreadyRunningError", +)<{ + name: string; +}> {} + +export class StopCommandError extends Data.TaggedError("StopCommandError")<{ + vmName: string; + exitCode: number; + message?: string; +}> {} + +export class KillQemuError extends Data.TaggedError("KillQemuError")<{ + vmName: string; +}> {} + +export class CommandError extends Data.TaggedError("CommandError")<{ + cause?: unknown; +}> {} + +// Log Errors +export class LogCommandError extends Data.TaggedError("LogCommandError")<{ + vmName?: string; + exitCode?: number; + cause?: unknown; +}> {} + +// Image/File Errors +export class InvalidImageNameError extends Data.TaggedError( + "InvalidImageNameError", +)<{ + image: string; + cause?: unknown; +}> {} + +export class NoSuchImageError extends Data.TaggedError("NoSuchImageError")<{ + cause: string; +}> {} + +export class NoSuchFileError extends Data.TaggedError("NoSuchFileError")<{ + cause: string; +}> {} diff --git a/src/images.ts b/src/images.ts index 45dc1bc..fdef453 100644 --- a/src/images.ts +++ b/src/images.ts @@ -1,11 +1,8 @@ -import { Data, Effect } from "effect"; +import { Effect } from "effect"; import type { DeleteResult, InsertResult } from "kysely"; import { ctx } from "./context.ts"; import type { Image } from "./db.ts"; - -export class DbError extends Data.TaggedError("DatabaseError")<{ - message?: string; -}> {} +import { DbError } from "./errors.ts"; export const listImages = (): Effect.Effect => Effect.tryPromise({ @@ -46,18 +43,16 @@ export const saveImage = ( ): Effect.Effect => Effect.tryPromise({ try: () => - ctx.db.insertInto("images") + ctx.db + .insertInto("images") .values(image) .onConflict((oc) => - oc - .column("repository") - .column("tag") - .doUpdateSet({ - size: image.size, - path: image.path, - format: image.format, - digest: image.digest, - }) + oc.column("repository").column("tag").doUpdateSet({ + size: image.size, + path: image.path, + format: image.format, + digest: image.digest, + }) ) .execute(), catch: (error) => @@ -71,15 +66,18 @@ export const deleteImage = ( ): Effect.Effect => Effect.tryPromise({ try: () => - ctx.db.deleteFrom("images").where((eb) => - eb.or([ - eb.and([ - eb("repository", "=", id.split(":")[0]), - eb("tag", "=", id.split(":")[1] || "latest"), - ]), - eb("id", "=", id), - ]) - ).execute(), + ctx.db + .deleteFrom("images") + .where((eb) => + eb.or([ + eb.and([ + eb("repository", "=", id.split(":")[0]), + eb("tag", "=", id.split(":")[1] || "latest"), + ]), + eb("id", "=", id), + ]) + ) + .execute(), catch: (error) => new DbError({ message: error instanceof Error ? error.message : String(error), diff --git a/src/network.ts b/src/network.ts index 77c9044..1f8556b 100644 --- a/src/network.ts +++ b/src/network.ts @@ -1,13 +1,6 @@ import chalk from "chalk"; -import { Data, Effect } from "effect"; - -export class NetworkError extends Data.TaggedError("NetworkError")<{ - cause?: unknown; -}> {} - -export class BridgeSetupError extends Data.TaggedError("BridgeSetupError")<{ - cause?: unknown; -}> {} +import { Effect } from "effect"; +import { BridgeSetupError, NetworkError } from "./errors.ts"; export const setupQemuBridge = (bridgeName: string) => Effect.tryPromise({ @@ -61,9 +54,7 @@ export const setupQemuBridge = (bridgeName: string) => catch: (error) => new BridgeSetupError({ cause: error }), }); -export const createBridgeNetworkIfNeeded = ( - bridgeName: string, -) => +export const createBridgeNetworkIfNeeded = (bridgeName: string) => Effect.tryPromise({ try: async () => { const bridgeExistsCmd = new Deno.Command("ip", { diff --git a/src/oras.ts b/src/oras.ts index db3c0ef..bcbe1ff 100644 --- a/src/oras.ts +++ b/src/oras.ts @@ -1,34 +1,19 @@ import { createId } from "@paralleldrive/cuid2"; import { basename, dirname } from "@std/path"; import chalk from "chalk"; -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import { IMAGE_DIR } from "./constants.ts"; +import { + ImageAlreadyPulledError, + PullImageError, + PushImageError, +} from "./errors.ts"; import { getImage, saveImage } from "./images.ts"; import { CONFIG_DIR, failOnMissingImage } from "./mod.ts"; import { du, getCurrentArch } from "./utils.ts"; const DEFAULT_ORAS_VERSION = "1.3.0"; -export class PushImageError extends Data.TaggedError("PushImageError")<{ - cause?: unknown; -}> {} - -export class PullImageError extends Data.TaggedError("PullImageError")<{ - cause?: unknown; -}> {} - -export class CreateDirectoryError extends Data.TaggedError( - "CreateDirectoryError", -)<{ - cause?: unknown; -}> {} - -export class ImageAlreadyPulledError extends Data.TaggedError( - "ImageAlreadyPulledError", -)<{ - name: string; -}> {} - export async function setupOrasBinary(): Promise { Deno.env.set("PATH", `${CONFIG_DIR}/bin:${Deno.env.get("PATH")}`); diff --git a/src/state.ts b/src/state.ts index aa3d65d..851ab50 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,20 +1,12 @@ -import { Data, Effect } from "effect"; +import { Effect } from "effect"; import { ctx } from "./context.ts"; import type { VirtualMachine } from "./db.ts"; +import { DbError } from "./errors.ts"; import type { STATUS } from "./types.ts"; -export class DbError extends Data.TaggedError("DatabaseError")<{ - cause?: unknown; -}> {} - -export const saveInstanceState = ( - vm: VirtualMachine, -) => +export const saveInstanceState = (vm: VirtualMachine) => Effect.tryPromise({ - try: () => - ctx.db.insertInto("virtual_machines") - .values(vm) - .execute(), + try: () => ctx.db.insertInto("virtual_machines").values(vm).execute(), catch: (error) => new DbError({ cause: error }), }); @@ -25,34 +17,24 @@ export const updateInstanceState = ( ) => Effect.tryPromise({ try: () => - ctx.db.updateTable("virtual_machines") + ctx.db + .updateTable("virtual_machines") .set({ status, pid, updatedAt: new Date().toISOString(), }) - .where((eb) => - eb.or([ - eb("name", "=", name), - eb("id", "=", name), - ]) - ) + .where((eb) => eb.or([eb("name", "=", name), eb("id", "=", name)])) .execute(), catch: (error) => new DbError({ cause: error }), }); -export const removeInstanceState = ( - name: string, -) => +export const removeInstanceState = (name: string) => Effect.tryPromise({ try: () => - ctx.db.deleteFrom("virtual_machines") - .where((eb) => - eb.or([ - eb("name", "=", name), - eb("id", "=", name), - ]) - ) + ctx.db + .deleteFrom("virtual_machines") + .where((eb) => eb.or([eb("name", "=", name), eb("id", "=", name)])) .execute(), catch: (error) => new DbError({ cause: error }), }); @@ -62,14 +44,10 @@ export const getInstanceState = ( ): Effect.Effect => Effect.tryPromise({ try: () => - ctx.db.selectFrom("virtual_machines") + ctx.db + .selectFrom("virtual_machines") .selectAll() - .where((eb) => - eb.or([ - eb("name", "=", name), - eb("id", "=", name), - ]) - ) + .where((eb) => eb.or([eb("name", "=", name), eb("id", "=", name)])) .executeTakeFirst(), catch: (error) => new DbError({ cause: error }), }); @@ -79,7 +57,8 @@ export const listInstances = ( ): Effect.Effect => Effect.tryPromise({ try: () => - ctx.db.selectFrom("virtual_machines") + ctx.db + .selectFrom("virtual_machines") .selectAll() .where((eb) => { if (all) { diff --git a/src/subcommands/inspect.ts b/src/subcommands/inspect.ts index 246ca0d..de89950 100644 --- a/src/subcommands/inspect.ts +++ b/src/subcommands/inspect.ts @@ -1,11 +1,8 @@ -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import type { VirtualMachine } from "../db.ts"; +import { VmNotFoundError } from "../errors.ts"; import { getInstanceState } from "../state.ts"; -class VmNotFoundError extends Data.TaggedError("VmNotFoundError")<{ - name: string; -}> {} - const findVm = (name: string) => pipe( getInstanceState(name), @@ -22,9 +19,7 @@ const displayVm = (vm: VirtualMachine) => const handleError = (error: VmNotFoundError | Error) => Effect.sync(() => { if (error instanceof VmNotFoundError) { - console.error( - `Virtual machine with name or ID ${error.name} not found.`, - ); + console.error(`Virtual machine with name or ID ${error.name} not found.`); } else { console.error(`An error occurred: ${error}`); } @@ -32,11 +27,7 @@ const handleError = (error: VmNotFoundError | Error) => }); const inspectEffect = (name: string) => - pipe( - findVm(name), - Effect.flatMap(displayVm), - Effect.catchAll(handleError), - ); + pipe(findVm(name), Effect.flatMap(displayVm), Effect.catchAll(handleError)); export default async function (name: string) { await Effect.runPromise(inspectEffect(name)); diff --git a/src/subcommands/logs.ts b/src/subcommands/logs.ts index a198591..e4e20f9 100644 --- a/src/subcommands/logs.ts +++ b/src/subcommands/logs.ts @@ -1,14 +1,6 @@ -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import { LOGS_DIR } from "../constants.ts"; - -class LogCommandError extends Data.TaggedError("LogCommandError")<{ - vmName: string; - exitCode: number; -}> {} - -class CommandError extends Data.TaggedError("CommandError")<{ - cause?: unknown; -}> {} +import { CommandError, LogCommandError } from "../errors.ts"; const createLogsDir = () => Effect.tryPromise({ @@ -23,10 +15,7 @@ const viewLogs = (name: string, follow: boolean, logPath: string) => Effect.tryPromise({ try: async () => { const cmd = new Deno.Command(follow ? "tail" : "cat", { - args: [ - ...(follow ? ["-n", "100", "-f"] : []), - logPath, - ], + args: [...(follow ? ["-n", "100", "-f"] : []), logPath], stdin: "inherit", stdout: "inherit", stderr: "inherit", diff --git a/src/subcommands/ps.ts b/src/subcommands/ps.ts index b0c2ec1..a992401 100644 --- a/src/subcommands/ps.ts +++ b/src/subcommands/ps.ts @@ -3,17 +3,14 @@ import { basename } from "@std/path"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime.js"; import utc from "dayjs/plugin/utc.js"; -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import { ctx } from "../context.ts"; import type { VirtualMachine } from "../db.ts"; +import { DbQueryError } from "../errors.ts"; dayjs.extend(relativeTime); dayjs.extend(utc); -class DbQueryError extends Data.TaggedError("DbQueryError")<{ - cause?: unknown; -}> {} - const fetchVMs = (all: boolean) => Effect.tryPromise({ try: () => diff --git a/src/subcommands/restart.ts b/src/subcommands/restart.ts index 23f9bc3..eea9bad 100644 --- a/src/subcommands/restart.ts +++ b/src/subcommands/restart.ts @@ -1,8 +1,9 @@ import _ from "@es-toolkit/es-toolkit/compat"; import chalk from "chalk"; -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import { LOGS_DIR } from "../constants.ts"; import type { VirtualMachine } from "../db.ts"; +import { CommandError, KillQemuError, VmNotFoundError } from "../errors.ts"; import { getInstanceState, updateInstanceState } from "../state.ts"; import { safeKillQemu, @@ -18,18 +19,6 @@ import { setupUbuntuArgs, } from "../utils.ts"; -class VmNotFoundError extends Data.TaggedError("VmNotFoundError")<{ - name: string; -}> {} - -class KillQemuError extends Data.TaggedError("KillQemuError")<{ - vmName: string; -}> {} - -class CommandError extends Data.TaggedError("CommandError")<{ - cause?: unknown; -}> {} - const findVm = (name: string) => pipe( getInstanceState(name), diff --git a/src/subcommands/rm.ts b/src/subcommands/rm.ts index e79cb7a..a905519 100644 --- a/src/subcommands/rm.ts +++ b/src/subcommands/rm.ts @@ -1,11 +1,8 @@ -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import type { VirtualMachine } from "../db.ts"; +import { VmNotFoundError } from "../errors.ts"; import { getInstanceState, removeInstanceState } from "../state.ts"; -class VmNotFoundError extends Data.TaggedError("VmNotFoundError")<{ - name: string; -}> {} - const findVm = (name: string) => pipe( getInstanceState(name), @@ -28,9 +25,7 @@ const removeVm = (name: string, vm: VirtualMachine) => const handleError = (error: VmNotFoundError | Error) => Effect.sync(() => { if (error instanceof VmNotFoundError) { - console.error( - `Virtual machine with name or ID ${error.name} not found.`, - ); + console.error(`Virtual machine with name or ID ${error.name} not found.`); } else { console.error(`An error occurred: ${error}`); } diff --git a/src/subcommands/run.ts b/src/subcommands/run.ts index 7c38bff..146c103 100644 --- a/src/subcommands/run.ts +++ b/src/subcommands/run.ts @@ -1,9 +1,10 @@ import { parseFlags } from "@cliffy/flags"; import { Effect, pipe } from "effect"; import type { Image, Volume } from "../db.ts"; +import { PullImageError } from "../errors.ts"; import { getImage } from "../images.ts"; import { createBridgeNetworkIfNeeded } from "../network.ts"; -import { pullImage, PullImageError, setupOrasBinary } from "../oras.ts"; +import { pullImage, setupOrasBinary } from "../oras.ts"; import { type Options, runQemu, validateImage } from "../utils.ts"; import { createVolume, getVolume } from "../volumes.ts"; diff --git a/src/subcommands/start.ts b/src/subcommands/start.ts index e788713..e29bf34 100644 --- a/src/subcommands/start.ts +++ b/src/subcommands/start.ts @@ -1,8 +1,13 @@ import { parseFlags } from "@cliffy/flags"; import _ from "@es-toolkit/es-toolkit/compat"; -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import { LOGS_DIR } from "../constants.ts"; import type { VirtualMachine, Volume } from "../db.ts"; +import { + CommandError, + VmAlreadyRunningError, + VmNotFoundError, +} from "../errors.ts"; import { getImage } from "../images.ts"; import { getInstanceState, updateInstanceState } from "../state.ts"; import { @@ -19,20 +24,6 @@ import { } from "../utils.ts"; import { createVolume, getVolume } from "../volumes.ts"; -export class VmNotFoundError extends Data.TaggedError("VmNotFoundError")<{ - name: string; -}> {} - -export class VmAlreadyRunningError extends Data.TaggedError( - "VmAlreadyRunningError", -)<{ - name: string; -}> {} - -export class CommandError extends Data.TaggedError("CommandError")<{ - cause?: unknown; -}> {} - const findVm = (name: string) => pipe( getInstanceState(name), diff --git a/src/subcommands/stop.ts b/src/subcommands/stop.ts index b0dcacd..028ec37 100644 --- a/src/subcommands/stop.ts +++ b/src/subcommands/stop.ts @@ -1,23 +1,10 @@ import _ from "@es-toolkit/es-toolkit/compat"; import chalk from "chalk"; -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import type { VirtualMachine } from "../db.ts"; +import { CommandError, StopCommandError, VmNotFoundError } from "../errors.ts"; import { getInstanceState, updateInstanceState } from "../state.ts"; -export class VmNotFoundError extends Data.TaggedError("VmNotFoundError")<{ - name: string; -}> {} - -export class StopCommandError extends Data.TaggedError("StopCommandError")<{ - vmName: string; - exitCode: number; - message?: string; -}> {} - -export class CommandError extends Data.TaggedError("CommandError")<{ - cause?: unknown; -}> {} - export const findVm = (name: string) => pipe( getInstanceState(name), @@ -29,9 +16,11 @@ export const findVm = (name: string) => export const logStopping = (vm: VirtualMachine) => Effect.sync(() => { console.log( - `Stopping virtual machine ${chalk.greenBright(vm.name)} (ID: ${ - chalk.greenBright(vm.id) - })...`, + `Stopping virtual machine ${ + chalk.greenBright( + vm.name, + ) + } (ID: ${chalk.greenBright(vm.id)})...`, ); }); @@ -39,11 +28,7 @@ export const killProcess = (vm: VirtualMachine) => Effect.tryPromise({ try: async () => { const cmd = new Deno.Command(vm.bridge ? "sudo" : "kill", { - args: [ - ..._.compact([vm.bridge && "kill"]), - "-TERM", - vm.pid.toString(), - ], + args: [..._.compact([vm.bridge && "kill"]), "-TERM", vm.pid.toString()], stdin: "inherit", stdout: "inherit", stderr: "inherit", @@ -84,7 +69,9 @@ const handleError = ( if (error instanceof VmNotFoundError) { console.error( `Virtual machine with name or ID ${ - chalk.greenBright(error.name) + chalk.greenBright( + error.name, + ) } not found.`, ); Deno.exit(1); diff --git a/src/subcommands/volume.ts b/src/subcommands/volume.ts index 5d121b5..362bf62 100644 --- a/src/subcommands/volume.ts +++ b/src/subcommands/volume.ts @@ -4,18 +4,14 @@ import relativeTime from "dayjs/plugin/relativeTime.js"; import utc from "dayjs/plugin/utc.js"; import { Effect, pipe } from "effect"; import type { Volume } from "../db.ts"; -import type { DbError } from "../mod.ts"; +import type { DbError } from "../errors.ts"; import { deleteVolume, getVolume, listVolumes } from "../volumes.ts"; dayjs.extend(relativeTime); dayjs.extend(utc); const createTable = () => - Effect.succeed( - new Table( - ["NAME", "VOLUME ID", "CREATED"], - ), - ); + Effect.succeed(new Table(["NAME", "VOLUME ID", "CREATED"])); const populateTable = (table: Table, volumes: Volume[]) => Effect.sync(() => { diff --git a/src/types.ts b/src/types.ts index 4930f3b..7e0f675 100644 --- a/src/types.ts +++ b/src/types.ts @@ -81,16 +81,30 @@ export const NewMachineSchema = MachineParamsSchema.extend({ export type NewMachine = z.infer; export const NewVolumeSchema = z.object({ - name: z.string(), + name: z.string().trim(), baseImage: z .string() + .trim() .regex( /^([a-zA-Z0-9\-\.]+\/)?([a-zA-Z0-9\-\.]+\/)?[a-zA-Z0-9\-\.]+(:[\w\.\-]+)?$/, ), size: z .string() + .trim() .regex(/^\d+(M|G|T)$/) .optional(), }); export type NewVolume = z.infer; + +export const NewImageSchema = z.object({ + from: z.string().trim(), + image: z + .string() + .trim() + .regex( + /^([a-zA-Z0-9\-\.]+\/)?([a-zA-Z0-9\-\.]+\/)?[a-zA-Z0-9\-\.]+(:[\w\.\-]+)?$/, + ), +}); + +export type NewImage = z.infer; diff --git a/src/utils.ts b/src/utils.ts index 0869263..9600c11 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,7 @@ import _ from "@es-toolkit/es-toolkit/compat"; import { createId } from "@paralleldrive/cuid2"; import { dirname } from "@std/path"; import chalk from "chalk"; -import { Data, Effect, pipe } from "effect"; +import { Effect, pipe } from "effect"; import Moniker from "moniker"; import { ALMA_LINUX_IMG_URL, @@ -25,6 +25,12 @@ import { UBUNTU_ISO_URL, } from "./constants.ts"; import type { Image } from "./db.ts"; +import { + InvalidImageNameError, + LogCommandError, + NoSuchFileError, + NoSuchImageError, +} from "./errors.ts"; import { generateRandomMacAddress } from "./network.ts"; import { saveInstanceState, updateInstanceState } from "./state.ts"; @@ -47,23 +53,6 @@ export interface Options { seed?: string; } -class LogCommandError extends Data.TaggedError("LogCommandError")<{ - cause?: unknown; -}> {} - -class InvalidImageNameError extends Data.TaggedError("InvalidImageNameError")<{ - image: string; - cause?: unknown; -}> {} - -class NoSuchImageError extends Data.TaggedError("NoSuchImageError")<{ - cause: string; -}> {} - -export class NoSuchFileError extends Data.TaggedError("NoSuchFileError")<{ - cause: string; -}> {} - export const getCurrentArch = (): string => { switch (Deno.build.arch) { case "x86_64": diff --git a/src/volumes.ts b/src/volumes.ts index 9218ff8..824d9e9 100644 --- a/src/volumes.ts +++ b/src/volumes.ts @@ -1,13 +1,10 @@ import { createId } from "@paralleldrive/cuid2"; -import { Data, Effect } from "effect"; +import { Effect } from "effect"; import type { DeleteResult, InsertResult } from "kysely"; import { VOLUME_DIR } from "./constants.ts"; import { ctx } from "./context.ts"; import type { Image, Volume } from "./db.ts"; - -export class VolumeError extends Data.TaggedError("VolumeError")<{ - message?: unknown; -}> {} +import { VolumeError } from "./errors.ts"; export const listVolumes = () => Effect.tryPromise({ -- 2.51.2