🎨 style: auto format

This commit is contained in:
2026-01-16 12:32:36 +07:00
parent 5a6d3da26d
commit 98b685fe1b
18 changed files with 743 additions and 703 deletions

View File

@@ -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"
} }
} }
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"editor.defaultFormatter": "biomejs.biome", "editor.defaultFormatter": "biomejs.biome",
"[prisma]": { "[prisma]": {
"editor.defaultFormatter": "Prisma.prisma" "editor.defaultFormatter": "Prisma.prisma"
} }
} }

View File

@@ -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",
}; };

View File

@@ -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;
@@ -19,16 +19,16 @@ export abstract class BaseController {
*/ */
public startWatching(): void { public startWatching(): void {
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);
@@ -54,4 +54,4 @@ export abstract class BaseController {
* Sync resources from database to Kubernetes * Sync resources from database to Kubernetes
*/ */
protected abstract syncResources(): Promise<void>; protected abstract syncResources(): Promise<void>;
} }

View File

@@ -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,31 +28,35 @@ 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);
} }
} }
// Create or update reverse proxy servers that are in the database // Create or update reverse proxy servers that are in the database
for (const proxy of proxies) { for (const proxy of proxies) {
const deployedProxy = this.deployedProxies.get(proxy.id); const deployedProxy = this.deployedProxies.get(proxy.id);
// 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,
external_address: proxy.external_address, external_address: proxy.external_address,
@@ -59,55 +66,55 @@ 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(
proxyConfig, proxyConfig,
appsApi, appsApi,
coreApi, coreApi,
networkingApi, networkingApi,
this.namespace this.namespace
); );
// Update cache // Update cache
this.deployedProxies.set(proxy.id, { ...proxy }); this.deployedProxies.set(proxy.id, { ...proxy });
} }
} }
} catch (error) { } catch (error) {
console.error('Error syncing reverse proxy servers:', error); console.error("Error syncing reverse proxy servers:", error);
throw error; throw error;
} }
} }
private hasProxyChanged( private hasProxyChanged(
oldProxy: ReverseProxyWithEnvVars, oldProxy: ReverseProxyWithEnvVars,
newProxy: ReverseProxyWithEnvVars newProxy: ReverseProxyWithEnvVars
): boolean { ): boolean {
// Check basic properties // Check basic properties
const basicPropsChanged = const basicPropsChanged =
oldProxy.external_address !== newProxy.external_address || oldProxy.external_address !== newProxy.external_address ||
oldProxy.external_port !== newProxy.external_port || oldProxy.external_port !== newProxy.external_port ||
oldProxy.listen_port !== newProxy.listen_port || oldProxy.listen_port !== newProxy.listen_port ||
oldProxy.description !== newProxy.description; oldProxy.description !== newProxy.description;
if (basicPropsChanged) return true; if (basicPropsChanged) return true;
// Check if environment variables have changed // Check if environment variables have changed
const oldEnvVars = oldProxy.env_variables || []; const oldEnvVars = oldProxy.env_variables || [];
const newEnvVars = newProxy.env_variables || []; const newEnvVars = newProxy.env_variables || [];
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;
} }
} }
return false; return false;
} }
} }

View File

@@ -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,31 +25,35 @@ 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);
} }
} }
// Create or update servers that are in the database // Create or update servers that are in the database
for (const server of servers) { for (const server of servers) {
const deployedServer = this.deployedServers.get(server.id); const deployedServer = this.deployedServers.get(server.id);
// 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,
type: server.type, type: server.type,
@@ -57,57 +61,48 @@ 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 ||
oldServer.listen_port !== newServer.listen_port || oldServer.listen_port !== newServer.listen_port ||
oldServer.description !== newServer.description; oldServer.description !== newServer.description;
if (basicPropsChanged) return true; if (basicPropsChanged) return true;
// Check if environment variables have changed // Check if environment variables have changed
const oldEnvVars = oldServer.env_variables || []; const oldEnvVars = oldServer.env_variables || [];
const newEnvVars = newServer.env_variables || []; const newEnvVars = newServer.env_variables || [];
// Check if the number of env vars has changed // Check if the number of env vars has changed
if (oldEnvVars.length !== newEnvVars.length) return true; if (oldEnvVars.length !== newEnvVars.length) return true;
// 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;
} }
} }
return false; return false;
} }
} }

