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

48
src/services/do.ts Normal file
View File

@@ -0,0 +1,48 @@
import { config } from "../lib/config";
const DO_API = "https://api.digitalocean.com/v2";
async function doFetch(path: string, options: RequestInit = {}) {
const res = await fetch(`${DO_API}${path}`, {
...options,
headers: {
Authorization: `Bearer ${config.doToken}`,
"Content-Type": "application/json",
...(options.headers ?? {}),
},
});
if (!res.ok) {
const body = await res.text();
throw new Error(`DO API ${options.method ?? "GET"} ${path}${res.status}: ${body}`);
}
if (res.status === 204) return null;
return res.json();
}
export async function createDatabase(name: string) {
return doFetch(`/databases/${config.doDbClusterId}/dbs`, {
method: "POST",
body: JSON.stringify({ name }),
});
}
export async function createDatabaseUser(name: string): Promise<{ name: string; password: string }> {
const res = await doFetch(`/databases/${config.doDbClusterId}/users`, {
method: "POST",
body: JSON.stringify({ name }),
});
return res.user;
}
export async function deleteDatabaseUser(name: string) {
return doFetch(`/databases/${config.doDbClusterId}/users/${name}`, { method: "DELETE" });
}
export async function deleteDatabase(name: string) {
return doFetch(`/databases/${config.doDbClusterId}/dbs/${name}`, { method: "DELETE" });
}
export async function getDatabaseHost(): Promise<string> {
const res = await doFetch(`/databases/${config.doDbClusterId}`);
return res.database.connection.host;
}

76
src/services/git.ts Normal file
View File

@@ -0,0 +1,76 @@
import { execSync } from "child_process";
import { writeFileSync, mkdirSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { config } from "../lib/config";
function withRepo<T>(fn: (dir: string) => T): T {
const keyPath = join(tmpdir(), `manager-ssh-key-${Date.now()}`);
const dir = join(tmpdir(), `lunarfront-charts-${Date.now()}`);
writeFileSync(keyPath, config.gitSshKey, { mode: 0o600 });
const env = {
...process.env,
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o StrictHostKeyChecking=no`,
};
try {
execSync(`git clone ${config.gitRepoUrl} ${dir}`, { env, stdio: "pipe" });
execSync(`git -C ${dir} config user.email "manager@lunarfront.tech"`, { env });
execSync(`git -C ${dir} config user.name "lunarfront-manager"`, { env });
const result = fn(dir);
execSync(`git -C ${dir} push origin main`, { env, stdio: "pipe" });
return result;
} finally {
rmSync(dir, { recursive: true, force: true });
rmSync(keyPath, { force: true });
}
}
export function addCustomerChart(slug: string, appVersion: string) {
withRepo((dir) => {
const manifest = buildArgoCDApp(slug, appVersion);
writeFileSync(join(dir, "customers", `${slug}.yaml`), manifest);
execSync(`git -C ${dir} add customers/${slug}.yaml`);
execSync(`git -C ${dir} commit -m "feat: provision customer ${slug}"`, { env: process.env });
});
}
export function removeCustomerChart(slug: string) {
withRepo((dir) => {
rmSync(join(dir, "customers", `${slug}.yaml`), { force: true });
execSync(`git -C ${dir} add customers/${slug}.yaml`);
execSync(`git -C ${dir} commit -m "chore: deprovision customer ${slug}"`, { env: process.env });
});
}
function buildArgoCDApp(slug: string, appVersion: string): string {
return `apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: customer-${slug}
namespace: argocd
spec:
project: default
sources:
- repoURL: git.lunarfront.tech/ryan/lunarfront-app
chart: lunarfront
targetRevision: "${appVersion}"
helm:
valueFiles:
- $values/customers/${slug}.yaml
- repoURL: ssh://git@git-ssh.lunarfront.tech/ryan/lunarfront-charts.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: customer-${slug}
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
`;
}

50
src/services/pgbouncer.ts Normal file
View File

@@ -0,0 +1,50 @@
import { getConfigMap, getSecret, patchConfigMap, patchSecret, rolloutRestart } from "../lib/k8s";
const NAMESPACE = "pgbouncer";
const DO_HOST = "lunarfront-postgres-do-user-35277853-0.e.db.ondigitalocean.com";
const DO_PORT = 25060;
export async function addCustomerToPool(slug: string, password: string) {
await Promise.all([
addDbEntry(slug),
addUserEntry(slug, password),
]);
await rolloutRestart(NAMESPACE, "pgbouncer");
}
export async function removeCustomerFromPool(slug: string) {
await Promise.all([
removeDbEntry(slug),
removeUserEntry(slug),
]);
await rolloutRestart(NAMESPACE, "pgbouncer");
}
async function addDbEntry(slug: string) {
const cm = await getConfigMap(NAMESPACE, "pgbouncer-config");
const ini = cm["pgbouncer.ini"];
const newLine = ` ${slug} = host=${DO_HOST} port=${DO_PORT} dbname=${slug} user=${slug} pool_mode=session pool_size=3`;
const updated = ini.replace("[pgbouncer]", `${newLine}\n [pgbouncer]`);
await patchConfigMap(NAMESPACE, "pgbouncer-config", { "pgbouncer.ini": updated });
}
async function removeDbEntry(slug: string) {
const cm = await getConfigMap(NAMESPACE, "pgbouncer-config");
const ini = cm["pgbouncer.ini"];
const updated = ini.split("\n").filter((l) => !l.includes(`dbname=${slug}`)).join("\n");
await patchConfigMap(NAMESPACE, "pgbouncer-config", { "pgbouncer.ini": updated });
}
async function addUserEntry(slug: string, password: string) {
const secret = await getSecret(NAMESPACE, "pgbouncer-userlist");
const userlist = secret["userlist.txt"];
const updated = `${userlist}\n"${slug}" "${password}"`;
await patchSecret(NAMESPACE, "pgbouncer-userlist", { "userlist.txt": updated });
}
async function removeUserEntry(slug: string) {
const secret = await getSecret(NAMESPACE, "pgbouncer-userlist");
const userlist = secret["userlist.txt"];
const updated = userlist.split("\n").filter((l) => !l.startsWith(`"${slug}"`)).join("\n");
await patchSecret(NAMESPACE, "pgbouncer-userlist", { "userlist.txt": updated });
}