Compare commits

..

92 Commits

Author SHA1 Message Date
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
Andras Bacsai
ed5f21da6a Merge pull request #335 from coollabsio/arm
v2.4.3 - ARM!
2022-04-12 09:10:57 +02:00
Andras Bacsai
78f3eb81dd Merge pull request #314 from Mobilpadde/fix-coloured-tooltips
Tooltip with corresponding colours
2022-04-12 07:57:09 +02:00
Andras Bacsai
6a833934ce Merge pull request #293 from dominicbachmann/improve-typing
Started to introduce more typing
2022-04-11 22:40:47 +02:00
Andras Bacsai
45bf6f77d1 Merge branch 'arm' into improve-typing 2022-04-11 22:39:45 +02:00
Andras Bacsai
a1b3b7b687 Merge branch 'arm' of github.com:coollabsio/coolify into arm 2022-04-11 22:31:32 +02:00
Andras Bacsai
7ebcad6abb fix: Update dockerfile 2022-04-11 22:31:27 +02:00
Andras Bacsai
fed6d2bf07 Merge pull request #301 from esdete2/main
Rearrange ARGs in Docker build pack
2022-04-11 22:31:16 +02:00
Andras Bacsai
bea4943e9f chore: update build packages 2022-04-11 20:43:19 +02:00
Andras Bacsai
1979e431b8 chore: update build scripts 2022-04-11 20:40:06 +02:00
Andras Bacsai
9bead1d6b4 chore: Version++ 2022-04-11 20:36:46 +02:00
Andras Bacsai
56c4295e16 chore: Update packages 2022-04-11 20:36:15 +02:00
Andras Bacsai
7c7b5a61e5 fix: Remove unnecessary save button haha 2022-04-11 20:36:03 +02:00
Andras Bacsai
abaa13fda8 Merge branch 'main' into arm 2022-04-11 20:29:29 +02:00
esdete
042bfeddbb Merge branch 'main' into main 2022-04-11 17:47:50 +02:00
Mads Bram Cordes
f45ab067ce Add fuchsia for IAM 2022-04-11 16:58:00 +02:00
Mads Bram Cordes
97a6f04aaa Merge branch 'main' into fix-coloured-tooltips 2022-04-11 16:55:37 +02:00
Andras Bacsai
417c01d6e0 Merge pull request #331 from coollabsio/v2.4.2
v2.4.2
2022-04-10 00:44:22 +02:00
Andras Bacsai
b2e7435d0f chore: version++ 2022-04-10 00:40:12 +02:00
Andras Bacsai
73c9cb1d51 Revert source configuration changes 2022-04-10 00:39:50 +02:00
Andras Bacsai
41c5dd3b53 fix: Show config missing on sources 2022-04-10 00:36:42 +02:00
Andras Bacsai
bb0c93dc2f fix: Return own and other sources better 2022-04-10 00:31:10 +02:00
Andras Bacsai
7953c1df30 fix: Missing install repositories GitHub 2022-04-10 00:30:47 +02:00
esdete
c3f4245164 Merge branch 'main' into main 2022-04-09 15:44:13 +02:00
esdete
157e5fd7aa Merge branch 'main' into main 2022-04-08 20:07:43 +02:00
Mads Bram Cordes
039953588e Add tooltip colours to correspond with colour of Icon 2022-04-08 00:11:30 +02:00
dominicbachmann
9da08d600b Merged v2.4.0 2022-04-07 01:03:13 +02:00
dominicbachmann
be41c0dd02 Added types for store 2022-04-06 21:51:19 +02:00
dominicbachmann
a17b7a564e Added types for form 2022-04-06 21:49:43 +02:00
dominicbachmann
de37ee9f1c Added types for crypto 2022-04-06 21:10:37 +02:00
dominicbachmann
8212868b92 Added types for api 2022-04-06 21:09:15 +02:00
dominicbachmann
b44d8578d9 Added types for queues/sslrenewal 2022-04-06 21:05:36 +02:00
dominicbachmann
0358cf2de2 Added types for queues/ssl 2022-04-06 21:05:12 +02:00
dominicbachmann
94da008a47 Added types for queues/proxy 2022-04-06 21:04:51 +02:00
dominicbachmann
456b1b8074 Added types for queues/logger 2022-04-06 21:04:14 +02:00
dominicbachmann
78e6a7d1d3 Improved code quality of queues/index 2022-04-06 21:03:20 +02:00
dominicbachmann
76dc7ffb68 Added types for queues/cleanup 2022-04-06 21:01:47 +02:00
dominicbachmann
211aff7170 Added types for letsencrypt/index 2022-04-06 20:52:46 +02:00
dominicbachmann
bcacefb841 Added types for importers/gitlab 2022-04-06 20:50:57 +02:00
dominicbachmann
4505ad37d8 Added types for importers/github 2022-04-06 20:50:04 +02:00
dominicbachmann
18cf57f33c Added types for haproxy/index 2022-04-06 20:47:22 +02:00
dominicbachmann
8a401f50cb Added types for haproxy/configuration 2022-04-06 20:40:25 +02:00
dominicbachmann
51a5b3b602 Added types to database/users 2022-04-06 20:36:51 +02:00
dominicbachmann
68f9bca054 Added types to database/teams 2022-04-06 20:34:22 +02:00
dominicbachmann
e9e92c6e9e Added types to databse/settings 2022-04-06 20:31:51 +02:00
dominicbachmann
008cfdba09 Added types to database/services 2022-04-06 20:30:29 +02:00
dominicbachmann
9973197fa5 Added types for database/secrets 2022-04-06 20:23:27 +02:00
dominicbachmann
ec3b94cf96 added types for database/logs 2022-04-06 20:16:21 +02:00
dominicbachmann
c4cb92c78d Added types for database/gitSource 2022-04-06 20:15:15 +02:00
dominicbachmann
c390f82246 Added types to database/gitlab 2022-04-06 20:01:35 +02:00
dominicbachmann
b4f98e24a1 Added types to database/github 2022-04-06 19:56:47 +02:00
dominicbachmann
e042c5cfde Added types for database/databases 2022-04-06 19:45:47 +02:00
dominicbachmann
faeae8fd6c Added typings for database/destinations 2022-04-06 19:34:17 +02:00
Philip Schmidt
fd652bfce6 write args at the beginning of dockerfile and inherit them for each stage 2022-04-06 18:33:02 +02:00
dominicbachmann
82f7633c3a Improved typing and quality of database/checks and database/common code 2022-04-05 21:15:02 +02:00
dominicbachmann
9fdac2741a Improved typing and quality of applications.ts 2022-04-05 20:48:33 +02:00
dominicbachmann
8fb5260809 Resolved merge conflicts 2022-04-05 20:17:53 +02:00
dominicbachmann
e08ec12d26 Introduced typing for the buildJob and cleaned up common.ts 2022-04-05 20:11:19 +02:00
Andras Bacsai
1b43976ff0 Update proxy build commands 2022-04-02 13:39:24 +02:00
Andras Bacsai
321fb019eb Update dockerfiles for arm 2022-04-01 23:02:23 +02:00
Andras Bacsai
f6858a68e0 Update schema 2022-04-01 22:51:08 +02:00
Andras Bacsai
fe17e2eaba Prisma Engine build script 2022-04-01 17:57:37 +02:00
Andras Bacsai
22ef0b5d29 Update packages 2022-04-01 17:46:08 +02:00
Andras Bacsai
823279fb60 Updates 2022-04-01 17:16:11 +02:00
Andras Bacsai
f56361c0ca updates for ARM 2022-04-01 14:25:55 +02:00
Andras Bacsai
4946ca2d91 Dockerfile for multiarch builds 2022-04-01 00:08:29 +02:00
70 changed files with 1679 additions and 1107 deletions

View File

@@ -1,31 +1,42 @@
FROM node:16.14.0-alpine
RUN apk add --no-cache g++ cmake make python3
WORKDIR /app
COPY package*.json .
RUN yarn install
COPY . .
RUN yarn build
FROM node:16.14.0-alpine
FROM node:16.14.2-alpine as install
WORKDIR /app
LABEL coolify.managed true
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl
RUN apk add --no-cache curl
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6
RUN pnpm add -g pnpm
RUN curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-20.10.9.tgz" | tar -xzvf - docker/docker -C . --strip-components 1 && mv docker /usr/bin/docker
RUN mkdir -p ~/.docker/cli-plugins/
RUN curl -SL https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose
COPY package*.json .
RUN pnpm install
FROM node:16.14.2-alpine
ARG TARGETPLATFORM
WORKDIR /app
ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/app/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary
COPY --from=coollabsio/prisma-engine:latest /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/
COPY --from=install /app/node_modules ./node_modules
COPY . .
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6
RUN pnpm add -g pnpm
RUN mkdir -p ~/.docker/cli-plugins/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.3.4 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN pnpm prisma generate
RUN pnpm build
COPY --from=0 /app/docker-compose.yaml .
COPY --from=0 /app/build .
COPY --from=0 /app/package.json .
COPY --from=0 /app/node_modules ./node_modules
COPY --from=0 /app/prisma ./prisma
EXPOSE 3000
CMD ["pnpm", "start"]

View File

@@ -0,0 +1 @@
nohup docker build -t coollabsio/prisma-engine:<arm64/amd64> --push . &

View File

@@ -4,10 +4,10 @@ global
defaults
mode http
log global
timeout http-request 60s
timeout connect 10s
timeout client 60s
timeout server 60s
timeout http-request 120s
timeout connect 20s
timeout client 120s
timeout server 120s
frontend "${APP}"
mode http

View File

@@ -5,10 +5,10 @@ global
defaults
mode http
log global
timeout http-request 60s
timeout connect 10s
timeout client 60s
timeout server 60s
timeout http-request 120s
timeout connect 20s
timeout client 120s
timeout server 120s
userlist haproxy-dataplaneapi
user admin insecure-password "${HAPROXY_PASSWORD}"

