146 lines
5.6 KiB
TypeScript
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(),
|
|
});
|
|
}
|