Compare commits

...

52 Commits

Author SHA1 Message Date
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
Andras Bacsai
c1adffe260 Merge pull request #343 from coollabsio/v2.4.7
v2.4.7
2022-04-13 13:12:35 +02:00
Andras Bacsai
e725887a55 chore:version++ 2022-04-13 13:12:23 +02:00
Andras Bacsai
5bf79b75b0 fix: Destinations to HAProxy 2022-04-13 13:10:04 +02:00
Andras Bacsai
6926975e40 Merge pull request #341 from coollabsio/v2.4.6
v2.4.6
2022-04-13 08:40:10 +02:00
Andras Bacsai
978a01c968 fix: Reverting postgres password for now 2022-04-13 08:35:20 +02:00
Andras Bacsai
f421f5ee84 fix: No permission on first registration 2022-04-12 23:57:08 +02:00
Andras Bacsai
383831c7b8 fix: Restart policy for resources 2022-04-12 23:12:09 +02:00
Andras Bacsai
41329facf7 fix: Try catch me 2022-04-12 22:49:48 +02:00
Andras Bacsai
7d3c644148 fix: DNS check before creating SSL cert 2022-04-12 22:18:54 +02:00
Andras Bacsai
7fab9b5930 fix: ProjectID for Github 2022-04-12 22:18:43 +02:00
Andras Bacsai
58763ef84c fix: Load all branches, not just the first 30 2022-04-12 21:48:50 +02:00
Andras Bacsai
0e6abf172b fix: Meilisearch service 2022-04-12 21:09:38 +02:00
Andras Bacsai
9e681ece41 chore: version++ 2022-04-12 20:58:02 +02:00
Andras Bacsai
28f87a306d fix: Cleanup images older than a day 2022-04-12 20:57:49 +02:00
Andras Bacsai
23e8833208 Merge pull request #339 from coollabsio/v2.4.5
v2.4.5
2022-04-12 19:08:46 +02:00
Andras Bacsai
03962663c2 fix: Timeout values 2022-04-12 18:21:10 +02:00
Andras Bacsai
cc2ec55c4d chore: version++ 2022-04-12 16:50:13 +02:00
Andras Bacsai
ff2c38aa16 fix: Invitations 2022-04-12 16:49:59 +02:00
Andras Bacsai
b5a9a2cea8 fix: Types 2022-04-12 16:49:52 +02:00
Andras Bacsai
cd3f661f7e Merge pull request #336 from coollabsio/v2.4.4
v2.4.4
2022-04-12 11:02:35 +02:00
Andras Bacsai
41bf6b5b86 fixes 2022-04-12 10:47:53 +02:00
Andras Bacsai
a4e7c85184 Add only amd release 2022-04-12 10:14:18 +02:00
Andras Bacsai
19aca9ab35 chore: version++ 2022-04-12 10:13:19 +02:00
Andras Bacsai
08704c289a fix: Proxy 2022-04-12 10:12:46 +02:00
Andras Bacsai
2224c22c6e fix: haproxy build stuffs 2022-04-12 09:22:27 +02:00
Andras Bacsai
b281889acd Merge branch 'main' of github.com:coollabsio/coolify into main 2022-04-12 09:20:12 +02:00
Andras Bacsai
cfc50a27b0 Package.json update 2022-04-12 09:19:48 +02:00
53 changed files with 638 additions and 421 deletions

View File

@@ -1,6 +1,6 @@
FROM haproxytech/haproxy-alpine:2.5 FROM haproxytech/haproxy-alpine:2.5
RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe
COPY haproxy/haproxy.cfg-http.template /usr/local/etc/haproxy/haproxy.cfg COPY data/haproxy/haproxy.cfg-http.template /usr/local/etc/haproxy/haproxy.cfg
COPY haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl COPY data/haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl
COPY haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem COPY data/haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem

View File

@@ -1,6 +1,6 @@
FROM haproxytech/haproxy-alpine:2.5 FROM haproxytech/haproxy-alpine:2.5
RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe
COPY haproxy/haproxy.cfg-tcp.template /usr/local/etc/haproxy/haproxy.cfg COPY data/haproxy/haproxy.cfg-tcp.template /usr/local/etc/haproxy/haproxy.cfg
COPY haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl COPY data/haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl
COPY haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem COPY data/haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem

View File

@@ -1,6 +1,6 @@
FROM haproxytech/haproxy-alpine:2.5 FROM haproxytech/haproxy-alpine:2.5
RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe RUN mkdir -p /usr/local/etc/haproxy/ssl /usr/local/etc/haproxy/maps /usr/local/etc/haproxy/spoe
COPY haproxy/haproxy.cfg.template /usr/local/etc/haproxy/haproxy.cfg COPY data/haproxy/haproxy.cfg.template /usr/local/etc/haproxy/haproxy.cfg
COPY haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl COPY data/haproxy/dataplaneapi.hcl /usr/local/etc/haproxy/dataplaneapi.hcl
COPY haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem COPY data/haproxy/ssl/default.pem /usr/local/etc/haproxy/ssl/default.pem

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:

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,7 +1,7 @@
{ {
"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.3", "version": "2.4.10",
"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",
@@ -18,11 +18,12 @@
"db:seed": "prisma db seed", "db:seed": "prisma db seed",
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name", "db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"release:production:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .", "release:production:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .",
"release:production:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .",
"release:staging:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version --push .", "release:staging:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version --push .",
"release:staging:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version --push .", "release:staging:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version --push .",
"release:haproxy": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-alpine:latest -t coollabsio/coolify-haproxy-alpine:1.1.0 -f haproxy.Dockerfile --push .", "release:haproxy": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-alpine:latest -t coollabsio/coolify-haproxy-alpine:1.1.0 -f data/haproxy.Dockerfile --push .",
"release:haproxy:tcp": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-tcp-alpine:latest -t coollabsio/coolify-haproxy-tcp-alpine:1.1.0 -f haproxy-tcp.Dockerfile --push .", "release:haproxy:tcp": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-tcp-alpine:latest -t coollabsio/coolify-haproxy-tcp-alpine:1.1.0 -f data/haproxy-tcp.Dockerfile --push .",
"release:haproxy:http": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-http-alpine:latest -t coollabsio/coolify-haproxy-http-alpine:1.1.0 -f haproxy-http.Dockerfile --push .", "release:haproxy:http": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-http-alpine:latest -t coollabsio/coolify-haproxy-http-alpine:1.1.0 -f data/haproxy-http.Dockerfile --push .",
"prepare": "husky install" "prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
@@ -63,7 +64,7 @@
"@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.79.0", "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",

16
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.310 '@sveltejs/kit': 1.0.0-next.303
'@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
@@ -15,7 +15,7 @@ specifiers:
'@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.79.0 bullmq: 1.78.1
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.79.0 bullmq: 1.78.1
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.310_svelte@3.47.0 '@sveltejs/kit': 1.0.0-next.303_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.310_svelte@3.47.0: /@sveltejs/kit/1.0.0-next.303_svelte@3.47.0:
resolution: resolution:
{ {
integrity: sha512-pTyMyaoyHS+V5cQZIQMfQXmLkhw1VaRwT9avOSgwDc0QBpnNw2LdzwoPYsUr96ca5B6cfT3SMUNolxErTNHmPQ== integrity: sha512-WdxDc8OiF1WEd/bEza7CBdzA+3qIcCi1GKBj/gieKX9I3N8iDJt/Cg2POrLo9wQoJ47nZcAd1eOhfr7XEX1aIQ==
} }
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.79.0: /bullmq/1.78.1:
resolution: resolution:
{ {
integrity: sha512-rVtNCDpcWdc+U1MinRtvhJv+GBFNkz0Q3Unf20010qIC6Pj+O2kkIUeepBkCmMNz6G9abrhsee2PheGRJ32+sw== integrity: sha512-er45mM8nGhgA83EVCJ4PNxPyDSzakvoxeFGU4vdSgYeB+SbeFQAlJYmAC50Ms7YFPstm1LeinbVZ+oX/BmBzOg==
} }
dependencies: dependencies:
cron-parser: 4.2.1 cron-parser: 4.2.1

View File

@@ -1,11 +1,9 @@
// TODO: Make this functions generic
async function send({ async function send({
method, method,
path, path,
data = {}, data = {},
headers, headers,
timeout = 30000 timeout = 120000
}): Promise<Record<string, unknown>> { }): Promise<Record<string, unknown>> {
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout); const id = setTimeout(() => controller.abort(), timeout);
@@ -53,7 +51,7 @@ async function send({
export function get( export function get(
path: string, path: string,
headers: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return send({ method: 'GET', path, headers }); return send({ method: 'GET', path, headers });
} }
@@ -61,7 +59,7 @@ export function get(
export function del( export function del(
path: string, path: string,
data: Record<string, unknown>, data: Record<string, unknown>,
headers: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return send({ method: 'DELETE', path, data, headers }); return send({ method: 'DELETE', path, data, headers });
} }
@@ -69,7 +67,7 @@ export function del(
export function post( export function post(
path: string, path: string,
data: Record<string, unknown>, data: Record<string, unknown>,
headers: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return send({ method: 'POST', path, data, headers }); return send({ method: 'POST', path, data, headers });
} }
@@ -77,7 +75,7 @@ export function post(
export function put( export function put(
path: string, path: string,
data: Record<string, unknown>, data: Record<string, unknown>,
headers: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return send({ method: 'PUT', path, data, headers }); return send({ method: 'PUT', path, data, headers });
} }

View File

@@ -93,11 +93,16 @@ export const getUserDetails = async (
}> => { }> => {
const teamId = getTeam(event); const teamId = getTeam(event);
const userId = event?.locals?.session?.data?.userId || null; const userId = event?.locals?.session?.data?.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({ let permission = 'read';
where: { teamId, userId }, if (teamId && userId) {
select: { permission: true }, const data = await db.prisma.permission.findFirst({
rejectOnNotFound: true where: { teamId, userId },
}); select: { permission: true },
rejectOnNotFound: true
});
if (data.permission) permission = data.permission;
}
const payload = { const payload = {
teamId, teamId,
userId, userId,

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({
@@ -219,7 +220,6 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
return { return {
privatePort: 5432, privatePort: 5432,
environmentVariables: { environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
POSTGRESQL_PASSWORD: dbUserPassword, POSTGRESQL_PASSWORD: dbUserPassword,
POSTGRESQL_USERNAME: dbUser, POSTGRESQL_USERNAME: dbUser,
POSTGRESQL_DATABASE: defaultDatabase POSTGRESQL_DATABASE: defaultDatabase
@@ -252,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

@@ -18,18 +18,10 @@ export async function listSources(
export async function newSource({ export async function newSource({
name, name,
teamId, teamId
type,
htmlUrl,
apiUrl,
organization
}: { }: {
name: string; name: string;
teamId: string; teamId: string;
type: string;
htmlUrl: string;
apiUrl: string;
organization: string;
}): Promise<GitSource> { }): Promise<GitSource> {
return await prisma.gitSource.create({ return await prisma.gitSource.create({
data: { data: {
@@ -74,8 +66,11 @@ export async function getSource({
if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret); if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret);
return body; return body;
} }
export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }) { export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization }) {
await prisma.gitSource.update({ where: { id }, data: { type, name, htmlUrl, apiUrl } }); await prisma.gitSource.update({
where: { id },
data: { type, name, htmlUrl, apiUrl, organization }
});
return await prisma.githubApp.create({ return await prisma.githubApp.create({
data: { data: {
teams: { connect: { id: teamId } }, teams: { connect: { id: teamId } },
@@ -95,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: {
@@ -123,10 +118,14 @@ export async function configureGitsource({
} }
export async function updateGitsource({ export async function updateGitsource({
id, id,
name name,
htmlUrl,
apiUrl
}: { }: {
id: string; id: string;
name: string; name: string;
htmlUrl: string;
apiUrl: string;
}): Promise<GitSource> { }): Promise<GitSource> {
return await prisma.gitSource.update({ return await prisma.gitSource.update({
where: { id }, where: { 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

@@ -1,6 +1,8 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import got, { type Got } from 'got'; import got, { type Got } from 'got';
import * as db from '$lib/database'; import * as db from '$lib/database';
import mustache from 'mustache';
import crypto from 'crypto';
import { checkContainer, checkHAProxy } from '.'; import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common'; import { supportedServiceTypesAndVersions } from '$lib/components/common';
@@ -18,10 +20,10 @@ global
defaults defaults
mode http mode http
log global log global
timeout http-request 60s timeout http-request 120s
timeout connect 10s timeout connect 10s
timeout client 60s timeout client 120s
timeout server 60s timeout server 120s
userlist haproxy-dataplaneapi userlist haproxy-dataplaneapi
user admin insecure-password "\${HAPROXY_PASSWORD}" user admin insecure-password "\${HAPROXY_PASSWORD}"
@@ -213,7 +215,8 @@ export async function configureHAProxy(): Promise<void> {
plausibleAnalytics: true, plausibleAnalytics: true,
vscodeserver: true, vscodeserver: true,
wordpress: true, wordpress: true,
ghost: true ghost: true,
meiliSearch: true
} }
}); });
@@ -262,36 +265,20 @@ export async function configureHAProxy(): Promise<void> {
redirectValue, redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
}); });
for (const service of services) { }
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; const output = mustache.render(template, data);
if (destinationDockerId) { const newHash = crypto.createHash('md5').update(output).digest('hex');
const { engine } = destinationDocker; const { proxyHash, id } = await db.listSettings();
const found = supportedServiceTypesAndVersions.find((a) => a.name === type); if (proxyHash !== newHash) {
if (found) { await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
const port = found.ports.main; await haproxy.post(`v2/services/haproxy/configuration/raw`, {
const publicPort = service[type]?.publicPort; searchParams: {
const isRunning = await checkContainer(engine, id); skip_version: true
if (fqdn) { },
const domain = getDomain(fqdn); body: output,
const isHttps = fqdn.startsWith('https://'); headers: {
const isWWW = fqdn.includes('www.'); 'Content-Type': 'text/plain'
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.services.push({
id,
port,
publicPort,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
}
}
} }
} });
} }
} }

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;
} }
@@ -236,9 +246,15 @@ export async function stopCoolifyProxy(
export async function configureNetworkCoolifyProxy(engine: string): Promise<void> { export async function configureNetworkCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine); const host = getEngine(engine);
const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } }); const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } });
const { stdout: networks } = await asyncExecShell(
`DOCKER_HOST="${host}" docker ps -a --filter name=coolify-haproxy --format '{{json .Networks}}'`
);
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
for (const destination of destinations) { for (const destination of destinations) {
await asyncExecShell( if (!configuredNetworks.includes(destination.network)) {
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy` await asyncExecShell(
); `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
);
}
} }
} }

View File

@@ -6,6 +6,7 @@ import cuid from 'cuid';
import fs from 'fs/promises'; import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common'; import { supportedServiceTypesAndVersions } from '$lib/components/common';
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 {
@@ -148,7 +149,8 @@ export async function generateSSLCerts(): Promise<void> {
plausibleAnalytics: true, plausibleAnalytics: true,
vscodeserver: true, vscodeserver: true,
wordpress: true, wordpress: true,
ghost: true ghost: true,
meiliSearch: true
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
}); });
@@ -198,16 +200,44 @@ export async function generateSSLCerts(): Promise<void> {
file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, '')); file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, ''));
} }
} }
const resolver = new dns.Resolver({ timeout: 2000 });
resolver.setServers(['8.8.8.8', '1.1.1.1']);
let ipv4, ipv6;
try {
ipv4 = await (await asyncExecShell(`curl -4s https://ifconfig.io`)).stdout;
} catch (error) {}
try {
ipv6 = await (await asyncExecShell(`curl -6s https://ifconfig.io`)).stdout;
} catch (error) {}
for (const ssl of ssls) { for (const ssl of ssls) {
if (!dev) { if (!dev) {
if ( if (
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 {
console.log('Generating SSL for', ssl.domain); // Checking DNS entry before generating certificate
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify); if (ipv4 || ipv6) {
let domains4 = [];
let domains6 = [];
try {
domains4 = await resolver.resolve4(ssl.domain);
} catch (error) {}
try {
domains6 = await resolver.resolve6(ssl.domain);
} catch (error) {}
if (domains4.length > 0 || domains6.length > 0) {
if (
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
) {
console.log('Generating SSL for', ssl.domain);
return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
} }
} else { } else {
if ( if (
@@ -216,7 +246,27 @@ export async function generateSSLCerts(): Promise<void> {
) { ) {
console.log(`Certificate for ${ssl.domain} already exists`); console.log(`Certificate for ${ssl.domain} already exists`);
} else { } else {
console.log('Generating SSL for', ssl.domain); // Checking DNS entry before generating certificate
if (ipv4 || ipv6) {
let domains4 = [];
let domains6 = [];
try {
domains4 = await resolver.resolve4(ssl.domain);
} catch (error) {}
try {
domains6 = await resolver.resolve6(ssl.domain);
} catch (error) {}
if (domains4.length > 0 || domains6.length > 0) {
if (
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
) {
console.log('Generating SSL for', ssl.domain);
return;
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
} }
} }
} }

View File

@@ -285,7 +285,15 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
networks: [docker.network], networks: [docker.network],
labels, labels,
depends_on: [], depends_on: [],
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

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(
@@ -14,56 +15,23 @@ export default async function (): Promise<void> {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`); await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
} }
} catch (error) { } catch (error) {
console.log(error); //console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`); await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) { } catch (error) {
console.log(error); //console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`); await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (error) { } catch (error) {
console.log(error); //console.log(error);
}
// Cleanup old images older than a day
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
} catch (error) {
//console.log(error);
} }
// Tagging images with labels
// try {
// const images = [
// `coollabsio/${defaultProxyImageTcp}`,
// `coollabsio/${defaultProxyImageHttp}`,
// 'certbot/certbot:latest',
// 'node:16.14.0-alpine',
// 'alpine:latest',
// 'nginx:stable-alpine',
// 'node:lts',
// 'php:apache',
// 'rust:latest'
// ];
// for (const image of images) {
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image inspect ${image}`);
// } catch (error) {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker pull ${image} && echo "FROM ${image}" | docker build --label coolify.image="true" -t "${image}" -`
// );
// }
// }
// } catch (error) {}
// if (!dev) {
// // Cleanup images that are not managed by coolify
// try {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker image prune --filter 'label!=coolify.image=true' -a -f`
// );
// } catch (error) {
// console.log(error);
// }
// // Cleanup old images >3 days
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
// } catch (error) {
// console.log(error);
// }
// }
} }
} }

View File

@@ -2,9 +2,12 @@ import * as Bullmq from 'bullmq';
import { default as ProdBullmq, QueueScheduler } from 'bullmq'; import { default as ProdBullmq, QueueScheduler } from 'bullmq';
import { dev } from '$app/env'; import { dev } from '$app/env';
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
import builder from './builder'; import builder from './builder';
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';
@@ -27,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();
@@ -52,6 +58,16 @@ const cron = async (): Promise<void> => {
} }
); );
new Worker(
'proxyTcpHttp',
async () => {
await proxyTcpHttp();
},
{
...connectionOptions
}
);
new Worker( new Worker(
'ssl', 'ssl',
async () => { async () => {
@@ -83,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 } });
@@ -142,5 +159,9 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
const buildLogQueueName = 'log_queue'; const buildLogQueueName = 'log_queue';
const buildLogQueue = new Queue(buildLogQueueName, connectionOptions); const buildLogQueue = new Queue(buildLogQueueName, connectionOptions);
const buildLogWorker = new Worker(buildLogQueueName, async (job) => await logger(job), {
concurrency: 1,
...connectionOptions
});
export { buildQueue, buildLogQueue }; export { buildQueue, buildLogQueue };

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

@@ -23,6 +23,14 @@ export type ComposeFileService = {
dockerfile: string; dockerfile: string;
args?: Record<string, unknown>; args?: Record<string, unknown>;
}; };
deploy?: {
restart_policy?: {
condition?: string;
delay?: string;
max_attempts?: number;
window?: string;
};
};
}; };
export type ComposerFileVersion = export type ComposerFileVersion =

View File

@@ -36,8 +36,15 @@
}); });
} }
async function loadBranchesByPage(page = 0) {
return await get(`${apiUrl}/repos/${selected.repository}/branches?per_page=100&page=${page}`, {
Authorization: `token ${$gitTokens.githubToken}`
});
}
let reposSelectOptions; let reposSelectOptions;
let branchSelectOptions; let branchSelectOptions;
async function loadRepositories() { async function loadRepositories() {
let page = 1; let page = 1;
let reposCount = 0; let reposCount = 0;
@@ -58,24 +65,28 @@
})); }));
} }
async function loadBranches(event) { async function loadBranches(event) {
branches = [];
selected.repository = event.detail.value; selected.repository = event.detail.value;
loading.branches = true;
selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id; selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try { let page = 1;
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, { let branchCount = 0;
Authorization: `token ${$gitTokens.githubToken}` loading.branches = true;
}); const loadedBranches = await loadBranchesByPage();
branchSelectOptions = branches.map((branch) => ({ branches = branches.concat(loadedBranches);
value: branch.name, branchCount = branches.length;
label: branch.name if (branchCount === 100) {
})); while (branchCount === 100) {
return; page = page + 1;
} catch ({ error }) { const nextBranches = await loadBranchesByPage(page);
return errorNotification(error); branches = branches.concat(nextBranches);
} finally { branchCount = nextBranches.length;
loading.branches = false; }
} }
loading.branches = false;
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
} }
async function isBranchAlreadyUsed(event) { async function isBranchAlreadyUsed(event) {
selected.branch = event.detail.value; selected.branch = event.detail.value;
@@ -166,30 +177,36 @@
{:else} {:else}
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center"> <form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
<div class="flex-col space-y-3 md:space-y-0 space-x-1"> <div class="flex-col space-y-3 md:space-y-0 space-x-1">
<div class="flex gap-4"> <div class="flex-col md:flex gap-4">
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
placeholder={loading.repositories placeholder={loading.repositories
? 'Loading repositories ...' ? 'Loading repositories...'
: 'Please select a repository'} : 'Please select a repository'}
id="repository" id="repository"
showIndicator={true}
isWaiting={loading.repositories}
on:select={loadBranches} on:select={loadBranches}
items={reposSelectOptions} items={reposSelectOptions}
isDisabled={loading.repositories} isDisabled={loading.repositories}
isClearable={false}
/> />
</div> </div>
<input class="hidden" bind:value={selected.projectId} name="projectId" /> <input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
placeholder={loading.branches placeholder={loading.branches
? 'Loading branches ...' ? 'Loading branches...'
: !selected.repository : !selected.repository
? 'Please select a repository first' ? 'Please select a repository first'
: 'Please select a branch'} : 'Please select a branch'}
id="repository" isWaiting={loading.branches}
showIndicator={selected.repository}
id="branches"
on:select={isBranchAlreadyUsed} on:select={isBranchAlreadyUsed}
items={branchSelectOptions} items={branchSelectOptions}
isDisabled={loading.branches || !selected.repository} isDisabled={loading.branches || !selected.repository}
isClearable={false}
/> />
</div> </div>
</div> </div>
@@ -202,13 +219,6 @@
class:bg-orange-600={showSave} class:bg-orange-600={showSave}
class:hover:bg-orange-500={showSave}>Save</button class:hover:bg-orange-500={showSave}>Save</button
> >
<!-- <button class="w-40"
><a
class="no-underline"
href="{apiUrl}/apps/{application.gitSource.githubApp.name}/installations/new"
>Modify Repositories</a
></button
> -->
</div> </div>
</form> </form>
{/if} {/if}

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

@@ -45,7 +45,15 @@ export const post: RequestHandler = async (event) => {
volumes: [volume], volumes: [volume],
ulimits, ulimits,
labels, labels,
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -8,7 +8,6 @@ import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
console.log(teamId);
const { id } = event.params; const { id } = event.params;
try { try {
const destination = await db.getDestination({ id, teamId }); const destination = await db.getDestination({ id, teamId });

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

@@ -32,21 +32,12 @@
export let account; export let account;
export let accounts; export let accounts;
export let invitations;
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?');
@@ -74,12 +65,51 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function acceptInvitation(id, teamId) {
try {
await post(`/iam/team/${teamId}/invitation/accept.json`, { id });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
async function revokeInvitation(id, teamId) {
try {
await post(`/iam/team/${teamId}/invitation/revoke.json`, { id });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div> <div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div>
</div> </div>
{#if invitations.length > 0}
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="title font-bold">Pending invitations</div>
<div class="pt-10 text-center">
{#each invitations as invitation}
<div class="flex justify-center space-x-2">
<div>
Invited to <span class="font-bold text-pink-600">{invitation.teamName}</span> with
<span class="font-bold text-rose-600">{invitation.permission}</span> permission.
</div>
<button
class="hover:bg-green-500"
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
>
<button
class="hover:bg-red-600"
on:click={() => revokeInvitation(invitation.id, invitation.teamId)}>Delete</button
>
</div>
{/each}
</div>
</div>
{/if}
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-4xl px-6 py-4">
{#if $session.teamId === '0' && accounts.length > 0} {#if $session.teamId === '0' && accounts.length > 0}
<div class="title font-bold">Accounts</div> <div class="title font-bold">Accounts</div>
@@ -127,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

@@ -76,10 +76,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>

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

@@ -105,6 +105,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

@@ -90,7 +90,15 @@ export const post: RequestHandler = async (event) => {
environment: config.ghost.environmentVariables, environment: config.ghost.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('ghost'), labels: makeLabelForServices('ghost'),
depends_on: [`${id}-mariadb`] depends_on: [`${id}-mariadb`],
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}, },
[`${id}-mariadb`]: { [`${id}-mariadb`]: {
container_name: `${id}-mariadb`, container_name: `${id}-mariadb`,
@@ -98,7 +106,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
volumes: [config.mariadb.volume], volumes: [config.mariadb.volume],
environment: config.mariadb.environmentVariables, environment: config.mariadb.environmentVariables,
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -43,7 +43,15 @@ export const post: RequestHandler = async (event) => {
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
volumes: [config.volume], volumes: [config.volume],
labels: makeLabelForServices('languagetool') labels: makeLabelForServices('languagetool'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -48,7 +48,15 @@ export const post: RequestHandler = async (event) => {
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
volumes: [config.volume], volumes: [config.volume],
labels: makeLabelForServices('meilisearch') labels: makeLabelForServices('meilisearch'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

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;
@@ -67,7 +62,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
volumes: [config.volume], volumes: [config.volume],
restart: 'always', restart: 'always',
labels: makeLabelForServices('minio') labels: makeLabelForServices('minio'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -44,7 +44,15 @@ export const post: RequestHandler = async (event) => {
volumes: [config.volume], volumes: [config.volume],
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('n8n') labels: makeLabelForServices('n8n'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -40,7 +40,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('nocodb') labels: makeLabelForServices('nocodb'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -133,7 +133,15 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
environment: config.plausibleAnalytics.environmentVariables, environment: config.plausibleAnalytics.environmentVariables,
restart: 'always', restart: 'always',
depends_on: [`${id}-postgresql`, `${id}-clickhouse`], depends_on: [`${id}-postgresql`, `${id}-clickhouse`],
labels: makeLabelForServices('plausibleAnalytics') labels: makeLabelForServices('plausibleAnalytics'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 5,
window: '120s'
}
}
}, },
[`${id}-postgresql`]: { [`${id}-postgresql`]: {
container_name: `${id}-postgresql`, container_name: `${id}-postgresql`,
@@ -141,7 +149,15 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
networks: [network], networks: [network],
environment: config.postgresql.environmentVariables, environment: config.postgresql.environmentVariables,
volumes: [config.postgresql.volume], volumes: [config.postgresql.volume],
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 5,
window: '120s'
}
}
}, },
[`${id}-clickhouse`]: { [`${id}-clickhouse`]: {
build: workdir, build: workdir,
@@ -149,7 +165,15 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
networks: [network], networks: [network],
environment: config.clickhouse.environmentVariables, environment: config.clickhouse.environmentVariables,
volumes: [config.clickhouse.volume], volumes: [config.clickhouse.volume],
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 5,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -42,7 +42,15 @@ export const post: RequestHandler = async (event) => {
volumes: [config.volume], volumes: [config.volume],
environment: config.environmentVariables, environment: config.environmentVariables,
restart: 'always', restart: 'always',
labels: makeLabelForServices('uptimekuma') labels: makeLabelForServices('uptimekuma'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -43,7 +43,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
volumes: [config.volume], volumes: [config.volume],
restart: 'always', restart: 'always',
labels: makeLabelForServices('vaultWarden') labels: makeLabelForServices('vaultWarden'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

View File

@@ -52,7 +52,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
volumes: [config.volume], volumes: [config.volume],
restart: 'always', restart: 'always',
labels: makeLabelForServices('vscodeServer') labels: makeLabelForServices('vscodeServer'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

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

@@ -77,7 +77,15 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
restart: 'always', restart: 'always',
depends_on: [`${id}-mysql`], depends_on: [`${id}-mysql`],
labels: makeLabelForServices('wordpress') labels: makeLabelForServices('wordpress'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}, },
[`${id}-mysql`]: { [`${id}-mysql`]: {
container_name: `${id}-mysql`, container_name: `${id}-mysql`,
@@ -85,7 +93,15 @@ export const post: RequestHandler = async (event) => {
volumes: [config.mysql.volume], volumes: [config.mysql.volume],
environment: config.mysql.environmentVariables, environment: config.mysql.environmentVariables,
networks: [network], networks: [network],
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {

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 {

View File

@@ -2,6 +2,7 @@
export let source; export let source;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { post } from '$lib/api'; import { post } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params; const { id } = $page.params;
@@ -51,7 +52,8 @@
type: 'github', type: 'github',
name: source.name, name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''), htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, '') apiUrl: source.apiUrl.replace(/\/$/, ''),
organization: source.organization
}); });
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
@@ -97,6 +99,22 @@
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label> <label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} /> <input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</div> </div>
<div class="grid grid-cols-2">
<div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
>Organization</label
>
<Explainer
text="Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."
/>
</div>
<input
name="organization"
id="organization"
placeholder="eg: coollabsio"
bind:value={source.organization}
/>
</div>
</div> </div>
{#if source.apiUrl && source.htmlUrl && source.name} {#if source.apiUrl && source.htmlUrl && source.name}
<div class="text-center"> <div class="text-center">
@@ -135,6 +153,21 @@
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label> <label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} /> <input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</div> </div>
<div class="grid grid-cols-2">
<div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
>Organization</label
>
</div>
<input
readonly
disabled
name="organization"
id="organization"
placeholder="eg: coollabsio"
bind:value={source.organization}
/>
</div>
</div> </div>
</form> </form>
{:else} {:else}

View File

@@ -9,8 +9,8 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
try { try {
let { type, name, htmlUrl, apiUrl } = await event.request.json(); let { type, name, htmlUrl, apiUrl, organization } = await event.request.json();
await db.addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }); await db.addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -36,7 +36,6 @@
export let settings; export let settings;
onMount(() => { onMount(() => {
const { organization, id, htmlUrl } = source; const { organization, id, htmlUrl } = source;
console.log(source);
const { fqdn } = settings; const { fqdn } = settings;
const host = dev const host = dev
? 'http://localhost:3000' ? 'http://localhost:3000'

View File

@@ -50,7 +50,10 @@ textarea {
} }
#svelte .listContainer { #svelte .listContainer {
@apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-coollabs scrollbar-track-coolgray-200; @apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200;
}
#svelte .selectedItem {
@apply pl-3;
} }
#svelte .item.hover { #svelte .item.hover {