Compare commits

...

39 Commits

Author SHA1 Message Date
Andras Bacsai
397ca7f20e Merge pull request #357 from coollabsio/next
v2.4.11
2022-04-20 14:19:49 +02:00
Andras Bacsai
e10b76a46b feat: Fluentbit investigation 2022-04-20 13:33:04 +02:00
Andras Bacsai
b46566280d fix: Application logs 2022-04-20 09:23:06 +02:00
Andras Bacsai
3ab6a231eb feat: Testing fluentd logging driver 2022-04-20 00:20:37 +02:00
Andras Bacsai
2b28f8bd8f feat: Multiply dockerfile locations for docker buildpack 2022-04-19 22:34:28 +02:00
Andras Bacsai
625e71ab08 fix: white-labeled custom logo 2022-04-19 18:23:04 +02:00
Andras Bacsai
b0af54587b feat: Add persistent storage for services 2022-04-18 23:49:08 +02:00
Andras Bacsai
be3080df08 fix: Pull new images for services all the time it's started. 2022-04-18 22:51:55 +02:00
Andras Bacsai
04685c9f9d fix: Scroll to top for logs 2022-04-18 22:46:39 +02:00
Andras Bacsai
1a83f2635f fix: Switch to stream on applications logs 2022-04-18 00:44:08 +02:00
Andras Bacsai
630aa45c87 fix: Application logs paginated 2022-04-18 00:42:08 +02:00
Andras Bacsai
0c3a381d1f fix: Buildlog line number is not string 2022-04-17 23:32:27 +02:00
Andras Bacsai
ffac7c5c87 chore:version++ 2022-04-17 21:08:19 +02:00
Andras Bacsai
410800e81c fix: use arm based certbot on arm 2022-04-17 21:07:59 +02:00
Andras Bacsai
9481beb61f Merge pull request #355 from coollabsio/next
v2.4.10
2022-04-17 20:37:56 +02:00
Andras Bacsai
141f2481a7 fix: Change user's id in sftp wp instance 2022-04-17 20:22:42 +02:00
Andras Bacsai
ea18f25adc ui: show extraconfig if wp is running 2022-04-17 20:22:21 +02:00
Andras Bacsai
9018184747 fix: Stop sFTP connection on wp stop 2022-04-17 20:22:07 +02:00
Andras Bacsai
4fc2dd55f5 chore: version++ 2022-04-17 19:17:20 +02:00
Andras Bacsai
5ef9a282eb fix: Wordpress extra config 2022-04-17 19:17:12 +02:00
Andras Bacsai
56b9a376bd fix: use redis-alpine 2022-04-14 23:48:52 +02:00
Andras Bacsai
0a1d31a188 Merge pull request #349 from coollabsio/v2.4.9
fix: Switch from bitnami/redis to normal redis
2022-04-14 23:42:13 +02:00
Andras Bacsai
64c9fb9a1b fix: Switch from bitnami/redis to normal redis 2022-04-14 23:40:23 +02:00
Andras Bacsai
47aad15cd5 Merge pull request #347 from coollabsio/v2.4.9
v2.4.9
2022-04-14 23:29:15 +02:00
Andras Bacsai
260a47a366 fix: Id of service container 2022-04-14 23:11:24 +02:00
Andras Bacsai
fd4bbe17f0 fix: Restart local docker coolify proxy in case of something happens to it 2022-04-14 21:43:22 +02:00
Andras Bacsai
25ff637703 fix: Remove proxy container in case of dependent container is down 2022-04-14 21:43:05 +02:00
Andras Bacsai
f571453696 fix: Better performance for cleanup images 2022-04-14 18:45:42 +02:00
Andras Bacsai
5cd7533972 fix: Loading of new destinations 2022-04-14 18:34:43 +02:00
Andras Bacsai
3a252509d0 fix: Add HTTP proxy checks 2022-04-14 15:04:18 +02:00
Andras Bacsai
2bd3802a6f fix: Improved tcp proxy monitoring for databases/ftp 2022-04-14 00:04:46 +02:00
Andras Bacsai
ce2757f514 fix: Teams view 2022-04-13 21:06:22 +02:00
Andras Bacsai
8419cdf604 fix: Postgres root pw is pw field 2022-04-13 19:59:30 +02:00
Andras Bacsai
907c2414ae chore:version++ 2022-04-13 19:52:56 +02:00
Andras Bacsai
f82207564f Merge pull request #344 from coollabsio/v2.4.8
v2.4.8
2022-04-13 19:19:04 +02:00
Andras Bacsai
991a09838c chore: version++ 2022-04-13 16:08:40 +02:00
Andras Bacsai
25df4bfd85 fix: Remove system wide pw reset 2022-04-13 16:05:26 +02:00
Andras Bacsai
d2f89d001b fix: GitLab typo 2022-04-13 16:05:08 +02:00
Andras Bacsai
1971f227fd fix: Register should happen if coolify proxy cannot be started 2022-04-13 14:23:42 +02:00
63 changed files with 877 additions and 393 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

@@ -2,10 +2,8 @@ version: '3.8'
services: services:
redis: redis:
image: 'bitnami/redis:6.2' image: redis:6.2-alpine
container_name: coolify-redis container_name: coolify-redis
environment:
- ALLOW_EMPTY_PASSWORD=yes
networks: networks:
- coolify-infra - coolify-infra
ports: ports:
@@ -13,7 +11,24 @@ services:
published: 6379 published: 6379
protocol: tcp protocol: tcp
mode: host 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: networks:
coolify-infra: coolify-infra:
attachable: true attachable: true

View File

@@ -21,11 +21,9 @@ services:
- coolify-infra - coolify-infra
depends_on: ['redis'] depends_on: ['redis']
redis: redis:
image: bitnami/redis:6.2 image: redis:6.2-alpine
restart: always restart: always
container_name: coolify-redis container_name: coolify-redis
environment:
- ALLOW_EMPTY_PASSWORD=yes
networks: networks:
- coolify-infra - coolify-infra

View File

@@ -1,10 +1,10 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.4.7", "version": "2.4.11",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "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:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10", "dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio", "studio": "npx prisma studio",
@@ -63,7 +63,7 @@
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@prisma/client": "3.11.1", "@prisma/client": "3.11.1",
"@sentry/node": "6.19.6", "@sentry/node": "6.19.6",
"bcryptjs": "^2.4.3", "bcryptjs": "2.4.3",
"bullmq": "1.80.0", "bullmq": "1.80.0",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cookie": "0.4.2", "cookie": "0.4.2",

18
pnpm-lock.yaml generated
View File

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

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

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

@@ -9,6 +9,7 @@ import { default as ProdPrisma } from '@prisma/client';
import type { Database, DatabaseSettings } from '@prisma/client'; import type { Database, DatabaseSettings } from '@prisma/client';
import generator from 'generate-password'; import generator from 'generate-password';
import forge from 'node-forge'; import forge from 'node-forge';
import getPort, { portNumbers } from 'get-port';
export function generatePassword(length = 24): string { export function generatePassword(length = 24): string {
return generator.generate({ return generator.generate({
@@ -251,3 +252,29 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
}; };
} }
} }
export async function getFreePort() {
const data = await prisma.setting.findFirst();
const { minPort, maxPort } = data;
const dbUsed = await (
await prisma.database.findMany({
where: { publicPort: { not: null } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const wpFtpUsed = await (
await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
select: { ftpPublicPort: true }
})
).map((a) => a.ftpPublicPort);
const wpUsed = await (
await prisma.wordpress.findMany({
where: { mysqlPublicPort: { not: null } },
select: { mysqlPublicPort: true }
})
).map((a) => a.mysqlPublicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed];
return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts });
}

View File

