feat: k8s operator

This commit is contained in:
2025-05-12 12:34:09 +07:00
parent cffe651d6f
commit 952928ffcb
19 changed files with 2486 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import { PrismaClient } from '@minikura/db';
import { KubernetesClient } from '../utils/k8s-client';
import { SYNC_INTERVAL } from '../config/constants';
export abstract class BaseController {
protected prisma: PrismaClient;
protected k8sClient: KubernetesClient;
protected namespace: string;
private intervalId: ReturnType<typeof setInterval> | null = null;
constructor(prisma: PrismaClient, namespace: string) {
this.prisma = prisma;
this.k8sClient = KubernetesClient.getInstance();
this.namespace = namespace;
}
/**
* Start watching for changes in the database and syncing to Kubernetes
*/
public startWatching(): void {
console.log(`Starting to watch for changes in ${this.getControllerName()}...`);
// Initial sync
this.syncResources().catch(err => {
console.error(`Error during initial sync of ${this.getControllerName()}:`, err);
});
// Polling interval for changes
// TODO: Maybe there's a better way to do this
this.intervalId = setInterval(() => {
this.syncResources().catch(err => {
console.error(`Error syncing ${this.getControllerName()}:`, err);
});
}, SYNC_INTERVAL);
}
/**
* Stop watching for changes
*/
public stopWatching(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
console.log(`Stopped watching for changes in ${this.getControllerName()}`);
}
}
/**
* Get a name for this controller for logging purposes
*/
protected abstract getControllerName(): string;
/**
* Sync resources from database to Kubernetes
*/
protected abstract syncResources(): Promise<void>;
}

View File

