Compare commits

..

79 Commits

Author SHA1 Message Date
Andras Bacsai
d3a1bbc3d0 Merge pull request #596 from coollabsio/next
v3.9.2
2022-09-07 10:16:54 +02:00
Andras Bacsai
0078574ee6 fix: add php 8.1/8.2 2022-09-07 10:02:59 +02:00
Andras Bacsai
7cfd313531 ui: fix loading start/stop db/services 2022-09-07 09:45:16 +02:00
Andras Bacsai
e7919e9a1b ui: fix initial loading icon bg 2022-09-07 09:39:29 +02:00
Andras Bacsai
98073202e9 fix: minio default env variables 2022-09-07 09:28:53 +02:00
Andras Bacsai
8dee345f85 enable autoupdate option for everyone 2022-09-07 09:23:03 +02:00
Andras Bacsai
9161882f33 debug: add debug log 2022-09-07 09:21:28 +02:00
Andras Bacsai
eef313665b fix: service volume generation 2022-09-07 09:18:44 +02:00
Andras Bacsai
53e70fbfcb dev: update devcontainer 2022-09-07 09:15:10 +02:00
Andras Bacsai
05a1721499 fix: revert last change with domain check 2022-09-07 09:15:03 +02:00
Andras Bacsai
2f772080b8 fix: add initial DNS servers 2022-09-07 09:06:30 +02:00
Andras Bacsai
a5548c080c fix: service state update 2022-09-07 08:58:51 +02:00
Andras Bacsai
7e0a1ecc80 ui: fix login/register page 2022-09-07 08:58:40 +02:00
Andras Bacsai
3f2dcccc07 fix: use ip instead of window location host 2022-09-07 08:58:27 +02:00
Andras Bacsai
adc5965b32 fix: use ip address instead of window location 2022-09-07 08:57:32 +02:00
Andras Bacsai
6088f2e573 chore: version++ 2022-09-06 15:46:22 +02:00
Andras Bacsai
fc705746c0 fix: gitlab webhook 2022-09-06 15:45:29 +02:00
Andras Bacsai
8182359fe4 Merge branch 'main' into next 2022-09-06 15:41:22 +02:00
Andras Bacsai
e7ae15162c Update GH Actions prod release 2022-09-06 15:40:16 +02:00
Andras Bacsai
12ca20432d v3.9.1 (#594) 2022-09-06 15:33:32 +02:00
Andras Bacsai
8b7406e168 revert 2022-09-06 15:25:18 +02:00
Andras Bacsai
9d6317f782 fix 2022-09-06 15:13:51 +02:00
Andras Bacsai
d8bdb73140 test tagging 2022-09-06 14:59:24 +02:00
Andras Bacsai
476db15431 test 2022-09-06 14:49:48 +02:00
Andras Bacsai
20ce356296 fix: move restart button to settings 2022-09-06 14:47:37 +02:00
Andras Bacsai
ea594dcbc6 fix: workdir 2022-09-06 14:32:50 +02:00
Andras Bacsai
021b9746a8 update production release 2022-09-06 14:29:54 +02:00
Andras Bacsai
c4615ae557 Dockerfile 2022-09-06 14:20:14 +02:00
Andras Bacsai
95a5089bdc fix: debug api logging + gh actions 2022-09-06 14:08:10 +02:00
Andras Bacsai
cef1fba281 finally?! 2022-09-06 13:23:53 +02:00
Andras Bacsai
5c7859a258 asd 2022-09-06 13:08:29 +02:00
Andras Bacsai
986cdae5b0 asd 2022-09-06 13:01:55 +02:00
Andras Bacsai
3b11e28d6c asd 2022-09-06 12:56:02 +02:00
Andras Bacsai
eba63e8e76 fix 2022-09-06 12:54:38 +02:00
Andras Bacsai
2fc65e3b42 grr 2022-09-06 12:04:58 +02:00
Andras Bacsai
7d504ab2bf fixxxx 2022-09-06 12:02:03 +02:00
Andras Bacsai
216c7efd42 fixxxxx 2022-09-06 11:55:16 +02:00
Andras Bacsai
8c4149db16 fix issues 2022-09-06 11:42:00 +02:00
Andras Bacsai
20ac8f69ea Update dockerfile 2022-09-06 11:38:36 +02:00
Andras Bacsai
1db3d7a6fb fixit 2022-09-06 11:31:54 +02:00
Andras Bacsai
b1c1138cf8 fixit now 2022-09-06 11:31:02 +02:00
Andras Bacsai
00b1a4f174 fix flow 2022-09-06 11:21:58 +02:00
Andras Bacsai
86cc665b58 fix 2022-09-06 11:05:40 +02:00
Andras Bacsai
e26dd578ef fixes 2022-09-06 11:02:33 +02:00
Andras Bacsai
f1f3217052 testing new gh actions 2022-09-06 10:59:50 +02:00
Andras Bacsai
8f14fd89ef show not allowed list 2022-09-06 10:43:59 +02:00
Andras Bacsai
26e4d52a61 github actions update 2022-09-06 10:30:37 +02:00
Andras Bacsai
319c647147 updates 2022-09-06 10:28:13 +02:00
Andras Bacsai
f4cd93bd36 test allowedlist 2022-09-06 10:14:39 +02:00
Andras Bacsai
5a80bb1d2a fix: dockerfile 2022-09-06 10:14:34 +02:00
Andras Bacsai
1126dcacf5 update 2022-09-06 10:08:31 +02:00
Andras Bacsai
bdf9a73d19 fix 2022-09-06 09:42:13 +02:00
Andras Bacsai
1f73b83a79 testing traefik stuff 2022-09-06 09:30:13 +02:00
Andras Bacsai
73bd62c51e Merge pull request #582 from coollabsio/next
v3.9.0
2022-09-06 09:09:42 +02:00
Andras Bacsai
9acd5c94e8 Merge pull request #592 from kaname-png/ui-reworks
feat(routes): rework ui from login and register page
2022-09-06 08:42:26 +02:00
Andras Bacsai
6e85eac14b update package 2022-09-06 08:39:17 +02:00
Andras Bacsai
936baf676e cleanup logs 2022-09-06 08:01:04 +02:00
Andras Bacsai
867f06d813 chore: version++ 2022-09-06 07:57:39 +02:00
Kaname
7f9f440789 feat(routes): rework ui from login and register page 2022-09-05 19:21:19 +00:00
Andras Bacsai
5a15e64471 fix: update prisma
feat(beta): database branching
2022-09-05 15:41:32 +02:00
Andras Bacsai
c9aecd51f3 remove console logs 2022-09-05 13:15:58 +02:00
Andras Bacsai
6ca1d978d4 fix restart 2022-09-05 13:09:59 +02:00
Andras Bacsai
a18c73bd7c fix 2022-09-05 12:59:06 +02:00
Andras Bacsai
26d86cbcb5 debug more 2022-09-05 12:09:13 +02:00
Andras Bacsai
b24a5d9aca updates 2022-09-05 11:52:03 +02:00
Andras Bacsai
5305bc1ceb debug on 2022-09-05 10:17:52 +02:00
Andras Bacsai
a672f0f56c Merge pull request #590 from AshKyd/main
Small typo in global settings
2022-09-05 10:11:58 +02:00
Andras Bacsai
9ab5e13e8f fix: remote verification 2022-09-05 10:01:49 +02:00
Andras Bacsai
a49171f8cc ui: login page 2022-09-05 09:29:58 +02:00
Andras Bacsai
65d8dc412a fix: expose port is not required 2022-09-05 09:19:25 +02:00
Andras Bacsai
8600400632 fix: service deploymentEnabled 2022-09-05 09:15:32 +02:00
Andras Bacsai
11d10bee12 migrate: database_branches 2022-09-05 09:09:49 +02:00
Andras Bacsai
dbd16e8285 fix: fqdn or expose port required 2022-09-05 09:09:32 +02:00
Andras Bacsai
eb26787079 fix 2022-09-05 08:37:54 +02:00
Andras Bacsai
b0a7b1eb3d ui fix 2022-09-05 08:21:18 +02:00
Andras Bacsai
f994092d7f fix: repository link trim 2022-09-05 08:16:53 +02:00
Andras Bacsai
946d8e5be5 chore: version++ 2022-09-05 08:14:50 +02:00
Andras Bacsai
6d7c2ae74a fix: ssh pid agent name 2022-09-05 08:14:43 +02:00
Ash Kyd
1ba71b0b1b Small typo in global settings 2022-09-05 13:00:14 +10:00
60 changed files with 1742 additions and 1230 deletions

View File

@@ -20,7 +20,8 @@
"svelte.svelte-vscode", "svelte.svelte-vscode",
"ardenivanov.svelte-intellisense", "ardenivanov.svelte-intellisense",
"Prisma.prisma", "Prisma.prisma",
"bradlc.vscode-tailwindcss" "bradlc.vscode-tailwindcss",
"waderyan.gitblame"
], ],
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000, 3001], "forwardPorts": [3000, 3001],

View File

