136 lines
4.8 KiB
TypeScript
136 lines
4.8 KiB
TypeScript
import { readFileSync } from "fs";
|
|
|
|
// Read in-cluster service account token and CA
|
|
const SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
|
const K8S_API = process.env.KUBERNETES_SERVICE_HOST
|
|
? `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT}`
|
|
: "https://kubernetes.default.svc";
|
|
|
|
function token() {
|
|
return readFileSync(SA_TOKEN_PATH, "utf-8").trim();
|
|
}
|
|
|
|
export async function k8sFetch(path: string, options: RequestInit = {}, allowStatuses: number[] = []) {
|
|
const res = await fetch(`${K8S_API}${path}`, {
|
|
...options,
|
|
headers: {
|
|
Authorization: `Bearer ${token()}`,
|
|
"Content-Type": "application/json",
|
|
...(options.headers ?? {}),
|
|
},
|
|
// @ts-ignore - bun supports this
|
|
tls: { rejectUnauthorized: false },
|
|
});
|
|
if (!res.ok && !allowStatuses.includes(res.status)) {
|
|
const body = await res.text();
|
|
throw new Error(`k8s API ${options.method ?? "GET"} ${path} → ${res.status}: ${body}`);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
export async function getSecret(namespace: string, name: string): Promise<Record<string, string>> {
|
|
const res = await k8sFetch(`/api/v1/namespaces/${namespace}/secrets/${name}`);
|
|
const secret = await res.json() as { data: Record<string, string> };
|
|
return Object.fromEntries(
|
|
Object.entries(secret.data).map(([k, v]) => [k, Buffer.from(v, "base64").toString()])
|
|
);
|
|
}
|
|
|
|
export async function patchSecret(namespace: string, name: string, data: Record<string, string>) {
|
|
const encoded = Object.fromEntries(
|
|
Object.entries(data).map(([k, v]) => [k, Buffer.from(v).toString("base64")])
|
|
);
|
|
return (await k8sFetch(`/api/v1/namespaces/${namespace}/secrets/${name}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/strategic-merge-patch+json" },
|
|
body: JSON.stringify({ data: encoded }),
|
|
})).json();
|
|
}
|
|
|
|
export async function createSecret(namespace: string, name: string, data: Record<string, string>) {
|
|
const encoded = Object.fromEntries(
|
|
Object.entries(data).map(([k, v]) => [k, Buffer.from(v).toString("base64")])
|
|
);
|
|
await k8sFetch(`/api/v1/namespaces/${namespace}/secrets`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
apiVersion: "v1",
|
|
kind: "Secret",
|
|
metadata: { name, namespace },
|
|
data: encoded,
|
|
}),
|
|
}, [409]); // 409 = already exists, ignore
|
|
}
|
|
|
|
export async function createDockerRegistrySecret(
|
|
namespace: string,
|
|
name: string,
|
|
opts: { server: string; username: string; password: string },
|
|
) {
|
|
const auth = Buffer.from(`${opts.username}:${opts.password}`).toString("base64");
|
|
const dockerConfig = JSON.stringify({
|
|
auths: { [opts.server]: { username: opts.username, password: opts.password, auth } },
|
|
});
|
|
const encoded = Buffer.from(dockerConfig).toString("base64");
|
|
await k8sFetch(`/api/v1/namespaces/${namespace}/secrets`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
apiVersion: "v1",
|
|
kind: "Secret",
|
|
metadata: { name, namespace },
|
|
type: "kubernetes.io/dockerconfigjson",
|
|
data: { ".dockerconfigjson": encoded },
|
|
}),
|
|
}, [409]);
|
|
}
|
|
|
|
export async function createNamespace(name: string) {
|
|
await k8sFetch(`/api/v1/namespaces`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
apiVersion: "v1",
|
|
kind: "Namespace",
|
|
metadata: { name },
|
|
}),
|
|
}, [409]); // 409 = already exists, ignore
|
|
}
|
|
|
|
export async function deleteNamespace(name: string) {
|
|
await k8sFetch(`/api/v1/namespaces/${name}`, { method: "DELETE" }, [404]);
|
|
}
|
|
|
|
export async function patchConfigMap(namespace: string, name: string, data: Record<string, string>) {
|
|
return (await k8sFetch(`/api/v1/namespaces/${namespace}/configmaps/${name}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/strategic-merge-patch+json" },
|
|
body: JSON.stringify({ data }),
|
|
})).json();
|
|
}
|
|
|
|
export async function getConfigMap(namespace: string, name: string): Promise<Record<string, string>> {
|
|
const res = await k8sFetch(`/api/v1/namespaces/${namespace}/configmaps/${name}`);
|
|
const cm = await res.json() as { data?: Record<string, string> };
|
|
return cm.data ?? {};
|
|
}
|
|
|
|
export async function syncArgoApp(name: string) {
|
|
await k8sFetch(`/apis/argoproj.io/v1alpha1/namespaces/argocd/applications/${name}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/merge-patch+json" },
|
|
body: JSON.stringify({
|
|
metadata: { annotations: { "argocd.argoproj.io/refresh": "hard" } },
|
|
operation: { sync: { prune: true } },
|
|
}),
|
|
}, [404]);
|
|
}
|
|
|
|
export async function rolloutRestart(namespace: string, deployment: string) {
|
|
return (await k8sFetch(`/apis/apps/v1/namespaces/${namespace}/deployments/${deployment}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/strategic-merge-patch+json" },
|
|
body: JSON.stringify({
|
|
spec: { template: { metadata: { annotations: { "kubectl.kubernetes.io/restartedAt": new Date().toISOString() } } } },
|
|
}),
|
|
})).json();
|
|
}
|