diff --git a/prisma/migrations/20220418214843_persistent_storage_services/migration.sql b/prisma/migrations/20220418214843_persistent_storage_services/migration.sql new file mode 100644 index 000000000..f85fd31df --- /dev/null +++ b/prisma/migrations/20220418214843_persistent_storage_services/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "ServicePersistentStorage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "path" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ServicePersistentStorage_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_path_key" ON "ServicePersistentStorage"("serviceId", "path"); diff --git a/prisma/migrations/20220419203408_multiply_dockerfile_locations/migration.sql b/prisma/migrations/20220419203408_multiply_dockerfile_locations/migration.sql new file mode 100644 index 000000000..ce32f0844 --- /dev/null +++ b/prisma/migrations/20220419203408_multiply_dockerfile_locations/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "dockerFileLocation" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 928562abd..668f4588a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { - provider = "prisma-client-js" - binaryTargets = ["linux-musl"] + provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl"] } datasource db { @@ -91,6 +91,7 @@ model Application { pythonWSGI String? pythonModule String? pythonVariable String? + dockerFileLocation String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt settings ApplicationSettings? @@ -118,14 +119,25 @@ model ApplicationSettings { model ApplicationPersistentStorage { id String @id @default(cuid()) application Application @relation(fields: [applicationId], references: [id]) - applicationId String - path String + applicationId String + path String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([applicationId, path]) } +model ServicePersistentStorage { + id String @id @default(cuid()) + service Service @relation(fields: [serviceId], references: [id]) + serviceId String + path String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([serviceId, path]) +} + model Secret { id String @id @default(cuid()) name String @@ -267,17 +279,17 @@ model DatabaseSettings { } model Service { - id String @id @default(cuid()) + id String @id @default(cuid()) name String fqdn String? - dualCerts Boolean @default(false) + dualCerts Boolean @default(false) type String? version String? teams Team[] destinationDockerId String? - destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt plausibleAnalytics PlausibleAnalytics? minio Minio? vscodeserver Vscodeserver? @@ -285,6 +297,7 @@ model Service { ghost Ghost? serviceSecret ServiceSecret[] meiliSearch MeiliSearch? + persistentStorage ServicePersistentStorage[] } model PlausibleAnalytics { diff --git a/src/app.d.ts b/src/app.d.ts index d432aef4a..37f419edf 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -6,7 +6,11 @@ declare namespace App { cookies: Record; } interface Platform {} - interface Session extends SessionData {} + interface Session extends SessionData { + whiteLabelDetails: { + icon: string | null; + }; + } interface Stuff { service: any; application: any; diff --git a/src/hooks.ts b/src/hooks.ts index c72c7fd6c..fef98375f 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -8,6 +8,9 @@ import cookie from 'cookie'; import { dev } from '$app/env'; const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true'; +const whiteLabelDetails = { + icon: (whiteLabeled && process.env['COOLIFY_WHITE_LABELED_ICON']) || null +}; export const handle = handleSession( { @@ -74,6 +77,7 @@ export const getSession: GetSession = function ({ locals }) { return { version, whiteLabeled, + whiteLabelDetails, ...locals.session.data }; }; diff --git a/src/lib/buildPacks/common.ts b/src/lib/buildPacks/common.ts index 65f4d6903..e6f62bd23 100644 --- a/src/lib/buildPacks/common.ts +++ b/src/lib/buildPacks/common.ts @@ -91,7 +91,8 @@ export const setDefaultConfiguration = async (data) => { startCommand, buildCommand, publishDirectory, - baseDirectory + baseDirectory, + dockerFileLocation } = data; const template = scanningTemplates[buildPack]; if (!port) { @@ -110,6 +111,12 @@ export const setDefaultConfiguration = async (data) => { if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`; } + if (dockerFileLocation) { + if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`; + if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1); + } else { + dockerFileLocation = '/Dockerfile'; + } return { buildPack, @@ -118,7 +125,8 @@ export const setDefaultConfiguration = async (data) => { startCommand, buildCommand, publishDirectory, - baseDirectory + baseDirectory, + dockerFileLocation }; }; diff --git a/src/lib/buildPacks/docker.ts b/src/lib/buildPacks/docker.ts index 2db5d567c..1ddf81a60 100644 --- a/src/lib/buildPacks/docker.ts +++ b/src/lib/buildPacks/docker.ts @@ -10,15 +10,16 @@ export default async function ({ buildId, baseDirectory, secrets, - pullmergeRequestId + pullmergeRequestId, + dockerFileLocation }) { try { - let file = `${workdir}/Dockerfile`; + const file = `${workdir}${dockerFileLocation}`; + let dockerFileOut = `${workdir}`; if (baseDirectory) { - file = `${workdir}/${baseDirectory}/Dockerfile`; - workdir = `${workdir}/${baseDirectory}`; + dockerFileOut = `${workdir}${baseDirectory}`; + workdir = `${workdir}${baseDirectory}`; } - const Dockerfile: Array = (await fs.readFile(`${file}`, 'utf8')) .toString() .trim() @@ -41,8 +42,8 @@ export default async function ({ } }); } - await fs.writeFile(`${file}`, Dockerfile.join('\n')); - await buildImage({ applicationId, tag, workdir, docker, buildId, debug }); + await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n')); + await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation }); } catch (error) { throw error; } diff --git a/src/lib/database/applications.ts b/src/lib/database/applications.ts index 3ce6286d7..020b9ba75 100644 --- a/src/lib/database/applications.ts +++ b/src/lib/database/applications.ts @@ -263,7 +263,8 @@ export async function configureApplication({ publishDirectory, pythonWSGI, pythonModule, - pythonVariable + pythonVariable, + dockerFileLocation }: { id: string; buildPack: string; @@ -278,6 +279,7 @@ export async function configureApplication({ pythonWSGI: string; pythonModule: string; pythonVariable: string; + dockerFileLocation: string; }): Promise { return await prisma.application.update({ where: { id }, @@ -293,7 +295,8 @@ export async function configureApplication({ publishDirectory, pythonWSGI, pythonModule, - pythonVariable + pythonVariable, + dockerFileLocation } }); } diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index 7beaaf76a..314cf4548 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -27,25 +27,35 @@ export async function newService({ export async function getService({ id, teamId }: { id: string; teamId: string }): Promise { let body; - const include = { - destinationDocker: true, - plausibleAnalytics: true, - minio: true, - vscodeserver: true, - wordpress: true, - ghost: true, - serviceSecret: true, - meiliSearch: true - }; if (teamId === '0') { body = await prisma.service.findFirst({ where: { id }, - include + include: { + destinationDocker: true, + plausibleAnalytics: true, + minio: true, + vscodeserver: true, + wordpress: true, + ghost: true, + serviceSecret: true, + meiliSearch: true, + persistentStorage: true + } }); } else { body = await prisma.service.findFirst({ where: { id, teams: { some: { id: teamId } } }, - include + include: { + destinationDocker: true, + plausibleAnalytics: true, + minio: true, + vscodeserver: true, + wordpress: true, + ghost: true, + serviceSecret: true, + meiliSearch: true, + persistentStorage: true + } }); } @@ -362,6 +372,7 @@ export async function updateGhostService({ } export async function removeService({ id }: { id: string }): Promise { + await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); diff --git a/src/lib/docker.ts b/src/lib/docker.ts index 04f68ac48..c2642623f 100644 --- a/src/lib/docker.ts +++ b/src/lib/docker.ts @@ -85,7 +85,8 @@ export async function buildImage({ docker, buildId, isCache = false, - debug = false + debug = false, + dockerFileLocation = '/Dockerfile' }) { if (isCache) { await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); @@ -103,7 +104,7 @@ export async function buildImage({ const stream = await docker.engine.buildImage( { src: ['.'], context: workdir }, { - dockerfile: isCache ? 'Dockerfile-cache' : 'Dockerfile', + dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation, t: `${applicationId}:${tag}${isCache ? '-cache' : ''}` } ); diff --git a/src/lib/queues/builder.ts b/src/lib/queues/builder.ts index e76104376..076c5a511 100644 --- a/src/lib/queues/builder.ts +++ b/src/lib/queues/builder.ts @@ -56,7 +56,8 @@ export default async function (job: Job): Promise): Promise): Promise { publishDirectory, pythonWSGI, pythonModule, - pythonVariable + pythonVariable, + dockerFileLocation } = await event.request.json(); if (port) port = Number(port); @@ -68,7 +69,8 @@ export const post: RequestHandler = async (event) => { startCommand, buildCommand, publishDirectory, - baseDirectory + baseDirectory, + dockerFileLocation }); await db.configureApplication({ id, @@ -84,6 +86,7 @@ export const post: RequestHandler = async (event) => { pythonWSGI, pythonModule, pythonVariable, + dockerFileLocation, ...defaultConfiguration }); return { status: 201 }; diff --git a/src/routes/applications/[id]/index.svelte b/src/routes/applications/[id]/index.svelte index f4ca842e0..690d97f97 100644 --- a/src/routes/applications/[id]/index.svelte +++ b/src/routes/applications/[id]/index.svelte @@ -68,11 +68,6 @@ value: 'Gunicorn', label: 'Gunicorn' } - // }, - // { - // value: 'uWSGI', - // label: 'uWSGI' - // } ]; if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { @@ -420,6 +415,23 @@ /> {/if} + {#if application.buildPack === 'docker'} +
+ + + +
+ {/if} {#if application.buildPack === 'deno'}
diff --git a/src/routes/applications/[id]/logs/index.json.ts b/src/routes/applications/[id]/logs/index.json.ts index f3ab6b9ca..4fa0c3d86 100644 --- a/src/routes/applications/[id]/logs/index.json.ts +++ b/src/routes/applications/[id]/logs/index.json.ts @@ -27,6 +27,7 @@ export const get: RequestHandler = async (event) => { .split('\n') .map((l) => l.slice(8)) .filter((a) => a) + .reverse() } }; } diff --git a/src/routes/applications/[id]/logs/index.svelte b/src/routes/applications/[id]/logs/index.svelte index 30b6be9a0..a07db25e4 100644 --- a/src/routes/applications/[id]/logs/index.svelte +++ b/src/routes/applications/[id]/logs/index.svelte @@ -24,19 +24,21 @@ export let application; import { page } from '$app/stores'; import LoadingLogs from './_Loading.svelte'; - import { getDomain } from '$lib/components/common'; import { get } from '$lib/api'; import { errorNotification } from '$lib/form'; let loadLogsInterval = null; + let allLogs = []; let logs = []; - let followingBuild; + let currentPage = 1; + let endOfLogs = false; + let startOfLogs = true; let followingInterval; let logsEl; const { id } = $page.params; onMount(async () => { - loadLogs(); + loadAllLogs(); loadLogsInterval = setInterval(() => { loadLogs(); }, 1000); @@ -45,25 +47,52 @@ clearInterval(loadLogsInterval); clearInterval(followingInterval); }); - async function loadLogs() { + async function loadAllLogs() { try { - const newLogs = await get(`/applications/${id}/logs.json`); - logs = newLogs.logs; + const data = await get(`/applications/${id}/logs.json`); + allLogs = data.logs; + logs = data.logs.slice(0, 100); return; } catch ({ error }) { return errorNotification(error); } } - - function followBuild() { - followingBuild = !followingBuild; - if (followingBuild) { - followingInterval = setInterval(() => { - logsEl.scrollTop = logsEl.scrollHeight; - window.scrollTo(0, document.body.scrollHeight); - }, 100); + async function loadLogs() { + try { + const newLogs = await get(`/applications/${id}/logs.json`); + logs = newLogs.logs.slice(0, 100); + return; + } catch ({ error }) { + return errorNotification(error); + } + } + async function loadOlderLogs() { + clearInterval(loadLogsInterval); + loadLogsInterval = null; + logsEl.scrollTop = 0; + if (logs.length < 100) { + endOfLogs = true; + return; + } + startOfLogs = false; + endOfLogs = false; + currentPage += 1; + logs = allLogs.slice(currentPage * 100 - 100, currentPage * 100); + } + async function loadNewerLogs() { + currentPage -= 1; + logsEl.scrollTop = 0; + if (currentPage !== 1) { + clearInterval(loadLogsInterval); + endOfLogs = false; + loadLogsInterval = null; + logs = allLogs.slice(currentPage * 100 - 100, currentPage * 100); } else { - window.clearInterval(followingInterval); + startOfLogs = true; + loadLogs(); + loadLogsInterval = setInterval(() => { + loadLogs(); + }, 1000); } } @@ -145,13 +174,17 @@
Waiting for the logs...
{:else}
- -
+
+ {#if loadLogsInterval} + + {/if} +
+
@@ -175,7 +230,7 @@ class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200" bind:this={logsEl} > -
+
{#each logs as log} {log + '\n'} {/each} diff --git a/src/routes/login/index.svelte b/src/routes/login/index.svelte index f37d8aa45..aa716b0a3 100644 --- a/src/routes/login/index.svelte +++ b/src/routes/login/index.svelte @@ -43,8 +43,15 @@ {:else}
-
Coolify
-
v{$session.version}
+ {#if $session.whiteLabelDetails.icon} + Icon for white labeled version of Coolify + {:else} +
Coolify
+ {/if} -
Coolify
-
v{$session.version}
+ {#if $session.whiteLabelDetails.icon} + Icon for white labeled version of Coolify + {:else} +
Coolify
+ {/if} + +
{/if} +
+ {:else} +
+
+ +
+
+ +
+
+ {/if} + diff --git a/src/routes/services/[id]/storage/index.json.ts b/src/routes/services/[id]/storage/index.json.ts new file mode 100644 index 000000000..a2a240fe4 --- /dev/null +++ b/src/routes/services/[id]/storage/index.json.ts @@ -0,0 +1,65 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { status, body, teamId } = await getUserDetails(event, false); + if (status === 401) return { status, body }; + + const { id } = event.params; + try { + const persistentStorages = await db.prisma.servicePersistentStorage.findMany({ + where: { serviceId: id } + }); + return { + body: { + persistentStorages + } + }; + } catch (error) { + return ErrorHandler(error); + } +}; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + const { path, newStorage, storageId } = await event.request.json(); + try { + if (newStorage) { + await db.prisma.servicePersistentStorage.create({ + data: { path, service: { connect: { id } } } + }); + } else { + await db.prisma.servicePersistentStorage.update({ + where: { id: storageId }, + data: { path } + }); + } + return { + status: 201 + }; + } catch (error) { + return ErrorHandler(error); + } +}; + +export const del: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + const { path } = await event.request.json(); + + try { + await db.prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id, path } }); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/storage/index.svelte b/src/routes/services/[id]/storage/index.svelte new file mode 100644 index 000000000..3d5c43c1b --- /dev/null +++ b/src/routes/services/[id]/storage/index.svelte @@ -0,0 +1,102 @@ + + + + +
+
+
+ Persistent Storage +
+ {service.name} +
+ {#if service.fqdn} + + + + + + + {/if} + + +
+ +
+
+ This is useful for storing data for VSCode server or WordPress.'} + /> +
+ + + + + + + + {#each persistentStorages as storage} + {#key storage.id} + + + + {/key} + {/each} + + + + +
Path
+
diff --git a/src/routes/services/[id]/uptimekuma/start.json.ts b/src/routes/services/[id]/uptimekuma/start.json.ts index 48af20f9e..6327db0ec 100644 --- a/src/routes/services/[id]/uptimekuma/start.json.ts +++ b/src/routes/services/[id]/uptimekuma/start.json.ts @@ -68,11 +68,7 @@ export const post: RequestHandler = async (event) => { await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); try { - if (version === 'latest') { - await asyncExecShell( - `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` - ); - } + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); return { status: 200 diff --git a/src/routes/services/[id]/vaultwarden/start.json.ts b/src/routes/services/[id]/vaultwarden/start.json.ts index a95973525..511b040a3 100644 --- a/src/routes/services/[id]/vaultwarden/start.json.ts +++ b/src/routes/services/[id]/vaultwarden/start.json.ts @@ -68,11 +68,7 @@ export const post: RequestHandler = async (event) => { const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); try { - if (version === 'latest') { - await asyncExecShell( - `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` - ); - } + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); return { status: 200 diff --git a/src/routes/services/[id]/vscodeserver/start.json.ts b/src/routes/services/[id]/vscodeserver/start.json.ts index d839ce5ff..e928de7bb 100644 --- a/src/routes/services/[id]/vscodeserver/start.json.ts +++ b/src/routes/services/[id]/vscodeserver/start.json.ts @@ -21,6 +21,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + persistentStorage, vscodeserver: { password } } = service; @@ -42,6 +43,28 @@ export const post: RequestHandler = async (event) => { config.environmentVariables[secret.name] = secret.value; }); } + + const volumes = + persistentStorage?.map((storage) => { + return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`; + }) || []; + + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const volumeMounts = Object.assign( + {}, + { + [config.volume.split(':')[0]]: { + name: config.volume.split(':')[0] + } + }, + ...composeVolumes + ); const composeFile: ComposeFile = { version: '3.8', services: { @@ -50,7 +73,7 @@ export const post: RequestHandler = async (event) => { image: config.image, environment: config.environmentVariables, networks: [network], - volumes: [config.volume], + volumes: [config.volume, ...volumes], restart: 'always', labels: makeLabelForServices('vscodeServer'), deploy: { @@ -68,19 +91,21 @@ export const post: RequestHandler = async (event) => { external: true } }, - volumes: { - [config.volume.split(':')[0]]: { - name: config.volume.split(':')[0] - } - } + volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - if (version === 'latest') { - await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); - } + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + + const changePermissionOn = persistentStorage.map((p) => p.path); + + await asyncExecShell( + `DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join( + ' ' + )}` + ); return { status: 200 }; diff --git a/src/routes/services/[id]/wordpress/start.json.ts b/src/routes/services/[id]/wordpress/start.json.ts index 40209819f..0572be971 100644 --- a/src/routes/services/[id]/wordpress/start.json.ts +++ b/src/routes/services/[id]/wordpress/start.json.ts @@ -121,11 +121,7 @@ export const post: RequestHandler = async (event) => { const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); try { - if (version === 'latest') { - await asyncExecShell( - `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` - ); - } + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); return { status: 200