Compare commits

..

133 Commits

Author SHA1 Message Date
Andras Bacsai
0a68a48fc5 Merge pull request #611 from coollabsio/next
v3.10.1
2022-09-10 10:47:47 +02:00
Andras Bacsai
d3af6792d0 remove debug 2022-09-10 08:46:31 +00:00
Andras Bacsai
44dc3b743e add debug 2022-09-10 08:35:31 +00:00
Andras Bacsai
b469d2832d fix: delete resource use window location 2022-09-10 07:58:56 +00:00
Andras Bacsai
d844026c29 dev: arm should be on next all the time 2022-09-10 07:41:36 +00:00
Andras Bacsai
21b4990652 ui: fix follow button 2022-09-10 07:29:51 +00:00
Andras Bacsai
39e24bdc97 remove console logs 2022-09-10 07:17:20 +00:00
Andras Bacsai
bc66b98176 fix: build secrets for apps 2022-09-10 07:14:30 +00:00
Andras Bacsai
d6d3fb46cc fix: volumes for services 2022-09-10 07:14:17 +00:00
Andras Bacsai
88ed1446f4 fix: secrets for PR 2022-09-09 15:46:47 +02:00
Andras Bacsai
c69312f128 fix: remove unnecessary gitlab group name 2022-09-09 15:19:14 +02:00
Andras Bacsai
c5bcff0e10 fix: show restarting application & logs 2022-09-09 15:14:58 +02:00
Andras Bacsai
871d1e2440 ui: fix button 2022-09-09 15:14:19 +02:00
Andras Bacsai
1619afb938 chore: version++ 2022-09-09 14:49:13 +02:00
Andras Bacsai
25528913f1 fix: show restarting apps 2022-09-09 14:48:58 +02:00
Andras Bacsai
ef91441c76 Merge pull request #603 from coollabsio/next
v3.10.0
2022-09-08 15:41:46 +02:00
Andras Bacsai
aa6c56b63d do not show servers to non admins 2022-09-08 15:23:01 +02:00
Andras Bacsai
18e899d15e disable remote servers for now 2022-09-08 15:00:54 +02:00
Andras Bacsai
63fa8924ae fix: autoupdater 2022-09-08 14:46:08 +02:00
Andras Bacsai
0e13e3bd81 feat: new servers view 2022-09-08 14:42:04 +02:00
Andras Bacsai
372c0ed457 fix: glitchtip env to pyhton boolean 2022-09-08 12:04:08 +02:00
Andras Bacsai
071077200b ui: fix tooltip 2022-09-08 11:57:15 +02:00
Andras Bacsai
65579a2861 Merge branch 'next' of github.com:coollabsio/coolify into next 2022-09-08 11:47:09 +02:00
Andras Bacsai
bb7603ae2a Merge pull request #601 from c0ldfront/fixes
fix typo located in postCreateCommand in devcontainers.json
2022-09-08 11:45:54 +02:00
Andras Bacsai
cce67d274e fix: ui variables 2022-09-08 11:44:23 +02:00
Andras Bacsai
794329dcad fix: port checkers 2022-09-08 11:41:38 +02:00
Andras Bacsai
e36fda3ff1 fix: ispublic status on databases 2022-09-08 10:54:24 +02:00
Andras Bacsai
3832d33259 fix: save search input 2022-09-08 10:45:12 +02:00
Andras Bacsai
7350524456 ui: dashboard updates 2022-09-08 10:30:13 +02:00
Andras Bacsai
a1a973a873 fix: change to execa from utils 2022-09-08 09:32:50 +02:00
David Mydlarz
f2a915700c fix type located in postCreateCommand in devcontainers.json 2022-09-08 13:33:18 +09:00
Andras Bacsai
e184f99617 Merge branch 'main' into next 2022-09-07 20:32:27 +00:00
Andras Bacsai
ab07adb14f fix: Settings for service 2022-09-07 20:32:15 +00:00
Andras Bacsai
6535c68276 Merge branch 'main' into next 2022-09-07 20:15:26 +00:00
Andras Bacsai
dde2772e52 fix: dnsServer formatting 2022-09-07 20:14:49 +00:00
Andras Bacsai
ac72c19d22 Merge branch 'main' into next 2022-09-07 15:40:37 +00:00
Andras Bacsai
67fc2fd3c0 fix: pr previews 2022-09-07 15:29:56 +00:00
Andras Bacsai
4acc59204c ui: dashboard updates and a lot more 2022-09-07 15:59:37 +02:00
Andras Bacsai
07cadb59e0 fix: edgedb 2022-09-07 11:40:58 +02:00
Andras Bacsai
6fa4741c81 feat: database secrets 2022-09-07 11:40:52 +02:00
Andras Bacsai
f4bac2382c fix: edgedb stuff 2022-09-07 11:02:13 +02:00
Andras Bacsai
67b72220c0 feat: add traefik acme json to coolify container 2022-09-07 11:01:04 +02:00
Andras Bacsai
feeb14ea47 fix: edgedb ui 2022-09-07 10:49:49 +02:00
Andras Bacsai
bdfb6f5f46 chore: version++ 2022-09-07 10:23:08 +02:00
Andras Bacsai
53491e9eaa Merge pull request #522 from ikezedev/edge-db
[New Database]: EdgeDB
2022-09-07 10:21:47 +02:00
Andras Bacsai
f720de65fa Update _DatabaseLinks.svelte 2022-09-07 10:21:12 +02:00
Andras Bacsai
3d70162a8d Merge branch 'next' into edge-db 2022-09-07 10:20:40 +02:00
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
Andras Bacsai
47c3af6a0e oops 2022-09-02 19:03:04 +00:00
Andras Bacsai
e5527a5aa5 temp 2022-09-02 18:46:24 +00:00
Andras Bacsai
9bb0dcd73f Testing different Dockerfile 2022-09-02 18:44:29 +00:00
Ikechukwu Eze
7ed1ced521 insecure mode 2022-08-04 13:11:13 +02:00
Ikechukwu Eze
801b9c1483 Merge branch 'next' into edge-db 2022-07-31 19:54:30 +02:00
Ikechukwu Eze
3990bebca3 add to supported databases 2022-07-31 19:50:25 +02:00
Ikechukwu Eze
8a2de1001f first steps with ui 2022-07-31 19:50:03 +02:00
108 changed files with 5171 additions and 2612 deletions

View File

