mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-01-31 14:57:49 +00:00
🎨 style: auto format
This commit is contained in:
@@ -1,31 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "Minikura Development",
|
"name": "Minikura Development",
|
||||||
"dockerComposeFile": "./docker-compose.yml",
|
"dockerComposeFile": "./docker-compose.yml",
|
||||||
"service": "devcontainer",
|
"service": "devcontainer",
|
||||||
"workspaceFolder": "/workspace",
|
"workspaceFolder": "/workspace",
|
||||||
"remoteUser": "dev",
|
"remoteUser": "dev",
|
||||||
|
|
||||||
"postCreateCommand": "bash /workspace/.devcontainer/post-create.sh",
|
"postCreateCommand": "bash /workspace/.devcontainer/post-create.sh",
|
||||||
|
|
||||||
"forwardPorts": [3000, 3001, 5432, 6443, 25565, 25577],
|
"forwardPorts": [3000, 3001, 5432, 6443, 25565, 25577],
|
||||||
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"biomejs.biome",
|
"biomejs.biome",
|
||||||
"Prisma.prisma",
|
"Prisma.prisma",
|
||||||
"ms-kubernetes-tools.vscode-kubernetes-tools",
|
"ms-kubernetes-tools.vscode-kubernetes-tools",
|
||||||
"ms-azuretools.vscode-docker",
|
"ms-azuretools.vscode-docker",
|
||||||
"bradlc.vscode-tailwindcss",
|
"bradlc.vscode-tailwindcss",
|
||||||
"redhat.vscode-yaml"
|
"redhat.vscode-yaml"
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"[prisma]": {
|
"[prisma]": {
|
||||||
"editor.defaultFormatter": "Prisma.prisma"
|
"editor.defaultFormatter": "Prisma.prisma"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"[prisma]": {
|
"[prisma]": {
|
||||||
"editor.defaultFormatter": "Prisma.prisma"
|
"editor.defaultFormatter": "Prisma.prisma"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
import { dotenvLoad } from "dotenv-mono";
|
import { dotenvLoad } from "dotenv-mono";
|
||||||
const dotenv = dotenvLoad();
|
const dotenv = dotenvLoad();
|
||||||
|
|
||||||
export const API_GROUP = 'minikura.kirameki.cafe';
|
export const API_GROUP = "minikura.kirameki.cafe";
|
||||||
export const API_VERSION = 'v1alpha1';
|
export const API_VERSION = "v1alpha1";
|
||||||
|
|
||||||
export const KUBERNETES_NAMESPACE_ENV = process.env.KUBERNETES_NAMESPACE;
|
export const KUBERNETES_NAMESPACE_ENV = process.env.KUBERNETES_NAMESPACE;
|
||||||
export const NAMESPACE = process.env.KUBERNETES_NAMESPACE || 'minikura';
|
export const NAMESPACE = process.env.KUBERNETES_NAMESPACE || "minikura";
|
||||||
|
|
||||||
export const ENABLE_CRD_REFLECTION = process.env.ENABLE_CRD_REFLECTION === 'true';
|
export const ENABLE_CRD_REFLECTION = process.env.ENABLE_CRD_REFLECTION === "true";
|
||||||
export const SKIP_TLS_VERIFY = process.env.KUBERNETES_SKIP_TLS_VERIFY === 'true';
|
export const SKIP_TLS_VERIFY = process.env.KUBERNETES_SKIP_TLS_VERIFY === "true";
|
||||||
|
|
||||||
if (SKIP_TLS_VERIFY) {
|
if (SKIP_TLS_VERIFY) {
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resource types
|
// Resource types
|
||||||
export const RESOURCE_TYPES = {
|
export const RESOURCE_TYPES = {
|
||||||
MINECRAFT_SERVER: {
|
MINECRAFT_SERVER: {
|
||||||
kind: 'MinecraftServer',
|
kind: "MinecraftServer",
|
||||||
plural: 'minecraftservers',
|
plural: "minecraftservers",
|
||||||
singular: 'minecraftserver',
|
singular: "minecraftserver",
|
||||||
shortNames: ['mcs'],
|
shortNames: ["mcs"],
|
||||||
},
|
},
|
||||||
REVERSE_PROXY_SERVER: {
|
REVERSE_PROXY_SERVER: {
|
||||||
kind: 'ReverseProxyServer',
|
kind: "ReverseProxyServer",
|
||||||
plural: 'reverseproxyservers',
|
plural: "reverseproxyservers",
|
||||||
singular: 'reverseproxyserver',
|
singular: "reverseproxyserver",
|
||||||
shortNames: ['rps'],
|
shortNames: ["rps"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Kubernetes resource label prefixes
|
// Kubernetes resource label prefixes
|
||||||
export const LABEL_PREFIX = 'minikura.kirameki.cafe';
|
export const LABEL_PREFIX = "minikura.kirameki.cafe";
|
||||||
|
|
||||||
// Polling intervals (in milliseconds)
|
// Polling intervals (in milliseconds)
|
||||||
export const SYNC_INTERVAL = 30 * 1000; // 30 seconds
|
export const SYNC_INTERVAL = 30 * 1000; // 30 seconds
|
||||||
|
|
||||||
export const IMAGES = {
|
export const IMAGES = {
|
||||||
MINECRAFT: 'itzg/minecraft-server',
|
MINECRAFT: "itzg/minecraft-server",
|
||||||
REVERSE_PROXY: 'itzg/minecraft-server',
|
REVERSE_PROXY: "itzg/minecraft-server",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULTS = {
|
export const DEFAULTS = {
|
||||||
MEMORY: '1G',
|
MEMORY: "1G",
|
||||||
CPU_REQUEST: '250m',
|
CPU_REQUEST: "250m",
|
||||||
CPU_LIMIT: '1000m',
|
CPU_LIMIT: "1000m",
|
||||||
STORAGE_SIZE: '1Gi',
|
STORAGE_SIZE: "1Gi",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PrismaClient } from '@minikura/db';
|
import type { PrismaClient } from "@minikura/db";
|
||||||
import { KubernetesClient } from '../utils/k8s-client';
|
import { KubernetesClient } from "../utils/k8s-client";
|
||||||
import { SYNC_INTERVAL } from '../config/constants';
|
import { SYNC_INTERVAL } from "../config/constants";
|
||||||
|
|
||||||
export abstract class BaseController {
|
export abstract class BaseController {
|
||||||
protected prisma: PrismaClient;
|
protected prisma: PrismaClient;
|
||||||
@@ -21,14 +21,14 @@ export abstract class BaseController {
|
|||||||
console.log(`Starting to watch for changes in ${this.getControllerName()}...`);
|
console.log(`Starting to watch for changes in ${this.getControllerName()}...`);
|
||||||
|
|
||||||
// Initial sync
|
// Initial sync
|
||||||
this.syncResources().catch(err => {
|
this.syncResources().catch((err) => {
|
||||||
console.error(`Error during initial sync of ${this.getControllerName()}:`, err);
|
console.error(`Error during initial sync of ${this.getControllerName()}:`, err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Polling interval for changes
|
// Polling interval for changes
|
||||||
// TODO: Maybe there's a better way to do this
|
// TODO: Maybe there's a better way to do this
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
this.syncResources().catch(err => {
|
this.syncResources().catch((err) => {
|
||||||
console.error(`Error syncing ${this.getControllerName()}:`, err);
|
console.error(`Error syncing ${this.getControllerName()}:`, err);
|
||||||
});
|
});
|
||||||
}, SYNC_INTERVAL);
|
}, SYNC_INTERVAL);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { PrismaClient } from '@minikura/db';
|
import type { PrismaClient } from "@minikura/db";
|
||||||
import type { ReverseProxyServer, CustomEnvironmentVariable } from '@minikura/db';
|
import type { ReverseProxyServer, CustomEnvironmentVariable } from "@minikura/db";
|
||||||
import { BaseController } from './base-controller';
|
import { BaseController } from "./base-controller";
|
||||||
import type { ReverseProxyConfig } from '../types';
|
import type { ReverseProxyConfig } from "../types";
|
||||||
import { createReverseProxyServer, deleteReverseProxyServer } from '../resources/reverseProxyServer';
|
import {
|
||||||
|
createReverseProxyServer,
|
||||||
|
deleteReverseProxyServer,
|
||||||
|
} from "../resources/reverseProxyServer";
|
||||||
|
|
||||||
type ReverseProxyWithEnvVars = ReverseProxyServer & {
|
type ReverseProxyWithEnvVars = ReverseProxyServer & {
|
||||||
env_variables: CustomEnvironmentVariable[];
|
env_variables: CustomEnvironmentVariable[];
|
||||||
@@ -16,7 +19,7 @@ export class ReverseProxyController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getControllerName(): string {
|
protected getControllerName(): string {
|
||||||
return 'ReverseProxyController';
|
return "ReverseProxyController";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async syncResources(): Promise<void> {
|
protected async syncResources(): Promise<void> {
|
||||||
@@ -25,18 +28,20 @@ export class ReverseProxyController extends BaseController {
|
|||||||
const coreApi = this.k8sClient.getCoreApi();
|
const coreApi = this.k8sClient.getCoreApi();
|
||||||
const networkingApi = this.k8sClient.getNetworkingApi();
|
const networkingApi = this.k8sClient.getNetworkingApi();
|
||||||
|
|
||||||
const proxies = await this.prisma.reverseProxyServer.findMany({
|
const proxies = (await this.prisma.reverseProxyServer.findMany({
|
||||||
include: {
|
include: {
|
||||||
env_variables: true,
|
env_variables: true,
|
||||||
}
|
},
|
||||||
}) as ReverseProxyWithEnvVars[];
|
})) as ReverseProxyWithEnvVars[];
|
||||||
|
|
||||||
const currentProxyIds = new Set(proxies.map(proxy => proxy.id));
|
const currentProxyIds = new Set(proxies.map((proxy) => proxy.id));
|
||||||
|
|
||||||
// Delete reverse proxy servers that are no longer in the database
|
// Delete reverse proxy servers that are no longer in the database
|
||||||
for (const [proxyId, proxy] of this.deployedProxies.entries()) {
|
for (const [proxyId, proxy] of this.deployedProxies.entries()) {
|
||||||
if (!currentProxyIds.has(proxyId)) {
|
if (!currentProxyIds.has(proxyId)) {
|
||||||
console.log(`Reverse proxy server ${proxy.id} (${proxyId}) has been removed from the database, deleting from Kubernetes...`);
|
console.log(
|
||||||
|
`Reverse proxy server ${proxy.id} (${proxyId}) has been removed from the database, deleting from Kubernetes...`
|
||||||
|
);
|
||||||
await deleteReverseProxyServer(proxy.id, proxy.type, appsApi, coreApi, this.namespace);
|
await deleteReverseProxyServer(proxy.id, proxy.type, appsApi, coreApi, this.namespace);
|
||||||
this.deployedProxies.delete(proxyId);
|
this.deployedProxies.delete(proxyId);
|
||||||
}
|
}
|
||||||
@@ -48,7 +53,9 @@ export class ReverseProxyController extends BaseController {
|
|||||||
|
|
||||||
// If proxy doesn't exist yet or has been updated
|
// If proxy doesn't exist yet or has been updated
|
||||||
if (!deployedProxy || this.hasProxyChanged(deployedProxy, proxy)) {
|
if (!deployedProxy || this.hasProxyChanged(deployedProxy, proxy)) {
|
||||||
console.log(`${!deployedProxy ? 'Creating' : 'Updating'} reverse proxy server ${proxy.id} (${proxy.id}) in Kubernetes...`);
|
console.log(
|
||||||
|
`${!deployedProxy ? "Creating" : "Updating"} reverse proxy server ${proxy.id} (${proxy.id}) in Kubernetes...`
|
||||||
|
);
|
||||||
|
|
||||||
const proxyConfig: ReverseProxyConfig = {
|
const proxyConfig: ReverseProxyConfig = {
|
||||||
id: proxy.id,
|
id: proxy.id,
|
||||||
@@ -59,10 +66,10 @@ export class ReverseProxyController extends BaseController {
|
|||||||
apiKey: proxy.api_key,
|
apiKey: proxy.api_key,
|
||||||
type: proxy.type,
|
type: proxy.type,
|
||||||
memory: proxy.memory,
|
memory: proxy.memory,
|
||||||
env_variables: proxy.env_variables?.map(ev => ({
|
env_variables: proxy.env_variables?.map((ev) => ({
|
||||||
key: ev.key,
|
key: ev.key,
|
||||||
value: ev.value
|
value: ev.value,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
await createReverseProxyServer(
|
await createReverseProxyServer(
|
||||||
@@ -78,7 +85,7 @@ export class ReverseProxyController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error syncing reverse proxy servers:', error);
|
console.error("Error syncing reverse proxy servers:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +109,7 @@ export class ReverseProxyController extends BaseController {
|
|||||||
|
|
||||||
if (oldEnvVars.length !== newEnvVars.length) return true;
|
if (oldEnvVars.length !== newEnvVars.length) return true;
|
||||||
for (const newEnv of newEnvVars) {
|
for (const newEnv of newEnvVars) {
|
||||||
const oldEnv = oldEnvVars.find(e => e.key === newEnv.key);
|
const oldEnv = oldEnvVars.find((e) => e.key === newEnv.key);
|
||||||
if (!oldEnv || oldEnv.value !== newEnv.value) {
|
if (!oldEnv || oldEnv.value !== newEnv.value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { PrismaClient, ServerType } from '@minikura/db';
|
import { type PrismaClient, ServerType } from "@minikura/db";
|
||||||
import type { Server, CustomEnvironmentVariable } from '@minikura/db';
|
import type { Server, CustomEnvironmentVariable } from "@minikura/db";
|
||||||
import { BaseController } from './base-controller';
|
import { BaseController } from "./base-controller";
|
||||||
import type { ServerConfig } from '../types';
|
import type { ServerConfig } from "../types";
|
||||||
import { createServer, deleteServer } from '../resources/server';
|
import { createServer, deleteServer } from "../resources/server";
|
||||||
|
|
||||||
type ServerWithEnvVars = Server & {
|
type ServerWithEnvVars = Server & {
|
||||||
env_variables: CustomEnvironmentVariable[];
|
env_variables: CustomEnvironmentVariable[];
|
||||||
@@ -16,7 +16,7 @@ export class ServerController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getControllerName(): string {
|
protected getControllerName(): string {
|
||||||
return 'ServerController';
|
return "ServerController";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async syncResources(): Promise<void> {
|
protected async syncResources(): Promise<void> {
|
||||||
@@ -25,18 +25,20 @@ export class ServerController extends BaseController {
|
|||||||
const coreApi = this.k8sClient.getCoreApi();
|
const coreApi = this.k8sClient.getCoreApi();
|
||||||
const networkingApi = this.k8sClient.getNetworkingApi();
|
const networkingApi = this.k8sClient.getNetworkingApi();
|
||||||
|
|
||||||
const servers = await this.prisma.server.findMany({
|
const servers = (await this.prisma.server.findMany({
|
||||||
include: {
|
include: {
|
||||||
env_variables: true,
|
env_variables: true,
|
||||||
}
|
},
|
||||||
}) as ServerWithEnvVars[];
|
})) as ServerWithEnvVars[];
|
||||||
|
|
||||||
const currentServerIds = new Set(servers.map(server => server.id));
|
const currentServerIds = new Set(servers.map((server) => server.id));
|
||||||
|
|
||||||
// Delete servers that are no longer in the database
|
// Delete servers that are no longer in the database
|
||||||
for (const [serverId, server] of this.deployedServers.entries()) {
|
for (const [serverId, server] of this.deployedServers.entries()) {
|
||||||
if (!currentServerIds.has(serverId)) {
|
if (!currentServerIds.has(serverId)) {
|
||||||
console.log(`Server ${server.id} (${serverId}) has been removed from the database, deleting from Kubernetes...`);
|
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, server.id, appsApi, coreApi, this.namespace);
|
||||||
this.deployedServers.delete(serverId);
|
this.deployedServers.delete(serverId);
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,9 @@ export class ServerController extends BaseController {
|
|||||||
|
|
||||||
// If server doesn't exist yet or has been updated
|
// If server doesn't exist yet or has been updated
|
||||||
if (!deployedServer || this.hasServerChanged(deployedServer, server)) {
|
if (!deployedServer || this.hasServerChanged(deployedServer, server)) {
|
||||||
console.log(`${!deployedServer ? 'Creating' : 'Updating'} server ${server.id} (${server.id}) in Kubernetes...`);
|
console.log(
|
||||||
|
`${!deployedServer ? "Creating" : "Updating"} server ${server.id} (${server.id}) in Kubernetes...`
|
||||||
|
);
|
||||||
|
|
||||||
const serverConfig: ServerConfig = {
|
const serverConfig: ServerConfig = {
|
||||||
id: server.id,
|
id: server.id,
|
||||||
@@ -57,34 +61,25 @@ export class ServerController extends BaseController {
|
|||||||
description: server.description,
|
description: server.description,
|
||||||
listen_port: server.listen_port,
|
listen_port: server.listen_port,
|
||||||
memory: server.memory,
|
memory: server.memory,
|
||||||
env_variables: server.env_variables?.map(ev => ({
|
env_variables: server.env_variables?.map((ev) => ({
|
||||||
key: ev.key,
|
key: ev.key,
|
||||||
value: ev.value
|
value: ev.value,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
await createServer(
|
await createServer(serverConfig, appsApi, coreApi, networkingApi, this.namespace);
|
||||||
serverConfig,
|
|
||||||
appsApi,
|
|
||||||
coreApi,
|
|
||||||
networkingApi,
|
|
||||||
this.namespace
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
this.deployedServers.set(server.id, { ...server });
|
this.deployedServers.set(server.id, { ...server });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error syncing servers:', error);
|
console.error("Error syncing servers:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasServerChanged(
|
private hasServerChanged(oldServer: ServerWithEnvVars, newServer: ServerWithEnvVars): boolean {
|
||||||
oldServer: ServerWithEnvVars,
|
|
||||||
newServer: ServerWithEnvVars
|
|
||||||
): boolean {
|
|
||||||
// Check basic properties
|
// Check basic properties
|
||||||
const basicPropsChanged =
|
const basicPropsChanged =
|
||||||
oldServer.type !== newServer.type ||
|
oldServer.type !== newServer.type ||
|
||||||
@@ -102,7 +97,7 @@ export class ServerController extends BaseController {
|
|||||||
|
|
||||||
// Check if any of the existing env vars have changed
|
// Check if any of the existing env vars have changed
|
||||||
for (const newEnv of newEnvVars) {
|
for (const newEnv of newEnvVars) {
|
||||||
const oldEnv = oldEnvVars.find(e => e.key === newEnv.key);
|
const oldEnv = oldEnvVars.find((e) => e.key === newEnv.key);
|
||||||
if (!oldEnv || oldEnv.value !== newEnv.value) {
|
if (!oldEnv || oldEnv.value !== newEnv.value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { NAMESPACE } from '../config/constants';
|
import { NAMESPACE } from "../config/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace definition
|
* Namespace definition
|
||||||
*/
|
*/
|
||||||
export const minikuraNamespace = {
|
export const minikuraNamespace = {
|
||||||
apiVersion: 'v1',
|
apiVersion: "v1",
|
||||||
kind: 'Namespace',
|
kind: "Namespace",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: NAMESPACE,
|
name: NAMESPACE,
|
||||||
},
|
},
|
||||||
@@ -15,10 +15,10 @@ export const minikuraNamespace = {
|
|||||||
* Service account
|
* Service account
|
||||||
*/
|
*/
|
||||||
export const minikuraServiceAccount = {
|
export const minikuraServiceAccount = {
|
||||||
apiVersion: 'v1',
|
apiVersion: "v1",
|
||||||
kind: 'ServiceAccount',
|
kind: "ServiceAccount",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: 'minikura-operator',
|
name: "minikura-operator",
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -27,41 +27,41 @@ export const minikuraServiceAccount = {
|
|||||||
* Cluster role
|
* Cluster role
|
||||||
*/
|
*/
|
||||||
export const minikuraClusterRole = {
|
export const minikuraClusterRole = {
|
||||||
apiVersion: 'rbac.authorization.k8s.io/v1',
|
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||||
kind: 'ClusterRole',
|
kind: "ClusterRole",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: 'minikura-operator-role',
|
name: "minikura-operator-role",
|
||||||
},
|
},
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
apiGroups: [''],
|
apiGroups: [""],
|
||||||
resources: ['configmaps', 'services', 'secrets'],
|
resources: ["configmaps", "services", "secrets"],
|
||||||
verbs: ['get', 'list', 'watch', 'create', 'update', 'patch', 'delete'],
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiGroups: ['apps'],
|
apiGroups: ["apps"],
|
||||||
resources: ['deployments', 'statefulsets'],
|
resources: ["deployments", "statefulsets"],
|
||||||
verbs: ['get', 'list', 'watch', 'create', 'update', 'patch', 'delete'],
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiGroups: ['networking.k8s.io'],
|
apiGroups: ["networking.k8s.io"],
|
||||||
resources: ['ingresses'],
|
resources: ["ingresses"],
|
||||||
verbs: ['get', 'list', 'watch', 'create', 'update', 'patch', 'delete'],
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiGroups: ['apiextensions.k8s.io'],
|
apiGroups: ["apiextensions.k8s.io"],
|
||||||
resources: ['customresourcedefinitions'],
|
resources: ["customresourcedefinitions"],
|
||||||
verbs: ['get', 'list', 'watch', 'create', 'update', 'patch', 'delete'],
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiGroups: ['minikura.kirameki.cafe'],
|
apiGroups: ["minikura.kirameki.cafe"],
|
||||||
resources: ['minecraftservers', 'velocityproxies'],
|
resources: ["minecraftservers", "velocityproxies"],
|
||||||
verbs: ['get', 'list', 'watch', 'create', 'update', 'patch', 'delete'],
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiGroups: ['minikura.kirameki.cafe'],
|
apiGroups: ["minikura.kirameki.cafe"],
|
||||||
resources: ['minecraftservers/status', 'velocityproxies/status'],
|
resources: ["minecraftservers/status", "velocityproxies/status"],
|
||||||
verbs: ['get', 'update', 'patch'],
|
verbs: ["get", "update", "patch"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -70,22 +70,22 @@ export const minikuraClusterRole = {
|
|||||||
* Cluster role binding
|
* Cluster role binding
|
||||||
*/
|
*/
|
||||||
export const minikuraClusterRoleBinding = {
|
export const minikuraClusterRoleBinding = {
|
||||||
apiVersion: 'rbac.authorization.k8s.io/v1',
|
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||||
kind: 'ClusterRoleBinding',
|
kind: "ClusterRoleBinding",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: 'minikura-operator-role-binding',
|
name: "minikura-operator-role-binding",
|
||||||
},
|
},
|
||||||
subjects: [
|
subjects: [
|
||||||
{
|
{
|
||||||
kind: 'ServiceAccount',
|
kind: "ServiceAccount",
|
||||||
name: 'minikura-operator',
|
name: "minikura-operator",
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
roleRef: {
|
roleRef: {
|
||||||
kind: 'ClusterRole',
|
kind: "ClusterRole",
|
||||||
name: 'minikura-operator-role',
|
name: "minikura-operator-role",
|
||||||
apiGroup: 'rbac.authorization.k8s.io',
|
apiGroup: "rbac.authorization.k8s.io",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,70 +93,70 @@ export const minikuraClusterRoleBinding = {
|
|||||||
* Deployment for the Minikura operator
|
* Deployment for the Minikura operator
|
||||||
*/
|
*/
|
||||||
export const minikuraOperatorDeployment = {
|
export const minikuraOperatorDeployment = {
|
||||||
apiVersion: 'apps/v1',
|
apiVersion: "apps/v1",
|
||||||
kind: 'Deployment',
|
kind: "Deployment",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: 'minikura-operator',
|
name: "minikura-operator",
|
||||||
namespace: NAMESPACE,
|
namespace: NAMESPACE,
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
replicas: 1,
|
replicas: 1,
|
||||||
selector: {
|
selector: {
|
||||||
matchLabels: {
|
matchLabels: {
|
||||||
app: 'minikura-operator',
|
app: "minikura-operator",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
metadata: {
|
metadata: {
|
||||||
labels: {
|
labels: {
|
||||||
app: 'minikura-operator',
|
app: "minikura-operator",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
serviceAccountName: 'minikura-operator',
|
serviceAccountName: "minikura-operator",
|
||||||
containers: [
|
containers: [
|
||||||
{
|
{
|
||||||
name: 'operator',
|
name: "operator",
|
||||||
image: '${REGISTRY_URL}/minikura-operator:latest',
|
image: "${REGISTRY_URL}/minikura-operator:latest",
|
||||||
env: [
|
env: [
|
||||||
{
|
{
|
||||||
name: 'DATABASE_URL',
|
name: "DATABASE_URL",
|
||||||
valueFrom: {
|
valueFrom: {
|
||||||
secretKeyRef: {
|
secretKeyRef: {
|
||||||
name: 'minikura-operator-secrets',
|
name: "minikura-operator-secrets",
|
||||||
key: 'DATABASE_URL',
|
key: "DATABASE_URL",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'KUBERNETES_NAMESPACE',
|
name: "KUBERNETES_NAMESPACE",
|
||||||
value: NAMESPACE,
|
value: NAMESPACE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'USE_CRDS',
|
name: "USE_CRDS",
|
||||||
value: 'true',
|
value: "true",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
resources: {
|
resources: {
|
||||||
requests: {
|
requests: {
|
||||||
memory: '256Mi',
|
memory: "256Mi",
|
||||||
cpu: '200m',
|
cpu: "200m",
|
||||||
},
|
},
|
||||||
limits: {
|
limits: {
|
||||||
memory: '512Mi',
|
memory: "512Mi",
|
||||||
cpu: '500m',
|
cpu: "500m",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
livenessProbe: {
|
livenessProbe: {
|
||||||
exec: {
|
exec: {
|
||||||
command: ['bun', '-e', "console.log('Health check')"],
|
command: ["bun", "-e", "console.log('Health check')"],
|
||||||
},
|
},
|
||||||
initialDelaySeconds: 30,
|
initialDelaySeconds: 30,
|
||||||
periodSeconds: 30,
|
periodSeconds: 30,
|
||||||
},
|
},
|
||||||
readinessProbe: {
|
readinessProbe: {
|
||||||
exec: {
|
exec: {
|
||||||
command: ['bun', '-e', "console.log('Ready check')"],
|
command: ["bun", "-e", "console.log('Ready check')"],
|
||||||
},
|
},
|
||||||
initialDelaySeconds: 5,
|
initialDelaySeconds: 5,
|
||||||
periodSeconds: 10,
|
periodSeconds: 10,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { API_GROUP, API_VERSION, RESOURCE_TYPES } from '../config/constants';
|
import { API_GROUP, API_VERSION, RESOURCE_TYPES } from "../config/constants";
|
||||||
|
|
||||||
export const REVERSE_PROXY_SERVER_CRD = {
|
export const REVERSE_PROXY_SERVER_CRD = {
|
||||||
apiVersion: 'apiextensions.k8s.io/v1',
|
apiVersion: "apiextensions.k8s.io/v1",
|
||||||
kind: 'CustomResourceDefinition',
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${RESOURCE_TYPES.REVERSE_PROXY_SERVER.plural}.${API_GROUP}`,
|
name: `${RESOURCE_TYPES.REVERSE_PROXY_SERVER.plural}.${API_GROUP}`,
|
||||||
},
|
},
|
||||||
@@ -15,67 +15,67 @@ export const REVERSE_PROXY_SERVER_CRD = {
|
|||||||
storage: true,
|
storage: true,
|
||||||
schema: {
|
schema: {
|
||||||
openAPIV3Schema: {
|
openAPIV3Schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
spec: {
|
spec: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
required: ['id', 'external_address', 'external_port'],
|
required: ["id", "external_address", "external_port"],
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
pattern: '^[a-zA-Z0-9-_]+$',
|
pattern: "^[a-zA-Z0-9-_]+$",
|
||||||
description: 'ID of the reverse proxy server',
|
description: "ID of the reverse proxy server",
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Optional description of the server',
|
description: "Optional description of the server",
|
||||||
},
|
},
|
||||||
external_address: {
|
external_address: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
description: 'External address of the proxy server',
|
description: "External address of the proxy server",
|
||||||
},
|
},
|
||||||
external_port: {
|
external_port: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 65535,
|
maximum: 65535,
|
||||||
description: 'External port of the proxy server',
|
description: "External port of the proxy server",
|
||||||
},
|
},
|
||||||
listen_port: {
|
listen_port: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 65535,
|
maximum: 65535,
|
||||||
default: 25565,
|
default: 25565,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Port the proxy server listens on internally',
|
description: "Port the proxy server listens on internally",
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
enum: ['VELOCITY', 'BUNGEECORD'],
|
enum: ["VELOCITY", "BUNGEECORD"],
|
||||||
default: 'VELOCITY',
|
default: "VELOCITY",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Type of the reverse proxy server',
|
description: "Type of the reverse proxy server",
|
||||||
},
|
},
|
||||||
memory: {
|
memory: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
default: '512M',
|
default: "512M",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Memory allocation for the server',
|
description: "Memory allocation for the server",
|
||||||
},
|
},
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
type: 'array',
|
type: "array",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
required: ['key', 'value'],
|
required: ["key", "value"],
|
||||||
properties: {
|
properties: {
|
||||||
key: {
|
key: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
description: 'Environment variable key',
|
description: "Environment variable key",
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
description: 'Environment variable value',
|
description: "Environment variable value",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -83,33 +83,33 @@ export const REVERSE_PROXY_SERVER_CRD = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
properties: {
|
properties: {
|
||||||
phase: {
|
phase: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
enum: ['Pending', 'Running', 'Failed'],
|
enum: ["Pending", "Running", "Failed"],
|
||||||
description: 'Current phase of the server',
|
description: "Current phase of the server",
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Detailed message about the current status',
|
description: "Detailed message about the current status",
|
||||||
},
|
},
|
||||||
apiKey: {
|
apiKey: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'API key for server communication',
|
description: "API key for server communication",
|
||||||
},
|
},
|
||||||
internalId: {
|
internalId: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Internal ID assigned by Minikura',
|
description: "Internal ID assigned by Minikura",
|
||||||
},
|
},
|
||||||
lastSyncedAt: {
|
lastSyncedAt: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Last time the server was synced with Kubernetes',
|
description: "Last time the server was synced with Kubernetes",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -118,34 +118,34 @@ export const REVERSE_PROXY_SERVER_CRD = {
|
|||||||
},
|
},
|
||||||
additionalPrinterColumns: [
|
additionalPrinterColumns: [
|
||||||
{
|
{
|
||||||
name: 'Type',
|
name: "Type",
|
||||||
type: 'string',
|
type: "string",
|
||||||
jsonPath: '.spec.type',
|
jsonPath: ".spec.type",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'External Address',
|
name: "External Address",
|
||||||
type: 'string',
|
type: "string",
|
||||||
jsonPath: '.spec.external_address',
|
jsonPath: ".spec.external_address",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'External Port',
|
name: "External Port",
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
jsonPath: '.spec.external_port',
|
jsonPath: ".spec.external_port",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Status',
|
name: "Status",
|
||||||
type: 'string',
|
type: "string",
|
||||||
jsonPath: '.status.phase',
|
jsonPath: ".status.phase",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Age',
|
name: "Age",
|
||||||
type: 'date',
|
type: "date",
|
||||||
jsonPath: '.metadata.creationTimestamp',
|
jsonPath: ".metadata.creationTimestamp",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
scope: 'Namespaced',
|
scope: "Namespaced",
|
||||||
names: {
|
names: {
|
||||||
singular: RESOURCE_TYPES.REVERSE_PROXY_SERVER.singular,
|
singular: RESOURCE_TYPES.REVERSE_PROXY_SERVER.singular,
|
||||||
plural: RESOURCE_TYPES.REVERSE_PROXY_SERVER.plural,
|
plural: RESOURCE_TYPES.REVERSE_PROXY_SERVER.plural,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { API_GROUP, API_VERSION, RESOURCE_TYPES } from '../config/constants';
|
import { API_GROUP, API_VERSION, RESOURCE_TYPES } from "../config/constants";
|
||||||
|
|
||||||
export const MINECRAFT_SERVER_CRD = {
|
export const MINECRAFT_SERVER_CRD = {
|
||||||
apiVersion: 'apiextensions.k8s.io/v1',
|
apiVersion: "apiextensions.k8s.io/v1",
|
||||||
kind: 'CustomResourceDefinition',
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${RESOURCE_TYPES.MINECRAFT_SERVER.plural}.${API_GROUP}`,
|
name: `${RESOURCE_TYPES.MINECRAFT_SERVER.plural}.${API_GROUP}`,
|
||||||
},
|
},
|
||||||
@@ -15,53 +15,53 @@ export const MINECRAFT_SERVER_CRD = {
|
|||||||
storage: true,
|
storage: true,
|
||||||
schema: {
|
schema: {
|
||||||
openAPIV3Schema: {
|
openAPIV3Schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
spec: {
|
spec: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
required: ['id', 'type', 'listen_port'],
|
required: ["id", "type", "listen_port"],
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
pattern: '^[a-zA-Z0-9-_]+$',
|
pattern: "^[a-zA-Z0-9-_]+$",
|
||||||
description: 'ID of the Minecraft server',
|
description: "ID of the Minecraft server",
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Optional description of the server',
|
description: "Optional description of the server",
|
||||||
},
|
},
|
||||||
listen_port: {
|
listen_port: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 65535,
|
maximum: 65535,
|
||||||
description: 'Port the server listens on',
|
description: "Port the server listens on",
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
enum: ['STATEFUL', 'STATELESS'],
|
enum: ["STATEFUL", "STATELESS"],
|
||||||
description: 'Type of the server',
|
description: "Type of the server",
|
||||||
},
|
},
|
||||||
memory: {
|
memory: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
default: '1G',
|
default: "1G",
|
||||||
description: 'Memory allocation for the server',
|
description: "Memory allocation for the server",
|
||||||
},
|
},
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
type: 'array',
|
type: "array",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
required: ['key', 'value'],
|
required: ["key", "value"],
|
||||||
properties: {
|
properties: {
|
||||||
key: {
|
key: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
description: 'Environment variable key',
|
description: "Environment variable key",
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
description: 'Environment variable value',
|
description: "Environment variable value",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -69,33 +69,33 @@ export const MINECRAFT_SERVER_CRD = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
properties: {
|
properties: {
|
||||||
phase: {
|
phase: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
enum: ['Pending', 'Running', 'Failed'],
|
enum: ["Pending", "Running", "Failed"],
|
||||||
description: 'Current phase of the server',
|
description: "Current phase of the server",
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Detailed message about the current status',
|
description: "Detailed message about the current status",
|
||||||
},
|
},
|
||||||
apiKey: {
|
apiKey: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'API key for server communication',
|
description: "API key for server communication",
|
||||||
},
|
},
|
||||||
internalId: {
|
internalId: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Internal ID assigned by Minikura',
|
description: "Internal ID assigned by Minikura",
|
||||||
},
|
},
|
||||||
lastSyncedAt: {
|
lastSyncedAt: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'Last time the server was synced with Kubernetes',
|
description: "Last time the server was synced with Kubernetes",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -104,24 +104,24 @@ export const MINECRAFT_SERVER_CRD = {
|
|||||||
},
|
},
|
||||||
additionalPrinterColumns: [
|
additionalPrinterColumns: [
|
||||||
{
|
{
|
||||||
name: 'Type',
|
name: "Type",
|
||||||
type: 'string',
|
type: "string",
|
||||||
jsonPath: '.spec.type',
|
jsonPath: ".spec.type",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Status',
|
name: "Status",
|
||||||
type: 'string',
|
type: "string",
|
||||||
jsonPath: '.status.phase',
|
jsonPath: ".status.phase",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Age',
|
name: "Age",
|
||||||
type: 'date',
|
type: "date",
|
||||||
jsonPath: '.metadata.creationTimestamp',
|
jsonPath: ".metadata.creationTimestamp",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
scope: 'Namespaced',
|
scope: "Namespaced",
|
||||||
names: {
|
names: {
|
||||||
singular: RESOURCE_TYPES.MINECRAFT_SERVER.singular,
|
singular: RESOURCE_TYPES.MINECRAFT_SERVER.singular,
|
||||||
plural: RESOURCE_TYPES.MINECRAFT_SERVER.plural,
|
plural: RESOURCE_TYPES.MINECRAFT_SERVER.plural,
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { dotenvLoad } from "dotenv-mono";
|
import { dotenvLoad } from "dotenv-mono";
|
||||||
const dotenv = dotenvLoad();
|
const dotenv = dotenvLoad();
|
||||||
|
|
||||||
import { NAMESPACE, KUBERNETES_NAMESPACE_ENV, ENABLE_CRD_REFLECTION } from './config/constants';
|
import { NAMESPACE, KUBERNETES_NAMESPACE_ENV, ENABLE_CRD_REFLECTION } from "./config/constants";
|
||||||
import { prisma } from "@minikura/db";
|
import { prisma } from "@minikura/db";
|
||||||
import { KubernetesClient } from './utils/k8s-client';
|
import { KubernetesClient } from "./utils/k8s-client";
|
||||||
import { ServerController } from './controllers/server-controller';
|
import { ServerController } from "./controllers/server-controller";
|
||||||
import { ReverseProxyController } from './controllers/reverse-proxy-controller';
|
import { ReverseProxyController } from "./controllers/reverse-proxy-controller";
|
||||||
import { setupCRDRegistration } from './utils/crd-registrar';
|
import { setupCRDRegistration } from "./utils/crd-registrar";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('Starting Minikura Kubernetes Operator...');
|
console.log("Starting Minikura Kubernetes Operator...");
|
||||||
console.log(`Using namespace: ${NAMESPACE}`);
|
console.log(`Using namespace: ${NAMESPACE}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const k8sClient = KubernetesClient.getInstance();
|
const k8sClient = KubernetesClient.getInstance();
|
||||||
console.log('Connected to Kubernetes cluster');
|
console.log("Connected to Kubernetes cluster");
|
||||||
|
|
||||||
const serverController = new ServerController(prisma, NAMESPACE);
|
const serverController = new ServerController(prisma, NAMESPACE);
|
||||||
const reverseProxyController = new ReverseProxyController(prisma, NAMESPACE);
|
const reverseProxyController = new ReverseProxyController(prisma, NAMESPACE);
|
||||||
@@ -23,7 +23,7 @@ async function main() {
|
|||||||
reverseProxyController.startWatching();
|
reverseProxyController.startWatching();
|
||||||
|
|
||||||
if (ENABLE_CRD_REFLECTION) {
|
if (ENABLE_CRD_REFLECTION) {
|
||||||
console.log('CRD reflection enabled - will create CRDs to reflect database state');
|
console.log("CRD reflection enabled - will create CRDs to reflect database state");
|
||||||
try {
|
try {
|
||||||
await setupCRDRegistration(prisma, k8sClient, NAMESPACE);
|
await setupCRDRegistration(prisma, k8sClient, NAMESPACE);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -32,25 +32,26 @@ async function main() {
|
|||||||
console.error(`Response status: ${error.response.statusCode}`);
|
console.error(`Response status: ${error.response.statusCode}`);
|
||||||
console.error(`Response body: ${JSON.stringify(error.response.body)}`);
|
console.error(`Response body: ${JSON.stringify(error.response.body)}`);
|
||||||
}
|
}
|
||||||
console.error('Continuing operation without CRD reflection');
|
console.error("Continuing operation without CRD reflection");
|
||||||
console.log('Kubernetes resources will still be created/updated, but CRD reflection is disabled');
|
console.log(
|
||||||
|
"Kubernetes resources will still be created/updated, but CRD reflection is disabled"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Minikura Kubernetes Operator is running');
|
console.log("Minikura Kubernetes Operator is running");
|
||||||
|
|
||||||
process.on('SIGINT', gracefulShutdown);
|
process.on("SIGINT", gracefulShutdown);
|
||||||
process.on('SIGTERM', gracefulShutdown);
|
process.on("SIGTERM", gracefulShutdown);
|
||||||
|
|
||||||
function gracefulShutdown() {
|
function gracefulShutdown() {
|
||||||
console.log('Shutting down operator gracefully...');
|
console.log("Shutting down operator gracefully...");
|
||||||
serverController.stopWatching();
|
serverController.stopWatching();
|
||||||
reverseProxyController.stopWatching();
|
reverseProxyController.stopWatching();
|
||||||
prisma.$disconnect();
|
prisma.$disconnect();
|
||||||
console.log('Resources released, exiting...');
|
console.log("Resources released, exiting...");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`Failed to start Minikura Kubernetes Operator: ${error.message}`);
|
console.error(`Failed to start Minikura Kubernetes Operator: ${error.message}`);
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
@@ -64,7 +65,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(error => {
|
main().catch((error) => {
|
||||||
console.error('Unhandled error:', error);
|
console.error("Unhandled error:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as k8s from '@kubernetes/client-node';
|
import type * as k8s from "@kubernetes/client-node";
|
||||||
import { ReverseProxyServerType } from '@minikura/db';
|
import type { ReverseProxyServerType } from "@minikura/db";
|
||||||
import { LABEL_PREFIX } from '../config/constants';
|
import { LABEL_PREFIX } from "../config/constants";
|
||||||
import { calculateJavaMemory, convertToK8sFormat } from '../utils/memory';
|
import { calculateJavaMemory, convertToK8sFormat } from "../utils/memory";
|
||||||
import type { ReverseProxyConfig } from '../types';
|
import type { ReverseProxyConfig } from "../types";
|
||||||
|
|
||||||
export async function createReverseProxyServer(
|
export async function createReverseProxyServer(
|
||||||
server: ReverseProxyConfig,
|
server: ReverseProxyConfig,
|
||||||
@@ -17,8 +17,8 @@ export async function createReverseProxyServer(
|
|||||||
const serverName = `${serverType}-${server.id}`;
|
const serverName = `${serverType}-${server.id}`;
|
||||||
|
|
||||||
const configMap = {
|
const configMap = {
|
||||||
apiVersion: 'v1',
|
apiVersion: "v1",
|
||||||
kind: 'ConfigMap',
|
kind: "ConfigMap",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${serverName}-config`,
|
name: `${serverName}-config`,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
@@ -26,11 +26,11 @@ export async function createReverseProxyServer(
|
|||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: serverType,
|
[`${LABEL_PREFIX}/server-type`]: serverType,
|
||||||
[`${LABEL_PREFIX}/proxy-id`]: server.id,
|
[`${LABEL_PREFIX}/proxy-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
'minikura-api-key': server.apiKey,
|
"minikura-api-key": server.apiKey,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -48,8 +48,8 @@ export async function createReverseProxyServer(
|
|||||||
|
|
||||||
// Create Service for the reverse proxy - Always LoadBalancer for now
|
// Create Service for the reverse proxy - Always LoadBalancer for now
|
||||||
const service = {
|
const service = {
|
||||||
apiVersion: 'v1',
|
apiVersion: "v1",
|
||||||
kind: 'Service',
|
kind: "Service",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
@@ -57,7 +57,7 @@ export async function createReverseProxyServer(
|
|||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: serverType,
|
[`${LABEL_PREFIX}/server-type`]: serverType,
|
||||||
[`${LABEL_PREFIX}/proxy-id`]: server.id,
|
[`${LABEL_PREFIX}/proxy-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
selector: {
|
selector: {
|
||||||
@@ -67,12 +67,12 @@ export async function createReverseProxyServer(
|
|||||||
{
|
{
|
||||||
port: server.external_port,
|
port: server.external_port,
|
||||||
targetPort: server.listen_port,
|
targetPort: server.listen_port,
|
||||||
protocol: 'TCP',
|
protocol: "TCP",
|
||||||
name: 'minecraft',
|
name: "minecraft",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
type: 'LoadBalancer',
|
type: "LoadBalancer",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -90,8 +90,8 @@ export async function createReverseProxyServer(
|
|||||||
|
|
||||||
// Create Deployment
|
// Create Deployment
|
||||||
const deployment = {
|
const deployment = {
|
||||||
apiVersion: 'apps/v1',
|
apiVersion: "apps/v1",
|
||||||
kind: 'Deployment',
|
kind: "Deployment",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
@@ -99,14 +99,14 @@ export async function createReverseProxyServer(
|
|||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: serverType,
|
[`${LABEL_PREFIX}/server-type`]: serverType,
|
||||||
[`${LABEL_PREFIX}/proxy-id`]: server.id,
|
[`${LABEL_PREFIX}/proxy-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
replicas: 1,
|
replicas: 1,
|
||||||
selector: {
|
selector: {
|
||||||
matchLabels: {
|
matchLabels: {
|
||||||
app: serverName,
|
app: serverName,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -114,33 +114,33 @@ export async function createReverseProxyServer(
|
|||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: serverType,
|
[`${LABEL_PREFIX}/server-type`]: serverType,
|
||||||
[`${LABEL_PREFIX}/proxy-id`]: server.id,
|
[`${LABEL_PREFIX}/proxy-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
containers: [
|
containers: [
|
||||||
{
|
{
|
||||||
name: serverType,
|
name: serverType,
|
||||||
image: 'itzg/mc-proxy:latest',
|
image: "itzg/mc-proxy:latest",
|
||||||
ports: [
|
ports: [
|
||||||
{
|
{
|
||||||
containerPort: server.listen_port,
|
containerPort: server.listen_port,
|
||||||
name: 'minecraft',
|
name: "minecraft",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
env: [
|
env: [
|
||||||
{
|
{
|
||||||
name: 'TYPE',
|
name: "TYPE",
|
||||||
value: server.type,
|
value: server.type,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'NETWORKADDRESS_CACHE_TTL',
|
name: "NETWORKADDRESS_CACHE_TTL",
|
||||||
value: '30',
|
value: "30",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'MEMORY',
|
name: "MEMORY",
|
||||||
value: calculateJavaMemory(server.memory || '512M', 0.8),
|
value: calculateJavaMemory(server.memory || "512M", 0.8),
|
||||||
},
|
},
|
||||||
...(server.env_variables || []).map(ev => ({
|
...(server.env_variables || []).map((ev) => ({
|
||||||
name: ev.key,
|
name: ev.key,
|
||||||
value: ev.value,
|
value: ev.value,
|
||||||
})),
|
})),
|
||||||
@@ -160,13 +160,13 @@ export async function createReverseProxyServer(
|
|||||||
limits: {
|
limits: {
|
||||||
memory: convertToK8sFormat(server.memory || "512M"),
|
memory: convertToK8sFormat(server.memory || "512M"),
|
||||||
cpu: "500m",
|
cpu: "500m",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as k8s from '@kubernetes/client-node';
|
import type * as k8s from "@kubernetes/client-node";
|
||||||
import { ServerType } from '@minikura/db';
|
import { ServerType } from "@minikura/db";
|
||||||
import { LABEL_PREFIX } from '../config/constants';
|
import { LABEL_PREFIX } from "../config/constants";
|
||||||
import { calculateJavaMemory, convertToK8sFormat } from '../utils/memory';
|
import { calculateJavaMemory, convertToK8sFormat } from "../utils/memory";
|
||||||
import type { ServerConfig } from '../types';
|
import type { ServerConfig } from "../types";
|
||||||
|
|
||||||
export async function createServer(
|
export async function createServer(
|
||||||
server: ServerConfig,
|
server: ServerConfig,
|
||||||
@@ -14,8 +14,8 @@ export async function createServer(
|
|||||||
const serverName = `minecraft-${server.id}`;
|
const serverName = `minecraft-${server.id}`;
|
||||||
|
|
||||||
const configMap = {
|
const configMap = {
|
||||||
apiVersion: 'v1',
|
apiVersion: "v1",
|
||||||
kind: 'ConfigMap',
|
kind: "ConfigMap",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: `${serverName}-config`,
|
name: `${serverName}-config`,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
@@ -23,12 +23,12 @@ export async function createServer(
|
|||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: server.type.toLowerCase(),
|
[`${LABEL_PREFIX}/server-type`]: server.type.toLowerCase(),
|
||||||
[`${LABEL_PREFIX}/server-id`]: server.id,
|
[`${LABEL_PREFIX}/server-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
'server-type': server.type,
|
"server-type": server.type,
|
||||||
'minikura-api-key': server.apiKey,
|
"minikura-api-key": server.apiKey,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -45,8 +45,8 @@ export async function createServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const service = {
|
const service = {
|
||||||
apiVersion: 'v1',
|
apiVersion: "v1",
|
||||||
kind: 'Service',
|
kind: "Service",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
@@ -54,7 +54,7 @@ export async function createServer(
|
|||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: server.type.toLowerCase(),
|
[`${LABEL_PREFIX}/server-type`]: server.type.toLowerCase(),
|
||||||
[`${LABEL_PREFIX}/server-id`]: server.id,
|
[`${LABEL_PREFIX}/server-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
selector: {
|
selector: {
|
||||||
@@ -64,12 +64,12 @@ export async function createServer(
|
|||||||
{
|
{
|
||||||
port: server.listen_port,
|
port: server.listen_port,
|
||||||
targetPort: 25565,
|
targetPort: 25565,
|
||||||
protocol: 'TCP',
|
protocol: "TCP",
|
||||||
name: 'minecraft',
|
name: "minecraft",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
type: 'ClusterIP', // Always ClusterIP for regular servers
|
type: "ClusterIP", // Always ClusterIP for regular servers
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -99,78 +99,78 @@ async function createDeployment(
|
|||||||
namespace: string
|
namespace: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const deployment = {
|
const deployment = {
|
||||||
apiVersion: 'apps/v1',
|
apiVersion: "apps/v1",
|
||||||
kind: 'Deployment',
|
kind: "Deployment",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
labels: {
|
labels: {
|
||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: 'stateless',
|
[`${LABEL_PREFIX}/server-type`]: "stateless",
|
||||||
[`${LABEL_PREFIX}/server-id`]: server.id,
|
[`${LABEL_PREFIX}/server-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
replicas: 1,
|
replicas: 1,
|
||||||
selector: {
|
selector: {
|
||||||
matchLabels: {
|
matchLabels: {
|
||||||
app: serverName,
|
app: serverName,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
metadata: {
|
metadata: {
|
||||||
labels: {
|
labels: {
|
||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: 'stateless',
|
[`${LABEL_PREFIX}/server-type`]: "stateless",
|
||||||
[`${LABEL_PREFIX}/server-id`]: server.id,
|
[`${LABEL_PREFIX}/server-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
containers: [
|
containers: [
|
||||||
{
|
{
|
||||||
name: 'minecraft',
|
name: "minecraft",
|
||||||
image: 'itzg/minecraft-server',
|
image: "itzg/minecraft-server",
|
||||||
ports: [
|
ports: [
|
||||||
{
|
{
|
||||||
containerPort: 25565,
|
containerPort: 25565,
|
||||||
name: 'minecraft',
|
name: "minecraft",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
env: [
|
env: [
|
||||||
{
|
{
|
||||||
name: 'EULA',
|
name: "EULA",
|
||||||
value: 'TRUE',
|
value: "TRUE",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'TYPE',
|
name: "TYPE",
|
||||||
value: 'VANILLA',
|
value: "VANILLA",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'MEMORY',
|
name: "MEMORY",
|
||||||
value: calculateJavaMemory(server.memory || '1G', 0.8),
|
value: calculateJavaMemory(server.memory || "1G", 0.8),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'OPS',
|
name: "OPS",
|
||||||
value: '',
|
value: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'OVERRIDE_SERVER_PROPERTIES',
|
name: "OVERRIDE_SERVER_PROPERTIES",
|
||||||
value: 'true',
|
value: "true",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ENABLE_RCON',
|
name: "ENABLE_RCON",
|
||||||
value: 'false',
|
value: "false",
|
||||||
},
|
},
|
||||||
...(server.env_variables || []).map(ev => ({
|
...(server.env_variables || []).map((ev) => ({
|
||||||
name: ev.key,
|
name: ev.key,
|
||||||
value: ev.value,
|
value: ev.value,
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
volumeMounts: [
|
volumeMounts: [
|
||||||
{
|
{
|
||||||
name: 'config',
|
name: "config",
|
||||||
mountPath: '/config',
|
mountPath: "/config",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
readinessProbe: {
|
readinessProbe: {
|
||||||
tcpSocket: {
|
tcpSocket: {
|
||||||
@@ -187,21 +187,21 @@ async function createDeployment(
|
|||||||
limits: {
|
limits: {
|
||||||
memory: convertToK8sFormat(server.memory || "1G"),
|
memory: convertToK8sFormat(server.memory || "1G"),
|
||||||
cpu: "500m",
|
cpu: "500m",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
volumes: [
|
volumes: [
|
||||||
{
|
{
|
||||||
name: 'config',
|
name: "config",
|
||||||
configMap: {
|
configMap: {
|
||||||
name: `${serverName}-config`,
|
name: `${serverName}-config`,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -225,16 +225,16 @@ async function createStatefulSet(
|
|||||||
namespace: string
|
namespace: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const statefulSet = {
|
const statefulSet = {
|
||||||
apiVersion: 'apps/v1',
|
apiVersion: "apps/v1",
|
||||||
kind: 'StatefulSet',
|
kind: "StatefulSet",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
labels: {
|
labels: {
|
||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: 'stateful',
|
[`${LABEL_PREFIX}/server-type`]: "stateful",
|
||||||
[`${LABEL_PREFIX}/server-id`]: server.id,
|
[`${LABEL_PREFIX}/server-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
serviceName: serverName,
|
serviceName: serverName,
|
||||||
@@ -242,66 +242,66 @@ async function createStatefulSet(
|
|||||||
selector: {
|
selector: {
|
||||||
matchLabels: {
|
matchLabels: {
|
||||||
app: serverName,
|
app: serverName,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
metadata: {
|
metadata: {
|
||||||
labels: {
|
labels: {
|
||||||
app: serverName,
|
app: serverName,
|
||||||
[`${LABEL_PREFIX}/server-type`]: 'stateful',
|
[`${LABEL_PREFIX}/server-type`]: "stateful",
|
||||||
[`${LABEL_PREFIX}/server-id`]: server.id,
|
[`${LABEL_PREFIX}/server-id`]: server.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
containers: [
|
containers: [
|
||||||
{
|
{
|
||||||
name: 'minecraft',
|
name: "minecraft",
|
||||||
image: 'itzg/minecraft-server',
|
image: "itzg/minecraft-server",
|
||||||
ports: [
|
ports: [
|
||||||
{
|
{
|
||||||
containerPort: 25565,
|
containerPort: 25565,
|
||||||
name: 'minecraft',
|
name: "minecraft",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
env: [
|
env: [
|
||||||
{
|
{
|
||||||
name: 'EULA',
|
name: "EULA",
|
||||||
value: 'TRUE',
|
value: "TRUE",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'TYPE',
|
name: "TYPE",
|
||||||
value: 'VANILLA',
|
value: "VANILLA",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'MEMORY',
|
name: "MEMORY",
|
||||||
value: calculateJavaMemory(server.memory || '1G', 0.8),
|
value: calculateJavaMemory(server.memory || "1G", 0.8),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'OPS',
|
name: "OPS",
|
||||||
value: '',
|
value: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'OVERRIDE_SERVER_PROPERTIES',
|
name: "OVERRIDE_SERVER_PROPERTIES",
|
||||||
value: 'true',
|
value: "true",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ENABLE_RCON',
|
name: "ENABLE_RCON",
|
||||||
value: 'false',
|
value: "false",
|
||||||
},
|
},
|
||||||
...(server.env_variables || []).map(ev => ({
|
...(server.env_variables || []).map((ev) => ({
|
||||||
name: ev.key,
|
name: ev.key,
|
||||||
value: ev.value,
|
value: ev.value,
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
volumeMounts: [
|
volumeMounts: [
|
||||||
{
|
{
|
||||||
name: 'data',
|
name: "data",
|
||||||
mountPath: '/data',
|
mountPath: "/data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'config',
|
name: "config",
|
||||||
mountPath: '/config',
|
mountPath: "/config",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
readinessProbe: {
|
readinessProbe: {
|
||||||
tcpSocket: {
|
tcpSocket: {
|
||||||
@@ -318,36 +318,36 @@ async function createStatefulSet(
|
|||||||
limits: {
|
limits: {
|
||||||
memory: convertToK8sFormat(server.memory),
|
memory: convertToK8sFormat(server.memory),
|
||||||
cpu: "500m",
|
cpu: "500m",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
volumes: [
|
volumes: [
|
||||||
{
|
{
|
||||||
name: 'config',
|
name: "config",
|
||||||
configMap: {
|
configMap: {
|
||||||
name: `${serverName}-config`,
|
name: `${serverName}-config`,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
volumeClaimTemplates: [
|
volumeClaimTemplates: [
|
||||||
{
|
{
|
||||||
metadata: {
|
metadata: {
|
||||||
name: 'data',
|
name: "data",
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
accessModes: ['ReadWriteOnce'],
|
accessModes: ["ReadWriteOnce"],
|
||||||
resources: {
|
resources: {
|
||||||
requests: {
|
requests: {
|
||||||
storage: '1Gi',
|
storage: "1Gi",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { KubernetesClient } from '../utils/k8s-client';
|
import { KubernetesClient } from "../utils/k8s-client";
|
||||||
import { registerRBACResources } from '../utils/rbac-registrar';
|
import { registerRBACResources } from "../utils/rbac-registrar";
|
||||||
import { setupCRDRegistration } from '../utils/crd-registrar';
|
import { setupCRDRegistration } from "../utils/crd-registrar";
|
||||||
import { NAMESPACE } from '../config/constants';
|
import { NAMESPACE } from "../config/constants";
|
||||||
import { PrismaClient } from '@minikura/db';
|
import { PrismaClient } from "@minikura/db";
|
||||||
import { dotenvLoad } from 'dotenv-mono';
|
import { dotenvLoad } from "dotenv-mono";
|
||||||
|
|
||||||
dotenvLoad();
|
dotenvLoad();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('Starting to apply TypeScript-defined CRDs to Kubernetes cluster...');
|
console.log("Starting to apply TypeScript-defined CRDs to Kubernetes cluster...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const k8sClient = KubernetesClient.getInstance();
|
const k8sClient = KubernetesClient.getInstance();
|
||||||
@@ -16,14 +16,14 @@ async function main() {
|
|||||||
|
|
||||||
await registerRBACResources(k8sClient);
|
await registerRBACResources(k8sClient);
|
||||||
|
|
||||||
console.log('Registering Custom Resource Definitions...');
|
console.log("Registering Custom Resource Definitions...");
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
await setupCRDRegistration(prisma, k8sClient, NAMESPACE);
|
await setupCRDRegistration(prisma, k8sClient, NAMESPACE);
|
||||||
|
|
||||||
console.log('Successfully applied all resources to Kubernetes cluster');
|
console.log("Successfully applied all resources to Kubernetes cluster");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to apply resources:', error.message);
|
console.error("Failed to apply resources:", error.message);
|
||||||
if (error.stack) {
|
if (error.stack) {
|
||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(error => {
|
main().catch((error) => {
|
||||||
console.error('Unhandled error:', error);
|
console.error("Unhandled error:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
@@ -3,8 +3,8 @@ import type {
|
|||||||
ReverseProxyServerType,
|
ReverseProxyServerType,
|
||||||
Server as PrismaServer,
|
Server as PrismaServer,
|
||||||
ReverseProxyServer as PrismaReverseProxyServer,
|
ReverseProxyServer as PrismaReverseProxyServer,
|
||||||
CustomEnvironmentVariable
|
CustomEnvironmentVariable,
|
||||||
} from '@minikura/db';
|
} from "@minikura/db";
|
||||||
|
|
||||||
// Base interface
|
// Base interface
|
||||||
export interface CustomResource {
|
export interface CustomResource {
|
||||||
@@ -19,17 +19,23 @@ export interface CustomResource {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerConfig = Pick<PrismaServer, 'id' | 'description' | 'type' | 'listen_port' | 'memory'> & {
|
export type ServerConfig = Pick<
|
||||||
|
PrismaServer,
|
||||||
|
"id" | "description" | "type" | "listen_port" | "memory"
|
||||||
|
> & {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
env_variables?: Array<Pick<CustomEnvironmentVariable, 'key' | 'value'>>;
|
env_variables?: Array<Pick<CustomEnvironmentVariable, "key" | "value">>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MinecraftServerSpec = Pick<PrismaServer, 'id' | 'description' | 'type' | 'listen_port' | 'memory'> & {
|
export type MinecraftServerSpec = Pick<
|
||||||
environmentVariables?: Array<Pick<CustomEnvironmentVariable, 'key' | 'value'>>;
|
PrismaServer,
|
||||||
|
"id" | "description" | "type" | "listen_port" | "memory"
|
||||||
|
> & {
|
||||||
|
environmentVariables?: Array<Pick<CustomEnvironmentVariable, "key" | "value">>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MinecraftServerStatus {
|
export interface MinecraftServerStatus {
|
||||||
phase: 'Pending' | 'Running' | 'Failed';
|
phase: "Pending" | "Running" | "Failed";
|
||||||
message?: string;
|
message?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
internalId?: string;
|
internalId?: string;
|
||||||
@@ -45,23 +51,26 @@ export interface MinecraftServerCRD extends CustomResource {
|
|||||||
|
|
||||||
export type ReverseProxyConfig = Pick<
|
export type ReverseProxyConfig = Pick<
|
||||||
PrismaReverseProxyServer,
|
PrismaReverseProxyServer,
|
||||||
'id' | 'description' | 'external_address' | 'external_port' | 'listen_port' | 'type' | 'memory'
|
"id" | "description" | "external_address" | "external_port" | "listen_port" | "type" | "memory"
|
||||||
> & {
|
> & {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
env_variables?: Array<Pick<CustomEnvironmentVariable, 'key' | 'value'>>;
|
env_variables?: Array<Pick<CustomEnvironmentVariable, "key" | "value">>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReverseProxyServerSpec = Partial<
|
export type ReverseProxyServerSpec = Partial<
|
||||||
Pick<PrismaReverseProxyServer, 'id' | 'description' | 'external_address' | 'external_port' | 'listen_port' | 'type' | 'memory'>
|
Pick<
|
||||||
|
PrismaReverseProxyServer,
|
||||||
|
"id" | "description" | "external_address" | "external_port" | "listen_port" | "type" | "memory"
|
||||||
|
>
|
||||||
> & {
|
> & {
|
||||||
id: string;
|
id: string;
|
||||||
external_address: string;
|
external_address: string;
|
||||||
external_port: number;
|
external_port: number;
|
||||||
environmentVariables?: Array<Pick<CustomEnvironmentVariable, 'key' | 'value'>>;
|
environmentVariables?: Array<Pick<CustomEnvironmentVariable, "key" | "value">>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ReverseProxyServerStatus {
|
export interface ReverseProxyServerStatus {
|
||||||
phase: 'Pending' | 'Running' | 'Failed';
|
phase: "Pending" | "Running" | "Failed";
|
||||||
message?: string;
|
message?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
internalId?: string;
|
internalId?: string;
|
||||||
@@ -73,4 +82,4 @@ export interface ReverseProxyServerCRD extends CustomResource {
|
|||||||
status?: ReverseProxyServerStatus;
|
status?: ReverseProxyServerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EnvironmentVariable = Pick<CustomEnvironmentVariable, 'key' | 'value'>;
|
export type EnvironmentVariable = Pick<CustomEnvironmentVariable, "key" | "value">;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { PrismaClient } from '@minikura/db';
|
import type { PrismaClient } from "@minikura/db";
|
||||||
import type { Server, ReverseProxyServer, CustomEnvironmentVariable } from '@minikura/db';
|
import type { Server, ReverseProxyServer, CustomEnvironmentVariable } from "@minikura/db";
|
||||||
import { KubernetesClient } from './k8s-client';
|
import type { KubernetesClient } from "./k8s-client";
|
||||||
import { API_GROUP, API_VERSION, LABEL_PREFIX } from '../config/constants';
|
import { API_GROUP, API_VERSION, LABEL_PREFIX } from "../config/constants";
|
||||||
import { MINECRAFT_SERVER_CRD } from '../crds/server';
|
import { MINECRAFT_SERVER_CRD } from "../crds/server";
|
||||||
import { REVERSE_PROXY_SERVER_CRD } from '../crds/reverseProxy';
|
import { REVERSE_PROXY_SERVER_CRD } from "../crds/reverseProxy";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the CRD registration and starts a reflector to sync database state to CRDs
|
* Sets up the CRD registration and starts a reflector to sync database state to CRDs
|
||||||
@@ -24,7 +24,7 @@ async function registerCRDs(k8sClient: KubernetesClient): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const apiExtensionsClient = k8sClient.getApiExtensionsApi();
|
const apiExtensionsClient = k8sClient.getApiExtensionsApi();
|
||||||
|
|
||||||
console.log('Registering CRDs...');
|
console.log("Registering CRDs...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiExtensionsClient.createCustomResourceDefinition(MINECRAFT_SERVER_CRD);
|
await apiExtensionsClient.createCustomResourceDefinition(MINECRAFT_SERVER_CRD);
|
||||||
@@ -32,9 +32,9 @@ async function registerCRDs(k8sClient: KubernetesClient): Promise<void> {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response?.statusCode === 409) {
|
if (error.response?.statusCode === 409) {
|
||||||
// TODO: Handle conflict
|
// TODO: Handle conflict
|
||||||
console.log('MinecraftServer CRD already exists');
|
console.log("MinecraftServer CRD already exists");
|
||||||
} else {
|
} else {
|
||||||
console.error('Error creating MinecraftServer CRD:', error);
|
console.error("Error creating MinecraftServer CRD:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,14 +43,14 @@ async function registerCRDs(k8sClient: KubernetesClient): Promise<void> {
|
|||||||
console.log(`ReverseProxyServer CRD created successfully (${API_GROUP}/${API_VERSION})`);
|
console.log(`ReverseProxyServer CRD created successfully (${API_GROUP}/${API_VERSION})`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response?.statusCode === 409) {
|
if (error.response?.statusCode === 409) {
|
||||||
// TODO: Handle conflict
|
// TODO: Handle conflict
|
||||||
console.log('ReverseProxyServer CRD already exists');
|
console.log("ReverseProxyServer CRD already exists");
|
||||||
} else {
|
} else {
|
||||||
console.error('Error creating ReverseProxyServer CRD:', error);
|
console.error("Error creating ReverseProxyServer CRD:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error registering CRDs:', error);
|
console.error("Error registering CRDs:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,17 +69,27 @@ async function startCRDReflector(
|
|||||||
const reflectedMinecraftServers = new Map<string, string>(); // DB ID -> CR name
|
const reflectedMinecraftServers = new Map<string, string>(); // DB ID -> CR name
|
||||||
const reflectedReverseProxyServers = new Map<string, string>(); // DB ID -> CR name
|
const reflectedReverseProxyServers = new Map<string, string>(); // DB ID -> CR name
|
||||||
|
|
||||||
console.log('Starting CRD reflector...');
|
console.log("Starting CRD reflector...");
|
||||||
|
|
||||||
// Initial sync to create CRs that reflect the DB state
|
// Initial sync to create CRs that reflect the DB state
|
||||||
await syncDBtoCRDs(prisma, customObjectsApi, namespace,
|
await syncDBtoCRDs(
|
||||||
reflectedMinecraftServers, reflectedReverseProxyServers);
|
prisma,
|
||||||
|
customObjectsApi,
|
||||||
|
namespace,
|
||||||
|
reflectedMinecraftServers,
|
||||||
|
reflectedReverseProxyServers
|
||||||
|
);
|
||||||
|
|
||||||
// Polling interval to check for changes in the DB
|
// Polling interval to check for changes in the DB
|
||||||
// TODO: Make this listener instead
|
// TODO: Make this listener instead
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
await syncDBtoCRDs(prisma, customObjectsApi, namespace,
|
await syncDBtoCRDs(
|
||||||
reflectedMinecraftServers, reflectedReverseProxyServers);
|
prisma,
|
||||||
|
customObjectsApi,
|
||||||
|
namespace,
|
||||||
|
reflectedMinecraftServers,
|
||||||
|
reflectedReverseProxyServers
|
||||||
|
);
|
||||||
}, 30 * 1000);
|
}, 30 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +106,12 @@ async function syncDBtoCRDs(
|
|||||||
try {
|
try {
|
||||||
console.log(`[${new Date().toISOString()}] Starting CRD sync operation...`);
|
console.log(`[${new Date().toISOString()}] Starting CRD sync operation...`);
|
||||||
await syncMinecraftServers(prisma, customObjectsApi, namespace, reflectedMinecraftServers);
|
await syncMinecraftServers(prisma, customObjectsApi, namespace, reflectedMinecraftServers);
|
||||||
await syncReverseProxyServers(prisma, customObjectsApi, namespace, reflectedReverseProxyServers);
|
await syncReverseProxyServers(
|
||||||
|
prisma,
|
||||||
|
customObjectsApi,
|
||||||
|
namespace,
|
||||||
|
reflectedReverseProxyServers
|
||||||
|
);
|
||||||
console.log(`[${new Date().toISOString()}] CRD sync operation completed`);
|
console.log(`[${new Date().toISOString()}] CRD sync operation completed`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[${new Date().toISOString()}] Error syncing database to CRDs:`, error);
|
console.error(`[${new Date().toISOString()}] Error syncing database to CRDs:`, error);
|
||||||
@@ -121,11 +136,11 @@ async function syncMinecraftServers(
|
|||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'minecraftservers'
|
"minecraftservers"
|
||||||
);
|
);
|
||||||
existingCRs = (response.body as any).items || [];
|
existingCRs = (response.body as any).items || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error listing MinecraftServer CRs:', error);
|
console.error("Error listing MinecraftServer CRs:", error);
|
||||||
// TODO: Potentially better error handling here
|
// TODO: Potentially better error handling here
|
||||||
// For now, continue anyway - it might just be that none exist yet
|
// For now, continue anyway - it might just be that none exist yet
|
||||||
}
|
}
|
||||||
@@ -155,41 +170,41 @@ async function syncMinecraftServers(
|
|||||||
|
|
||||||
// Build the CR object
|
// Build the CR object
|
||||||
const serverCR: {
|
const serverCR: {
|
||||||
apiVersion: string,
|
apiVersion: string;
|
||||||
kind: string,
|
kind: string;
|
||||||
metadata: {
|
metadata: {
|
||||||
name: string,
|
name: string;
|
||||||
namespace: string,
|
namespace: string;
|
||||||
annotations: Record<string, string>,
|
annotations: Record<string, string>;
|
||||||
resourceVersion?: string
|
resourceVersion?: string;
|
||||||
},
|
};
|
||||||
spec: any,
|
spec: any;
|
||||||
status: any
|
status: any;
|
||||||
} = {
|
} = {
|
||||||
apiVersion: `${API_GROUP}/${API_VERSION}`,
|
apiVersion: `${API_GROUP}/${API_VERSION}`,
|
||||||
kind: 'MinecraftServer',
|
kind: "MinecraftServer",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: crName,
|
name: crName,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
annotations: {
|
annotations: {
|
||||||
[`${LABEL_PREFIX}/database-managed`]: 'true',
|
[`${LABEL_PREFIX}/database-managed`]: "true",
|
||||||
[`${LABEL_PREFIX}/last-synced`]: new Date().toISOString()
|
[`${LABEL_PREFIX}/last-synced`]: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
id: server.id,
|
id: server.id,
|
||||||
description: server.description,
|
description: server.description,
|
||||||
listen_port: server.listen_port,
|
listen_port: server.listen_port,
|
||||||
type: server.type,
|
type: server.type,
|
||||||
memory: server.memory
|
memory: server.memory,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
phase: 'Running',
|
phase: "Running",
|
||||||
message: 'Managed by database',
|
message: "Managed by database",
|
||||||
internalId: server.id,
|
internalId: server.id,
|
||||||
apiKey: '[REDACTED]', // Don't expose actual API key
|
apiKey: "[REDACTED]", // Don't expose actual API key
|
||||||
lastSyncedAt: new Date().toISOString()
|
lastSyncedAt: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -205,7 +220,7 @@ async function syncMinecraftServers(
|
|||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'minecraftservers',
|
"minecraftservers",
|
||||||
crName
|
crName
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -220,7 +235,7 @@ async function syncMinecraftServers(
|
|||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'minecraftservers',
|
"minecraftservers",
|
||||||
crName,
|
crName,
|
||||||
serverCR
|
serverCR
|
||||||
);
|
);
|
||||||
@@ -234,7 +249,7 @@ async function syncMinecraftServers(
|
|||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'minecraftservers',
|
"minecraftservers",
|
||||||
serverCR
|
serverCR
|
||||||
);
|
);
|
||||||
console.log(`Created MinecraftServer CR ${crName} for server ${server.id}`);
|
console.log(`Created MinecraftServer CR ${crName} for server ${server.id}`);
|
||||||
@@ -249,13 +264,13 @@ async function syncMinecraftServers(
|
|||||||
|
|
||||||
// Delete CRs for servers that no longer exist
|
// Delete CRs for servers that no longer exist
|
||||||
for (const [dbId, crName] of existingCRMap.entries()) {
|
for (const [dbId, crName] of existingCRMap.entries()) {
|
||||||
if (!servers.some(s => s.id === dbId)) {
|
if (!servers.some((s) => s.id === dbId)) {
|
||||||
try {
|
try {
|
||||||
await customObjectsApi.deleteNamespacedCustomObject(
|
await customObjectsApi.deleteNamespacedCustomObject(
|
||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'minecraftservers',
|
"minecraftservers",
|
||||||
crName
|
crName
|
||||||
);
|
);
|
||||||
console.log(`Deleted MinecraftServer CR ${crName} for removed server ID ${dbId}`);
|
console.log(`Deleted MinecraftServer CR ${crName} for removed server ID ${dbId}`);
|
||||||
@@ -265,7 +280,7 @@ async function syncMinecraftServers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error syncing Minecraft servers to CRDs:', error);
|
console.error("Error syncing Minecraft servers to CRDs:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,8 +296,8 @@ async function syncReverseProxyServers(
|
|||||||
try {
|
try {
|
||||||
const proxies = await prisma.reverseProxyServer.findMany({
|
const proxies = await prisma.reverseProxyServer.findMany({
|
||||||
include: {
|
include: {
|
||||||
env_variables: true
|
env_variables: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let existingCRs: any[] = [];
|
let existingCRs: any[] = [];
|
||||||
@@ -291,11 +306,11 @@ async function syncReverseProxyServers(
|
|||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'reverseproxyservers'
|
"reverseproxyservers"
|
||||||
);
|
);
|
||||||
existingCRs = (response.body as any).items || [];
|
existingCRs = (response.body as any).items || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error listing ReverseProxyServer CRs:', error);
|
console.error("Error listing ReverseProxyServer CRs:", error);
|
||||||
// TODO: Potentially better error handling here
|
// TODO: Potentially better error handling here
|
||||||
// For now, continue anyway - it might just be that none exist yet
|
// For now, continue anyway - it might just be that none exist yet
|
||||||
}
|
}
|
||||||
@@ -325,26 +340,26 @@ async function syncReverseProxyServers(
|
|||||||
|
|
||||||
// Build the CR object
|
// Build the CR object
|
||||||
const proxyCR: {
|
const proxyCR: {
|
||||||
apiVersion: string,
|
apiVersion: string;
|
||||||
kind: string,
|
kind: string;
|
||||||
metadata: {
|
metadata: {
|
||||||
name: string,
|
name: string;
|
||||||
namespace: string,
|
namespace: string;
|
||||||
annotations: Record<string, string>,
|
annotations: Record<string, string>;
|
||||||
resourceVersion?: string
|
resourceVersion?: string;
|
||||||
},
|
};
|
||||||
spec: any,
|
spec: any;
|
||||||
status: any
|
status: any;
|
||||||
} = {
|
} = {
|
||||||
apiVersion: `${API_GROUP}/${API_VERSION}`,
|
apiVersion: `${API_GROUP}/${API_VERSION}`,
|
||||||
kind: 'ReverseProxyServer',
|
kind: "ReverseProxyServer",
|
||||||
metadata: {
|
metadata: {
|
||||||
name: crName,
|
name: crName,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
annotations: {
|
annotations: {
|
||||||
[`${LABEL_PREFIX}/database-managed`]: 'true',
|
[`${LABEL_PREFIX}/database-managed`]: "true",
|
||||||
[`${LABEL_PREFIX}/last-synced`]: new Date().toISOString()
|
[`${LABEL_PREFIX}/last-synced`]: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
id: proxy.id,
|
id: proxy.id,
|
||||||
@@ -354,18 +369,18 @@ async function syncReverseProxyServers(
|
|||||||
listen_port: proxy.listen_port,
|
listen_port: proxy.listen_port,
|
||||||
type: proxy.type,
|
type: proxy.type,
|
||||||
memory: proxy.memory,
|
memory: proxy.memory,
|
||||||
environmentVariables: proxy.env_variables?.map(ev => ({
|
environmentVariables: proxy.env_variables?.map((ev) => ({
|
||||||
key: ev.key,
|
key: ev.key,
|
||||||
value: ev.value
|
value: ev.value,
|
||||||
}))
|
})),
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
phase: 'Running',
|
phase: "Running",
|
||||||
message: 'Managed by database',
|
message: "Managed by database",
|
||||||
internalId: proxy.id,
|
internalId: proxy.id,
|
||||||
apiKey: '[REDACTED]', // Don't expose actual API key
|
apiKey: "[REDACTED]", // Don't expose actual API key
|
||||||
lastSyncedAt: new Date().toISOString()
|
lastSyncedAt: new Date().toISOString(),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -381,7 +396,7 @@ async function syncReverseProxyServers(
|
|||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'reverseproxyservers',
|
"reverseproxyservers",
|
||||||
crName
|
crName
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -396,7 +411,7 @@ async function syncReverseProxyServers(
|
|||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'reverseproxyservers',
|
"reverseproxyservers",
|
||||||
crName,
|
crName,
|
||||||
proxyCR
|
proxyCR
|
||||||
);
|
);
|
||||||
@@ -410,7 +425,7 @@ async function syncReverseProxyServers(
|
|||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'reverseproxyservers',
|
"reverseproxyservers",
|
||||||
proxyCR
|
proxyCR
|
||||||
);
|
);
|
||||||
console.log(`Created ReverseProxyServer CR ${crName} for proxy ${proxy.id}`);
|
console.log(`Created ReverseProxyServer CR ${crName} for proxy ${proxy.id}`);
|
||||||
@@ -425,13 +440,13 @@ async function syncReverseProxyServers(
|
|||||||
|
|
||||||
// Delete CRs for proxies that no longer exist
|
// Delete CRs for proxies that no longer exist
|
||||||
for (const [dbId, crName] of existingCRMap.entries()) {
|
for (const [dbId, crName] of existingCRMap.entries()) {
|
||||||
if (!proxies.some(p => p.id === dbId)) {
|
if (!proxies.some((p) => p.id === dbId)) {
|
||||||
try {
|
try {
|
||||||
await customObjectsApi.deleteNamespacedCustomObject(
|
await customObjectsApi.deleteNamespacedCustomObject(
|
||||||
API_GROUP,
|
API_GROUP,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
namespace,
|
namespace,
|
||||||
'reverseproxyservers',
|
"reverseproxyservers",
|
||||||
crName
|
crName
|
||||||
);
|
);
|
||||||
console.log(`Deleted ReverseProxyServer CR ${crName} for removed proxy ID ${dbId}`);
|
console.log(`Deleted ReverseProxyServer CR ${crName} for removed proxy ID ${dbId}`);
|
||||||
@@ -441,6 +456,6 @@ async function syncReverseProxyServers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error syncing Reverse Proxy servers to CRDs:', error);
|
console.error("Error syncing Reverse Proxy servers to CRDs:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as k8s from '@kubernetes/client-node';
|
import * as k8s from "@kubernetes/client-node";
|
||||||
import { SKIP_TLS_VERIFY, NAMESPACE } from '../config/constants';
|
import { SKIP_TLS_VERIFY, NAMESPACE } from "../config/constants";
|
||||||
|
|
||||||
export class KubernetesClient {
|
export class KubernetesClient {
|
||||||
private static instance: KubernetesClient;
|
private static instance: KubernetesClient;
|
||||||
@@ -12,8 +12,8 @@ export class KubernetesClient {
|
|||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
if (SKIP_TLS_VERIFY) {
|
if (SKIP_TLS_VERIFY) {
|
||||||
console.log('Disabling TLS certificate validation');
|
console.log("Disabling TLS certificate validation");
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.kc = new k8s.KubeConfig();
|
this.kc = new k8s.KubeConfig();
|
||||||
@@ -31,23 +31,23 @@ export class KubernetesClient {
|
|||||||
private setupConfig(): void {
|
private setupConfig(): void {
|
||||||
try {
|
try {
|
||||||
this.kc.loadFromDefault();
|
this.kc.loadFromDefault();
|
||||||
console.log('Loaded Kubernetes config from default location');
|
console.log("Loaded Kubernetes config from default location");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to load Kubernetes config from default location:', err);
|
console.warn("Failed to load Kubernetes config from default location:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Running in a cluster, try to load in-cluster config
|
// Running in a cluster, try to load in-cluster config
|
||||||
if (!this.kc.getCurrentContext()) {
|
if (!this.kc.getCurrentContext()) {
|
||||||
try {
|
try {
|
||||||
this.kc.loadFromCluster();
|
this.kc.loadFromCluster();
|
||||||
console.log('Loaded Kubernetes config from cluster');
|
console.log("Loaded Kubernetes config from cluster");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to load Kubernetes config from cluster:', err);
|
console.warn("Failed to load Kubernetes config from cluster:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.kc.getCurrentContext()) {
|
if (!this.kc.getCurrentContext()) {
|
||||||
throw new Error('Failed to setup Kubernetes client - no valid configuration found');
|
throw new Error("Failed to setup Kubernetes client - no valid configuration found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCluster = this.kc.getCurrentCluster();
|
const currentCluster = this.kc.getCurrentCluster();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function convertToK8sFormat(memoryString: string): string {
|
|||||||
|
|
||||||
const [, valueStr, unit] = match;
|
const [, valueStr, unit] = match;
|
||||||
|
|
||||||
if (unit.toUpperCase() === 'G') {
|
if (unit.toUpperCase() === "G") {
|
||||||
return `${valueStr}Gi`;
|
return `${valueStr}Gi`;
|
||||||
} else {
|
} else {
|
||||||
return `${valueStr}Mi`;
|
return `${valueStr}Mi`;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { KubernetesClient } from './k8s-client';
|
import type { KubernetesClient } from "./k8s-client";
|
||||||
import {
|
import {
|
||||||
minikuraNamespace,
|
minikuraNamespace,
|
||||||
minikuraServiceAccount,
|
minikuraServiceAccount,
|
||||||
minikuraClusterRole,
|
minikuraClusterRole,
|
||||||
minikuraClusterRoleBinding,
|
minikuraClusterRoleBinding,
|
||||||
minikuraOperatorDeployment
|
minikuraOperatorDeployment,
|
||||||
} from '../crds/rbac';
|
} from "../crds/rbac";
|
||||||
import fetch from 'node-fetch';
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers all RBAC resources required
|
* Registers all RBAC resources required
|
||||||
@@ -14,16 +14,16 @@ import fetch from 'node-fetch';
|
|||||||
*/
|
*/
|
||||||
export async function registerRBACResources(k8sClient: KubernetesClient): Promise<void> {
|
export async function registerRBACResources(k8sClient: KubernetesClient): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Starting RBAC resources registration...');
|
console.log("Starting RBAC resources registration...");
|
||||||
|
|
||||||
await registerNamespace(k8sClient);
|
await registerNamespace(k8sClient);
|
||||||
await registerServiceAccount(k8sClient);
|
await registerServiceAccount(k8sClient);
|
||||||
await registerClusterRole(k8sClient);
|
await registerClusterRole(k8sClient);
|
||||||
await registerClusterRoleBinding(k8sClient);
|
await registerClusterRoleBinding(k8sClient);
|
||||||
|
|
||||||
console.log('RBAC resources registration completed successfully');
|
console.log("RBAC resources registration completed successfully");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error registering RBAC resources:', error.message);
|
console.error("Error registering RBAC resources:", error.message);
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
console.error(`Response status: ${error.response.statusCode}`);
|
console.error(`Response status: ${error.response.statusCode}`);
|
||||||
console.error(`Response body: ${JSON.stringify(error.response.body)}`);
|
console.error(`Response body: ${JSON.stringify(error.response.body)}`);
|
||||||
@@ -82,19 +82,22 @@ async function registerClusterRole(k8sClient: KubernetesClient): Promise<void> {
|
|||||||
// Get cluster URL
|
// Get cluster URL
|
||||||
const cluster = kc.getCurrentCluster();
|
const cluster = kc.getCurrentCluster();
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
throw new Error('No active cluster found in KubeConfig');
|
throw new Error("No active cluster found in KubeConfig");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${cluster.server}/apis/rbac.authorization.k8s.io/v1/clusterroles`, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
`${cluster.server}/apis/rbac.authorization.k8s.io/v1/clusterroles`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: "POST",
|
||||||
...(opts as any).headers
|
headers: {
|
||||||
},
|
"Content-Type": "application/json",
|
||||||
body: JSON.stringify(minikuraClusterRole),
|
...(opts as any).headers,
|
||||||
agent: (opts as any).agent
|
},
|
||||||
});
|
body: JSON.stringify(minikuraClusterRole),
|
||||||
|
agent: (opts as any).agent,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log(`Created cluster role ${minikuraClusterRole.metadata.name}`);
|
console.log(`Created cluster role ${minikuraClusterRole.metadata.name}`);
|
||||||
@@ -102,11 +105,13 @@ async function registerClusterRole(k8sClient: KubernetesClient): Promise<void> {
|
|||||||
console.log(`Cluster role ${minikuraClusterRole.metadata.name} already exists`);
|
console.log(`Cluster role ${minikuraClusterRole.metadata.name} already exists`);
|
||||||
} else {
|
} else {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
throw new Error(`Failed to create cluster role: ${response.status} ${response.statusText} - ${text}`);
|
throw new Error(
|
||||||
|
`Failed to create cluster role: ${response.status} ${response.statusText} - ${text}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// If the error message contains "already exists", that's OK
|
// If the error message contains "already exists", that's OK
|
||||||
if (error.message?.includes('already exists') || error.message?.includes('409')) {
|
if (error.message?.includes("already exists") || error.message?.includes("409")) {
|
||||||
console.log(`Cluster role ${minikuraClusterRole.metadata.name} already exists`);
|
console.log(`Cluster role ${minikuraClusterRole.metadata.name} already exists`);
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -131,36 +136,45 @@ async function registerClusterRoleBinding(k8sClient: KubernetesClient): Promise<
|
|||||||
// Get cluster URL
|
// Get cluster URL
|
||||||
const cluster = kc.getCurrentCluster();
|
const cluster = kc.getCurrentCluster();
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
throw new Error('No active cluster found in KubeConfig');
|
throw new Error("No active cluster found in KubeConfig");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the cluster role binding
|
// Create the cluster role binding
|
||||||
const { default: fetch } = await import('node-fetch');
|
const { default: fetch } = await import("node-fetch");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${cluster.server}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
`${cluster.server}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: "POST",
|
||||||
...(opts as any).headers
|
headers: {
|
||||||
},
|
"Content-Type": "application/json",
|
||||||
body: JSON.stringify(minikuraClusterRoleBinding),
|
...(opts as any).headers,
|
||||||
agent: (opts as any).agent
|
},
|
||||||
});
|
body: JSON.stringify(minikuraClusterRoleBinding),
|
||||||
|
agent: (opts as any).agent,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log(`Created cluster role binding ${minikuraClusterRoleBinding.metadata.name}`);
|
console.log(`Created cluster role binding ${minikuraClusterRoleBinding.metadata.name}`);
|
||||||
} else if (response.status === 409) {
|
} else if (response.status === 409) {
|
||||||
console.log(`Cluster role binding ${minikuraClusterRoleBinding.metadata.name} already exists`);
|
console.log(
|
||||||
|
`Cluster role binding ${minikuraClusterRoleBinding.metadata.name} already exists`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
throw new Error(`Failed to create cluster role binding: ${response.status} ${response.statusText} - ${text}`);
|
throw new Error(
|
||||||
|
`Failed to create cluster role binding: ${response.status} ${response.statusText} - ${text}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// If the error message contains "already exists"
|
// If the error message contains "already exists"
|
||||||
// TODO: Potentially better error handling here
|
// TODO: Potentially better error handling here
|
||||||
if (error.message?.includes('already exists') || error.message?.includes('409')) {
|
if (error.message?.includes("already exists") || error.message?.includes("409")) {
|
||||||
console.log(`Cluster role binding ${minikuraClusterRoleBinding.metadata.name} already exists`);
|
console.log(
|
||||||
|
`Cluster role binding ${minikuraClusterRoleBinding.metadata.name} already exists`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -182,28 +196,27 @@ export async function registerOperatorDeployment(
|
|||||||
try {
|
try {
|
||||||
// Replace the registry URL placeholder, for future use
|
// Replace the registry URL placeholder, for future use
|
||||||
const deployment = JSON.parse(
|
const deployment = JSON.parse(
|
||||||
JSON.stringify(minikuraOperatorDeployment).replace('${REGISTRY_URL}', registryUrl)
|
JSON.stringify(minikuraOperatorDeployment).replace("${REGISTRY_URL}", registryUrl)
|
||||||
);
|
);
|
||||||
|
|
||||||
const appsApi = k8sClient.getAppsApi();
|
const appsApi = k8sClient.getAppsApi();
|
||||||
await appsApi.createNamespacedDeployment(
|
await appsApi.createNamespacedDeployment(deployment.metadata.namespace, deployment);
|
||||||
deployment.metadata.namespace,
|
|
||||||
deployment
|
|
||||||
);
|
|
||||||
console.log(`Created deployment ${deployment.metadata.name}`);
|
console.log(`Created deployment ${deployment.metadata.name}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response?.statusCode === 409) {
|
if (error.response?.statusCode === 409) {
|
||||||
console.log(`Deployment ${minikuraOperatorDeployment.metadata.name} already exists`);
|
console.log(`Deployment ${minikuraOperatorDeployment.metadata.name} already exists`);
|
||||||
// Update the deployment if it already exists
|
// Update the deployment if it already exists
|
||||||
const deployment = JSON.parse(
|
const deployment = JSON.parse(
|
||||||
JSON.stringify(minikuraOperatorDeployment).replace('${REGISTRY_URL}', registryUrl)
|
JSON.stringify(minikuraOperatorDeployment).replace("${REGISTRY_URL}", registryUrl)
|
||||||
);
|
);
|
||||||
|
|
||||||
await k8sClient.getAppsApi().replaceNamespacedDeployment(
|
await k8sClient
|
||||||
deployment.metadata.name,
|
.getAppsApi()
|
||||||
deployment.metadata.namespace,
|
.replaceNamespacedDeployment(
|
||||||
deployment
|
deployment.metadata.name,
|
||||||
);
|
deployment.metadata.namespace,
|
||||||
|
deployment
|
||||||
|
);
|
||||||
console.log(`Updated deployment ${deployment.metadata.name}`);
|
console.log(`Updated deployment ${deployment.metadata.name}`);
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user