feat: provision k8s namespace and secrets during customer setup
Some checks failed
Build & Release / build (push) Has been cancelled
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:
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user