@@ -20,12 +20,13 @@
"svelte.svelte-vscode",
"ardenivanov.svelte-intellisense",
"Prisma.prisma",
"bradlc.vscode-tailwindcss"
"bradlc.vscode-tailwindcss",
"waderyan.gitblame"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000, 3001],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp apps/api/.env.example pps/api/.env && pnpm install && pnpm db:push && pnpm db:seed",
"postCreateCommand": "cp apps/api/.env.example apps/api/.env && pnpm install && pnpm db:push && pnpm db:seed",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"features": {

View File

@@ -5,11 +5,11 @@ on:
types: [released]
jobs:
making-something-cool:
runs-on: ubuntu-latest
arm64-build:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
@@ -26,11 +26,59 @@ jobs:
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/arm64
push: true
tags: coollabsio/coolify:latest,coollabsio/coolify:${{steps.package-version.outputs.current-version}}
cache-from: type=registry,ref=coollabsio/coolify:buildcache
cache-to: type=registry,ref=coollabsio/coolify:buildcache,mode=max
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
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
if: always()
with:

View File

@@ -1,17 +1,16 @@
name: release-candidate
on:
release:
types: [prereleased]
jobs:
making-something-cool:
runs-on: ubuntu-latest
arm64-making-something-cool:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'next'
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
@@ -28,12 +27,64 @@ jobs:
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/arm64
push: true
tags: coollabsio/coolify:${{github.event.release.name}}
cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc
cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc,mode=max
tags: coollabsio/coolify:${{github.event.release.name}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc-arm64,mode=max
- uses: sarisia/actions-status-discord@v1
if: always()
with:
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
jobs:
staging-release:
runs-on: ubuntu-latest
arm64-making-something-cool:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
@@ -20,15 +22,65 @@ jobs:
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@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:
context: .
platforms: linux/amd64
push: true
tags: coollabsio/coolify:next
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next,mode=max
tags: coollabsio/coolify:next-amd64,coollabsio/coolify:next-test
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
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
if: always()
with:

108
CONTRIBUTION_NEW.md Normal file
View File

@@ -0,0 +1,108 @@
---
head:
- - meta
- name: description
content: Coolify - Databases
- - meta
- name: keywords
content: databases coollabs coolify
- - meta
- name: twitter:card
content: summary_large_image
- - meta
- name: twitter:site
content: '@andrasbacsai'
- - meta
- name: twitter:title
content: Coolify
- - meta
- name: twitter:description
content: An open-source & self-hostable Heroku / Netlify alternative.
- - meta
- name: twitter:image
content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png
- - meta
- property: og:type
content: website
- - meta
- property: og:url
content: https://coolify.io
- - meta
- property: og:title
content: Coolify
- - meta
- property: og:description
content: An open-source & self-hostable Heroku / Netlify alternative.
- - meta
- property: og:site_name
content: Coolify
- - meta
- property: og:image
content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png
---
# Contribution
First, thanks for considering to contribute to my project. It really means a lot! :)
You can ask for guidance anytime on our Discord server in the #contribution channel.
## Setup your development environment
### Github codespaces
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
### Gitpod
If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
### Local Machine
> At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces.
- Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
- You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
- You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
- You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.
Optional:
- To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally.
### Inside a Docker container
`WIP`
## Setup Coolify
- Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool.
- `pnpm install` to install dependencies.
- `pnpm db:push` to o create a local SQlite database.
This will apply all migrations at `db/dev.db`.
- `pnpm db:seed` seed the database.
- `pnpm dev` start coding.
## Technical skills required
- **Languages**: Node.js / Javascript / Typescript
- **Framework JS/TS**: [SvelteKit](https://kit.svelte.dev/) & [Fastify](https://www.fastify.io/)
- **Database ORM**: [Prisma.io](https://www.prisma.io/)
- **Docker Engine API**
## Add a new service
### Which service is eligable to add to Coolify?
The following statements needs to be true:
- Self-hostable
- Open-source
- Maintained (I do not want to add software full of bugs)
### Create Prisma / Database schema for the new service.
All data that needs to be persist for a service should be saved to the database in `cleartext` or `encrypted`.
very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it.
Update Prisma schema in [src/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma).
- Add new model with the new service name.
- Make a relationship with `Service` model.
- In the `Service` model, the name of the new field should be with low-capital.
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.

View File

@@ -1,30 +1,26 @@
FROM node:18-alpine3.16 as build
ARG PNPM_VERSION=7.11.0
ARG NPM_VERSION=8.19.1
FROM node:18-slim as build
WORKDIR /app
RUN apk add --no-cache curl
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node
RUN apt update && apt -y install curl
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
COPY . .
RUN pnpm install
RUN pnpm build
# Production build
FROM node:18-alpine3.16
FROM node:18-slim
WORKDIR /app
ENV NODE_ENV production
ARG TARGETPLATFORM
# ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \
# PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \
# PRISMA_INTROSPECTION_ENGINE_BINARY=/app/prisma-engines/introspection-engine \
# PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \
# PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
# PRISMA_CLIENT_ENGINE_TYPE=binary
# COPY --from=coollabsio/prisma-engine:3.15 /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl psmisc
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
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/
# https://download.docker.com/linux/static/stable/

View File

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

@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "DatabaseSecret" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"databaseId" TEXT NOT NULL,
CONSTRAINT "DatabaseSecret_databaseId_fkey" FOREIGN KEY ("databaseId") REFERENCES "Database" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "DatabaseSecret_name_databaseId_key" ON "DatabaseSecret"("name", "databaseId");

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import env from '@fastify/env';
import cookie from '@fastify/cookie';
import path, { join } from 'path';
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 { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful'
@@ -26,119 +26,143 @@ declare module 'fastify' {
const port = isDev ? 3001 : 3000;
const host = '0.0.0.0';
const fastify = Fastify({
logger: 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
prisma.setting.findFirst().then(async (settings) => {
const fastify = Fastify({
logger: settings?.isAPIDebuggingEnabled || false,
trustProxy: 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');
});
}
fastify.register(autoLoad, {
dir: join(__dirname, 'plugins')
});
fastify.register(autoLoad, {
dir: join(__dirname, 'routes')
});
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'
},
fastify.register(cookie)
fastify.register(cors);
fastify.listen({ port, host }, async (err: any, address: any) => {
if (err) {
console.error(err);
process.exit(1);
}
};
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) {
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}`);
await initServer();
fastify.register(autoLoad, {
dir: join(__dirname, 'plugins')
});
fastify.register(autoLoad, {
dir: join(__dirname, 'routes')
});
const graceful = new Graceful({ brees: [scheduler] });
graceful.listen();
fastify.register(cookie)
fastify.register(cors);
fastify.addHook('onRequest', async (request, reply) => {
let allowedList = ['coolify:3000'];
const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({})
setInterval(async () => {
if (!scheduler.workers.has('deployApplication')) {
scheduler.run('deployApplication');
ipv4 && allowedList.push(`${ipv4}:3000`);
ipv6 && allowedList.push(ipv6);
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')) {
scheduler.run('infrastructure');
if (!allowedList.includes(request.headers.host)) {
// 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
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
}, isDev ? 5000 : 60000 * 15)
const graceful = new Graceful({ brees: [scheduler] });
graceful.listen();
// cleanupStorage
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
}, isDev ? 6000 : 60000 * 10)
setInterval(async () => {
if (!scheduler.workers.has('deployApplication')) {
scheduler.run('deployApplication');
}
if (!scheduler.workers.has('infrastructure')) {
scheduler.run('infrastructure');
}
}, 2000)
// checkProxies
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
}, 10000)
// autoUpdater
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
}, isDev ? 5000 : 60000 * 15)
// cleanupPrismaEngines
// setInterval(async () => {
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
// }, 60000)
// cleanupStorage
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
}, 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() {
const { publicIpv4, publicIpv6 } = await import('public-ip')
try {
@@ -175,4 +199,15 @@ async function getArch() {
} 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 {
await prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) {
console.log(err);
}
} catch (err) { }
if (!pullmergeRequestId) {
if (configHash !== currentHash) {
@@ -263,7 +261,10 @@ import * as buildpacks from '../lib/buildPacks';
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
@@ -359,21 +360,15 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error, buildId, applicationId: application.id });
}
});
}
await pAll.default(actions, { concurrency })
}
} catch (error) {
console.log(error)
} finally {
}
})
while (true) {
await th()
}
} else process.exit(0);
})();

View File

@@ -1,7 +1,7 @@
import { parentPort } from 'node:worker_threads';
import axios from 'axios';
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() {
try {
@@ -21,23 +21,23 @@ async function autoUpdater() {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=true' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
);
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
);
}
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) {
console.log(error);
}
} catch (error) { }
}
async function checkProxies() {
try {
@@ -45,18 +45,35 @@ async function checkProxies() {
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
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 })
if (!portReachable) {
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
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
@@ -113,9 +130,7 @@ async function cleanupPrismaEngines() {
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
}
} catch (error) {
console.log(error);
}
} catch (error) { }
}
}
async function cleanupStorage() {
@@ -166,9 +181,7 @@ async function cleanupStorage() {
lowDiskSpace = true;
}
}
} catch (error) {
console.log(error);
}
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}

View File

@@ -89,6 +89,22 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
}
];
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',
label: 'webdevops/php-apache:8.0'
@@ -145,6 +161,22 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
value: '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',
label: 'webdevops/php-apache:8.0-alpine'
@@ -305,11 +337,11 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
payload.baseImage = 'denoland/deno:latest';
}
if (buildPack === 'php') {
payload.baseImage = 'webdevops/php-apache:8.0-alpine';
payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseImages = phpVersions;
}
if (buildPack === 'laravel') {
payload.baseImage = 'webdevops/php-apache:8.0-alpine';
payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions;
}
@@ -512,7 +544,6 @@ export async function copyBaseConfigurationFiles(
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
}
@@ -525,7 +556,6 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma
);
}
export async function buildImage({
applicationId,
tag,
@@ -646,8 +676,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
secrets,
pullmergeRequestId
} = data;
const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
@@ -657,7 +685,10 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
@@ -676,6 +707,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
Dockerfile.push(`RUN ${installCommand}`);
}
Dockerfile.push(`RUN ${buildCommand}`);
console.log(Dockerfile.join('\n'))
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
}
@@ -691,7 +723,10 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {

View File

@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {

View File

@@ -28,6 +28,7 @@ export default async function (data) {
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
// TODO: fix secrets
if (
(pullmergeRequestId && secret.isPRMRSecret) ||
(!pullmergeRequestId && !secret.isPRMRSecret)

View File

@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {

View File

@@ -23,7 +23,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {

View File

@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {

View File

@@ -16,7 +16,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {

View File

@@ -21,7 +21,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {

View File

@@ -24,7 +24,10 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {

View File

@@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common';
export const version = '3.9.0-rc.1';
export const version = '3.10.1';
export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr';
@@ -193,7 +193,7 @@ export async function isDNSValid(hostname: any, domain: string): Promise<any> {
const { isIP } = await import('is-ip');
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([DNSServers]);
dns.setServers([...DNSServers.split(',')]);
}
let resolves = [];
try {
@@ -302,7 +302,7 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([DNSServers]);
dns.setServers([...DNSServers.split(',')]);
}
let resolves = [];
@@ -414,6 +414,12 @@ export const supportedDatabaseTypesAndVersions = [
baseImageARM: 'couchdb',
versions: ['3.2.2', '3.1.2', '2.3.1'],
versionsARM: ['3.2.2', '3.1.2', '2.3.1']
},
{
name: 'edgedb',
fancyName: 'EdgeDB',
baseImage: 'edgedb/edgedb',
versions: ['latest', '2.1', '2.0', '1.4']
}
];
@@ -439,7 +445,6 @@ export async function getFreeSSHLocalPort(id: string): Promise<number | boolean>
return Number(alreadyConfigured.sshLocalPort)
}
const range = generateRangeArray(minPort, maxPort)
console.log({ ports })
const availablePorts = range.filter(port => !ports.map(p => p.sshLocalPort).includes(port))
for (const port of availablePorts) {
const found = await isReachable(port, { host: 'localhost' })
@@ -458,20 +463,21 @@ export async function createRemoteEngineConfiguration(id: string) {
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 })
// 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) {
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`)
if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
try {
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`)
} catch (error) {
console.log(error)
}
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) { }
}
const config = sshConfig.parse('')
@@ -493,8 +499,26 @@ export async function createRemoteEngineConfiguration(id: string) {
}
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config))
}
export async function executeSSHCmd({ dockerId, command }) {
const { execaCommand } = await import('execa')
let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId)
engine = `ssh://${remoteIpAddress}`
} else {
engine = 'unix:///var/run/docker.sock'
}
if (process.env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose')
}
}
command = `ssh ${remoteIpAddress} ${command}`
return await execaCommand(command)
}
export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise<any> {
let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
const { execaCommand } = await import('execa')
let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId)
engine = `ssh://${remoteIpAddress}`
@@ -509,14 +533,11 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId
if (command.startsWith(`docker build --progress plain`)) {
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
}
return await asyncExecShell(
`DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}`
);
return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })
}
export async function startTraefikProxy(id: string): Promise<void> {
const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } })
const found = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true });
const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true });
const { id: settingsId, ipv4, ipv6 } = await listSettings();
if (!found) {
@@ -600,7 +621,7 @@ export async function configureNetworkTraefikProxy(destination: any): Promise<vo
export async function stopTraefikProxy(
id: string
): Promise<{ stdout: string; stderr: string } | Error> {
const found = await checkContainer({ dockerId: id, container: 'coolify-proxy' });
const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy' });
await prisma.destinationDocker.update({
where: { id },
data: { isCoolifyProxyUsed: false }
@@ -642,21 +663,20 @@ export function generatePassword({ length = 24, symbols = false, isHex = false }
return password;
}
export function generateDatabaseConfiguration(database: any, arch: string):
| {
volume: string;
image: string;
command?: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MYSQL_DATABASE: string;
MYSQL_PASSWORD: string;
MYSQL_ROOT_USER: string;
MYSQL_USER: string;
MYSQL_ROOT_PASSWORD: string;
};
}
type DatabaseConfiguration = {
volume: string;
image: string;
command?: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MYSQL_DATABASE: string;
MYSQL_PASSWORD: string;
MYSQL_ROOT_USER: string;
MYSQL_USER: string;
MYSQL_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
@@ -691,22 +711,13 @@ export function generateDatabaseConfiguration(database: any, arch: string):
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: string;
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string;
};
}
| {
volume: string;
image: string;
command?: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRES_USER: string;
POSTGRES_PASSWORD: string;
POSTGRES_DB: string;
POSTGRES_PASSWORD?: string;
POSTGRES_USER?: string;
POSTGRES_DB?: string;
POSTGRESQL_POSTGRES_PASSWORD?: string;
POSTGRESQL_USERNAME?: string;
POSTGRESQL_PASSWORD?: string;
POSTGRESQL_DATABASE?: string;
};
}
| {
@@ -730,7 +741,21 @@ export function generateDatabaseConfiguration(database: any, arch: string):
COUCHDB_PASSWORD: string;
COUCHDB_USER: string;
};
} {
}
| {
volume: string;
image: string;
command?: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
EDGEDB_SERVER_PASSWORD: string;
EDGEDB_SERVER_USER: string;
EDGEDB_SERVER_DATABASE: string;
EDGEDB_SERVER_TLS_CERT_MODE: string;
};
}
export function generateDatabaseConfiguration(database: any, arch: string): DatabaseConfiguration {
const {
id,
dbUser,
@@ -740,7 +765,6 @@ export function generateDatabaseConfiguration(database: any, arch: string):
defaultDatabase,
version,
type,
settings: { appendOnly }
} = database;
const baseImage = getDatabaseImage(type, arch);
if (type === 'mysql') {
@@ -762,7 +786,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
}
return configuration
} else if (type === 'mariadb') {
const configuration = {
const configuration: DatabaseConfiguration = {
privatePort: 3306,
environmentVariables: {
MARIADB_ROOT_USER: rootUser,
@@ -780,7 +804,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
}
return configuration
} else if (type === 'mongodb') {
const configuration = {
const configuration: DatabaseConfiguration = {
privatePort: 27017,
environmentVariables: {
MONGODB_ROOT_USER: rootUser,
@@ -799,7 +823,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
}
return configuration
} else if (type === 'postgresql') {
const configuration = {
const configuration: DatabaseConfiguration = {
privatePort: 5432,
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
@@ -821,7 +845,8 @@ export function generateDatabaseConfiguration(database: any, arch: string):
}
return configuration
} else if (type === 'redis') {
const configuration = {
const { settings: { appendOnly } } = database;
const configuration: DatabaseConfiguration = {
privatePort: 6379,
command: undefined,
environmentVariables: {
@@ -838,7 +863,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
}
return configuration
} else if (type === 'couchdb') {
const configuration = {
const configuration: DatabaseConfiguration = {
privatePort: 5984,
environmentVariables: {
COUCHDB_PASSWORD: dbUserPassword,
@@ -852,6 +877,20 @@ export function generateDatabaseConfiguration(database: any, arch: string):
configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
}
return configuration
} else if (type === 'edgedb') {
const configuration: DatabaseConfiguration = {
privatePort: 5656,
environmentVariables: {
EDGEDB_SERVER_PASSWORD: rootUserPassword,
EDGEDB_SERVER_USER: rootUser,
EDGEDB_SERVER_DATABASE: defaultDatabase,
EDGEDB_SERVER_TLS_CERT_MODE: 'generate_self_signed'
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/var/lib/edgedb/data`,
ulimits: {}
};
return configuration
}
}
export function isARM(arch: string) {
@@ -974,9 +1013,14 @@ export const createDirectories = async ({
}): Promise<{ workdir: string; repodir: string }> => {
const repodir = `/tmp/build-sources/${repository}/`;
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}`);
return {
workdir,
repodir
@@ -1018,7 +1062,7 @@ export async function stopTcpHttpProxy(
const { id: dockerId } = destinationDocker;
let container = `${id}-${publicPort}`;
if (forceName) container = forceName;
const found = await checkContainer({ dockerId, container });
const { found } = await checkContainer({ dockerId, container });
try {
if (found) {
return await executeDockerCmd({
@@ -1085,90 +1129,150 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) {
}
}
}
export async function checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, dockerId: string, remoteIpAddress?: string }) {
export async function checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, engine: string, remoteEngine: boolean, remoteIpAddress?: string }) {
if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
}
if (configuredPort) {
if (configuredPort !== exposePort) {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
} else {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
}
export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) {
export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress) {
const { default: checkPort } = await import('is-port-reachable');
const applicationUsed = await (
await prisma.application.findMany({
where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
select: { exposePort: true }
})
).map((a) => a.exposePort);
const serviceUsed = await (
await prisma.service.findMany({
where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
select: { exposePort: true }
})
).map((a) => a.exposePort);
const usedPorts = [...applicationUsed, ...serviceUsed];
if (usedPorts.includes(exposePort)) {
if (remoteEngine) {
const applicationUsed = await (
await prisma.application.findMany({
where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
select: { exposePort: true }
})
).map((a) => a.exposePort);
const serviceUsed = await (
await prisma.service.findMany({
where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
select: { exposePort: true }
})
).map((a) => a.exposePort);
const usedPorts = [...applicationUsed, ...serviceUsed];
if (usedPorts.includes(exposePort)) {
return false
}
const found = await checkPort(exposePort, { host: remoteIpAddress });
if (!found) {
return exposePort
}
return false
} else {
const applicationUsed = await (
await prisma.application.findMany({
where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } },
select: { exposePort: true }
})
).map((a) => a.exposePort);
const serviceUsed = await (
await prisma.service.findMany({
where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } },
select: { exposePort: true }
})
).map((a) => a.exposePort);
const usedPorts = [...applicationUsed, ...serviceUsed];
if (usedPorts.includes(exposePort)) {
return false
}
const found = await checkPort(exposePort, { host: 'localhost' });
if (!found) {
return exposePort
}
return false
}
const found = await checkPort(exposePort, { host: remoteIpAddress || 'localhost' });
if (!found) {
return exposePort
}
return false
}
export function generateRangeArray(start, end) {
return Array.from({ length: (end - start) }, (v, k) => k + start);
}
export async function getFreePublicPort(id, dockerId) {
export async function getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }) {
const { default: isReachable } = await import('is-port-reachable');
const data = await prisma.setting.findFirst();
const { minPort, maxPort } = data;
const dbUsed = await (
await prisma.database.findMany({
where: { publicPort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const wpFtpUsed = await (
await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
select: { ftpPublicPort: true }
})
).map((a) => a.ftpPublicPort);
const wpUsed = await (
await prisma.wordpress.findMany({
where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
select: { mysqlPublicPort: true }
})
).map((a) => a.mysqlPublicPort);
const minioUsed = await (
await prisma.minio.findMany({
where: { publicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
const range = generateRangeArray(minPort, maxPort)
const availablePorts = range.filter(port => !usedPorts.includes(port))
for (const port of availablePorts) {
const found = await isReachable(port, { host: 'localhost' })
if (!found) {
return port
if (remoteEngine) {
const dbUsed = await (
await prisma.database.findMany({
where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const wpFtpUsed = await (
await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
select: { ftpPublicPort: true }
})
).map((a) => a.ftpPublicPort);
const wpUsed = await (
await prisma.wordpress.findMany({
where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
select: { mysqlPublicPort: true }
})
).map((a) => a.mysqlPublicPort);
const minioUsed = await (
await prisma.minio.findMany({
where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
const range = generateRangeArray(minPort, maxPort)
const availablePorts = range.filter(port => !usedPorts.includes(port))
for (const port of availablePorts) {
const found = await isReachable(port, { host: remoteIpAddress })
if (!found) {
return port
}
}
return false
} else {
const dbUsed = await (
await prisma.database.findMany({
where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { engine } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const wpFtpUsed = await (
await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } },
select: { ftpPublicPort: true }
})
).map((a) => a.ftpPublicPort);
const wpUsed = await (
await prisma.wordpress.findMany({
where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } },
select: { mysqlPublicPort: true }
})
).map((a) => a.mysqlPublicPort);
const minioUsed = await (
await prisma.minio.findMany({
where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
const range = generateRangeArray(minPort, maxPort)
const availablePorts = range.filter(port => !usedPorts.includes(port))
for (const port of availablePorts) {
const found = await isReachable(port, { host: 'localhost' })
if (!found) {
return port
}
}
return false
}
return false
}
export async function startTraefikTCPProxy(
@@ -1180,7 +1284,7 @@ export async function startTraefikTCPProxy(
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, id: dockerId, remoteEngine } = destinationDocker;
const container = `${id}-${publicPort}`;
const found = await checkContainer({ dockerId, container, remove: true });
const { found } = await checkContainer({ dockerId, container, remove: true });
const { ipv4, ipv6 } = await listSettings();
let dependentId = id;
@@ -1248,7 +1352,6 @@ export async function startTraefikTCPProxy(
})
}
} catch (error) {
console.log(error);
return error;
}
}
@@ -1436,15 +1539,13 @@ export function convertTolOldVolumeNames(type) {
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
// Cleanup old coolify images
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();
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) {
//console.log(error);
}
} catch (error) { }
if (lowDiskSpace || force) {
if (isDev) {
if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`);
@@ -1452,25 +1553,17 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
}
try {
await executeDockerCmd({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` })
} catch (error) {
//console.log(error);
}
} catch (error) { }
try {
await executeDockerCmd({ dockerId, command: `docker image prune -f` })
} catch (error) {
//console.log(error);
}
} catch (error) { }
try {
await executeDockerCmd({ dockerId, command: `docker image prune -a -f` })
} catch (error) {
//console.log(error);
}
} catch (error) { }
// Cleanup build caches
try {
await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` })
} catch (error) {
//console.log(error);
}
} catch (error) { }
}
}

View File

@@ -13,7 +13,7 @@ export function formatLabelsOnDocker(data) {
return container
})
}
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<boolean> {
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> {
let containerFound = false;
try {
const { stdout } = await executeDockerCmd({
@@ -21,10 +21,12 @@ export async function checkContainer({ dockerId, container, remove = false }: {
command:
`docker inspect --format '{{json .State}}' ${container}`
});
containerFound = true
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running';
const isRestarting = status === 'restarting'
const isExited = status === 'exited'
if (status === 'created') {
await executeDockerCmd({
dockerId,
@@ -39,13 +41,23 @@ export async function checkContainer({ dockerId, container, remove = false }: {
`docker rm ${container}`
});
}
if (isRunning) {
containerFound = true;
}
return {
found: containerFound,
status: {
isRunning,
isRestarting,
isExited
}
};
} catch (err) {
// Container not found
}
return containerFound;
return {
found: false
};
}
export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {
@@ -76,7 +88,6 @@ export async function removeContainer({
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
}
} catch (error) {
console.log(error);
throw error;
}
}

View File

@@ -10,7 +10,8 @@ export default async function ({
branch,
buildId,
privateSshKey,
customPort
customPort,
forPublic
}: {
applicationId: string;
workdir: string;
@@ -21,11 +22,15 @@ export default async function ({
repodir: string;
privateSshKey: string;
customPort: number;
forPublic: boolean;
}): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
if (!forPublic) {
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
}
await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`,
@@ -33,9 +38,16 @@ export default async function ({
applicationId
});
await asyncExecShell(
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
);
if (forPublic) {
await asyncExecShell(
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
);
} else {
await asyncExecShell(
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
);
}
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', '');
}

View File

@@ -198,7 +198,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.plausibleAnalytics)
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
@@ -321,8 +321,8 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('minio');
const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
const publicPort = await getFreePublicPort(id, dockerId);
const { service: { destinationDocker: { remoteEngine, engine, remoteIpAddress } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
const consolePort = 9001;
const { workdir } = await createDirectories({ repository: type, buildId: id });
@@ -333,6 +333,8 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
image: `${image}:${version}`,
volumes: [`${id}-minio-data:/data`],
environmentVariables: {
MINIO_SERVER_URL: fqdn,
MINIO_DOMAIN: getDomain(fqdn),
MINIO_ROOT_USER: rootUser,
MINIO_ROOT_PASSWORD: rootUserPassword,
MINIO_BROWSER_REDIRECT_URL: fqdn
@@ -653,7 +655,7 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
image: config.languagetool.image,
environment: config.languagetool.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes: config.languagetool,
volumes: config.languagetool.volumes,
labels: makeLabelForServices('languagetool'),
...defaultComposeConfiguration(network),
}
@@ -708,7 +710,7 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
[id]: {
container_name: id,
image: config.n8n.image,
volumes: config.n8n,
volumes: config.n8n.volumes,
environment: config.n8n.environmentVariables,
labels: makeLabelForServices('n8n'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
@@ -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 = {
version: '3.8',
services: {
@@ -1086,7 +1088,7 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
FROM ${config.postgresql.image}
COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.umami)
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
@@ -1167,7 +1169,7 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
});
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.hasura)
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
@@ -1272,7 +1274,7 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
config.fider.environmentVariables[secret.name] = secret.value;
});
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.fider)
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
@@ -1880,7 +1882,7 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
config.moodle.environmentVariables[secret.name] = secret.value;
});
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.moodle)
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
@@ -1976,8 +1978,8 @@ async function startGlitchTipService(request: FastifyRequest<ServiceStartStop>)
EMAIL_PORT: emailSmtpPort,
EMAIL_HOST_USER: emailSmtpUser,
EMAIL_HOST_PASSWORD: emailSmtpPassword,
EMAIL_USE_TLS: emailSmtpUseTls,
EMAIL_USE_SSL: emailSmtpUseSsl,
EMAIL_USE_TLS: emailSmtpUseTls ? 'True' : 'False',
EMAIL_USE_SSL: emailSmtpUseSsl ? 'True' : 'False',
EMAIL_BACKEND: emailBackend,
MAILGUN_API_KEY: mailgunApiKey,
SENDGRID_API_KEY: sendgridApiKey,
@@ -2006,7 +2008,7 @@ async function startGlitchTipService(request: FastifyRequest<ServiceStartStop>)
config.glitchTip.environmentVariables[secret.name] = secret.value;
});
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip)
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
@@ -2475,7 +2477,7 @@ async function startTaigaService(request: FastifyRequest<ServiceStartStop>) {
TAIGA_SECRET_KEY: secretKey,
}
},
postgresql: {
image: `postgres:12.3`,
volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`],

View File

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

View File

@@ -74,14 +74,21 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
const { teamId } = request.user
let isRunning = false;
let isExited = false;
let isRestarting = false;
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
isExited = await isContainerExited(application.destinationDocker.id, id);
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
}
// isExited = await isContainerExited(application.destinationDocker.id, id);
}
return {
isRunning,
isRestarting,
isExited,
};
} catch ({ status, message }) {
@@ -156,7 +163,8 @@ export async function getApplicationFromDB(id: string, teamId: string) {
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
persistentStorage: true,
connectedDatabase: true
}
});
if (!application) {
@@ -190,7 +198,8 @@ export async function getApplicationFromDBWebhook(projectId: number, branch: str
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
persistentStorage: true,
connectedDatabase: true
}
});
if (applications.length === 0) {
@@ -242,15 +251,16 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
denoOptions,
baseImage,
baseBuildImage,
deploymentType
deploymentType,
baseDatabaseBranch
} = request.body
if (port) port = Number(port);
if (exposePort) {
exposePort = Number(exposePort);
}
const { destinationDocker: { id: dockerId, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (denoOptions) denoOptions = denoOptions.trim();
const defaultConfiguration = await setDefaultConfiguration({
buildPack,
@@ -263,22 +273,43 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
dockerFileLocation,
denoMainFile
});
await prisma.application.update({
where: { id },
data: {
name,
fqdn,
exposePort,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
baseImage,
baseBuildImage,
deploymentType,
...defaultConfiguration
}
});
if (baseDatabaseBranch) {
await prisma.application.update({
where: { id },
data: {
name,
fqdn,
exposePort,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
baseImage,
baseBuildImage,
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();
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -289,7 +320,7 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
try {
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);
// if (isDouble && autodeploy) {
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
@@ -297,7 +328,7 @@ export async function saveApplicationSettings(request: FastifyRequest<SaveApplic
// }
await prisma.application.update({
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 }
});
return reply.code(201).send();
@@ -315,7 +346,7 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
if (application?.destinationDockerId) {
const container = `${id}-${pullmergeRequestId}`
const { id: dockerId } = application.destinationDocker;
const found = await checkContainer({ dockerId, container });
const { found } = await checkContainer({ dockerId, container });
if (found) {
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
}
@@ -342,7 +373,10 @@ export async function restartApplication(request: FastifyRequest<OnlyId>, reply:
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
@@ -439,7 +473,7 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const { id: dockerId } = application.destinationDocker;
const found = await checkContainer({ dockerId, container: id });
const { found } = await checkContainer({ dockerId, container: id });
if (found) {
await removeContainer({ id, dockerId: application.destinationDocker.id });
}
@@ -478,6 +512,7 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
await prisma.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } });
if (teamId === '0') {
await prisma.application.deleteMany({ where: { id } });
} else {
@@ -501,20 +536,22 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
export async function checkDNS(request: FastifyRequest<CheckDNS>) {
try {
const { id } = request.params
let { exposePort, fqdn, forceSave, dualCerts } = request.body
if (fqdn) fqdn = fqdn.toLowerCase();
if (!fqdn) {
return {}
} else {
fqdn = fqdn.toLowerCase();
}
if (exposePort) exposePort = Number(exposePort);
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
const { destinationDocker: { engine, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
const found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
}
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (isDNSCheckEnabled && !isDev && !forceSave) {
let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress;
@@ -734,6 +771,16 @@ export async function saveBuildPack(request, reply) {
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>) {
try {
@@ -890,7 +937,6 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
})
}
} catch ({ status, message }) {
console.log({ status, message })
return errorHandler({ status, message })
}
}
@@ -982,11 +1028,13 @@ export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
where: { buildId, time: { gt: sequence } },
orderBy: { time: 'asc' }
});
const data = await prisma.build.findFirst({ where: { id: buildId } });
const createdAt = day(data.createdAt).utc();
return {
logs,
logs: logs.map(log => {
log.time = Number(log.time)
return log
}),
took: day().diff(createdAt) / 1000,
status: data?.status || 'queued'
}
@@ -1063,3 +1111,58 @@ export async function cancelDeployment(request: FastifyRequest<CancelDeployment>
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 { 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';
@@ -55,6 +55,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
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/database', async (request, reply) => await saveConnectedDatabase(request, reply));
fastify.get<OnlyId>('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request));
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,
baseImage: string,
baseBuildImage: string,
deploymentType: string
deploymentType: string,
baseDatabaseBranch: string
}
}
export interface SaveApplicationSettings extends OnlyId {
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 {
Querystring: { domain: string; };

View File

@@ -11,6 +11,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
version,
whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true',
whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON,
isRegistrationEnabled: settings.isRegistrationEnabled,
}
} catch ({ status, message }) {
return errorHandler({ status, message })

View File

@@ -3,11 +3,11 @@ import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import yaml from 'js-yaml';
import fs from 'fs/promises';
import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
import { DeleteDatabase, SaveDatabaseType } from './types';
import type { OnlyId } from '../../../../types';
import type { DeleteDatabase, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveDatabaseType, SaveVersion } from './types';
export async function listDatabases(request: FastifyRequest) {
try {
@@ -61,16 +61,18 @@ export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) {
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
const { destinationDockerId, destinationDocker } = database;
if (destinationDockerId) {
try {
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
if (database) {
const { destinationDockerId, destinationDocker } = database;
if (destinationDockerId) {
try {
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
if (JSON.parse(stdout).Running) {
isRunning = true;
if (JSON.parse(stdout).Running) {
isRunning = true;
}
} catch (error) {
//
}
} catch (error) {
//
}
}
return {
@@ -92,15 +94,14 @@ export async function getDatabase(request: FastifyRequest<OnlyId>) {
if (!database) {
throw { status: 404, message: 'Database not found.' }
}
const { arch } = await listSettings();
const settings = await listSettings();
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const configuration = generateDatabaseConfiguration(database, arch);
const settings = await listSettings();
const configuration = generateDatabaseConfiguration(database, settings.arch);
return {
privatePort: configuration?.privatePort,
database,
versions: await getDatabaseVersions(database.type, arch),
versions: await getDatabaseVersions(database.type, settings.arch),
settings
};
} catch ({ status, message }) {
@@ -220,7 +221,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
include: { destinationDocker: true, settings: true, databaseSecret: true }
});
const { arch } = await listSettings();
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
@@ -230,7 +231,8 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
destinationDockerId,
destinationDocker,
publicPort,
settings: { isPublic }
settings: { isPublic },
databaseSecret
} = database;
const { privatePort, command, environmentVariables, image, volume, ulimits } =
generateDatabaseConfiguration(database, arch);
@@ -240,7 +242,11 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
const { workdir } = await createDirectories({ repository: type, buildId: id });
if (databaseSecret.length > 0) {
databaseSecret.forEach((secret) => {
environmentVariables[secret.name] = decrypt(secret.value);
});
}
const composeFile: ComposeFile = {
version: '3.8',
services: {
@@ -248,20 +254,11 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
container_name: id,
image,
command,
networks: [network],
environment: environmentVariables,
volumes: [volume],
ulimits,
labels,
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
...defaultComposeConfiguration(network),
}
},
networks: {
@@ -271,28 +268,16 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
},
volumes: {
[volumeName]: {
external: true
name: volumeName,
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` })
} catch (error) {
console.log(error);
}
try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
return {};
} catch (error) {
console.log(error)
throw {
error
};
}
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
return {};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
@@ -379,6 +364,7 @@ export async function deleteDatabase(request: FastifyRequest<DeleteDatabase>) {
}
}
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
return {}
} catch ({ status, message }) {
@@ -439,10 +425,10 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
let publicPort = null
const { destinationDocker: { id: dockerId } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } })
const { destinationDocker: { remoteEngine, engine, remoteIpAddress } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } })
if (isPublic) {
publicPort = await getFreePublicPort(id, dockerId);
publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
}
await prisma.database.update({
where: { id },
@@ -474,4 +460,69 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
}
export async function getDatabaseSecrets(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
let secrets = await prisma.databaseSecret.findMany({
where: { databaseId: id },
orderBy: { createdAt: 'desc' }
});
secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value);
return secret;
});
return {
secrets
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveDatabaseSecret(request: FastifyRequest<SaveDatabaseSecret>, reply: FastifyReply) {
try {
const { id } = request.params
let { name, value, isNew } = request.body
if (isNew) {
const found = await prisma.databaseSecret.findFirst({ where: { name, databaseId: id } });
if (found) {
throw `Secret ${name} already exists.`
} else {
value = encrypt(value.trim());
await prisma.databaseSecret.create({
data: { name, value, database: { connect: { id } } }
});
}
} else {
value = encrypt(value.trim());
const found = await prisma.databaseSecret.findFirst({ where: { databaseId: id, name } });
if (found) {
await prisma.databaseSecret.updateMany({
where: { databaseId: id, name },
data: { value }
});
} else {
await prisma.databaseSecret.create({
data: { name, value, database: { connect: { id } } }
});
}
}
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteDatabaseSecret(request: FastifyRequest<DeleteDatabaseSecret>) {
try {
const { id } = request.params
const { name } = request.body
await prisma.databaseSecret.deleteMany({ where: { databaseId: id, name } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,8 +1,9 @@
import { FastifyPluginAsync } from 'fastify';
import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import { deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import type { DeleteDatabase, GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
import type { SaveDatabaseType } from './types';
import type { OnlyId } from '../../../../types';
import type { DeleteDatabase, SaveDatabaseType, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveVersion } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
@@ -19,6 +20,10 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<SaveDatabaseSettings>('/:id/settings', async (request) => await saveDatabaseSettings(request));
fastify.get<OnlyId>('/:id/secrets', async (request) => await getDatabaseSecrets(request));
fastify.post<SaveDatabaseSecret>('/:id/secrets', async (request, reply) => await saveDatabaseSecret(request, reply));
fastify.delete<DeleteDatabaseSecret>('/:id/secrets', async (request) => await deleteDatabaseSecret(request));
fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));
fastify.post<SaveDatabaseType>('/:id/configuration/type', async (request, reply) => await saveDatabaseType(request, reply));

View File

@@ -5,4 +5,51 @@ export interface SaveDatabaseType extends OnlyId {
}
export interface DeleteDatabase extends OnlyId {
Body: { force: string }
}
}
export interface SaveVersion extends OnlyId {
Body: {
version: string
}
}
export interface SaveDatabaseDestination extends OnlyId {
Body: {
destinationId: string
}
}
export interface GetDatabaseLogs extends OnlyId {
Querystring: {
since: number
}
}
export interface SaveDatabase extends OnlyId {
Body: {
name: string,
defaultDatabase: string,
dbUser: string,
dbUserPassword: string,
rootUser: string,
rootUserPassword: string,
version: string,
isRunning: boolean
}
}
export interface SaveDatabaseSettings extends OnlyId {
Body: {
isPublic: boolean,
appendOnly: boolean
}
}
export interface SaveDatabaseSecret extends OnlyId {
Body: {
name: string,
value: string,
isNew: string,
}
}
export interface DeleteDatabaseSecret extends OnlyId {
Body: {
name: string,
}
}

View File

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

View File

@@ -1,5 +1,4 @@
import os from 'node:os';
import osu from 'node-os-utils';
import axios from 'axios';
import { compareVersions } from 'compare-versions';
import cuid from 'cuid';
@@ -15,9 +14,10 @@ export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, saltRounds);
}
export async function cleanupManually() {
export async function cleanupManually(request: FastifyRequest) {
try {
const destination = await prisma.destinationDocker.findFirst({ where: { engine: '/var/run/docker.sock' } })
const { serverId } = request.body;
const destination = await prisma.destinationDocker.findUnique({ where: { id: serverId } })
await cleanupDockerStorage(destination.id, true, true)
return {}
} catch ({ status, message }) {
@@ -26,7 +26,7 @@ export async function cleanupManually() {
}
export async function checkUpdate(request: FastifyRequest) {
try {
const isStaging = request.hostname === 'staging.coolify.io'
const isStaging = request.hostname === 'staging.coolify.io' || request.hostname === 'arm.coolify.io'
const currentVersion = version;
const { data: versions } = await axios.get(
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
@@ -52,9 +52,7 @@ export async function update(request: FastifyRequest<Update>) {
const { latestVersion } = request.body;
try {
if (!isDev) {
const { isAutoUpdateEnabled } = (await prisma.setting.findFirst()) || {
isAutoUpdateEnabled: false
};
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
@@ -65,7 +63,6 @@ export async function update(request: FastifyRequest<Update>) {
);
return {};
} else {
console.log(latestVersion);
await asyncSleep(2000);
return {};
}
@@ -78,10 +75,9 @@ export async function restartCoolify(request: FastifyRequest<any>) {
const teamId = request.user.teamId;
if (teamId === '0') {
if (!isDev) {
await asyncExecShell(`docker restart coolify`);
asyncExecShell(`docker restart coolify`);
return {};
} else {
console.log('Restarting Coolify')
return {};
}
}
@@ -90,45 +86,38 @@ export async function restartCoolify(request: FastifyRequest<any>) {
return errorHandler({ status, message })
}
}
export async function showUsage() {
try {
return {
usage: {
uptime: os.uptime(),
memory: await osu.mem.info(),
cpu: {
load: os.loadavg(),
usage: await osu.cpu.usage(),
count: os.cpus().length
},
disk: await osu.drive.info('/')
}
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function showDashboard(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
const applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true }
include: { settings: true, destinationDocker: true, teams: true }
});
const databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true }
include: { settings: true, destinationDocker: true, teams: true }
});
const services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, teams: true }
});
const gitSources = await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true }
});
const destinations = await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true }
});
const settings = await listSettings();
return {
applications,
databases,
services,
gitSources,
destinations,
settings,
};
} catch ({ status, message }) {

View File

@@ -43,17 +43,13 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
onRequest: [fastify.authenticate]
}, async (request) => await showDashboard(request));
fastify.get('/usage', {
onRequest: [fastify.authenticate]
}, async () => await showUsage());
fastify.post('/internal/restart', {
onRequest: [fastify.authenticate]
}, async (request) => await restartCoolify(request));
fastify.post('/internal/cleanup', {
onRequest: [fastify.authenticate]
}, async () => await cleanupManually());
}, async (request) => await cleanupManually(request));
};
export default root;

