Merge pull request #357 from coollabsio/next

v2.4.11
This commit is contained in:
Andras Bacsai 2022-04-20 14:19:49 +02:00 committed by GitHub
commit 397ca7f20e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 661 additions and 148 deletions

View File

@ -0,0 +1,6 @@
FROM fluent/fluent-bit:1.9.0
COPY fluentbit-dev.conf /tmp/fluentbit.conf
ENTRYPOINT ["/fluent-bit/bin/fluent-bit", "-c", "/tmp/fluentbit.conf"]
# USER root
# RUN ["gem", "install", "fluent-plugin-mongo"]
# USER fluent

View File

@ -0,0 +1,24 @@
[INPUT]
Name forward
Listen 0.0.0.0
Port 24224
Buffer_Chunk_Size 32KB
Buffer_Max_Size 64KB
[OUTPUT]
Name influxdb
Match *
Host coolify-influxdb
Port 8086
Bucket containerlogs
Org organization
HTTP_Token supertoken
Sequence_Tag _seq
Tag_Keys container_name
[OUTPUT]
Name http
Match *
Host host.docker.internal
Port 3000
URI /logs.json
Format json

View File

@ -0,0 +1,28 @@
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
<match **>
@type http
endpoint http://host.docker.internal:3000/logs.json
<buffer>
flush_at_shutdown true
flush_mode immediate
flush_thread_count 8
flush_thread_interval 1
flush_thread_burst_interval 1
retry_forever true
retry_type exponential_backoff
</buffer>
</match>
<filter docker.**>
@type parser
key_name log
reserve_data true
<parse>
@type json
</parse>
</filter>

View File

@ -11,7 +11,24 @@ services:
published: 6379
protocol: tcp
mode: host
# fluentbit:
# container_name: coolify-fluentbit
# build:
# context: ./data/fluentd
# dockerfile: Dockerfile-dev
# ports:
# - target: 24224
# published: 24224
# protocol: tcp
# mode: host
# - target: 24224
# published: 24224
# protocol: udp
# mode: host
# networks:
# - coolify-infra
# extra_hosts:
# - 'host.docker.internal:host-gateway'
networks:
coolify-infra:
attachable: true

View File

@ -1,10 +1,10 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.4.10",
"version": "2.4.11",
"license": "AGPL-3.0",
"scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0",
"dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio",
@ -63,7 +63,7 @@
"@iarna/toml": "2.2.5",
"@prisma/client": "3.11.1",
"@sentry/node": "6.19.6",
"bcryptjs": "^2.4.3",
"bcryptjs": "2.4.3",
"bullmq": "1.80.0",
"compare-versions": "4.1.3",
"cookie": "0.4.2",

18
pnpm-lock.yaml generated
View File

