Files
vibn-frontend/lib/coolify-workspace.ts

146 lines
5.6 KiB
TypeScript

/**
* Coolify Workspace Provisioning
*
* Provisions a dedicated Theia IDE instance per Vibn project using the
* Coolify Docker image application API. Each workspace is:
* - Hosted at {slug}.ide.vibnai.com
* - Protected by the vibn-auth ForwardAuth (project-owner only)
* - Running ghcr.io/eclipse-theia/theia-blueprint/theia-ide:latest
*/
const COOLIFY_URL = process.env.COOLIFY_URL ?? 'http://34.19.250.135:8000';
const COOLIFY_API_TOKEN = process.env.COOLIFY_API_TOKEN ?? '';
// Coolify resource IDs (stable — tied to the Vibn server/project setup)
const COOLIFY_PROJECT_UUID = 'f4owwggokksgw0ogo0844os0'; // "Vibn" project
const COOLIFY_ENVIRONMENT = 'production';
const COOLIFY_SERVER_UUID = 'jws4g4cgssss4cw48s488woc'; // localhost (Coolify host)
const THEIA_IMAGE_NAME = 'ghcr.io/eclipse-theia/theia-blueprint/theia-ide';
const THEIA_IMAGE_TAG = 'latest';
const THEIA_PORT = '3000';
const IDE_DOMAIN_SUFFIX = '.ide.vibnai.com';
function coolifyHeaders() {
return {
Authorization: `Bearer ${COOLIFY_API_TOKEN}`,
'Content-Type': 'application/json',
};
}
/**
* Builds the newline-separated Traefik label string that Coolify stores
* as custom_labels. We add vibn-auth@file to the HTTPS router middleware
* chain after Coolify's generated labels.
*
* Router naming convention observed in Coolify:
* https-0-{uuid} → the TLS router for the app
*/
function buildCustomLabels(appUuid: string): string {
const routerName = `https-0-${appUuid}`;
return [
'traefik.enable=true',
`traefik.http.routers.${routerName}.middlewares=vibn-auth@file,gzip`,
].join('\n');
}
export interface ProvisionResult {
appUuid: string;
workspaceUrl: string;
}
/**
* Creates a new Coolify Docker-image application for a Vibn project Theia workspace.
* Sets the vibn-auth ForwardAuth middleware so only the project owner can access it.
*/
export async function provisionTheiaWorkspace(
slug: string,
projectId: string,
giteaRepo: string | null,
): Promise<ProvisionResult> {
const workspaceUrl = `https://${slug}${IDE_DOMAIN_SUFFIX}`;
const appName = `theia-${slug}`;
// ── Step 1: Create the app ────────────────────────────────────────────────
const createRes = await fetch(`${COOLIFY_URL}/api/v1/applications/dockerimage`, {
method: 'POST',
headers: coolifyHeaders(),
body: JSON.stringify({
project_uuid: COOLIFY_PROJECT_UUID,
environment_name: COOLIFY_ENVIRONMENT,
server_uuid: COOLIFY_SERVER_UUID,
docker_registry_image_name: THEIA_IMAGE_NAME,
docker_registry_image_tag: THEIA_IMAGE_TAG,
name: appName,
description: `Theia IDE for Vibn project ${slug}`,
ports_exposes: THEIA_PORT,
domains: workspaceUrl,
instant_deploy: false, // we deploy after patching labels
}),
});
if (!createRes.ok) {
const body = await createRes.text();
throw new Error(`Coolify create app failed (${createRes.status}): ${body}`);
}
const { uuid: appUuid } = await createRes.json() as { uuid: string };
// ── Step 2: Patch with vibn-auth Traefik labels ───────────────────────────
const patchRes = await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}`, {
method: 'PATCH',
headers: coolifyHeaders(),
body: JSON.stringify({
custom_labels: buildCustomLabels(appUuid),
}),
});
if (!patchRes.ok) {
console.warn(`[workspace] PATCH labels failed (${patchRes.status}) — continuing`);
}
// ── Step 3: Set environment variables ────────────────────────────────────
const giteaBaseUrl = process.env.GITEA_URL ?? 'https://git.vibnai.com';
const giteaToken = process.env.GITEA_TOKEN ?? '';
// Authenticated clone URL so Theia can git clone on startup
const giteaCloneUrl = giteaRepo
? `https://${giteaToken ? `oauth2:${giteaToken}@` : ''}${giteaBaseUrl.replace(/^https?:\/\//, '')}/${giteaRepo}.git`
: '';
const envVars = [
{ key: 'VIBN_PROJECT_ID', value: projectId, is_preview: false },
{ key: 'VIBN_PROJECT_SLUG', value: slug, is_preview: false },
{ key: 'GITEA_REPO', value: giteaRepo ?? '', is_preview: false },
{ key: 'GITEA_CLONE_URL', value: giteaCloneUrl, is_preview: false },
{ key: 'GITEA_API_URL', value: giteaBaseUrl, is_preview: false },
// Theia opens this path as its workspace root
{ key: 'THEIA_WORKSPACE_ROOT', value: `/home/theia/${slug}`, is_preview: false },
];
await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}/envs/bulk`, {
method: 'POST',
headers: coolifyHeaders(),
body: JSON.stringify({ data: envVars }),
});
// ── Step 4: Deploy ────────────────────────────────────────────────────────
await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}/start`, {
method: 'POST',
headers: coolifyHeaders(),
});
console.log(`[workspace] Provisioned ${appName}${workspaceUrl} (uuid: ${appUuid})`);
return { appUuid, workspaceUrl };
}
/**
* Deletes a provisioned Theia workspace from Coolify.
*/
export async function deleteTheiaWorkspace(appUuid: string): Promise<void> {
await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}`, {
method: 'DELETE',
headers: coolifyHeaders(),
});
}