feat: initial lunarfront-manager app

This commit is contained in:
Ryan Moon
2026-04-03 06:23:56 -05:00
commit 8287fbf5b8
16 changed files with 793 additions and 0 deletions

14
src/lib/config.ts Normal file
View File

@@ -0,0 +1,14 @@
export const config = {
port: Number(process.env.PORT ?? 3000),
doToken: process.env.DO_API_TOKEN!,
doDbClusterId: process.env.DO_DB_CLUSTER_ID!,
gitSshKey: process.env.GIT_SSH_KEY!,
gitRepoUrl: process.env.GIT_REPO_URL ?? "ssh://git@git-ssh.lunarfront.tech/ryan/lunarfront-charts.git",
dbUrl: process.env.DATABASE_URL!,
};
for (const [key, val] of Object.entries(config)) {
if (val === undefined || val === "") {
throw new Error(`Missing required env var for config key: ${key}`);
}
}

78
src/lib/k8s.ts Normal file
View File

@@ -0,0 +1,78 @@
import { readFileSync } from "fs";
import { execSync } from "child_process";
// 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();
}
async function k8sFetch(path: string, options: RequestInit = {}) {
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) {
const body = await res.text();
throw new Error(`k8s API ${options.method ?? "GET"} ${path}${res.status}: ${body}`);
}
return res.json();
}
export async function getSecret(namespace: string, name: string): Promise<Record<string, string>> {
const secret = await k8sFetch(`/api/v1/namespaces/${namespace}/secrets/${name}`);
return Object.fromEntries(
Object.entries(secret.data as Record<string, string>).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 k8sFetch(`/api/v1/namespaces/${namespace}/secrets/${name}`, {
method: "PATCH",
headers: { "Content-Type": "application/strategic-merge-patch+json" },
body: JSON.stringify({ data: encoded }),
});
}
export async function patchConfigMap(namespace: string, name: string, data: Record<string, string>) {
return k8sFetch(`/api/v1/namespaces/${namespace}/configmaps/${name}`, {
method: "PATCH",
headers: { "Content-Type": "application/strategic-merge-patch+json" },
body: JSON.stringify({ data }),
});
}
export async function getConfigMap(namespace: string, name: string): Promise<Record<string, string>> {
const cm = await k8sFetch(`/api/v1/namespaces/${namespace}/configmaps/${name}`);
return cm.data ?? {};
}
export async function createArgoCDApp(manifest: object) {
return k8sFetch(`/apis/argoproj.io/v1alpha1/namespaces/argocd/applications`, {
method: "POST",
body: JSON.stringify(manifest),
});
}
export async function rolloutRestart(namespace: string, deployment: string) {
return 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() } } } },
}),
});
}