diff --git a/frontend/index.html b/frontend/index.html
index 12d9dc1..e46316c 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -741,6 +741,20 @@
Prefix
${infra.spaces.prefix}
` : ''}
+
Provisioning Steps
diff --git a/src/lib/config.ts b/src/lib/config.ts
index ffeda21..fd5d4d4 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -12,6 +12,9 @@ export const config = {
spacesRegion: process.env.SPACES_REGION ?? "nyc3",
spacesKey: process.env.SPACES_KEY!,
spacesSecret: process.env.SPACES_SECRET!,
+ cfApiToken: process.env.CF_API_TOKEN!,
+ cfZoneId: process.env.CF_ZONE_ID!,
+ ingressIp: process.env.INGRESS_IP ?? "167.99.21.170",
};
for (const [key, val] of Object.entries(config)) {
diff --git a/src/routes/customers.ts b/src/routes/customers.ts
index ffb69f3..1b34a3b 100644
--- a/src/routes/customers.ts
+++ b/src/routes/customers.ts
@@ -8,6 +8,7 @@ import { addCustomerChart, removeCustomerChart } from "../services/git";
import { setupCustomerDatabase, teardownCustomerDatabase } from "../services/db";
import { createNamespace, deleteNamespace, createSecret, createDockerRegistrySecret, patchSecret, getSecret, k8sFetch } from "../lib/k8s";
import { deleteSpacesObjects, getSpacesUsage } from "../services/spaces";
+import { createCustomerDnsRecord, deleteCustomerDnsRecord, getCustomerDnsRecord, checkCustomerHealth } from "../services/cloudflare";
import { db } from "../db/manager";
import { config } from "../lib/config";
import { getCachedStatus, setCachedStatus } from "../lib/cache";
@@ -75,6 +76,7 @@ export async function customerRoutes(app: FastifyInstance) {
namespace: "pending",
secrets: "pending",
storage: "pending",
+ dns: "pending",
chart: "pending",
};
@@ -135,6 +137,9 @@ export async function customerRoutes(app: FastifyInstance) {
});
await setStep("storage", "done");
+ await createCustomerDnsRecord(slug);
+ await setStep("dns", "done");
+
addCustomerChart(slug, body.appVersion);
await setStep("chart", "done");
@@ -166,6 +171,10 @@ export async function customerRoutes(app: FastifyInstance) {
await deleteSpacesObjects(config.spacesKey, config.spacesSecret, config.spacesBucket, config.spacesRegion, `${slug}/`);
} catch {}
+ try {
+ await deleteCustomerDnsRecord(slug);
+ } catch {}
+
await Promise.all([
deleteDatabase(slug),
deleteDatabaseUser(slug),
@@ -220,7 +229,7 @@ export async function customerRoutes(app: FastifyInstance) {
}
// ── Infrastructure checks ─────────────────────────────────────────────────
- const [dbCheck, sizeHistory, secrets] = await Promise.allSettled([
+ const [dbCheck, sizeHistory, secrets, dnsCheck, healthCheck] = await Promise.allSettled([
// Try connecting to the customer DB
(async () => {
const sql = postgres(config.doadminDbUrl.replace(/\/([^/?]+)(\?|$)/, `/${slug}$2`), { max: 1, connect_timeout: 5 });
@@ -239,10 +248,14 @@ export async function customerRoutes(app: FastifyInstance) {
LIMIT 30
`,
getSecret(namespace, "lunarfront-secrets").catch(() => null),
+ getCustomerDnsRecord(slug),
+ checkCustomerHealth(slug),
]);
const dbExists = dbCheck.status === "fulfilled" ? dbCheck.value : false;
const secretData = secrets.status === "fulfilled" ? secrets.value : null;
+ const dns = dnsCheck.status === "fulfilled" ? dnsCheck.value : { exists: false, proxied: false, ip: null };
+ const health = healthCheck.status === "fulfilled" ? healthCheck.value : { reachable: false, status: null };
const infra = {
database: { exists: dbExists },
spaces: {
@@ -250,6 +263,8 @@ export async function customerRoutes(app: FastifyInstance) {
bucket: secretData?.["spaces-bucket"] ?? null,
prefix: secretData?.["spaces-prefix"] ?? null,
},
+ dns,
+ health,
};
return reply.send({
diff --git a/src/services/cloudflare.ts b/src/services/cloudflare.ts
new file mode 100644
index 0000000..1f42c58
--- /dev/null
+++ b/src/services/cloudflare.ts
@@ -0,0 +1,54 @@
+import { config } from "../lib/config";
+
+const CF_API = "https://api.cloudflare.com/client/v4";
+
+async function cfFetch(path: string, options: RequestInit = {}) {
+ const res = await fetch(`${CF_API}${path}`, {
+ ...options,
+ headers: {
+ "Authorization": `Bearer ${config.cfApiToken}`,
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ });
+ const data = await res.json() as { success: boolean; result: any; errors: any[] };
+ if (!data.success) throw new Error(`Cloudflare API error: ${JSON.stringify(data.errors)}`);
+ return data.result;
+}
+
+export async function createCustomerDnsRecord(slug: string): Promise
{
+ await cfFetch(`/zones/${config.cfZoneId}/dns_records`, {
+ method: "POST",
+ body: JSON.stringify({
+ type: "A",
+ name: `${slug}.lunarfront.tech`,
+ content: config.ingressIp,
+ ttl: 1, // auto TTL
+ proxied: true,
+ }),
+ });
+}
+
+export async function deleteCustomerDnsRecord(slug: string): Promise {
+ const records = await cfFetch(`/zones/${config.cfZoneId}/dns_records?name=${slug}.lunarfront.tech&type=A`);
+ for (const record of records) {
+ await cfFetch(`/zones/${config.cfZoneId}/dns_records/${record.id}`, { method: "DELETE" });
+ }
+}
+
+export async function getCustomerDnsRecord(slug: string): Promise<{ exists: boolean; proxied: boolean; ip: string | null }> {
+ const records = await cfFetch(`/zones/${config.cfZoneId}/dns_records?name=${slug}.lunarfront.tech&type=A`);
+ if (!records.length) return { exists: false, proxied: false, ip: null };
+ return { exists: true, proxied: records[0].proxied, ip: records[0].content };
+}
+
+export async function checkCustomerHealth(slug: string): Promise<{ reachable: boolean; status: number | null }> {
+ try {
+ const res = await fetch(`https://${slug}.lunarfront.tech/health`, {
+ signal: AbortSignal.timeout(5000),
+ });
+ return { reachable: res.ok, status: res.status };
+ } catch {
+ return { reachable: false, status: null };
+ }
+}
diff --git a/src/services/git.ts b/src/services/git.ts
index fcb05ea..cb792a8 100644
--- a/src/services/git.ts
+++ b/src/services/git.ts
@@ -66,7 +66,7 @@ spec:
helm:
parameters:
- name: ingress.host
- value: ${slug}.lunarfront.app
+ value: ${slug}.lunarfront.tech
destination:
server: https://kubernetes.default.svc
namespace: customer-${slug}