diff --git a/apps/backend/src/application/interfaces/user.service.interface.ts b/apps/backend/src/application/interfaces/user.service.interface.ts index 7929fbd..2bf6a7a 100644 --- a/apps/backend/src/application/interfaces/user.service.interface.ts +++ b/apps/backend/src/application/interfaces/user.service.interface.ts @@ -2,11 +2,8 @@ import type { UpdateSuspensionInput, UpdateUserInput, User } from "@minikura/db" export interface IUserService { getUserById(id: string): Promise; - getUserByEmail(email: string): Promise; getAllUsers(): Promise; updateUser(id: string, input: UpdateUserInput): Promise; updateSuspension(id: string, input: UpdateSuspensionInput): Promise; - suspendUser(id: string, suspendedUntil?: Date | null): Promise; - unsuspendUser(id: string): Promise; deleteUser(requestingUserId: string, targetUserId: string): Promise; } diff --git a/apps/backend/src/application/services/user.service.ts b/apps/backend/src/application/services/user.service.ts index eb15e94..5f04a9f 100644 --- a/apps/backend/src/application/services/user.service.ts +++ b/apps/backend/src/application/services/user.service.ts @@ -3,7 +3,7 @@ import { BusinessRuleError, NotFoundError } from "../../domain/errors/base.error import { UserSuspendedEvent, UserUnsuspendedEvent, -} from "../../domain/events/server-lifecycle.events"; +} from "../../domain/events/user-lifecycle.events"; import type { UserRepository } from "../../domain/repositories/user.repository"; import { eventBus } from "../../infrastructure/event-bus"; import type { IUserService } from "../interfaces/user.service.interface"; @@ -19,10 +19,6 @@ export class UserService implements IUserService { return user; } - async getUserByEmail(email: string): Promise { - return this.userRepo.findByEmail(email); - } - async getAllUsers(): Promise { return this.userRepo.findAll(); } @@ -42,20 +38,6 @@ export class UserService implements IUserService { return user; } - async suspendUser(id: string, suspendedUntil?: Date | null): Promise { - return this.updateSuspension(id, { - isSuspended: true, - suspendedUntil: suspendedUntil ?? null, - }); - } - - async unsuspendUser(id: string): Promise { - return this.updateSuspension(id, { - isSuspended: false, - suspendedUntil: null, - }); - } - async deleteUser(requestingUserId: string, targetUserId: string): Promise { if (requestingUserId === targetUserId) { throw new BusinessRuleError("Cannot delete yourself"); diff --git a/apps/backend/src/config/constants.ts b/apps/backend/src/config/constants.ts deleted file mode 100644 index 7dbfeb1..0000000 --- a/apps/backend/src/config/constants.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const API_KEY_PREFIXES = { - SERVER: "minikura_server_api_key_", - REVERSE_PROXY: "minikura_reverse_proxy_server_api_key_", -} as const; - -export const DEFAULT_PORTS = { - MINECRAFT: 25565, -} as const; - -export const DEFAULT_MEMORY = { - SERVER: 2048, - REVERSE_PROXY: 512, -} as const; - -export const DEFAULT_MEMORY_REQUEST = { - SERVER: 1024, - REVERSE_PROXY: 512, -} as const; - -export const DEFAULT_CPU = { - SERVER: { - REQUEST: "500m", - LIMIT: "2", - }, - REVERSE_PROXY: { - REQUEST: "250m", - LIMIT: "500m", - }, -} as const; - -export const VALIDATION = { - ID_PATTERN: /^[a-zA-Z0-9-_]+$/, - ID_ERROR_MESSAGE: "ID must be alphanumeric with - or _", - PORT_MIN: 1, - PORT_MAX: 65535, -} as const; diff --git a/apps/backend/src/domain/events/server-lifecycle.events.ts b/apps/backend/src/domain/events/server-lifecycle.events.ts index ed48fd2..f1da06a 100644 --- a/apps/backend/src/domain/events/server-lifecycle.events.ts +++ b/apps/backend/src/domain/events/server-lifecycle.events.ts @@ -26,18 +26,3 @@ export class ServerDeletedEvent extends DomainEvent { super(); } } - -export class UserSuspendedEvent extends DomainEvent { - constructor( - public readonly userId: string, - public readonly suspendedUntil: Date | null - ) { - super(); - } -} - -export class UserUnsuspendedEvent extends DomainEvent { - constructor(public readonly userId: string) { - super(); - } -} diff --git a/apps/backend/src/domain/events/user-lifecycle.events.ts b/apps/backend/src/domain/events/user-lifecycle.events.ts new file mode 100644 index 0000000..cb8ff4f --- /dev/null +++ b/apps/backend/src/domain/events/user-lifecycle.events.ts @@ -0,0 +1,16 @@ +import { DomainEvent } from "./domain-event"; + +export class UserSuspendedEvent extends DomainEvent { + constructor( + public readonly userId: string, + public readonly suspendedUntil: Date | null + ) { + super(); + } +} + +export class UserUnsuspendedEvent extends DomainEvent { + constructor(public readonly userId: string) { + super(); + } +} diff --git a/apps/backend/src/domain/repositories/user.repository.ts b/apps/backend/src/domain/repositories/user.repository.ts index f9a25ea..37581bb 100644 --- a/apps/backend/src/domain/repositories/user.repository.ts +++ b/apps/backend/src/domain/repositories/user.repository.ts @@ -2,10 +2,8 @@ import type { UpdateSuspensionInput, UpdateUserInput, User } from "@minikura/db" export interface UserRepository { findById(id: string): Promise; - findByEmail(email: string): Promise; findAll(): Promise; update(id: string, input: UpdateUserInput): Promise; updateSuspension(id: string, input: UpdateSuspensionInput): Promise; delete(id: string): Promise; - count(): Promise; } diff --git a/apps/backend/src/domain/value-objects/api-key.vo.ts b/apps/backend/src/domain/value-objects/api-key.vo.ts deleted file mode 100644 index 1eecf93..0000000 --- a/apps/backend/src/domain/value-objects/api-key.vo.ts +++ /dev/null @@ -1,33 +0,0 @@ -export class ApiKey { - private static readonly SERVER_PREFIX = "minikura_srv_"; - private static readonly REVERSE_PROXY_PREFIX = "minikura_proxy_"; - private static readonly TOKEN_BYTES = 32; - - private constructor(private readonly value: string) {} - - static generate(type: "server" | "reverse-proxy"): ApiKey { - const prefix = type === "server" ? ApiKey.SERVER_PREFIX : ApiKey.REVERSE_PROXY_PREFIX; - const token = Buffer.from(crypto.randomUUID()) - .toString("base64") - .replace(/[^a-zA-Z0-9]/g, "") - .substring(0, ApiKey.TOKEN_BYTES); - return new ApiKey(`${prefix}${token}`); - } - - static validate(value: string): boolean { - const patterns = [ - new RegExp(`^${ApiKey.SERVER_PREFIX}[a-zA-Z0-9]{32}$`), - new RegExp(`^${ApiKey.REVERSE_PROXY_PREFIX}[a-zA-Z0-9]{32}$`), - ]; - return patterns.some((pattern) => pattern.test(value)); - } - - toString(): string { - return this.value; - } - - getType(): "server" | "reverse-proxy" { - if (this.value.startsWith(ApiKey.SERVER_PREFIX)) return "server"; - return "reverse-proxy"; - } -} diff --git a/apps/backend/src/domain/value-objects/k8s-connection-info.vo.ts b/apps/backend/src/domain/value-objects/k8s-connection-info.vo.ts deleted file mode 100644 index c2f6cc3..0000000 --- a/apps/backend/src/domain/value-objects/k8s-connection-info.vo.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class K8sConnectionInfo { - constructor( - public readonly host: string, - public readonly port: number, - public readonly namespace: string - ) {} - - toUrl(): string { - return `${this.host}:${this.port}`; - } - - toConnectionString(): string { - return `Host: ${this.host}, Port: ${this.port}, Namespace: ${this.namespace}`; - } -} diff --git a/apps/backend/src/domain/value-objects/server-config.vo.ts b/apps/backend/src/domain/value-objects/server-config.vo.ts deleted file mode 100644 index f87bbb0..0000000 --- a/apps/backend/src/domain/value-objects/server-config.vo.ts +++ /dev/null @@ -1,39 +0,0 @@ -export class ServerConfig { - constructor( - public readonly memory: number, - public readonly memoryRequest: number, - public readonly cpuRequest: string, - public readonly cpuLimit: string, - public readonly jvmOpts: string | null - ) {} - - static fromDefaults(): ServerConfig { - return new ServerConfig(2048, 1024, "250m", "500m", null); - } - - static fromInput(input: { - memory?: number; - memoryRequest?: number; - cpuRequest?: string; - cpuLimit?: string; - jvmOpts?: string; - }): ServerConfig { - return new ServerConfig( - input.memory ?? 2048, - input.memoryRequest ?? 1024, - input.cpuRequest ?? "250m", - input.cpuLimit ?? "500m", - input.jvmOpts ?? null - ); - } - - getJvmArgs(): string { - const args: string[] = [`-Xmx${this.memory}M`]; - - if (this.jvmOpts) { - args.push(this.jvmOpts); - } - - return args.join(" "); - } -} diff --git a/apps/backend/src/infrastructure/event-bus.ts b/apps/backend/src/infrastructure/event-bus.ts index 5d77d0e..84dee35 100644 --- a/apps/backend/src/infrastructure/event-bus.ts +++ b/apps/backend/src/infrastructure/event-bus.ts @@ -5,7 +5,6 @@ type EventHandler = (event: T) => void | Pr export class EventBus { private handlers = new Map>(); - private eventHistory: DomainEvent[] = []; subscribe( eventClass: { new (...args: any[]): T }, @@ -22,7 +21,6 @@ export class EventBus { } async publish(event: T): Promise { - this.eventHistory.push(event); const eventName = event.constructor.name; const handlers = this.handlers.get(eventName) || []; for (const handler of handlers) { @@ -33,14 +31,6 @@ export class EventBus { } } } - - getHistory(): DomainEvent[] { - return [...this.eventHistory]; - } - - clearHistory(): void { - this.eventHistory = []; - } } export const eventBus = new EventBus(); diff --git a/apps/backend/src/infrastructure/event-handlers/user-event.handler.ts b/apps/backend/src/infrastructure/event-handlers/user-event.handler.ts index 55d2e03..3886ee8 100644 --- a/apps/backend/src/infrastructure/event-handlers/user-event.handler.ts +++ b/apps/backend/src/infrastructure/event-handlers/user-event.handler.ts @@ -1,7 +1,7 @@ import { UserSuspendedEvent, UserUnsuspendedEvent, -} from "../../domain/events/server-lifecycle.events"; +} from "../../domain/events/user-lifecycle.events"; import { eventBus } from "../event-bus"; import { logger } from "../logger"; diff --git a/apps/backend/src/infrastructure/repositories/prisma/user.repository.impl.ts b/apps/backend/src/infrastructure/repositories/prisma/user.repository.impl.ts index 83ad3ce..fdae2ba 100644 --- a/apps/backend/src/infrastructure/repositories/prisma/user.repository.impl.ts +++ b/apps/backend/src/infrastructure/repositories/prisma/user.repository.impl.ts @@ -8,12 +8,6 @@ export class PrismaUserRepository implements UserRepository { }); } - async findByEmail(email: string): Promise { - return await prisma.user.findUnique({ - where: { email }, - }); - } - async findAll(): Promise { return await prisma.user.findMany({ orderBy: { createdAt: "desc" }, @@ -39,8 +33,4 @@ export class PrismaUserRepository implements UserRepository { where: { id }, }); } - - async count(): Promise { - return await prisma.user.count(); - } } diff --git a/apps/backend/src/middleware/auth-guards.ts b/apps/backend/src/middleware/auth-guards.ts index 339dd39..9d4fb96 100644 --- a/apps/backend/src/middleware/auth-guards.ts +++ b/apps/backend/src/middleware/auth-guards.ts @@ -1,57 +1,41 @@ import type { User } from "@minikura/db"; import type { Elysia } from "elysia"; import { ForbiddenError, UnauthorizedError } from "../domain/errors/base.error"; +import { bearerToken, findApiKeyOwner } from "./api-key"; + +function authenticatedUser(ctx: { user?: User | null; isSuspended?: boolean }) { + const { user, isSuspended } = ctx; + if (!user) { + throw new UnauthorizedError(); + } + if (isSuspended) { + throw new ForbiddenError("Account is suspended"); + } + return { user }; +} export const requireAuth = (app: Elysia) => { - return app.derive((ctx: any) => { - const { user, isSuspended } = ctx as { - user: User | null; - isSuspended: boolean; - }; - if (!user) { - throw new UnauthorizedError(); - } - if (isSuspended) { - throw new ForbiddenError("Account is suspended"); - } - return { user }; - }); -}; - -export const requireAdmin = (app: Elysia) => { - return app.derive((ctx: any) => { - const { user, isSuspended } = ctx as { - user: User | null; - isSuspended: boolean; - }; - if (!user) { - throw new UnauthorizedError(); - } - if (isSuspended) { - throw new ForbiddenError("Account is suspended"); - } - if (user.role !== "admin") { - throw new ForbiddenError("Admin access required"); - } - return { user }; - }); + return app.derive((ctx: any) => authenticatedUser(ctx)); }; export const requireRole = (role: string) => (app: Elysia) => { return app.derive((ctx: any) => { - const { user, isSuspended } = ctx as { - user: User | null; - isSuspended: boolean; - }; - if (!user) { - throw new UnauthorizedError(); - } - if (isSuspended) { - throw new ForbiddenError("Account is suspended"); - } + const { user } = authenticatedUser(ctx); if (user.role !== role) { throw new ForbiddenError(`${role} access required`); } return { user }; }); }; + +export const requireAdmin = requireRole("admin"); + +export const requirePluginApiKey = (app: Elysia) => { + return app.derive(async ({ request }) => { + const owner = await findApiKeyOwner(bearerToken(request.headers.get("authorization"))); + if (!owner) { + throw new UnauthorizedError(); + } + return { pluginAuth: owner }; + }); +}; diff --git a/apps/backend/src/middleware/zod-validator.ts b/apps/backend/src/middleware/zod-validator.ts deleted file mode 100644 index 2189e95..0000000 --- a/apps/backend/src/middleware/zod-validator.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { z } from "zod"; - -type ErrorHandler = (code: number, value: unknown) => never; - -export function validateBody( - schema: T, - body: unknown, - error: ErrorHandler -): z.infer { - const result = schema.safeParse(body); - - if (!result.success) { - const firstError = result.error.issues[0]; - const message = `${firstError.path.join(".")}: ${firstError.message}`; - throw error(400, { message }); - } - - return result.data; -} - -export function zodValidate(schema: T) { - return (context: { body: unknown; error: ErrorHandler }) => { - return validateBody(schema, context.body, context.error); - }; -} diff --git a/apps/backend/src/routes/k8s.ts b/apps/backend/src/routes/k8s.ts index 8e8d0cf..fc1cab5 100644 --- a/apps/backend/src/routes/k8s.ts +++ b/apps/backend/src/routes/k8s.ts @@ -2,10 +2,8 @@ import { labelKeys } from "@minikura/api"; import { Elysia } from "elysia"; import { k8sService } from "../application/di-container"; import { requireAuth } from "../middleware/auth-guards"; -import { authPlugin } from "../middleware/auth-plugin"; export const k8sRoutes = new Elysia({ prefix: "/k8s" }) - .use(authPlugin) .use(requireAuth) .get("/status", async () => { return k8sService.getConnectionInfo(); diff --git a/apps/backend/src/schemas/server.schema.ts b/apps/backend/src/schemas/server.schema.ts index 05834c5..18bf705 100644 --- a/apps/backend/src/schemas/server.schema.ts +++ b/apps/backend/src/schemas/server.schema.ts @@ -54,49 +54,7 @@ export const createServerSchema = z.object({ level_type: z.string().optional(), }); -export const updateServerSchema = z.object({ - description: z.string().nullable().optional(), - listen_port: z.number().int().min(1).max(65535).optional(), - service_type: z.nativeEnum(ServiceType).optional(), - node_port: z - .union([ - z - .number() - .int() - .min(30000, "Node port must be at least 30000") - .max(32767, "Node port must be at most 32767"), - z.null(), - ]) - .optional(), - env_variables: z - .array( - z.object({ - key: z.string().min(1), - value: z.string(), - }) - ) - .optional(), - memory: z.number().int().min(256).optional(), - memory_request: z.number().int().min(256).optional(), - cpu_request: z.string().optional(), - cpu_limit: z.string().optional(), - - jar_type: z.nativeEnum(MinecraftServerJarType).optional(), - minecraft_version: z.string().optional(), - - jvm_opts: z.string().optional(), - use_aikar_flags: z.boolean().optional(), - use_meowice_flags: z.boolean().optional(), - - difficulty: z.nativeEnum(ServerDifficulty).optional(), - game_mode: z.nativeEnum(GameMode).optional(), - max_players: z.number().int().min(1).max(1000).optional(), - pvp: z.boolean().optional(), - online_mode: z.boolean().optional(), - motd: z.string().optional(), - level_seed: z.string().optional(), - level_type: z.string().optional(), -}); +export const updateServerSchema = createServerSchema.omit({ id: true, type: true }).partial(); export const createReverseProxySchema = z.object({ id: z @@ -123,18 +81,9 @@ export const createReverseProxySchema = z.object({ cpu_limit: z.string().optional(), }); -export const updateReverseProxySchema = z.object({ - description: z.string().nullable().optional(), - external_address: z.string().optional(), - external_port: z.number().int().min(1).max(65535).optional(), - listen_port: z.number().int().min(1).max(65535).optional(), - type: z.nativeEnum(ReverseProxyServerType).optional(), - service_type: z.nativeEnum(ServiceType).optional(), - node_port: z.union([z.number().int().min(30000).max(32767), z.null()]).optional(), - memory: z.number().int().min(256).optional(), - cpu_request: z.string().optional(), - cpu_limit: z.string().optional(), -}); +export const updateReverseProxySchema = createReverseProxySchema + .omit({ id: true, env_variables: true }) + .partial(); export const envVariableSchema = z.object({ key: z.string().min(1, "Key is required"), diff --git a/apps/backend/src/schemas/user.schema.ts b/apps/backend/src/schemas/user.schema.ts index 4fbede8..efd8254 100644 --- a/apps/backend/src/schemas/user.schema.ts +++ b/apps/backend/src/schemas/user.schema.ts @@ -10,10 +10,5 @@ export const updateSuspensionSchema = z.object({ suspendedUntil: z.string().nullable().optional(), }); -export const suspendUserSchema = z.object({ - suspendedUntil: z.string().nullable().optional(), -}); - export type UpdateUserInput = z.infer; export type UpdateSuspensionInput = z.infer; -export type SuspendUserInput = z.infer; diff --git a/apps/backend/src/services/k8s.ts b/apps/backend/src/services/k8s.ts index 8548fde..800d4da 100644 --- a/apps/backend/src/services/k8s.ts +++ b/apps/backend/src/services/k8s.ts @@ -54,7 +54,7 @@ export class K8sService implements IK8sService { private initializeOperations(): void { this.podOps = new PodOperations(this.coreApi, this.namespace); - this.clusterOps = new ClusterOperations(this.coreApi, this.customObjectsApi, this.namespace); + this.clusterOps = new ClusterOperations(this.customObjectsApi, this.namespace); this.customResourceOps = new CustomResourceOperations(this.customObjectsApi, this.namespace); } diff --git a/apps/backend/src/services/kubernetes/operations/cluster.operations.ts b/apps/backend/src/services/kubernetes/operations/cluster.operations.ts index 42ff2cf..49cc0ef 100644 --- a/apps/backend/src/services/kubernetes/operations/cluster.operations.ts +++ b/apps/backend/src/services/kubernetes/operations/cluster.operations.ts @@ -3,20 +3,12 @@ import { BaseK8sOperations } from "./base.operations"; export class ClusterOperations extends BaseK8sOperations { constructor( - private coreApi: k8s.CoreV1Api, private customObjectsApi: k8s.CustomObjectsApi, namespace: string ) { super(namespace); } - async listNodes() { - return this.executeOperation( - () => this.coreApi.listNode().then((r) => r.items), - "Failed to fetch nodes" - ); - } - async getNodeMetrics() { return this.executeOperation( () => @@ -28,12 +20,4 @@ export class ClusterOperations extends BaseK8sOperations { "Failed to fetch node metrics" ); } - - async listConfigMaps(namespace?: string) { - const ns = namespace || this.namespace; - return this.executeOperation( - () => this.coreApi.listNamespacedConfigMap({ namespace: ns }).then((r) => r.items), - "Failed to fetch configmaps" - ); - } } diff --git a/apps/backend/src/services/kubernetes/operations/index.ts b/apps/backend/src/services/kubernetes/operations/index.ts index e2ca181..70d5716 100644 --- a/apps/backend/src/services/kubernetes/operations/index.ts +++ b/apps/backend/src/services/kubernetes/operations/index.ts @@ -1,6 +1,4 @@ export { BaseK8sOperations } from "./base.operations"; export { ClusterOperations } from "./cluster.operations"; export { CustomResourceOperations } from "./custom-resource.operations"; -export { NetworkOperations } from "./network.operations"; export { PodOperations } from "./pod.operations"; -export { WorkloadOperations } from "./workload.operations"; diff --git a/apps/backend/src/services/kubernetes/operations/network.operations.ts b/apps/backend/src/services/kubernetes/operations/network.operations.ts deleted file mode 100644 index 1bb1e92..0000000 --- a/apps/backend/src/services/kubernetes/operations/network.operations.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type * as k8s from "@kubernetes/client-node"; -import { BaseK8sOperations } from "./base.operations"; - -export class NetworkOperations extends BaseK8sOperations { - constructor( - private coreApi: k8s.CoreV1Api, - private networkingApi: k8s.NetworkingV1Api, - namespace: string - ) { - super(namespace); - } - - async listServices() { - return this.executeOperation( - () => this.coreApi.listNamespacedService({ namespace: this.namespace }).then((r) => r.items), - "Failed to fetch services" - ); - } - - async listIngresses() { - return this.executeOperation( - () => - this.networkingApi - .listNamespacedIngress({ namespace: this.namespace }) - .then((r) => r.items), - "Failed to fetch ingresses" - ); - } - - async getServiceInfo(serviceName: string) { - return this.executeOperation( - () => this.coreApi.readNamespacedService({ name: serviceName, namespace: this.namespace }), - `Failed to fetch service info for ${serviceName}` - ); - } - - async getServerConnectionInfo(serviceName: string) { - return this.executeOperation(async () => { - const service = await this.coreApi.readNamespacedService({ - name: serviceName, - namespace: this.namespace, - }); - - const serviceType = service.spec?.type || "ClusterIP"; - const ports = service.spec?.ports || []; - - let host: string; - let externalHost: string | undefined; - - switch (serviceType) { - case "LoadBalancer": { - const ingress = service.status?.loadBalancer?.ingress?.[0]; - host = - ingress?.hostname || - ingress?.ip || - `${serviceName}.${this.namespace}.svc.cluster.local`; - externalHost = ingress?.hostname || ingress?.ip; - break; - } - - case "NodePort": - host = `${serviceName}.${this.namespace}.svc.cluster.local`; - externalHost = ""; - break; - - default: - host = `${serviceName}.${this.namespace}.svc.cluster.local`; - externalHost = undefined; - } - - const portMappings = ports.map((port) => ({ - name: port.name, - port: port.port, - targetPort: port.targetPort, - nodePort: port.nodePort, - protocol: port.protocol || "TCP", - })); - - return { - serviceName, - namespace: this.namespace, - serviceType, - internalHost: host, - externalHost, - ports: portMappings, - }; - }, `Failed to fetch connection info for service ${serviceName}`); - } -} diff --git a/apps/backend/src/services/kubernetes/operations/pod.operations.ts b/apps/backend/src/services/kubernetes/operations/pod.operations.ts index 9e79c4b..9413cf7 100644 --- a/apps/backend/src/services/kubernetes/operations/pod.operations.ts +++ b/apps/backend/src/services/kubernetes/operations/pod.operations.ts @@ -9,30 +9,6 @@ export class PodOperations extends BaseK8sOperations { super(namespace); } - async listPods() { - return this.executeOperation( - () => this.coreApi.listNamespacedPod({ namespace: this.namespace }).then((r) => r.items), - "Failed to fetch pods" - ); - } - - async listPodsByLabel(labelSelector: string) { - return this.executeOperation( - () => - this.coreApi - .listNamespacedPod({ namespace: this.namespace, labelSelector }) - .then((r) => r.items), - "Failed to fetch pods by label" - ); - } - - async getPodInfo(podName: string) { - return this.executeOperation( - () => this.coreApi.readNamespacedPod({ name: podName, namespace: this.namespace }), - `Failed to fetch pod info for ${podName}` - ); - } - async getPodLogs( podName: string, options?: { diff --git a/apps/backend/src/services/kubernetes/operations/workload.operations.ts b/apps/backend/src/services/kubernetes/operations/workload.operations.ts deleted file mode 100644 index a424914..0000000 --- a/apps/backend/src/services/kubernetes/operations/workload.operations.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type * as k8s from "@kubernetes/client-node"; -import { BaseK8sOperations } from "./base.operations"; - -export class WorkloadOperations extends BaseK8sOperations { - constructor( - private appsApi: k8s.AppsV1Api, - namespace: string - ) { - super(namespace); - } - - async listDeployments() { - return this.executeOperation( - () => - this.appsApi.listNamespacedDeployment({ namespace: this.namespace }).then((r) => r.items), - "Failed to fetch deployments" - ); - } - - async listStatefulSets() { - return this.executeOperation( - () => - this.appsApi.listNamespacedStatefulSet({ namespace: this.namespace }).then((r) => r.items), - "Failed to fetch statefulsets" - ); - } -} diff --git a/apps/backend/src/services/operator-resource-sync.ts b/apps/backend/src/services/operator-resource-sync.ts index 4b1e583..a10179d 100644 --- a/apps/backend/src/services/operator-resource-sync.ts +++ b/apps/backend/src/services/operator-resource-sync.ts @@ -1,9 +1,8 @@ import * as k8s from "@kubernetes/client-node"; +import { API_GROUP } from "@minikura/api"; import { prisma, type ReverseProxyWithEnvVars, type ServerWithEnvVars } from "@minikura/db"; import { buildKubeConfig } from "@minikura/shared/kube-auth"; import { logger } from "../infrastructure/logger"; - -const API_GROUP = "minikura.kirameki.cafe"; const API_VERSION = "v1alpha1"; const FIELD_MANAGER = "minikura-backend"; const SYNC_INTERVAL_MS = 30_000; diff --git a/apps/web/components/topology/layouts/hierarchical-layout.ts b/apps/web/components/topology/layouts/hierarchical-layout.ts deleted file mode 100644 index 232a908..0000000 --- a/apps/web/components/topology/layouts/hierarchical-layout.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { TopologyEdge, TopologyNode } from "@/lib/topology-types"; - -const LAYOUT_CONFIG = { - TIER_SPACING: 300, - NODE_SPACING: 250, - START_X: 150, - START_Y: 100, -} as const; - -export function applyHierarchicalLayout( - nodes: TopologyNode[], - edges: TopologyEdge[] -): { nodes: TopologyNode[]; edges: TopologyEdge[] } { - const proxyNodes = nodes.filter((n) => n.data.type === "proxy"); - const serverNodes = nodes.filter((n) => n.data.type === "server"); - - const layoutedNodes: TopologyNode[] = []; - - const maxNodesInTier = Math.max(proxyNodes.length, serverNodes.length); - const tierWidth = maxNodesInTier * LAYOUT_CONFIG.NODE_SPACING; - - const proxyOffsetX = (tierWidth - proxyNodes.length * LAYOUT_CONFIG.NODE_SPACING) / 2; - proxyNodes.forEach((node, index) => { - const x = LAYOUT_CONFIG.START_X + proxyOffsetX + index * LAYOUT_CONFIG.NODE_SPACING; - const y = LAYOUT_CONFIG.START_Y; - - layoutedNodes.push({ - ...node, - position: { x, y }, - }); - }); - - const serverOffsetX = (tierWidth - serverNodes.length * LAYOUT_CONFIG.NODE_SPACING) / 2; - serverNodes.forEach((node, index) => { - const x = LAYOUT_CONFIG.START_X + serverOffsetX + index * LAYOUT_CONFIG.NODE_SPACING; - const y = LAYOUT_CONFIG.START_Y + LAYOUT_CONFIG.TIER_SPACING; - - layoutedNodes.push({ - ...node, - position: { x, y }, - }); - }); - - return { - nodes: layoutedNodes, - edges, - }; -} - -export function applyGridLayout( - nodes: TopologyNode[], - edges: TopologyEdge[] -): { nodes: TopologyNode[]; edges: TopologyEdge[] } { - const columns = Math.ceil(Math.sqrt(nodes.length)); - - const layoutedNodes = nodes.map((node, index) => ({ - ...node, - position: { - x: LAYOUT_CONFIG.START_X + (index % columns) * LAYOUT_CONFIG.NODE_SPACING, - y: LAYOUT_CONFIG.START_Y + Math.floor(index / columns) * LAYOUT_CONFIG.TIER_SPACING, - }, - })); - - return { - nodes: layoutedNodes, - edges, - }; -} - -export function layoutTopologyGraph( - nodes: TopologyNode[], - edges: TopologyEdge[] -): { nodes: TopologyNode[]; edges: TopologyEdge[] } { - if (nodes.length === 0) { - return { nodes: [], edges: [] }; - } - - if (nodes.length < 20) { - return applyHierarchicalLayout(nodes, edges); - } - - return applyGridLayout(nodes, edges); -} diff --git a/apps/web/hooks/use-k8s-resources.ts b/apps/web/hooks/use-k8s-resources.ts index cdf360a..e955008 100644 --- a/apps/web/hooks/use-k8s-resources.ts +++ b/apps/web/hooks/use-k8s-resources.ts @@ -9,24 +9,10 @@ import type { PodInfo, StatefulSetInfo, } from "@minikura/api"; +import { getErrorMessage } from "@minikura/shared/errors"; import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "@/lib/api-client"; -function getErrorMessage(error: unknown) { - if (error instanceof Error) { - return error.message; - } - - if (typeof error === "object" && error) { - const value = "value" in error ? error.value : error; - if (typeof value === "object" && value && "message" in value) { - return String(value.message); - } - } - - return "Failed to fetch Kubernetes resources"; -} - export function useK8sResources() { const [status, setStatus] = useState(null); const [pods, setPods] = useState([]); diff --git a/apps/web/lib/topology-types.ts b/apps/web/lib/topology-types.ts index 8ec3d38..777bba9 100644 --- a/apps/web/lib/topology-types.ts +++ b/apps/web/lib/topology-types.ts @@ -6,6 +6,9 @@ import type { ReverseProxyServer, } from "@minikura/api"; import type { Edge, Node } from "@xyflow/react"; +import type { ResourceMetrics } from "./k8s-metrics"; + +export type { ResourceMetrics }; export type HealthStatus = "healthy" | "degraded" | "unhealthy" | "unknown"; @@ -13,13 +16,6 @@ export type NodeType = "server" | "proxy" | "k8s-node"; export type EdgeType = "proxy-to-server" | "pod-to-node"; -export interface ResourceMetrics { - cpuUsage?: string; - memoryUsage?: string; - cpuUsagePercent?: number; - memoryUsagePercent?: number; -} - export interface K8sNodeMetadata { node: K8sNodeSummary; podCount: number; diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index 9ad0e20..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Config } from "drizzle-kit"; - -export default { - schema: "./packages/db/src/schema.ts", - out: "./packages/db/src/drizzle", - dialect: "postgresql", - dbCredentials: { - url: process.env.DATABASE_URL || "", - }, -} satisfies Config; diff --git a/packages/shared/src/errors.ts b/packages/shared/src/errors.ts index 231185f..dbe6884 100644 --- a/packages/shared/src/errors.ts +++ b/packages/shared/src/errors.ts @@ -5,6 +5,12 @@ export function getErrorMessage(error: unknown): string { if (typeof error === "string") { return error; } + if (typeof error === "object" && error) { + const value = "value" in error ? error.value : error; + if (typeof value === "object" && value && "message" in value) { + return String(value.message); + } + } return String(error); } export function getAge(timestamp: Date | string | undefined): string {