feat: single shared Spaces key, deactivate/reactivate customer, status badge for inactive
Some checks failed
Build & Release / build (push) Has been cancelled

This commit is contained in:
Ryan Moon
2026-04-03 20:21:51 -05:00
parent 78503f993d
commit fe62514515
4 changed files with 74 additions and 36 deletions

View File

@@ -7,7 +7,7 @@ import { addCustomerToPool, removeCustomerFromPool } from "../services/pgbouncer
import { addCustomerChart, removeCustomerChart } from "../services/git";
import { setupCustomerDatabase, teardownCustomerDatabase } from "../services/db";
import { createNamespace, deleteNamespace, createSecret, createDockerRegistrySecret, patchSecret, getSecret, k8sFetch } from "../lib/k8s";
import { createSpacesKey, deleteSpacesKey, deleteSpacesObjects, getSpacesUsage } from "../services/spaces";
import { deleteSpacesObjects, getSpacesUsage } from "../services/spaces";
import { db } from "../db/manager";
import { config } from "../lib/config";
import { getCachedStatus, setCachedStatus } from "../lib/cache";
@@ -126,18 +126,13 @@ export async function customerRoutes(app: FastifyInstance) {
await setStep("namespace", "done");
await setStep("secrets", "done");
const { accessKey: spacesKey, secretKey: spacesSecret } = await createSpacesKey(
`customer-${slug}`,
config.spacesBucket,
);
await patchSecret(namespace, "lunarfront-secrets", {
"spaces-key": spacesKey,
"spaces-secret": spacesSecret,
"spaces-key": config.spacesKey,
"spaces-secret": config.spacesSecret,
"spaces-bucket": config.spacesBucket,
"spaces-endpoint": `https://${config.spacesRegion}.digitaloceanspaces.com`,
"spaces-prefix": `${slug}/`,
});
await db`UPDATE customers SET spaces_key = ${spacesKey}, updated_at = NOW() WHERE slug = ${slug}`;
await setStep("storage", "done");
addCustomerChart(slug, body.appVersion);
@@ -162,19 +157,14 @@ export async function customerRoutes(app: FastifyInstance) {
app.log.info({ slug }, "deprovisioning customer");
const [customer] = await db`SELECT spaces_key FROM customers WHERE slug = ${slug}`;
removeCustomerChart(slug);
await removeCustomerFromPool(slug);
await teardownCustomerDatabase(slug, slug);
if (customer?.spaces_key) {
try {
const secrets = await getSecret(namespace, "lunarfront-secrets");
await deleteSpacesObjects(customer.spaces_key, secrets["spaces-secret"], config.spacesBucket, config.spacesRegion, `${slug}/`);
} catch {}
try { await deleteSpacesKey(customer.spaces_key); } catch {}
}
// Delete all objects under this customer's prefix in Spaces
try {
await deleteSpacesObjects(config.spacesKey, config.spacesSecret, config.spacesBucket, config.spacesRegion, `${slug}/`);
} catch {}
await Promise.all([
deleteDatabase(slug),
@@ -247,6 +237,35 @@ export async function customerRoutes(app: FastifyInstance) {
// Remove only the manager DB record without touching infrastructure —
// useful for cleaning up failed partial deployments
// Deactivate: remove ArgoCD chart (stops pods) but keep DB, namespace, secrets, and manager record
app.post("/customers/:slug/deactivate", async (req, reply) => {
const { slug } = req.params as { slug: string };
removeCustomerChart(slug);
await removeCustomerFromPool(slug);
await db`UPDATE customers SET status = 'inactive', updated_at = NOW() WHERE slug = ${slug}`;
app.log.info({ slug }, "customer deactivated");
return reply.code(200).send({ slug, status: "inactive" });
});
// Reactivate: push ArgoCD chart back, re-add to pool
app.post("/customers/:slug/reactivate", async (req, reply) => {
const { slug } = req.params as { slug: string };
const [customer] = await db`SELECT * FROM customers WHERE slug = ${slug}`;
if (!customer) return reply.code(404).send({ message: "Not found" });
// Re-add to pgbouncer — need the password from the k8s secret
const namespace = `customer-${slug}`;
const secrets = await getSecret(namespace, "lunarfront-secrets");
const dbUrl = new URL(secrets["database-url"]);
await addCustomerToPool(slug, dbUrl.password);
addCustomerChart(slug, "*");
await db`UPDATE customers SET status = 'provisioned', updated_at = NOW() WHERE slug = ${slug}`;
app.log.info({ slug }, "customer reactivated");
return reply.code(200).send({ slug, status: "provisioned" });
});
// Remove only the manager DB record without touching infrastructure
app.delete("/customers/:slug/record", async (req, reply) => {
const { slug } = req.params as { slug: string };
await db`DELETE FROM customers WHERE slug = ${slug}`;