Merge remote-tracking branch 'upstream/next' into buildpack-deno

# Conflicts:
#	src/routes/applications/[id]/index.svelte
This commit is contained in:
lichtscheu 2022-04-19 23:17:03 +02:00
commit 2bc2ae9b6e
No known key found for this signature in database
GPG Key ID: 26BE8D73E895F37A
33 changed files with 534 additions and 125 deletions

View File

@ -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");

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "dockerFileLocation" TEXT;

View File

@ -1,6 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["linux-musl"] binaryTargets = ["native", "linux-musl"]
} }
datasource db { datasource db {
@ -91,6 +91,7 @@ model Application {
pythonWSGI String? pythonWSGI String?
pythonModule String? pythonModule String?
pythonVariable String? pythonVariable String?
dockerFileLocation String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
settings ApplicationSettings? settings ApplicationSettings?
@ -118,14 +119,25 @@ model ApplicationSettings {
model ApplicationPersistentStorage { model ApplicationPersistentStorage {
id String @id @default(cuid()) id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
applicationId String applicationId String
path String path String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@unique([applicationId, path]) @@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 { model Secret {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
@ -267,17 +279,17 @@ model DatabaseSettings {
} }
model Service { model Service {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? fqdn String?
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
type String? type String?
version String? version String?
teams Team[] teams Team[]
destinationDockerId String? destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
plausibleAnalytics PlausibleAnalytics? plausibleAnalytics PlausibleAnalytics?
minio Minio? minio Minio?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
@ -285,6 +297,7 @@ model Service {
ghost Ghost? ghost Ghost?
serviceSecret ServiceSecret[] serviceSecret ServiceSecret[]
meiliSearch MeiliSearch? meiliSearch MeiliSearch?
persistentStorage ServicePersistentStorage[]
} }
model PlausibleAnalytics { model PlausibleAnalytics {

6
src/app.d.ts vendored
View File

@ -6,7 +6,11 @@ declare namespace App {
cookies: Record<string, string>; cookies: Record<string, string>;
} }
interface Platform {} interface Platform {}
interface Session extends SessionData {} interface Session extends SessionData {
whiteLabelDetails: {
icon: string | null;
};
}
interface Stuff { interface Stuff {
service: any; service: any;
application: any; application: any;

View File

@ -8,6 +8,9 @@ import cookie from 'cookie';
import { dev } from '$app/env'; import { dev } from '$app/env';
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true'; const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
const whiteLabelDetails = {
icon: (whiteLabeled && process.env['COOLIFY_WHITE_LABELED_ICON']) || null
};
export const handle = handleSession( export const handle = handleSession(
{ {
@ -74,6 +77,7 @@ export const getSession: GetSession = function ({ locals }) {
return { return {
version, version,
whiteLabeled, whiteLabeled,
whiteLabelDetails,
...locals.session.data ...locals.session.data
}; };
}; };

View File

@ -91,7 +91,8 @@ export const setDefaultConfiguration = async (data) => {
startCommand, startCommand,
buildCommand, buildCommand,
publishDirectory, publishDirectory,
baseDirectory baseDirectory,
dockerFileLocation
} = data; } = data;
const template = scanningTemplates[buildPack]; const template = scanningTemplates[buildPack];
if (!port) { if (!port) {
@ -110,6 +111,12 @@ export const setDefaultConfiguration = async (data) => {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) 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 { return {
buildPack, buildPack,
@ -118,7 +125,8 @@ export const setDefaultConfiguration = async (data) => {
startCommand, startCommand,
buildCommand, buildCommand,
publishDirectory, publishDirectory,
baseDirectory baseDirectory,
dockerFileLocation
}; };
}; };

View File

@ -10,15 +10,16 @@ export default async function ({
buildId, buildId,
baseDirectory, baseDirectory,
secrets, secrets,
pullmergeRequestId pullmergeRequestId,
dockerFileLocation
}) { }) {
try { try {
let file = `${workdir}/Dockerfile`; const file = `${workdir}${dockerFileLocation}`;
let dockerFileOut = `${workdir}`;
if (baseDirectory) { if (baseDirectory) {
file = `${workdir}/${baseDirectory}/Dockerfile`; dockerFileOut = `${workdir}${baseDirectory}`;
workdir = `${workdir}/${baseDirectory}`; workdir = `${workdir}${baseDirectory}`;
} }
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8')) const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))
.toString() .toString()
.trim() .trim()
@ -41,8 +42,8 @@ export default async function ({
} }
}); });
} }
await fs.writeFile(`${file}`, Dockerfile.join('\n')); await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug }); await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -263,7 +263,8 @@ export async function configureApplication({
publishDirectory, publishDirectory,
pythonWSGI, pythonWSGI,
pythonModule, pythonModule,
pythonVariable pythonVariable,
dockerFileLocation
}: { }: {
id: string; id: string;
buildPack: string; buildPack: string;
@ -278,6 +279,7 @@ export async function configureApplication({
pythonWSGI: string; pythonWSGI: string;
pythonModule: string; pythonModule: string;
pythonVariable: string; pythonVariable: string;
dockerFileLocation: string;
}): Promise<Application> { }): Promise<Application> {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
@ -293,7 +295,8 @@ export async function configureApplication({
publishDirectory, publishDirectory,
pythonWSGI, pythonWSGI,
pythonModule, pythonModule,
pythonVariable pythonVariable,
dockerFileLocation
} }
}); });
} }

View File

@ -27,25 +27,35 @@ export async function newService({
export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> { export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> {
let body; let body;
const include = {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true
};
if (teamId === '0') { if (teamId === '0') {
body = await prisma.service.findFirst({ body = await prisma.service.findFirst({
where: { id }, where: { id },
include include: {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true,
persistentStorage: true
}
}); });
} else { } else {
body = await prisma.service.findFirst({ body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId } } }, 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<void> { export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });

View File

@ -85,7 +85,8 @@ export async function buildImage({
docker, docker,
buildId, buildId,
isCache = false, isCache = false,
debug = false debug = false,
dockerFileLocation = '/Dockerfile'
}) { }) {
if (isCache) { if (isCache) {
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
@ -103,7 +104,7 @@ export async function buildImage({
const stream = await docker.engine.buildImage( const stream = await docker.engine.buildImage(
{ src: ['.'], context: workdir }, { src: ['.'], context: workdir },
{ {
dockerfile: isCache ? 'Dockerfile-cache' : 'Dockerfile', dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
t: `${applicationId}:${tag}${isCache ? '-cache' : ''}` t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
} }
); );

View File

@ -56,7 +56,8 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
dockerFileLocation
} = job.data; } = job.data;
const { debug } = settings; const { debug } = settings;
@ -107,6 +108,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
buildCommand = configuration.buildCommand; buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory; publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory; baseDirectory = configuration.baseDirectory;
dockerFileLocation = configuration.dockerFileLocation;
const commit = await importers[gitSource.type]({ const commit = await importers[gitSource.type]({
applicationId, applicationId,
@ -209,7 +211,8 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
phpModules, phpModules,
pythonWSGI, pythonWSGI,
pythonModule, pythonModule,
pythonVariable pythonVariable,
dockerFileLocation
}); });
else { else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });

View File

@ -21,6 +21,7 @@ export type BuilderJob = {
pythonWSGI: string; pythonWSGI: string;
pythonModule: string; pythonModule: string;
pythonVariable: string; pythonVariable: string;
dockerFileLocation: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
destinationDockerId: string; destinationDockerId: string;

View File

@ -56,7 +56,8 @@ export const post: RequestHandler = async (event) => {
publishDirectory, publishDirectory,
pythonWSGI, pythonWSGI,
pythonModule, pythonModule,
pythonVariable pythonVariable,
dockerFileLocation
} = await event.request.json(); } = await event.request.json();
if (port) port = Number(port); if (port) port = Number(port);
@ -68,7 +69,8 @@ export const post: RequestHandler = async (event) => {
startCommand, startCommand,
buildCommand, buildCommand,
publishDirectory, publishDirectory,
baseDirectory baseDirectory,
dockerFileLocation
}); });
await db.configureApplication({ await db.configureApplication({
id, id,
@ -84,6 +86,7 @@ export const post: RequestHandler = async (event) => {
pythonWSGI, pythonWSGI,
pythonModule, pythonModule,
pythonVariable, pythonVariable,
dockerFileLocation,
...defaultConfiguration ...defaultConfiguration
}); });
return { status: 201 }; return { status: 201 };

View File

@ -68,11 +68,6 @@
value: 'Gunicorn', value: 'Gunicorn',
label: 'Gunicorn' label: 'Gunicorn'
} }
// },
// {
// value: 'uWSGI',
// label: 'uWSGI'
// }
]; ];
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
@ -420,6 +415,23 @@
/> />
</div> </div>
{/if} {/if}
{#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center">
<label for="dockerFileLocation" class="text-base font-bold text-stone-100"
>Dockerfile Location</label
>
<input
readonly={!$session.isAdmin}
name="dockerFileLocation"
id="dockerFileLocation"
bind:value={application.dockerFileLocation}
placeholder="default: /Dockerfile"
/>
<Explainer
text="Does not rely on Base Directory. <br>Should be absolute path, like <span class='text-green-500 font-bold'>/data/Dockerfile</span> or <span class='text-green-500 font-bold'>/Dockerfile.</span>"
/>
</div>
{/if}
{#if application.buildPack === 'deno'} {#if application.buildPack === 'deno'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="startCommand" class="text-base font-bold text-stone-100">Start Command</label> <label for="startCommand" class="text-base font-bold text-stone-100">Start Command</label>

View File

@ -27,6 +27,7 @@ export const get: RequestHandler = async (event) => {
.split('\n') .split('\n')
.map((l) => l.slice(8)) .map((l) => l.slice(8))
.filter((a) => a) .filter((a) => a)
.reverse()
} }
}; };
} }