View File

@@ -0,0 +1,10 @@
FROM rust:1.58.1-alpine3.14 as prisma
WORKDIR /prisma
ENV RUSTFLAGS="-C target-feature=-crt-static"
RUN apk --no-cache add openssl direnv git musl-dev openssl-dev build-base perl protoc
RUN git clone --depth=1 --branch=3.11.x https://github.com/prisma/prisma-engines.git /prisma
RUN cargo build --release
FROM alpine
WORKDIR /prisma-engines
COPY --from=prisma /prisma/target/release/query-engine /prisma/target/release/migration-engine /prisma/target/release/introspection-engine /prisma/target/release/prisma-fmt /prisma-engines/

View File

@@ -1,14 +1,14 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.4.1",
"version": "2.4.7",
"license": "AGPL-3.0",
"scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",
"dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio",
"start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js",
"start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node build/index.js",
"build": "svelte-kit build",
"preview": "svelte-kit preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
@@ -17,18 +17,18 @@
"db:push": "prisma db push && prisma generate",
"db:seed": "prisma db seed",
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"release:staging": "cross-var docker build -t coollabsio/coolify:$npm_package_version . && docker push coollabsio/coolify:$npm_package_version",
"release:pre": "cross-var docker build -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest .",
"release:coolify": "cross-var yarn release:pre && docker push coollabsio/coolify:$npm_package_version && docker push coollabsio/coolify:latest",
"release:haproxy": "docker build -f haproxy.Dockerfile -t coollabsio/coolify-haproxy-alpine:1.0.0 -t coollabsio/coolify-haproxy-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-alpine",
"release:haproxy:tcp": "docker build -f haproxy-tcp.Dockerfile -t coollabsio/coolify-haproxy-tcp-alpine:1.0.0 -t coollabsio/coolify-haproxy-tcp-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-tcp-alpine",
"release:haproxy:http": "docker build -f haproxy-http.Dockerfile -t coollabsio/coolify-haproxy-http-alpine:1.0.0 -t coollabsio/coolify-haproxy-http-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-http-alpine",
"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: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 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 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 data/haproxy-http.Dockerfile --push .",
"prepare": "husky install"
},
"devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.73",
"@sveltejs/kit": "1.0.0-next.303",
"@types/bcrypt": "5.0.0",
"@sveltejs/kit": "1.0.0-next.310",
"@types/js-cookie": "3.0.1",
"@types/js-yaml": "4.0.5",
"@types/node": "17.0.23",
@@ -45,13 +45,13 @@
"husky": "7.0.4",
"lint-staged": "12.3.7",
"postcss": "8.4.12",
"prettier": "2.6.1",
"prettier-plugin-svelte": "2.6.0",
"prettier": "2.6.2",
"prettier-plugin-svelte": "2.7.0",
"prettier-plugin-tailwindcss": "0.1.8",
"prisma": "3.11.1",
"svelte": "3.46.4",
"svelte-check": "2.4.6",
"svelte-preprocess": "4.10.4",
"svelte": "3.47.0",
"svelte-check": "2.6.0",
"svelte-preprocess": "4.10.5",
"svelte-select": "4.4.7",
"tailwindcss": "3.0.23",
"ts-node": "10.7.0",
@@ -62,24 +62,23 @@
"dependencies": {
"@iarna/toml": "2.2.5",
"@prisma/client": "3.11.1",
"@sentry/node": "6.19.2",
"bcrypt": "5.0.1",
"bullmq": "1.78.1",
"@sentry/node": "6.19.6",
"bcryptjs": "^2.4.3",
"bullmq": "1.80.0",
"compare-versions": "4.1.3",
"cookie": "0.4.2",
"cooltipz-css": "2.1.0",
"cuid": "2.1.8",
"dayjs": "1.11.0",
"dockerode": "3.3.1",
"dotenv-extended": "2.9.0",
"generate-password": "1.7.0",
"get-port": "6.1.2",
"got": "12.0.2",
"got": "12.0.3",
"js-cookie": "3.0.1",
"js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1",
"mustache": "4.2.0",
"node-forge": "1.3.0",
"node-forge": "1.3.1",
"p-limit": "4.0.0",
"svelte-kit-cookie-session": "2.1.2",
"tailwindcss-scrollbar": "0.1.0",

609
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["linux-musl"]
}
datasource db {

View File

@@ -1,9 +1,15 @@
async function send({ method, path, data = {}, headers, timeout = 30000 }) {
async function send({
method,
path,
data = {},
headers,
timeout = 120000
}): Promise<Record<string, unknown>> {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const opts = { method, headers: {}, body: null, signal: controller.signal };
if (Object.keys(data).length > 0) {
let parsedData = data;
const parsedData = data;
for (const [key, value] of Object.entries(data)) {
if (value === '') {
parsedData[key] = null;
@@ -43,18 +49,33 @@ async function send({ method, path, data = {}, headers, timeout = 30000 }) {
return responseData;
}
export function get(path, headers = {}): Promise<any> {
export function get(
path: string,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'GET', path, headers });
}
export function del(path, data = {}, headers = {}): Promise<any> {
export function del(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'DELETE', path, data, headers });
}
export function post(path, data, headers = {}): Promise<any> {
export function post(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'POST', path, data, headers });
}
export function put(path, data, headers = {}): Promise<any> {
export function put(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'PUT', path, data, headers });
}

View File

@@ -26,14 +26,17 @@ export default async function ({
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
if (
(pullmergeRequestId && secret.isPRMRSecret) ||
(!pullmergeRequestId && !secret.isPRMRSecret)
) {
Dockerfile.unshift(`ARG ${secret.name}=${secret.value}`);
Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `ARG ${secret.name}`);
}
});
}
}
});

View File