@@ -90,7 +90,7 @@ export async function addGitLabSource({
appSecret, appSecret,
groupName groupName
}) { }) {
const encrptedAppSecret = encrypt(appSecret); const encryptedAppSecret = encrypt(appSecret);
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } }); await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
return await prisma.gitlabApp.create({ return await prisma.gitlabApp.create({
data: { data: {

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

@@ -46,8 +46,12 @@ export async function login({
if (users === 0) { if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
// Create default network & start Coolify Proxy // Create default network & start Coolify Proxy
await asyncExecShell(`docker network create --attachable coolify`); try {
await startCoolifyProxy('/var/run/docker.sock'); await asyncExecShell(`docker network create --attachable coolify`);
} catch (error) {}
try {
await startCoolifyProxy('/var/run/docker.sock');
} catch (error) {}
uid = '0'; uid = '0';
} }

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

@@ -127,10 +127,10 @@ export async function startTcpProxy(
const containerName = `haproxy-for-${publicPort}`; const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName); const found = await checkContainer(engine, containerName);
const foundDB = await checkContainer(engine, id); const foundDependentContainer = await checkContainer(engine, id);
try { try {
if (foundDB && !found) { if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell( const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
); );
@@ -141,6 +141,11 @@ export async function startTcpProxy(
} -d coollabsio/${defaultProxyImageTcp}` } -d coollabsio/${defaultProxyImageTcp}`
); );
} }
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) { } catch (error) {
return error; return error;
} }
@@ -157,10 +162,10 @@ export async function startHttpProxy(
const containerName = `haproxy-for-${publicPort}`; const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName); const found = await checkContainer(engine, containerName);
const foundDB = await checkContainer(engine, id); const foundDependentContainer = await checkContainer(engine, id);
try { try {
if (foundDB && !found) { if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell( const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
); );
@@ -169,6 +174,11 @@ export async function startHttpProxy(
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageHttp}` `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageHttp}`
); );
} }
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) { } catch (error) {
return error; return error;
} }

View File

@@ -10,6 +10,9 @@ import { promises as dns } from 'dns';
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> { export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try { try {
const certbotImage =
process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest';
const data = await db.prisma.setting.findFirst(); const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data; const { minPort, maxPort } = data;
@@ -63,7 +66,7 @@ export async function letsEncrypt(domain: string, id?: string, isCoolify = false
if (found) return; if (found) return;
await asyncExecShell( 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' : '' dev ? '--test-cert' : ''
}` }`
); );
@@ -83,7 +86,7 @@ export async function letsEncrypt(domain: string, id?: string, isCoolify = false
} }
if (found) return; if (found) return;
await asyncExecShell( 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' : '' dev ? '--test-cert' : ''
}` }`
); );
@@ -215,7 +218,7 @@ export async function generateSSLCerts(): Promise<void> {
certificates.includes(ssl.domain) || certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', '')) certificates.includes(ssl.domain.replace('www.', ''))
) { ) {
console.log(`Certificate for ${ssl.domain} already exists`); // console.log(`Certificate for ${ssl.domain} already exists`);
} else { } else {
// Checking DNS entry before generating certificate // Checking DNS entry before generating certificate
if (ipv4 || ipv6) { if (ipv4 || ipv6) {
@@ -232,7 +235,7 @@ export async function generateSSLCerts(): Promise<void> {
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) || (ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', ''))) (ipv6 && domains6.includes(ipv6.replace('\n', '')))
) { ) {
console.log('Generating SSL for', ssl.domain, '.'); console.log('Generating SSL for', ssl.domain);
return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify); return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
} }
} }
@@ -261,7 +264,7 @@ export async function generateSSLCerts(): Promise<void> {
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) || (ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', ''))) (ipv6 && domains6.includes(ipv6.replace('\n', '')))
) { ) {
console.log('Generating SSL for', ssl.domain, '.'); console.log('Generating SSL for', ssl.domain);
return; return;
} }
} }

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 });
@@ -286,6 +289,9 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
labels, labels,
depends_on: [], depends_on: [],
restart: 'always', restart: 'always',
// logging: {
// driver: 'fluentd',
// },
deploy: { deploy: {
restart_policy: { restart_policy: {
condition: 'on-failure', condition: 'on-failure',

View File

@@ -2,8 +2,9 @@ import { asyncExecShell, getEngine, version } from '$lib/common';
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
export default async function (): Promise<void> { export default async function (): Promise<void> {
const destinationDockers = await prisma.destinationDocker.findMany(); const destinationDockers = await prisma.destinationDocker.findMany();
for (const destinationDocker of destinationDockers) { const engines = [...new Set(destinationDockers.map(({ engine }) => engine))];
const host = getEngine(destinationDocker.engine); for (const engine of engines) {
const host = getEngine(engine);
// Cleanup old coolify images // Cleanup old coolify images
try { try {
let { stdout: images } = await asyncExecShell( let { stdout: images } = await asyncExecShell(
@@ -28,7 +29,7 @@ export default async function (): Promise<void> {
} }
// Cleanup old images older than a day // Cleanup old images older than a day
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=24h" -a -f`); await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
} catch (error) { } catch (error) {
//console.log(error); //console.log(error);
} }

View File