View File

@@ -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,
@@ -166,4 +166,4 @@ export const minikuraOperatorDeployment = {
}, },
}, },
}, },
}; };

View File

@@ -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,
@@ -153,4 +153,4 @@ export const REVERSE_PROXY_SERVER_CRD = {
shortNames: RESOURCE_TYPES.REVERSE_PROXY_SERVER.shortNames, shortNames: RESOURCE_TYPES.REVERSE_PROXY_SERVER.shortNames,
}, },
}, },
}; };

View File

@@ -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,
@@ -129,4 +129,4 @@ export const MINECRAFT_SERVER_CRD = {
shortNames: RESOURCE_TYPES.MINECRAFT_SERVER.shortNames, shortNames: RESOURCE_TYPES.MINECRAFT_SERVER.shortNames,
}, },
}, },
}; };

View File

@@ -1,29 +1,29 @@
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);
serverController.startWatching(); serverController.startWatching();
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);
}); });

View File

@@ -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,
@@ -12,13 +12,13 @@ export async function createReverseProxyServer(
namespace: string namespace: string
): Promise<void> { ): Promise<void> {
console.log(`Creating reverse proxy server ${server.id} in namespace '${namespace}'`); console.log(`Creating reverse proxy server ${server.id} in namespace '${namespace}'`);
const serverType = server.type.toLowerCase(); const serverType = server.type.toLowerCase();
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,13 +26,13 @@ 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 {
await coreApi.createNamespacedConfigMap(namespace, configMap); await coreApi.createNamespacedConfigMap(namespace, configMap);
console.log(`Created ConfigMap for reverse proxy server ${server.id}`); console.log(`Created ConfigMap for reverse proxy server ${server.id}`);
@@ -45,11 +45,11 @@ export async function createReverseProxyServer(
throw error; throw error;
} }
} }
// 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,14 +67,14 @@ 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 {
await coreApi.createNamespacedService(namespace, service); await coreApi.createNamespacedService(namespace, service);
console.log(`Created Service for reverse proxy server ${server.id}`); console.log(`Created Service for reverse proxy server ${server.id}`);
@@ -87,11 +87,11 @@ export async function createReverseProxyServer(
throw error; throw error;
} }
} }
// 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,61 +114,61 @@ 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,
})), })),
], ],
readinessProbe: { readinessProbe: {
tcpSocket: { tcpSocket: {
port: server.listen_port, port: server.listen_port,
}, },
initialDelaySeconds: 30, initialDelaySeconds: 30,
periodSeconds: 10, periodSeconds: 10,
}, },
resources: { resources: {
requests: { requests: {
memory: convertToK8sFormat(server.memory || "512M"), memory: convertToK8sFormat(server.memory || "512M"),
cpu: "250m", cpu: "250m",
}, },
limits: { limits: {
memory: convertToK8sFormat(server.memory || "512M"), memory: convertToK8sFormat(server.memory || "512M"),
cpu: "500m", cpu: "500m",
} },
} },
} },
] ],
} },
} },
} },
}; };
try { try {
await appsApi.createNamespacedDeployment(namespace, deployment); await appsApi.createNamespacedDeployment(namespace, deployment);
console.log(`Created Deployment for reverse proxy server ${server.id}`); console.log(`Created Deployment for reverse proxy server ${server.id}`);
@@ -192,7 +192,7 @@ export async function deleteReverseProxyServer(
): Promise<void> { ): Promise<void> {
const serverType = proxyType.toLowerCase(); const serverType = proxyType.toLowerCase();
const name = `${serverType}-${proxyId}`; const name = `${serverType}-${proxyId}`;
try { try {
await appsApi.deleteNamespacedDeployment(name, namespace); await appsApi.deleteNamespacedDeployment(name, namespace);
console.log(`Deleted Deployment for reverse proxy server ${proxyId}`); console.log(`Deleted Deployment for reverse proxy server ${proxyId}`);
@@ -201,7 +201,7 @@ export async function deleteReverseProxyServer(
console.error(`Error deleting Deployment for reverse proxy server ${proxyId}:`, error); console.error(`Error deleting Deployment for reverse proxy server ${proxyId}:`, error);
} }
} }
try { try {
await coreApi.deleteNamespacedService(name, namespace); await coreApi.deleteNamespacedService(name, namespace);
console.log(`Deleted Service for reverse proxy server ${proxyId}`); console.log(`Deleted Service for reverse proxy server ${proxyId}`);
@@ -210,7 +210,7 @@ export async function deleteReverseProxyServer(
console.error(`Error deleting Service for reverse proxy server ${proxyId}:`, error); console.error(`Error deleting Service for reverse proxy server ${proxyId}:`, error);
} }
} }
try { try {
await coreApi.deleteNamespacedConfigMap(`${name}-config`, namespace); await coreApi.deleteNamespacedConfigMap(`${name}-config`, namespace);
console.log(`Deleted ConfigMap for reverse proxy server ${proxyId}`); console.log(`Deleted ConfigMap for reverse proxy server ${proxyId}`);
@@ -219,4 +219,4 @@ export async function deleteReverseProxyServer(
console.error(`Error deleting ConfigMap for reverse proxy server ${proxyId}:`, error); console.error(`Error deleting ConfigMap for reverse proxy server ${proxyId}:`, error);
} }
} }
} }