View File

@@ -0,0 +1,119 @@
import type { FastifyRequest } from 'fastify';
import { errorHandler, executeDockerCmd, prisma, createRemoteEngineConfiguration, executeSSHCmd } from '../../../../lib/common';
import os from 'node:os';
import osu from 'node-os-utils';
export async function listServers(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }, remoteEngine: false }, distinct: ['engine'] })
// const remoteServers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, distinct: ['remoteIpAddress', 'engine'] })
return {
servers
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
const mappingTable = [
['K total memory', 'totalMemoryKB'],
['K used memory', 'usedMemoryKB'],
['K active memory', 'activeMemoryKB'],
['K inactive memory', 'inactiveMemoryKB'],
['K free memory', 'freeMemoryKB'],
['K buffer memory', 'bufferMemoryKB'],
['K swap cache', 'swapCacheKB'],
['K total swap', 'totalSwapKB'],
['K used swap', 'usedSwapKB'],
['K free swap', 'freeSwapKB'],
['non-nice user cpu ticks', 'nonNiceUserCpuTicks'],
['nice user cpu ticks', 'niceUserCpuTicks'],
['system cpu ticks', 'systemCpuTicks'],
['idle cpu ticks', 'idleCpuTicks'],
['IO-wait cpu ticks', 'ioWaitCpuTicks'],
['IRQ cpu ticks', 'irqCpuTicks'],
['softirq cpu ticks', 'softIrqCpuTicks'],
['stolen cpu ticks', 'stolenCpuTicks'],
['pages paged in', 'pagesPagedIn'],
['pages paged out', 'pagesPagedOut'],
['pages swapped in', 'pagesSwappedIn'],
['pages swapped out', 'pagesSwappedOut'],
['interrupts', 'interrupts'],
['CPU context switches', 'cpuContextSwitches'],
['boot time', 'bootTime'],
['forks', 'forks']
];
function parseFromText(text) {
var data = {};
var lines = text.split(/\r?\n/);
for (const line of lines) {
for (const [key, value] of mappingTable) {
if (line.indexOf(key) >= 0) {
const values = line.match(/[0-9]+/)[0];
data[value] = parseInt(values, 10);
}
}
}
return data;
}
export async function showUsage(request: FastifyRequest) {
const { id } = request.params;
let { remoteEngine } = request.query
remoteEngine = remoteEngine === 'true' ? true : false
if (remoteEngine) {
const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` })
const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` })
const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` })
// const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` })
// console.log(cpuUsage)
const parsed: any = parseFromText(stats)
return {
usage: {
uptime: parsed.bootTime / 1024,
memory: {
totalMemMb: parsed.totalMemoryKB / 1024,
usedMemMb: parsed.usedMemoryKB / 1024,
freeMemMb: parsed.freeMemoryKB / 1024,
usedMemPercentage: (parsed.usedMemoryKB / parsed.totalMemoryKB) * 100,
freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100
},
cpu: {
load: 0,
usage: 0,
count: cpus
},
disk: {
totalGb: (disks.split(' ')[0] / 1024).toFixed(1),
usedGb: (disks.split(' ')[1] / 1024).toFixed(1),
freeGb: (disks.split(' ')[0] - disks.split(' ')[1]).toFixed(1),
usedPercentage: disks.split(' ')[2].replace('%', ''),
freePercentage: 100 - disks.split(' ')[2].replace('%', '')
}
}
}
} else {
try {
return {
usage: {
uptime: os.uptime(),
memory: await osu.mem.info(),
cpu: {
load: os.loadavg(),
usage: await osu.cpu.usage(),
count: os.cpus().length
},
disk: await osu.drive.info('/')
}
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
}

View File

@@ -0,0 +1,14 @@
import { FastifyPluginAsync } from 'fastify';
import { listServers, showUsage } from './handlers';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listServers(request));
fastify.get('/usage/:id', async (request) => await showUsage(request));
};
export default root;

View File

@@ -0,0 +1,27 @@
import { OnlyId } from "../../../../types"
export interface SaveTeam extends OnlyId {
Body: {
name: string
}
}
export interface InviteToTeam {
Body: {
email: string,
permission: string,
teamId: string,
teamName: string
}
}
export interface BodyId {
Body: {
id: string
}
}
export interface SetPermission {
Body: {
userId: string,
newPermission: string,
permissionId: string
}
}

View File

@@ -1,7 +1,7 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort } from '../../../../lib/common';
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
import { checkContainer, isContainerExited } from '../../../../lib/docker';
import cuid from 'cuid';
@@ -43,13 +43,17 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
let isRunning = false;
let isExited = false
let isRestarting = false;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, settings } = service;
if (destinationDockerId) {
isRunning = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
isExited = await isContainerExited(service.destinationDocker.id, id);
const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
}
}
return {
isRunning,
@@ -70,6 +74,7 @@ export async function getService(request: FastifyRequest<OnlyId>) {
throw { status: 404, message: 'Service not found.' }
}
return {
settings: await listSettings(),
service
}
} catch ({ status, message }) {
@@ -232,7 +237,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
if (exposePort) exposePort = Number(exposePort);
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
const { destinationDocker: { remoteIpAddress, remoteEngine, engine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
@@ -247,7 +252,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
}
}
}
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress })
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (isDNSCheckEnabled && !isDev && !forceSave) {
let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress;
@@ -484,9 +489,9 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
const { id } = request.params
const { ftpEnabled } = request.body;
const { service: { destinationDocker: { id: dockerId } } } = await prisma.wordpress.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
const { service: { destinationDocker: { engine, remoteEngine, remoteIpAddress } } } = await prisma.wordpress.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
const publicPort = await getFreePublicPort(id, dockerId);
const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
let ftpUser = cuid();
let ftpPassword = generatePassword({});
@@ -553,17 +558,14 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
});
try {
const isRunning = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
const { found: isRunning } = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
if (isRunning) {
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
})
}
} catch (error) {
console.log(error);
//
}
} catch (error) { }
const volumes = [
`${id}-wordpress-data:/home/${ftpUser}/wordpress`,
`${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
@@ -642,9 +644,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
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`
);
} catch (error) {
console.log(error)
}
} catch (error) { }
}

View File