@@ -7,6 +7,7 @@ import builder from './builder';
import logger from './logger'; import logger from './logger';
import cleanup from './cleanup'; import cleanup from './cleanup';
import proxy from './proxy'; import proxy from './proxy';
import proxyTcpHttp from './proxyTcpHttp';
import ssl from './ssl'; import ssl from './ssl';
import sslrenewal from './sslrenewal'; import sslrenewal from './sslrenewal';
@@ -29,17 +30,20 @@ const connectionOptions = {
const cron = async (): Promise<void> => { const cron = async (): Promise<void> => {
new QueueScheduler('proxy', connectionOptions); new QueueScheduler('proxy', connectionOptions);
new QueueScheduler('proxyTcpHttp', connectionOptions);
new QueueScheduler('cleanup', connectionOptions); new QueueScheduler('cleanup', connectionOptions);
new QueueScheduler('ssl', connectionOptions); new QueueScheduler('ssl', connectionOptions);
new QueueScheduler('sslRenew', connectionOptions); new QueueScheduler('sslRenew', connectionOptions);
const queue = { const queue = {
proxy: new Queue('proxy', { ...connectionOptions }), proxy: new Queue('proxy', { ...connectionOptions }),
proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }),
cleanup: new Queue('cleanup', { ...connectionOptions }), cleanup: new Queue('cleanup', { ...connectionOptions }),
ssl: new Queue('ssl', { ...connectionOptions }), ssl: new Queue('ssl', { ...connectionOptions }),
sslRenew: new Queue('sslRenew', { ...connectionOptions }) sslRenew: new Queue('sslRenew', { ...connectionOptions })
}; };
await queue.proxy.drain(); await queue.proxy.drain();
await queue.proxyTcpHttp.drain();
await queue.cleanup.drain(); await queue.cleanup.drain();
await queue.ssl.drain(); await queue.ssl.drain();
await queue.sslRenew.drain(); await queue.sslRenew.drain();
@@ -54,6 +58,16 @@ const cron = async (): Promise<void> => {
} }
); );
new Worker(
'proxyTcpHttp',
async () => {
await proxyTcpHttp();
},
{
...connectionOptions
}
);
new Worker( new Worker(
'ssl', 'ssl',
async () => { async () => {
@@ -85,6 +99,7 @@ const cron = async (): Promise<void> => {
); );
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } }); await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } });
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });

View File

@@ -0,0 +1,55 @@
import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database';
import { startCoolifyProxy, startHttpProxy, startTcpProxy } from '$lib/haproxy';
export default async function (): Promise<void | {
status: number;
body: { message: string; error: string };
}> {
try {
// Coolify Proxy
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine: '/var/run/docker.sock' }
});
if (localDocker && localDocker.isCoolifyProxyUsed) {
await startCoolifyProxy('/var/run/docker.sock');
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId) {
const { privatePort } = generateDatabaseConfiguration(database);
await startTcpProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId) {
await startTcpProxy(destinationDocker, `${id}-ftp`, ftpPublicPort, 22);
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId) {
await startHttpProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
return ErrorHandler(error.response?.body || error);
}
}

View File

@@ -1,8 +1,6 @@
export const publicPaths = [ export const publicPaths = [
'/login', '/login',
'/register', '/register',
'/reset',
'/reset/password',
'/webhooks/success', '/webhooks/success',
'/webhooks/github', '/webhooks/github',
'/webhooks/github/install', '/webhooks/github/install',

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}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100" <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') .split('\n')
.map((l) => l.slice(8)) .map((l) => l.slice(8))
.filter((a) => a) .filter((a) => a)
.reverse()
} }
}; };
} }

View File

@@ -24,19 +24,23 @@
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 = {
logs: []
};
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 +49,55 @@
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: any = await get(`/applications/${id}/logs.json`);
logs = newLogs.logs; allLogs = data.logs;
logs = data.logs.slice(0, 100);
if (logs.length < 100) {
endOfLogs = true;
}
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 +179,18 @@
<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="bg-transparent" class:text-coolgray-100={endOfLogs}
data-tooltip="Follow logs" class:hover:bg-coolgray-400={!endOfLogs}
class:text-green-500={followingBuild} class="bg-transparent tooltip-bottom"
data-tooltip="Older logs"
disabled={endOfLogs}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -164,10 +203,33 @@
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 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> </svg>
</button> </button>
</div> </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" 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

@@ -29,10 +29,12 @@
disabled={!isRunning} disabled={!isRunning}
readonly={!isRunning} readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword" id="rootUserPassword"
name="rootUserPassword" name="rootUserPassword"
bind:value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label> <label for="dbUser" class="text-base font-bold text-stone-100">User</label>