View File

@@ -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,14 +23,14 @@ 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 {
await coreApi.createNamespacedConfigMap(namespace, configMap); await coreApi.createNamespacedConfigMap(namespace, configMap);
console.log(`Created ConfigMap for server ${server.id}`); console.log(`Created ConfigMap for server ${server.id}`);
@@ -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,14 +64,14 @@ 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 {
await coreApi.createNamespacedService(namespace, service); await coreApi.createNamespacedService(namespace, service);
console.log(`Created Service for server ${server.id}`); console.log(`Created Service for server ${server.id}`);
@@ -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,23 +187,23 @@ 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 {
await appsApi.createNamespacedDeployment(namespace, deployment); await appsApi.createNamespacedDeployment(namespace, deployment);
console.log(`Created Deployment for server ${server.id}`); console.log(`Created Deployment for server ${server.id}`);
@@ -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,38 +318,38 @@ 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 {
await appsApi.createNamespacedStatefulSet(namespace, statefulSet); await appsApi.createNamespacedStatefulSet(namespace, statefulSet);
console.log(`Created StatefulSet for server ${server.id}`); console.log(`Created StatefulSet for server ${server.id}`);
@@ -372,7 +372,7 @@ export async function deleteServer(
namespace: string namespace: string
): Promise<void> { ): Promise<void> {
const serverName = `minecraft-${serverId2}`; const serverName = `minecraft-${serverId2}`;
try { try {
await appsApi.deleteNamespacedDeployment(serverName, namespace); await appsApi.deleteNamespacedDeployment(serverName, namespace);
console.log(`Deleted Deployment for server ${serverName}`); console.log(`Deleted Deployment for server ${serverName}`);
@@ -381,7 +381,7 @@ export async function deleteServer(
console.error(`Error deleting Deployment for server ${serverName}:`, err); console.error(`Error deleting Deployment for server ${serverName}:`, err);
} }
} }
try { try {
await appsApi.deleteNamespacedStatefulSet(serverName, namespace); await appsApi.deleteNamespacedStatefulSet(serverName, namespace);
console.log(`Deleted StatefulSet for server ${serverName}`); console.log(`Deleted StatefulSet for server ${serverName}`);
@@ -390,7 +390,7 @@ export async function deleteServer(
console.error(`Error deleting StatefulSet for server ${serverName}:`, err); console.error(`Error deleting StatefulSet for server ${serverName}:`, err);
} }
} }
try { try {
await coreApi.deleteNamespacedService(serverName, namespace); await coreApi.deleteNamespacedService(serverName, namespace);
console.log(`Deleted Service for server ${serverName}`); console.log(`Deleted Service for server ${serverName}`);
@@ -399,7 +399,7 @@ export async function deleteServer(
console.error(`Error deleting Service for server ${serverName}:`, err); console.error(`Error deleting Service for server ${serverName}:`, err);
} }
} }
try { try {
await coreApi.deleteNamespacedConfigMap(`${serverName}-config`, namespace); await coreApi.deleteNamespacedConfigMap(`${serverName}-config`, namespace);
console.log(`Deleted ConfigMap for server ${serverName}`); console.log(`Deleted ConfigMap for server ${serverName}`);
@@ -408,4 +408,4 @@ export async function deleteServer(
console.error(`Error deleting ConfigMap for server ${serverName}:`, err); console.error(`Error deleting ConfigMap for server ${serverName}:`, err);
} }
} }
} }