@@ -0,0 +1,113 @@
import { PrismaClient } from '@minikura/db';
import type { ReverseProxyServer, CustomEnvironmentVariable } from '@minikura/db';
import { BaseController } from './base-controller';
import type { ReverseProxyConfig } from '../types';
import { createReverseProxyServer, deleteReverseProxyServer } from '../resources/reverseProxyServer';
type ReverseProxyWithEnvVars = ReverseProxyServer & {
env_variables: CustomEnvironmentVariable[];
};
export class ReverseProxyController extends BaseController {
private deployedProxies = new Map<string, ReverseProxyWithEnvVars>();
constructor(prisma: PrismaClient, namespace: string) {
super(prisma, namespace);
}
protected getControllerName(): string {
return 'ReverseProxyController';
}
protected async syncResources(): Promise<void> {
try {
const appsApi = this.k8sClient.getAppsApi();
const coreApi = this.k8sClient.getCoreApi();
const networkingApi = this.k8sClient.getNetworkingApi();
const proxies = await this.prisma.reverseProxyServer.findMany({
include: {
env_variables: true,
}
}) as ReverseProxyWithEnvVars[];
const currentProxyIds = new Set(proxies.map(proxy => proxy.id));
// Delete reverse proxy servers that are no longer in the database
for (const [proxyId, proxy] of this.deployedProxies.entries()) {
if (!currentProxyIds.has(proxyId)) {
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);
this.deployedProxies.delete(proxyId);
}
}
// Create or update reverse proxy servers that are in the database
for (const proxy of proxies) {
const deployedProxy = this.deployedProxies.get(proxy.id);
// If proxy doesn't exist yet or has been updated
if (!deployedProxy || this.hasProxyChanged(deployedProxy, proxy)) {
console.log(`${!deployedProxy ? 'Creating' : 'Updating'} reverse proxy server ${proxy.id} (${proxy.id}) in Kubernetes...`);
const proxyConfig: ReverseProxyConfig = {
id: proxy.id,
external_address: proxy.external_address,
external_port: proxy.external_port,
listen_port: proxy.listen_port,
description: proxy.description,
apiKey: proxy.api_key,
type: proxy.type,
memory: proxy.memory,
env_variables: proxy.env_variables?.map(ev => ({
key: ev.key,
value: ev.value
}))
};
await createReverseProxyServer(
proxyConfig,
appsApi,
coreApi,
networkingApi,
this.namespace
);
// Update cache
this.deployedProxies.set(proxy.id, { ...proxy });
}
}
} catch (error) {
console.error('Error syncing reverse proxy servers:', error);
throw error;
}
}
private hasProxyChanged(
oldProxy: ReverseProxyWithEnvVars,
newProxy: ReverseProxyWithEnvVars
): boolean {
// Check basic properties
const basicPropsChanged =
oldProxy.external_address !== newProxy.external_address ||
oldProxy.external_port !== newProxy.external_port ||
oldProxy.listen_port !== newProxy.listen_port ||
oldProxy.description !== newProxy.description;
if (basicPropsChanged) return true;
// Check if environment variables have changed
const oldEnvVars = oldProxy.env_variables || [];
const newEnvVars = newProxy.env_variables || [];
if (oldEnvVars.length !== newEnvVars.length) return true;
for (const newEnv of newEnvVars) {
const oldEnv = oldEnvVars.find(e => e.key === newEnv.key);
if (!oldEnv || oldEnv.value !== newEnv.value) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,113 @@
import { PrismaClient, ServerType } from '@minikura/db';
import type { Server, CustomEnvironmentVariable } from '@minikura/db';
import { BaseController } from './base-controller';
import type { ServerConfig } from '../types';
import { createServer, deleteServer } from '../resources/server';
type ServerWithEnvVars = Server & {
env_variables: CustomEnvironmentVariable[];
};
export class ServerController extends BaseController {
private deployedServers = new Map<string, ServerWithEnvVars>();
constructor(prisma: PrismaClient, namespace: string) {
super(prisma, namespace);
}
protected getControllerName(): string {
return 'ServerController';
}
protected async syncResources(): Promise<void> {
try {
const appsApi = this.k8sClient.getAppsApi();
const coreApi = this.k8sClient.getCoreApi();
const networkingApi = this.k8sClient.getNetworkingApi();
const servers = await this.prisma.server.findMany({
include: {
env_variables: true,
}
}) as ServerWithEnvVars[];
const currentServerIds = new Set(servers.map(server => server.id));
// Delete servers that are no longer in the database
for (const [serverId, server] of this.deployedServers.entries()) {
if (!currentServerIds.has(serverId)) {
console.log(`Server ${server.id} (${serverId}) has been removed from the database, deleting from Kubernetes...`);
await deleteServer(serverId, server.id, appsApi, coreApi, this.namespace);
this.deployedServers.delete(serverId);
}
}
// Create or update servers that are in the database
for (const server of servers) {
const deployedServer = this.deployedServers.get(server.id);
// If server doesn't exist yet or has been updated
if (!deployedServer || this.hasServerChanged(deployedServer, server)) {
console.log(`${!deployedServer ? 'Creating' : 'Updating'} server ${server.id} (${server.id}) in Kubernetes...`);
const serverConfig: ServerConfig = {
id: server.id,
type: server.type,
apiKey: server.api_key,
description: server.description,
listen_port: server.listen_port,
memory: server.memory,
env_variables: server.env_variables?.map(ev => ({
key: ev.key,
value: ev.value
}))
};
await createServer(
serverConfig,
appsApi,
coreApi,
networkingApi,
this.namespace
);
// Update cache
this.deployedServers.set(server.id, { ...server });
}
}
} catch (error) {
console.error('Error syncing servers:', error);
throw error;
}
}
private hasServerChanged(
oldServer: ServerWithEnvVars,
newServer: ServerWithEnvVars
): boolean {
// Check basic properties
const basicPropsChanged =
oldServer.type !== newServer.type ||
oldServer.listen_port !== newServer.listen_port ||
oldServer.description !== newServer.description;
if (basicPropsChanged) return true;
// Check if environment variables have changed
const oldEnvVars = oldServer.env_variables || [];
const newEnvVars = newServer.env_variables || [];
// Check if the number of env vars has changed
if (oldEnvVars.length !== newEnvVars.length) return true;
// Check if any of the existing env vars have changed
for (const newEnv of newEnvVars) {
const oldEnv = oldEnvVars.find(e => e.key === newEnv.key);
if (!oldEnv || oldEnv.value !== newEnv.value) {
return true;
}
}
return false;
}
}