@ -5,7 +5,7 @@ specifiers:
'@prisma/client': 3.11.1
'@sentry/node': 6.19.6
'@sveltejs/adapter-node': 1.0.0-next.73
'@sveltejs/kit': 1.0.0-next.303
'@sveltejs/kit': 1.0.0-next.310
'@types/js-cookie': 3.0.1
'@types/js-yaml': 4.0.5
'@types/node': 17.0.23
@ -14,8 +14,8 @@ specifiers:
'@typescript-eslint/parser': 4.31.1
'@zerodevx/svelte-toast': 0.7.1
autoprefixer: 10.4.4
bcryptjs: ^2.4.3
bullmq: 1.78.1
bcryptjs: 2.4.3
bullmq: 1.80.0
compare-versions: 4.1.3
cookie: 0.4.2
cross-env: 7.0.3
@ -60,7 +60,7 @@ dependencies:
'@prisma/client': 3.11.1_prisma@3.11.1
'@sentry/node': 6.19.6
bcryptjs: 2.4.3
bullmq: 1.78.1
bullmq: 1.80.0
compare-versions: 4.1.3
cookie: 0.4.2
cuid: 2.1.8
@ -82,7 +82,7 @@ dependencies:
devDependencies:
'@sveltejs/adapter-node': 1.0.0-next.73
'@sveltejs/kit': 1.0.0-next.303_svelte@3.47.0
'@sveltejs/kit': 1.0.0-next.310_svelte@3.47.0
'@types/js-cookie': 3.0.1
'@types/js-yaml': 4.0.5
'@types/node': 17.0.23
@ -374,10 +374,10 @@ packages:
tiny-glob: 0.2.9
dev: true
/@sveltejs/kit/1.0.0-next.303_svelte@3.47.0:
/@sveltejs/kit/1.0.0-next.310_svelte@3.47.0:
resolution:
{
integrity: sha512-WdxDc8OiF1WEd/bEza7CBdzA+3qIcCi1GKBj/gieKX9I3N8iDJt/Cg2POrLo9wQoJ47nZcAd1eOhfr7XEX1aIQ==
integrity: sha512-pTyMyaoyHS+V5cQZIQMfQXmLkhw1VaRwT9avOSgwDc0QBpnNw2LdzwoPYsUr96ca5B6cfT3SMUNolxErTNHmPQ==
}
engines: { node: '>=14.13' }
hasBin: true
@ -1669,10 +1669,10 @@ packages:
ieee754: 1.2.1
dev: false
/bullmq/1.78.1:
/bullmq/1.80.0:
resolution:
{
integrity: sha512-er45mM8nGhgA83EVCJ4PNxPyDSzakvoxeFGU4vdSgYeB+SbeFQAlJYmAC50Ms7YFPstm1LeinbVZ+oX/BmBzOg==
integrity: sha512-oz7GZIg7gAGIIlLQ3KdpYSA5WSz5205pQHyGwOtQof9MmkOf+Kmo6sxqr+BiQrjhFOrB6JLSCqS3EGEbMA34MA==
}
dependencies:
cron-parser: 4.2.1

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 {
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 {

6
src/app.d.ts vendored
View File

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

View File

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

View File

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

View File

@ -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<string> = (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;
}

View File

@ -61,14 +61,12 @@ export const saveBuildLog = async ({
buildId: string;
applicationId: string;
}): Promise<Job> => {
if (line) {
if (line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
if (line && typeof line === 'string' && line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
};
export const getTeam = (event: RequestEvent): string | null => {

View File

@ -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<Application> {
return await prisma.application.update({
where: { id },
@ -293,7 +295,8 @@ export async function configureApplication({
publishDirectory,
pythonWSGI,
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> {
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<void> {
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 } });

View File

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

View File

@ -10,6 +10,9 @@ import { promises as dns } from 'dns';
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try {
const certbotImage =
process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest';
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
@ -63,7 +66,7 @@ export async function letsEncrypt(domain: string, id?: string, isCoolify = false
if (found) return;
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
@ -83,7 +86,7 @@ export async function letsEncrypt(domain: string, id?: string, isCoolify = false
}
if (found) return;
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);

View File

@ -56,7 +56,8 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
buildCommand,
startCommand,
baseDirectory,
publishDirectory
publishDirectory,
dockerFileLocation
} = job.data;
const { debug } = settings;
@ -107,6 +108,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
dockerFileLocation = configuration.dockerFileLocation;
const commit = await importers[gitSource.type]({
applicationId,
@ -209,7 +211,8 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
phpModules,
pythonWSGI,
pythonModule,
pythonVariable
pythonVariable,
dockerFileLocation
});
else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@ -286,6 +289,9 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
labels,
depends_on: [],
restart: 'always',
// logging: {
// driver: 'fluentd',
// },
deploy: {
restart_policy: {
condition: 'on-failure',

View File

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

View File

@ -56,7 +56,8 @@ export const post: RequestHandler = async (event) => {
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 };

View File

@ -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 @@
/>
</div>
{/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}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"

View File

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

View File

@ -24,19 +24,23 @@
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 = {
logs: []
};
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 +49,55 @@
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadLogs() {
async function loadAllLogs() {
try {
const newLogs = await get(`/applications/${id}/logs.json`);
logs = newLogs.logs;
const data: any = await get(`/applications/${id}/logs.json`);
allLogs = data.logs;
logs = data.logs.slice(0, 100);
if (logs.length < 100) {
endOfLogs = true;
}
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);
}
}
</script>
@ -145,13 +179,18 @@
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
{:else}
<div class="relative w-full">
<LoadingLogs />
<div class="flex justify-end sticky top-0 p-2">
<div class="text-right " />
{#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button
on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs"
class:text-green-500={followingBuild}
on:click={loadOlderLogs}
class:text-coolgray-100={endOfLogs}
class:hover:bg-coolgray-400={!endOfLogs}
class="bg-transparent tooltip-bottom"
data-tooltip="Older logs"
disabled={endOfLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -164,10 +203,33 @@
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
<path
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"
/>
</svg>
</button>
<button
on:click={loadNewerLogs}
class:text-coolgray-100={startOfLogs}
class:hover:bg-coolgray-400={!startOfLogs}
class="bg-transparent tooltip-bottom"
data-tooltip="Newer logs"
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>
</button>
</div>
@ -175,7 +237,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}
>
<div class="px-2">
<div class="px-2 pr-14">
{#each logs as log}
{log + '\n'}
{/each}

View File

@ -43,8 +43,15 @@
{:else}
<div class="flex justify-center px-4">
<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>
<div class="text-xs text-center font-bold pb-10">v{$session.version}</div>
{#if $session.whiteLabelDetails.icon}
<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
type="email"
name="email"

18
src/routes/logs.json.ts Normal file
View File

@ -0,0 +1,18 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
export const post: RequestHandler = async (event) => {
const data = await event.request.json();
for (const d of data) {
if (d.container_name) {
const { log, container_name: containerId, source } = d;
console.log(log);
// await db.prisma.applicationLogs.create({ data: { log, containerId: containerId.substr(1), source } });
}
}
return {
status: 200,
body: {}
};
};

View File

@ -64,8 +64,15 @@
{:else}
<div class="flex justify-center px-4">
<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>
<div class="text-xs text-center font-bold pb-10">v{$session.version}</div>
{#if $session.whiteLabelDetails.icon}
<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
type="email"
name="email"

View File

@ -239,6 +239,35 @@
</svg></button
></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" />
{/if}
<button

View File

@ -135,11 +135,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

View File

@ -69,11 +69,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

View File

@ -74,11 +74,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

View File

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

View File

@ -61,11 +61,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

View File

@ -192,9 +192,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
};
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 --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));
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

View File

@ -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

View File

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

View File

@ -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