View File

@@ -1,29 +1,29 @@
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();
console.log(`Connected to Kubernetes cluster, using namespace: ${NAMESPACE}`); console.log(`Connected to Kubernetes cluster, using namespace: ${NAMESPACE}`);
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);
}); });

View File

@@ -1,10 +1,10 @@
import type { import type {
ServerType, ServerType,
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;
@@ -44,24 +50,27 @@ export interface MinecraftServerCRD extends CustomResource {
// Reverse Proxy Types // Reverse Proxy Types
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">;

View File

@@ -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);
@@ -114,18 +129,18 @@ async function syncMinecraftServers(
): Promise<void> { ): Promise<void> {
try { try {
const servers = await prisma.server.findMany(); const servers = await prisma.server.findMany();
let existingCRs: any[] = []; let existingCRs: any[] = [];
try { try {
const response = await customObjectsApi.listNamespacedCustomObject( const response = await customObjectsApi.listNamespacedCustomObject(
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
} }
@@ -134,7 +149,7 @@ async function syncMinecraftServers(
const existingCRMap = new Map<string, string>(); const existingCRMap = new Map<string, string>();
// Map of CR names to their resourceVersions for updates // Map of CR names to their resourceVersions for updates
const crResourceVersions = new Map<string, string>(); const crResourceVersions = new Map<string, string>();
for (const cr of existingCRs) { for (const cr of existingCRs) {
const internalId = cr.status?.internalId; const internalId = cr.status?.internalId;
if (internalId) { if (internalId) {
@@ -148,48 +163,48 @@ async function syncMinecraftServers(
// Refresh tracking map // Refresh tracking map
reflectedMinecraftServers.clear(); reflectedMinecraftServers.clear();
// Create or update CRs for each server // Create or update CRs for each server
for (const server of servers) { for (const server of servers) {
const crName = existingCRMap.get(server.id) || `${server.id.toLowerCase()}`; const crName = existingCRMap.get(server.id) || `${server.id.toLowerCase()}`;
// 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 {
@@ -198,29 +213,29 @@ async function syncMinecraftServers(
// Get the current resource first // Get the current resource first
const crName = existingCRMap.get(server.id)!; const crName = existingCRMap.get(server.id)!;
try { try {
// Get the existing resource to get the current resourceVersion // Get the existing resource to get the current resourceVersion
const existingResource = await customObjectsApi.getNamespacedCustomObject( const existingResource = await customObjectsApi.getNamespacedCustomObject(
API_GROUP, API_GROUP,
API_VERSION, API_VERSION,
namespace, namespace,
'minecraftservers', "minecraftservers",
crName crName
); );
// Extract the resourceVersion from the existing resource // Extract the resourceVersion from the existing resource
if (existingResource?.body?.metadata?.resourceVersion) { if (existingResource?.body?.metadata?.resourceVersion) {
// Add the resourceVersion to our custom resource // Add the resourceVersion to our custom resource
serverCR.metadata.resourceVersion = existingResource.body.metadata.resourceVersion; serverCR.metadata.resourceVersion = existingResource.body.metadata.resourceVersion;
} }
// Now update with the correct resourceVersion // Now update with the correct resourceVersion
await customObjectsApi.replaceNamespacedCustomObject( await customObjectsApi.replaceNamespacedCustomObject(
API_GROUP, API_GROUP,
API_VERSION, API_VERSION,
namespace, namespace,
'minecraftservers', "minecraftservers",
crName, crName,
serverCR serverCR
); );
@@ -234,28 +249,28 @@ 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}`);
} }
// Remember this mapping // Remember this mapping
reflectedMinecraftServers.set(server.id, crName); reflectedMinecraftServers.set(server.id, crName);
} catch (error) { } catch (error) {
console.error(`Error creating/updating MinecraftServer CR for ${server.id}:`, error); console.error(`Error creating/updating MinecraftServer CR for ${server.id}:`, error);
} }
} }
// 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,21 +296,21 @@ 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[] = [];
try { try {
const response = await customObjectsApi.listNamespacedCustomObject( const response = await customObjectsApi.listNamespacedCustomObject(
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
} }
@@ -304,7 +319,7 @@ async function syncReverseProxyServers(
const existingCRMap = new Map<string, string>(); const existingCRMap = new Map<string, string>();
// Map of CR names to their resourceVersions for updates // Map of CR names to their resourceVersions for updates
const crResourceVersions = new Map<string, string>(); const crResourceVersions = new Map<string, string>();
for (const cr of existingCRs) { for (const cr of existingCRs) {
const internalId = cr.status?.internalId; const internalId = cr.status?.internalId;
if (internalId) { if (internalId) {
@@ -318,33 +333,33 @@ async function syncReverseProxyServers(
// Refresh tracking map // Refresh tracking map
reflectedReverseProxyServers.clear(); reflectedReverseProxyServers.clear();
// Create or update CRs for each proxy // Create or update CRs for each proxy
for (const proxy of proxies) { for (const proxy of proxies) {
const crName = existingCRMap.get(proxy.id) || `${proxy.id.toLowerCase()}`; const crName = existingCRMap.get(proxy.id) || `${proxy.id.toLowerCase()}`;
// 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 {
@@ -374,29 +389,29 @@ async function syncReverseProxyServers(
// Get the current resource first // Get the current resource first
const crName = existingCRMap.get(proxy.id)!; const crName = existingCRMap.get(proxy.id)!;
try { try {
// Get the existing resource to get the current resourceVersion // Get the existing resource to get the current resourceVersion
const existingResource = await customObjectsApi.getNamespacedCustomObject( const existingResource = await customObjectsApi.getNamespacedCustomObject(
API_GROUP, API_GROUP,
API_VERSION, API_VERSION,
namespace, namespace,
'reverseproxyservers', "reverseproxyservers",
crName crName
); );
// Extract the resourceVersion from the existing resource // Extract the resourceVersion from the existing resource
if (existingResource?.body?.metadata?.resourceVersion) { if (existingResource?.body?.metadata?.resourceVersion) {
// Add the resourceVersion to our custom resource // Add the resourceVersion to our custom resource
proxyCR.metadata.resourceVersion = existingResource.body.metadata.resourceVersion; proxyCR.metadata.resourceVersion = existingResource.body.metadata.resourceVersion;
} }
// Now update with the correct resourceVersion // Now update with the correct resourceVersion
await customObjectsApi.replaceNamespacedCustomObject( await customObjectsApi.replaceNamespacedCustomObject(
API_GROUP, API_GROUP,
API_VERSION, API_VERSION,
namespace, namespace,
'reverseproxyservers', "reverseproxyservers",
crName, crName,
proxyCR proxyCR
); );
@@ -410,28 +425,28 @@ 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}`);
} }
// Remember this mapping // Remember this mapping
reflectedReverseProxyServers.set(proxy.id, crName); reflectedReverseProxyServers.set(proxy.id, crName);
} catch (error) { } catch (error) {
console.error(`Error creating/updating ReverseProxyServer CR for ${proxy.id}:`, error); console.error(`Error creating/updating ReverseProxyServer CR for ${proxy.id}:`, error);
} }
} }
// 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);
} }
} }

View File

@@ -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,10 +12,10 @@ 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();
this.setupConfig(); this.setupConfig();
this.initializeClients(); this.initializeClients();
@@ -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();
@@ -90,12 +90,12 @@ export class KubernetesClient {
async handleApiError(error: any, context: string): Promise<never> { async handleApiError(error: any, context: string): Promise<never> {
console.error(`Kubernetes API error (${context}):`, error?.message || error); console.error(`Kubernetes API error (${context}):`, error?.message || error);
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)}`);
} }
throw error; throw error;
} }
} }

View File

@@ -11,10 +11,10 @@
export function calculateJavaMemory(memoryString: string, factor: number): string { export function calculateJavaMemory(memoryString: string, factor: number): string {
const match = memoryString.match(/^(\d+)([MG])$/i); const match = memoryString.match(/^(\d+)([MG])$/i);
if (!match) return "512M"; // Default if format is not recognized if (!match) return "512M"; // Default if format is not recognized
const [, valueStr, unit] = match; const [, valueStr, unit] = match;
const value = parseInt(valueStr, 10); const value = parseInt(valueStr, 10);
const calculatedValue = Math.round(value * factor); const calculatedValue = Math.round(value * factor);
return `${calculatedValue}${unit.toUpperCase()}`; return `${calculatedValue}${unit.toUpperCase()}`;
} }
@@ -27,12 +27,12 @@ export function calculateJavaMemory(memoryString: string, factor: number): strin
export function convertToK8sFormat(memoryString: string): string { export function convertToK8sFormat(memoryString: string): string {
const match = memoryString.match(/^(\d+)([MG])$/i); const match = memoryString.match(/^(\d+)([MG])$/i);
if (!match) return "1Gi"; // Default if format is not recognized if (!match) return "1Gi"; // Default if format is not recognized
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`;
} }
} }

View File

@@ -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)}`);
@@ -78,35 +78,40 @@ async function registerClusterRole(k8sClient: KubernetesClient): Promise<void> {
const kc = k8sClient.getKubeConfig(); const kc = k8sClient.getKubeConfig();
const opts = {}; const opts = {};
kc.applyToRequest(opts as any); kc.applyToRequest(opts as any);
// 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}`);
} else if (response.status === 409) { } else if (response.status === 409) {
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;
@@ -127,40 +132,49 @@ async function registerClusterRoleBinding(k8sClient: KubernetesClient): Promise<
const kc = k8sClient.getKubeConfig(); const kc = k8sClient.getKubeConfig();
const opts = {}; const opts = {};
kc.applyToRequest(opts as any); kc.applyToRequest(opts as any);
// 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;
} }
@@ -176,37 +190,36 @@ async function registerClusterRoleBinding(k8sClient: KubernetesClient): Promise<
* Note: This requires the secret to be created first * Note: This requires the secret to be created first
*/ */
export async function registerOperatorDeployment( export async function registerOperatorDeployment(
k8sClient: KubernetesClient, k8sClient: KubernetesClient,
registryUrl: string registryUrl: string
): Promise<void> { ): Promise<void> {
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(
deployment.metadata.name,
deployment.metadata.namespace,
deployment
); );
await k8sClient
.getAppsApi()
.replaceNamespacedDeployment(
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;
} }
} }
} }