@@ -28,6 +28,7 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
try {
const {
fqdn,
isAPIDebuggingEnabled,
isRegistrationEnabled,
dualCerts,
minPort,
@@ -39,7 +40,7 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
const { id } = await listSettings();
await prisma.setting.update({
where: { id },
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers }
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled }
});
if (fqdn) {
await prisma.setting.update({ where: { id }, data: { fqdn } });
@@ -57,7 +58,7 @@ export async function deleteDomain(request: FastifyRequest<DeleteDomain>, reply:
const { fqdn } = request.body
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([DNSServers]);
dns.setServers([...DNSServers.split(',')]);
}
let ip;
try {

View File

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

View File

@@ -3,8 +3,7 @@ import cuid from "cuid";
import crypto from "crypto";
import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker";
import { scheduler } from "../../../lib/scheduler";
import { getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
import type { FastifyReply, FastifyRequest } from "fastify";
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> {
try {
const allowedGithubEvents = ['push', 'pull_request'];
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
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');
//@ts-ignore
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 },
data: { updatedAt: new Date() }
});
console.log(application.id)
await prisma.build.create({
data: {
id: buildId,
@@ -159,7 +154,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
if (application.settings.previews) {
if (application.destinationDockerId) {
const isRunning = await checkContainer(
const { found: isRunning } = await checkContainer(
{
dockerId: application.destinationDocker.id,
container: application.id
@@ -178,6 +173,16 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
where: { id: application.id },
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({
data: {
id: buildId,
@@ -197,9 +202,17 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
} else if (pullmergeRequestAction === 'closed') {
if (application.destinationDockerId) {
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

@@ -107,7 +107,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
const buildId = cuid();
if (application.settings.previews) {
if (application.destinationDockerId) {
const isRunning = await checkContainer(
const { found: isRunning } = await checkContainer(
{
dockerId: application.destinationDocker.id,
container: application.id
@@ -133,7 +133,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
await prisma.build.create({
data: {
id: buildId,
pullmergeRequestId,
pullmergeRequestId: pullmergeRequestId.toString(),
sourceBranch,
applicationId: application.id,
destinationDockerId: application.destinationDocker.id,

View File

@@ -1,4 +1,5 @@
import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../types';
import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers';
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<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;

View File

@@ -1,38 +1,4 @@
export interface OnlyId {
Params: { id: string },
}
export interface SaveVersion extends OnlyId {
Body: {
version: string
}
}
export interface SaveDatabaseDestination extends OnlyId {
Body: {
destinationId: string
}
}
export interface GetDatabaseLogs extends OnlyId {
Querystring: {
since: number
}
}
export interface SaveDatabase extends OnlyId {
Body: {
name: string,
defaultDatabase: string,
dbUser: string,
dbUserPassword: string,
rootUser: string,
rootUserPassword: string,
version: string,
isRunning: boolean
}
}
export interface SaveDatabaseSettings extends OnlyId {
Body: {
isPublic: boolean,
appendOnly: boolean
}
}

View File

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

View File

@@ -4,6 +4,7 @@
import { addToast, appSession, features } from '$lib/store';
import { asyncSleep, errorNotification } from '$lib/common';
import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte';
let isUpdateAvailable = false;
let updateStatus: any = {
@@ -16,7 +17,6 @@
updateStatus.loading = true;
try {
if (dev) {
console.log(`updating to ${latestVersion}`);
await asyncSleep(4000);
return window.location.reload();
} else {
@@ -76,14 +76,14 @@
});
</script>
<div class="flex flex-col space-y-4 py-2">
<div class="py-2">
{#if $appSession.teamId === '0'}
{#if isUpdateAvailable}
<button
id="update"
disabled={updateStatus.success === false}
on:click={update}
class="icons tooltip tooltip-right tooltip-primary bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
data-tip="Update Available!"
class="icons bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
>
{#if updateStatus.loading}
<svg
@@ -184,6 +184,7 @@
>
{/if}
</button>
<Tooltip triggeredBy="#update" placement="right" color="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500">New Version Available!</Tooltip>
{/if}
{/if}
</div>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
export let server: any;
let usage = {
cpu: {
load: [0, 0, 0],
@@ -20,8 +21,7 @@
let usageInterval: any;
let loading = {
usage: false,
cleanup: false,
restart: false
cleanup: false
};
import { addToast, appSession } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
@@ -30,29 +30,11 @@
async function getStatus() {
if (loading.usage) return;
loading.usage = true;
const data = await get('/usage');
const data = await get(`/servers/usage/${server.id}?remoteEngine=${server.remoteEngine}`);
usage = data.usage;
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(() => {
clearInterval(usageInterval);
});
@@ -71,7 +53,7 @@
async function manuallyCleanupStorage() {
try {
loading.cleanup = true;
await post('/internal/cleanup', {});
await post('/internal/cleanup', { serverId: server.id });
return addToast({
message: 'Cleanup done.',
type: 'success'
@@ -84,23 +66,42 @@
}
</script>
<div class="w-full">
<div class="flex lg:flex-row flex-col gap-4">
<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">
{#if $appSession.teamId === '0'}
<button
on:click={manuallyCleanupStorage}
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}
<div class="w-full relative p-5 ">
{#if loading.usage}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
{:else}
<span class="indicator-item badge bg-success badge-sm" />
{/if}
{#if server.remoteEngine}
<div
class="absolute top-0 right-0 text-xl font-bold uppercase bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 p-1 rounded m-2"
>
BETA
</div>
{/if}
<div class="w-full flex flex-row space-x-4">
<div class="flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">
{server.name}
</h1>
<div class="text-xs ">
{#if server?.remoteIpAddress}
<h2>{server?.remoteIpAddress}</h2>
{:else}
<h2>localhost</h2>
{/if}
</div>
</div>
{#if $appSession.teamId === '0'}
<button
on:click={manuallyCleanupStorage}
class:loading={loading.cleanup}
class="btn btn-sm bg-coollabs">Cleanup Storage</button
>
{/if}
</div>
<div class="flex lg:flex-row flex-col gap-4">
<div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0" />
</div>
<div class="divider" />
<div class="grid grid-flow-col gap-4 grid-rows-3 justify-start lg:justify-center lg:grid-rows-1">
@@ -108,21 +109,21 @@
<div class="stat">
<div class="stat-title">Total Memory</div>
<div class="stat-value text-2xl">
{(usage?.memory.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
{(usage?.memory?.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Used Memory</div>
<div class="stat-value text-2xl">
{(usage?.memory.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
{(usage?.memory?.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Free Memory</div>
<div class="stat-value text-2xl">
{usage?.memory.freeMemPercentage}<span class="text-sm">%</span>
{(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span>
</div>
</div>
</div>
@@ -131,41 +132,41 @@
<div class="stat">
<div class="stat-title">Total CPU</div>
<div class="stat-value text-2xl">
{usage?.cpu.count}
{usage?.cpu?.count}
</div>
</div>
<div class="stat">
<div class="stat-title">CPU Usage</div>
<div class="stat-value text-2xl">
{usage?.cpu.usage}<span class="text-sm">%</span>
{usage?.cpu?.usage}<span class="text-sm">%</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Load Average (5,10,30mins)</div>
<div class="stat-value text-2xl">{usage?.cpu.load}</div>
<div class="stat-value text-2xl">{usage?.cpu?.load}</div>
</div>
</div>
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
<div class="stat">
<div class="stat-title">Total Disk</div>
<div class="stat-value text-2xl">
{usage?.disk.totalGb}<span class="text-sm">GB</span>
{usage?.disk?.totalGb}<span class="text-sm">GB</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Used Disk</div>
<div class="stat-value text-2xl">
{usage?.disk.usedGb}<span class="text-sm">GB</span>
{usage?.disk?.usedGb}<span class="text-sm">GB</span>
</div>
</div>
<div class="stat">
<div class="stat-title">Free Disk</div>
<div class="stat-value text-2xl">
{usage?.disk.freePercentage}<span class="text-sm">%</span>
{usage?.disk?.freePercentage}<span class="text-sm">%</span>
</div>
</div>
</div>

View File

@@ -16,4 +16,6 @@
<Icons.Redis {isAbsolute} />
{:else if type === 'couchdb'}
<Icons.CouchDB {isAbsolute} />
{:else if type === 'edgedb'}
<Icons.EdgeDB {isAbsolute} />
{/if}

View File

@@ -0,0 +1,22 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-8 h-16 w-16' : 'mx-auto w-12 h-12'}
width="88"
fill="#1F8AED"
height="101"
viewBox="0 -15 88 101"
><path
class="pageNav_logoBar__2v4ah"
style="transform-origin:center 35.5px"
fill-rule="evenodd"
clip-rule="evenodd"
d="M55.1436 71H58.1436V0H55.1436V71Z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M74.5362 35.3047C74.5362 41.3776 72.1013 42.4662 69.3799 42.4662H63.5935V28.1432H69.3799C72.1013 28.1432 74.5362 29.2318 74.5362 35.3047V35.3047ZM71.5862 35.3047C71.5862 31.0651 70.2971 30.8646 68.4352 30.8646H66.6305V39.7448H68.4352C70.2971 39.7448 71.5862 39.5443 71.5862 35.3047V35.3047ZM40.9348 42.4662V28.1432H50.0442V30.8646H43.9713V33.7865H48.5546V36.4792H43.9713V39.7448H50.0442V42.4662H40.9348ZM80.6092 36.1068V39.7448H83.13C84.7055 39.7448 85.1066 38.7135 85.1066 37.9401C85.1066 37.3385 84.8201 36.1068 82.6717 36.1068H80.6092ZM80.6092 30.8646V33.5859H82.6717C83.8462 33.5859 84.5337 33.0703 84.5337 32.2109C84.5337 31.3516 83.8462 30.8646 82.6717 30.8646H80.6092ZM77.5732 28.1432H83.4169C86.482 28.1432 87.3987 30.2917 87.3987 31.8385C87.3987 33.2708 86.482 34.3021 85.8518 34.5885C87.6851 35.4766 88.0002 37.2813 88.0002 38.1979C88.0002 39.401 87.3987 42.4662 83.4169 42.4662H77.5732V28.1432ZM23.4899 35.3047C23.4899 41.3776 21.055 42.4662 18.3337 42.4662H12.5472V28.1432H18.3337C21.055 28.1432 23.4899 29.2318 23.4899 35.3047V35.3047ZM32.4272 39.8594C33.974 39.8594 34.7761 39.3438 35.0626 39V37.4245H32.599V34.9609H37.4975V40.6615C37.0678 41.3203 34.7188 42.6094 32.5704 42.6094C29.047 42.6094 26.0678 41.2344 26.0678 35.1615C26.0678 29.0885 29.0756 28 31.797 28C36.0652 28 37.1251 30.2344 37.4688 32.2109L34.948 32.7839C34.8048 31.8672 34.0027 30.7214 32.1694 30.7214C30.3074 30.7214 29.0183 30.9219 29.0183 35.1615C29.0183 39.401 30.3647 39.8594 32.4272 39.8594V39.8594ZM20.539 35.3047C20.539 31.0651 19.2499 30.8646 17.3879 30.8646H15.5833V39.7448H17.3879C19.2499 39.7448 20.539 39.5443 20.539 35.3047V35.3047ZM0 42.4662V28.1432H9.10938V30.8646H3.03646V33.7865H7.61979V36.4792H3.03646V39.7448H9.10938V42.4662H0Z"
/></svg
>

View File

@@ -6,5 +6,6 @@ export { default as MongoDB } from './MongoDB.svelte';
export { default as MySQL } from './MySQL.svelte';
export { default as PostgreSQL } from './PostgreSQL.svelte';
export { default as Redis } from './Redis.svelte';
export { default as EdgeDB } from './EdgeDB.svelte';

View File

@@ -306,7 +306,7 @@
"change_language": "Change Language",
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
"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.",
"registration_allowed": "Registration allowed?",
"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';
interface AppSession {
isRegistrationEnabled: boolean;
ipv4: string | null,
ipv6: string | null,
version: string | null,
@@ -25,8 +26,11 @@ interface AddToast {
message: string,
timeout?: number | undefined
}
export const search: any = writable('')
export const loginEmail: Writable<string | undefined> = writable()
export const appSession: Writable<AppSession> = writable({
isRegistrationEnabled: false,
ipv4: null,
ipv6: null,
version: null,
@@ -45,10 +49,31 @@ export const appSession: Writable<AppSession> = writable({
supportedServiceTypesAndVersions: []
});
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({
application: {
isRunning: false,
isExited: false,
isRestarting: false,
loading: false,
initialLoading: true
},
@@ -62,7 +87,8 @@ export const status: Writable<any> = writable({
isRunning: false,
isExited: false,
loading: false,
initialLoading: true
initialLoading: true,
isPublic: false
}
});

View File

@@ -0,0 +1,150 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { post } from '$lib/api';
async function newApplication() {
const { id } = await post('/applications/new', {});
return await goto(`/applications/${id}`, { replaceState: true });
}
async function newService() {
const { id } = await post('/services/new', {});
return await goto(`/services/${id}`, { replaceState: true });
}
async function newDatabase() {
const { id } = await post('/databases/new', {});
return await goto(`/databases/${id}`, { replaceState: true });
}
</script>
<div class="dropdown dropdown-hover">
<slot>
<label for="new" tabindex="0" class="btn btn-square btn-sm bg-coollabs">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
></label
>
</slot>
<ul
id="new"
tabindex="0"
class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52"
>
<li>
<button on:click={newApplication} class="no-underline hover:bg-applications rounded-none ">
<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" />
<rect x="4" y="4" width="6" height="6" rx="1" />
<rect x="4" y="14" width="6" height="6" rx="1" />
<rect x="14" y="14" width="6" height="6" rx="1" />
<line x1="14" y1="7" x2="20" y2="7" />
<line x1="17" y1="4" x2="17" y2="10" />
</svg>Application</button
>
</li>
<li>
<button on:click={newService} class="no-underline hover:bg-services rounded-none ">
<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="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
</svg>Service</button
>
</li>
<li>
<button on:click={newDatabase} class="no-underline hover:bg-databases rounded-none ">
<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" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>Database</button
>
</li>
<li>
<a href="/sources/new" class="no-underline hover:bg-sources rounded-none ">
<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" />
<circle cx="6" cy="6" r="2" />
<circle cx="18" cy="18" r="2" />
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
<polyline points="14 9 11 6 14 3" />
<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" />
<polyline points="10 15 13 18 10 21" />
</svg>Git Source</a
>
</li>
<li>
<a href="/destinations/new" class="no-underline hover:bg-destinations rounded-none ">
<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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>Destination</a
>
</li>
</ul>
</div>

View File

@@ -66,6 +66,7 @@
<script lang="ts">
export let baseSettings: any;
export let supportedServiceTypesAndVersions: any;
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
$appSession.ipv4 = baseSettings.ipv4;
$appSession.ipv6 = baseSettings.ipv6;
$appSession.version = baseSettings.version;
@@ -136,13 +137,14 @@
id="dashboard"
sveltekit:prefetch
href="/"
class="icons bg-coolgray-200 hover:text-white"
class="icons hover:text-white"
class:text-white={$page.url.pathname === '/'}
class:bg-coolgray-500={$page.url.pathname === '/'}
class:bg-coolgray-200={!($page.url.pathname === '/')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
class="h-9 w-9"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@@ -157,157 +159,37 @@
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
</svg>
</a>
<div class="border-t border-stone-700" />
<a
id="applications"
sveltekit:prefetch
href="/applications"
class="icons bg-coolgray-200"
class:text-applications={$page.url.pathname.startsWith('/applications') ||
$page.url.pathname.startsWith('/new/application')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
$page.url.pathname.startsWith('/new/application')}
data-tip="Applications"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
{#if $appSession.teamId === '0'}
<a
id="servers"
sveltekit:prefetch
href="/servers"
class="icons hover:text-white"
class:text-white={$page.url.pathname === '/servers'}
class:bg-coolgray-500={$page.url.pathname === '/servers'}
class:bg-coolgray-200={!($page.url.pathname === '/servers')}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="4" width="6" height="6" rx="1" />
<rect x="4" y="14" width="6" height="6" rx="1" />
<rect x="14" y="14" width="6" height="6" rx="1" />
<line x1="14" y1="7" x2="20" y2="7" />
<line x1="17" y1="4" x2="17" y2="10" />
</svg>
</a>
<a
id="sources"
sveltekit:prefetch
href="/sources"
class="icons bg-coolgray-200"
class:text-sources={$page.url.pathname.startsWith('/sources') ||
$page.url.pathname.startsWith('/new/source')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
$page.url.pathname.startsWith('/new/source')}
data-tip="Git Sources"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
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" />
<circle cx="6" cy="6" r="2" />
<circle cx="18" cy="18" r="2" />
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
<polyline points="14 9 11 6 14 3" />
<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" />
<polyline points="10 15 13 18 10 21" />
</svg>
</a>
<a
id="destinations"
sveltekit:prefetch
href="/destinations"
class="icons bg-coolgray-200"
class:text-destinations={$page.url.pathname.startsWith('/destinations') ||
$page.url.pathname.startsWith('/new/destination')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') ||
$page.url.pathname.startsWith('/new/destination')}
data-tip="Destinations"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>
</a>
<div class="border-t border-stone-700" />
<a
id="databases"
sveltekit:prefetch
href="/databases"
class="icons bg-coolgray-200"
class:text-databases={$page.url.pathname.startsWith('/databases') ||
$page.url.pathname.startsWith('/new/database')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') ||
$page.url.pathname.startsWith('/new/database')}
data-tip="Databases"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</a>
<a
id="services"
sveltekit:prefetch
href="/services"
class="icons bg-coolgray-200"
class:text-services={$page.url.pathname.startsWith('/services') ||
$page.url.pathname.startsWith('/new/service')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/services') ||
$page.url.pathname.startsWith('/new/service')}
data-tip="Services"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
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 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
</svg>
</a>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8 mx-auto"
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="3" y="4" width="18" height="8" rx="3" />
<rect x="3" y="12" width="18" height="8" rx="3" />
<line x1="7" y1="8" x2="7" y2="8.01" />
<line x1="7" y1="16" x2="7" y2="16.01" />
</svg>
</a>
{/if}
</div>
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
<div class="flex-1" />
<UpdateAvailable />
@@ -318,12 +200,12 @@
href="/iam"
class="icons bg-coolgray-200"
class:text-iam={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
data-tip="IAM"
class:bg-coolgray-500={$page.url.pathname === '/iam'}
class:bg-coolgray-200={!($page.url.pathname === '/iam')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
class="h-9 w-9"
stroke-width="1.5"
stroke="currentColor"
fill="none"
@@ -343,13 +225,13 @@
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
class="icons bg-coolgray-200"
class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tip="Settings"
class:bg-coolgray-500={$page.url.pathname === '/settings'}
class:bg-coolgray-200={!($page.url.pathname === '/settings')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
class="h-9 w-9"
stroke-width="1.5"
stroke="currentColor"
fill="none"
@@ -364,15 +246,10 @@
</svg>
</a>
<div
id="logout"
class="icons bg-coolgray-200 hover:text-error"
data-tip="Logout"
on:click={logout}
>
<div id="logout" class="icons bg-coolgray-200 hover:text-error" on:click={logout}>
<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"
stroke-width="1.5"
stroke="currentColor"
@@ -406,19 +283,11 @@
{/if}
{/if}
<main>
<div class="pl-14 lg:px-20">
<div class={$appSession.userId ? 'pl-14 lg:px-20' : null}>
<slot />
</div>
</main>
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#applications" placement="right" color="bg-applications">Applications</Tooltip
>
<Tooltip triggeredBy="#sources" placement="right" color="bg-sources">Git Sources</Tooltip>
<Tooltip triggeredBy="#destinations" placement="right" color="bg-destinations">Destinations</Tooltip
>
<Tooltip triggeredBy="#databases" placement="right" color="bg-databases">Databases</Tooltip>
<Tooltip triggeredBy="#services" placement="right" color="bg-services">Services</Tooltip>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip
>

View File

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

View File

@@ -60,23 +60,26 @@
import { goto } from '$app/navigation';
import { onDestroy, onMount } from 'svelte';
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 Tooltip from '$lib/components/Tooltip.svelte';
let statusInterval: any;
let forceDelete = false;
$disabledButton =
!$appSession.isAdmin ||
(!application.fqdn && !application.settings.isBot) ||
!application.gitSource ||
!application.repository ||
!application.destinationDocker ||
!application.buildPack;
const { id } = $page.params;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
async function handleDeploySubmit(forceRebuild = false) {
if (!$isDeploymentEnabled) return;
try {
const { buildId } = await post(`/applications/${id}/deploy`, {
...application,
@@ -104,9 +107,9 @@
$status.application.initialLoading = true;
try {
await del(`/applications/${id}`, { id, force });
return await goto(`/applications`);
return await window.location.assign(`/`);
} 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;
}
return errorNotification(error);
@@ -151,6 +154,7 @@
const data = await get(`/applications/${id}/status`);
$status.application.isRunning = data.isRunning;
$status.application.isExited = data.isExited;
$status.application.isRestarting = data.isRestarting;
$status.application.loading = false;
$status.application.initialLoading = false;
}
@@ -159,14 +163,17 @@
$status.application.initialLoading = true;
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.isRestarting = false;
$status.application.loading = false;
$location = null;
$isDeploymentEnabled = false;
clearInterval(statusInterval);
});
onMount(async () => {
setLocation(application, settings);
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.isRestarting = false;
$status.application.loading = false;
if (
application.gitSourceId &&
@@ -211,10 +218,10 @@
<div class="border border-coolgray-500 h-8" />
{/if}
{#if $status.application.isExited}
{#if $status.application.isExited || $status.application.isRestarting}
<a
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"
sveltekit:prefetch
>
@@ -236,11 +243,34 @@
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
<Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
<Tooltip triggeredBy="#applicationerror">Application exited or restarting!</Tooltip>
<button
id="stop"
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
>
<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>
{/if}
{#if $status.application.initialLoading}
<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
xmlns="http://www.w3.org/2000/svg"
@@ -266,7 +296,7 @@
id="stop"
on:click={stopApplication}
type="submit"
disabled={$disabledButton}
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
>
<svg
@@ -290,7 +320,7 @@
id="restart"
on:click={restartApplication}
type="submit"
disabled={$disabledButton}
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2"
>
<svg
@@ -314,7 +344,7 @@
<button
id="forceredeploy"
type="submit"
disabled={$disabledButton}
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2"
>
<svg
@@ -341,7 +371,7 @@
<button
id="deploy"
type="submit"
disabled={$disabledButton}
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-success"
>
<svg
@@ -364,14 +394,17 @@
<div class="border border-coolgray-500 h-8" />
<a
id="configurations"
href={!$disabledButton ? `/applications/${id}` : null}
href={$isDeploymentEnabled ? `/applications/${id}` : null}
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-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
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -395,15 +428,16 @@
</svg></button
></a
>
<Tooltip triggeredBy="#configurations">Configurations</Tooltip>
<a
id="secrets"
href={!$disabledButton ? `/applications/${id}/secrets` : null}
href={$isDeploymentEnabled ? `/applications/${id}/secrets` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-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
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -423,15 +457,19 @@
</svg></button
></a
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<a
id="persistentstorages"
href={!$disabledButton ? `/applications/${id}/storages` : null}
href={$isDeploymentEnabled ? `/applications/${id}/storages` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-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
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -449,16 +487,16 @@
</svg>
</button></a
>
<Tooltip triggeredBy="#persistentstorages">Persistent Storages</Tooltip>
{#if !application.settings.isBot}
<a
id="previews"
href={!$disabledButton ? `/applications/${id}/previews` : null}
href={$isDeploymentEnabled ? `/applications/${id}/previews` : null}
sveltekit:prefetch
class="hover:text-orange-500 rounded"
class:text-orange-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
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -478,18 +516,19 @@
</svg></button
></a
>
<Tooltip triggeredBy="#previews">Previews</Tooltip>
{/if}
<div class="border border-coolgray-500 h-8" />
<a
id="applicationlogs"
href={!$disabledButton && $status.application.isRunning ? `/applications/${id}/logs` : null}
href={$isDeploymentEnabled && $status.application.isRunning ? `/applications/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
disabled={$disabledButton || !$status.application.isRunning}
id="applicationlogs"
disabled={!$isDeploymentEnabled || !$status.application.isRunning}
class="icons bg-transparent text-sm"
>
<svg
@@ -511,15 +550,15 @@
</svg>
</button></a
>
<Tooltip triggeredBy="#applicationlogs">Application Logs</Tooltip>
<a
id="buildlogs"
href={!$disabledButton ? `/applications/${id}/logs/build` : null}
href={$isDeploymentEnabled ? `/applications/${id}/logs/build` : null}
sveltekit:prefetch
class="hover:text-red-500 rounded"
class:text-red-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
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -542,10 +581,12 @@
</svg>
</button></a
>
<Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip>
<div class="border border-coolgray-500 h-8" />
{#if forceDelete}
<button
id="forcedelete"
on:click={() => deleteApplication(application.name, true)}
type="submit"
disabled={!$appSession.isAdmin}
@@ -555,6 +596,7 @@
>
Force Delete
</button>
<Tooltip triggeredBy="#forcedelete">Force Delete</Tooltip>
{:else}
<button
id="delete"
@@ -566,14 +608,7 @@
>
<DeleteIcon />
</button>
<Tooltip triggeredBy="#delete">Delete</Tooltip>
{/if}
</nav>
<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() {
try {
loading.branches = true;
publicRepositoryLink = publicRepositoryLink.trim();
const protocol = publicRepositoryLink.split(':')[0];
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,16 +31,24 @@
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
import Select from 'svelte-select';
import { get, post } from '$lib/api';
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 { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation';
const { id } = $page.params;
$: isDisabled =
@@ -64,7 +72,9 @@
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
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 isNonWWWDomainOK = false;
let isWWWDomainOK = false;
@@ -160,8 +170,12 @@
if ($status.application.isRunning) return;
isBot = !isBot;
application.settings.isBot = isBot;
application.fqdn = null;
setLocation(application, settings);
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
try {
await post(`/applications/${id}/settings`, {
previews,
@@ -169,6 +183,7 @@
dualCerts,
isBot,
autodeploy,
isDBBranching,
branch: application.branch,
projectId: application.projectId
});
@@ -192,11 +207,16 @@
if (name === 'isBot') {
isBot = !isBot;
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
return errorNotification(error);
} finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
}
}
async function handleSubmit() {
if (loading || (!application.fqdn && !isBot)) return;
if (loading) return;
loading = true;
try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
@@ -209,16 +229,17 @@
dualCerts,
exposePort: application.exposePort
}));
await post(`/applications/${id}`, { ...application });
await post(`/applications/${id}`, { ...application, baseDatabaseBranch });
setLocation(application, settings);
$disabledButton = false;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
forceSave = false;
addToast({
message: 'Configuration saved.',
type: 'success'
});
} catch (error) {
console.log(error);
//@ts-ignore
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
@@ -509,6 +530,46 @@
</div>
</div>
{/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 class="flex space-x-1 py-5 font-bold">
<div class="title">{$t('application.application')}</div>
@@ -538,7 +599,7 @@
/>
</div>
{#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"
>{$t('application.url_fqdn')}
<Explainer
@@ -551,7 +612,6 @@
disabled={isDisabled}
name="fqdn"
id="fqdn"
required
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
@@ -658,7 +718,7 @@
/>
</div>
{/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"
>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.'}
@@ -700,7 +760,7 @@
placeholder="{$t('forms.default')}: yarn build"
/>
</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"
>{$t('application.start_command')}</label
>

View File

@@ -67,7 +67,6 @@
}
}, 1000);
} catch (error) {
console.log(error);
return errorNotification(error);
}
}
@@ -80,7 +79,6 @@
applicationId: id
});
} catch (error) {
console.log(error);
return errorNotification(error);
}
}
@@ -105,7 +103,7 @@
<button
id="follow"
on:click={followBuild}
class="bg-transparent btn btn-sm btn-linkhover:text-green-500 hover:bg-coolgray-500"
class="bg-transparent btn btn-sm btn-link hover:text-green-500 hover:bg-coolgray-500"
class:text-green-500={followingBuild}
>
<svg

View File

@@ -6,7 +6,8 @@
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import { status } from '$lib/store';
import { goto } from '$app/navigation';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
@@ -16,7 +17,13 @@
let followingLogs: any;
let logsEl: any;
let position = 0;
if (
!$status.application.isExited &&
!$status.application.isRestarting &&
!$status.application.isRunning
) {
goto(`/applications/${$page.params.id}/`, { replaceState: true });
}
const { id } = $page.params;
onMount(async () => {
const response = await get(`/applications/${id}`);
@@ -39,7 +46,6 @@
logs = data.logs;
}
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
logsLoading = false;

View File

@@ -0,0 +1,142 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/applications`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let applications: Array<any> = [];
let ownApplications: Array<any> = [];
let otherApplications: Array<any> = [];
import { get, post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { getDomain } from '$lib/common';
import { appSession } from '$lib/store';
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
ownApplications = applications.filter((application) => {
if (application.teams[0].id === $appSession.teamId) {
return application;
}
});
otherApplications = applications.filter((application) => {
if (application.teams[0].id !== $appSession.teamId) {
return application;
}
});
async function newApplication() {
const { id } = await post('/applications/new', {});
return await goto(`/applications/${id}`, { replaceState: true });
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl">{$t('index.applications')}</div>
{#if $appSession.isAdmin}
<button on:click={newApplication} class="btn btn-square btn-sm bg-applications">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
{/if}
</div>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !applications || ownApplications.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>
</div>
{/if}
{#if ownApplications.length > 0 || otherApplications.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownApplications as application}
<a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
<ApplicationsIcons {application} />
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $appSession.teamId === '0' && otherApplications.length > 0}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if application.settings.isBot}
<div class="truncate text-center">BOT</div>
{/if}
{#if application.destinationDocker?.name}
<div class="truncate text-center">{application.destinationDocker.name}</div>
{/if}
{#if !application.gitSourceId || !application.repository || !application.branch}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Git Source Missing
</div>
{:else if !application.destinationDockerId}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Destination Missing
</div>
{:else if !application.fqdn && !application.settings.isBot}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
URL Missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherApplications.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Applications</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherApplications as application}
<a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
<ApplicationsIcons {application} />
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $appSession.teamId === '0'}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>

View File

@@ -1,142 +1,4 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/applications`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let applications: Array<any> = [];
let ownApplications: Array<any> = [];
let otherApplications: Array<any> = [];
import { get, post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { getDomain } from '$lib/common';
import { appSession } from '$lib/store';
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
ownApplications = applications.filter((application) => {
if (application.teams[0].id === $appSession.teamId) {
return application;
}
});
otherApplications = applications.filter((application) => {
if (application.teams[0].id !== $appSession.teamId) {
return application;
}
});
async function newApplication() {
const { id } = await post('/applications/new', {});
return await goto(`/applications/${id}`, { replaceState: true });
}
goto('/');
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl">{$t('index.applications')}</div>
{#if $appSession.isAdmin}
<button on:click={newApplication} class="btn btn-square btn-sm bg-applications">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
{/if}
</div>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !applications || ownApplications.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>
</div>
{/if}
{#if ownApplications.length > 0 || otherApplications.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownApplications as application}
<a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
<ApplicationsIcons {application} />
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $appSession.teamId === '0' && otherApplications.length > 0}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if application.settings.isBot}
<div class="truncate text-center">BOT</div>
{/if}
{#if application.destinationDocker?.name}
<div class="truncate text-center">{application.destinationDocker.name}</div>
{/if}
{#if !application.gitSourceId || !application.repository || !application.branch}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Git Source Missing
</div>
{:else if !application.destinationDockerId}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Destination Missing
</div>
{:else if !application.fqdn && !application.settings.isBot}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
URL Missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherApplications.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Applications</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherApplications as application}
<a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
<ApplicationsIcons {application} />
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $appSession.teamId === '0'}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>

View File

@@ -3,6 +3,7 @@
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
import CouchDb from '$lib/components/svg/databases/CouchDB.svelte';
import EdgeDb from '$lib/components/svg/databases/EdgeDB.svelte';
import MariaDb from '$lib/components/svg/databases/MariaDB.svelte';
import MongoDb from '$lib/components/svg/databases/MongoDB.svelte';
import MySql from '$lib/components/svg/databases/MySQL.svelte';
@@ -25,5 +26,7 @@
<PostgreSql />
{:else if database.type === 'redis'}
<Redis />
{:else if database.type === 'edgedb'}
<EdgeDb />
{/if}
</span>

View File

@@ -12,6 +12,7 @@
import PostgreSql from './_PostgreSQL.svelte';
import Redis from './_Redis.svelte';
import CouchDb from './_CouchDb.svelte';
import EdgeDB from './_EdgeDB.svelte';
import { post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
@@ -23,7 +24,6 @@
let loading = false;
let publicLoading = false;
let isPublic = database.settings.isPublic || false;
let appendOnly = database.settings.appendOnly;
let databaseDefault: any;
@@ -36,8 +36,10 @@
databaseDefault = database.defaultDatabase;
databaseDbUser = database.dbUser;
databaseDbUserPassword = database.dbUserPassword;
if (database.type === 'mongodb') {
databaseDefault = '?readPreference=primary&ssl=false';
if (database.type === 'mongodb' || database.type === 'edgedb') {
if (database.type === 'mongodb') {
databaseDefault = '?readPreference=primary&ssl=false';
}
databaseDbUser = database.rootUser;
databaseDbUserPassword = database.rootUserPassword;
} else if (database.type === 'redis') {
@@ -49,12 +51,12 @@
return `${database.type}://${
databaseDbUser ? databaseDbUser + ':' : ''
}${databaseDbUserPassword}@${
isPublic
$status.database.isPublic
? database.destinationDocker.remoteEngine
? database.destinationDocker.remoteIpAddress
: $appSession.ipv4
: database.id
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
}:${$status.database.isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
}
async function changeSettings(name: any) {
@@ -63,11 +65,11 @@
}
publicLoading = true;
let data = {
isPublic,
isPublic: $status.database.isPublic,
appendOnly
};
if (name === 'isPublic') {
data.isPublic = !isPublic;
data.isPublic = !$status.database.isPublic;
}
if (name === 'appendOnly') {
data.appendOnly = !appendOnly;
@@ -77,9 +79,9 @@
isPublic: data.isPublic,
appendOnly: data.appendOnly
});
isPublic = data.isPublic;
$status.database.isPublic = data.isPublic;
appendOnly = data.appendOnly;
if (isPublic) {
if ($status.database.isPublic) {
database.publicPort = publicPort;
}
} catch (error) {
@@ -188,7 +190,7 @@
readonly
disabled
name="publicPort"
value={publicLoading ? 'Loading...' : isPublic ? database.publicPort : privatePort}
value={publicLoading ? 'Loading...' : $status.database.isPublic ? database.publicPort : privatePort}
/>
</div>
</div>
@@ -205,12 +207,14 @@
<Redis bind:database />
{:else if database.type === 'couchdb'}
<CouchDb {database} />
{:else if database.type === 'edgedb'}
<EdgeDB {database} />
{/if}
<div class="grid grid-cols-2 items-center px-10 pb-8">
<div>
<label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}
{#if !isPublic && database.destinationDocker.remoteEngine}
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
<Explainer
explanation="You can only access the database with this URL if your application is deployed to the same Destination."
/>
@@ -238,7 +242,7 @@
<Setting
id="isPublic"
loading={publicLoading}
bind:setting={isPublic}
bind:setting={$status.database.isPublic}
on:click={() => changeSettings('isPublic')}
title={$t('database.set_public')}
description={$t('database.warning_database_public')}

View File

@@ -0,0 +1,54 @@
<script lang="ts">
export let database: any;
import { status } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">EdgeDB</div>
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="{$t('forms.eg')}: edgedb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
readonly
disabled
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100"
>Root Password <Explainer
explanation="Could be changed while the database is running."
/></label
>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -0,0 +1,186 @@
<script lang="ts">
export let name = '';
export let value = '';
export let isBuildSecret = false;
export let isNewSecret = false;
export let isPRMRSecret = false;
export let PRMRSecret: any = {};
if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores';
import { del } from '$lib/api';
import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store';
import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte';
import { saveSecret } from './utils';
const dispatch = createEventDispatcher();
const { id } = $page.params;
async function removeSecret() {
try {
await del(`/databases/${id}/secrets`, { name });
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
addToast({
message: 'Secret removed.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
async function createSecret(isNew: any) {
try {
if (!name || !value) return;
await saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
databaseId: id
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
addToast({
message: 'Secret added.',
type: 'success'
});
} else {
addToast({
message: 'Secret updated.',
type: 'success'
});
}
dispatch('refresh');
} catch (error) {
return errorNotification(error);
}
}
async function setSecretValue() {
if (!isPRMRSecret) {
isBuildSecret = !isBuildSecret;
if (!isNewSecret) {
await saveSecret({
isNew: isNewSecret,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
databaseId: id
});
addToast({
message: 'Secret updated.',
type: 'success'
});
}
}
}
</script>
<td>
<input
id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name}
required
placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret}
class:bg-transparent={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
/>
</td>
<td>
<CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
isPasswordField={true}
bind:value
required
placeholder="J$#@UIO%HO#$U%H"
/>
</td>
<td class="text-center">
<button
on:click={setSecretValue}
aria-pressed="false"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret}
class:opacity-50={isPRMRSecret}
class:cursor-not-allowed={isPRMRSecret}
class:cursor-pointer={!isPRMRSecret}
>
<span class="sr-only">Need during buildtime?</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={isBuildSecret}
class:translate-x-0={!isBuildSecret}
>
<span
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret}
class:opacity-100={!isBuildSecret}
aria-hidden="true"
>
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={isBuildSecret}
class:opacity-0={!isBuildSecret}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</button>
</td>
<td>
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="btn bg-databases btn-sm" on:click={() => createSecret(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn bg-databases btn-sm" on:click={() => createSecret(false)}
>{$t('forms.set')}</button
>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end">
<button class="btn btn-sm bg-red-600 hover:bg-red-500" on:click={removeSecret}
>{$t('forms.remove')}</button
>
</div>
{/if}
</div>
{/if}
</td>

View File

@@ -58,16 +58,17 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, status, disabledButton } from '$lib/store';
import { appSession, status, isDeploymentEnabled } from '$lib/store';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { onDestroy, onMount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params;
$status.database.isPublic = database.settings.isPublic || false;
let statusInterval: any = false;
let forceDelete = false;
$disabledButton = !$appSession.isAdmin;
$isDeploymentEnabled = !$appSession.isAdmin;
async function deleteDatabase(force: boolean) {
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
@@ -75,7 +76,7 @@
$status.database.initialLoading = true;
try {
await del(`/databases/${database.id}`, { id: database.id, force });
return await goto('/databases');
return await window.location.assign('/');
} catch (error) {
return errorNotification(error);
} finally {
@@ -87,12 +88,16 @@
const sure = confirm($t('database.confirm_stop', { name: database.name }));
if (sure) {
$status.database.initialLoading = true;
$status.database.loading = true;
try {
await post(`/databases/${database.id}/stop`, {});
$status.database.isPublic = false;
} catch (error) {
return errorNotification(error);
} finally {
$status.database.initialLoading = false;
$status.database.loading = false;
await getStatus();
}
}
}
@@ -145,11 +150,11 @@
{#if id !== 'new'}
<nav class="nav-side">
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
{#if database.type && database.destinationDockerId && database.version}
{#if $status.database.isExited}
<a
id="exited"
href={!$disabledButton ? `/databases/${id}/logs` : null}
href={!$status.database.isRunning ? `/databases/${id}/logs` : null}
class="icons bg-transparent text-sm flex items-center text-red-500 tooltip-error"
sveltekit:prefetch
>
@@ -175,7 +180,7 @@
{/if}
{#if $status.database.initialLoading}
<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
xmlns="http://www.w3.org/2000/svg"
@@ -243,71 +248,100 @@
</button>
<Tooltip triggeredBy="#start">{'Start'}</Tooltip>
{/if}
<div class="border border-stone-700 h-8" />
<a
id="configuration"
href="/databases/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
>
<button class="icons bg-transparent m text-sm disabled:text-red-500">
<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" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<Tooltip triggeredBy="#configuration">{'Configuration'}</Tooltip>
<a
href="/databases/{id}/secrets"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/secrets`}
>
<button id="secrets" disabled={$isDeploymentEnabled} class="icons bg-transparent text-sm ">
<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="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<div class="border border-stone-700 h-8" />
<a
id="databaselogs"
href={$status.database.isRunning ? `/databases/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
>
<button disabled={!$status.database.isRunning} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg></button
></a
>
<Tooltip triggeredBy="#databaselogs">{'Logs'}</Tooltip>
{/if}
<div class="border border-stone-700 h-8" />
<a
id="configuration"
href="/databases/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
>
<button class="icons bg-transparent m text-sm disabled:text-red-500">
<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" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<Tooltip triggeredBy="#configuration">{'Configuration'}</Tooltip>
<div class="border border-stone-700 h-8" />
<a
id="databaselogs"
href={$status.database.isRunning ? `/databases/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}
>
<button disabled={!$status.database.isRunning} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg></button
></a
>
<Tooltip triggeredBy="#databaselogs">{'Logs'}</Tooltip>
{#if forceDelete}
<button
on:click={() => deleteDatabase(true)}

View File

@@ -31,6 +31,7 @@
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
@@ -55,7 +56,7 @@
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-purple-700">
<DatabaseIcons type={type.name} isAbsolute={true} />
<DatabaseIcons type={type.name} isAbsolute={true} />
{type.fancyName}
</button>
</form>

View File

@@ -45,7 +45,6 @@
logs = data.logs;
}
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
loadingLogs = false;
@@ -113,7 +112,7 @@
<button
id="follow"
on:click={followBuild}
class="bg-transparent btn btn-sm"
class="bg-transparent btn btn-sm btn-link"
class:text-green-500={followingLogs}
>
<svg

View File

@@ -0,0 +1,109 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
try {
const response = await get(`/databases/${params.id}/secrets`);
return {
props: {
database: stuff.database,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let secrets: any;
export let database: any;
import pLimit from 'p-limit';
import Secret from './_Secret.svelte';
import { page } from '$app/stores';
import { t } from '$lib/translations';
import { get } from '$lib/api';
import { saveSecret } from './utils';
import { addToast } from '$lib/store';
const limit = pLimit(1);
const { id } = $page.params;
let batchSecrets = '';
async function refreshSecrets() {
const data = await get(`/databases/${id}/secrets`);
secrets = [...data.secrets];
}
async function getValues(e: any) {
e.preventDefault();
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, ...rest] = secret.split('=');
const value = rest.join('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
isNew: !secrets.find((secret: any) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, databaseId: id, isNew }))
)
);
batchSecrets = '';
await refreshSecrets();
addToast({
message: 'Secrets saved.',
type: 'success'
});
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Secrets</div>
<span class="text-xs">{database.name}</span>
</div>
</div>
<div class="mx-auto max-w-6xl px-6 pt-4">
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center">Need during buildtime?</th>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each secrets as secret}
{#key secret.id}
<tr>
<Secret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
<tr>
<Secret isNewSecret on:refresh={refreshSecrets} />
</tr>
</tbody>
</table>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button class="btn btn-sm bg-databases" type="submit">Batch add secrets</button>
</form>
</div>

View File

@@ -0,0 +1,42 @@
import { post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
databaseId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
databaseId
}: Props): Promise<void> {
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try {
await post(`/databases/${databaseId}/secrets`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch (error) {
throw error
}
}

View File

@@ -0,0 +1,124 @@
<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 { goto } from '$app/navigation';
import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
async function newDatabase() {
const { id } = await post('/databases/new', {});
return await goto(`/databases/${id}`, { replaceState: true });
}
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;
}
});
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div>
{#if $appSession.isAdmin}
<button on:click={newDatabase} class="btn btn-square btn-sm bg-databases">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
{/if}
</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}
<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' && 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>
</a>
{/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>

View File

@@ -1,122 +1,4 @@
<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 { goto } from '$app/navigation';
import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
async function newDatabase() {
const { id } = await post('/databases/new', {});
return await goto(`/databases/${id}`, { replaceState: true });
}
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;
}
});
goto('/');
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div>
<button on:click={newDatabase} class="btn btn-square btn-sm bg-databases">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
</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}
<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' && 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>
</a>
{/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>

View File

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

View File

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

View File

@@ -0,0 +1,187 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/destinations`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let destinations: any[];
import { t } from '$lib/translations';
import { appSession } from '$lib/store';
import { get, post } from '$lib/api';
const ownDestinations = destinations.filter((destination) => {
if (destination.teams[0].id === $appSession.teamId) {
return destination;
}
});
const otherDestinations = destinations.filter((destination) => {
if (destination.teams[0].id !== $appSession.teamId) {
return destination;
}
});
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div>
{#if $appSession.isAdmin}
<a href="/destinations/new" class="btn btn-square btn-sm bg-destinations">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
{/if}
</div>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !destinations || ownDestinations.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div>
</div>
{/if}
{#if ownDestinations.length > 0 || otherDestinations.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDestinations as destination}
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<div class="box-selection hover:bg-sky-600 ">
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-4 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $appSession.teamId === '0' && otherDestinations.length > 0}
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<div class="truncate text-center">{destination.network}</div>
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<div class="truncate text-center text-sm text-red-500">Not verified yet</div>
{/if}
{#if destination.remoteEngine && !destination.sshKeyId}
<div class="truncate text-center text-sm text-red-500">SSH Key missing!</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Destinations</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDestinations as destination}
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<div class="box-selection hover:bg-sky-600">
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-4 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $appSession.teamId === '0'}
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<div class="truncate text-center">{destination.network}</div>
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>

View File

@@ -1,187 +1,4 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/destinations`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let destinations: any[];
import { t } from '$lib/translations';
import { appSession } from '$lib/store';
import { get, post } from '$lib/api';
const ownDestinations = destinations.filter((destination) => {
if (destination.teams[0].id === $appSession.teamId) {
return destination;
}
});
const otherDestinations = destinations.filter((destination) => {
if (destination.teams[0].id !== $appSession.teamId) {
return destination;
}
});
import { goto } from '$app/navigation';
goto('/');
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div>
{#if $appSession.isAdmin}
<a href="/destinations/new" class="btn btn-square btn-sm bg-destinations">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
{/if}
</div>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !destinations || ownDestinations.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div>
</div>
{/if}
{#if ownDestinations.length > 0 || otherDestinations.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDestinations as destination}
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<div class="box-selection hover:bg-sky-600 ">
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-4 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $appSession.teamId === '0' && otherDestinations.length > 0}
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<div class="truncate text-center">{destination.network}</div>
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<div class="truncate text-center text-sm text-red-500">Not verified yet</div>
{/if}
{#if destination.remoteEngine && !destination.sshKeyId}
<div class="truncate text-center text-sm text-red-500">SSH Key missing!</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Destinations</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDestinations as destination}
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<div class="box-selection hover:bg-sky-600">
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
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="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-4 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $appSession.teamId === '0'}
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<div class="truncate text-center">{destination.network}</div>
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -49,61 +49,88 @@
<title>{$t('login.login')}</title>
</svelt:head>
<div class="flex h-screen flex-col items-center justify-center">
<div class="flex justify-center px-4">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
{#if $appSession.whiteLabeledDetails.icon}
<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 class="flex lg:flex-row flex-col h-screen">
<div class="bg-neutral-focus h-screen lg:flex hidden flex-col justify-end p-20 flex-1">
<h1 class="title lg:text-6xl mb-5 border-gradient">Coolify</h1>
<h3 class="title">Made self-hosting simple.</h3>
</div>
{#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 class="flex flex-1 flex-col lg:max-w-2xl">
<div class="flex flex-row p-8 items-center space-x-3">
{#if $appSession.whiteLabeledDetails.icon}
<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 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
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">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
>
{#if $appSession.isRegistrationEnabled}
<button on:click|preventDefault={gotoRegister} class="btn btn-ghost"
>{$t('register.register')}</button
>
{:else}
<div class="text-stone-600 text-xs">
Registration is disabled. Please ask an admin to activate it.
</div>
{/if}
</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>
{/if}
</div>
</div>

View File

@@ -1,6 +1,6 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff }) => {
export const load: Load = async ({ stuff }) => {
return {
props: { ...stuff }
};
@@ -9,7 +9,6 @@
<script lang="ts">
export let userCount: number;
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { errorNotification } from '$lib/common';
@@ -17,7 +16,9 @@
import { t } from '$lib/translations';
import { onMount } from 'svelte';
import Cookies from 'js-cookie';
if (!$appSession.isRegistrationEnabled) {
window.location.assign('/');
}
let loading = false;
let emailEl: HTMLInputElement;
let passwordEl: HTMLInputElement;
@@ -61,38 +62,58 @@
}
</script>
<div class="icons fixed top-0 left-0 m-3 cursor-pointer" on:click={() => goto('/')}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="5" y1="12" x2="11" y2="18" />
<line x1="5" y1="12" x2="11" y2="6" />
</svg>
</div>
<div class="flex h-screen flex-col items-center justify-center">
{#if $appSession.userId}
<div class="flex justify-center px-4 text-xl font-bold">{$t('login.already_logged_in')}</div>
{:else}
<div class="flex justify-center px-4">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
<div class="flex lg:flex-row flex-col h-screen">
<div class="bg-neutral-focus h-screen lg:flex hidden flex-col justify-end p-20 flex-1">
<h1 class="title lg:text-6xl mb-5 border-gradient">Coolify</h1>
<h3 class="title">Made self-hosting simple.</h3>
</div>
<div class="flex flex-1 flex-col lg:max-w-2xl">
<div class="flex flex-row p-8 items-center space-x-3 justify-between">
<div class="icons cursor-pointer" on:click={() => goto('/')}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="5" y1="12" x2="11" y2="18" />
<line x1="5" y1="12" x2="11" y2="6" />
</svg>
</div>
<div class="flex flex-row items-center space-x-3">
{#if $appSession.whiteLabeledDetails.icon}
<img
class="w-32 mx-auto pb-8"
src={$appSession.whiteLabeledDetails.icon}
alt="Icon for white labeled version of Coolify"
/>
<div class="avatar" style="width: 40px; height: 40px">
<img
src={$appSession.whiteLabeledDetails.icon}
alt="Icon for white labeled version of Coolify"
/>
</div>
{: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}
</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
type="email"
name="email"
@@ -101,7 +122,7 @@
required
bind:this={emailEl}
bind:value={email}
class="w-56 md:w-96"
class="w-full"
/>
<input
type="password"
@@ -110,7 +131,7 @@
bind:this={passwordEl}
bind:value={password}
required
class="w-56 md:w-96"
class="w-full"
/>
<input
type="password"
@@ -118,13 +139,13 @@
placeholder={$t('forms.password_again')}
bind:value={passwordCheck}
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
type="submit"
class="btn btn-sm"
class="btn"
disabled={loading}
class:bg-transparent={loading}
class:bg-coollabs={!loading}
@@ -132,11 +153,11 @@
>
</div>
</form>
{#if userCount === 0}
<div class="pt-5">
{$t('register.first_user')}
</div>
{/if}
</div>
{#if userCount === 0}
<div class="pt-5">
{$t('register.first_user')}
</div>
{/if}
{/if}
</div>
</div>

View File

@@ -0,0 +1,52 @@
<script context="module" lang="ts">
import { get } from '$lib/api';
import Usage from '$lib/components/Usage.svelte';
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({}) => {
try {
const { servers } = await get('/servers');
return {
props: {
servers
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let servers: any;
import { appSession } from '$lib/store';
import { goto } from '$app/navigation';
if ($appSession.teamId !== '0') {
goto('/');
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Servers</div>
</div>
<div class="container lg:mx-auto lg:p-0 px-8 p-5">
{#if servers.length > 0}
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 p-4">
{#each servers as server}
<div class="no-underline mb-5">
<div class="w-full rounded bg-coolgray-100 indicator">
{#if $appSession.teamId === '0'}
<Usage {server} />
{/if}
</div>
</div>
{/each}
</div>
{:else}
<h1 class="text-center text-xs">Nothing here.</h1>
{/if}
</div>
<div class="text-xs text-center">Remote servers will be here soon</div>

View File

@@ -73,12 +73,12 @@
<td>
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="btn btn-sm bg-success" on:click={() => saveSecret(true)}>Add</button>
<button class="btn btn-sm bg-services" on:click={() => saveSecret(true)}>Add</button>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm bg-success" on:click={() => saveSecret(false)}>Set</button>
<button class="btn btn-sm bg-services" on:click={() => saveSecret(false)}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="btn btn-sm bg-error" on:click={removeSecret}>Remove</button>

View File

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

View File

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

View File

@@ -47,7 +47,6 @@
}
};
} catch (error) {
console.log(error);
return handlerNotFoundLoad(error, url);
}
};
@@ -60,19 +59,21 @@
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
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 Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params;
export let service: any;
$disabledButton =
!$appSession.isAdmin ||
!service.fqdn ||
!service.destinationDocker ||
!service.version ||
!service.type;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
let statusInterval: any;
@@ -84,7 +85,7 @@
if (service.type && $status.service.isRunning)
await post(`/services/${service.id}/${service.type}/stop`, {});
await del(`/services/${service.id}`, { id: service.id });
return await goto(`/services`);
return await window.location.assign(`/`);
} catch (error) {
return errorNotification(error);
} finally {
@@ -96,12 +97,15 @@
const sure = confirm($t('database.confirm_stop', { name: service.name }));
if (sure) {
$status.service.initialLoading = true;
$status.service.loading = true;
try {
await post(`/services/${service.id}/${service.type}/stop`, {});
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
}
}
}
@@ -133,6 +137,7 @@
$status.service.isExited = false;
$status.service.loading = false;
$location = null;
$isDeploymentEnabled = false;
clearInterval(statusInterval);
});
onMount(async () => {
@@ -151,141 +156,142 @@
</script>
<nav class="nav-side">
{#if service.type && service.destinationDockerId && service.version && service.fqdn}
{#if $location}
<a
id="open"
href={$location}
target="_blank"
class="icons flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
{#if $location}
<a
id="open"
href={$location}
target="_blank"
class="icons flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<Tooltip triggeredBy="#open">Open</Tooltip>
<div class="border border-stone-700 h-8" />
{/if}
{#if $status.service.isExited}
<a
id="error"
href={!$disabledButton ? `/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={$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}
<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>
<div class="border border-stone-700 h-8" />
{/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}
<a
id="configuration"
href="/services/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-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
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -311,14 +317,17 @@
>
<Tooltip triggeredBy="#configuration">Configuration</Tooltip>
<a
id="secrets"
href="/services/{id}/secrets"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-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
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -340,14 +349,17 @@
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<a
id="persistentstorage"
href="/services/{id}/storages"
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-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
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -368,14 +380,13 @@
<Tooltip triggeredBy="#persistentstorage">Persistent Storage</Tooltip>
<div class="border border-stone-700 h-8" />
<a
id="logs"
href={!$disabledButton && $status.service.isRunning ? `/services/${id}/logs` : null}
href={$isDeploymentEnabled && $status.service.isRunning ? `/services/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-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
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"

View File

@@ -41,7 +41,6 @@
logs = data.logs;
}
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
logsLoading = false;
@@ -129,7 +128,7 @@
<button
id="follow"
on:click={followBuild}
class="bg-transparent btn btn-sm"
class="bg-transparent btn btn-sm btn-link"
class:text-green-500={followingLogs}
>
<svg

View File

@@ -129,6 +129,6 @@
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button class="btn btn-sm bg-applications" type="submit">Batch add secrets</button>
<button class="btn btn-sm bg-services" type="submit">Batch add secrets</button>
</form>
</div>

View File

@@ -0,0 +1,131 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/services`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let services: any = [];
import { post, get } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { appSession } from '$lib/store';
import { getDomain } from '$lib/common';
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
async function newService() {
const { id } = await post('/services/new', {});
return await goto(`/services/${id}`, { replaceState: true });
}
const ownServices = services.filter((service: any) => {
if (service.teams[0].id === $appSession.teamId) {
return service;
}
});
const otherServices = services.filter((service: any) => {
if (service.teams[0].id !== $appSession.teamId) {
return service;
}
});
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.services')}</div>
{#if $appSession.isAdmin}
<button on:click={newService} class="btn btn-square btn-sm bg-services">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
{/if}
</div>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !services || ownServices.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">{$t('service.no_service')}</div>
</div>
{/if}
{#if ownServices.length > 0 || otherServices.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownServices as service}
<a href="/services/{service.id}" class=" p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600">
<ServiceIcons type={service.type} />
<div class="truncate text-center text-xl font-bold">
{service.name}
</div>
{#if $appSession.teamId === '0' && otherServices.length > 0}
<div class="truncate text-center">{service.teams[0].name}</div>
{/if}
{#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if}
{#if service.destinationDocker?.name}
<div class="truncate text-center">{service.destinationDocker.name}</div>
{/if}
{#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherServices.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Services</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherServices as service}
<a href="/services/{service.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600">
<ServiceIcons type={service.type} />
<div class="truncate text-center text-xl font-bold">
{service.name}
</div>
{#if $appSession.teamId === '0'}
<div class="truncate text-center">{service.teams[0].name}</div>
{/if}
{#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if}
{#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{:else}
<div class="text-center truncate">{service.type}</div>
{/if}
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>

View File

@@ -1,129 +1,4 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/services`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let services: any = [];
import { post, get } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { appSession } from '$lib/store';
import { getDomain } from '$lib/common';
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
async function newService() {
const { id } = await post('/services/new', {});
return await goto(`/services/${id}`, { replaceState: true });
}
const ownServices = services.filter((service: any) => {
if (service.teams[0].id === $appSession.teamId) {
return service;
}
});
const otherServices = services.filter((service: any) => {
if (service.teams[0].id !== $appSession.teamId) {
return service;
}
});
goto('/');
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.services')}</div>
<button on:click={newService} class="btn btn-square btn-sm bg-services">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
</div>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16">
{#if !services || ownServices.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">{$t('service.no_service')}</div>
</div>
{/if}
{#if ownServices.length > 0 || otherServices.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownServices as service}
<a href="/services/{service.id}" class=" p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600">
<ServiceIcons type={service.type} />
<div class="truncate text-center text-xl font-bold">
{service.name}
</div>
{#if $appSession.teamId === '0' && otherServices.length > 0}
<div class="truncate text-center">{service.teams[0].name}</div>
{/if}
{#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if}
{#if service.destinationDocker?.name}
<div class="truncate text-center">{service.destinationDocker.name}</div>
{/if}
{#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherServices.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Services</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherServices as service}
<a href="/services/{service.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600">
<ServiceIcons type={service.type} />
<div class="truncate text-center text-xl font-bold">
{service.name}
</div>
{#if $appSession.teamId === '0'}
<div class="truncate text-center">{service.teams[0].name}</div>
{/if}
{#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if}
{#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{:else}
<div class="text-center truncate">{service.type}</div>
{/if}
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>

View File

@@ -23,10 +23,11 @@
import { browser } from '$app/env';
import { t } from '$lib/translations';
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 Explainer from '$lib/components/Explainer.svelte';
let isAPIDebuggingEnabled = settings.isAPIDebuggingEnabled;
let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
@@ -44,7 +45,8 @@
let loading = {
save: false,
remove: false,
proxyMigration: false
proxyMigration: false,
restart: false
};
async function removeFqdn() {
@@ -75,8 +77,11 @@
if (name === 'isDNSCheckEnabled') {
isDNSCheckEnabled = !isDNSCheckEnabled;
}
if (name === 'isAPIDebuggingEnabled') {
isAPIDebuggingEnabled = !isAPIDebuggingEnabled;
}
await post(`/settings`, {
isAPIDebuggingEnabled,
isRegistrationEnabled,
dualCerts,
isAutoUpdateEnabled,
@@ -129,7 +134,6 @@
}
}
}
console.log(error);
return errorNotification(error);
} finally {
loading.save = false;
@@ -153,6 +157,41 @@
function resetView() {
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>
<div class="flex space-x-1 p-6 font-bold">
@@ -183,11 +222,14 @@
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
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
>
{/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 class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> -->
@@ -311,17 +353,24 @@
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
id="isAutoUpdateEnabled"
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<Setting
id="isAPIDebuggingEnabled"
bind:setting={isAPIDebuggingEnabled}
title="API Debugging"
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."
on:click={() => changeSettings('isAPIDebuggingEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<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>
</form>
</div>

View File

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

Some files were not shown because too many files have changed in this diff Show More