View File

@@ -1,7 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, stopDatabase } from '$lib/database'; import { ErrorHandler, stopDatabase } from '$lib/database';
import { deleteProxy } from '$lib/haproxy'; import { stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const del: RequestHandler = async (event) => { export const del: RequestHandler = async (event) => {
@@ -12,7 +12,7 @@ export const del: RequestHandler = async (event) => {
const database = await db.getDatabase({ id, teamId }); const database = await db.getDatabase({ id, teamId });
if (database.destinationDockerId) { if (database.destinationDockerId) {
const everStarted = await stopDatabase(database); const everStarted = await stopDatabase(database);
if (everStarted) await deleteProxy({ id }); if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort);
} }
await db.removeDatabase({ id }); await db.removeDatabase({ id });
return { status: 200 }; return { status: 200 };

View File

@@ -1,20 +1,16 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler } from '$lib/database'; import { generateDatabaseConfiguration, ErrorHandler, getFreePort } from '$lib/database';
import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import getPort, { portNumbers } from 'get-port';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event); const { status, body, teamId } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const { isPublic, appendOnly = true } = await event.request.json(); const { isPublic, appendOnly = true } = await event.request.json();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) }); const publicPort = await getFreePort();
try { try {
await db.setDatabase({ id, isPublic, appendOnly }); await db.setDatabase({ id, isPublic, appendOnly });

View File

@@ -13,20 +13,25 @@ export const get: RequestHandler = async (event) => {
select: { id: true, email: true, teams: true } select: { id: true, email: true, teams: true }
}); });
let accounts = []; let accounts = [];
let allTeams = [];
if (teamId === '0') { if (teamId === '0') {
accounts = await db.prisma.user.findMany({ select: { id: true, email: true, teams: true } }); accounts = await db.prisma.user.findMany({ select: { id: true, email: true, teams: true } });
allTeams = await db.prisma.team.findMany({
where: { users: { none: { id: userId } } },
include: { permissions: true }
});
} }
const ownTeams = await db.prisma.team.findMany({
const teams = await db.prisma.permission.findMany({ where: { users: { some: { id: userId } } },
where: { userId: teamId === '0' ? undefined : userId }, include: { permissions: true }
include: { team: { include: { _count: { select: { users: true } } } } }
}); });
const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } }); const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } });
return { return {
status: 200, status: 200,
body: { body: {
teams, ownTeams,
allTeams,
invitations, invitations,
account, account,
accounts accounts

View File

@@ -36,18 +36,8 @@
if (accounts.length === 0) { if (accounts.length === 0) {
accounts.push(account); accounts.push(account);
} }
export let teams; export let ownTeams;
export let allTeams;
const ownTeams = teams.filter((team) => {
if (team.team.id === $session.teamId) {
return team;
}
});
const otherTeams = teams.filter((team) => {
if (team.team.id !== $session.teamId) {
return team;
}
});
async function resetPassword(id) { async function resetPassword(id) {
const sure = window.confirm('Are you sure you want to reset the password?'); const sure = window.confirm('Are you sure you want to reset the password?');
@@ -167,49 +157,51 @@
<div class="title font-bold">Teams</div> <div class="title font-bold">Teams</div>
<div class="flex items-center justify-center pt-10"> <div class="flex items-center justify-center pt-10">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 pb-10 md:flex-row"> <div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
{#each ownTeams as team} {#each ownTeams as team}
<a href="/iam/team/{team.teamId}" class="w-96 p-2 no-underline"> <a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div <div
class="box-selection relative" class="box-selection relative"
class:hover:bg-cyan-600={team.team?.id !== '0'} class:hover:bg-cyan-600={team.id !== '0'}
class:hover:bg-red-500={team.team?.id === '0'} class:hover:bg-red-500={team.id === '0'}
> >
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{team.team.name} {team.name}
</div> </div>
<div class="truncate text-center font-bold"> <div class="truncate text-center font-bold">
{team.team?.id === '0' ? 'root team' : ''} {team.id === '0' ? 'root team' : ''}
</div> </div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div> <div class:mt-6={team.id !== '0'} class="mt-1 text-center">
{team.permissions?.length} member(s)
</div>
</div> </div>
</a> </a>
{/each} {/each}
</div> </div>
{#if $session.teamId === '0' && otherTeams.length > 0} {#if $session.teamId === '0' && allTeams.length > 0}
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div> <div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
{/if} <div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> {#each allTeams as team}
{#each otherTeams as team} <a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<a href="/iam/team/{team.teamId}" class="w-96 p-2 no-underline"> <div
<div class="box-selection relative"
class="box-selection relative" class:hover:bg-cyan-600={team.id !== '0'}
class:hover:bg-cyan-600={team.team?.id !== '0'} class:hover:bg-red-500={team.id === '0'}
class:hover:bg-red-500={team.team?.id === '0'} >
> <div class="truncate text-center text-xl font-bold">
<div class="truncate text-center text-xl font-bold"> {team.name}
{team.team.name} </div>
</div> <div class="truncate text-center font-bold">
<div class="truncate text-center font-bold"> {team.id === '0' ? 'root team' : ''}
{team.team?.id === '0' ? 'root team' : ''} </div>
</div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div> <div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
</div> </div>
</a> </a>
{/each} {/each}
</div> </div>
{/if}
</div> </div>
</div> </div>
</div> </div>

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"
@@ -76,10 +83,6 @@
on:click|preventDefault={() => goto('/register')} on:click|preventDefault={() => goto('/register')}
class="bg-transparent hover:bg-coolgray-300 text-white ">Register</button class="bg-transparent hover:bg-coolgray-300 text-white ">Register</button
> >
<button
class="bg-transparent hover:bg-coolgray-300"
on:click|preventDefault={() => goto('/reset')}>Reset password</button
>
</div> </div>
</form> </form>
</div> </div>

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

@@ -11,7 +11,9 @@
let loading = false; let loading = false;
async function handleSubmit() { async function handleSubmit() {
if (loading) return;
try { try {
loading = true;
await post('/new/destination/check.json', { network: payload.network }); await post('/new/destination/check.json', { network: payload.network });
const { id } = await post('/new/destination/docker.json', { const { id } = await post('/new/destination/docker.json', {
...payload ...payload

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"
@@ -105,6 +112,9 @@
{#if userCount === 0} {#if userCount === 0}
<div class="pt-5"> <div class="pt-5">
You are registering the first user. It will be the administrator of your Coolify instance. You are registering the first user. It will be the administrator of your Coolify instance.
<br />
It will take a while, because Coolify will configure itself, the proxy and other docker related
stuff.
</div> </div>
{/if} {/if}
{/if} {/if}

View File

@@ -1,26 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
export const get: RequestHandler = async () => {
const users = await db.prisma.user.findMany({});
return {
status: 200,
body: {
users
}
};
};
export const post: RequestHandler = async (event) => {
const { secretKey } = await event.request.json();
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
return {
status: 500,
body: {
error: 'Invalid secret key.'
}
};
}
return {
status: 200
};
};

View File

@@ -1,96 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
let secretKey;
let password = false;
let users = [];
async function handleSubmit() {
try {
await post(`/reset.json`, { secretKey });
password = true;
const data = await get('/reset.json');
users = data.users;
return;
} catch ({ error }) {
return errorNotification(error);
}
}
async function resetPassword(user) {
try {
await post(`/reset/password.json`, { secretKey, user });
toast.push('Password reset done.');
return;
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="icons fixed top-0 left-0 m-3 cursor-pointer" on:click={() => goto('/')}>
<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" />
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="5" y1="12" x2="11" y2="18" />
<line x1="5" y1="12" x2="11" y2="6" />
</svg>
</div>
<div class="pb-10 pt-24 text-center text-4xl font-bold">Reset Password</div>
<div class="flex items-center justify-center">
{#if password}
<table class="mx-2 text-left">
<thead class="mb-2">
<tr>
<th class="px-2">Email</th>
<th>New password</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr>
<td class="px-2">{user.email}</td>
<td class="flex space-x-2">
<input
id="newPassword"
name="newPassword"
bind:value={user.newPassword}
placeholder="Super secure new password"
/>
<button
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100"
on:click={() => resetPassword(user)}>Reset</button
></td
>
</tr>
{/each}
</tbody>
</table>
{:else}
<form class="flex flex-col" on:submit|preventDefault={handleSubmit}>
<div class="text-center text-2xl py-2 font-bold">Secret Key</div>
<CopyPasswordField
isPasswordField={true}
id="secretKey"
name="secretKey"
bind:value={secretKey}
placeholder="You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)"
/>
<button type="submit" class="bg-coollabs hover:bg-coollabs-100 mx-auto w-32 my-4"
>Submit</button
>
</form>
{/if}
</div>

View File

@@ -1,27 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
import { ErrorHandler, hashPassword } from '$lib/database';
export const post: RequestHandler = async (event) => {
const { secretKey, user } = await event.request.json();
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
return {
status: 500,
body: {
error: 'Invalid secret key.'
}
};
}
try {
const hashedPassword = await hashPassword(user.newPassword);
await db.prisma.user.update({
where: { email: user.email },
data: { password: hashedPassword }
});
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -62,10 +62,11 @@
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="extraConfig">Extra Config</label> <label for="extraConfig">Extra Config</label>
<textarea <textarea
bind:value={service.wordpress.extraConfig}
disabled={isRunning} disabled={isRunning}
readonly={isRunning} readonly={isRunning}
class:resize-none={isRunning} class:resize-none={isRunning}
rows={isRunning ? 1 : 5} rows="5"
name="extraConfig" name="extraConfig"
id="extraConfig" id="extraConfig"
placeholder={!isRunning placeholder={!isRunning
@@ -74,8 +75,8 @@
define('WP_ALLOW_MULTISITE', true); define('WP_ALLOW_MULTISITE', true);
define('MULTISITE', true); define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', false);` define('SUBDOMAIN_INSTALL', false);`
: 'N/A'}>{service.wordpress.extraConfig}</textarea : 'N/A'}
> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting

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

@@ -4,9 +4,7 @@ import { promises as fs } from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { startHttpProxy } from '$lib/haproxy'; import { startHttpProxy } from '$lib/haproxy';
import getPort, { portNumbers } from 'get-port'; import { ErrorHandler, getFreePort, getServiceImage } from '$lib/database';
import { getDomain } from '$lib/components/common';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile'; import type { ComposeFile } from '$lib/types/composeFile';
@@ -28,13 +26,10 @@ export const post: RequestHandler = async (event) => {
serviceSecret serviceSecret
} = service; } = service;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) }); const publicPort = await getFreePort();
const consolePort = 9001; const consolePort = 9001;
const apiPort = 9000; const apiPort = 9000;
@@ -93,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

@@ -2,7 +2,7 @@ import { dev } from '$app/env';
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler, generatePassword } from '$lib/database'; import { ErrorHandler, generatePassword, getFreePort } from '$lib/database';
import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile'; import type { ComposeFile } from '$lib/types/composeFile';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
@@ -16,11 +16,10 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const { ftpEnabled } = await event.request.json(); const { ftpEnabled } = await event.request.json();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) }); const publicPort = await getFreePort();
let ftpUser = cuid(); let ftpUser = cuid();
let ftpPassword = generatePassword(); let ftpPassword = generatePassword();
@@ -114,7 +113,7 @@ export const post: RequestHandler = async (event) => {
services: { services: {
[`${id}-ftp`]: { [`${id}-ftp`]: {
image: `atmoz/sftp:alpine`, image: `atmoz/sftp:alpine`,
command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:1001'`, command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`,
extra_hosts: ['host.docker.internal:host-gateway'], extra_hosts: ['host.docker.internal:host-gateway'],
container_name: `${id}-ftp`, container_name: `${id}-ftp`,
volumes, volumes,

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

View File

@@ -12,21 +12,44 @@ export const post: RequestHandler = async (event) => {
try { try {
const service = await db.getService({ id, teamId }); const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const {
destinationDockerId,
destinationDocker,
fqdn,
wordpress: { ftpEnabled }
} = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine; const engine = destinationDocker.engine;
try { try {
let found = await checkContainer(engine, id); const found = await checkContainer(engine, id);
if (found) { if (found) {
await removeDestinationDocker({ id, engine }); await removeDestinationDocker({ id, engine });
} }
found = await checkContainer(engine, `${id}-mysql`); } catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-mysql`);
if (found) { if (found) {
await removeDestinationDocker({ id: `${id}-mysql`, engine }); await removeDestinationDocker({ id: `${id}-mysql`, engine });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try {
if (ftpEnabled) {
const found = await checkContainer(engine, `${id}-ftp`);
if (found) {
await removeDestinationDocker({ id: `${id}-ftp`, engine });
}
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpEnabled: false }
});
}
} catch (error) {
console.error(error);
}
} }
return { return {