@@ -5,11 +5,11 @@ on:
types: [released] types: [released]
jobs: jobs:
making-something-cool: arm64-build:
runs-on: ubuntu-latest runs-on: [self-hosted, arm64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -26,11 +26,59 @@ jobs:
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/arm64
push: true push: true
tags: coollabsio/coolify:latest,coollabsio/coolify:${{steps.package-version.outputs.current-version}} tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache,mode=max cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
amd64-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
merge-manifest:
runs-on: ubuntu-latest
needs: [amd64-build, arm64-build]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Create & publish manifest
run: |
docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:

View File

@@ -1,17 +1,16 @@
name: release-candidate name: release-candidate
on: on:
release: release:
types: [prereleased] types: [prereleased]
jobs: jobs:
making-something-cool: arm64-making-something-cool:
runs-on: ubuntu-latest runs-on: [self-hosted, arm64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: 'next' ref: "next"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -28,12 +27,64 @@ jobs:
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/arm64
push: true push: true
tags: coollabsio/coolify:${{github.event.release.name}} tags: coollabsio/coolify:${{github.event.release.name}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc,mode=max cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc-arm64,mode=max
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
amd64-making-something-cool:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: coollabsio/coolify:${{github.event.release.name}}-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc-amd64,mode=max
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
merge-manifest-to-be-cool:
runs-on: ubuntu-latest
needs: [arm64-making-something-cool, amd64-making-something-cool]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker manifest create coollabsio/coolify:${{github.event.release.name}} --amend coollabsio/coolify:${{github.event.release.name}}-amd64 --amend coollabsio/coolify:${{github.event.release.name}}-arm64
docker manifest push coollabsio/coolify:${{github.event.release.name}}

View File

@@ -6,11 +6,13 @@ on:
- next - next
jobs: jobs:
staging-release: arm64-making-something-cool:
runs-on: ubuntu-latest runs-on: [self-hosted, arm64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -20,15 +22,65 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with:
context: .
platforms: linux/arm64
push: true
tags: coollabsio/coolify:next-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max
amd64-making-something-cool:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v3
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: coollabsio/coolify:next tags: coollabsio/coolify:next-amd64,coollabsio/coolify:next-test
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next,mode=max cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
merge-manifest-to-be-cool:
runs-on: ubuntu-latest
needs: [arm64-making-something-cool, amd64-making-something-cool]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker manifest create coollabsio/coolify:next --amend coollabsio/coolify:next-amd64 --amend coollabsio/coolify:next-arm64
docker manifest push coollabsio/coolify:next
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:

View File

@@ -1,8 +1,11 @@
ARG PNPM_VERSION=7.11.0
ARG NPM_VERSION=8.19.1
FROM node:18-slim as build FROM node:18-slim as build
WORKDIR /app WORKDIR /app
RUN apt update && apt -y install curl RUN apt update && apt -y install curl
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
COPY . . COPY . .
RUN pnpm install RUN pnpm install
@@ -14,8 +17,10 @@ WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt update && apt -y install git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3 && apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/ RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION}
RUN mkdir -p ~/.docker/cli-plugins/ RUN mkdir -p ~/.docker/cli-plugins/
# https://download.docker.com/linux/static/stable/ # https://download.docker.com/linux/static/stable/

View File

@@ -15,7 +15,7 @@
}, },
"dependencies": { "dependencies": {
"@breejs/ts-worker": "2.0.0", "@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.2.0", "@fastify/autoload": "5.3.1",
"@fastify/cookie": "8.1.0", "@fastify/cookie": "8.1.0",
"@fastify/cors": "8.1.0", "@fastify/cors": "8.1.0",
"@fastify/env": "4.1.0", "@fastify/env": "4.1.0",
@@ -23,7 +23,7 @@
"@fastify/static": "6.5.0", "@fastify/static": "6.5.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2", "@ladjs/graceful": "3.0.2",
"@prisma/client": "3.15.2", "@prisma/client": "4.3.1",
"axios": "0.27.2", "axios": "0.27.2",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bree": "9.1.2", "bree": "9.1.2",
@@ -37,7 +37,7 @@
"fastify": "4.5.3", "fastify": "4.5.3",
"fastify-plugin": "4.2.1", "fastify-plugin": "4.2.1",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"got": "12.3.1", "got": "12.4.1",
"is-ip": "5.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0", "is-port-reachable": "4.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
@@ -52,20 +52,20 @@
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.7.13", "@types/node": "18.7.15",
"@types/node-os-utils": "1.3.0", "@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.35.1", "@typescript-eslint/eslint-plugin": "5.36.2",
"@typescript-eslint/parser": "5.35.1", "@typescript-eslint/parser": "5.36.2",
"esbuild": "0.15.5", "esbuild": "0.15.7",
"eslint": "8.23.0", "eslint": "8.23.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19", "nodemon": "2.0.19",
"prettier": "2.7.1", "prettier": "2.7.1",
"prisma": "3.15.2", "prisma": "4.3.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"tsconfig-paths": "4.1.0", "tsconfig-paths": "4.1.0",
"typescript": "4.7.4" "typescript": "4.8.2"
}, },
"prisma": { "prisma": {
"seed": "node prisma/seed.js" "seed": "node prisma/seed.js"

View File

@@ -0,0 +1,22 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,20 @@
/*
Warnings:
- You are about to alter the column `time` on the `BuildLog` table. The data in that column could be lost. The data in that column will be cast from `Int` to `BigInt`.
*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_BuildLog" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT,
"buildId" TEXT NOT NULL,
"line" TEXT NOT NULL,
"time" BIGINT NOT NULL
);
INSERT INTO "new_BuildLog" ("applicationId", "buildId", "id", "line", "time") SELECT "applicationId", "buildId", "id", "line", "time" FROM "BuildLog";
DROP TABLE "BuildLog";
ALTER TABLE "new_BuildLog" RENAME TO "BuildLog";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "ApplicationConnectedDatabase" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"databaseId" TEXT,
"hostedDatabaseType" TEXT,
"hostedDatabaseHost" TEXT,
"hostedDatabasePort" INTEGER,
"hostedDatabaseName" TEXT,
"hostedDatabaseUser" TEXT,
"hostedDatabasePassword" TEXT,
"hostedDatabaseDBName" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationConnectedDatabase_databaseId_fkey" FOREIGN KEY ("databaseId") REFERENCES "Database" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "ApplicationConnectedDatabase_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "ApplicationConnectedDatabase_applicationId_key" ON "ApplicationConnectedDatabase"("applicationId");

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Setting" ADD COLUMN "isAPIDebuggingEnabled" BOOLEAN DEFAULT false;

View File

@@ -11,6 +11,7 @@ datasource db {
model Setting { model Setting {
id String @id @default(cuid()) id String @id @default(cuid())
fqdn String? @unique fqdn String? @unique
isAPIDebuggingEnabled Boolean? @default(false)
isRegistrationEnabled Boolean @default(false) isRegistrationEnabled Boolean @default(false)
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
minPort Int @default(9000) minPort Int @default(9000)
@@ -117,6 +118,24 @@ model Application {
settings ApplicationSettings? settings ApplicationSettings?
secrets Secret[] secrets Secret[]
teams Team[] teams Team[]
connectedDatabase ApplicationConnectedDatabase?
}
model ApplicationConnectedDatabase {
id String @id @default(cuid())
applicationId String @unique
databaseId String?
hostedDatabaseType String?
hostedDatabaseHost String?
hostedDatabasePort Int?
hostedDatabaseName String?
hostedDatabaseUser String?
hostedDatabasePassword String?
hostedDatabaseDBName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
database Database? @relation(fields: [databaseId], references: [id])
application Application @relation(fields: [applicationId], references: [id])
} }
model ApplicationSettings { model ApplicationSettings {
@@ -128,6 +147,7 @@ model ApplicationSettings {
autodeploy Boolean @default(true) autodeploy Boolean @default(true)
isBot Boolean @default(false) isBot Boolean @default(false)
isPublicRepository Boolean @default(false) isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
@@ -186,7 +206,7 @@ model BuildLog {
applicationId String? applicationId String?
buildId String buildId String
line String line String
time Int time BigInt
} }
model Build { model Build {
@@ -291,22 +311,23 @@ model GitlabApp {
} }
model Database { model Database {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
publicPort Int? publicPort Int?
defaultDatabase String? defaultDatabase String?
type String? type String?
version String? version String?
dbUser String? dbUser String?
dbUserPassword String? dbUserPassword String?
rootUser String? rootUser String?
rootUserPassword String? rootUserPassword String?
destinationDockerId String? destinationDockerId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
settings DatabaseSettings? settings DatabaseSettings?
teams Team[] teams Team[]
applicationConnectedDatabase ApplicationConnectedDatabase[]
} }
model DatabaseSettings { model DatabaseSettings {

View File

@@ -17,7 +17,6 @@ const algorithm = 'aes-256-ctr';
async function main() { async function main() {
// Enable registration for the first user // Enable registration for the first user
// Set initial HAProxy password
const settingsFound = await prisma.setting.findFirst({}); const settingsFound = await prisma.setting.findFirst({});
if (!settingsFound) { if (!settingsFound) {
await prisma.setting.create({ await prisma.setting.create({
@@ -25,7 +24,8 @@ async function main() {
isRegistrationEnabled: true, isRegistrationEnabled: true,
proxyPassword: encrypt(generatePassword()), proxyPassword: encrypt(generatePassword()),
proxyUser: cuid(), proxyUser: cuid(),
arch: process.arch arch: process.arch,
DNSServers: '1.1.1.1,8.8.8.8'
} }
}); });
} else { } else {

View File

@@ -5,7 +5,7 @@ import env from '@fastify/env';
import cookie from '@fastify/cookie'; import cookie from '@fastify/cookie';
import path, { join } from 'path'; import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import { asyncExecShell, isDev, listSettings, prisma, version } from './lib/common'; import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
import { scheduler } from './lib/scheduler'; import { scheduler } from './lib/scheduler';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful' import Graceful from '@ladjs/graceful'
@@ -26,119 +26,143 @@ declare module 'fastify' {
const port = isDev ? 3001 : 3000; const port = isDev ? 3001 : 3000;
const host = '0.0.0.0'; const host = '0.0.0.0';
const fastify = Fastify({ prisma.setting.findFirst().then(async (settings) => {
logger: false, const fastify = Fastify({
trustProxy: true logger: settings?.isAPIDebuggingEnabled || false,
}); trustProxy: true
const schema = {
type: 'object',
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
properties: {
COOLIFY_APP_ID: {
type: 'string',
},
COOLIFY_SECRET_KEY: {
type: 'string',
},
COOLIFY_DATABASE_URL: {
type: 'string',
default: 'file:../db/dev.db'
},
COOLIFY_SENTRY_DSN: {
type: 'string',
default: null
},
COOLIFY_IS_ON: {
type: 'string',
default: 'docker'
},
COOLIFY_WHITE_LABELED: {
type: 'string',
default: 'false'
},
COOLIFY_WHITE_LABELED_ICON: {
type: 'string',
default: null
},
COOLIFY_AUTO_UPDATE: {
type: 'string',
default: 'false'
},
}
};
const options = {
schema,
dotenv: true
};
fastify.register(env, options);
if (!isDev) {
fastify.register(serve, {
root: path.join(__dirname, './public'),
preCompressed: true
}); });
fastify.setNotFoundHandler(async function (request, reply) { const schema = {
if (request.raw.url && request.raw.url.startsWith('/api')) { type: 'object',
return reply.status(404).send({ required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
success: false properties: {
}); COOLIFY_APP_ID: {
} type: 'string',
return reply.status(200).sendFile('index.html'); },
}); COOLIFY_SECRET_KEY: {
} type: 'string',
fastify.register(autoLoad, { },
dir: join(__dirname, 'plugins') COOLIFY_DATABASE_URL: {
}); type: 'string',
fastify.register(autoLoad, { default: 'file:../db/dev.db'
dir: join(__dirname, 'routes') },
}); COOLIFY_SENTRY_DSN: {
type: 'string',
default: null
},
COOLIFY_IS_ON: {
type: 'string',
default: 'docker'
},
COOLIFY_WHITE_LABELED: {
type: 'string',
default: 'false'
},
COOLIFY_WHITE_LABELED_ICON: {
type: 'string',
default: null
},
COOLIFY_AUTO_UPDATE: {
type: 'string',
default: 'false'
},
fastify.register(cookie) }
fastify.register(cors); };
fastify.listen({ port, host }, async (err: any, address: any) => {
if (err) { const options = {
console.error(err); schema,
process.exit(1); dotenv: true
};
fastify.register(env, options);
if (!isDev) {
fastify.register(serve, {
root: path.join(__dirname, './public'),
preCompressed: true
});
fastify.setNotFoundHandler(async function (request, reply) {
if (request.raw.url && request.raw.url.startsWith('/api')) {
return reply.status(404).send({
success: false
});
}
return reply.status(200).sendFile('index.html');
});
} }
console.log(`Coolify's API is listening on ${host}:${port}`); fastify.register(autoLoad, {
await initServer(); dir: join(__dirname, 'plugins')
});
fastify.register(autoLoad, {
dir: join(__dirname, 'routes')
});
const graceful = new Graceful({ brees: [scheduler] }); fastify.register(cookie)
graceful.listen(); fastify.register(cors);
fastify.addHook('onRequest', async (request, reply) => {
let allowedList = ['coolify:3000'];
const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({})
setInterval(async () => { ipv4 && allowedList.push(`${ipv4}:3000`);
if (!scheduler.workers.has('deployApplication')) { ipv6 && allowedList.push(ipv6);
scheduler.run('deployApplication'); fqdn && allowedList.push(getDomain(fqdn));
isDev && allowedList.push('localhost:3000') && allowedList.push('localhost:3001') && allowedList.push('host.docker.internal:3001');
const remotes = await prisma.destinationDocker.findMany({ where: { remoteEngine: true, remoteVerified: true } })
if (remotes.length > 0) {
remotes.forEach(remote => {
allowedList.push(`${remote.remoteIpAddress}:3000`);
})
} }
if (!scheduler.workers.has('infrastructure')) { if (!allowedList.includes(request.headers.host)) {
scheduler.run('infrastructure'); // console.log('not allowed', request.headers.host)
} }
}, 2000) })
fastify.listen({ port, host }, async (err: any, address: any) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Coolify's API is listening on ${host}:${port}`);
await initServer();
// autoUpdater const graceful = new Graceful({ brees: [scheduler] });
setInterval(async () => { graceful.listen();
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
}, isDev ? 5000 : 60000 * 15)
// cleanupStorage setInterval(async () => {
setInterval(async () => { if (!scheduler.workers.has('deployApplication')) {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage") scheduler.run('deployApplication');
}, isDev ? 6000 : 60000 * 10) }
if (!scheduler.workers.has('infrastructure')) {
scheduler.run('infrastructure');
}
}, 2000)
// checkProxies // autoUpdater
setInterval(async () => { setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies") scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
}, 10000) }, isDev ? 5000 : 60000 * 15)
// cleanupPrismaEngines // cleanupStorage
// setInterval(async () => { setInterval(async () => {
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines") scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
// }, 60000) }, isDev ? 6000 : 60000 * 10)
// checkProxies
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
}, 10000)
// cleanupPrismaEngines
// setInterval(async () => {
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
// }, 60000)
await Promise.all([
getArch(),
getIPAddress(),
configureRemoteDockers(),
])
});
})
await getArch();
await getIPAddress();
});
async function getIPAddress() { async function getIPAddress() {
const { publicIpv4, publicIpv6 } = await import('public-ip') const { publicIpv4, publicIpv6 } = await import('public-ip')
try { try {
@@ -175,4 +199,15 @@ async function getArch() {
} catch (error) { } } catch (error) { }
} }
async function configureRemoteDockers() {
try {
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteVerified: true, remoteEngine: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
await createRemoteEngineConfiguration(docker.id)
}
}
} catch (error) { }
}

View File

@@ -177,9 +177,7 @@ import * as buildpacks from '../lib/buildPacks';
try { try {
await prisma.build.update({ where: { id: buildId }, data: { commit } }); await prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) { } catch (err) { }
console.log(err);
}
if (!pullmergeRequestId) { if (!pullmergeRequestId) {
if (configHash !== currentHash) { if (configHash !== currentHash) {
@@ -359,21 +357,15 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error, buildId, applicationId: application.id }); await saveBuildLog({ line: error, buildId, applicationId: application.id });
} }
}); });
} }
await pAll.default(actions, { concurrency }) await pAll.default(actions, { concurrency })
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} finally {
} }
}) })
while (true) { while (true) {
await th() await th()
} }
} else process.exit(0); } else process.exit(0);
})(); })();

View File

@@ -1,7 +1,7 @@
import { parentPort } from 'node:worker_threads'; import { parentPort } from 'node:worker_threads';
import axios from 'axios'; import axios from 'axios';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version } from '../lib/common'; import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration } from '../lib/common';
async function autoUpdater() { async function autoUpdater() {
try { try {
@@ -21,7 +21,6 @@ async function autoUpdater() {
const activeCount = 0 const activeCount = 0
if (activeCount === 0) { if (activeCount === 0) {
if (!isDev) { if (!isDev) {
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`); await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell( await asyncExecShell(
@@ -35,9 +34,7 @@ async function autoUpdater() {
} }
} }
} }
} catch (error) { } catch (error) { }
console.log(error);
}
} }
async function checkProxies() { async function checkProxies() {
try { try {
@@ -45,18 +42,35 @@ async function checkProxies() {
let portReachable; let portReachable;
const { arch, ipv4, ipv6 } = await listSettings(); const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local // Coolify Proxy local
const engine = '/var/run/docker.sock'; const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({ const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' } where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
}); });
if (localDocker && localDocker.isCoolifyProxyUsed) { if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 }) portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) { if (!portReachable) {
await startTraefikProxy(localDocker.id); await startTraefikProxy(localDocker.id);
} }
} }
// Coolify Proxy remote
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteEngine: true, remoteVerified: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
if (!portReachable) {
await startTraefikProxy(docker.id);
}
}
try {
await createRemoteEngineConfiguration(docker.id)
} catch (error) { }
}
}
// TCP Proxies // TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({ const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } }, where: { publicPort: { not: null } },
@@ -113,9 +127,7 @@ async function cleanupPrismaEngines() {
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) { if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`) await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
} }
} catch (error) { } catch (error) { }
console.log(error);
}
} }
} }
async function cleanupStorage() { async function cleanupStorage() {
@@ -166,9 +178,7 @@ async function cleanupStorage() {
lowDiskSpace = true; lowDiskSpace = true;
} }
} }
} catch (error) { } catch (error) { }
console.log(error);
}
await cleanupDockerStorage(destination.id, lowDiskSpace, false) await cleanupDockerStorage(destination.id, lowDiskSpace, false)
} }
} }

View File

@@ -89,6 +89,22 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
} }
]; ];
const phpVersions = [ const phpVersions = [
{
value: 'webdevops/php-apache:8.2',
label: 'webdevops/php-apache:8.2'
},
{
value: 'webdevops/php-nginx:8.2',
label: 'webdevops/php-nginx:8.2'
},
{
value: 'webdevops/php-apache:8.1',
label: 'webdevops/php-apache:8.1'
},
{
value: 'webdevops/php-nginx:8.1',
label: 'webdevops/php-nginx:8.1'
},
{ {
value: 'webdevops/php-apache:8.0', value: 'webdevops/php-apache:8.0',
label: 'webdevops/php-apache:8.0' label: 'webdevops/php-apache:8.0'
@@ -145,6 +161,22 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
value: 'webdevops/php-nginx:5.6', value: 'webdevops/php-nginx:5.6',
label: 'webdevops/php-nginx:5.6' label: 'webdevops/php-nginx:5.6'
}, },
{
value: 'webdevops/php-apache:8.2-alpine',
label: 'webdevops/php-apache:8.2-alpine'
},
{
value: 'webdevops/php-nginx:8.2-alpine',
label: 'webdevops/php-nginx:8.2-alpine'
},
{
value: 'webdevops/php-apache:8.1-alpine',
label: 'webdevops/php-apache:8.1-alpine'
},
{
value: 'webdevops/php-nginx:8.1-alpine',
label: 'webdevops/php-nginx:8.1-alpine'
},
{ {
value: 'webdevops/php-apache:8.0-alpine', value: 'webdevops/php-apache:8.0-alpine',
label: 'webdevops/php-apache:8.0-alpine' label: 'webdevops/php-apache:8.0-alpine'
@@ -305,11 +337,11 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
payload.baseImage = 'denoland/deno:latest'; payload.baseImage = 'denoland/deno:latest';
} }
if (buildPack === 'php') { if (buildPack === 'php') {
payload.baseImage = 'webdevops/php-apache:8.0-alpine'; payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseImages = phpVersions; payload.baseImages = phpVersions;
} }
if (buildPack === 'laravel') { if (buildPack === 'laravel') {
payload.baseImage = 'webdevops/php-apache:8.0-alpine'; payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseBuildImage = 'node:18'; payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} }
@@ -512,7 +544,6 @@ export async function copyBaseConfigurationFiles(
); );
} }
} catch (error) { } catch (error) {
console.log(error);
throw new Error(error); throw new Error(error);
} }
} }

View File

@@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common'; import { includeServices } from './services/common';
export const version = '3.9.0-rc.1'; export const version = '3.9.2';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
@@ -439,7 +439,6 @@ export async function getFreeSSHLocalPort(id: string): Promise<number | boolean>
return Number(alreadyConfigured.sshLocalPort) return Number(alreadyConfigured.sshLocalPort)
} }
const range = generateRangeArray(minPort, maxPort) const range = generateRangeArray(minPort, maxPort)
console.log({ ports })
const availablePorts = range.filter(port => !ports.map(p => p.sshLocalPort).includes(port)) const availablePorts = range.filter(port => !ports.map(p => p.sshLocalPort).includes(port))
for (const port of availablePorts) { for (const port of availablePorts) {
const found = await isReachable(port, { host: 'localhost' }) const found = await isReachable(port, { host: 'localhost' })
@@ -458,20 +457,21 @@ export async function createRemoteEngineConfiguration(id: string) {
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } }) const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 }) await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
// Needed for remote docker compose // Needed for remote docker compose
const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(`ps ax | grep [s]sh-agent | grep ssh-agent.pid | grep -v grep | wc -l`) const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(`ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`)
if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) { if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
await asyncExecShell(`eval $(ssh-agent -sa /tmp/ssh-agent.pid)`) try {
await fs.stat(`/tmp/coolify-ssh-agent.pid`)
await fs.rm(`/tmp/coolify-ssh-agent.pid`)
} catch (error) { }
await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`)
} }
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh-add -q ${sshKeyFile}`) await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`)
const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`) const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`)
if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) { if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
try { try {
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`) await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`)
} catch (error) { }
} catch (error) {
console.log(error)
}
} }
const config = sshConfig.parse('') const config = sshConfig.parse('')
@@ -974,9 +974,14 @@ export const createDirectories = async ({
}): Promise<{ workdir: string; repodir: string }> => { }): Promise<{ workdir: string; repodir: string }> => {
const repodir = `/tmp/build-sources/${repository}/`; const repodir = `/tmp/build-sources/${repository}/`;
const workdir = `/tmp/build-sources/${repository}/${buildId}`; const workdir = `/tmp/build-sources/${repository}/${buildId}`;
let workdirFound = false;
try {
workdirFound = !!(await fs.stat(workdir));
} catch (error) { }
if (workdirFound) {
await asyncExecShell(`rm -fr ${workdir}`);
}
await asyncExecShell(`mkdir -p ${workdir}`); await asyncExecShell(`mkdir -p ${workdir}`);
return { return {
workdir, workdir,
repodir repodir
@@ -1248,7 +1253,6 @@ export async function startTraefikTCPProxy(
}) })
} }
} catch (error) { } catch (error) {
console.log(error);
return error; return error;
} }
} }
@@ -1436,15 +1440,13 @@ export function convertTolOldVolumeNames(type) {
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
// Cleanup old coolify images // Cleanup old coolify images
try { try {
let { stdout: images } = await executeDockerCmd({ dockerId, command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs` }) let { stdout: images } = await executeDockerCmd({ dockerId, command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r` })
images = images.trim(); images = images.trim();
if (images) { if (images) {
await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs` }) await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r` })
} }
} catch (error) { } catch (error) { }
//console.log(error);
}
if (lowDiskSpace || force) { if (lowDiskSpace || force) {
if (isDev) { if (isDev) {
if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`); if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`);
@@ -1452,25 +1454,17 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
} }
try { try {
await executeDockerCmd({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` }) await executeDockerCmd({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` })
} catch (error) { } catch (error) { }
//console.log(error);
}
try { try {
await executeDockerCmd({ dockerId, command: `docker image prune -f` }) await executeDockerCmd({ dockerId, command: `docker image prune -f` })
} catch (error) { } catch (error) { }
//console.log(error);
}
try { try {
await executeDockerCmd({ dockerId, command: `docker image prune -a -f` }) await executeDockerCmd({ dockerId, command: `docker image prune -a -f` })
} catch (error) { } catch (error) { }
//console.log(error);
}
// Cleanup build caches // Cleanup build caches
try { try {
await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` }) await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` })
} catch (error) { } catch (error) { }
//console.log(error);
}
} }
} }

View File

@@ -76,7 +76,6 @@ export async function removeContainer({
await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
} }
} catch (error) { } catch (error) {
console.log(error);
throw error; throw error;
} }
} }

View File

@@ -198,7 +198,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.plausibleAnalytics) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
@@ -333,6 +333,8 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
image: `${image}:${version}`, image: `${image}:${version}`,
volumes: [`${id}-minio-data:/data`], volumes: [`${id}-minio-data:/data`],
environmentVariables: { environmentVariables: {
MINIO_SERVER_URL: fqdn,
MINIO_DOMAIN: getDomain(fqdn),
MINIO_ROOT_USER: rootUser, MINIO_ROOT_USER: rootUser,
MINIO_ROOT_PASSWORD: rootUserPassword, MINIO_ROOT_PASSWORD: rootUserPassword,
MINIO_BROWSER_REDIRECT_URL: fqdn MINIO_BROWSER_REDIRECT_URL: fqdn
@@ -852,7 +854,7 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.ghost) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1086,7 +1088,7 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
FROM ${config.postgresql.image} FROM ${config.postgresql.image}
COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`; COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.umami) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1114,6 +1116,7 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
}, },
volumes: volumeMounts volumes: volumeMounts
}; };
console.log(composeFile)
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination) await startServiceContainers(destinationDocker.id, composeFileDestination)
@@ -1167,7 +1170,7 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.hasura) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1272,7 +1275,7 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
config.fider.environmentVariables[secret.name] = secret.value; config.fider.environmentVariables[secret.name] = secret.value;
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.fider) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1880,7 +1883,7 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
config.moodle.environmentVariables[secret.name] = secret.value; config.moodle.environmentVariables[secret.name] = secret.value;
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.moodle) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -2006,7 +2009,7 @@ async function startGlitchTipService(request: FastifyRequest<ServiceStartStop>)
config.glitchTip.environmentVariables[secret.name] = secret.value; config.glitchTip.environmentVariables[secret.name] = secret.value;
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {

View File

@@ -21,7 +21,6 @@ export default fp<FastifyJWTOptions>(async (fastify, opts) => {
try { try {
await request.jwtVerify() await request.jwtVerify()
} catch (err) { } catch (err) {
console.log(err)
reply.send(err) reply.send(err)
} }
}) })

View File

@@ -156,7 +156,8 @@ export async function getApplicationFromDB(id: string, teamId: string) {
settings: true, settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } }, gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true, secrets: true,
persistentStorage: true persistentStorage: true,
connectedDatabase: true
} }
}); });
if (!application) { if (!application) {
@@ -190,7 +191,8 @@ export async function getApplicationFromDBWebhook(projectId: number, branch: str
settings: true, settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } }, gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true, secrets: true,
persistentStorage: true persistentStorage: true,
connectedDatabase: true
} }
}); });
if (applications.length === 0) { if (applications.length === 0) {
@@ -242,7 +244,8 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
denoOptions, denoOptions,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType deploymentType,
baseDatabaseBranch
} = request.body } = request.body
if (port) port = Number(port); if (port) port = Number(port);
if (exposePort) { if (exposePort) {
@@ -263,22 +266,43 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
dockerFileLocation, dockerFileLocation,
denoMainFile denoMainFile
}); });
await prisma.application.update({ if (baseDatabaseBranch) {
where: { id }, await prisma.application.update({
data: { where: { id },
name, data: {
fqdn, name,
exposePort, fqdn,
pythonWSGI, exposePort,
pythonModule, pythonWSGI,
pythonVariable, pythonModule,
denoOptions, pythonVariable,
baseImage, denoOptions,
baseBuildImage, baseImage,
deploymentType, baseBuildImage,
...defaultConfiguration deploymentType,
} ...defaultConfiguration,
}); connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
}
});
} else {
await prisma.application.update({
where: { id },
data: {
name,
fqdn,
exposePort,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
baseImage,
baseBuildImage,
deploymentType,
...defaultConfiguration
}
});
}
return reply.code(201).send(); return reply.code(201).send();
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@@ -289,7 +313,7 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) { export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot } = request.body const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching } = request.body
// const isDouble = await checkDoubleBranch(branch, projectId); // const isDouble = await checkDoubleBranch(branch, projectId);
// if (isDouble && autodeploy) { // if (isDouble && autodeploy) {
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
@@ -297,7 +321,7 @@ export async function saveApplicationSettings(request: FastifyRequest<SaveApplic
// } // }
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot } } }, data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching } } },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
return reply.code(201).send(); return reply.code(201).send();
@@ -478,6 +502,7 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
await prisma.build.deleteMany({ where: { applicationId: id } }); await prisma.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } }); await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } });
if (teamId === '0') { if (teamId === '0') {
await prisma.application.deleteMany({ where: { id } }); await prisma.application.deleteMany({ where: { id } });
} else { } else {
@@ -501,10 +526,12 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
export async function checkDNS(request: FastifyRequest<CheckDNS>) { export async function checkDNS(request: FastifyRequest<CheckDNS>) {
try { try {
const { id } = request.params const { id } = request.params
let { exposePort, fqdn, forceSave, dualCerts } = request.body let { exposePort, fqdn, forceSave, dualCerts } = request.body
if (!fqdn) {
if (fqdn) fqdn = fqdn.toLowerCase(); return {}
} else {
fqdn = fqdn.toLowerCase();
}
if (exposePort) exposePort = Number(exposePort); if (exposePort) exposePort = Number(exposePort);
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
@@ -734,6 +761,16 @@ export async function saveBuildPack(request, reply) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function saveConnectedDatabase(request, reply) {
try {
const { id } = request.params
const { databaseId, type } = request.body
await prisma.application.update({ where: { id }, data: { connectedDatabase: { upsert: { create: { database: { connect: { id: databaseId } }, hostedDatabaseType: type }, update: { database: { connect: { id: databaseId } }, hostedDatabaseType: type } } } } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getSecrets(request: FastifyRequest<OnlyId>) { export async function getSecrets(request: FastifyRequest<OnlyId>) {
try { try {
@@ -890,7 +927,6 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
}) })
} }
} catch ({ status, message }) { } catch ({ status, message }) {
console.log({ status, message })
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
@@ -982,11 +1018,13 @@ export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
where: { buildId, time: { gt: sequence } }, where: { buildId, time: { gt: sequence } },
orderBy: { time: 'asc' } orderBy: { time: 'asc' }
}); });
const data = await prisma.build.findFirst({ where: { id: buildId } }); const data = await prisma.build.findFirst({ where: { id: buildId } });
const createdAt = day(data.createdAt).utc(); const createdAt = day(data.createdAt).utc();
return { return {
logs, logs: logs.map(log => {
log.time = Number(log.time)
return log
}),
took: day().diff(createdAt) / 1000, took: day().diff(createdAt) / 1000,
status: data?.status || 'queued' status: data?.status || 'queued'
} }
@@ -1063,3 +1101,58 @@ export async function cancelDeployment(request: FastifyRequest<CancelDeployment>
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function createdBranchDatabase(database: any, baseDatabaseBranch: string, pullmergeRequestId: string) {
try {
if (!baseDatabaseBranch) return
const { id, type, destinationDockerId, rootUser, rootUserPassword, dbUser } = database;
if (destinationDockerId) {
if (type === 'postgresql') {
const decryptedRootUserPassword = decrypt(rootUserPassword);
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"`
})
}
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function removeBranchDatabase(database: any, pullmergeRequestId: string) {
try {
const { id, type, destinationDockerId, rootUser, rootUserPassword } = database;
if (destinationDockerId) {
if (type === 'postgresql') {
const decryptedRootUserPassword = decrypt(rootUserPassword);
// Terminate all connections to the database
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"`
})
}
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
@@ -55,6 +55,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/:id/configuration/buildpack', async (request) => await getBuildPack(request)); fastify.get('/:id/configuration/buildpack', async (request) => await getBuildPack(request));
fastify.post('/:id/configuration/buildpack', async (request, reply) => await saveBuildPack(request, reply)); fastify.post('/:id/configuration/buildpack', async (request, reply) => await saveBuildPack(request, reply));
fastify.post('/:id/configuration/database', async (request, reply) => await saveConnectedDatabase(request, reply));
fastify.get<OnlyId>('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request)); fastify.get<OnlyId>('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request));
fastify.post<OnlyId>('/:id/configuration/sshkey', async (request, reply) => await saveGitLabSSHKey(request, reply)); fastify.post<OnlyId>('/:id/configuration/sshkey', async (request, reply) => await saveGitLabSSHKey(request, reply));

View File

@@ -20,12 +20,13 @@ export interface SaveApplication extends OnlyId {
denoOptions: string, denoOptions: string,
baseImage: string, baseImage: string,
baseBuildImage: string, baseBuildImage: string,
deploymentType: string deploymentType: string,
baseDatabaseBranch: string
} }
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; }; Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean };
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };

View File

@@ -280,15 +280,12 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` }) await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` })
} catch (error) { } catch (error) { }
console.log(error);
}
try { try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` }) await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
return {}; return {};
} catch (error) { } catch (error) {
console.log(error)
throw { throw {
error error
}; };

View File

@@ -30,7 +30,6 @@ export async function listDestinations(request: FastifyRequest<ListDestinations>
destinations destinations
} }
} catch ({ status, message }) { } catch ({ status, message }) {
console.log({ status, message })
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
@@ -114,7 +113,6 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
} }
} catch ({ status, message }) { } catch ({ status, message }) {
console.log({ status, message })
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
@@ -162,7 +160,6 @@ export async function startProxy(request: FastifyRequest<Proxy>) {
await startTraefikProxy(id); await startTraefikProxy(id);
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
console.log({ status, message })
await stopTraefikProxy(id); await stopTraefikProxy(id);
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
@@ -205,23 +202,21 @@ export async function assignSSHKey(request: FastifyRequest) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) { export async function verifyRemoteDockerEngine(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try { try {
const { id } = request.params; const { id } = request.params;
await createRemoteEngineConfiguration(id); await createRemoteEngineConfiguration(id);
const { remoteIpAddress, remoteUser, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } })
const { remoteIpAddress, remoteUser, network } = await prisma.destinationDocker.findFirst({ where: { id } })
const host = `ssh://${remoteUser}@${remoteIpAddress}` const host = `ssh://${remoteUser}@${remoteIpAddress}`
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`); const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
if (!stdout) { if (!stdout) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`); await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
} }
const { stdout:coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`); const { stdout: coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`);
if (!coolifyNetwork) { if (!coolifyNetwork) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`); await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
} }
if (isCoolifyProxyUsed) await startTraefikProxy(id);
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } }) await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
return reply.code(201).send() return reply.code(201).send()
@@ -234,7 +229,7 @@ export async function getDestinationStatus(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
const destination = await prisma.destinationDocker.findUnique({ where: { id } }) const destination = await prisma.destinationDocker.findUnique({ where: { id } })
const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy' }) const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true })
return { return {
isRunning isRunning
} }

View File

@@ -23,7 +23,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request)); fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request));
fastify.post('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply)); fastify.post<OnlyId>('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply));
}; };
export default root; export default root;

View File

@@ -65,7 +65,6 @@ export async function update(request: FastifyRequest<Update>) {
); );
return {}; return {};
} else { } else {
console.log(latestVersion);
await asyncSleep(2000); await asyncSleep(2000);
return {}; return {};
} }
@@ -78,10 +77,9 @@ export async function restartCoolify(request: FastifyRequest<any>) {
const teamId = request.user.teamId; const teamId = request.user.teamId;
if (teamId === '0') { if (teamId === '0') {
if (!isDev) { if (!isDev) {
await asyncExecShell(`docker restart coolify`); asyncExecShell(`docker restart coolify`);
return {}; return {};
} else { } else {
console.log('Restarting Coolify')
return {}; return {};
} }
} }

View File

@@ -560,10 +560,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
}) })
} }
} catch (error) { } catch (error) { }
console.log(error);
//
}
const volumes = [ const volumes = [
`${id}-wordpress-data:/home/${ftpUser}/wordpress`, `${id}-wordpress-data:/home/${ftpUser}/wordpress`,
`${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' `${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
@@ -642,9 +639,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
await asyncExecShell( await asyncExecShell(
`rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh` `rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
); );
} catch (error) { } catch (error) { }
console.log(error)
}
} }

View File

@@ -28,6 +28,7 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
try { try {
const { const {
fqdn, fqdn,
isAPIDebuggingEnabled,
isRegistrationEnabled, isRegistrationEnabled,
dualCerts, dualCerts,
minPort, minPort,
@@ -39,7 +40,7 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
const { id } = await listSettings(); const { id } = await listSettings();
await prisma.setting.update({ await prisma.setting.update({
where: { id }, where: { id },
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers } data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled }
}); });
if (fqdn) { if (fqdn) {
await prisma.setting.update({ where: { id }, data: { fqdn } }); await prisma.setting.update({ where: { id }, data: { fqdn } });

View File

@@ -3,6 +3,7 @@ import { OnlyId } from "../../../../types"
export interface SaveSettings { export interface SaveSettings {
Body: { Body: {
fqdn: string, fqdn: string,
isAPIDebuggingEnabled: boolean,
isRegistrationEnabled: boolean, isRegistrationEnabled: boolean,
dualCerts: boolean, dualCerts: boolean,
minPort: number, minPort: number,

View File

@@ -3,8 +3,7 @@ import cuid from "cuid";
import crypto from "crypto"; import crypto from "crypto";
import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common"; import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker"; import { checkContainer, removeContainer } from "../../../lib/docker";
import { scheduler } from "../../../lib/scheduler"; import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
import { getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
import type { FastifyReply, FastifyRequest } from "fastify"; import type { FastifyReply, FastifyRequest } from "fastify";
import type { GitHubEvents, InstallGithub } from "./types"; import type { GitHubEvents, InstallGithub } from "./types";
@@ -67,7 +66,6 @@ export async function configureGitHubApp(request, reply) {
} }
export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> { export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> {
try { try {
const allowedGithubEvents = ['push', 'pull_request']; const allowedGithubEvents = ['push', 'pull_request'];
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed']; const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase(); const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
@@ -102,8 +100,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
const checksum = Buffer.from(githubSignature, 'utf8'); const checksum = Buffer.from(githubSignature, 'utf8');
//@ts-ignore //@ts-ignore
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) { if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
console.log('SHA256 checksum failed. Are you doing something fishy?') throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' }
// throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?'
}; };
} }
@@ -133,8 +130,6 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
where: { id: application.id }, where: { id: application.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
console.log(application.id)
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,
@@ -178,6 +173,16 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
where: { id: application.id }, where: { id: application.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
// Coolify hosted database
if (application.connectedDatabase.databaseId) {
const databaseId = application.connectedDatabase.databaseId;
const database = await prisma.database.findUnique({ where: { id: databaseId } });
if (database) {
await createdBranchDatabase(database, application.connectedDatabase.hostedDatabaseDBName, pullmergeRequestId);
}
}
}
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,
@@ -197,9 +202,17 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
} else if (pullmergeRequestAction === 'closed') { } else if (pullmergeRequestAction === 'closed') {
if (application.destinationDockerId) { if (application.destinationDockerId) {
const id = `${application.id}-${pullmergeRequestId}`; const id = `${application.id}-${pullmergeRequestId}`;
await removeContainer({ id, dockerId: application.destinationDocker.id }); try {
await removeContainer({ id, dockerId: application.destinationDocker.id });
} catch (error) { }
}
if (application.connectedDatabase.databaseId) {
const databaseId = application.connectedDatabase.databaseId;
const database = await prisma.database.findUnique({ where: { id: databaseId } });
if (database) {
await removeBranchDatabase(database, pullmergeRequestId);
}
} }
} }
} }
} }

View File

@@ -133,7 +133,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,
pullmergeRequestId, pullmergeRequestId: pullmergeRequestId.toString(),
sourceBranch, sourceBranch,
applicationId: application.id, applicationId: application.id,
destinationDockerId: application.destinationDocker.id, destinationDockerId: application.destinationDocker.id,

View File

@@ -1,4 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../types';
import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers'; import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers';
import { TraefikOtherConfiguration } from './types'; import { TraefikOtherConfiguration } from './types';
@@ -6,7 +7,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply)); fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply));
fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request)); fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request));
fastify.get('/remote/:id', async (request) => remoteTraefikConfiguration(request)); fastify.get<OnlyId>('/remote/:id', async (request) => remoteTraefikConfiguration(request));
}; };
export default root; export default root;

View File

@@ -19,11 +19,11 @@
"@popperjs/core": "2.11.6", "@popperjs/core": "2.11.6",
"@sveltejs/kit": "1.0.0-next.405", "@sveltejs/kit": "1.0.0-next.405",
"@types/js-cookie": "3.0.2", "@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.35.1", "@typescript-eslint/eslint-plugin": "5.36.1",
"@typescript-eslint/parser": "5.35.1", "@typescript-eslint/parser": "5.36.1",
"autoprefixer": "10.4.8", "autoprefixer": "10.4.8",
"classnames": "2.3.1", "classnames": "2.3.1",
"eslint": "8.22.0", "eslint": "8.23.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0", "eslint-plugin-svelte3": "4.0.0",
"flowbite": "1.5.2", "flowbite": "1.5.2",
@@ -31,21 +31,21 @@
"postcss": "8.4.16", "postcss": "8.4.16",
"prettier": "2.7.1", "prettier": "2.7.1",
"prettier-plugin-svelte": "2.7.0", "prettier-plugin-svelte": "2.7.0",
"svelte": "3.49.0", "svelte": "3.50.0",
"svelte-check": "2.8.1", "svelte-check": "2.9.0",
"svelte-preprocess": "4.10.7", "svelte-preprocess": "4.10.7",
"tailwindcss": "3.1.8", "tailwindcss": "3.1.8",
"tailwindcss-scrollbar": "0.1.0", "tailwindcss-scrollbar": "0.1.0",
"tslib": "2.4.0", "tslib": "2.4.0",
"typescript": "4.7.4", "typescript": "4.8.2",
"vite": "3.0.5" "vite": "3.1.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@sveltejs/adapter-static": "1.0.0-next.39", "@sveltejs/adapter-static": "1.0.0-next.39",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.7",
"cuid": "2.1.8", "cuid": "2.1.8",
"daisyui": "2.24.0", "daisyui": "2.24.2",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"p-limit": "4.0.0", "p-limit": "4.0.0",
"svelte-select": "4.4.7", "svelte-select": "4.4.7",

View File

@@ -16,7 +16,6 @@
updateStatus.loading = true; updateStatus.loading = true;
try { try {
if (dev) { if (dev) {
console.log(`updating to ${latestVersion}`);
await asyncSleep(4000); await asyncSleep(4000);
return window.location.reload(); return window.location.reload();
} else { } else {

View File

@@ -20,8 +20,7 @@
let usageInterval: any; let usageInterval: any;
let loading = { let loading = {
usage: false, usage: false,
cleanup: false, cleanup: false
restart: false
}; };
import { addToast, appSession } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@@ -34,25 +33,7 @@
usage = data.usage; usage = data.usage;
loading.usage = false; loading.usage = false;
} }
async function restartCoolify() {
const sure = confirm(
'Are you sure you would like to restart Coolify? Currently running deployments will be stopped and restarted.'
);
if (sure) {
loading.restart = true;
try {
await post(`/internal/restart`, {});
addToast({
type: 'success',
message: 'Coolify restarted successfully. It will take a moment.'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.restart = false;
}
}
}
onDestroy(() => { onDestroy(() => {
clearInterval(usageInterval); clearInterval(usageInterval);
}); });
@@ -89,15 +70,8 @@
<h1 class="title lg:text-3xl">Hardware Details</h1> <h1 class="title lg:text-3xl">Hardware Details</h1>
<div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0"> <div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0">
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<button <button on:click={manuallyCleanupStorage} class:loading={loading.cleanup} class="btn btn-sm"
on:click={manuallyCleanupStorage} >Cleanup Storage</button
class:loading={loading.cleanup}
class="btn btn-sm">Cleanup Storage</button
>
<button
on:click={restartCoolify}
class:loading={loading.restart}
class="btn btn-sm bg-red-600 hover:bg-red-500">Restart Coolify</button
> >
{/if} {/if}
</div> </div>

View File

@@ -306,7 +306,7 @@
"change_language": "Change Language", "change_language": "Change Language",
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.", "permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
"domain_removed": "Domain removed", "domain_removed": "Domain removed",
"ssl_explainer": "If you specify <span class='text-settings font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings font-bold'>WARNING:</span> If you change an already set domain, it will brake webhooks and other integrations! You need to manually update them.", "ssl_explainer": "If you specify <span class='text-settings font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings font-bold'>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.",
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.", "must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
"registration_allowed": "Registration allowed?", "registration_allowed": "Registration allowed?",
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.", "registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",

View File

@@ -3,6 +3,7 @@ import cuid from 'cuid';
import { writable, readable, type Writable } from 'svelte/store'; import { writable, readable, type Writable } from 'svelte/store';
interface AppSession { interface AppSession {
registrationEnabled: boolean;
ipv4: string | null, ipv4: string | null,
ipv6: string | null, ipv6: string | null,
version: string | null, version: string | null,
@@ -45,6 +46,26 @@ export const appSession: Writable<AppSession> = writable({
supportedServiceTypesAndVersions: [] supportedServiceTypesAndVersions: []
}); });
export const disabledButton: Writable<boolean> = writable(false); export const disabledButton: Writable<boolean> = writable(false);
export const isDeploymentEnabled: Writable<boolean> = writable(false);
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
return (
isAdmin &&
(application.fqdn || application.settings.isBot) &&
application.gitSource &&
application.repository &&
application.destinationDocker &&
application.buildPack
);
}
export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any) {
return (
isAdmin &&
service.fqdn &&
service.destinationDocker &&
service.version &&
service.type
);
}
export const status: Writable<any> = writable({ export const status: Writable<any> = writable({
application: { application: {
isRunning: false, isRunning: false,

View File

@@ -66,6 +66,7 @@
<script lang="ts"> <script lang="ts">
export let baseSettings: any; export let baseSettings: any;
export let supportedServiceTypesAndVersions: any; export let supportedServiceTypesAndVersions: any;
$appSession.registrationEnabled = baseSettings.registrationEnabled;
$appSession.ipv4 = baseSettings.ipv4; $appSession.ipv4 = baseSettings.ipv4;
$appSession.ipv6 = baseSettings.ipv6; $appSession.ipv6 = baseSettings.ipv6;
$appSession.version = baseSettings.version; $appSession.version = baseSettings.version;
@@ -142,7 +143,7 @@
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8" class="h-9 w-9"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -172,7 +173,7 @@
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8" class="h-9 w-9"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentcolor" stroke="currentcolor"
@@ -202,7 +203,7 @@
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8" class="h-9 w-9"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -232,7 +233,7 @@
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8" class="h-9 w-9"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -268,7 +269,7 @@
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8" class="h-9 w-9"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -295,7 +296,7 @@
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8" class="h-9 w-9"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -319,11 +320,10 @@
class="icons bg-coolgray-200" class="icons bg-coolgray-200"
class:text-iam={$page.url.pathname.startsWith('/iam')} class:text-iam={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')} class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
data-tip="IAM"
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="h-9 w-9"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
@@ -344,12 +344,11 @@
class="icons bg-coolgray-200" class="icons bg-coolgray-200"
class:text-settings={$page.url.pathname.startsWith('/settings')} class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')} class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tip="Settings"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="h-9 w-9"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
@@ -367,12 +366,11 @@
<div <div
id="logout" id="logout"
class="icons bg-coolgray-200 hover:text-error" class="icons bg-coolgray-200 hover:text-error"
data-tip="Logout"
on:click={logout} on:click={logout}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-7 w-7" class="ml-1 h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -406,7 +404,7 @@
{/if} {/if}
{/if} {/if}
<main> <main>
<div class="pl-14 lg:px-20"> <div class={$appSession.userId ? 'pl-14 lg:px-20' : null}>
<slot /> <slot />
</div> </div>
</main> </main>

View File

@@ -65,7 +65,6 @@
} }
dispatch('refresh'); dispatch('refresh');
} catch (error) { } catch (error) {
console.log(error);
return errorNotification(error); return errorNotification(error);
} }
} }

View File

@@ -60,23 +60,26 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store'; import {
appSession,
status,
location,
setLocation,
addToast,
isDeploymentEnabled,
checkIfDeploymentEnabledApplications
} from '$lib/store';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
let statusInterval: any; let statusInterval: any;
let forceDelete = false; let forceDelete = false;
$disabledButton =
!$appSession.isAdmin ||
(!application.fqdn && !application.settings.isBot) ||
!application.gitSource ||
!application.repository ||
!application.destinationDocker ||
!application.buildPack;
const { id } = $page.params; const { id } = $page.params;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
async function handleDeploySubmit(forceRebuild = false) { async function handleDeploySubmit(forceRebuild = false) {
if (!$isDeploymentEnabled) return;
try { try {
const { buildId } = await post(`/applications/${id}/deploy`, { const { buildId } = await post(`/applications/${id}/deploy`, {
...application, ...application,
@@ -106,7 +109,7 @@
await del(`/applications/${id}`, { id, force }); await del(`/applications/${id}`, { id, force });
return await goto(`/applications`); return await goto(`/applications`);
} catch (error) { } catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/ssh-agent.pid`)) { if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
forceDelete = true; forceDelete = true;
} }
return errorNotification(error); return errorNotification(error);
@@ -161,6 +164,7 @@
$status.application.isExited = false; $status.application.isExited = false;
$status.application.loading = false; $status.application.loading = false;
$location = null; $location = null;
$isDeploymentEnabled = false;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
onMount(async () => { onMount(async () => {
@@ -214,7 +218,7 @@
{#if $status.application.isExited} {#if $status.application.isExited}
<a <a
id="applicationerror" id="applicationerror"
href={!$disabledButton ? `/applications/${id}/logs` : null} href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-error" class="icons bg-transparent text-sm flex items-center text-error"
sveltekit:prefetch sveltekit:prefetch
> >
@@ -240,7 +244,7 @@
{/if} {/if}
{#if $status.application.initialLoading} {#if $status.application.initialLoading}
<button <button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out" class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -266,7 +270,7 @@
id="stop" id="stop"
on:click={stopApplication} on:click={stopApplication}
type="submit" type="submit"
disabled={$disabledButton} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-error" class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
> >
<svg <svg
@@ -290,7 +294,7 @@
id="restart" id="restart"
on:click={restartApplication} on:click={restartApplication}
type="submit" type="submit"
disabled={$disabledButton} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent text-sm flex items-center space-x-2"
> >
<svg <svg
@@ -314,7 +318,7 @@
<button <button
id="forceredeploy" id="forceredeploy"
type="submit" type="submit"
disabled={$disabledButton} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent text-sm flex items-center space-x-2"
> >
<svg <svg
@@ -341,7 +345,7 @@
<button <button
id="deploy" id="deploy"
type="submit" type="submit"
disabled={$disabledButton} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-success" class="icons bg-transparent text-sm flex items-center space-x-2 text-success"
> >
<svg <svg
@@ -364,14 +368,17 @@
<div class="border border-coolgray-500 h-8" /> <div class="border border-coolgray-500 h-8" />
<a <a
id="configurations" href={$isDeploymentEnabled ? `/applications/${id}` : null}
href={!$disabledButton ? `/applications/${id}` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-yellow-500 rounded" class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`} class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
> >
<button disabled={$disabledButton} class="icons bg-transparent text-sm"> <button
disabled={!$isDeploymentEnabled}
id="configurations"
class="icons bg-transparent text-sm"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@@ -395,15 +402,16 @@
</svg></button </svg></button
></a ></a
> >
<Tooltip triggeredBy="#configurations">Configurations</Tooltip>
<a <a
id="secrets" href={$isDeploymentEnabled ? `/applications/${id}/secrets` : null}
href={!$disabledButton ? `/applications/${id}/secrets` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-pink-500 rounded" class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`} class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
> >
<button disabled={$disabledButton} class="icons bg-transparent text-sm"> <button id="secrets" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@@ -423,15 +431,19 @@
</svg></button </svg></button
></a ></a
> >
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<a <a
id="persistentstorages" href={$isDeploymentEnabled ? `/applications/${id}/storages` : null}
href={!$disabledButton ? `/applications/${id}/storages` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-pink-500 rounded" class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`} class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`}
> >
<button disabled={$disabledButton} class="icons bg-transparent text-sm"> <button
id="persistentstorages"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@@ -449,16 +461,16 @@
</svg> </svg>
</button></a </button></a
> >
<Tooltip triggeredBy="#persistentstorages">Persistent Storages</Tooltip>
{#if !application.settings.isBot} {#if !application.settings.isBot}
<a <a
id="previews" href={$isDeploymentEnabled ? `/applications/${id}/previews` : null}
href={!$disabledButton ? `/applications/${id}/previews` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-orange-500 rounded" class="hover:text-orange-500 rounded"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`} class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
> >
<button disabled={$disabledButton} class="icons bg-transparent text-sm"> <button id="previews" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@@ -478,18 +490,19 @@
</svg></button </svg></button
></a ></a
> >
<Tooltip triggeredBy="#previews">Previews</Tooltip>
{/if} {/if}
<div class="border border-coolgray-500 h-8" /> <div class="border border-coolgray-500 h-8" />
<a <a
id="applicationlogs" href={$isDeploymentEnabled && $status.application.isRunning ? `/applications/${id}/logs` : null}
href={!$disabledButton && $status.application.isRunning ? `/applications/${id}/logs` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-sky-500 rounded" class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`} class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
> >
<button <button
disabled={$disabledButton || !$status.application.isRunning} id="applicationlogs"
disabled={!$isDeploymentEnabled || !$status.application.isRunning}
class="icons bg-transparent text-sm" class="icons bg-transparent text-sm"
> >
<svg <svg
@@ -511,15 +524,15 @@
</svg> </svg>
</button></a </button></a
> >
<Tooltip triggeredBy="#applicationlogs">Application Logs</Tooltip>
<a <a
id="buildlogs" href={$isDeploymentEnabled ? `/applications/${id}/logs/build` : null}
href={!$disabledButton ? `/applications/${id}/logs/build` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-red-500 rounded" class="hover:text-red-500 rounded"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`} class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
> >
<button disabled={$disabledButton} class="icons bg-transparent text-sm"> <button id="buildlogs" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@@ -542,10 +555,12 @@
</svg> </svg>
</button></a </button></a
> >
<Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip>
<div class="border border-coolgray-500 h-8" /> <div class="border border-coolgray-500 h-8" />
{#if forceDelete} {#if forceDelete}
<button <button
id="forcedelete"
on:click={() => deleteApplication(application.name, true)} on:click={() => deleteApplication(application.name, true)}
type="submit" type="submit"
disabled={!$appSession.isAdmin} disabled={!$appSession.isAdmin}
@@ -555,6 +570,7 @@
> >
Force Delete Force Delete
</button> </button>
<Tooltip triggeredBy="#forcedelete">Force Delete</Tooltip>
{:else} {:else}
<button <button
id="delete" id="delete"
@@ -566,14 +582,7 @@
> >
<DeleteIcon /> <DeleteIcon />
</button> </button>
<Tooltip triggeredBy="#delete">Delete</Tooltip>
{/if} {/if}
</nav> </nav>
<slot /> <slot />
<Tooltip triggeredBy="#configurations">Configurations</Tooltip>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<Tooltip triggeredBy="#persistentstorages">Persistent Storages</Tooltip>
<Tooltip triggeredBy="#previews">Previews</Tooltip>
<Tooltip triggeredBy="#applicationlogs">Application Logs</Tooltip>
<Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip>
<Tooltip triggeredBy="#delete">Delete</Tooltip>

View File

@@ -22,7 +22,7 @@
async function loadBranches() { async function loadBranches() {
try { try {
loading.branches = true; loading.branches = true;
publicRepositoryLink = publicRepositoryLink.trim();
const protocol = publicRepositoryLink.split(':')[0]; const protocol = publicRepositoryLink.split(':')[0];
const gitUrl = publicRepositoryLink.replace('http://', '').replace('https://', ''); const gitUrl = publicRepositoryLink.replace('http://', '').replace('https://', '');

View File

@@ -0,0 +1,162 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/databases`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let databases: any = [];
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { appSession } from '$lib/store';
import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
import { errorNotification } from '$lib/common';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
const from = $page.url.searchParams.get('from');
let remoteDatabase = {
name: null,
type: null,
host: null,
port: null,
user: null,
password: null,
database: null
};
const ownDatabases = databases.filter((database: any) => {
if (database.teams[0].id === $appSession.teamId) {
return database;
}
});
const otherDatabases = databases.filter((database: any) => {
if (database.teams[0].id !== $appSession.teamId) {
return database;
}
});
async function addCoolifyDatabase(database: any) {
try {
await post(`/applications/${$page.params.id}/configuration/database`, {
databaseId: database.id,
type: database.type
});
return window.location.assign(from || `/applications/${$page.params.id}/`);
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Database</div>
</div>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !databases || ownDatabases.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>
</div>
{/if}
{#if ownDatabases.length > 0 || otherDatabases.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDatabases as database}
<button on:click={() => addCoolifyDatabase(database)} class="p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600">
<DatabaseIcons type={database.type} isAbsolute={true} />
<div class="truncate text-center text-xl font-bold">
{database.name}
</div>
{#if $appSession.teamId === '0' && otherDatabases.length > 0}
<div class="truncate text-center">{database.teams[0].name}</div>
{/if}
{#if database.destinationDocker?.name}
<div class="truncate text-center">{database.destinationDocker.name}</div>
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</div>
</button>
{/each}
</div>
{#if otherDatabases.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Databases</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDatabases as database}
<a href="/databases/{database.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600">
<DatabaseIcons type={database.type} isAbsolute={true} />
<div class="truncate text-center text-xl font-bold">
{database.name}
</div>
{#if $appSession.teamId === '0'}
<div class="truncate text-center">{database.teams[0].name}</div>
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{:else}
<div class="text-center truncate">{database.type}</div>
{/if}
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
<div class="mx-auto max-w-4xl p-6">
<div class="grid grid-flow-row gap-2 px-10">
<div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div>
<div class="mt-2 grid grid-cols-2 items-center px-4">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input name="name" id="name" required bind:value={remoteDatabase.name} />
</div>
<div class="mt-2 grid grid-cols-2 items-center px-4">
<label for="type" class="text-base font-bold text-stone-100">Type</label>
<input name="type" id="type" required bind:value={remoteDatabase.type} />
</div>
<div class="mt-2 grid grid-cols-2 items-center px-4">
<label for="host" class="text-base font-bold text-stone-100">Host</label>
<input name="host" id="host" required bind:value={remoteDatabase.host} />
</div>
<div class="mt-2 grid grid-cols-2 items-center px-4">
<label for="port" class="text-base font-bold text-stone-100">Port</label>
<input name="port" id="port" required bind:value={remoteDatabase.port} />
</div>
<div class="mt-2 grid grid-cols-2 items-center px-4">
<label for="user" class="text-base font-bold text-stone-100">User</label>
<input name="user" id="user" required bind:value={remoteDatabase.user} />
</div>
<div class="mt-2 grid grid-cols-2 items-center px-4">
<label for="password" class="text-base font-bold text-stone-100">Password</label>
<input name="password" id="password" required bind:value={remoteDatabase.password} />
</div>
<div class="mt-2 grid grid-cols-2 items-center px-4">
<label for="database" class="text-base font-bold text-stone-100">Database Name</label>
<input name="database" id="database" required bind:value={remoteDatabase.database} />
</div>
</div>
</div>
</div>

View File

@@ -31,15 +31,23 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Select from 'svelte-select'; import Select from 'svelte-select';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import cuid from 'cuid'; import cuid from 'cuid';
import { addToast, appSession, disabledButton, setLocation, status } from '$lib/store'; import {
addToast,
appSession,
checkIfDeploymentEnabledApplications,
setLocation,
status,
isDeploymentEnabled,
features
} from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation';
const { id } = $page.params; const { id } = $page.params;
@@ -64,7 +72,9 @@
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot; let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching;
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false; let isNonWWWDomainOK = false;
let isWWWDomainOK = false; let isWWWDomainOK = false;
@@ -162,6 +172,9 @@
application.settings.isBot = isBot; application.settings.isBot = isBot;
setLocation(application, settings); setLocation(application, settings);
} }
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
try { try {
await post(`/applications/${id}/settings`, { await post(`/applications/${id}/settings`, {
previews, previews,
@@ -169,6 +182,7 @@
dualCerts, dualCerts,
isBot, isBot,
autodeploy, autodeploy,
isDBBranching,
branch: application.branch, branch: application.branch,
projectId: application.projectId projectId: application.projectId
}); });
@@ -192,14 +206,20 @@
if (name === 'isBot') { if (name === 'isBot') {
isBot = !isBot; isBot = !isBot;
} }
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
return errorNotification(error); return errorNotification(error);
} finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
} }
} }
async function handleSubmit() { async function handleSubmit() {
if (loading || (!application.fqdn && !isBot)) return; if (loading) return;
loading = true; loading = true;
try { try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
console.log({debug: nonWWWDomain})
if (application.deploymentType) if (application.deploymentType)
application.deploymentType = application.deploymentType.toLowerCase(); application.deploymentType = application.deploymentType.toLowerCase();
!isBot && !isBot &&
@@ -209,16 +229,17 @@
dualCerts, dualCerts,
exposePort: application.exposePort exposePort: application.exposePort
})); }));
await post(`/applications/${id}`, { ...application }); await post(`/applications/${id}`, { ...application, baseDatabaseBranch });
setLocation(application, settings); setLocation(application, settings);
$disabledButton = false; $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
forceSave = false; forceSave = false;
addToast({ addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'
}); });
} catch (error) { } catch (error) {
console.log(error);
//@ts-ignore //@ts-ignore
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) { if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true; forceSave = true;
@@ -509,6 +530,46 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if $features.beta}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="isDBBranching"
isCenter={false}
bind:setting={isDBBranching}
on:click={() => changeSettings('isDBBranching')}
title="Enable DB Branching"
description="Enable DB Branching"
/>
</div>
{#if isDBBranching}
<button
on:click|stopPropagation|preventDefault={() =>
goto(`/applications/${id}/configuration/database`)}
class="btn btn-sm">Configure Connected Database</button
>
{#if application.connectedDatabase}
<div class="grid grid-cols-2 items-center">
<label for="baseImage" class="text-base font-bold text-stone-100"
>Base Database
<Explainer
explanation={'The name of the database that will be used as base when branching.'}
/></label
>
<input
name="baseDatabaseBranch"
required
id="baseDatabaseBranch"
bind:value={baseDatabaseBranch}
/>
</div>
<div class="text-center bg-green-600 rounded">
Connected to {application.connectedDatabase.databaseId}
</div>
{/if}
{/if}
{/if}
{/if}
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">{$t('application.application')}</div> <div class="title">{$t('application.application')}</div>
@@ -538,7 +599,7 @@
/> />
</div> </div>
{#if !isBot} {#if !isBot}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center pb-8">
<label for="fqdn" class="text-base font-bold text-stone-100" <label for="fqdn" class="text-base font-bold text-stone-100"
>{$t('application.url_fqdn')} >{$t('application.url_fqdn')}
<Explainer <Explainer
@@ -551,7 +612,6 @@
disabled={isDisabled} disabled={isDisabled}
name="fqdn" name="fqdn"
id="fqdn" id="fqdn"
required
bind:value={application.fqdn} bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io" placeholder="eg: https://coollabs.io"
@@ -658,7 +718,7 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center pb-8">
<label for="exposePort" class="text-base font-bold text-stone-100" <label for="exposePort" class="text-base font-bold text-stone-100"
>Exposed Port <Explainer >Exposed Port <Explainer
explanation={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'} explanation={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
@@ -700,7 +760,7 @@
placeholder="{$t('forms.default')}: yarn build" placeholder="{$t('forms.default')}: yarn build"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center pb-8">
<label for="startCommand" class="text-base font-bold text-stone-100" <label for="startCommand" class="text-base font-bold text-stone-100"
>{$t('application.start_command')}</label >{$t('application.start_command')}</label
> >

View File

@@ -67,7 +67,6 @@
} }
}, 1000); }, 1000);
} catch (error) { } catch (error) {
console.log(error);
return errorNotification(error); return errorNotification(error);
} }
} }
@@ -80,7 +79,6 @@
applicationId: id applicationId: id
}); });
} catch (error) { } catch (error) {
console.log(error);
return errorNotification(error); return errorNotification(error);
} }
} }

View File

@@ -39,7 +39,6 @@
logs = data.logs; logs = data.logs;
} }
} catch (error) { } catch (error) {
console.log(error);
return errorNotification(error); return errorNotification(error);
} finally { } finally {
logsLoading = false; logsLoading = false;

View File

@@ -87,12 +87,15 @@
const sure = confirm($t('database.confirm_stop', { name: database.name })); const sure = confirm($t('database.confirm_stop', { name: database.name }));
if (sure) { if (sure) {
$status.database.initialLoading = true; $status.database.initialLoading = true;
$status.database.loading = true;
try { try {
await post(`/databases/${database.id}/stop`, {}); await post(`/databases/${database.id}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$status.database.initialLoading = false; $status.database.initialLoading = false;
$status.database.loading = false;
await getStatus();
} }
} }
} }
@@ -175,7 +178,7 @@
{/if} {/if}
{#if $status.database.initialLoading} {#if $status.database.initialLoading}
<button <button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out" class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -45,7 +45,6 @@
logs = data.logs; logs = data.logs;
} }
} catch (error) { } catch (error) {
console.log(error);
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loadingLogs = false; loadingLogs = false;

View File

@@ -38,8 +38,8 @@
} }
} }
onMount(async () => { onMount(async () => {
loading.proxy = true;
if (destination.remoteEngine && destination.remoteVerified) { if (destination.remoteEngine && destination.remoteVerified) {
loading.proxy = true;
const { isRunning } = await get(`/destinations/${id}/status`); const { isRunning } = await get(`/destinations/${id}/status`);
if (isRunning === false && destination.isCoolifyProxyUsed === true) { if (isRunning === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
@@ -69,6 +69,7 @@
loading.proxy = false; loading.proxy = false;
}); });
async function changeProxySetting() { async function changeProxySetting() {
if (!destination.remoteVerified) return
loading.proxy = true; loading.proxy = true;
if (!cannotDisable) { if (!cannotDisable) {
const isProxyActivated = destination.isCoolifyProxyUsed; const isProxyActivated = destination.isCoolifyProxyUsed;
@@ -262,7 +263,7 @@
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
id="changeProxySetting" id="changeProxySetting"
disabled={cannotDisable} disabled={cannotDisable || !destination.remoteVerified}
loading={loading.proxy} loading={loading.proxy}
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}

View File

@@ -40,7 +40,6 @@
} }
}; };
} catch (error) { } catch (error) {
console.log(error);
return handlerNotFoundLoad(error, url); return handlerNotFoundLoad(error, url);
} }
}; };
@@ -58,11 +57,11 @@
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params;
const isDestinationDeletable = const isDestinationDeletable =
destination?.application.length === 0 && (destination?.application.length === 0 &&
destination?.database.length === 0 && destination?.database.length === 0 &&
destination?.service.length === 0; destination?.service.length === 0) ||
true;
async function deleteDestination(destination: any) { async function deleteDestination(destination: any) {
if (!isDestinationDeletable) return; if (!isDestinationDeletable) return;
@@ -88,7 +87,7 @@
} }
</script> </script>
{#if id !== 'new'} {#if $page.params.id !== 'new'}
<nav class="nav-side"> <nav class="nav-side">
<button <button
id="delete" id="delete"

View File

@@ -49,61 +49,83 @@
<title>{$t('login.login')}</title> <title>{$t('login.login')}</title>
</svelt:head> </svelt:head>
<div class="flex h-screen flex-col items-center justify-center"> <div class="flex lg:flex-row flex-col h-screen">
<div class="flex justify-center px-4"> <div class="bg-neutral-focus h-screen lg:flex hidden flex-col justify-end p-20 flex-1">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2"> <h1 class="title lg:text-6xl mb-5 border-gradient">Coolify</h1>
{#if $appSession.whiteLabeledDetails.icon} <h3 class="title">Made self-hosting simple.</h3>
<img
class="w-32 mx-auto pb-8"
src={$appSession.whiteLabeledDetails.icon}
alt="Icon for white labeled version of Coolify"
/>
{:else}
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4 mb-8">Coolify</div>
{/if}
<input
type="email"
name="email"
placeholder={$t('forms.email')}
autocomplete="off"
required
bind:this={emailEl}
bind:value={email}
class="w-56 md:w-96"
/>
<input
type="password"
name="password"
placeholder={$t('forms.password')}
bind:value={password}
required
class="w-56 md:w-96"
/>
<div class="flex space-x-2 h-8 items-center justify-center pt-8">
<button
type="submit"
disabled={loading}
class="btn btn-sm"
class:loading
class:bg-coollabs={!loading}
>{loading ? $t('login.authenticating') : $t('login.login')}</button
>
<button on:click|preventDefault={gotoRegister} class="btn btn-sm"
>{$t('register.register')}</button
>
</div>
</form>
</div> </div>
{#if browser && window.location.host === 'demo.coolify.io'} <div class="flex flex-1 flex-col lg:max-w-2xl">
<div class="pt-5 font-bold"> <div class="flex flex-row p-8 items-center space-x-3">
Registration is <span class="text-pink-500">open</span>, just fill in an email (does not need {#if $appSession.whiteLabeledDetails.icon}
to be live email address for the demo instance) and a password. <div class="avatar" style="width: 40px; height: 40px">
<img
src={$appSession.whiteLabeledDetails.icon}
alt="Icon for white labeled version of Coolify"
/>
</div>
{:else}
<div>
<div class="avatar" style="width: 40px; height: 40px">
<img src="favicon.png" alt="Coolify icon" />
</div>
</div>
<div class="prose">
<h4>Coolify</h4>
</div>
{/if}
</div> </div>
<div class="pt-5 font-bold"> <div
All users gets an <span class="text-pink-500">own namespace</span>, so you won't be able to class="w-full md:px-20 lg:px-10 xl:px-20 p-6 flex flex-col h-full justify-center items-center"
access other users data. >
<div class="mb-5 w-full prose prose-neutral">
<h1 class="m-0 white">Welcome back</h1>
<h5>Please login to continue.</h5>
</div>
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-3 w-full">
<input
type="email"
name="email"
placeholder={$t('forms.email')}
autocomplete="off"
required
bind:this={emailEl}
bind:value={email}
class="w-full"
/>
<input
type="password"
name="password"
placeholder={$t('forms.password')}
bind:value={password}
required
class="w-full"
/>
<div class="flex space-y-3 flex-col pt-3">
<button
type="submit"
disabled={loading}
class="btn"
class:loading
class:bg-coollabs={!loading}
>{loading ? $t('login.authenticating') : $t('login.login')}</button
>
<button on:click|preventDefault={gotoRegister} class="btn btn-ghost"
>{$t('register.register')}</button
>
</div>
</form>
{#if browser && window.location.host === 'demo.coolify.io'}
<div class="pt-5 font-bold">
Registration is <span class="text-pink-500">open</span>, just fill in an email (does not
need to be live email address for the demo instance) and a password.
</div>
<div class="pt-5 font-bold">
All users gets an <span class="text-pink-500">own namespace</span>, so you won't be able
to access other users data.
</div>
{/if}
</div> </div>
{/if} </div>
</div> </div>

View File

@@ -61,38 +61,58 @@
} }
</script> </script>
<div class="icons fixed top-0 left-0 m-3 cursor-pointer" on:click={() => goto('/')}> <div class="flex lg:flex-row flex-col h-screen">
<svg <div class="bg-neutral-focus h-screen lg:flex hidden flex-col justify-end p-20 flex-1">
xmlns="http://www.w3.org/2000/svg" <h1 class="title lg:text-6xl mb-5 border-gradient">Coolify</h1>
class="h-6 w-6" <h3 class="title">Made self-hosting simple.</h3>
viewBox="0 0 24 24" </div>
stroke-width="1.5" <div class="flex flex-1 flex-col lg:max-w-2xl">
stroke="currentColor" <div class="flex flex-row p-8 items-center space-x-3 justify-between">
fill="none" <div class="icons cursor-pointer" on:click={() => goto('/')}>
stroke-linecap="round" <svg
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"
> class="h-6 w-6"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> viewBox="0 0 24 24"
<line x1="5" y1="12" x2="19" y2="12" /> stroke-width="1.5"
<line x1="5" y1="12" x2="11" y2="18" /> stroke="currentColor"
<line x1="5" y1="12" x2="11" y2="6" /> fill="none"
</svg> stroke-linecap="round"
</div> stroke-linejoin="round"
<div class="flex h-screen flex-col items-center justify-center"> >
{#if $appSession.userId} <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<div class="flex justify-center px-4 text-xl font-bold">{$t('login.already_logged_in')}</div> <line x1="5" y1="12" x2="19" y2="12" />
{:else} <line x1="5" y1="12" x2="11" y2="18" />
<div class="flex justify-center px-4"> <line x1="5" y1="12" x2="11" y2="6" />
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2"> </svg>
</div>
<div class="flex flex-row items-center space-x-3">
{#if $appSession.whiteLabeledDetails.icon} {#if $appSession.whiteLabeledDetails.icon}
<img <div class="avatar" style="width: 40px; height: 40px">
class="w-32 mx-auto pb-8" <img
src={$appSession.whiteLabeledDetails.icon} src={$appSession.whiteLabeledDetails.icon}
alt="Icon for white labeled version of Coolify" alt="Icon for white labeled version of Coolify"
/> />
</div>
{:else} {:else}
<div class="text-6xl font-bold border-gradient w-48 mx-auto border-b-4 mb-8">Coolify</div> <div>
<div class="avatar" style="width: 40px; height: 40px">
<img src="favicon.png" alt="Coolify icon" />
</div>
</div>
<div class="prose">
<h4>Coolify</h4>
</div>
{/if} {/if}
</div>
</div>
<div
class="w-full md:px-20 lg:px-10 xl:px-20 p-6 flex flex-col h-full justify-center items-center"
>
<div class="mb-5 w-full prose prose-neutral">
<h1 class="m-0 white">Get started</h1>
<h5>Enter the required fields to complete the registration.</h5>
</div>
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-3 w-full">
<input <input
type="email" type="email"
name="email" name="email"
@@ -101,7 +121,7 @@
required required
bind:this={emailEl} bind:this={emailEl}
bind:value={email} bind:value={email}
class="w-56 md:w-96" class="w-full"
/> />
<input <input
type="password" type="password"
@@ -110,7 +130,7 @@
bind:this={passwordEl} bind:this={passwordEl}
bind:value={password} bind:value={password}
required required
class="w-56 md:w-96" class="w-full"
/> />
<input <input
type="password" type="password"
@@ -118,13 +138,13 @@
placeholder={$t('forms.password_again')} placeholder={$t('forms.password_again')}
bind:value={passwordCheck} bind:value={passwordCheck}
required required
class="w-56 md:w-96" class="w-full"
/> />
<div class="flex space-x-2 h-8 items-center justify-center pt-8"> <div class="flex space-y-3 flex-col pt-3">
<button <button
type="submit" type="submit"
class="btn btn-sm" class="btn"
disabled={loading} disabled={loading}
class:bg-transparent={loading} class:bg-transparent={loading}
class:bg-coollabs={!loading} class:bg-coollabs={!loading}
@@ -132,11 +152,11 @@
> >
</div> </div>
</form> </form>
{#if userCount === 0}
<div class="pt-5">
{$t('register.first_user')}
</div>
{/if}
</div> </div>
{#if userCount === 0} </div>
<div class="pt-5">
{$t('register.first_user')}
</div>
{/if}
{/if}
</div> </div>

View File

@@ -12,7 +12,14 @@
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { errorNotification, getDomain } from '$lib/common'; import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store'; import {
appSession,
status,
setLocation,
addToast,
checkIfDeploymentEnabledServices,
isDeploymentEnabled
} from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
@@ -78,8 +85,8 @@
}); });
await post(`/services/${id}`, { ...service }); await post(`/services/${id}`, { ...service });
setLocation(service); setLocation(service);
$disabledButton = false;
forceSave = false; forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
return addToast({ return addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'

View File

@@ -12,7 +12,7 @@
export let readOnly: any; export let readOnly: any;
export let settings: any; export let settings: any;
const { id } = $page.params; const { id } = $page.params;
const { ipv4, ipv6 } = settings;
let ftpUrl = generateUrl(service.wordpress.ftpPublicPort); let ftpUrl = generateUrl(service.wordpress.ftpPublicPort);
let ftpUser = service.wordpress.ftpUser; let ftpUser = service.wordpress.ftpUser;
let ftpPassword = service.wordpress.ftpPassword; let ftpPassword = service.wordpress.ftpPassword;
@@ -22,7 +22,7 @@
function generateUrl(publicPort: any) { function generateUrl(publicPort: any) {
return browser return browser
? `sftp://${ ? `sftp://${
settings?.fqdn ? getDomain(settings.fqdn) : window.location.hostname settings?.fqdn ? getDomain(settings.fqdn) : ipv4 || ipv6
}:${publicPort}` }:${publicPort}`
: 'Loading...'; : 'Loading...';
} }

View File

@@ -47,7 +47,6 @@
} }
}; };
} catch (error) { } catch (error) {
console.log(error);
return handlerNotFoundLoad(error, url); return handlerNotFoundLoad(error, url);
} }
}; };
@@ -60,19 +59,21 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, disabledButton, status, location, setLocation } from '$lib/store'; import {
appSession,
isDeploymentEnabled,
status,
location,
setLocation,
checkIfDeploymentEnabledServices
} from '$lib/store';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
export let service: any; export let service: any;
$disabledButton = $isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
!$appSession.isAdmin ||
!service.fqdn ||
!service.destinationDocker ||
!service.version ||
!service.type;
let statusInterval: any; let statusInterval: any;
@@ -96,12 +97,15 @@
const sure = confirm($t('database.confirm_stop', { name: service.name })); const sure = confirm($t('database.confirm_stop', { name: service.name }));
if (sure) { if (sure) {
$status.service.initialLoading = true; $status.service.initialLoading = true;
$status.service.loading = true;
try { try {
await post(`/services/${service.id}/${service.type}/stop`, {}); await post(`/services/${service.id}/${service.type}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$status.service.initialLoading = false; $status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
} }
} }
} }
@@ -133,6 +137,7 @@
$status.service.isExited = false; $status.service.isExited = false;
$status.service.loading = false; $status.service.loading = false;
$location = null; $location = null;
$isDeploymentEnabled = false;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
onMount(async () => { onMount(async () => {
@@ -151,141 +156,142 @@
</script> </script>
<nav class="nav-side"> <nav class="nav-side">
{#if service.type && service.destinationDockerId && service.version && service.fqdn} {#if $location}
{#if $location} <a
<a id="open"
id="open" href={$location}
href={$location} target="_blank"
target="_blank" class="icons flex items-center bg-transparent text-sm"
class="icons flex items-center bg-transparent text-sm" ><svg
><svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
class="h-6 w-6" viewBox="0 0 24 24"
viewBox="0 0 24 24" stroke-width="1.5"
stroke-width="1.5" stroke="currentColor"
stroke="currentColor" fill="none"
fill="none" stroke-linecap="round"
stroke-linecap="round" stroke-linejoin="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
> >
<Tooltip triggeredBy="#open">Open</Tooltip> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<div class="border border-stone-700 h-8" /> <path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
{/if} <line x1="10" y1="14" x2="20" y2="4" />
{#if $status.service.isExited} <polyline points="15 4 20 4 20 9" />
<a </svg></a
id="error" >
href={!$disabledButton ? `/services/${id}/logs` : null} <Tooltip triggeredBy="#open">Open</Tooltip>
class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-error"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
<Tooltip triggeredBy="#error">Service exited with an error!</Tooltip>
{/if}
{#if $status.service.initialLoading}
<button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.service.isRunning}
<button
id="stop"
on:click={stopService}
type="submit"
disabled={$disabledButton}
class="icons bg-transparent text-sm flex items-center space-x-2 text-red-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
{:else}
<button
id="start"
on:click={startService}
type="submit"
disabled={$disabledButton}
class="icons bg-transparent text-sm flex items-center space-x-2 text-green-500"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
<Tooltip triggeredBy="#start">Start</Tooltip>
{/if}
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
{/if} {/if}
{#if $status.service.isExited}
<a
id="error"
href={$isDeploymentEnabled ? `/services/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-error"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
<Tooltip triggeredBy="#error">Service exited with an error!</Tooltip>
{/if}
{#if $status.service.initialLoading}
<button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.service.isRunning}
<button
id="stop"
on:click={stopService}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-red-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
{:else}
<button
id="start"
on:click={startService}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-green-500"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
</button>
<Tooltip triggeredBy="#start">Start</Tooltip>
{/if}
<div class="border border-stone-700 h-8" />
{#if service.type && service.destinationDockerId && service.version} {#if service.type && service.destinationDockerId && service.version}
<a <a
id="configuration"
href="/services/{id}" href="/services/{id}"
sveltekit:prefetch sveltekit:prefetch
class="hover:text-yellow-500 rounded" class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/services/${id}`} class:text-yellow-500={$page.url.pathname === `/services/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}`} class:bg-coolgray-500={$page.url.pathname === `/services/${id}`}
> >
<button class="icons bg-transparent text-sm disabled:text-red-500"> <button
id="configuration"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@@ -311,14 +317,17 @@
> >
<Tooltip triggeredBy="#configuration">Configuration</Tooltip> <Tooltip triggeredBy="#configuration">Configuration</Tooltip>
<a <a
id="secrets"
href="/services/{id}/secrets" href="/services/{id}/secrets"
sveltekit:prefetch sveltekit:prefetch
class="hover:text-pink-500 rounded" class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/secrets`} class:text-pink-500={$page.url.pathname === `/services/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/secrets`} class:bg-coolgray-500={$page.url.pathname === `/services/${id}/secrets`}
> >
<button class="icons bg-transparent text-sm disabled:text-red-500"> <button
id="secrets"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm "
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@@ -340,14 +349,17 @@
> >
<Tooltip triggeredBy="#secrets">Secrets</Tooltip> <Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<a <a
id="persistentstorage"
href="/services/{id}/storages" href="/services/{id}/storages"
sveltekit:prefetch sveltekit:prefetch
class="hover:text-pink-500 rounded" class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/storages`} class:text-pink-500={$page.url.pathname === `/services/${id}/storages`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/storages`} class:bg-coolgray-500={$page.url.pathname === `/services/${id}/storages`}
> >
<button class="icons bg-transparent text-sm disabled:text-red-500"> <button
id="persistentstorage"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@@ -368,14 +380,13 @@
<Tooltip triggeredBy="#persistentstorage">Persistent Storage</Tooltip> <Tooltip triggeredBy="#persistentstorage">Persistent Storage</Tooltip>
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
<a <a
id="logs" href={$isDeploymentEnabled && $status.service.isRunning ? `/services/${id}/logs` : null}
href={!$disabledButton && $status.service.isRunning ? `/services/${id}/logs` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-pink-500 rounded" class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`} class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`} class:bg-coolgray-500={$page.url.pathname === `/services/${id}/logs`}
> >
<button disabled={!$status.service.isRunning} class="icons bg-transparent text-sm"> <button id="logs" disabled={!$status.service.isRunning} class="icons bg-transparent text-sm">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"

View File

@@ -41,7 +41,6 @@
logs = data.logs; logs = data.logs;
} }
} catch (error) { } catch (error) {
console.log(error);
return errorNotification(error); return errorNotification(error);
} finally { } finally {
logsLoading = false; logsLoading = false;

View File

@@ -23,10 +23,11 @@
import { browser } from '$app/env'; import { browser } from '$app/env';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { addToast, appSession, features } from '$lib/store'; import { addToast, appSession, features } from '$lib/store';
import { errorNotification, getDomain } from '$lib/common'; import { asyncSleep, errorNotification, getDomain } from '$lib/common';
import Menu from './_Menu.svelte'; import Menu from './_Menu.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
let isAPIDebuggingEnabled = settings.isAPIDebuggingEnabled;
let isRegistrationEnabled = settings.isRegistrationEnabled; let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts; let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled; let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
@@ -44,7 +45,8 @@
let loading = { let loading = {
save: false, save: false,
remove: false, remove: false,
proxyMigration: false proxyMigration: false,
restart: false
}; };
async function removeFqdn() { async function removeFqdn() {
@@ -75,8 +77,11 @@
if (name === 'isDNSCheckEnabled') { if (name === 'isDNSCheckEnabled') {
isDNSCheckEnabled = !isDNSCheckEnabled; isDNSCheckEnabled = !isDNSCheckEnabled;
} }
if (name === 'isAPIDebuggingEnabled') {
isAPIDebuggingEnabled = !isAPIDebuggingEnabled;
}
await post(`/settings`, { await post(`/settings`, {
isAPIDebuggingEnabled,
isRegistrationEnabled, isRegistrationEnabled,
dualCerts, dualCerts,
isAutoUpdateEnabled, isAutoUpdateEnabled,
@@ -129,7 +134,6 @@
} }
} }
} }
console.log(error);
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading.save = false; loading.save = false;
@@ -153,6 +157,41 @@
function resetView() { function resetView() {
forceSave = false; forceSave = false;
} }
async function restartCoolify() {
const sure = confirm(
'Are you sure you would like to restart Coolify? Currently running deployments will be stopped and restarted.'
);
if (sure) {
loading.restart = true;
try {
await post(`/internal/restart`, {});
await asyncSleep(10000);
let reachable = false;
let tries = 0;
do {
await asyncSleep(4000);
try {
await get(`/undead`);
reachable = true;
} catch (error) {
reachable = false;
}
if (reachable) break;
tries++;
} while (!reachable || tries < 120);
addToast({
message: 'New version reachable. Reloading...',
type: 'success'
});
await asyncSleep(3000);
return window.location.reload();
} catch (error) {
return errorNotification(error);
} finally {
loading.restart = false;
}
}
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@@ -183,11 +222,14 @@
on:click|preventDefault={removeFqdn} on:click|preventDefault={removeFqdn}
disabled={loading.remove} disabled={loading.remove}
class="btn btn-sm" class="btn btn-sm"
class:bg-red-600={!loading.remove}
class:hover:bg-red-500={!loading.remove}
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button >{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
> >
{/if} {/if}
<button
on:click={restartCoolify}
class:loading={loading.restart}
class="btn btn-sm bg-red-600 hover:bg-red-500">Restart Coolify</button
>
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> --> <!-- <Language /> -->
@@ -311,17 +353,24 @@
on:click={() => changeSettings('isRegistrationEnabled')} on:click={() => changeSettings('isRegistrationEnabled')}
/> />
</div> </div>
{#if browser && $features.beta} <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-2 items-center"> <Setting
<Setting id="isAPIDebuggingEnabled"
id="isAutoUpdateEnabled" bind:setting={isAPIDebuggingEnabled}
bind:setting={isAutoUpdateEnabled} title="API Debugging"
title={$t('setting.auto_update_enabled')} description="Enable API debugging. This will log all API requests and responses.<br><br>You need to restart the Coolify for this to take effect."
description={$t('setting.auto_update_enabled_explainer')} on:click={() => changeSettings('isAPIDebuggingEnabled')}
on:click={() => changeSettings('isAutoUpdateEnabled')} />
/> </div>
</div> <div class="grid grid-cols-2 items-center">
{/if} <Setting
id="isAutoUpdateEnabled"
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -46,8 +46,8 @@
customPort: source.customPort customPort: source.customPort
}); });
const { organization, htmlUrl } = source; const { organization, htmlUrl } = source;
const { fqdn } = settings; const { fqdn, ipv4, ipv6 } = settings;
const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${window.location.host}` || ''; const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${ipv4 || ipv6}` || '';
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
let url = 'settings/apps/new'; let url = 'settings/apps/new';

View File

@@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.9.0-rc.1", "version": "3.9.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {

903
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff