feat: add Cloudflare DNS provisioning and health checks
All checks were successful
Build & Release / build (push) Successful in 12s
All checks were successful
Build & Release / build (push) Successful in 12s
This commit is contained in:
@@ -741,6 +741,20 @@
|
|||||||
<span class="stat-label">Prefix</span>
|
<span class="stat-label">Prefix</span>
|
||||||
<span class="stat-value" style="font-family:monospace;font-size:0.8rem">${infra.spaces.prefix}</span>
|
<span class="stat-value" style="font-family:monospace;font-size:0.8rem">${infra.spaces.prefix}</span>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">DNS</span>
|
||||||
|
<span class="stat-value">${infra?.dns?.exists
|
||||||
|
? '<span class="badge badge-green">Active</span>'
|
||||||
|
: '<span class="badge badge-red">Missing</span>'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Health</span>
|
||||||
|
<span class="stat-value">${infra?.health?.reachable
|
||||||
|
? '<span class="badge badge-green">Healthy</span>'
|
||||||
|
: infra?.health?.status
|
||||||
|
? `<span class="badge badge-red">HTTP ${infra.health.status}</span>`
|
||||||
|
: '<span class="badge badge-gray">Unreachable</span>'}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="card-title">Provisioning Steps</div>
|
<div class="card-title">Provisioning Steps</div>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export const config = {
|
|||||||
spacesRegion: process.env.SPACES_REGION ?? "nyc3",
|
spacesRegion: process.env.SPACES_REGION ?? "nyc3",
|
||||||
spacesKey: process.env.SPACES_KEY!,
|
spacesKey: process.env.SPACES_KEY!,
|
||||||
spacesSecret: process.env.SPACES_SECRET!,
|
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)) {
|
for (const [key, val] of Object.entries(config)) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { addCustomerChart, removeCustomerChart } from "../services/git";
|
|||||||
import { setupCustomerDatabase, teardownCustomerDatabase } from "../services/db";
|
import { setupCustomerDatabase, teardownCustomerDatabase } from "../services/db";
|
||||||
import { createNamespace, deleteNamespace, createSecret, createDockerRegistrySecret, patchSecret, getSecret, k8sFetch } from "../lib/k8s";
|
import { createNamespace, deleteNamespace, createSecret, createDockerRegistrySecret, patchSecret, getSecret, k8sFetch } from "../lib/k8s";
|
||||||
import { deleteSpacesObjects, getSpacesUsage } from "../services/spaces";
|
import { deleteSpacesObjects, getSpacesUsage } from "../services/spaces";
|
||||||
|
import { createCustomerDnsRecord, deleteCustomerDnsRecord, getCustomerDnsRecord, checkCustomerHealth } from "../services/cloudflare";
|
||||||
import { db } from "../db/manager";
|
import { db } from "../db/manager";
|
||||||
import { config } from "../lib/config";
|
import { config } from "../lib/config";
|
||||||
import { getCachedStatus, setCachedStatus } from "../lib/cache";
|
import { getCachedStatus, setCachedStatus } from "../lib/cache";
|
||||||
@@ -75,6 +76,7 @@ export async function customerRoutes(app: FastifyInstance) {
|
|||||||
namespace: "pending",
|
namespace: "pending",
|
||||||
secrets: "pending",
|
secrets: "pending",
|
||||||
storage: "pending",
|
storage: "pending",
|
||||||
|
dns: "pending",
|
||||||
chart: "pending",
|
chart: "pending",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,6 +137,9 @@ export async function customerRoutes(app: FastifyInstance) {
|
|||||||
});
|
});
|
||||||
await setStep("storage", "done");
|
await setStep("storage", "done");
|
||||||
|
|
||||||
|
await createCustomerDnsRecord(slug);
|
||||||
|
await setStep("dns", "done");
|
||||||
|
|
||||||
addCustomerChart(slug, body.appVersion);
|
addCustomerChart(slug, body.appVersion);
|
||||||
await setStep("chart", "done");
|
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}/`);
|
await deleteSpacesObjects(config.spacesKey, config.spacesSecret, config.spacesBucket, config.spacesRegion, `${slug}/`);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteCustomerDnsRecord(slug);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
deleteDatabase(slug),
|
deleteDatabase(slug),
|
||||||
deleteDatabaseUser(slug),
|
deleteDatabaseUser(slug),
|
||||||
@@ -220,7 +229,7 @@ export async function customerRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Infrastructure checks ─────────────────────────────────────────────────
|
// ── Infrastructure checks ─────────────────────────────────────────────────
|
||||||
const [dbCheck, sizeHistory, secrets] = await Promise.allSettled([
|
const [dbCheck, sizeHistory, secrets, dnsCheck, healthCheck] = await Promise.allSettled([
|
||||||
// Try connecting to the customer DB
|
// Try connecting to the customer DB
|
||||||
(async () => {
|
(async () => {
|
||||||
const sql = postgres(config.doadminDbUrl.replace(/\/([^/?]+)(\?|$)/, `/${slug}$2`), { max: 1, connect_timeout: 5 });
|
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
|
LIMIT 30
|
||||||
`,
|
`,
|
||||||
getSecret(namespace, "lunarfront-secrets").catch(() => null),
|
getSecret(namespace, "lunarfront-secrets").catch(() => null),
|
||||||
|
getCustomerDnsRecord(slug),
|
||||||
|
checkCustomerHealth(slug),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dbExists = dbCheck.status === "fulfilled" ? dbCheck.value : false;
|
const dbExists = dbCheck.status === "fulfilled" ? dbCheck.value : false;
|
||||||
const secretData = secrets.status === "fulfilled" ? secrets.value : null;
|
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 = {
|
const infra = {
|
||||||
database: { exists: dbExists },
|
database: { exists: dbExists },
|
||||||
spaces: {
|
spaces: {
|
||||||
@@ -250,6 +263,8 @@ export async function customerRoutes(app: FastifyInstance) {
|
|||||||
bucket: secretData?.["spaces-bucket"] ?? null,
|
bucket: secretData?.["spaces-bucket"] ?? null,
|
||||||
prefix: secretData?.["spaces-prefix"] ?? null,
|
prefix: secretData?.["spaces-prefix"] ?? null,
|
||||||
},
|
},
|
||||||
|
dns,
|
||||||
|
health,
|
||||||
};
|
};
|
||||||
|
|
||||||
return reply.send({
|
return reply.send({
|
||||||
|
|||||||
54
src/services/cloudflare.ts
Normal file
54
src/services/cloudflare.ts
Normal file
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,7 +66,7 @@ spec:
|
|||||||
helm:
|
helm:
|
||||||
parameters:
|
parameters:
|
||||||
- name: ingress.host
|
- name: ingress.host
|
||||||
value: ${slug}.lunarfront.app
|
value: ${slug}.lunarfront.tech
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
namespace: customer-${slug}
|
namespace: customer-${slug}
|
||||||
|
|||||||
Reference in New Issue
Block a user