mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-03-30 12:25:35 +00:00
✨ feat: initial prototype
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"name": "@minikura/api",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"exports": "./src/index.ts",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/eden": "^1.1.3",
|
||||
"@minikura/backend": "workspace:*",
|
||||
"elysia": "^1.1.13"
|
||||
}
|
||||
"name": "@minikura/api",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"exports": "./src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/eden": "^1.4.6",
|
||||
"elysia": "^1.4.22"
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/api/src/constants.ts
Normal file
4
packages/api/src/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const DOMAIN = "minikura.kirameki.cafe";
|
||||
|
||||
export const LABEL_PREFIX = DOMAIN;
|
||||
export const API_GROUP = DOMAIN;
|
||||
@@ -1,4 +1,7 @@
|
||||
import { edenTreaty } from "@elysiajs/eden";
|
||||
import type { App } from "@minikura/backend";
|
||||
import { treaty } from "@elysiajs/eden";
|
||||
|
||||
export const api = edenTreaty<App>("http://localhost:3000");
|
||||
export const api = treaty("http://localhost:3000");
|
||||
|
||||
export * from "./constants";
|
||||
export * from "./labels";
|
||||
export * from "./types";
|
||||
|
||||
10
packages/api/src/labels.ts
Normal file
10
packages/api/src/labels.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { LABEL_PREFIX } from "./constants";
|
||||
|
||||
export const labelKeys = {
|
||||
serverType: `${LABEL_PREFIX}/server-type`,
|
||||
serverId: `${LABEL_PREFIX}/server-id`,
|
||||
proxyId: `${LABEL_PREFIX}/proxy-id`,
|
||||
restartAt: `${LABEL_PREFIX}/restart-at`,
|
||||
databaseManaged: `${LABEL_PREFIX}/database-managed`,
|
||||
lastSynced: `${LABEL_PREFIX}/last-synced`,
|
||||
} as const;
|
||||
209
packages/api/src/types.ts
Normal file
209
packages/api/src/types.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import type {
|
||||
EnvVariable as DbEnvVariable,
|
||||
ReverseProxyWithEnvVars,
|
||||
ServerWithEnvVars,
|
||||
} from "@minikura/db";
|
||||
|
||||
export type EnvVariable = DbEnvVariable;
|
||||
|
||||
export type NormalServer = ServerWithEnvVars;
|
||||
|
||||
export type ReverseProxyServer = ReverseProxyWithEnvVars;
|
||||
|
||||
export type K8sResource = {
|
||||
name: string;
|
||||
kind: string;
|
||||
namespace?: string;
|
||||
age: string;
|
||||
labels?: Record<string, string>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type K8sServiceSummary = {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
type?: string;
|
||||
clusterIP?: string | null;
|
||||
externalIP?: string;
|
||||
ports?: string;
|
||||
age: string;
|
||||
labels?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type K8sConfigMapSummary = {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
data: number;
|
||||
age: string;
|
||||
labels?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type K8sIngressSummary = {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
className?: string | null;
|
||||
hosts: string;
|
||||
address: string;
|
||||
age: string;
|
||||
labels?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type K8sServicePort = {
|
||||
name?: string | null;
|
||||
protocol?: string | null;
|
||||
port?: number;
|
||||
targetPort?: number | string;
|
||||
nodePort?: number | null;
|
||||
};
|
||||
|
||||
export type K8sServiceInfo = {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
type?: string;
|
||||
clusterIP?: string | null;
|
||||
externalIPs: string[];
|
||||
loadBalancerIP: string | null;
|
||||
loadBalancerHostname: string | null;
|
||||
ports: K8sServicePort[];
|
||||
selector?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type K8sNodeSummary = {
|
||||
name?: string;
|
||||
status: string;
|
||||
roles: string;
|
||||
age: string;
|
||||
version?: string;
|
||||
internalIP?: string;
|
||||
externalIP?: string;
|
||||
hostname?: string;
|
||||
};
|
||||
|
||||
export type CustomResourceSummary = {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
age: string;
|
||||
labels?: Record<string, string>;
|
||||
spec?: Record<string, unknown>;
|
||||
status?: { phase?: string; [key: string]: unknown };
|
||||
};
|
||||
|
||||
export type K8sStatus = {
|
||||
initialized: boolean;
|
||||
};
|
||||
|
||||
export type PodInfo = {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
status: string;
|
||||
ready: string;
|
||||
restarts: number;
|
||||
age?: string;
|
||||
containers?: string[];
|
||||
nodeName?: string;
|
||||
ip?: string;
|
||||
labels?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type PodCondition = {
|
||||
type?: string;
|
||||
status?: string;
|
||||
lastTransitionTime?: string;
|
||||
};
|
||||
|
||||
export type PodContainerStatus = {
|
||||
name?: string;
|
||||
ready?: boolean;
|
||||
restartCount?: number;
|
||||
state?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PodDetails = PodInfo & {
|
||||
conditions?: PodCondition[];
|
||||
containerStatuses?: PodContainerStatus[];
|
||||
};
|
||||
|
||||
export type StatefulSetInfo = {
|
||||
name: string;
|
||||
ready: string;
|
||||
desired: number;
|
||||
current: number;
|
||||
updated: number;
|
||||
age: string;
|
||||
labels?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type DeploymentInfo = {
|
||||
name: string;
|
||||
ready: string;
|
||||
desired: number;
|
||||
current: number;
|
||||
updated: number;
|
||||
upToDate?: number;
|
||||
available?: number;
|
||||
age: string;
|
||||
labels?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type ConnectionInfo = {
|
||||
type: string;
|
||||
connectionString?: string | null;
|
||||
note?: string | null;
|
||||
ip?: string;
|
||||
port?: number | null;
|
||||
nodeIP?: string;
|
||||
nodePort?: number;
|
||||
externalIP?: string;
|
||||
};
|
||||
|
||||
export type CreateServerRequest = {
|
||||
id: string;
|
||||
type: "STATEFUL" | "STATELESS";
|
||||
description?: string | null;
|
||||
listen_port: number;
|
||||
service_type?: string;
|
||||
node_port?: number | null;
|
||||
env_variables?: EnvVariable[];
|
||||
memory?: number;
|
||||
memory_request?: number;
|
||||
cpu_request?: string;
|
||||
cpu_limit?: string;
|
||||
jar_type?: string;
|
||||
minecraft_version?: string;
|
||||
jvm_opts?: string;
|
||||
use_aikar_flags?: boolean;
|
||||
use_meowice_flags?: boolean;
|
||||
difficulty?: string;
|
||||
game_mode?: string;
|
||||
max_players?: number;
|
||||
pvp?: boolean;
|
||||
online_mode?: boolean;
|
||||
motd?: string | null;
|
||||
level_seed?: string | null;
|
||||
level_type?: string | null;
|
||||
};
|
||||
|
||||
export type UpdateServerRequest = {
|
||||
description?: string | null;
|
||||
listen_port?: number;
|
||||
service_type?: string;
|
||||
node_port?: number | null;
|
||||
env_variables?: EnvVariable[];
|
||||
memory?: number;
|
||||
memory_request?: number;
|
||||
cpu_request?: string;
|
||||
cpu_limit?: string;
|
||||
jar_type?: string;
|
||||
minecraft_version?: string;
|
||||
jvm_opts?: string;
|
||||
use_aikar_flags?: boolean;
|
||||
use_meowice_flags?: boolean;
|
||||
difficulty?: string;
|
||||
game_mode?: string;
|
||||
max_players?: number;
|
||||
pvp?: boolean;
|
||||
online_mode?: boolean;
|
||||
motd?: string | null;
|
||||
level_seed?: string | null;
|
||||
level_type?: string | null;
|
||||
};
|
||||
1
packages/db/.gitignore
vendored
1
packages/db/.gitignore
vendored
@@ -58,6 +58,7 @@ build/Release
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
src/generated/prisma/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
{
|
||||
"name": "@minikura/db",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"exports": "./src/index.ts",
|
||||
"scripts": {
|
||||
"generate": "prisma generate",
|
||||
"studio": "bun with-env prisma studio",
|
||||
"push": "bun with-env prisma db push",
|
||||
"reset": "bun with-env prisma migrate reset --force",
|
||||
"with-env": "dotenv -e ../../.env --"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.20.0",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"prisma": "^5.20.0"
|
||||
}
|
||||
"name": "@minikura/db",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"exports": "./src/index.ts",
|
||||
"scripts": {
|
||||
"generate": "prisma generate",
|
||||
"studio": "bun with-env prisma studio",
|
||||
"push": "bun with-env prisma db push",
|
||||
"reset": "bun with-env prisma migrate reset --force",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env --"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/pg": "^8.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "7.2.0",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"pg": "^8.17.1",
|
||||
"prisma": "^7.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
9
packages/db/prisma.config.ts
Normal file
9
packages/db/prisma.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: path.join(__dirname, "prisma/schema.prisma"),
|
||||
datasource: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
@@ -1,12 +1,74 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// Better Auth Models
|
||||
model User {
|
||||
id String @id
|
||||
name String
|
||||
email String @unique
|
||||
emailVerified Boolean @default(false) @map("email_verified")
|
||||
image String?
|
||||
role String @default("user")
|
||||
isSuspended Boolean @default(false) @map("is_suspended")
|
||||
suspendedUntil DateTime? @map("suspended_until")
|
||||
banned Boolean @default(false) @map("banned")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id
|
||||
expiresAt DateTime @map("expires_at")
|
||||
token String @unique
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
ipAddress String? @map("ip_address")
|
||||
userAgent String? @map("user_agent")
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("session")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id
|
||||
accountId String @map("account_id")
|
||||
providerId String @map("provider_id")
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
accessToken String? @map("access_token")
|
||||
refreshToken String? @map("refresh_token")
|
||||
idToken String? @map("id_token")
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
password String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("account")
|
||||
}
|
||||
|
||||
model Verification {
|
||||
id String @id
|
||||
identifier String
|
||||
value String
|
||||
expiresAt DateTime @map("expires_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("verification")
|
||||
}
|
||||
|
||||
// Application Models
|
||||
enum ServerType {
|
||||
STATEFUL
|
||||
STATELESS
|
||||
@@ -17,6 +79,26 @@ enum ReverseProxyServerType {
|
||||
BUNGEECORD
|
||||
}
|
||||
|
||||
enum ServiceType {
|
||||
CLUSTER_IP
|
||||
NODE_PORT
|
||||
LOAD_BALANCER
|
||||
}
|
||||
|
||||
enum GameMode {
|
||||
SURVIVAL
|
||||
CREATIVE
|
||||
ADVENTURE
|
||||
SPECTATOR
|
||||
}
|
||||
|
||||
enum ServerDifficulty {
|
||||
PEACEFUL
|
||||
EASY
|
||||
NORMAL
|
||||
HARD
|
||||
}
|
||||
|
||||
model ReverseProxyServer {
|
||||
id String @id @default(cuid())
|
||||
type ReverseProxyServerType
|
||||
@@ -24,43 +106,62 @@ model ReverseProxyServer {
|
||||
external_address String
|
||||
external_port Int
|
||||
listen_port Int @default(25565)
|
||||
memory String @default("512M")
|
||||
memory Int @default(512) // Memory in MB
|
||||
cpu_request String? @default("250m") // CPU request, e.g., "250m", "1"
|
||||
cpu_limit String? @default("500m") // CPU limit, e.g., "500m", "2"
|
||||
service_type ServiceType @default(LOAD_BALANCER)
|
||||
node_port Int?
|
||||
api_key String @unique
|
||||
env_variables CustomEnvironmentVariable[] @relation("ReverseProxyServerEnvVars")
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum MinecraftServerJarType {
|
||||
VANILLA
|
||||
PAPER
|
||||
SPIGOT
|
||||
PURPUR
|
||||
FABRIC
|
||||
FORGE
|
||||
FOLIA
|
||||
}
|
||||
|
||||
model Server {
|
||||
id String @id @default(cuid())
|
||||
type ServerType
|
||||
description String?
|
||||
listen_port Int @default(25565)
|
||||
memory String @default("1G")
|
||||
env_variables CustomEnvironmentVariable[] @relation("ServerEnvVars")
|
||||
api_key String @unique
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
}
|
||||
id String @id @default(cuid())
|
||||
type ServerType
|
||||
description String?
|
||||
listen_port Int @default(25565)
|
||||
memory Int @default(2048) // Memory limit in MB
|
||||
memory_request Int @default(1024) // Memory request in MB
|
||||
cpu_request String? @default("500m") // CPU request, e.g., "500m", "1"
|
||||
cpu_limit String? @default("2") // CPU limit, e.g., "2", "500m"
|
||||
service_type ServiceType @default(CLUSTER_IP)
|
||||
node_port Int?
|
||||
env_variables CustomEnvironmentVariable[] @relation("ServerEnvVars")
|
||||
api_key String @unique
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
password String
|
||||
sessions Session[]
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
}
|
||||
// Minecraft specific configurations
|
||||
jar_type MinecraftServerJarType @default(VANILLA)
|
||||
minecraft_version String @default("LATEST") // e.g., "LATEST", "1.20.4", "SNAPSHOT"
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
token String @unique
|
||||
user_id String
|
||||
user User @relation(fields: [user_id], references: [id])
|
||||
revoked Boolean @default(false)
|
||||
expires_at DateTime
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
// JVM Options
|
||||
jvm_opts String? // Custom JVM options
|
||||
use_aikar_flags Boolean @default(false)
|
||||
use_meowice_flags Boolean @default(false)
|
||||
|
||||
// Server Properties (common ones)
|
||||
difficulty ServerDifficulty @default(EASY)
|
||||
game_mode GameMode @default(SURVIVAL)
|
||||
max_players Int @default(20)
|
||||
pvp Boolean @default(true)
|
||||
online_mode Boolean @default(true)
|
||||
motd String?
|
||||
level_seed String?
|
||||
level_type String? // default, flat, largeBiomes, amplified
|
||||
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CustomEnvironmentVariable {
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { Pool } from "pg";
|
||||
import { PrismaClient } from "./generated/prisma";
|
||||
|
||||
export * from "@prisma/client";
|
||||
export * from "./generated/prisma";
|
||||
export type {
|
||||
ReverseProxyCreateInput,
|
||||
ReverseProxyServer,
|
||||
ReverseProxyUpdateInput,
|
||||
ReverseProxyWithEnvVars,
|
||||
} from "./models/reverse-proxy";
|
||||
export type {
|
||||
EnvVariable,
|
||||
Server,
|
||||
ServerCreateInput,
|
||||
ServerUpdateInput,
|
||||
ServerWithEnvVars,
|
||||
} from "./models/server";
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
export type { SessionWithUser } from "./models/session";
|
||||
|
||||
export { isSessionExpired } from "./models/session";
|
||||
export type {
|
||||
CreateUserInput,
|
||||
UpdateSuspensionInput,
|
||||
UpdateUserInput,
|
||||
} from "./models/user";
|
||||
export { isUserSuspended } from "./models/user";
|
||||
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
const adapter = new PrismaPg(pool);
|
||||
|
||||
export const prisma = new PrismaClient({ adapter });
|
||||
|
||||
15
packages/db/src/models/reverse-proxy.ts
Normal file
15
packages/db/src/models/reverse-proxy.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type {
|
||||
CustomEnvironmentVariable,
|
||||
Prisma,
|
||||
ReverseProxyServer as PrismaReverseProxyServer,
|
||||
} from "../generated/prisma";
|
||||
|
||||
export type ReverseProxyWithEnvVars = Prisma.ReverseProxyServerGetPayload<{
|
||||
include: { env_variables: true };
|
||||
}>;
|
||||
|
||||
export type ReverseProxyCreateInput = Prisma.ReverseProxyServerCreateInput;
|
||||
|
||||
export type ReverseProxyUpdateInput = Prisma.ReverseProxyServerUpdateInput;
|
||||
|
||||
export type ReverseProxyServer = PrismaReverseProxyServer;
|
||||
17
packages/db/src/models/server.ts
Normal file
17
packages/db/src/models/server.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type {
|
||||
CustomEnvironmentVariable,
|
||||
Prisma,
|
||||
Server as PrismaServer,
|
||||
} from "../generated/prisma";
|
||||
|
||||
export type ServerWithEnvVars = Prisma.ServerGetPayload<{
|
||||
include: { env_variables: true };
|
||||
}>;
|
||||
|
||||
export type ServerCreateInput = Prisma.ServerCreateInput;
|
||||
|
||||
export type ServerUpdateInput = Prisma.ServerUpdateInput;
|
||||
|
||||
export type EnvVariable = Pick<CustomEnvironmentVariable, "key" | "value">;
|
||||
|
||||
export type Server = PrismaServer;
|
||||
11
packages/db/src/models/session.ts
Normal file
11
packages/db/src/models/session.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Prisma, Session as PrismaSession } from "../generated/prisma";
|
||||
|
||||
export type Session = PrismaSession;
|
||||
|
||||
export type SessionWithUser = Prisma.SessionGetPayload<{
|
||||
include: { user: true };
|
||||
}>;
|
||||
|
||||
export function isSessionExpired(session: Pick<Session, "expiresAt">): boolean {
|
||||
return session.expiresAt < new Date();
|
||||
}
|
||||
21
packages/db/src/models/user.ts
Normal file
21
packages/db/src/models/user.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Prisma, User as PrismaUser } from "../generated/prisma";
|
||||
|
||||
export type User = PrismaUser;
|
||||
|
||||
export type CreateUserInput = Prisma.UserCreateInput;
|
||||
|
||||
export type UpdateUserInput = Prisma.UserUpdateInput;
|
||||
|
||||
export type UpdateSuspensionInput = Prisma.UserUpdateInput;
|
||||
|
||||
export function isUserSuspended(user: Pick<PrismaUser, "isSuspended" | "suspendedUntil">): boolean {
|
||||
if (!user.isSuspended) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.suspendedUntil && user.suspendedUntil <= new Date()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -6,21 +6,24 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "bun run src/index.ts",
|
||||
"watch": "bun --watch run src/index.ts",
|
||||
"apply-crds": "bun --elide-lines=0 run src/scripts/apply-crds.ts"
|
||||
"start": "bun dist/index.js",
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"watch": "bun --watch src/index.ts",
|
||||
"apply-crds": "bun src/scripts/apply-crds.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^0.18.0",
|
||||
"@kubernetes/client-node": "^1.4.0",
|
||||
"@minikura/api": "workspace:*",
|
||||
"@minikura/db": "workspace:*",
|
||||
"dotenv-mono": "^1.3.11",
|
||||
"node-fetch": "^3.3.2"
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pg": "^8.11.3",
|
||||
"yaml": "^2.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
"@types/node": "^25.0.9",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ServerController extends BaseController {
|
||||
console.log(
|
||||
`Server ${server.id} (${serverId}) has been removed from the database, deleting from Kubernetes...`
|
||||
);
|
||||
await deleteServer(serverId, server.id, appsApi, coreApi, this.namespace);
|
||||
await deleteServer(serverId, appsApi, coreApi, this.namespace);
|
||||
this.deployedServers.delete(serverId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function createReverseProxyServer(
|
||||
server: ReverseProxyConfig,
|
||||
appsApi: k8s.AppsV1Api,
|
||||
coreApi: k8s.CoreV1Api,
|
||||
networkingApi: k8s.NetworkingV1Api,
|
||||
_networkingApi: k8s.NetworkingV1Api,
|
||||
namespace: string
|
||||
): Promise<void> {
|
||||
console.log(`Creating reverse proxy server ${server.id} in namespace '${namespace}'`);
|
||||
@@ -34,19 +34,17 @@ export async function createReverseProxyServer(
|
||||
};
|
||||
|
||||
try {
|
||||
await coreApi.createNamespacedConfigMap(namespace, configMap);
|
||||
await coreApi.createNamespacedConfigMap({ namespace, body: configMap });
|
||||
console.log(`Created ConfigMap for reverse proxy server ${server.id}`);
|
||||
} catch (error: any) {
|
||||
// Conflict, update it
|
||||
if (error.response?.statusCode === 409) {
|
||||
await coreApi.replaceNamespacedConfigMap(`${serverName}-config`, namespace, configMap);
|
||||
await coreApi.replaceNamespacedConfigMap({ name: `${serverName}-config`, namespace, body: configMap });
|
||||
console.log(`Updated ConfigMap for reverse proxy server ${server.id}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Service for the reverse proxy - Always LoadBalancer for now
|
||||
const service = {
|
||||
apiVersion: "v1",
|
||||
kind: "Service",
|
||||
@@ -76,19 +74,17 @@ export async function createReverseProxyServer(
|
||||
};
|
||||
|
||||
try {
|
||||
await coreApi.createNamespacedService(namespace, service);
|
||||
await coreApi.createNamespacedService({ namespace, body: service });
|
||||
console.log(`Created Service for reverse proxy server ${server.id}`);
|
||||
} catch (error: any) {
|
||||
// Conflict, update it
|
||||
if (error.response?.statusCode === 409) {
|
||||
await coreApi.replaceNamespacedService(serverName, namespace, service);
|
||||
await coreApi.replaceNamespacedService({ name: serverName, namespace, body: service });
|
||||
console.log(`Updated Service for reverse proxy server ${server.id}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Deployment
|
||||
const deployment = {
|
||||
apiVersion: "apps/v1",
|
||||
kind: "Deployment",
|
||||
@@ -170,12 +166,11 @@ export async function createReverseProxyServer(
|
||||
};
|
||||
|
||||
try {
|
||||
await appsApi.createNamespacedDeployment(namespace, deployment);
|
||||
await appsApi.createNamespacedDeployment({ namespace, body: deployment });
|
||||
console.log(`Created Deployment for reverse proxy server ${server.id}`);
|
||||
} catch (error: any) {
|
||||
// Conflict, update it
|
||||
if (error.response?.statusCode === 409) {
|
||||
await appsApi.replaceNamespacedDeployment(serverName, namespace, deployment);
|
||||
await appsApi.replaceNamespacedDeployment({ name: serverName, namespace, body: deployment });
|
||||
console.log(`Updated Deployment for reverse proxy server ${server.id}`);
|
||||
} else {
|
||||
throw error;
|
||||
@@ -194,7 +189,7 @@ export async function deleteReverseProxyServer(
|
||||
const name = `${serverType}-${proxyId}`;
|
||||
|
||||
try {
|
||||
await appsApi.deleteNamespacedDeployment(name, namespace);
|
||||
await appsApi.deleteNamespacedDeployment({ name, namespace });
|
||||
console.log(`Deleted Deployment for reverse proxy server ${proxyId}`);
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode !== 404) {
|
||||
@@ -203,7 +198,7 @@ export async function deleteReverseProxyServer(
|
||||
}
|
||||
|
||||
try {
|
||||
await coreApi.deleteNamespacedService(name, namespace);
|
||||
await coreApi.deleteNamespacedService({ name, namespace });
|
||||
console.log(`Deleted Service for reverse proxy server ${proxyId}`);
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode !== 404) {
|
||||
@@ -212,7 +207,7 @@ export async function deleteReverseProxyServer(
|
||||
}
|
||||
|
||||
try {
|
||||
await coreApi.deleteNamespacedConfigMap(`${name}-config`, namespace);
|
||||
await coreApi.deleteNamespacedConfigMap({ name: `${name}-config`, namespace });
|
||||
console.log(`Deleted ConfigMap for reverse proxy server ${proxyId}`);
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode !== 404) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type * as k8s from "@kubernetes/client-node";
|
||||
import { ServerType } from "@minikura/db";
|
||||
import { LABEL_PREFIX } from "../config/constants";
|
||||
import { calculateJavaMemory, convertToK8sFormat } from "../utils/memory";
|
||||
import type { ServerConfig } from "../types";
|
||||
import { calculateJavaMemory, convertToK8sFormat } from "../utils/memory";
|
||||
|
||||
export async function createServer(
|
||||
server: ServerConfig,
|
||||
appsApi: k8s.AppsV1Api,
|
||||
coreApi: k8s.CoreV1Api,
|
||||
networkingApi: k8s.NetworkingV1Api,
|
||||
_networkingApi: k8s.NetworkingV1Api,
|
||||
namespace: string
|
||||
): Promise<void> {
|
||||
const serverName = `minecraft-${server.id}`;
|
||||
@@ -32,12 +32,15 @@ export async function createServer(
|
||||
};
|
||||
|
||||
try {
|
||||
await coreApi.createNamespacedConfigMap(namespace, configMap);
|
||||
await coreApi.createNamespacedConfigMap({ namespace, body: configMap });
|
||||
console.log(`Created ConfigMap for server ${server.id}`);
|
||||
} catch (err: any) {
|
||||
// Conflict, update it
|
||||
if (err.response?.statusCode === 409) {
|
||||
await coreApi.replaceNamespacedConfigMap(`${serverName}-config`, namespace, configMap);
|
||||
await coreApi.replaceNamespacedConfigMap({
|
||||
name: `${serverName}-config`,
|
||||
namespace,
|
||||
body: configMap,
|
||||
});
|
||||
console.log(`Updated ConfigMap for server ${server.id}`);
|
||||
} else {
|
||||
throw err;
|
||||
@@ -68,17 +71,16 @@ export async function createServer(
|
||||
name: "minecraft",
|
||||
},
|
||||
],
|
||||
type: "ClusterIP", // Always ClusterIP for regular servers
|
||||
type: "ClusterIP",
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await coreApi.createNamespacedService(namespace, service);
|
||||
await coreApi.createNamespacedService({ namespace, body: service });
|
||||
console.log(`Created Service for server ${server.id}`);
|
||||
} catch (err: any) {
|
||||
// Conflict, update it
|
||||
if (err.response?.statusCode === 409) {
|
||||
await coreApi.replaceNamespacedService(serverName, namespace, service);
|
||||
await coreApi.replaceNamespacedService({ name: serverName, namespace, body: service });
|
||||
console.log(`Updated Service for server ${server.id}`);
|
||||
} else {
|
||||
throw err;
|
||||
@@ -205,12 +207,11 @@ async function createDeployment(
|
||||
};
|
||||
|
||||
try {
|
||||
await appsApi.createNamespacedDeployment(namespace, deployment);
|
||||
await appsApi.createNamespacedDeployment({ namespace, body: deployment });
|
||||
console.log(`Created Deployment for server ${server.id}`);
|
||||
} catch (err: any) {
|
||||
if (err.response?.statusCode === 409) {
|
||||
// Deployment already exists, update it
|
||||
await appsApi.replaceNamespacedDeployment(serverName, namespace, deployment);
|
||||
await appsApi.replaceNamespacedDeployment({ name: serverName, namespace, body: deployment });
|
||||
console.log(`Updated Deployment for server ${server.id}`);
|
||||
} else {
|
||||
throw err;
|
||||
@@ -351,12 +352,15 @@ async function createStatefulSet(
|
||||
};
|
||||
|
||||
try {
|
||||
await appsApi.createNamespacedStatefulSet(namespace, statefulSet);
|
||||
await appsApi.createNamespacedStatefulSet({ namespace, body: statefulSet });
|
||||
console.log(`Created StatefulSet for server ${server.id}`);
|
||||
} catch (err: any) {
|
||||
if (err.response?.statusCode === 409) {
|
||||
// StatefulSet already exists, update it
|
||||
await appsApi.replaceNamespacedStatefulSet(serverName, namespace, statefulSet);
|
||||
await appsApi.replaceNamespacedStatefulSet({
|
||||
name: serverName,
|
||||
namespace,
|
||||
body: statefulSet,
|
||||
});
|
||||
console.log(`Updated StatefulSet for server ${server.id}`);
|
||||
} else {
|
||||
throw err;
|
||||
@@ -366,15 +370,14 @@ async function createStatefulSet(
|
||||
|
||||
export async function deleteServer(
|
||||
serverId: string,
|
||||
serverId2: string,
|
||||
appsApi: k8s.AppsV1Api,
|
||||
coreApi: k8s.CoreV1Api,
|
||||
namespace: string
|
||||
): Promise<void> {
|
||||
const serverName = `minecraft-${serverId2}`;
|
||||
const serverName = `minecraft-${serverId}`;
|
||||
|
||||
try {
|
||||
await appsApi.deleteNamespacedDeployment(serverName, namespace);
|
||||
await appsApi.deleteNamespacedDeployment({ name: serverName, namespace });
|
||||
console.log(`Deleted Deployment for server ${serverName}`);
|
||||
} catch (err: any) {
|
||||
if (err.response?.statusCode !== 404) {
|
||||
@@ -383,7 +386,7 @@ export async function deleteServer(
|
||||
}
|
||||
|
||||
try {
|
||||
await appsApi.deleteNamespacedStatefulSet(serverName, namespace);
|
||||
await appsApi.deleteNamespacedStatefulSet({ name: serverName, namespace });
|
||||
console.log(`Deleted StatefulSet for server ${serverName}`);
|
||||
} catch (err: any) {
|
||||
if (err.response?.statusCode !== 404) {
|
||||
@@ -392,7 +395,7 @@ export async function deleteServer(
|
||||
}
|
||||
|
||||
try {
|
||||
await coreApi.deleteNamespacedService(serverName, namespace);
|
||||
await coreApi.deleteNamespacedService({ name: serverName, namespace });
|
||||
console.log(`Deleted Service for server ${serverName}`);
|
||||
} catch (err: any) {
|
||||
if (err.response?.statusCode !== 404) {
|
||||
@@ -401,7 +404,7 @@ export async function deleteServer(
|
||||
}
|
||||
|
||||
try {
|
||||
await coreApi.deleteNamespacedConfigMap(`${serverName}-config`, namespace);
|
||||
await coreApi.deleteNamespacedConfigMap({ name: `${serverName}-config`, namespace });
|
||||
console.log(`Deleted ConfigMap for server ${serverName}`);
|
||||
} catch (err: any) {
|
||||
if (err.response?.statusCode !== 404) {
|
||||
|
||||
83
packages/k8s-operator/src/services/notification.service.ts
Normal file
83
packages/k8s-operator/src/services/notification.service.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import pg from "pg";
|
||||
|
||||
export class NotificationService {
|
||||
private pgClient: pg.Client | null = null;
|
||||
private handlers = new Map<string, Set<(payload: unknown) => void | Promise<void>>>();
|
||||
|
||||
async connect(connectionString: string): Promise<void> {
|
||||
if (!connectionString) {
|
||||
throw new Error("Database connection string is required");
|
||||
}
|
||||
|
||||
console.log("\n[NotificationService] Connecting to PostgreSQL...");
|
||||
this.pgClient = new pg.Client({ connectionString });
|
||||
await this.pgClient.connect();
|
||||
|
||||
this.pgClient.on("notification", async (msg) => {
|
||||
const handlers = this.handlers.get(msg.channel);
|
||||
if (!handlers) return;
|
||||
|
||||
try {
|
||||
const payload = msg.payload ? JSON.parse(msg.payload) : {};
|
||||
console.log(
|
||||
`\n[NotificationService] Received notification on channel '${msg.channel}':`,
|
||||
payload
|
||||
);
|
||||
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
await handler(payload);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[NotificationService] Error in handler for channel '${msg.channel}':`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[NotificationService] Failed to parse notification payload:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[NotificationService] Connected successfully");
|
||||
}
|
||||
|
||||
async listen(
|
||||
channel: string,
|
||||
handler: (payload: unknown) => void | Promise<void>
|
||||
): Promise<void> {
|
||||
if (!this.pgClient) {
|
||||
throw new Error("NotificationService not connected");
|
||||
}
|
||||
|
||||
if (!this.handlers.has(channel)) {
|
||||
this.handlers.set(channel, new Set());
|
||||
await this.pgClient.query(`LISTEN ${channel}`);
|
||||
console.log(`[NotificationService] Listening on channel: ${channel}`);
|
||||
}
|
||||
|
||||
this.handlers.get(channel)!.add(handler);
|
||||
}
|
||||
|
||||
async unlisten(channel: string): Promise<void> {
|
||||
if (!this.pgClient) return;
|
||||
|
||||
this.handlers.delete(channel);
|
||||
await this.pgClient.query(`UNLISTEN ${channel}`);
|
||||
console.log(`[NotificationService] Stopped listening on channel: ${channel}`);
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (!this.pgClient) return;
|
||||
|
||||
console.log("\n[NotificationService] Disconnecting...");
|
||||
await this.pgClient.end();
|
||||
this.pgClient = null;
|
||||
this.handlers.clear();
|
||||
console.log("[NotificationService] Disconnected");
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.pgClient !== null;
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,10 @@ async function registerCRDs(k8sClient: KubernetesClient): Promise<void> {
|
||||
console.log("Registering CRDs...");
|
||||
|
||||
try {
|
||||
await apiExtensionsClient.createCustomResourceDefinition(MINECRAFT_SERVER_CRD);
|
||||
await apiExtensionsClient.createCustomResourceDefinition({ body: MINECRAFT_SERVER_CRD });
|
||||
console.log(`MinecraftServer CRD created successfully (${API_GROUP}/${API_VERSION})`);
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 409) {
|
||||
// TODO: Handle conflict
|
||||
console.log("MinecraftServer CRD already exists");
|
||||
} else {
|
||||
console.error("Error creating MinecraftServer CRD:", error);
|
||||
@@ -39,11 +38,10 @@ async function registerCRDs(k8sClient: KubernetesClient): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
await apiExtensionsClient.createCustomResourceDefinition(REVERSE_PROXY_SERVER_CRD);
|
||||
await apiExtensionsClient.createCustomResourceDefinition({ body: REVERSE_PROXY_SERVER_CRD });
|
||||
console.log(`ReverseProxyServer CRD created successfully (${API_GROUP}/${API_VERSION})`);
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 409) {
|
||||
// TODO: Handle conflict
|
||||
console.log("ReverseProxyServer CRD already exists");
|
||||
} else {
|
||||
console.error("Error creating ReverseProxyServer CRD:", error);
|
||||
|
||||
9
packages/k8s-operator/src/utils/errors.ts
Normal file
9
packages/k8s-operator/src/utils/errors.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const getErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
return "Unknown error";
|
||||
};
|
||||
108
packages/k8s-operator/src/utils/kube-auth.ts
Normal file
108
packages/k8s-operator/src/utils/kube-auth.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { spawnSync } from "bun";
|
||||
import YAML from "yaml";
|
||||
|
||||
type KubeConfigDoc = {
|
||||
users?: Array<{ name: string; user: { token?: string } }>;
|
||||
contexts?: Array<{
|
||||
name: string;
|
||||
context: { cluster: string; user: string; namespace?: string };
|
||||
}>;
|
||||
clusters?: Array<{ name: string }>;
|
||||
};
|
||||
|
||||
import { NAMESPACE } from "../config/constants";
|
||||
|
||||
const SA_NAME = process.env.K8S_SA_NAME || "minikura-operator";
|
||||
const TOKEN_DURATION_HOURS = Number(process.env.K8S_TOKEN_DURATION_HOURS || 24);
|
||||
const TOKEN_REFRESH_MIN = Number(process.env.K8S_TOKEN_REFRESH_MIN || 60);
|
||||
|
||||
function kubeconfigPath(): string {
|
||||
return process.env.KUBECONFIG || `${process.env.HOME || process.env.USERPROFILE}/.kube/config`;
|
||||
}
|
||||
|
||||
function refreshSaToken(): void {
|
||||
const duration = `${TOKEN_DURATION_HOURS}h`;
|
||||
const args = ["kubectl", "-n", NAMESPACE, "create", "token", SA_NAME, "--duration", duration];
|
||||
|
||||
if (process.env.KUBERNETES_SKIP_TLS_VERIFY === "true") {
|
||||
args.push("--insecure-skip-tls-verify");
|
||||
}
|
||||
|
||||
const proc = spawnSync(args);
|
||||
|
||||
if (proc.exitCode !== 0) {
|
||||
console.error("[kube-auth] kubectl create token failed:", proc.stderr.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
const token = proc.stdout.toString().trim();
|
||||
const kcPath = kubeconfigPath();
|
||||
|
||||
if (!existsSync(kcPath)) {
|
||||
console.error("[kube-auth] kubeconfig not found at:", kcPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = YAML.parse(readFileSync(kcPath, "utf8")) as KubeConfigDoc;
|
||||
|
||||
let user = doc.users?.find((existingUser) => existingUser.name === SA_NAME);
|
||||
if (!user) {
|
||||
user = { name: SA_NAME, user: {} };
|
||||
if (!doc.users) doc.users = [];
|
||||
doc.users.push(user);
|
||||
}
|
||||
user.user = { token };
|
||||
|
||||
let ctx = doc.contexts?.find((context) => context.name === "bun-local-operator");
|
||||
if (!ctx) {
|
||||
const clusterName = doc.clusters?.[0]?.name || "default";
|
||||
ctx = {
|
||||
name: "bun-local-operator",
|
||||
context: {
|
||||
cluster: clusterName,
|
||||
user: SA_NAME,
|
||||
namespace: NAMESPACE,
|
||||
},
|
||||
};
|
||||
if (!doc.contexts) doc.contexts = [];
|
||||
doc.contexts.push(ctx);
|
||||
} else {
|
||||
ctx.context.user = SA_NAME;
|
||||
ctx.context.namespace = NAMESPACE;
|
||||
}
|
||||
|
||||
writeFileSync(kcPath, YAML.stringify(doc));
|
||||
console.log(
|
||||
`[kube-auth] kubeconfig updated with fresh token for ${SA_NAME} (expires in ${duration})`
|
||||
);
|
||||
}
|
||||
|
||||
export function buildKubeConfig(): KubeConfig {
|
||||
const kc = new KubeConfig();
|
||||
|
||||
const isInCluster =
|
||||
process.env.KUBERNETES_SERVICE_HOST &&
|
||||
existsSync("/var/run/secrets/kubernetes.io/serviceaccount/token");
|
||||
|
||||
if (isInCluster) {
|
||||
console.log("[kube-auth] Running in-cluster, loading from service account");
|
||||
kc.loadFromCluster();
|
||||
return kc;
|
||||
}
|
||||
|
||||
console.log("[kube-auth] Running locally, using ServiceAccount token auth");
|
||||
refreshSaToken();
|
||||
|
||||
setInterval(refreshSaToken, TOKEN_REFRESH_MIN * 60_000);
|
||||
|
||||
kc.loadFromDefault();
|
||||
try {
|
||||
kc.setCurrentContext("bun-local-operator");
|
||||
} catch (error) {
|
||||
console.warn("[kube-auth] Could not set bun-local-operator context, using default");
|
||||
}
|
||||
|
||||
return kc;
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
/**
|
||||
* Memory utility functions for Kubernetes resources
|
||||
*/
|
||||
export function calculateJavaMemory(memory: number | string, factor: number): string {
|
||||
if (typeof memory === "number") {
|
||||
const calculatedValue = Math.round(memory * factor);
|
||||
return `${calculatedValue}M`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate memory for Java (lower than what's requested to account for JVM overhead)
|
||||
* @param memoryString Memory string in format like "512M" or "1G"
|
||||
* @param factor Multiplicative factor to apply (e.g., 0.8 for 80%)
|
||||
* @returns Calculated memory string in same format
|
||||
*/
|
||||
export function calculateJavaMemory(memoryString: string, factor: number): string {
|
||||
const match = memoryString.match(/^(\d+)([MG])$/i);
|
||||
if (!match) return "512M"; // Default if format is not recognized
|
||||
const match = memory.match(/^(\d+)([MG])$/i);
|
||||
if (!match) return "512M";
|
||||
|
||||
const [, valueStr, unit] = match;
|
||||
const value = parseInt(valueStr, 10);
|
||||
@@ -19,14 +14,13 @@ export function calculateJavaMemory(memoryString: string, factor: number): strin
|
||||
return `${calculatedValue}${unit.toUpperCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert memory string to Kubernetes format (e.g., "1G" -> "1Gi")
|
||||
* @param memoryString Memory string in format like "512M" or "1G"
|
||||
* @returns Memory string in Kubernetes format
|
||||
*/
|
||||
export function convertToK8sFormat(memoryString: string): string {
|
||||
const match = memoryString.match(/^(\d+)([MG])$/i);
|
||||
if (!match) return "1Gi"; // Default if format is not recognized
|
||||
export function convertToK8sFormat(memory: number | string): string {
|
||||
if (typeof memory === "number") {
|
||||
return `${memory}Mi`;
|
||||
}
|
||||
|
||||
const match = memory.match(/^(\d+)([MG])$/i);
|
||||
if (!match) return "1Gi";
|
||||
|
||||
const [, valueStr, unit] = match;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function registerRBACResources(k8sClient: KubernetesClient): Promis
|
||||
async function registerNamespace(k8sClient: KubernetesClient): Promise<void> {
|
||||
try {
|
||||
const coreApi = k8sClient.getCoreApi();
|
||||
await coreApi.createNamespace(minikuraNamespace);
|
||||
await coreApi.createNamespace({ body: minikuraNamespace });
|
||||
console.log(`Created namespace ${minikuraNamespace.metadata.name}`);
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 409) {
|
||||
@@ -55,10 +55,10 @@ async function registerNamespace(k8sClient: KubernetesClient): Promise<void> {
|
||||
async function registerServiceAccount(k8sClient: KubernetesClient): Promise<void> {
|
||||
try {
|
||||
const coreApi = k8sClient.getCoreApi();
|
||||
await coreApi.createNamespacedServiceAccount(
|
||||
minikuraServiceAccount.metadata.namespace,
|
||||
minikuraServiceAccount
|
||||
);
|
||||
await coreApi.createNamespacedServiceAccount({
|
||||
namespace: minikuraServiceAccount.metadata.namespace,
|
||||
body: minikuraServiceAccount
|
||||
});
|
||||
console.log(`Created service account ${minikuraServiceAccount.metadata.name}`);
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 409) {
|
||||
@@ -74,10 +74,9 @@ async function registerServiceAccount(k8sClient: KubernetesClient): Promise<void
|
||||
*/
|
||||
async function registerClusterRole(k8sClient: KubernetesClient): Promise<void> {
|
||||
try {
|
||||
// TODO: I can't get this working with the k8s client, so I'm using fetch directly, fix later
|
||||
const kc = k8sClient.getKubeConfig();
|
||||
const opts = {};
|
||||
kc.applyToRequest(opts as any);
|
||||
const opts: any = {};
|
||||
await kc.applyToHTTPSOptions(opts);
|
||||
|
||||
// Get cluster URL
|
||||
const cluster = kc.getCurrentCluster();
|
||||
@@ -128,10 +127,9 @@ async function registerClusterRole(k8sClient: KubernetesClient): Promise<void> {
|
||||
*/
|
||||
async function registerClusterRoleBinding(k8sClient: KubernetesClient): Promise<void> {
|
||||
try {
|
||||
// We need to use the raw client for cluster roles
|
||||
const kc = k8sClient.getKubeConfig();
|
||||
const opts = {};
|
||||
kc.applyToRequest(opts as any);
|
||||
const opts: any = {};
|
||||
await kc.applyToHTTPSOptions(opts);
|
||||
|
||||
// Get cluster URL
|
||||
const cluster = kc.getCurrentCluster();
|
||||
@@ -212,11 +210,11 @@ export async function registerOperatorDeployment(
|
||||
|
||||
await k8sClient
|
||||
.getAppsApi()
|
||||
.replaceNamespacedDeployment(
|
||||
deployment.metadata.name,
|
||||
deployment.metadata.namespace,
|
||||
deployment
|
||||
);
|
||||
.replaceNamespacedDeployment({
|
||||
name: deployment.metadata.name,
|
||||
namespace: deployment.metadata.namespace,
|
||||
body: deployment
|
||||
});
|
||||
console.log(`Updated deployment ${deployment.metadata.name}`);
|
||||
} else {
|
||||
throw error;
|
||||
|
||||
19
packages/k8s-operator/src/utils/service-type.ts
Normal file
19
packages/k8s-operator/src/utils/service-type.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ServiceType } from "@minikura/db";
|
||||
|
||||
export function mapServiceType(
|
||||
serviceType?: ServiceType | null,
|
||||
defaultType: string = "ClusterIP"
|
||||
): string {
|
||||
if (!serviceType) return defaultType;
|
||||
|
||||
switch (serviceType) {
|
||||
case "CLUSTER_IP":
|
||||
return "ClusterIP";
|
||||
case "NODE_PORT":
|
||||
return "NodePort";
|
||||
case "LOAD_BALANCER":
|
||||
return "LoadBalancer";
|
||||
default:
|
||||
return defaultType;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user