Add initial Portal version
This commit is contained in:
132
src/lib/k8s.ts
Normal file
132
src/lib/k8s.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import type { PiecedTenant, PiecedTenantSpec } from "@/types";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const kc = new k8s.KubeConfig();
|
||||
if (process.env.KUBERNETES_SERVICE_HOST) {
|
||||
kc.loadFromCluster();
|
||||
} else {
|
||||
kc.loadFromDefault();
|
||||
}
|
||||
|
||||
const API_VERSION = "pieced.ch/v1alpha1";
|
||||
const PLURAL = "piecedtenants";
|
||||
|
||||
// Raw K8s API client — avoids @kubernetes/client-node API surface instability
|
||||
// across versions. The REST API itself is stable.
|
||||
|
||||
function getBaseUrl(): string {
|
||||
const cluster = kc.getCurrentCluster();
|
||||
if (!cluster) throw new Error("No active K8s cluster in kubeconfig");
|
||||
return cluster.server;
|
||||
}
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
// In-cluster: read SA token
|
||||
if (process.env.KUBERNETES_SERVICE_HOST) {
|
||||
const token = readFileSync(
|
||||
"/var/run/secrets/kubernetes.io/serviceaccount/token",
|
||||
"utf8"
|
||||
);
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
// Local dev: extract token from kubeconfig current user
|
||||
const user = kc.getCurrentUser();
|
||||
if (user?.token) {
|
||||
return { Authorization: `Bearer ${user.token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function k8sRequest<T>(
|
||||
path: string,
|
||||
method: string = "GET",
|
||||
body?: unknown
|
||||
): Promise<T> {
|
||||
const url = `${getBaseUrl()}/apis/${API_VERSION}/${PLURAL}${path}`;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err = new Error(`K8s ${method} ${path}: ${res.status} ${text}`);
|
||||
(err as any).statusCode = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function listTenants(): Promise<PiecedTenant[]> {
|
||||
const result = await k8sRequest<{ items: PiecedTenant[] }>("");
|
||||
return result.items ?? [];
|
||||
}
|
||||
|
||||
export async function getTenant(name: string): Promise<PiecedTenant | null> {
|
||||
try {
|
||||
return await k8sRequest<PiecedTenant>(`/${name}`);
|
||||
} catch (e: any) {
|
||||
if (e.statusCode === 404) return null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTenant(
|
||||
name: string,
|
||||
spec: PiecedTenantSpec,
|
||||
labels?: Record<string, string>
|
||||
): Promise<PiecedTenant> {
|
||||
return k8sRequest<PiecedTenant>("", "POST", {
|
||||
apiVersion: API_VERSION,
|
||||
kind: "PiecedTenant",
|
||||
metadata: { name, labels },
|
||||
spec,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateTenantSpec(
|
||||
name: string,
|
||||
spec: Partial<PiecedTenantSpec>
|
||||
): Promise<PiecedTenant> {
|
||||
const existing = await getTenant(name);
|
||||
if (!existing) throw new Error(`Tenant ${name} not found`);
|
||||
|
||||
return k8sRequest<PiecedTenant>(`/${name}`, "PUT", {
|
||||
...existing,
|
||||
spec: { ...existing.spec, ...spec },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteTenant(name: string): Promise<void> {
|
||||
await k8sRequest(`/${name}`, "DELETE");
|
||||
}
|
||||
|
||||
export async function patchTenantSpec(
|
||||
name: string,
|
||||
spec: Partial<PiecedTenantSpec>
|
||||
): Promise<PiecedTenant> {
|
||||
const url = `${getBaseUrl()}/apis/${API_VERSION}/${PLURAL}/${name}`;
|
||||
const res = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/merge-patch+json",
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ spec }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err = new Error(`K8s PATCH /${name}: ${res.status} ${text}`);
|
||||
(err as any).statusCode = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json() as Promise<PiecedTenant>;
|
||||
}
|
||||
Reference in New Issue
Block a user