feat: provision k8s namespace and secrets during customer setup
Some checks failed
Build & Release / build (push) Has been cancelled

- k8s.ts: add createNamespace, deleteNamespace, createSecret, createDockerRegistrySecret
- customers.ts: create namespace + DOCR pull secret + app secrets (DATABASE_URL, JWT_SECRET, REDIS_URL) before pushing ArgoCD chart
- customers.ts: delete namespace on deprovision, search name field too
- git.ts: use DOCR OCI chart URL and helm parameters for customer ArgoCD apps
- Add 'namespace' and 'secrets' steps to provisioning step tracker
This commit is contained in:
Ryan Moon
2026-04-03 18:54:11 -05:00
parent cadf0bb191
commit 19135b0520
3 changed files with 102 additions and 31 deletions

View File

@@ -1,12 +1,17 @@
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import crypto from "crypto";
import { createDatabase, createDatabaseUser, deleteDatabase, deleteDatabaseUser } from "../services/do";
import { addCustomerToPool, removeCustomerFromPool } from "../services/pgbouncer";
import { addCustomerChart, removeCustomerChart } from "../services/git";
import { setupCustomerDatabase, teardownCustomerDatabase } from "../services/db";
import { createNamespace, deleteNamespace, createSecret, createDockerRegistrySecret } from "../lib/k8s";
import { db } from "../db/manager";
import { config } from "../lib/config";
const MODULES = ["pos", "inventory", "rentals", "scheduling", "repairs", "accounting"] as const;
const PGBOUNCER_HOST = "pgbouncer.pgbouncer.svc";
const PGBOUNCER_PORT = 5432;
const ProvisionSchema = z.object({
slug: z.string().min(2).max(32).regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only"),
@@ -33,10 +38,10 @@ export async function customerRoutes(app: FastifyInstance) {
const [rows, [{ count }]] = await Promise.all([
search
? db`SELECT * FROM customers WHERE slug ILIKE ${search} ORDER BY ${db(query.sort)} ${db.unsafe(query.order)} LIMIT ${query.limit} OFFSET ${offset}`
? db`SELECT * FROM customers WHERE slug ILIKE ${search} OR name ILIKE ${search} ORDER BY ${db(query.sort)} ${db.unsafe(query.order)} LIMIT ${query.limit} OFFSET ${offset}`
: db`SELECT * FROM customers ORDER BY ${db(query.sort)} ${db.unsafe(query.order)} LIMIT ${query.limit} OFFSET ${offset}`,
search
? db`SELECT COUNT(*)::int FROM customers WHERE slug ILIKE ${search}`
? db`SELECT COUNT(*)::int FROM customers WHERE slug ILIKE ${search} OR name ILIKE ${search}`
: db`SELECT COUNT(*)::int FROM customers`,
]);
@@ -54,6 +59,7 @@ export async function customerRoutes(app: FastifyInstance) {
app.post("/customers", async (req, reply) => {
const body = ProvisionSchema.parse(req.body);
const slug = body.slug;
const namespace = `customer-${slug}`;
app.log.info({ slug }, "provisioning customer");
@@ -63,6 +69,8 @@ export async function customerRoutes(app: FastifyInstance) {
database_user: "pending",
database_setup: "pending",
pool: "pending",
namespace: "pending",
secrets: "pending",
chart: "pending",
};
@@ -71,7 +79,6 @@ export async function customerRoutes(app: FastifyInstance) {
await db`UPDATE customers SET steps = ${db.json(steps)}, updated_at = NOW() WHERE slug = ${slug}`;
};
// Insert record immediately so partial failures are visible in the UI
await db`
INSERT INTO customers (slug, name, modules, start_date, expiration_date, status, steps)
VALUES (${slug}, ${body.name}, ${body.modules}, ${body.startDate}, ${body.expirationDate}, 'provisioning', ${db.json(steps)})
@@ -97,6 +104,24 @@ export async function customerRoutes(app: FastifyInstance) {
await addCustomerToPool(slug, user.password);
await setStep("pool", "done");
// Create k8s namespace and secrets before pushing chart so ArgoCD can deploy immediately
await createNamespace(namespace);
await createDockerRegistrySecret(namespace, "registry-lunarfront", {
server: "registry.digitalocean.com",
username: "token",
password: config.doToken,
});
const jwtSecret = crypto.randomBytes(32).toString("hex");
const databaseUrl = `postgresql://${slug}:${user.password}@${PGBOUNCER_HOST}:${PGBOUNCER_PORT}/${slug}`;
const redisUrl = `redis://${namespace}-valkey:6379`;
await createSecret(namespace, "lunarfront-secrets", {
"database-url": databaseUrl,
"jwt-secret": jwtSecret,
"redis-url": redisUrl,
});
await setStep("namespace", "done");
await setStep("secrets", "done");
addCustomerChart(slug, body.appVersion);
await setStep("chart", "done");
@@ -115,6 +140,7 @@ export async function customerRoutes(app: FastifyInstance) {
app.delete("/customers/:slug", async (req, reply) => {
const { slug } = req.params as { slug: string };
const namespace = `customer-${slug}`;
app.log.info({ slug }, "deprovisioning customer");
@@ -124,6 +150,7 @@ export async function customerRoutes(app: FastifyInstance) {
await Promise.all([
deleteDatabase(slug),
deleteDatabaseUser(slug),
deleteNamespace(namespace),
]);
await db`DELETE FROM customers WHERE slug = ${slug}`;