import { execSync } from "child_process"; import { writeFileSync, mkdirSync, rmSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { config } from "../lib/config"; export async function getLatestChartVersion(): Promise { // Query backend image tags instead of Helm chart tags — DO API is unreliable for OCI helm artifacts const res = await fetch("https://api.digitalocean.com/v2/registry/lunarfront/repositories/lunarfront-app/tags?page=1&per_page=100", { headers: { Authorization: `Bearer ${config.doToken}` }, }); const data = await res.json() as { tags: { tag: string }[] }; const versions = (data.tags ?? []) .map(t => t.tag) .filter(t => /^\d+\.\d+\.\d+$/.test(t)) .sort((a, b) => { const [aMaj, aMin, aPat] = a.split(".").map(Number); const [bMaj, bMin, bPat] = b.split(".").map(Number); return bMaj - aMaj || bMin - aMin || bPat - aPat; }); if (!versions.length) throw new Error("No chart versions found in DOCR"); return versions[0]; } function withRepo(fn: (dir: string, env: NodeJS.ProcessEnv) => T): T { const keyPath = join(tmpdir(), `manager-ssh-key-${Date.now()}`); const dir = join(tmpdir(), `lunarfront-charts-${Date.now()}`); const keyContent = config.gitSshKey.endsWith("\n") ? config.gitSshKey : config.gitSshKey + "\n"; writeFileSync(keyPath, keyContent, { 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, env); const unpushed = execSync(`git -C ${dir} log origin/main..HEAD --oneline`, { env }).toString().trim(); if (unpushed) execSync(`git -C ${dir} push origin main`, { env, stdio: "pipe" }); return result; } finally { rmSync(dir, { recursive: true, force: true }); rmSync(keyPath, { force: true }); } } export async function addCustomerChart(slug: string, appVersion: string) { const version = (appVersion === "*" || appVersion === "latest") ? await getLatestChartVersion() : appVersion; withRepo((dir, env) => { const manifest = buildArgoCDApp(slug, version); writeFileSync(join(dir, "customers", `${slug}.yaml`), manifest); execSync(`git -C ${dir} add customers/${slug}.yaml`, { env }); execSync(`git -C ${dir} commit -m "feat: provision customer ${slug}"`, { env }); }); } export async function upgradeCustomerChart(slug: string, version: string) { withRepo((dir, env) => { const manifest = buildArgoCDApp(slug, version); writeFileSync(join(dir, "customers", `${slug}.yaml`), manifest); execSync(`git -C ${dir} add customers/${slug}.yaml`, { env }); const diff = execSync(`git -C ${dir} diff --cached --name-only`, { env }).toString().trim(); if (!diff) return; execSync(`git -C ${dir} commit -m "chore: upgrade customer ${slug} to chart ${version}"`, { env }); }); } export async function upgradeAllCustomerCharts(slugs: string[], version: string) { withRepo((dir, env) => { for (const slug of slugs) { const manifest = buildArgoCDApp(slug, version); writeFileSync(join(dir, "customers", `${slug}.yaml`), manifest); execSync(`git -C ${dir} add customers/${slug}.yaml`, { env }); } const diff = execSync(`git -C ${dir} diff --cached --name-only`, { env }).toString().trim(); if (!diff) return; execSync(`git -C ${dir} commit -m "chore: upgrade all customers to chart ${version}"`, { env }); }); } export function removeCustomerChart(slug: string) { withRepo((dir, env) => { const filePath = join(dir, "customers", `${slug}.yaml`); const tracked = execSync(`git -C ${dir} ls-files customers/${slug}.yaml`, { env }).toString().trim(); if (!tracked) return; rmSync(filePath, { force: true }); execSync(`git -C ${dir} add customers/${slug}.yaml`, { env }); execSync(`git -C ${dir} commit -m "chore: deprovision customer ${slug}"`, { env }); }); } function buildArgoCDApp(slug: string, version: string): string { const revision = version; return `apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: customer-${slug} namespace: argocd spec: project: default source: repoURL: registry.digitalocean.com/lunarfront chart: lunarfront targetRevision: "${revision}" helm: parameters: - name: ingress.host value: ${slug}.lunarfront.tech destination: server: https://kubernetes.default.svc namespace: customer-${slug} syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true `; }