@@ -12,7 +12,8 @@ import { version as currentVersion } from '../../package.json';
import dayjs from 'dayjs';
import Cookie from 'cookie';
import os from 'os';
import cuid from 'cuid';
import type { RequestEvent } from '@sveltejs/kit/types/internal';
import type { Job } from 'bullmq';
try {
if (!dev) {
@@ -45,13 +46,21 @@ const customConfig: Config = {
export const version = currentVersion;
export const asyncExecShell = util.promisify(child.exec);
export const asyncSleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
export const asyncSleep = (delay: number): Promise<unknown> =>
new Promise((resolve) => setTimeout(resolve, delay));
export const sentry = Sentry;
export const uniqueName = () => uniqueNamesGenerator(customConfig);
export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
export const saveBuildLog = async ({ line, buildId, applicationId }) => {
export const saveBuildLog = async ({
line,
buildId,
applicationId
}: {
line: string;
buildId: string;
applicationId: string;
}): Promise<Job> => {
if (line) {
if (line.includes('ghs_')) {
const regex = /ghs_.*@/g;
@@ -62,20 +71,7 @@ export const saveBuildLog = async ({ line, buildId, applicationId }) => {
}
};
export const isTeamIdTokenAvailable = (request) => {
const cookie = request.headers.cookie
?.split(';')
.map((s) => s.trim())
.find((s) => s.startsWith('teamId='))
?.split('=')[1];
if (!cookie) {
return getTeam(request);
} else {
return cookie;
}
};
export const getTeam = (event) => {
export const getTeam = (event: RequestEvent): string | null => {
const cookies = Cookie.parse(event.request.headers.get('cookie'));
if (cookies?.teamId) {
return cookies.teamId;
@@ -85,14 +81,28 @@ export const getTeam = (event) => {
return null;
};
export const getUserDetails = async (event, isAdminRequired = true) => {
export const getUserDetails = async (
event: RequestEvent,
isAdminRequired = true
): Promise<{
teamId: string;
userId: string;
permission: string;
status: number;
body: { message: string };
}> => {
const teamId = getTeam(event);
const userId = event?.locals?.session?.data?.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({
where: { teamId, userId },
select: { permission: true },
rejectOnNotFound: true
});
let permission = 'read';
if (teamId && userId) {
const data = await db.prisma.permission.findFirst({
where: { teamId, userId },
select: { permission: true },
rejectOnNotFound: true
});
if (data.permission) permission = data.permission;
}
const payload = {
teamId,
userId,
@@ -112,11 +122,11 @@ export const getUserDetails = async (event, isAdminRequired = true) => {
return payload;
};
export function getEngine(engine) {
export function getEngine(engine: string): string {
return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine;
}
export async function removeContainer(id, engine) {
export async function removeContainer(id: string, engine: string): Promise<void> {
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
@@ -132,11 +142,23 @@ export async function removeContainer(id, engine) {
}
}
export const removeDestinationDocker = async ({ id, engine }) => {
export const removeDestinationDocker = async ({
id,
engine
}: {
id: string;
engine: string;
}): Promise<void> => {
return await removeContainer(id, engine);
};
export const createDirectories = async ({ repository, buildId }) => {
export const createDirectories = async ({
repository,
buildId
}: {
repository: string;
buildId: string;
}): Promise<{ workdir: string; repodir: string }> => {
const repodir = `/tmp/build-sources/${repository}/`;
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
@@ -148,20 +170,10 @@ export const createDirectories = async ({ repository, buildId }) => {
};
};
export function generateTimestamp() {
export function generateTimestamp(): string {
return `${dayjs().format('HH:mm:ss.SSS')} `;
}
export function getDomain(domain) {
export function getDomain(domain: string): string {
return domain?.replace('https://', '').replace('http://', '');
}
export function dashify(str: string, options?: any): string {
if (typeof str !== 'string') return str;
return str
.trim()
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
}

View File

@@ -1,13 +1,13 @@
import crypto from 'crypto';
const algorithm = 'aes-256-ctr';
export const base64Encode = (text: string) => {
export const base64Encode = (text: string): string => {
return Buffer.from(text).toString('base64');
};
export const base64Decode = (text: string) => {
export const base64Decode = (text: string): string => {
return Buffer.from(text, 'base64').toString('ascii');
};
export const encrypt = (text: string) => {
export const encrypt = (text: string): string => {
if (text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
@@ -19,7 +19,7 @@ export const encrypt = (text: string) => {
}
};
export const decrypt = (hashString: string) => {
export const decrypt = (hashString: string): string => {
if (hashString) {
const hash: Hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv(

View File

@@ -1,10 +1,19 @@
import { decrypt, encrypt } from '$lib/crypto';
import { asyncExecShell, getEngine } from '$lib/common';
import { getDomain, removeDestinationDocker } from '$lib/common';
import { removeDestinationDocker } from '$lib/common';
import { prisma } from './common';
export async function listApplications(teamId) {
import type {
DestinationDocker,
GitSource,
Secret,
ApplicationSettings,
Application,
ApplicationPersistentStorage
} from '@prisma/client';
export async function listApplications(teamId: string): Promise<Application[]> {
if (teamId === '0') {
return await prisma.application.findMany({ include: { teams: true } });
}
@@ -14,7 +23,13 @@ export async function listApplications(teamId) {
});
}
export async function newApplication({ name, teamId }) {
export async function newApplication({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Application> {
return await prisma.application.create({
data: {
name,
@@ -24,34 +39,17 @@ export async function newApplication({ name, teamId }) {
});
}
export async function importApplication({
name,
teamId,
fqdn,
port,
buildCommand,
startCommand,
installCommand
}) {
return await prisma.application.create({
data: {
name,
fqdn,
port,
buildCommand,
startCommand,
installCommand,
teams: { connect: { id: teamId } }
}
});
}
export async function removeApplication({ id, teamId }) {
const { fqdn, destinationDockerId, destinationDocker } = await prisma.application.findUnique({
export async function removeApplication({
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<void> {
const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
const domain = getDomain(fqdn);
if (destinationDockerId) {
const host = getEngine(destinationDocker.engine);
const { stdout: containers } = await asyncExecShell(
@@ -62,7 +60,6 @@ export async function removeApplication({ id, teamId }) {
for (const container of containersArray) {
const containerObj = JSON.parse(container);
const id = containerObj.ID;
const preview = containerObj.Image.split('-')[1];
await removeDestinationDocker({ id, engine: destinationDocker.engine });
}
}
@@ -80,9 +77,23 @@ export async function removeApplication({ id, teamId }) {
}
}
export async function getApplicationWebhook({ projectId, branch }) {
export async function getApplicationWebhook({
projectId,
branch
}: {
projectId: number;
branch: string;
}): Promise<
Application & {
destinationDocker: DestinationDocker;
settings: ApplicationSettings;
gitSource: GitSource;
secrets: Secret[];
persistentStorage: ApplicationPersistentStorage[];
}
> {
try {
let application = await prisma.application.findFirst({
const application = await prisma.application.findFirst({
where: { projectId, branch, settings: { autodeploy: true } },
include: {
destinationDocker: true,
@@ -131,16 +142,17 @@ export async function getApplicationWebhook({ projectId, branch }) {
throw { status: 404, body: { message: e.message } };
}
}
export async function getApplicationById({ id }) {
const body = await prisma.application.findFirst({
where: { id },
include: { destinationDocker: true }
});
return { ...body };
}
export async function getApplication({ id, teamId }) {
let body = {};
export async function getApplication({ id, teamId }: { id: string; teamId: string }): Promise<
Application & {
destinationDocker: DestinationDocker;
settings: ApplicationSettings;
gitSource: GitSource;
secrets: Secret[];
persistentStorage: ApplicationPersistentStorage[];
}
> {
let body;
if (teamId === '0') {
body = await prisma.application.findFirst({
where: { id },
@@ -194,7 +206,14 @@ export async function configureGitRepository({
projectId,
webhookToken,
autodeploy
}) {
}: {
id: string;
repository: string;
branch: string;
projectId: number;
webhookToken: string;
autodeploy: boolean;
}): Promise<void> {
if (webhookToken) {
const encryptedWebhookToken = encrypt(webhookToken);
await prisma.application.update({
@@ -224,7 +243,10 @@ export async function configureGitRepository({
}
}
export async function configureBuildPack({ id, buildPack }) {
export async function configureBuildPack({
id,
buildPack
}: Pick<Application, 'id' | 'buildPack'>): Promise<Application> {
return await prisma.application.update({ where: { id }, data: { buildPack } });
}
@@ -242,7 +264,21 @@ export async function configureApplication({
pythonWSGI,
pythonModule,
pythonVariable
}) {
}: {
id: string;
buildPack: string;
name: string;
fqdn: string;
port: number;
installCommand: string;
buildCommand: string;
startCommand: string;
baseDirectory: string;
publishDirectory: string;
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
}): Promise<Application> {
return await prisma.application.update({
where: { id },
data: {
@@ -262,11 +298,24 @@ export async function configureApplication({
});
}
export async function checkDoubleBranch(branch, projectId) {
export async function checkDoubleBranch(branch: string, projectId: number): Promise<boolean> {
const applications = await prisma.application.findMany({ where: { branch, projectId } });
return applications.length > 1;
}
export async function setApplicationSettings({ id, debug, previews, dualCerts, autodeploy }) {
export async function setApplicationSettings({
id,
debug,
previews,
dualCerts,
autodeploy
}: {
id: string;
debug: boolean;
previews: boolean;
dualCerts: boolean;
autodeploy: boolean;
}): Promise<Application & { destinationDocker: DestinationDocker }> {
return await prisma.application.update({
where: { id },
data: { settings: { update: { debug, previews, dualCerts, autodeploy } } },
@@ -274,29 +323,6 @@ export async function setApplicationSettings({ id, debug, previews, dualCerts, a
});
}
export async function createBuild({
id,
applicationId,
destinationDockerId,
gitSourceId,
githubAppId,
gitlabAppId,
type
}) {
return await prisma.build.create({
data: {
id,
applicationId,
destinationDockerId,
gitSourceId,
githubAppId,
gitlabAppId,
status: 'running',
type
}
});
}
export async function getPersistentStorage(id) {
export async function getPersistentStorage(id: string): Promise<ApplicationPersistentStorage[]> {
return await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } });
}

View File

@@ -1,7 +1,16 @@
import { getDomain } from '$lib/common';
import { prisma } from './common';
import type { Application, ServiceSecret, DestinationDocker, Secret } from '@prisma/client';
export async function isBranchAlreadyUsed({ repository, branch, id }) {
export async function isBranchAlreadyUsed({
repository,
branch,
id
}: {
id: string;
repository: string;
branch: string;
}): Promise<Application> {
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: true }
@@ -11,18 +20,42 @@ export async function isBranchAlreadyUsed({ repository, branch, id }) {
});
}
export async function isDockerNetworkExists({ network }) {
export async function isDockerNetworkExists({
network
}: {
network: string;
}): Promise<DestinationDocker> {
return await prisma.destinationDocker.findFirst({ where: { network } });
}
export async function isServiceSecretExists({ id, name }) {
export async function isServiceSecretExists({
id,
name
}: {
id: string;
name: string;
}): Promise<ServiceSecret> {
return await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
}
export async function isSecretExists({ id, name, isPRMRSecret }) {
export async function isSecretExists({
id,
name,
isPRMRSecret
}: {
id: string;
name: string;
isPRMRSecret: boolean;
}): Promise<Secret> {
return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
}
export async function isDomainConfigured({ id, fqdn }) {
export async function isDomainConfigured({
id,
fqdn
}: {
id: string;
fqdn: string;
}): Promise<boolean> {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace('www.', '');
const foundApp = await prisma.application.findFirst({
@@ -55,6 +88,5 @@ export async function isDomainConfigured({ id, fqdn }) {
},
select: { fqdn: true }
});
if (foundApp || foundService || coolifyFqdn) return true;
return false;
return !!(foundApp || foundService || coolifyFqdn);
}

View File

@@ -6,11 +6,11 @@ import {
} from '$lib/components/common';
import * as Prisma from '@prisma/client';
import { default as ProdPrisma } from '@prisma/client';
import type { PrismaClientOptions } from '@prisma/client/runtime';
import type { Database, DatabaseSettings } from '@prisma/client';
import generator from 'generate-password';
import forge from 'node-forge';
export function generatePassword(length = 24) {
export function generatePassword(length = 24): string {
return generator.generate({
length,
numbers: true,
@@ -30,8 +30,14 @@ export const prisma = new PrismaClient({
rejectOnNotFound: false
});
export function ErrorHandler(e) {
if (e! instanceof Error) {
export function ErrorHandler(e: {
stdout?;
message?: string;
status?: number;
name?: string;
error?: string;
}): { status: number; body: { message: string; error: string } } {
if (e && e instanceof Error) {
e = new Error(e.toString());
}
let truncatedError = e;
@@ -39,8 +45,7 @@ export function ErrorHandler(e) {
truncatedError = e.stdout;
}
if (e.message?.includes('docker run')) {
let truncatedArray = [];
truncatedArray = truncatedError.message.split('-').filter((line) => {
const truncatedArray: string[] = truncatedError.message.split('-').filter((line) => {
if (!line.startsWith('e ')) {
return line;
}
@@ -68,11 +73,11 @@ export function ErrorHandler(e) {
payload.body.message = 'Already exists. Choose another name.';
}
}
// console.error(e)
return payload;
}
export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> {
return await new Promise(async (resolve, reject) => {
return await new Promise((resolve, reject) => {
forge.pki.rsa.generateKeyPair({ bits: 4096, workers: -1 }, function (err, keys) {
if (keys) {
resolve({
@@ -86,35 +91,93 @@ export async function generateSshKeyPair(): Promise<{ publicKey: string; private
});
}
export function getVersions(type) {
export function getVersions(type: string): string[] {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.versions;
}
return [];
}
export function getDatabaseImage(type) {
export function getDatabaseImage(type: string): string {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.baseImage;
}
return '';
}
export function getServiceImage(type) {
export function getServiceImage(type: string): string {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.baseImage;
}
return '';
}
export function getServiceImages(type) {
export function getServiceImages(type: string): string[] {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.images;
}
return [];
}
export function generateDatabaseConfiguration(database) {
export function generateDatabaseConfiguration(database: Database & { settings: DatabaseSettings }):
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MYSQL_DATABASE: string;
MYSQL_PASSWORD: string;
MYSQL_ROOT_USER: string;
MYSQL_USER: string;
MYSQL_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MONGODB_ROOT_USER: string;
MONGODB_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
REDIS_AOF_ENABLED: string;
REDIS_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
COUCHDB_PASSWORD: string;
COUCHDB_USER: string;
};
} {
const {
id,
dbUser,
@@ -129,7 +192,6 @@ export function generateDatabaseConfiguration(database) {
const baseImage = getDatabaseImage(type);
if (type === 'mysql') {
return {
// url: `mysql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 3306}/${defaultDatabase}`,
privatePort: 3306,
environmentVariables: {
MYSQL_USER: dbUser,
@@ -144,7 +206,6 @@ export function generateDatabaseConfiguration(database) {
};
} else if (type === 'mongodb') {
return {
// url: `mongodb://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 27017}/${defaultDatabase}`,
privatePort: 27017,
environmentVariables: {
MONGODB_ROOT_USER: rootUser,
@@ -156,10 +217,8 @@ export function generateDatabaseConfiguration(database) {
};
} else if (type === 'postgresql') {
return {
// url: `psql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5432}/${defaultDatabase}`,
privatePort: 5432,
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
POSTGRESQL_PASSWORD: dbUserPassword,
POSTGRESQL_USERNAME: dbUser,
POSTGRESQL_DATABASE: defaultDatabase
@@ -170,7 +229,6 @@ export function generateDatabaseConfiguration(database) {
};
} else if (type === 'redis') {
return {
// url: `redis://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 6379}/${defaultDatabase}`,
privatePort: 6379,
environmentVariables: {
REDIS_PASSWORD: dbUserPassword,
@@ -182,7 +240,6 @@ export function generateDatabaseConfiguration(database) {
};
} else if (type === 'couchdb') {
return {
// url: `couchdb://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5984}/${defaultDatabase}`,
privatePort: 5984,
environmentVariables: {
COUCHDB_PASSWORD: dbUserPassword,
@@ -193,18 +250,4 @@ export function generateDatabaseConfiguration(database) {
ulimits: {}
};
}
// } else if (type === 'clickhouse') {
// return {
// url: `clickhouse://${dbUser}:${dbUserPassword}@${id}:${port}/${defaultDatabase}`,
// privatePort: 9000,
// image: `bitnami/clickhouse-server:${version}`,
// volume: `${id}-${type}-data:/var/lib/clickhouse`,
// ulimits: {
// nofile: {
// soft: 262144,
// hard: 262144
// }
// }
// }
// }
}

View File

@@ -1,12 +1,11 @@
import { decrypt, encrypt } from '$lib/crypto';
import * as db from '$lib/database';
import cuid from 'cuid';
import { generatePassword } from '.';
import { prisma, ErrorHandler } from './common';
import getPort, { portNumbers } from 'get-port';
import { prisma } from './common';
import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
import type { Database, DatabaseSettings, DestinationDocker } from '@prisma/client';
export async function listDatabases(teamId) {
export async function listDatabases(teamId: string): Promise<Database[]> {
if (teamId === '0') {
return await prisma.database.findMany({ include: { teams: true } });
} else {
@@ -16,7 +15,14 @@ export async function listDatabases(teamId) {
});
}
}
export async function newDatabase({ name, teamId }) {
export async function newDatabase({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Database> {
const dbUser = cuid();
const dbUserPassword = encrypt(generatePassword());
const rootUser = cuid();
@@ -37,8 +43,14 @@ export async function newDatabase({ name, teamId }) {
});
}
export async function getDatabase({ id, teamId }) {
let body = {};
export async function getDatabase({
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<Database & { destinationDocker: DestinationDocker; settings: DatabaseSettings }> {
let body;
if (teamId === '0') {
body = await prisma.database.findFirst({
where: { id },
@@ -50,20 +62,25 @@ export async function getDatabase({ id, teamId }) {
include: { destinationDocker: true, settings: true }
});
}
if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword);
if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword);
return { ...body };
return body;
}
export async function removeDatabase({ id }) {
export async function removeDatabase({ id }: { id: string }): Promise<void> {
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
return;
}
export async function configureDatabaseType({ id, type }) {
export async function configureDatabaseType({
id,
type
}: {
id: string;
type: string;
}): Promise<Database> {
return await prisma.database.update({
where: { id },
data: { type }
@@ -79,7 +96,7 @@ export async function setDatabase({
version?: string;
isPublic?: boolean;
appendOnly?: boolean;
}) {
}): Promise<Database> {
return await prisma.database.update({
where: { id },
data: {
@@ -97,7 +114,16 @@ export async function updateDatabase({
rootUser,
rootUserPassword,
version
}) {
}: {
id: string;
name: string;
defaultDatabase: string;
dbUser: string;
dbUserPassword: string;
rootUser: string;
rootUserPassword: string;
version: string;
}): Promise<Database> {
const encryptedDbUserPassword = dbUserPassword && encrypt(dbUserPassword);
const encryptedRootUserPassword = rootUserPassword && encrypt(rootUserPassword);
return await prisma.database.update({
@@ -114,7 +140,9 @@ export async function updateDatabase({
});
}
export async function stopDatabase(database) {
export async function stopDatabase(
database: Database & { destinationDocker: DestinationDocker }
): Promise<boolean> {
let everStarted = false;
const {
id,

View File

@@ -1,11 +1,22 @@
import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import { dockerInstance } from '$lib/docker';
import { startCoolifyProxy } from '$lib/haproxy';
import { getDatabaseImage } from '.';
import { prisma } from './common';
import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client';
import type { CreateDockerDestination } from '$lib/types/destinations';
export async function listDestinations(teamId) {
type DestinationConfigurationObject = {
id: string;
destinationId: string;
};
type FindDestinationFromTeam = {
id: string;
teamId: string;
};
export async function listDestinations(teamId: string): Promise<DestinationDocker[]> {
if (teamId === '0') {
return await prisma.destinationDocker.findMany({ include: { teams: true } });
}
@@ -15,19 +26,28 @@ export async function listDestinations(teamId) {
});
}
export async function configureDestinationForService({ id, destinationId }) {
export async function configureDestinationForService({
id,
destinationId
}: DestinationConfigurationObject): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { destinationDocker: { connect: { id: destinationId } } }
});
}
export async function configureDestinationForApplication({ id, destinationId }) {
export async function configureDestinationForApplication({
id,
destinationId
}: DestinationConfigurationObject): Promise<Application> {
return await prisma.application.update({
where: { id },
data: { destinationDocker: { connect: { id: destinationId } } }
});
}
export async function configureDestinationForDatabase({ id, destinationId }) {
export async function configureDestinationForDatabase({
id,
destinationId
}: DestinationConfigurationObject): Promise<void> {
await prisma.database.update({
where: { id },
data: { destinationDocker: { connect: { id: destinationId } } }
@@ -48,7 +68,12 @@ export async function configureDestinationForDatabase({ id, destinationId }) {
}
}
}
export async function updateDestination({ id, name, engine, network }) {
export async function updateDestination({
id,
name,
engine,
network
}: Pick<DestinationDocker, 'id' | 'name' | 'engine' | 'network'>): Promise<DestinationDocker> {
return await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
}
@@ -58,13 +83,8 @@ export async function newRemoteDestination({
engine,
network,
isCoolifyProxyUsed,
remoteEngine,
ipAddress,
user,
port,
sshPrivateKey
}) {
const encryptedPrivateKey = encrypt(sshPrivateKey);
remoteEngine
}: CreateDockerDestination): Promise<string> {
const destination = await prisma.destinationDocker.create({
data: {
name,
@@ -72,16 +92,18 @@ export async function newRemoteDestination({
engine,
network,
isCoolifyProxyUsed,
remoteEngine,
ipAddress,
user,
port,
sshPrivateKey: encryptedPrivateKey
remoteEngine
}
});
return destination.id;
}
export async function newLocalDestination({ name, teamId, engine, network, isCoolifyProxyUsed }) {
export async function newLocalDestination({
name,
teamId,
engine,
network,
isCoolifyProxyUsed
}: CreateDockerDestination): Promise<string> {
const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
@@ -99,18 +121,14 @@ export async function newLocalDestination({ name, teamId, engine, network, isCoo
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
if (proxyConfigured.isCoolifyProxyUsed) {
isCoolifyProxyUsed = true;
} else {
isCoolifyProxyUsed = false;
}
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
}
if (isCoolifyProxyUsed) await startCoolifyProxy(engine);
return destination.id;
}
export async function removeDestination({ id }) {
export async function removeDestination({ id }: Pick<DestinationDocker, 'id'>): Promise<void> {
const destination = await prisma.destinationDocker.delete({ where: { id } });
if (destination.isCoolifyProxyUsed) {
const host = getEngine(destination.engine);
@@ -127,8 +145,11 @@ export async function removeDestination({ id }) {
}
}
export async function getDestination({ id, teamId }) {
let destination = {};
export async function getDestination({
id,
teamId
}: FindDestinationFromTeam): Promise<DestinationDocker & { sshPrivateKey?: string }> {
let destination;
if (teamId === '0') {
destination = await prisma.destinationDocker.findFirst({
where: { id }
@@ -141,13 +162,22 @@ export async function getDestination({ id, teamId }) {
return destination;
}
export async function getDestinationByApplicationId({ id, teamId }) {
export async function getDestinationByApplicationId({
id,
teamId
}: FindDestinationFromTeam): Promise<DestinationDocker> {
return await prisma.destinationDocker.findFirst({
where: { application: { some: { id } }, teams: { some: { id: teamId } } }
});
}
export async function setDestinationSettings({ engine, isCoolifyProxyUsed }) {
export async function setDestinationSettings({
engine,
isCoolifyProxyUsed
}: {
engine: string;
isCoolifyProxyUsed: boolean;
}): Promise<Prisma.BatchPayload> {
return await prisma.destinationDocker.updateMany({
where: { engine },
data: { isCoolifyProxyUsed }

View File

@@ -1,7 +1,10 @@
import { decrypt, encrypt } from '$lib/crypto';
import { prisma } from './common';
import type { GithubApp, GitlabApp, GitSource, Prisma, Application } from '@prisma/client';
export async function listSources(teamId) {
export async function listSources(
teamId: string | Prisma.StringFilter
): Promise<(GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp })[]> {
if (teamId === '0') {
return await prisma.gitSource.findMany({
include: { githubApp: true, gitlabApp: true, teams: true }
@@ -13,7 +16,13 @@ export async function listSources(teamId) {
});
}
export async function newSource({ teamId, name }) {
export async function newSource({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<GitSource> {
return await prisma.gitSource.create({
data: {
name,
@@ -21,7 +30,7 @@ export async function newSource({ teamId, name }) {
}
});
}
export async function removeSource({ id }) {
export async function removeSource({ id }: { id: string }): Promise<void> {
const source = await prisma.gitSource.delete({
where: { id },
include: { githubApp: true, gitlabApp: true }
@@ -30,8 +39,14 @@ export async function removeSource({ id }) {
if (source.gitlabAppId) await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } });
}
export async function getSource({ id, teamId }) {
let body = {};
export async function getSource({
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<GitSource & { githubApp: GithubApp; gitlabApp: GitlabApp }> {
let body;
if (teamId === '0') {
body = await prisma.gitSource.findFirst({
where: { id },
@@ -51,8 +66,11 @@ export async function getSource({ id, teamId }) {
if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret);
return body;
}
export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }) {
await prisma.gitSource.update({ where: { id }, data: { 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, organization }
});
return await prisma.githubApp.create({
data: {
teams: { connect: { id: teamId } },
@@ -80,19 +98,35 @@ export async function addGitLabSource({
appId,
oauthId,
groupName,
appSecret: encrptedAppSecret,
appSecret: encryptedAppSecret,
gitSource: { connect: { id } }
}
});
}
export async function configureGitsource({ id, gitSourceId }) {
export async function configureGitsource({
id,
gitSourceId
}: {
id: string;
gitSourceId: string;
}): Promise<Application> {
return await prisma.application.update({
where: { id },
data: { gitSource: { connect: { id: gitSourceId } } }
});
}
export async function updateGitsource({ id, name, htmlUrl, apiUrl }) {
export async function updateGitsource({
id,
name,
htmlUrl,
apiUrl
}: {
id: string;
name: string;
htmlUrl: string;
apiUrl: string;
}): Promise<GitSource> {
return await prisma.gitSource.update({
where: { id },
data: { name, htmlUrl, apiUrl }

View File

@@ -1,7 +1,15 @@
import { decrypt, encrypt } from '$lib/crypto';
import { prisma } from './common';
import type { GithubApp } from '@prisma/client';
export async function addInstallation({ gitSourceId, installation_id }) {
// TODO: We should change installation_id to be camelCase
export async function addInstallation({
gitSourceId,
installation_id
}: {
gitSourceId: string;
installation_id: string;
}): Promise<GithubApp> {
const source = await prisma.gitSource.findUnique({
where: { id: gitSourceId },
include: { githubApp: true }
@@ -12,8 +20,12 @@ export async function addInstallation({ gitSourceId, installation_id }) {
});
}
export async function getUniqueGithubApp({ githubAppId }) {
let body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
export async function getUniqueGithubApp({
githubAppId
}: {
githubAppId: string;
}): Promise<GithubApp> {
const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
if (body.privateKey) body.privateKey = decrypt(body.privateKey);
return body;
}
@@ -26,7 +38,15 @@ export async function createGithubApp({
pem,
webhook_secret,
state
}) {
}: {
id: number;
client_id: string;
slug: string;
client_secret: string;
pem: string;
webhook_secret: string;
state: string;
}): Promise<GithubApp> {
const encryptedClientSecret = encrypt(client_secret);
const encryptedWebhookSecret = encrypt(webhook_secret);
const encryptedPem = encrypt(pem);

View File

@@ -1,7 +1,14 @@
import { encrypt } from '$lib/crypto';
import { generateSshKeyPair, prisma } from './common';
import type { GitlabApp } from '@prisma/client';
export async function updateDeployKey({ id, deployKeyId }) {
export async function updateDeployKey({
id,
deployKeyId
}: {
id: string;
deployKeyId: number;
}): Promise<GitlabApp> {
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }
@@ -11,14 +18,24 @@ export async function updateDeployKey({ id, deployKeyId }) {
data: { deployKeyId }
});
}
export async function getSshKey({ id }) {
export async function getSshKey({
id
}: {
id: string;
}): Promise<{ status: number; body: { publicKey: string } }> {
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }
});
return { status: 200, body: { publicKey: application.gitSource.gitlabApp.publicSshKey } };
}
export async function generateSshKey({ id }) {
export async function generateSshKey({
id
}: {
id: string;
}): Promise<
{ status: number; body: { publicKey: string } } | { status: number; body?: undefined }
> {
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }

View File

@@ -1,6 +1,13 @@
import type { BuildLog } from '@prisma/client';
import { prisma, ErrorHandler } from './common';
export async function listLogs({ buildId, last = 0 }) {
export async function listLogs({
buildId,
last = 0
}: {
buildId: string;
last: number;
}): Promise<BuildLog[] | { status: number; body: { message: string; error: string } }> {
try {
const body = await prisma.buildLog.findMany({
where: { buildId, time: { gt: last } },

View File

@@ -1,7 +1,8 @@
import { encrypt, decrypt } from '$lib/crypto';
import { prisma } from './common';
import type { ServiceSecret, Secret, Prisma } from '@prisma/client';
export async function listServiceSecrets(serviceId: string) {
export async function listServiceSecrets(serviceId: string): Promise<ServiceSecret[]> {
let secrets = await prisma.serviceSecret.findMany({
where: { serviceId },
orderBy: { createdAt: 'desc' }
@@ -14,7 +15,7 @@ export async function listServiceSecrets(serviceId: string) {
return secrets;
}
export async function listSecrets(applicationId: string) {
export async function listSecrets(applicationId: string): Promise<Secret[]> {
let secrets = await prisma.secret.findMany({
where: { applicationId },
orderBy: { createdAt: 'desc' }
@@ -27,20 +28,48 @@ export async function listSecrets(applicationId: string) {
return secrets;
}
export async function createServiceSecret({ id, name, value }) {
export async function createServiceSecret({
id,
name,
value
}: {
id: string;
name: string;
value: string;
}): Promise<ServiceSecret> {
value = encrypt(value);
return await prisma.serviceSecret.create({
data: { name, value, service: { connect: { id } } }
});
}
export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
export async function createSecret({
id,
name,
value,
isBuildSecret,
isPRMRSecret
}: {
id: string;
name: string;
value: string;
isBuildSecret: boolean;
isPRMRSecret: boolean;
}): Promise<Secret> {
value = encrypt(value);
return await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
export async function updateServiceSecret({ id, name, value }) {
export async function updateServiceSecret({
id,
name,
value
}: {
id: string;
name: string;
value: string;
}): Promise<Prisma.BatchPayload | ServiceSecret> {
value = encrypt(value);
const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } });
@@ -55,7 +84,19 @@ export async function updateServiceSecret({ id, name, value }) {
});
}
}
export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
export async function updateSecret({
id,
name,
value,
isBuildSecret,
isPRMRSecret
}: {
id: string;
name: string;
value: string;
isBuildSecret: boolean;
isPRMRSecret: boolean;
}): Promise<Prisma.BatchPayload | Secret> {
value = encrypt(value);
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
@@ -71,10 +112,22 @@ export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecre
}
}
export async function removeServiceSecret({ id, name }) {
export async function removeServiceSecret({
id,
name
}: {
id: string;
name: string;
}): Promise<Prisma.BatchPayload> {
return await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } });
}
export async function removeSecret({ id, name }) {
export async function removeSecret({
id,
name
}: {
id: string;
name: string;
}): Promise<Prisma.BatchPayload> {
return await prisma.secret.deleteMany({ where: { applicationId: id, name } });
}

View File

@@ -1,10 +1,10 @@
import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import type { Minio, Service } from '@prisma/client';
import cuid from 'cuid';
import { generatePassword } from '.';
import { prisma } from './common';
export async function listServices(teamId) {
export async function listServices(teamId: string): Promise<Service[]> {
if (teamId === '0') {
return await prisma.service.findMany({ include: { teams: true } });
} else {
@@ -15,12 +15,18 @@ export async function listServices(teamId) {
}
}
export async function newService({ name, teamId }) {
export async function newService({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Service> {
return await prisma.service.create({ data: { name, teams: { connect: { id: teamId } } } });
}
export async function getService({ id, teamId }) {
let body = {};
export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> {
let body;
const include = {
destinationDocker: true,
plausibleAnalytics: true,
@@ -83,7 +89,13 @@ export async function getService({ id, teamId }) {
return { ...body, settings };
}
export async function configureServiceType({ id, type }) {
export async function configureServiceType({
id,
type
}: {
id: string;
type: string;
}): Promise<void> {
if (type === 'plausibleanalytics') {
const password = encrypt(generatePassword());
const postgresqlUser = cuid();
@@ -199,44 +211,157 @@ export async function configureServiceType({ id, type }) {
});
}
}
export async function setServiceVersion({ id, version }) {
export async function setServiceVersion({
id,
version
}: {
id: string;
version: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { version }
});
}
export async function setServiceSettings({ id, dualCerts }) {
export async function setServiceSettings({
id,
dualCerts
}: {
id: string;
dualCerts: boolean;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { dualCerts }
});
}
export async function updatePlausibleAnalyticsService({ id, fqdn, email, username, name }) {
export async function updatePlausibleAnalyticsService({
id,
fqdn,
email,
username,
name
}: {
id: string;
fqdn: string;
name: string;
email: string;
username: string;
}): Promise<void> {
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } });
}
export async function updateService({ id, fqdn, name }) {
export async function updateService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConfig }) {
export async function updateLanguageToolService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateMeiliSearchService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateVaultWardenService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateVsCodeServer({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateWordpress({
id,
fqdn,
name,
mysqlDatabase,
extraConfig
}: {
id: string;
fqdn: string;
name: string;
mysqlDatabase: string;
extraConfig: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } }
});
}
export async function updateMinioService({ id, publicPort }) {
export async function updateMinioService({
id,
publicPort
}: {
id: string;
publicPort: number;
}): Promise<Minio> {
return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
}
export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) {
export async function updateGhostService({
id,
fqdn,
name,
mariadbDatabase
}: {
id: string;
fqdn: string;
name: string;
mariadbDatabase: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, ghost: { update: { mariadbDatabase } } }
});
}
export async function removeService({ id }) {
export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });

View File

@@ -1,8 +1,9 @@
import { decrypt } from '$lib/crypto';
import { prisma } from './common';
import type { Setting } from '@prisma/client';
export async function listSettings() {
let settings = await prisma.setting.findFirst({});
export async function listSettings(): Promise<Setting> {
const settings = await prisma.setting.findFirst({});
if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword);
return settings;
}

View File

@@ -1,9 +1,10 @@
import type { Team, Permission } from '@prisma/client';
import { prisma } from './common';
export async function listTeams() {
export async function listTeams(): Promise<Team[]> {
return await prisma.team.findMany();
}
export async function newTeam({ name, userId }) {
export async function newTeam({ name, userId }: { name: string; userId: string }): Promise<Team> {
return await prisma.team.create({
data: {
name,
@@ -12,7 +13,11 @@ export async function newTeam({ name, userId }) {
}
});
}
export async function getMyTeams({ userId }) {
export async function getMyTeams({
userId
}: {
userId: string;
}): Promise<(Permission & { team: Team & { _count: { users: number } } })[]> {
return await prisma.permission.findMany({
where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } }

View File

@@ -1,16 +1,30 @@
import cuid from 'cuid';
import bcrypt from 'bcrypt';
import bcrypt from 'bcryptjs';
import { prisma } from './common';
import { asyncExecShell, uniqueName } from '$lib/common';
import * as db from '$lib/database';
import { startCoolifyProxy } from '$lib/haproxy';
export async function hashPassword(password: string) {
import type { User } from '@prisma/client';
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15;
return bcrypt.hash(password, saltRounds);
}
export async function login({ email, password, isLogin }) {
export async function login({
email,
password,
isLogin
}: {
email: string;
password: string;
isLogin: boolean;
}): Promise<{
status: number;
headers: { 'Set-Cookie': string };
body: { userId: string; teamId: string; permission: string; isAdmin: boolean };
}> {
const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({
where: { email },
@@ -140,6 +154,6 @@ export async function login({ email, password, isLogin }) {
};
}
export async function getUser({ userId }) {
export async function getUser({ userId }: { userId: string }): Promise<User> {
return await prisma.user.findUnique({ where: { id: userId } });
}

View File

@@ -1,5 +1,6 @@
import { toast } from '@zerodevx/svelte-toast';
export function errorNotification(message: string) {
export function errorNotification(message: string): void {
console.error(message);
if (typeof message !== 'string') {
toast.push('Ooops, something is not okay, are you okay?');
@@ -30,7 +31,7 @@ export function enhance(
e.preventDefault();
let body = new FormData(form);
let parsedData = body;
const parsedData = body;
body.forEach((data, key) => {
if (data === '' || data === null) parsedData.delete(key);

View File

@@ -1,16 +1,15 @@
import { dev } from '$app/env';
import got from 'got';
import got, { type Got } from 'got';
import * as db from '$lib/database';
import mustache from 'mustache';
import crypto from 'crypto';
import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
let template = `program api
const template = `program api
command /usr/bin/dataplaneapi -f /usr/local/etc/haproxy/dataplaneapi.hcl --userlist haproxy-dataplaneapi
no option start-on-reload
@@ -21,10 +20,10 @@ global
defaults
mode http
log global
timeout http-request 60s
timeout http-request 120s
timeout connect 10s
timeout client 60s
timeout server 60s
timeout client 120s
timeout server 120s
userlist haproxy-dataplaneapi
user admin insecure-password "\${HAPROXY_PASSWORD}"
@@ -128,7 +127,8 @@ backend {{domain}}
server {{id}} {{id}}:{{port}} check fall 10
{{/coolify}}
`;
export async function haproxyInstance() {
export async function haproxyInstance(): Promise<Got> {
const { proxyPassword } = await db.listSettings();
return got.extend({
prefixUrl: url,
@@ -137,31 +137,97 @@ export async function haproxyInstance() {
});
}
export async function configureHAProxy() {
export async function configureHAProxy(): Promise<void> {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
try {
const data = {
applications: [],
services: [],
coolify: []
};
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true }
});
for (const application of applications) {
const {
fqdn,
id,
port,
destinationDocker,
destinationDockerId,
settings: { previews },
updatedAt
} = application;
if (destinationDockerId) {
const { engine, network } = destinationDocker;
const data = {
applications: [],
services: [],
coolify: []
};
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true }
});
for (const application of applications) {
const {
fqdn,
id,
port,
destinationDocker,
destinationDockerId,
settings: { previews },
updatedAt
} = application;
if (destinationDockerId) {
const { engine, network } = destinationDocker;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.applications.push({
id,
port: port || 3000,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
const previewDomain = `${container.split('-')[1]}.${domain}`;
data.applications.push({
id: container,
port: port || 3000,
domain: previewDomain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true
}
});
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
@@ -169,9 +235,10 @@ export async function configureHAProxy() {
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.applications.push({
data.services.push({
id,
port: port || 3000,
port,
publicPort,
domain,
isRunning,
isHttps,
@@ -180,108 +247,38 @@ export async function configureHAProxy() {
updatedAt: updatedAt.getTime()
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
data.applications.push({
id: container,
port: port || 3000,
domain: previewDomain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true
}
const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
});
}
const output = mustache.render(template, data);
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings();
if (proxyHash !== newHash) {
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
await haproxy.post(`v2/services/haproxy/configuration/raw`, {
searchParams: {
skip_version: true
},
body: output,
headers: {
'Content-Type': 'text/plain'
}
});
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
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()
});
}
}
}
}
}
const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
});
}
const output = mustache.render(template, data);
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings();
if (proxyHash !== newHash) {
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
await haproxy.post(`v2/services/haproxy/configuration/raw`, {
searchParams: {
skip_version: true
},
body: output,
headers: {
'Content-Type': 'text/plain'
}
});
}
} catch (error) {
throw error;
}
}

View File

@@ -1,7 +1,8 @@
import { dev } from '$app/env';
import { asyncExecShell, getEngine } from '$lib/common';
import got from 'got';
import got, { type Got, type Response } from 'got';
import * as db from '$lib/database';
import type { DestinationDocker } from '@prisma/client';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@@ -9,7 +10,7 @@ export const defaultProxyImage = `coolify-haproxy-alpine:latest`;
export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`;
export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`;
export async function haproxyInstance() {
export async function haproxyInstance(): Promise<Got> {
const { proxyPassword } = await db.listSettings();
return got.extend({
prefixUrl: url,
@@ -17,6 +18,7 @@ export async function haproxyInstance() {
password: proxyPassword
});
}
export async function getRawConfiguration(): Promise<RawHaproxyConfiguration> {
return await (await haproxyInstance()).get(`v2/services/haproxy/configuration/raw`).json();
}
@@ -43,11 +45,12 @@ export async function getNextTransactionId(): Promise<string> {
return newTransaction.id;
}
export async function completeTransaction(transactionId) {
export async function completeTransaction(transactionId: string): Promise<Response<string>> {
const haproxy = await haproxyInstance();
return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`);
}
export async function deleteProxy({ id }) {
export async function deleteProxy({ id }: { id: string }): Promise<void> {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
@@ -77,11 +80,12 @@ export async function deleteProxy({ id }) {
}
}
export async function reloadHaproxy(engine) {
export async function reloadHaproxy(engine: string): Promise<{ stdout: string; stderr: string }> {
const host = getEngine(engine);
return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`);
}
export async function checkHAProxy(haproxy?: any) {
export async function checkHAProxy(haproxy?: Got): Promise<void> {
if (!haproxy) haproxy = await haproxyInstance();
try {
await haproxy.get('v2/info');
@@ -93,7 +97,10 @@ export async function checkHAProxy(haproxy?: any) {
}
}
export async function stopTcpHttpProxy(destinationDocker, publicPort) {
export async function stopTcpHttpProxy(
destinationDocker: DestinationDocker,
publicPort: number
): Promise<{ stdout: string; stderr: string } | Error> {
const { engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`;
@@ -108,7 +115,13 @@ export async function stopTcpHttpProxy(destinationDocker, publicPort) {
return error;
}
}
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort, volume = null) {
export async function startTcpProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number,
volume?: string
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
@@ -132,7 +145,13 @@ export async function startTcpProxy(destinationDocker, id, publicPort, privatePo
return error;
}
}
export async function startHttpProxy(destinationDocker, id, publicPort, privatePort) {
export async function startHttpProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
@@ -154,7 +173,8 @@ export async function startHttpProxy(destinationDocker, id, publicPort, privateP
return error;
}
}
export async function startCoolifyProxy(engine) {
export async function startCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy');
const { proxyPassword, proxyUser, id } = await db.listSettings();
@@ -170,7 +190,8 @@ export async function startCoolifyProxy(engine) {
}
await configureNetworkCoolifyProxy(engine);
}
export async function checkContainer(engine, container) {
export async function checkContainer(engine: string, container: string): Promise<boolean> {
const host = getEngine(engine);
let containerFound = false;
@@ -180,7 +201,7 @@ export async function checkContainer(engine, container) {
);
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running' ? true : false;
const isRunning = status === 'running';
if (status === 'exited' || status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
}
@@ -193,7 +214,9 @@ export async function checkContainer(engine, container) {
return containerFound;
}
export async function stopCoolifyProxy(engine) {
export async function stopCoolifyProxy(
engine: string
): Promise<{ stdout: string; stderr: string } | Error> {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy');
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false });
@@ -210,16 +233,18 @@ export async function stopCoolifyProxy(engine) {
}
}
export async function configureNetworkCoolifyProxy(engine) {
export async function configureNetworkCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine);
const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } });
destinations.forEach(async (destination) => {
try {
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) {
if (!configuredNetworks.includes(destination.network)) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
);
} catch (err) {
// TODO: handle error
}
});
}
}

View File

@@ -2,11 +2,9 @@ import { asyncExecShell, saveBuildLog } from '$lib/common';
import got from 'got';
import jsonwebtoken from 'jsonwebtoken';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
export default async function ({
applicationId,
debug,
workdir,
githubAppId,
repository,
@@ -14,7 +12,16 @@ export default async function ({
htmlUrl,
branch,
buildId
}): Promise<any> {
}: {
applicationId: string;
workdir: string;
githubAppId: string;
repository: string;
apiUrl: string;
htmlUrl: string;
branch: string;
buildId: string;
}): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId });

View File

@@ -9,7 +9,16 @@ export default async function ({
branch,
buildId,
privateSshKey
}): Promise<any> {
}: {
applicationId: string;
workdir: string;
repository: string;
htmlUrl: string;
branch: string;
buildId: string;
repodir: string;
privateSshKey: string;
}): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);

View File

@@ -6,8 +6,9 @@ import cuid from 'cuid';
import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { promises as dns } from 'dns';
export async function letsEncrypt(domain, id = null, isCoolify = false) {
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try {
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
@@ -98,7 +99,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
}
}
export async function generateSSLCerts() {
export async function generateSSLCerts(): Promise<void> {
const ssls = [];
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true },
@@ -131,7 +132,7 @@ export async function generateSSLCerts() {
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
const previewDomain = `${container.split('-')[1]}.${domain}`;
if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false });
}
}
@@ -148,7 +149,8 @@ export async function generateSSLCerts() {
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true
ghost: true,
meiliSearch: true
},
orderBy: { createdAt: 'desc' }
});
@@ -198,6 +200,15 @@ export async function generateSSLCerts() {
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) {
if (!dev) {
if (
@@ -206,8 +217,27 @@ export async function generateSSLCerts() {
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
console.log('Generating SSL for', ssl.domain);
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
// 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 await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
}
} else {
if (
@@ -216,7 +246,27 @@ export async function generateSSLCerts() {
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} 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

@@ -20,27 +20,22 @@ import {
setDefaultConfiguration
} from '$lib/buildPacks/common';
import yaml from 'js-yaml';
import type { Job } from 'bullmq';
import type { BuilderJob } from '$lib/types/builderJob';
import type { ComposeFile } from '$lib/types/composeFile';
export default async function (job) {
let {
export default async function (job: Job<BuilderJob, void, string>): Promise<void> {
const {
id: applicationId,
repository,
branch,
buildPack,
name,
destinationDocker,
destinationDockerId,
gitSource,
build_id: buildId,
configHash,
port,
installCommand,
buildCommand,
startCommand,
fqdn,
baseDirectory,
publishDirectory,
projectId,
secrets,
phpModules,
@@ -53,6 +48,16 @@ export default async function (job) {
pythonModule,
pythonVariable
} = job.data;
let {
branch,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
} = job.data;
const { debug } = settings;
await asyncSleep(500);
@@ -67,7 +72,7 @@ export default async function (job) {
});
let imageId = applicationId;
let domain = getDomain(fqdn);
let volumes =
const volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${
buildPack !== 'docker' ? '/app' : ''
@@ -103,7 +108,7 @@ export default async function (job) {
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
let commit = await importers[gitSource.type]({
const commit = await importers[gitSource.type]({
applicationId,
debug,
workdir,
@@ -210,9 +215,7 @@ export default async function (job) {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
throw new Error(`Build pack ${buildPack} not found.`);
}
deployNeeded = true;
} else {
deployNeeded = false;
await saveBuildLog({ line: 'Nothing changed.', buildId, applicationId });
}
@@ -282,7 +285,15 @@ export default async function (job) {
networks: [docker.network],
labels,
depends_on: [],
restart: 'always'
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {

View File

@@ -1,8 +1,6 @@
import { dev } from '$app/env';
import { asyncExecShell, getEngine, version } from '$lib/common';
import { prisma } from '$lib/database';
import { defaultProxyImageHttp, defaultProxyImageTcp } from '$lib/haproxy';
export default async function () {
export default async function (): Promise<void> {
const destinationDockers = await prisma.destinationDocker.findMany();
for (const destinationDocker of destinationDockers) {
const host = getEngine(destinationDocker.engine);
@@ -16,56 +14,23 @@ export default async function () {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
}
} catch (error) {
console.log(error);
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
console.log(error);
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} 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=24h" -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

@@ -1,6 +1,5 @@
import * as Bullmq from 'bullmq';
import { default as ProdBullmq, Job, QueueEvents, QueueScheduler } from 'bullmq';
import cuid from 'cuid';
import { default as ProdBullmq, QueueScheduler } from 'bullmq';
import { dev } from '$app/env';
import { prisma } from '$lib/database';
@@ -28,7 +27,7 @@ const connectionOptions = {
}
};
const cron = async () => {
const cron = async (): Promise<void> => {
new QueueScheduler('proxy', connectionOptions);
new QueueScheduler('cleanup', connectionOptions);
new QueueScheduler('ssl', connectionOptions);
@@ -89,18 +88,6 @@ const cron = async () => {
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
const events = {
proxy: new QueueEvents('proxy', { ...connectionOptions }),
ssl: new QueueEvents('ssl', { ...connectionOptions })
};
events.proxy.on('completed', (data) => {
// console.log(data)
});
events.ssl.on('completed', (data) => {
// console.log(data)
});
};
cron().catch((error) => {
console.log('cron failed to start');

View File

@@ -1,7 +1,8 @@
import { prisma } from '$lib/database';
import { dev } from '$app/env';
import type { Job } from 'bullmq';
export default async function (job) {
export default async function (job: Job): Promise<void> {
const { line, applicationId, buildId } = job.data;
if (dev) console.debug(`[${applicationId}] ${line}`);
await prisma.buildLog.create({ data: { line, buildId, time: Number(job.id), applicationId } });

View File

@@ -1,7 +1,10 @@
import { ErrorHandler } from '$lib/database';
import { configureHAProxy } from '$lib/haproxy/configuration';
export default async function () {
export default async function (): Promise<void | {
status: number;
body: { message: string; error: string };
}> {
try {
return await configureHAProxy();
} catch (error) {

View File

@@ -1,6 +1,6 @@
import { generateSSLCerts } from '$lib/letsencrypt';
export default async function () {
export default async function (): Promise<void> {
try {
return await generateSSLCerts();
} catch (error) {

View File

@@ -1,13 +1,9 @@
import { asyncExecShell } from '$lib/common';
import { reloadHaproxy } from '$lib/haproxy';
export default async function () {
try {
await asyncExecShell(
`docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew`
);
await reloadHaproxy('unix:///var/run/docker.sock');
} catch (error) {
throw error;
}
export default async function (): Promise<void> {
await asyncExecShell(
`docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew`
);
await reloadHaproxy('unix:///var/run/docker.sock');
}

View File

@@ -1,6 +1,7 @@
import { writable } from 'svelte/store';
import { writable, type Writable } from 'svelte/store';
export const gitTokens = writable({
githubToken: null,
gitlabToken: null
});
export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: string | null }> =
writable({
githubToken: null,
gitlabToken: null
});

View File

@@ -0,0 +1,51 @@
import type { DestinationDocker, GithubApp, GitlabApp, GitSource, Secret } from '@prisma/client';
export type BuilderJob = {
build_id: string;
type: BuildType;
id: string;
name: string;
fqdn: string;
repository: string;
configHash: unknown;
branch: string;
buildPack: BuildPackName;
projectId: number;
port: number;
installCommand: string;
buildCommand?: string;
startCommand?: string;
baseDirectory: string;
publishDirectory: string;
phpModules: string;
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
createdAt: string;
updatedAt: string;
destinationDockerId: string;
destinationDocker: DestinationDocker;
gitSource: GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp };
settings: BuilderJobSettings;
secrets: Secret[];
persistentStorage: { path: string }[];
pullmergeRequestId?: unknown;
sourceBranch?: string;
};
// TODO: Add the other build types
export type BuildType = 'manual';
// TODO: Add the other buildpack names
export type BuildPackName = 'node' | 'docker';
export type BuilderJobSettings = {
id: string;
applicationId: string;
dualCerts: boolean;
debug: boolean;
previews: boolean;
autodeploy: boolean;
createdAt: string;
updatedAt: string;
};

View File

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

View File

@@ -0,0 +1,8 @@
export type CreateDockerDestination = {
name: string;
engine: string;
remoteEngine: boolean;
network: string;
isCoolifyProxyUsed: boolean;
teamId: string;
};

View File

@@ -176,7 +176,7 @@
<a
sveltekit:prefetch
href="/applications"
class="icons tooltip-right bg-coolgray-200 hover:text-green-500"
class="icons tooltip-green-500 tooltip-right bg-coolgray-200 hover:text-green-500"
class:text-green-500={$page.url.pathname.startsWith('/applications') ||
$page.url.pathname.startsWith('/new/application')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
@@ -204,7 +204,7 @@
<a
sveltekit:prefetch
href="/sources"
class="icons tooltip-right bg-coolgray-200 hover:text-orange-500"
class="icons tooltip-orange-500 tooltip-right bg-coolgray-200 hover:text-orange-500"
class:text-orange-500={$page.url.pathname.startsWith('/sources') ||
$page.url.pathname.startsWith('/new/source')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
@@ -234,7 +234,7 @@
<a
sveltekit:prefetch
href="/destinations"
class="icons tooltip-right bg-coolgray-200 hover:text-sky-500"
class="icons tooltip-sky-500 tooltip-right bg-coolgray-200 hover:text-sky-500"
class:text-sky-500={$page.url.pathname.startsWith('/destinations') ||
$page.url.pathname.startsWith('/new/destination')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') ||
@@ -269,7 +269,7 @@
<a
sveltekit:prefetch
href="/databases"
class="icons tooltip-right bg-coolgray-200 hover:text-purple-500"
class="icons tooltip-purple-500 tooltip-right bg-coolgray-200 hover:text-purple-500"
class:text-purple-500={$page.url.pathname.startsWith('/databases') ||
$page.url.pathname.startsWith('/new/database')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') ||
@@ -296,7 +296,7 @@
<a
sveltekit:prefetch
href="/services"
class="icons tooltip-right bg-coolgray-200 hover:text-pink-500"
class="icons tooltip-pink-500 tooltip-right bg-coolgray-200 hover:text-pink-500"
class:text-pink-500={$page.url.pathname.startsWith('/services') ||
$page.url.pathname.startsWith('/new/service')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/services') ||
@@ -348,7 +348,7 @@
{:else if updateStatus.success === null}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-9"
class="h-9 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -363,7 +363,7 @@
<line x1="16" y1="12" x2="12" y2="8" />
</svg>
{:else if updateStatus.success}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8"
><path
fill="#DD2E44"
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
@@ -408,7 +408,7 @@
/></svg
>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8"
><path
fill="#FFCC4D"
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
@@ -435,7 +435,7 @@
<a
sveltekit:prefetch
href="/iam"
class="icons tooltip-right bg-coolgray-200 hover:text-fuchsia-500"
class="icons tooltip-fuchsia-500 tooltip-right bg-coolgray-200 hover:text-fuchsia-500"
class:text-fuchsia-500={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
data-tooltip="IAM"
@@ -461,7 +461,7 @@
<a
sveltekit:prefetch
href="/settings"
class="icons tooltip-right bg-coolgray-200 hover:text-yellow-500"
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tooltip="Settings"
@@ -486,7 +486,7 @@
{/if}
<div
class="icons tooltip-right bg-coolgray-200 hover:text-red-500"
class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"
data-tooltip="Logout"
on:click={logout}
>

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 branchSelectOptions;
async function loadRepositories() {
let page = 1;
let reposCount = 0;
@@ -58,24 +65,28 @@
}));
}
async function loadBranches(event) {
branches = [];
selected.repository = event.detail.value;
loading.branches = true;
selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try {
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${$gitTokens.githubToken}`
});
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
return;
} catch ({ error }) {
return errorNotification(error);
} finally {
loading.branches = false;
let page = 1;
let branchCount = 0;
loading.branches = true;
const loadedBranches = await loadBranchesByPage();
branches = branches.concat(loadedBranches);
branchCount = branches.length;
if (branchCount === 100) {
while (branchCount === 100) {
page = page + 1;
const nextBranches = await loadBranchesByPage(page);
branches = branches.concat(nextBranches);
branchCount = nextBranches.length;
}
}
loading.branches = false;
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
}
async function isBranchAlreadyUsed(event) {
selected.branch = event.detail.value;
@@ -166,30 +177,36 @@
{:else}
<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 gap-4">
<div class="flex-col md:flex gap-4">
<div class="custom-select-wrapper">
<Select
placeholder={loading.repositories
? 'Loading repositories ...'
? 'Loading repositories...'
: 'Please select a repository'}
id="repository"
showIndicator={true}
isWaiting={loading.repositories}
on:select={loadBranches}
items={reposSelectOptions}
isDisabled={loading.repositories}
isClearable={false}
/>
</div>
<input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper">
<Select
placeholder={loading.branches
? 'Loading branches ...'
? 'Loading branches...'
: !selected.repository
? 'Please select a repository first'
: 'Please select a branch'}
id="repository"
isWaiting={loading.branches}
showIndicator={selected.repository}
id="branches"
on:select={isBranchAlreadyUsed}
items={branchSelectOptions}
isDisabled={loading.branches || !selected.repository}
isClearable={false}
/>
</div>
</div>
@@ -202,13 +219,6 @@
class:bg-orange-600={showSave}
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>
</form>
{/if}

View File

@@ -61,9 +61,6 @@
await refreshSecrets();
toast.push('Secrets saved');
}
function asd() {
console.log(secrets);
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
@@ -164,7 +161,6 @@
</tr>
</tbody>
</table>
<button on:click={asd}>Save</button>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />

View File

@@ -1,4 +1,4 @@
import { getTeam, getUserDetails } from '$lib/common';
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';

View File

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

View File

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

View File

@@ -32,6 +32,7 @@
export let account;
export let accounts;
export let invitations;
if (accounts.length === 0) {
accounts.push(account);
}
@@ -74,12 +75,51 @@
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>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</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">
{#if $session.teamId === '0' && accounts.length > 0}
<div class="title font-bold">Accounts</div>

View File

@@ -1,43 +1,22 @@
import { asyncExecShell, getUserDetails } from '$lib/common';
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { dockerInstance } from '$lib/docker';
import type { RequestHandler } from '@sveltejs/kit';
import type { CreateDockerDestination } from '$lib/types/destinations';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const {
name,
engine,
network,
isCoolifyProxyUsed,
remoteEngine,
ipAddress,
user,
port,
sshPrivateKey
} = await event.request.json();
const dockerDestinationProps = {
...((await event.request.json()) as Omit<CreateDockerDestination, 'teamId'>),
teamId
};
try {
let id = null;
if (remoteEngine) {
id = await db.newRemoteDestination({
name,
teamId,
engine,
network,
isCoolifyProxyUsed,
remoteEngine,
ipAddress,
user,
port,
sshPrivateKey
});
} else {
id = await db.newLocalDestination({ name, teamId, engine, network, isCoolifyProxyUsed });
}
const id = dockerDestinationProps.remoteEngine
? await db.newRemoteDestination(dockerDestinationProps)
: await db.newLocalDestination(dockerDestinationProps);
return { status: 200, body: { id } };
} catch (error) {
return ErrorHandler(error);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
export let source;
import { page, session } from '$app/stores';
import { post } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params;
@@ -51,7 +52,8 @@
type: 'github',
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, '')
apiUrl: source.apiUrl.replace(/\/$/, ''),
organization: source.organization
});
} catch ({ error }) {
return errorNotification(error);
@@ -97,6 +99,22 @@
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</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>
{#if source.apiUrl && source.htmlUrl && source.name}
<div class="text-center">
@@ -104,7 +122,7 @@
</div>
{/if}
</form>
{:else if source.githubAppId}
{:else if source.githubApp?.installationId}
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
@@ -135,6 +153,21 @@
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</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>
</form>
{:else}

View File

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

View File

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

View File

@@ -82,7 +82,8 @@
{#if $session.teamId === '0' && otherSources.length > 0}
<div class="truncate text-center">{source.teams[0].name}</div>
{/if}
{#if (source.type === 'gitlab' && !source.gitlabAppId) || (source.type === 'github' && !source.githubAppId && !source.githubApp?.installationId)}
{#if (source.type === 'gitlab' && !source.gitlabAppId) || (source.type === 'github' && source.githubApp?.installationId === null)}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
@@ -109,7 +110,7 @@
{#if $session.teamId === '0'}
<div class="truncate text-center">{source.teams[0].name}</div>
{/if}
{#if (source.type === 'gitlab' && !source.gitlabAppId) || (source.type === 'github' && !source.githubAppId && !source.githubApp?.installationId)}
{#if (source.type === 'gitlab' && !source.gitlabAppId) || (source.type === 'github' && source.githubApp?.installationId === null)}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>

View File

@@ -50,7 +50,10 @@ textarea {
}
#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 {
@@ -209,7 +212,55 @@ a {
padding: 8px;
color: #fff;
content: attr(data-tooltip);
@apply min-w-[100px] rounded bg-coollabs text-center font-normal;
@apply min-w-[100px] rounded text-center font-normal;
}
/* Base colours for tooltips */
/* coollabs - default */
.tooltip:after,
[data-tooltip]:after {
@apply bg-coollabs;
}
/* Green 500 */
.tooltip-green-500:after {
@apply bg-green-500;
}
/* Orange 500 */
.tooltip-orange-500:after {
@apply bg-orange-500;
}
/* Sky 500 */
.tooltip-sky-500:after {
@apply bg-sky-500;
}
/* Purple 500 */
.tooltip-purple-500:after {
@apply bg-purple-500;
}
/* Pink 500 */
.tooltip-pink-500:after {
@apply bg-pink-500;
}
/* Fuchsia 500 */
.tooltip-fuchsia-500:after {
@apply bg-fuchsia-500;
}
/* Yellow 500 */
.tooltip-yellow-500:after {
@apply bg-yellow-500;
}
/* Red-500 */
.tooltip-red-500:after {
@apply bg-red-500;
}
/* Directions */