refactor: switch from go to bun

This commit is contained in:
Nicolas Meienberger
2025-08-31 17:32:00 +02:00
parent a0be690eb9
commit a16fc37b44
76 changed files with 4283 additions and 4173 deletions

View File

@@ -0,0 +1,11 @@
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: "ironmount.db",
},
});

View File

@@ -0,0 +1,7 @@
CREATE TABLE `volumes_table` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`type` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`);

View File

@@ -0,0 +1,4 @@
ALTER TABLE `volumes_table` ADD `path` text NOT NULL;--> statement-breakpoint
ALTER TABLE `volumes_table` ADD `created_at` integer DEFAULT (current_timestamp) NOT NULL;--> statement-breakpoint
ALTER TABLE `volumes_table` ADD `updated_at` integer DEFAULT (current_timestamp) NOT NULL;--> statement-breakpoint
ALTER TABLE `volumes_table` ADD `config` text NOT NULL;

View File

@@ -0,0 +1,57 @@
{
"version": "6",
"dialect": "sqlite",
"id": "f4a793d2-279b-4688-bbe3-2b69673fa7fa",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"volumes_table": {
"name": "volumes_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"volumes_table_name_unique": {
"name": "volumes_table_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,87 @@
{
"version": "6",
"dialect": "sqlite",
"id": "004e25a0-ecda-4b1a-aeab-46c8f78d5275",
"prevId": "f4a793d2-279b-4688-bbe3-2b69673fa7fa",
"tables": {
"volumes_table": {
"name": "volumes_table",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(current_timestamp)"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(current_timestamp)"
},
"config": {
"name": "config",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"volumes_table_name_unique": {
"name": "volumes_table_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1755765658194,
"tag": "0000_known_madelyne_pryor",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1755775437391,
"tag": "0001_far_frank_castle",
"breakpoints": true
}
]
}

21
apps/server/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "@ironmount/server",
"type": "module",
"scripts": {
"dev": "bun run --hot src/index.ts",
"gen:migrations": "drizzle-kit generate"
},
"dependencies": {
"@hono/arktype-validator": "^2.0.1",
"@scalar/hono-api-reference": "^0.9.13",
"arktype": "^2.1.20",
"dotenv": "^17.2.1",
"drizzle-orm": "^0.44.4",
"hono": "^4.9.2",
"hono-openapi": "^0.4.8"
},
"devDependencies": {
"@types/bun": "^1.2.20",
"drizzle-kit": "^0.31.4"
}
}

View File

@@ -0,0 +1,58 @@
import { Hono } from "hono";
export const driverController = new Hono()
.post("/VolumeDriver.Capabilities", (c) => {
return c.json({
Capabilities: {
Scope: "global",
},
});
})
.post("/Plugin.Activate", (c) => {
return c.json({
Implements: ["VolumeDriver"],
});
})
.post("/VolumeDriver.Create", (c) => {
return c.json({
Err: "",
});
})
.post("/VolumeDriver.Remove", (c) => {
return c.json({
Err: "",
});
})
.post("/VolumeDriver.Mount", (c) => {
return c.json({
Mountpoint: `/mnt/something`,
});
})
.post("/VolumeDriver.Unmount", (c) => {
return c.json({
Err: "",
});
})
.post("/VolumeDriver.Path", (c) => {
return c.json({
Mountpoint: `/mnt/something`,
});
})
.post("/VolumeDriver.Get", (c) => {
return c.json({
Name: "my-volume",
Mountpoint: `/mnt/something`,
Status: {},
});
})
.post("/VolumeDriver.List", (c) => {
return c.json({
Volumes: [
{
Name: "my-volume",
Mountpoint: `/mnt/something`,
Status: {},
},
],
});
});

View File

@@ -0,0 +1,31 @@
import { type } from "arktype";
import { Hono } from "hono";
import {
listVolumesDescriptor,
listVolumesResponse,
} from "../descriptors/volume.descriptors";
export const volumeController = new Hono()
.get("/", listVolumesDescriptor, (c) => {
const res = listVolumesResponse({
volumes: [],
});
if (res instanceof type.errors) {
return c.json({ error: "Invalid response format" }, 500);
}
return c.json(res, 200);
})
.post("/", (c) => {
return c.json({ message: "Create a new volume" }, 201);
})
.get("/:name", (c) => {
return c.json({ message: `Details of volume ${c.req.param("name")}` });
})
.put("/:name", (c) => {
return c.json({ message: `Update volume ${c.req.param("name")}` });
})
.delete("/:name", (c) => {
return c.json({ message: `Delete volume ${c.req.param("name")}` });
});

View File

@@ -0,0 +1,25 @@
import { type } from "arktype";
import "dotenv/config";
const envSchema = type({
NODE_ENV: type
.enumerated("development", "production", "test")
.default("development"),
DB_FILE_NAME: "string",
}).pipe((s) => ({
__prod__: s.NODE_ENV === "production",
environment: s.NODE_ENV,
dbFileName: s.DB_FILE_NAME,
}));
const parseConfig = (env: unknown) => {
const result = envSchema(env);
if (result instanceof type.errors) {
throw new Error(`Invalid environment variables: ${result.toString()}`);
}
return result;
};
export const config = parseConfig(process.env);

14
apps/server/src/db/db.ts Normal file
View File

@@ -0,0 +1,14 @@
import "dotenv/config";
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import { config } from "../core/config";
import * as schema from "./schema";
const sqlite = new Database(config.dbFileName);
export const db = drizzle({ client: sqlite, schema });
export const runDbMigrations = () => {
migrate(db, { migrationsFolder: "./drizzle" });
};

View File

@@ -0,0 +1,35 @@
import { type } from "arktype";
import { sql } from "drizzle-orm";
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
const nfsConfigSchema = type({
backend: "'nfs'",
server: "string",
exportPath: "string",
port: "number",
version: type.enumerated(["3", "4"]),
});
const smbConfigSchema = type({
backend: "'smb'",
});
const directoryConfigSchema = type({
backend: "'directory'",
});
const configSchema = nfsConfigSchema
.or(smbConfigSchema)
.or(directoryConfigSchema);
export const volumesTable = sqliteTable("volumes_table", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull().unique(),
path: text().notNull(),
type: text().notNull(),
createdAt: int("created_at").notNull().default(sql`(current_timestamp)`),
updatedAt: int("updated_at").notNull().default(sql`(current_timestamp)`),
config: text("config", { mode: "json" })
.$type<typeof configSchema.inferOut>()
.notNull(),
});

View File

@@ -0,0 +1,23 @@
import { Scalar } from "@scalar/hono-api-reference";
import type { Hono } from "hono";
import { openAPISpecs } from "hono-openapi";
export const generalDescriptor = (app: Hono) =>
openAPISpecs(app, {
documentation: {
info: {
title: "Ironmount API",
version: "1.0.0",
description: "API for managing Docker volumes",
},
servers: [
{ url: "http://localhost:3000", description: "Development Server" },
],
},
});
export const scalarDescriptor = Scalar({
title: "Ironmount API Docs",
pageTitle: "Ironmount API Docs",
url: "/api/v1/openapi.json",
});

View File

@@ -0,0 +1,26 @@
import { type } from "arktype";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/arktype";
export const listVolumesResponse = type({
volumes: type({
name: "string",
mountpoint: "string",
createdAt: "string",
}).array(),
});
export const listVolumesDescriptor = describeRoute({
description: "List all volumes",
tags: ["Volumes"],
responses: {
200: {
description: "A list of volumes",
content: {
"application/json": {
schema: resolver(listVolumesResponse),
},
},
},
},
});

48
apps/server/src/index.ts Normal file
View File

@@ -0,0 +1,48 @@
import * as fs from "node:fs/promises";
import { Hono } from "hono";
import { logger } from "hono/logger";
import { driverController } from "./controllers/driver.controller";
import { volumeController } from "./controllers/volume.controller";
import { runDbMigrations } from "./db/db";
import {
generalDescriptor,
scalarDescriptor,
} from "./descriptors/general.descriptors";
const driver = new Hono().use(logger()).route("/", driverController);
const app = new Hono()
.use(logger())
.get("healthcheck", (c) => c.json({ status: "ok" }))
.basePath("/api/v1")
.route("/volumes", volumeController);
app.get("/openapi.json", generalDescriptor(app));
app.get("/docs", scalarDescriptor);
app.get("/", (c) => {
return c.json({ message: "Welcome to the Ironmount API" });
});
const socketPath = "/run/docker/plugins/ironmount.sock";
(async () => {
await fs.mkdir("/run/docker/plugins", { recursive: true });
runDbMigrations();
Bun.serve({
unix: socketPath,
fetch: driver.fetch,
});
Bun.serve({
port: 8080,
fetch: app.fetch,
});
console.log(
`Server is running at http://localhost:8080 and unix socket at ${socketPath}`,
);
})();
export type AppType = typeof app;

View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}