View File

@ -24,19 +24,21 @@
export let application; export let application;
import { page } from '$app/stores'; import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte'; import LoadingLogs from './_Loading.svelte';
import { getDomain } from '$lib/components/common';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
let loadLogsInterval = null; let loadLogsInterval = null;
let allLogs = [];
let logs = []; let logs = [];
let followingBuild; let currentPage = 1;
let endOfLogs = false;
let startOfLogs = true;
let followingInterval; let followingInterval;
let logsEl; let logsEl;
const { id } = $page.params; const { id } = $page.params;
onMount(async () => { onMount(async () => {
loadLogs(); loadAllLogs();
loadLogsInterval = setInterval(() => { loadLogsInterval = setInterval(() => {
loadLogs(); loadLogs();
}, 1000); }, 1000);
@ -45,25 +47,52 @@
clearInterval(loadLogsInterval); clearInterval(loadLogsInterval);
clearInterval(followingInterval); clearInterval(followingInterval);
}); });
async function loadLogs() { async function loadAllLogs() {
try { try {
const newLogs = await get(`/applications/${id}/logs.json`); const data = await get(`/applications/${id}/logs.json`);
logs = newLogs.logs; allLogs = data.logs;
logs = data.logs.slice(0, 100);
return; return;
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
} }
async function loadLogs() {
function followBuild() { try {
followingBuild = !followingBuild; const newLogs = await get(`/applications/${id}/logs.json`);
if (followingBuild) { logs = newLogs.logs.slice(0, 100);
followingInterval = setInterval(() => { return;
logsEl.scrollTop = logsEl.scrollHeight; } catch ({ error }) {
window.scrollTo(0, document.body.scrollHeight); return errorNotification(error);
}, 100); }
}
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 { } else {
window.clearInterval(followingInterval); startOfLogs = true;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
} }
} }
</script> </script>
@ -145,13 +174,17 @@
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div> <div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full">
<LoadingLogs /> <div class="text-right " />
<div class="flex justify-end sticky top-0 p-2"> {#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button <button
on:click={followBuild} on:click={loadOlderLogs}
class:text-coolgray-100={endOfLogs}
class:hover:bg-coolgray-400={!endOfLogs}
class="bg-transparent" class="bg-transparent"
data-tooltip="Follow logs" disabled={endOfLogs}
class:text-green-500={followingBuild}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -164,10 +197,32 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" /> <path
<line x1="8" y1="12" x2="12" y2="16" /> d="M20 15h-8v3.586a1 1 0 0 1 -1.707 .707l-6.586 -6.586a1 1 0 0 1 0 -1.414l6.586 -6.586a1 1 0 0 1 1.707 .707v3.586h8a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1z"
<line x1="12" y1="8" x2="12" y2="16" /> />
<line x1="16" y1="12" x2="12" y2="16" /> </svg>
</button>
<button
on:click={loadNewerLogs}
class:text-coolgray-100={startOfLogs}
class:hover:bg-coolgray-400={!startOfLogs}
class="bg-transparent"
disabled={startOfLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M4 9h8v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-8a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1z"
/>
</svg> </svg>
</button> </button>
</div> </div>
@ -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" 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} bind:this={logsEl}
> >
<div class="px-2"> <div class="px-2 pr-14">
{#each logs as log} {#each logs as log}
{log + '\n'} {log + '\n'}
{/each} {/each}

View File

@ -43,8 +43,15 @@
{:else} {:else}
<div class="flex justify-center px-4"> <div class="flex justify-center px-4">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2"> <form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4">Coolify</div> {#if $session.whiteLabelDetails.icon}
<div class="text-xs text-center font-bold pb-10">v{$session.version}</div> <img
class="w-32 mx-auto pb-8"
src={$session.whiteLabelDetails.icon}
alt="Icon for white labeled version of Coolify"
/>
{:else}
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4 mb-8">Coolify</div>
{/if}
<input <input
type="email" type="email"
name="email" name="email"

View File

@ -64,8 +64,15 @@
{:else} {:else}
<div class="flex justify-center px-4"> <div class="flex justify-center px-4">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2"> <form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4">Coolify</div> {#if $session.whiteLabelDetails.icon}
<div class="text-xs text-center font-bold pb-10">v{$session.version}</div> <img
class="w-32 mx-auto pb-8"
src={$session.whiteLabelDetails.icon}
alt="Icon for white labeled version of Coolify"
/>
{:else}
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4 mb-8">Coolify</div>
{/if}
<input <input
type="email" type="email"
name="email" name="email"

View File

@ -239,6 +239,35 @@
</svg></button </svg></button
></a ></a
> >
<a
href="/services/{id}/storage"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/storage`}
>
<button
title="Persistent Storage"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Persistent Storage"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
{/if} {/if}
<button <button

View File

@ -135,11 +135,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { 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`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@ -69,11 +69,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { 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`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@ -74,11 +74,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { 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`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@ -88,6 +88,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
await db.updateMinioService({ id, publicPort }); await db.updateMinioService({ id, publicPort });
await startHttpProxy(destinationDocker, id, publicPort, apiPort); await startHttpProxy(destinationDocker, id, publicPort, apiPort);

View File

@ -70,11 +70,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { 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`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@ -61,11 +61,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { 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`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@ -192,9 +192,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); 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( await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d` `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
); );

View File

@ -0,0 +1,73 @@
<script lang="ts">
export let isNew = false;
export let storage = {
id: null,
path: null
};
import { del, post } from '$lib/api';
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
try {
if (!storage.path) return errorNotification('Path is required.');
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/');
await post(`/services/${id}/storage.json`, {
path: storage.path,
storageId: storage.id,
newStorage
});
dispatch('refresh');
if (isNew) {
storage.path = null;
storage.id = null;
}
if (newStorage) toast.push('Storage saved.');
else toast.push('Storage updated.');
} catch ({ error }) {
return errorNotification(error);
}
}
async function removeStorage() {
try {
await del(`/services/${id}/storage.json`, { path: storage.path });
dispatch('refresh');
toast.push('Storage deleted.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<td>
<input
bind:value={storage.path}
required
placeholder="eg: /data"
class=" border border-dashed border-coolgray-300"
/>
</td>
<td>
{#if isNew}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => saveStorage(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
</div>
</div>
{/if}
</td>

View File

@ -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);
}
};

View File

@ -0,0 +1,102 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
let endpoint = `/services/${params.id}/storage.json`;
const res = await fetch(endpoint);
if (res.ok) {
return {
props: {
service: stuff.service,
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
};
};
</script>
<script lang="ts">
export let service;
export let persistentStorages;
import { page } from '$app/stores';
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import ServiceLinks from '$lib/components/ServiceLinks.svelte';
const { id } = $page.params;
async function refreshStorage() {
const data = await get(`/services/${id}/storage.json`);
persistentStorages = [...data.persistentStorages];
}
</script>
<div
class="flex items-center space-x-2 p-5 px-6 font-bold"
class:p-5={service.fqdn}
class:p-6={!service.fqdn}
>
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Persistent Storage
</div>
<span class="text-xs">{service.name}</span>
</div>
{#if service.fqdn}
<a
href={service.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<ServiceLinks {service} />
</div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={'You can specify any folder that you want to be persistent across restarts. <br>This is useful for storing data for VSCode server or WordPress.'}
/>
</div>
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Path</th>
</tr>
</thead>
<tbody>
{#each persistentStorages as storage}
{#key storage.id}
<tr>
<Storage on:refresh={refreshStorage} {storage} />
</tr>
{/key}
{/each}
<tr>
<Storage on:refresh={refreshStorage} isNew />
</tr>
</tbody>
</table>
</div>

View File

@ -68,11 +68,7 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { 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`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@ -68,11 +68,7 @@ export const post: RequestHandler = async (event) => {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { 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`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200

View File

@ -21,6 +21,7 @@ export const post: RequestHandler = async (event) => {
destinationDockerId, destinationDockerId,
destinationDocker, destinationDocker,
serviceSecret, serviceSecret,
persistentStorage,
vscodeserver: { password } vscodeserver: { password }
} = service; } = service;
@ -42,6 +43,28 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; 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 = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@ -50,7 +73,7 @@ export const post: RequestHandler = async (event) => {
image: config.image, image: config.image,
environment: config.environmentVariables, environment: config.environmentVariables,
networks: [network], networks: [network],
volumes: [config.volume], volumes: [config.volume, ...volumes],
restart: 'always', restart: 'always',
labels: makeLabelForServices('vscodeServer'), labels: makeLabelForServices('vscodeServer'),
deploy: { deploy: {
@ -68,19 +91,21 @@ export const post: RequestHandler = async (event) => {
external: true external: true
} }
}, },
volumes: { volumes: volumeMounts
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
}
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); 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`); 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 { return {
status: 200 status: 200
}; };

View File

@ -121,11 +121,7 @@ export const post: RequestHandler = async (event) => {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { 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`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return { return {
status: 200 status: 200