mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-27 04:59:31 +00:00
Compare commits
235 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8902056fdb | ||
|
|
defa6ff6e8 | ||
|
|
eed44e81be | ||
|
|
1951aec5ec | ||
|
|
9c4e0b4107 | ||
|
|
c8deac660d | ||
|
|
4cc5ec9bd0 | ||
|
|
c41bef2e81 | ||
|
|
5b735cf960 | ||
|
|
604e960aa9 | ||
|
|
6c465aa1f2 | ||
|
|
c266832fdc | ||
|
|
906d8d0413 | ||
|
|
cb05fd4a3c | ||
|
|
41e221f0cb | ||
|
|
f75af035bb | ||
|
|
e9e6449edf | ||
|
|
f09d76da35 | ||
|
|
40dfe0919b | ||
|
|
85990dd074 | ||
|
|
38acc16e1c | ||
|
|
b7cc4c1e92 | ||
|
|
1f232d96d8 | ||
|
|
83508f165d | ||
|
|
7cc58e7e84 | ||
|
|
31d9740aac | ||
|
|
69891a64a0 | ||
|
|
0940309600 | ||
|
|
a762b1ed60 | ||
|
|
1b9d9d3a8b | ||
|
|
d9908b3d61 | ||
|
|
c40b80436a | ||
|
|
8f1e352bcc | ||
|
|
18e769b5e5 | ||
|
|
27af6459b3 | ||
|
|
ad80e7f48b | ||
|
|
d81b75b084 | ||
|
|
90f1431047 | ||
|
|
61ea7dabae | ||
|
|
5d9f5f4a7d | ||
|
|
f956f612d3 | ||
|
|
3f5108268d | ||
|
|
4c0dfc3f30 | ||
|
|
1670fe9b1c | ||
|
|
300b28c0f2 | ||
|
|
24e77a5211 | ||
|
|
9df039fbc2 | ||
|
|
143cd46a81 | ||
|
|
680e9871ed | ||
|
|
0a68a48fc5 | ||
|
|
d3af6792d0 | ||
|
|
44dc3b743e | ||
|
|
b469d2832d | ||
|
|
d844026c29 | ||
|
|
21b4990652 | ||
|
|
39e24bdc97 | ||
|
|
bc66b98176 | ||
|
|
d6d3fb46cc | ||
|
|
88ed1446f4 | ||
|
|
c69312f128 | ||
|
|
c5bcff0e10 | ||
|
|
871d1e2440 | ||
|
|
1619afb938 | ||
|
|
25528913f1 | ||
|
|
ef91441c76 | ||
|
|
aa6c56b63d | ||
|
|
18e899d15e | ||
|
|
63fa8924ae | ||
|
|
0e13e3bd81 | ||
|
|
372c0ed457 | ||
|
|
071077200b | ||
|
|
65579a2861 | ||
|
|
bb7603ae2a | ||
|
|
cce67d274e | ||
|
|
794329dcad | ||
|
|
e36fda3ff1 | ||
|
|
3832d33259 | ||
|
|
7350524456 | ||
|
|
a1a973a873 | ||
|
|
f2a915700c | ||
|
|
e184f99617 | ||
|
|
ab07adb14f | ||
|
|
6535c68276 | ||
|
|
dde2772e52 | ||
|
|
ac72c19d22 | ||
|
|
67fc2fd3c0 | ||
|
|
4acc59204c | ||
|
|
07cadb59e0 | ||
|
|
6fa4741c81 | ||
|
|
f4bac2382c | ||
|
|
67b72220c0 | ||
|
|
feeb14ea47 | ||
|
|
bdfb6f5f46 | ||
|
|
53491e9eaa | ||
|
|
f720de65fa | ||
|
|
3d70162a8d | ||
|
|
d3a1bbc3d0 | ||
|
|
0078574ee6 | ||
|
|
7cfd313531 | ||
|
|
e7919e9a1b | ||
|
|
98073202e9 | ||
|
|
8dee345f85 | ||
|
|
9161882f33 | ||
|
|
eef313665b | ||
|
|
53e70fbfcb | ||
|
|
05a1721499 | ||
|
|
2f772080b8 | ||
|
|
a5548c080c | ||
|
|
7e0a1ecc80 | ||
|
|
3f2dcccc07 | ||
|
|
adc5965b32 | ||
|
|
6088f2e573 | ||
|
|
fc705746c0 | ||
|
|
8182359fe4 | ||
|
|
e7ae15162c | ||
|
|
12ca20432d | ||
|
|
8b7406e168 | ||
|
|
9d6317f782 | ||
|
|
d8bdb73140 | ||
|
|
476db15431 | ||
|
|
20ce356296 | ||
|
|
ea594dcbc6 | ||
|
|
021b9746a8 | ||
|
|
c4615ae557 | ||
|
|
95a5089bdc | ||
|
|
cef1fba281 | ||
|
|
5c7859a258 | ||
|
|
986cdae5b0 | ||
|
|
3b11e28d6c | ||
|
|
eba63e8e76 | ||
|
|
2fc65e3b42 | ||
|
|
7d504ab2bf | ||
|
|
216c7efd42 | ||
|
|
8c4149db16 | ||
|
|
20ac8f69ea | ||
|
|
1db3d7a6fb | ||
|
|
b1c1138cf8 | ||
|
|
00b1a4f174 | ||
|
|
86cc665b58 | ||
|
|
e26dd578ef | ||
|
|
f1f3217052 | ||
|
|
8f14fd89ef | ||
|
|
26e4d52a61 | ||
|
|
319c647147 | ||
|
|
f4cd93bd36 | ||
|
|
5a80bb1d2a | ||
|
|
1126dcacf5 | ||
|
|
bdf9a73d19 | ||
|
|
1f73b83a79 | ||
|
|
73bd62c51e | ||
|
|
9acd5c94e8 | ||
|
|
6e85eac14b | ||
|
|
936baf676e | ||
|
|
867f06d813 | ||
|
|
7f9f440789 | ||
|
|
5a15e64471 | ||
|
|
c9aecd51f3 | ||
|
|
6ca1d978d4 | ||
|
|
a18c73bd7c | ||
|
|
26d86cbcb5 | ||
|
|
b24a5d9aca | ||
|
|
5305bc1ceb | ||
|
|
a672f0f56c | ||
|
|
9ab5e13e8f | ||
|
|
a49171f8cc | ||
|
|
65d8dc412a | ||
|
|
8600400632 | ||
|
|
11d10bee12 | ||
|
|
dbd16e8285 | ||
|
|
eb26787079 | ||
|
|
b0a7b1eb3d | ||
|
|
f994092d7f | ||
|
|
946d8e5be5 | ||
|
|
6d7c2ae74a | ||
|
|
1ba71b0b1b | ||
|
|
47c3af6a0e | ||
|
|
e5527a5aa5 | ||
|
|
9bb0dcd73f | ||
|
|
4159804052 | ||
|
|
adb27cf143 | ||
|
|
a4879d854f | ||
|
|
8b92dfb889 | ||
|
|
eb4868cb6e | ||
|
|
e06e6e05ae | ||
|
|
4fce4f81c7 | ||
|
|
ae11283574 | ||
|
|
15fc9aa483 | ||
|
|
2ebfb8e6a9 | ||
|
|
d098ea675f | ||
|
|
8ad152e5fc | ||
|
|
14077fcf51 | ||
|
|
b427573e19 | ||
|
|
46268f0dcf | ||
|
|
006c178eb1 | ||
|
|
d63b20dabb | ||
|
|
fcf0a391ed | ||
|
|
263b9c4b3e | ||
|
|
1dc7355952 | ||
|
|
67e4a72a28 | ||
|
|
e6ea07f9b7 | ||
|
|
44a691ae29 | ||
|
|
290dbc43cb | ||
|
|
219f1f9f3f | ||
|
|
582170f26e | ||
|
|
4e2dad7720 | ||
|
|
d002ec72ad | ||
|
|
f6bb14f7c4 | ||
|
|
e1697848a5 | ||
|
|
4d48bba350 | ||
|
|
a690cc5564 | ||
|
|
be16f76034 | ||
|
|
ae4cf44728 | ||
|
|
4ac0df71b1 | ||
|
|
dbd948867c | ||
|
|
a9b5cd6c31 | ||
|
|
92f513d514 | ||
|
|
b239d21961 | ||
|
|
40e8dd4a8d | ||
|
|
a667435ef2 | ||
|
|
042b4e7587 | ||
|
|
c46a1b4a59 | ||
|
|
e6035d5479 | ||
|
|
008d090093 | ||
|
|
6ff080c36b | ||
|
|
086ca30323 | ||
|
|
17f3ecbbcb | ||
|
|
1f2c8c4ad2 | ||
|
|
bc03331c66 | ||
|
|
ffd1711d4f | ||
|
|
1cdbda1b6b | ||
|
|
fd15e5182d | ||
|
|
7ed1ced521 | ||
|
|
801b9c1483 | ||
|
|
3990bebca3 | ||
|
|
8a2de1001f |
@@ -20,12 +20,13 @@
|
|||||||
"svelte.svelte-vscode",
|
"svelte.svelte-vscode",
|
||||||
"ardenivanov.svelte-intellisense",
|
"ardenivanov.svelte-intellisense",
|
||||||
"Prisma.prisma",
|
"Prisma.prisma",
|
||||||
"bradlc.vscode-tailwindcss"
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"waderyan.gitblame"
|
||||||
],
|
],
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"forwardPorts": [3000, 3001],
|
"forwardPorts": [3000, 3001],
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// 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.
|
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
"remoteUser": "node",
|
"remoteUser": "node",
|
||||||
"features": {
|
"features": {
|
||||||
|
|||||||
64
.github/workflows/production-release.yml
vendored
64
.github/workflows/production-release.yml
vendored
@@ -2,14 +2,14 @@ name: production-release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [released]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
making-something-cool:
|
arm64-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, arm64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -26,11 +26,59 @@ jobs:
|
|||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: coollabsio/coolify:latest,coollabsio/coolify:${{steps.package-version.outputs.current-version}}
|
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
|
||||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
|
||||||
cache-to: type=registry,ref=coollabsio/coolify:buildcache,mode=max
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
|
||||||
|
amd64-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Get current package version
|
||||||
|
uses: martinbeentjes/npm-get-version-action@v1.2.3
|
||||||
|
id: package-version
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64
|
||||||
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
|
||||||
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
|
||||||
|
merge-manifest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [amd64-build, arm64-build]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Get current package version
|
||||||
|
uses: martinbeentjes/npm-get-version-action@v1.2.3
|
||||||
|
id: package-version
|
||||||
|
- name: Create & publish manifest
|
||||||
|
run: |
|
||||||
|
docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
|
||||||
|
docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
|
||||||
- uses: sarisia/actions-status-discord@v1
|
- uses: sarisia/actions-status-discord@v1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
|
|||||||
90
.github/workflows/release-candidate.yml
vendored
Normal file
90
.github/workflows/release-candidate.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
name: release-candidate
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [prereleased]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
arm64-making-something-cool:
|
||||||
|
runs-on: [self-hosted, arm64]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: "next"
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
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:${{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}}
|
||||||
|
|
||||||
64
.github/workflows/staging-release.yml
vendored
64
.github/workflows/staging-release.yml
vendored
@@ -6,11 +6,13 @@ on:
|
|||||||
- next
|
- next
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
staging-release:
|
arm64-making-something-cool:
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, arm64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: "next"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -20,15 +22,65 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Get current package version
|
||||||
|
uses: martinbeentjes/npm-get-version-action@v1.2.3
|
||||||
|
id: package-version
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: coollabsio/coolify:next-arm64
|
||||||
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64
|
||||||
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max
|
||||||
|
amd64-making-something-cool:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: "next"
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Get current package version
|
||||||
|
uses: martinbeentjes/npm-get-version-action@v1.2.3
|
||||||
|
id: package-version
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: coollabsio/coolify:next
|
tags: coollabsio/coolify:next-amd64,coollabsio/coolify:next-test
|
||||||
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next
|
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
|
||||||
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next,mode=max
|
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
|
||||||
|
merge-manifest-to-be-cool:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [arm64-making-something-cool, amd64-making-something-cool]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Create & publish manifest
|
||||||
|
run: |
|
||||||
|
docker manifest create coollabsio/coolify:next --amend coollabsio/coolify:next-amd64 --amend coollabsio/coolify:next-arm64
|
||||||
|
docker manifest push coollabsio/coolify:next
|
||||||
- uses: sarisia/actions-status-discord@v1
|
- uses: sarisia/actions-status-discord@v1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,4 +12,5 @@ client
|
|||||||
apps/api/db/*.db
|
apps/api/db/*.db
|
||||||
local-serve
|
local-serve
|
||||||
apps/api/db/migration.db-journal
|
apps/api/db/migration.db-journal
|
||||||
apps/api/core*
|
apps/api/core*
|
||||||
|
logs
|
||||||
256
CONTRIBUTING.md
256
CONTRIBUTING.md
@@ -1,256 +0,0 @@
|
|||||||
# 👋 Welcome
|
|
||||||
|
|
||||||
First of all, thank you for considering contributing to my project! It means a lot 💜.
|
|
||||||
|
|
||||||
|
|
||||||
## 🙋 Want to help?
|
|
||||||
|
|
||||||
If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide.
|
|
||||||
|
|
||||||
Follow the [introduction](#introduction) to get started then start contributing!
|
|
||||||
|
|
||||||
This is a little list of what you can do to help the project:
|
|
||||||
|
|
||||||
- [🧑💻 Develop your own ideas](#developer-contribution)
|
|
||||||
- [🌐 Translate the project](#translation)
|
|
||||||
|
|
||||||
## 👋 Introduction
|
|
||||||
|
|
||||||
### Setup with 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.
|
|
||||||
|
|
||||||
### Setup with 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.
|
|
||||||
|
|
||||||
### Setup locally in your machine
|
|
||||||
|
|
||||||
> 🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. Consider using Gitpod or Github Codespaces.
|
|
||||||
|
|
||||||
#### Recommended Pull Request Guideline
|
|
||||||
|
|
||||||
- Fork the project
|
|
||||||
- Clone your fork repo to local
|
|
||||||
- Create a new branch
|
|
||||||
- Push to your fork repo
|
|
||||||
- Create a pull request: https://github.com/coollabsio/coolify/compare
|
|
||||||
- Write a proper description
|
|
||||||
- Open the pull request to review against `next` branch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🧑💻 Developer contribution
|
|
||||||
## 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**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to start after you set up your local fork?
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
1. 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!
|
|
||||||
|
|
||||||
2. You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
|
|
||||||
3. You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
|
|
||||||
4. You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
|
|
||||||
4. To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally.
|
|
||||||
|
|
||||||
### Steps for local setup
|
|
||||||
|
|
||||||
1. Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool.
|
|
||||||
2. Install dependencies with `pnpm install`.
|
|
||||||
3. Need to create a local SQlite database with `pnpm db:push`.
|
|
||||||
|
|
||||||
This will apply all migrations at `db/dev.db`.
|
|
||||||
|
|
||||||
4. Seed the database with base entities with `pnpm db:seed`
|
|
||||||
5. You can start coding after starting `pnpm dev`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Database migrations
|
|
||||||
|
|
||||||
During development, if you change the database layout, you need to run `pnpm db:push` to migrate the database and create types for Prisma. You also need to restart the development process.
|
|
||||||
|
|
||||||
If the schema is finalized, you need to create a migration file with `pnpm db:migrate <nameOfMigration>` where `nameOfMigration` is given by you. Make it sense. :)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to add new services
|
|
||||||
|
|
||||||
You can add any open-source and self-hostable software (service/application) to Coolify if the following statements are true:
|
|
||||||
|
|
||||||
- Self-hostable (obviously)
|
|
||||||
- Open-source
|
|
||||||
- Maintained (I do not want to add software full of bugs)
|
|
||||||
|
|
||||||
## Backend
|
|
||||||
|
|
||||||
There are 5 steps you should make on the backend side.
|
|
||||||
|
|
||||||
1. Create Prisma / database schema for the new service.
|
|
||||||
2. Add supported versions of the service.
|
|
||||||
3. Update global functions.
|
|
||||||
4. Create API endpoints.
|
|
||||||
5. Define automatically generated variables.
|
|
||||||
|
|
||||||
> I will use [Umami](https://umami.is/) as an example service.
|
|
||||||
|
|
||||||
### Create Prisma / Database schema for the new service.
|
|
||||||
|
|
||||||
You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB.
|
|
||||||
|
|
||||||
Update Prisma schema in [prisma/schema.prisma](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.
|
|
||||||
|
|
||||||
If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command.
|
|
||||||
|
|
||||||
> You must restart the running development environment to be able to use the new model
|
|
||||||
|
|
||||||
> If you use VSCode/TLS, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running environment.
|
|
||||||
|
|
||||||
### Add supported versions
|
|
||||||
|
|
||||||
Supported versions are hardcoded into Coolify (for now).
|
|
||||||
|
|
||||||
You need to update `supportedServiceTypesAndVersions` function at [src/apps/api/src/lib/supportedVersions.ts](src/apps/api/src/lib/supportedVersions.ts). Example JSON:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
// Name used to identify the service internally
|
|
||||||
name: 'umami',
|
|
||||||
// Fancier name to show to the user
|
|
||||||
fancyName: 'Umami',
|
|
||||||
// Docker base image for the service
|
|
||||||
baseImage: 'ghcr.io/mikecao/umami',
|
|
||||||
// Optional: If there is any dependent image, you should list it here
|
|
||||||
images: [],
|
|
||||||
// Usable tags
|
|
||||||
versions: ['postgresql-latest'],
|
|
||||||
// Which tag is the recommended
|
|
||||||
recommendedVersion: 'postgresql-latest',
|
|
||||||
// Application's default port, Umami listens on 3000
|
|
||||||
ports: {
|
|
||||||
main: 3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add required functions/properties
|
|
||||||
|
|
||||||
1. Add the new service to the `includeServices` variable in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts), so it will be included in all places in the database queries where it is required.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const include: any = {
|
|
||||||
destinationDocker: true,
|
|
||||||
persistentStorage: true,
|
|
||||||
serviceSecret: true,
|
|
||||||
minio: true,
|
|
||||||
plausibleAnalytics: true,
|
|
||||||
vscodeserver: true,
|
|
||||||
wordpress: true,
|
|
||||||
ghost: true,
|
|
||||||
meiliSearch: true,
|
|
||||||
umami: true // This line!
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Update the database update query with the new service type to `configureServiceType` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable).
|
|
||||||
|
|
||||||
```js
|
|
||||||
[...]
|
|
||||||
else if (type === 'umami') {
|
|
||||||
const postgresqlUser = cuid();
|
|
||||||
const postgresqlPassword = encrypt(generatePassword());
|
|
||||||
const postgresqlDatabase = 'umami';
|
|
||||||
const hashSalt = encrypt(generatePassword(64));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
umami: {
|
|
||||||
create: {
|
|
||||||
postgresqlDatabase,
|
|
||||||
postgresqlPassword,
|
|
||||||
postgresqlUser,
|
|
||||||
hashSalt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Add field details to [apps/api/src/lib/services/serviceFields.ts](apps/api/src/lib/services/serviceFields.ts), so every component will know what to do with the values (decrypt/show it by default/readonly)
|
|
||||||
|
|
||||||
```js
|
|
||||||
export const umami = [{
|
|
||||||
name: 'postgresqlUser',
|
|
||||||
isEditable: false,
|
|
||||||
isLowerCase: false,
|
|
||||||
isNumber: false,
|
|
||||||
isBoolean: false,
|
|
||||||
isEncrypted: false
|
|
||||||
}]
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts)
|
|
||||||
|
|
||||||
|
|
||||||
5. You need to add start process for the new service in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
|
|
||||||
|
|
||||||
> See startUmamiService() function as example.
|
|
||||||
|
|
||||||
|
|
||||||
6. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts)
|
|
||||||
|
|
||||||
SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
|
|
||||||
|
|
||||||
7. You need to include it the logo at:
|
|
||||||
|
|
||||||
- [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`.
|
|
||||||
- [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service
|
|
||||||
|
|
||||||
8. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte).
|
|
||||||
|
|
||||||
If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [apps/ui/src/routes/services/[id]/_Services](apps/ui/src/routes/services/[id]/_Services) with an underscore.
|
|
||||||
|
|
||||||
> For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
|
|
||||||
|
|
||||||
|
|
||||||
Good job! 👏
|
|
||||||
|
|
||||||
<!-- # 🌐 Translate the project
|
|
||||||
|
|
||||||
The project use [sveltekit-i18n](https://github.com/sveltekit-i18n/lib) to translate the project.
|
|
||||||
It follows the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to name languages.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
You must have gone throw all the [intro](#introduction) steps before you can start translating.
|
|
||||||
|
|
||||||
It's only an advice, but I recommend you to use:
|
|
||||||
|
|
||||||
- Visual Studio Code
|
|
||||||
- [i18n Ally for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally): ideal to see the progress of the translation.
|
|
||||||
- [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode): to get the syntax color for the project
|
|
||||||
|
|
||||||
### Adding a language
|
|
||||||
|
|
||||||
If your language doesn't appear in the [locales folder list](src/lib/locales/), follow the step below:
|
|
||||||
|
|
||||||
1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`).
|
|
||||||
2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`).
|
|
||||||
3. Have fun translating! -->
|
|
||||||
115
CONTRIBUTION.md
Normal file
115
CONTRIBUTION.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# 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/apps/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.
|
||||||
|
|
||||||
|
Once done, create Prisma schema with `pnpm db:push`.
|
||||||
|
> You may also need to restart `Typescript Language Server` in your IDE to get the new types.
|
||||||
|
|
||||||
|
### Add available versions
|
||||||
|
|
||||||
|
Versions are hardcoded into Coolify at the moment and based on Docker image tags.
|
||||||
|
- Update `supportedServiceTypesAndVersions` function [here](apps/api/src/lib/services/supportedVersions.ts)
|
||||||
|
|
||||||
|
### Include the new service in queries
|
||||||
|
|
||||||
|
At [here](apps/api/src/lib/services/common.ts) in `includeServices` function add the new table name, so it will be included in all places in the database queries where it is required.
|
||||||
|
|
||||||
|
### Define auto-generated fields
|
||||||
|
|
||||||
|
At [here](apps/api/src/lib/services/common.ts) in `configureServiceType` function add the initial auto-generated details such as password, users etc, and the encryption process of secrets (if applicable).
|
||||||
|
|
||||||
|
### Define input field details
|
||||||
|
|
||||||
|
At [here](apps/api/src/lib/services/serviceFields.ts) add details about the input fields shown in the UI, so every component (API/UI) will know what to do with the values (decrypt/show it by default/readonly/etc).
|
||||||
|
|
||||||
|
### Define the start process
|
||||||
|
|
||||||
|
- At [here](apps/api/src/lib/services/handlers.ts), define how the service should start. It could be complex and based on `docker-compose` definitions.
|
||||||
|
|
||||||
|
> See `startUmamiService()` function as example.
|
||||||
|
|
||||||
|
- At [here](apps/api/src/routes/api/v1/services/handlers.ts), add the new start service process to `startService` function.
|
||||||
|
|
||||||
|
### Define the deletion process
|
||||||
|
|
||||||
|
[Here](apps/api/src/lib/services/common.ts) in `removeService` add the database deletion process.
|
||||||
|
|
||||||
|
### Custom logo
|
||||||
|
|
||||||
|
- At [here](apps/ui/src/lib/components/svg/services) add the service custom log as a Svelte component and export it [here](apps/ui/src/lib/components/svg/services/index.ts).
|
||||||
|
|
||||||
|
> SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
|
||||||
|
|
||||||
|
- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property.
|
||||||
|
|
||||||
|
- At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service.
|
||||||
|
|
||||||
|
### Custom fields on the UI
|
||||||
|
By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/routes/services/[id]/_Services/_Services.svelte)
|
||||||
|
|
||||||
|
> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
|
||||||
|
|
||||||
|
Good job! 👏
|
||||||
27
Dockerfile
27
Dockerfile
@@ -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
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache curl
|
RUN apt update && apt -y install curl
|
||||||
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node
|
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pnpm install
|
RUN pnpm install
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# Production build
|
# Production build
|
||||||
FROM node:18-alpine3.16
|
FROM node:18-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \
|
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
|
||||||
PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \
|
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
|
||||||
PRISMA_INTROSPECTION_ENGINE_BINARY=/app/prisma-engines/introspection-engine \
|
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
|
||||||
PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \
|
RUN npm install -g npm@${PNPM_VERSION}
|
||||||
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
|
|
||||||
PRISMA_CLIENT_ENGINE_TYPE=binary
|
|
||||||
|
|
||||||
COPY --from=coollabsio/prisma-engine:4.2.0 /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 mkdir -p ~/.docker/cli-plugins/
|
RUN mkdir -p ~/.docker/cli-plugins/
|
||||||
# https://download.docker.com/linux/static/stable/
|
# https://download.docker.com/linux/static/stable/
|
||||||
@@ -37,6 +33,7 @@ RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
|
|||||||
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
|
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
|
||||||
|
|
||||||
COPY --from=build /app/apps/api/build/ .
|
COPY --from=build /app/apps/api/build/ .
|
||||||
|
COPY --from=build /app/others/fluentbit/ ./fluentbit
|
||||||
COPY --from=build /app/apps/ui/build/ ./public
|
COPY --from=build /app/apps/ui/build/ ./public
|
||||||
COPY --from=build /app/apps/api/prisma/ ./prisma
|
COPY --from=build /app/apps/api/prisma/ ./prisma
|
||||||
COPY --from=build /app/apps/api/package.json .
|
COPY --from=build /app/apps/api/package.json .
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breejs/ts-worker": "2.0.0",
|
"@breejs/ts-worker": "2.0.0",
|
||||||
"@fastify/autoload": "5.2.0",
|
"@fastify/autoload": "5.3.1",
|
||||||
"@fastify/cookie": "8.1.0",
|
"@fastify/cookie": "8.1.0",
|
||||||
"@fastify/cors": "8.1.0",
|
"@fastify/cors": "8.1.0",
|
||||||
"@fastify/env": "4.1.0",
|
"@fastify/env": "4.1.0",
|
||||||
@@ -23,12 +23,14 @@
|
|||||||
"@fastify/static": "6.5.0",
|
"@fastify/static": "6.5.0",
|
||||||
"@iarna/toml": "2.2.5",
|
"@iarna/toml": "2.2.5",
|
||||||
"@ladjs/graceful": "3.0.2",
|
"@ladjs/graceful": "3.0.2",
|
||||||
"@prisma/client": "4.2.1",
|
"@prisma/client": "4.3.1",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"bree": "9.1.2",
|
"bree": "9.1.2",
|
||||||
"cabin": "9.1.2",
|
"cabin": "9.1.2",
|
||||||
"compare-versions": "5.0.1",
|
"compare-versions": "5.0.1",
|
||||||
|
"csv-parse": "^5.3.0",
|
||||||
|
"csvtojson": "^2.0.10",
|
||||||
"cuid": "2.1.8",
|
"cuid": "2.1.8",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.5",
|
||||||
"dockerode": "3.3.4",
|
"dockerode": "3.3.4",
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
"fastify": "4.5.3",
|
"fastify": "4.5.3",
|
||||||
"fastify-plugin": "4.2.1",
|
"fastify-plugin": "4.2.1",
|
||||||
"generate-password": "1.7.0",
|
"generate-password": "1.7.0",
|
||||||
"got": "12.3.1",
|
"got": "12.4.1",
|
||||||
"is-ip": "5.0.0",
|
"is-ip": "5.0.0",
|
||||||
"is-port-reachable": "4.0.0",
|
"is-port-reachable": "4.0.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
@@ -52,20 +54,20 @@
|
|||||||
"unique-names-generator": "4.7.1"
|
"unique-names-generator": "4.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.7.13",
|
"@types/node": "18.7.15",
|
||||||
"@types/node-os-utils": "1.3.0",
|
"@types/node-os-utils": "1.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "5.35.1",
|
"@typescript-eslint/eslint-plugin": "5.36.2",
|
||||||
"@typescript-eslint/parser": "5.35.1",
|
"@typescript-eslint/parser": "5.36.2",
|
||||||
"esbuild": "0.15.5",
|
"esbuild": "0.15.7",
|
||||||
"eslint": "8.23.0",
|
"eslint": "8.23.0",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"nodemon": "2.0.19",
|
"nodemon": "2.0.19",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
"prisma": "4.2.1",
|
"prisma": "4.3.1",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"tsconfig-paths": "4.1.0",
|
"tsconfig-paths": "4.1.0",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.8.2"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "node prisma/seed.js"
|
"seed": "node prisma/seed.js"
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Weblate" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"adminPassword" TEXT NOT NULL,
|
||||||
|
"postgresqlHost" TEXT NOT NULL,
|
||||||
|
"postgresqlPort" INTEGER NOT NULL,
|
||||||
|
"postgresqlUser" TEXT NOT NULL,
|
||||||
|
"postgresqlPassword" TEXT NOT NULL,
|
||||||
|
"postgresqlDatabase" TEXT NOT NULL,
|
||||||
|
"postgresqlPublicPort" INTEGER,
|
||||||
|
"serviceId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Weblate_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Weblate_serviceId_key" ON "Weblate"("serviceId");
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Taiga" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"secretKey" TEXT NOT NULL,
|
||||||
|
"erlangSecret" TEXT NOT NULL,
|
||||||
|
"djangoAdminPassword" TEXT NOT NULL,
|
||||||
|
"djangoAdminUser" TEXT NOT NULL,
|
||||||
|
"rabbitMQUser" TEXT NOT NULL,
|
||||||
|
"rabbitMQPassword" TEXT NOT NULL,
|
||||||
|
"postgresqlHost" TEXT NOT NULL,
|
||||||
|
"postgresqlPort" INTEGER NOT NULL,
|
||||||
|
"postgresqlUser" TEXT NOT NULL,
|
||||||
|
"postgresqlPassword" TEXT NOT NULL,
|
||||||
|
"postgresqlDatabase" TEXT NOT NULL,
|
||||||
|
"postgresqlPublicPort" INTEGER,
|
||||||
|
"serviceId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Taiga_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Taiga_serviceId_key" ON "Taiga"("serviceId");
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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");
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Setting" ADD COLUMN "isAPIDebuggingEnabled" BOOLEAN DEFAULT false;
|
||||||
@@ -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");
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PreviewApplication" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"pullmergeRequestId" TEXT NOT NULL,
|
||||||
|
"sourceBranch" TEXT NOT NULL,
|
||||||
|
"isRandomDomain" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"customDomain" TEXT,
|
||||||
|
"applicationId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "PreviewApplication_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "PreviewApplication_applicationId_key" ON "PreviewApplication"("applicationId");
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["native", "linux-musl"]
|
binaryTargets = ["native"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
@@ -11,6 +11,7 @@ datasource db {
|
|||||||
model Setting {
|
model Setting {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
fqdn String? @unique
|
fqdn String? @unique
|
||||||
|
isAPIDebuggingEnabled Boolean? @default(false)
|
||||||
isRegistrationEnabled Boolean @default(false)
|
isRegistrationEnabled Boolean @default(false)
|
||||||
dualCerts Boolean @default(false)
|
dualCerts Boolean @default(false)
|
||||||
minPort Int @default(9000)
|
minPort Int @default(9000)
|
||||||
@@ -117,6 +118,37 @@ model Application {
|
|||||||
settings ApplicationSettings?
|
settings ApplicationSettings?
|
||||||
secrets Secret[]
|
secrets Secret[]
|
||||||
teams Team[]
|
teams Team[]
|
||||||
|
connectedDatabase ApplicationConnectedDatabase?
|
||||||
|
previewApplication PreviewApplication[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model PreviewApplication {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
pullmergeRequestId String
|
||||||
|
sourceBranch String
|
||||||
|
isRandomDomain Boolean @default(false)
|
||||||
|
customDomain String?
|
||||||
|
applicationId String @unique
|
||||||
|
application Application @relation(fields: [applicationId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model ApplicationConnectedDatabase {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
applicationId String @unique
|
||||||
|
databaseId String?
|
||||||
|
hostedDatabaseType String?
|
||||||
|
hostedDatabaseHost String?
|
||||||
|
hostedDatabasePort Int?
|
||||||
|
hostedDatabaseName String?
|
||||||
|
hostedDatabaseUser String?
|
||||||
|
hostedDatabasePassword String?
|
||||||
|
hostedDatabaseDBName String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
database Database? @relation(fields: [databaseId], references: [id])
|
||||||
|
application Application @relation(fields: [applicationId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApplicationSettings {
|
model ApplicationSettings {
|
||||||
@@ -128,6 +160,7 @@ model ApplicationSettings {
|
|||||||
autodeploy Boolean @default(true)
|
autodeploy Boolean @default(true)
|
||||||
isBot Boolean @default(false)
|
isBot Boolean @default(false)
|
||||||
isPublicRepository Boolean @default(false)
|
isPublicRepository Boolean @default(false)
|
||||||
|
isDBBranching Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
application Application @relation(fields: [applicationId], references: [id])
|
application Application @relation(fields: [applicationId], references: [id])
|
||||||
@@ -186,25 +219,26 @@ model BuildLog {
|
|||||||
applicationId String?
|
applicationId String?
|
||||||
buildId String
|
buildId String
|
||||||
line String
|
line String
|
||||||
time Int
|
time BigInt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Build {
|
model Build {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
type String
|
type String
|
||||||
applicationId String?
|
applicationId String?
|
||||||
destinationDockerId String?
|
destinationDockerId String?
|
||||||
gitSourceId String?
|
gitSourceId String?
|
||||||
githubAppId String?
|
githubAppId String?
|
||||||
gitlabAppId String?
|
gitlabAppId String?
|
||||||
commit String?
|
commit String?
|
||||||
pullmergeRequestId String?
|
pullmergeRequestId String?
|
||||||
forceRebuild Boolean @default(false)
|
previewApplicationId String?
|
||||||
sourceBranch String?
|
forceRebuild Boolean @default(false)
|
||||||
branch String?
|
sourceBranch String?
|
||||||
status String? @default("queued")
|
branch String?
|
||||||
createdAt DateTime @default(now())
|
status String? @default("queued")
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model DestinationDocker {
|
model DestinationDocker {
|
||||||
@@ -291,22 +325,36 @@ model GitlabApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Database {
|
model Database {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
publicPort Int?
|
publicPort Int?
|
||||||
defaultDatabase String?
|
defaultDatabase String?
|
||||||
type String?
|
type String?
|
||||||
version String?
|
version String?
|
||||||
dbUser String?
|
dbUser String?
|
||||||
dbUserPassword String?
|
dbUserPassword String?
|
||||||
rootUser String?
|
rootUser String?
|
||||||
rootUserPassword String?
|
rootUserPassword String?
|
||||||
destinationDockerId String?
|
destinationDockerId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
||||||
settings DatabaseSettings?
|
settings DatabaseSettings?
|
||||||
teams Team[]
|
teams Team[]
|
||||||
|
applicationConnectedDatabase ApplicationConnectedDatabase[]
|
||||||
|
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 {
|
model DatabaseSettings {
|
||||||
@@ -348,6 +396,8 @@ model Service {
|
|||||||
wordpress Wordpress?
|
wordpress Wordpress?
|
||||||
appwrite Appwrite?
|
appwrite Appwrite?
|
||||||
searxng Searxng?
|
searxng Searxng?
|
||||||
|
weblate Weblate?
|
||||||
|
taiga Taiga?
|
||||||
}
|
}
|
||||||
|
|
||||||
model PlausibleAnalytics {
|
model PlausibleAnalytics {
|
||||||
@@ -559,3 +609,38 @@ model Searxng {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
service Service @relation(fields: [serviceId], references: [id])
|
service Service @relation(fields: [serviceId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Weblate {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
adminPassword String
|
||||||
|
postgresqlHost String
|
||||||
|
postgresqlPort Int
|
||||||
|
postgresqlUser String
|
||||||
|
postgresqlPassword String
|
||||||
|
postgresqlDatabase String
|
||||||
|
postgresqlPublicPort Int?
|
||||||
|
serviceId String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
service Service @relation(fields: [serviceId], references: [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Taiga {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
secretKey String
|
||||||
|
erlangSecret String
|
||||||
|
djangoAdminPassword String
|
||||||
|
djangoAdminUser String
|
||||||
|
rabbitMQUser String
|
||||||
|
rabbitMQPassword String
|
||||||
|
postgresqlHost String
|
||||||
|
postgresqlPort Int
|
||||||
|
postgresqlUser String
|
||||||
|
postgresqlPassword String
|
||||||
|
postgresqlDatabase String
|
||||||
|
postgresqlPublicPort Int?
|
||||||
|
serviceId String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
service Service @relation(fields: [serviceId], references: [id])
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const algorithm = 'aes-256-ctr';
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Enable registration for the first user
|
// Enable registration for the first user
|
||||||
// Set initial HAProxy password
|
|
||||||
const settingsFound = await prisma.setting.findFirst({});
|
const settingsFound = await prisma.setting.findFirst({});
|
||||||
if (!settingsFound) {
|
if (!settingsFound) {
|
||||||
await prisma.setting.create({
|
await prisma.setting.create({
|
||||||
@@ -25,7 +24,8 @@ async function main() {
|
|||||||
isRegistrationEnabled: true,
|
isRegistrationEnabled: true,
|
||||||
proxyPassword: encrypt(generatePassword()),
|
proxyPassword: encrypt(generatePassword()),
|
||||||
proxyUser: cuid(),
|
proxyUser: cuid(),
|
||||||
arch: process.arch
|
arch: process.arch,
|
||||||
|
DNSServers: '1.1.1.1,8.8.8.8'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import env from '@fastify/env';
|
|||||||
import cookie from '@fastify/cookie';
|
import cookie from '@fastify/cookie';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
import autoLoad from '@fastify/autoload';
|
import autoLoad from '@fastify/autoload';
|
||||||
import { asyncExecShell, isDev, listSettings, prisma, version } from './lib/common';
|
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
|
||||||
import { scheduler } from './lib/scheduler';
|
import { scheduler } from './lib/scheduler';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import Graceful from '@ladjs/graceful'
|
import Graceful from '@ladjs/graceful'
|
||||||
@@ -26,119 +26,143 @@ declare module 'fastify' {
|
|||||||
|
|
||||||
const port = isDev ? 3001 : 3000;
|
const port = isDev ? 3001 : 3000;
|
||||||
const host = '0.0.0.0';
|
const host = '0.0.0.0';
|
||||||
const fastify = Fastify({
|
prisma.setting.findFirst().then(async (settings) => {
|
||||||
logger: false,
|
const fastify = Fastify({
|
||||||
trustProxy: true
|
logger: settings?.isAPIDebuggingEnabled || false,
|
||||||
});
|
trustProxy: true
|
||||||
const schema = {
|
|
||||||
type: 'object',
|
|
||||||
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
|
|
||||||
properties: {
|
|
||||||
COOLIFY_APP_ID: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
COOLIFY_SECRET_KEY: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
COOLIFY_DATABASE_URL: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'file:../db/dev.db'
|
|
||||||
},
|
|
||||||
COOLIFY_SENTRY_DSN: {
|
|
||||||
type: 'string',
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
COOLIFY_IS_ON: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'docker'
|
|
||||||
},
|
|
||||||
COOLIFY_WHITE_LABELED: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'false'
|
|
||||||
},
|
|
||||||
COOLIFY_WHITE_LABELED_ICON: {
|
|
||||||
type: 'string',
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
COOLIFY_AUTO_UPDATE: {
|
|
||||||
type: 'string',
|
|
||||||
default: 'false'
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
schema,
|
|
||||||
dotenv: true
|
|
||||||
};
|
|
||||||
fastify.register(env, options);
|
|
||||||
if (!isDev) {
|
|
||||||
fastify.register(serve, {
|
|
||||||
root: path.join(__dirname, './public'),
|
|
||||||
preCompressed: true
|
|
||||||
});
|
});
|
||||||
fastify.setNotFoundHandler(async function (request, reply) {
|
const schema = {
|
||||||
if (request.raw.url && request.raw.url.startsWith('/api')) {
|
type: 'object',
|
||||||
return reply.status(404).send({
|
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
|
||||||
success: false
|
properties: {
|
||||||
});
|
COOLIFY_APP_ID: {
|
||||||
}
|
type: 'string',
|
||||||
return reply.status(200).sendFile('index.html');
|
},
|
||||||
});
|
COOLIFY_SECRET_KEY: {
|
||||||
}
|
type: 'string',
|
||||||
fastify.register(autoLoad, {
|
},
|
||||||
dir: join(__dirname, 'plugins')
|
COOLIFY_DATABASE_URL: {
|
||||||
});
|
type: 'string',
|
||||||
fastify.register(autoLoad, {
|
default: 'file:../db/dev.db'
|
||||||
dir: join(__dirname, 'routes')
|
},
|
||||||
});
|
COOLIFY_SENTRY_DSN: {
|
||||||
|
type: 'string',
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
COOLIFY_IS_ON: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'docker'
|
||||||
|
},
|
||||||
|
COOLIFY_WHITE_LABELED: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'false'
|
||||||
|
},
|
||||||
|
COOLIFY_WHITE_LABELED_ICON: {
|
||||||
|
type: 'string',
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
COOLIFY_AUTO_UPDATE: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'false'
|
||||||
|
},
|
||||||
|
|
||||||
fastify.register(cookie)
|
}
|
||||||
fastify.register(cors);
|
};
|
||||||
fastify.listen({ port, host }, async (err: any, address: any) => {
|
|
||||||
if (err) {
|
const options = {
|
||||||
console.error(err);
|
schema,
|
||||||
process.exit(1);
|
dotenv: true
|
||||||
|
};
|
||||||
|
fastify.register(env, options);
|
||||||
|
if (!isDev) {
|
||||||
|
fastify.register(serve, {
|
||||||
|
root: path.join(__dirname, './public'),
|
||||||
|
preCompressed: true
|
||||||
|
});
|
||||||
|
fastify.setNotFoundHandler(async function (request, reply) {
|
||||||
|
if (request.raw.url && request.raw.url.startsWith('/api')) {
|
||||||
|
return reply.status(404).send({
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return reply.status(200).sendFile('index.html');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
console.log(`Coolify's API is listening on ${host}:${port}`);
|
fastify.register(autoLoad, {
|
||||||
await initServer();
|
dir: join(__dirname, 'plugins')
|
||||||
|
});
|
||||||
|
fastify.register(autoLoad, {
|
||||||
|
dir: join(__dirname, 'routes')
|
||||||
|
});
|
||||||
|
|
||||||
const graceful = new Graceful({ brees: [scheduler] });
|
fastify.register(cookie)
|
||||||
graceful.listen();
|
fastify.register(cors);
|
||||||
|
fastify.addHook('onRequest', async (request, reply) => {
|
||||||
|
let allowedList = ['coolify:3000'];
|
||||||
|
const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({})
|
||||||
|
|
||||||
setInterval(async () => {
|
ipv4 && allowedList.push(`${ipv4}:3000`);
|
||||||
if (!scheduler.workers.has('deployApplication')) {
|
ipv6 && allowedList.push(ipv6);
|
||||||
scheduler.run('deployApplication');
|
fqdn && allowedList.push(getDomain(fqdn));
|
||||||
|
isDev && allowedList.push('localhost:3000') && allowedList.push('localhost:3001') && allowedList.push('host.docker.internal:3001');
|
||||||
|
const remotes = await prisma.destinationDocker.findMany({ where: { remoteEngine: true, remoteVerified: true } })
|
||||||
|
if (remotes.length > 0) {
|
||||||
|
remotes.forEach(remote => {
|
||||||
|
allowedList.push(`${remote.remoteIpAddress}:3000`);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (!scheduler.workers.has('infrastructure')) {
|
if (!allowedList.includes(request.headers.host)) {
|
||||||
scheduler.run('infrastructure');
|
// console.log('not allowed', request.headers.host)
|
||||||
}
|
}
|
||||||
}, 2000)
|
})
|
||||||
|
fastify.listen({ port, host }, async (err: any, address: any) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(`Coolify's API is listening on ${host}:${port}`);
|
||||||
|
await initServer();
|
||||||
|
|
||||||
// autoUpdater
|
const graceful = new Graceful({ brees: [scheduler] });
|
||||||
setInterval(async () => {
|
graceful.listen();
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
|
|
||||||
}, isDev ? 5000 : 60000 * 15)
|
|
||||||
|
|
||||||
// cleanupStorage
|
setInterval(async () => {
|
||||||
setInterval(async () => {
|
if (!scheduler.workers.has('deployApplication')) {
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
|
scheduler.run('deployApplication');
|
||||||
}, isDev ? 6000 : 60000 * 10)
|
}
|
||||||
|
if (!scheduler.workers.has('infrastructure')) {
|
||||||
|
scheduler.run('infrastructure');
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
|
||||||
// checkProxies
|
// autoUpdater
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
|
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
|
||||||
}, 10000)
|
}, isDev ? 5000 : 60000 * 15)
|
||||||
|
|
||||||
// cleanupPrismaEngines
|
// cleanupStorage
|
||||||
// setInterval(async () => {
|
setInterval(async () => {
|
||||||
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
|
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
|
||||||
// }, 60000)
|
}, isDev ? 6000 : 60000 * 10)
|
||||||
|
|
||||||
|
// checkProxies
|
||||||
|
setInterval(async () => {
|
||||||
|
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
// cleanupPrismaEngines
|
||||||
|
// setInterval(async () => {
|
||||||
|
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")
|
||||||
|
// }, 60000)
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
getArch(),
|
||||||
|
getIPAddress(),
|
||||||
|
configureRemoteDockers(),
|
||||||
|
])
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
await getArch();
|
|
||||||
await getIPAddress();
|
|
||||||
});
|
|
||||||
async function getIPAddress() {
|
async function getIPAddress() {
|
||||||
const { publicIpv4, publicIpv6 } = await import('public-ip')
|
const { publicIpv4, publicIpv6 } = await import('public-ip')
|
||||||
try {
|
try {
|
||||||
@@ -175,4 +199,15 @@ async function getArch() {
|
|||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function configureRemoteDockers() {
|
||||||
|
try {
|
||||||
|
const remoteDocker = await prisma.destinationDocker.findMany({
|
||||||
|
where: { remoteVerified: true, remoteEngine: true }
|
||||||
|
});
|
||||||
|
if (remoteDocker.length > 0) {
|
||||||
|
for (const docker of remoteDocker) {
|
||||||
|
await createRemoteEngineConfiguration(docker.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,8 +38,16 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
for (const queueBuild of queuedBuilds) {
|
for (const queueBuild of queuedBuilds) {
|
||||||
actions.push(async () => {
|
actions.push(async () => {
|
||||||
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
|
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
|
||||||
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, forceRebuild } = queueBuild
|
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild } = queueBuild
|
||||||
application = decryptApplication(application)
|
application = decryptApplication(application)
|
||||||
|
const originalApplicationId = application.id
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } })
|
||||||
|
if (previewApplications.length > 0) {
|
||||||
|
previewApplicationId = previewApplications[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const usableApplicationId = previewApplicationId || originalApplicationId
|
||||||
try {
|
try {
|
||||||
if (queueBuild.status === 'running') {
|
if (queueBuild.status === 'running') {
|
||||||
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
|
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
|
||||||
@@ -104,17 +112,17 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
)
|
)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
const { debug } = settings;
|
const { debug } = settings;
|
||||||
if (concurrency === 1) {
|
// if (concurrency === 1) {
|
||||||
await prisma.build.updateMany({
|
// await prisma.build.updateMany({
|
||||||
where: {
|
// where: {
|
||||||
status: { in: ['queued', 'running'] },
|
// status: { in: ['queued', 'running'] },
|
||||||
id: { not: buildId },
|
// id: { not: buildId },
|
||||||
applicationId,
|
// applicationId,
|
||||||
createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
|
// createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
|
||||||
},
|
// },
|
||||||
data: { status: 'failed' }
|
// data: { status: 'failed' }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
let imageId = applicationId;
|
let imageId = applicationId;
|
||||||
let domain = getDomain(fqdn);
|
let domain = getDomain(fqdn);
|
||||||
const volumes =
|
const volumes =
|
||||||
@@ -177,9 +185,7 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.build.update({ where: { id: buildId }, data: { commit } });
|
await prisma.build.update({ where: { id: buildId }, data: { commit } });
|
||||||
} catch (err) {
|
} catch (err) { }
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pullmergeRequestId) {
|
if (!pullmergeRequestId) {
|
||||||
if (configHash !== currentHash) {
|
if (configHash !== currentHash) {
|
||||||
@@ -263,7 +269,10 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
if (secrets.length > 0) {
|
if (secrets.length > 0) {
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
envs.push(`${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -337,10 +346,15 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
|
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await saveBuildLog({ line: error, buildId, applicationId });
|
await saveBuildLog({ line: error, buildId, applicationId });
|
||||||
await prisma.build.updateMany({
|
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||||
where: { id: buildId, status: { in: ['queued', 'running'] } },
|
if (foundBuild) {
|
||||||
data: { status: 'failed' }
|
await prisma.build.update({
|
||||||
});
|
where: { id: buildId },
|
||||||
|
data: {
|
||||||
|
status: 'failed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
|
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
|
||||||
@@ -352,28 +366,29 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
await prisma.build.updateMany({
|
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||||
where: { id: buildId, status: { in: ['queued', 'running'] } },
|
if (foundBuild) {
|
||||||
data: { status: 'failed' }
|
await prisma.build.update({
|
||||||
});
|
where: { id: buildId },
|
||||||
await saveBuildLog({ line: error, buildId, applicationId: application.id });
|
data: {
|
||||||
|
status: 'failed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error !== 1) {
|
||||||
|
await saveBuildLog({ line: error, buildId, applicationId: application.id });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await pAll.default(actions, { concurrency })
|
await pAll.default(actions, { concurrency })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
await th()
|
await th()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} else process.exit(0);
|
} else process.exit(0);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { parentPort } from 'node:worker_threads';
|
import { parentPort } from 'node:worker_threads';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version } from '../lib/common';
|
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration } from '../lib/common';
|
||||||
|
|
||||||
async function disconnect() {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
async function autoUpdater() {
|
async function autoUpdater() {
|
||||||
try {
|
try {
|
||||||
const currentVersion = version;
|
const currentVersion = version;
|
||||||
@@ -24,20 +21,23 @@ async function autoUpdater() {
|
|||||||
const activeCount = 0
|
const activeCount = 0
|
||||||
if (activeCount === 0) {
|
if (activeCount === 0) {
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
console.log(`Updating Coolify to ${latestVersion}.`);
|
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
|
||||||
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
if (isAutoUpdateEnabled) {
|
||||||
await asyncExecShell(`env | grep COOLIFY > .env`);
|
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
||||||
await asyncExecShell(
|
await asyncExecShell(`env | grep COOLIFY > .env`);
|
||||||
`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"`
|
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 coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Updating (not really in dev mode).');
|
console.log('Updating (not really in dev mode).');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async function checkProxies() {
|
async function checkProxies() {
|
||||||
try {
|
try {
|
||||||
@@ -45,18 +45,35 @@ async function checkProxies() {
|
|||||||
let portReachable;
|
let portReachable;
|
||||||
|
|
||||||
const { arch, ipv4, ipv6 } = await listSettings();
|
const { arch, ipv4, ipv6 } = await listSettings();
|
||||||
|
|
||||||
// Coolify Proxy local
|
// Coolify Proxy local
|
||||||
const engine = '/var/run/docker.sock';
|
const engine = '/var/run/docker.sock';
|
||||||
const localDocker = await prisma.destinationDocker.findFirst({
|
const localDocker = await prisma.destinationDocker.findFirst({
|
||||||
where: { engine, network: 'coolify' }
|
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
|
||||||
});
|
});
|
||||||
if (localDocker && localDocker.isCoolifyProxyUsed) {
|
if (localDocker) {
|
||||||
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
|
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
|
||||||
if (!portReachable) {
|
if (!portReachable) {
|
||||||
await startTraefikProxy(localDocker.id);
|
await startTraefikProxy(localDocker.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Coolify Proxy remote
|
||||||
|
const remoteDocker = await prisma.destinationDocker.findMany({
|
||||||
|
where: { remoteEngine: true, remoteVerified: true }
|
||||||
|
});
|
||||||
|
if (remoteDocker.length > 0) {
|
||||||
|
for (const docker of remoteDocker) {
|
||||||
|
if (docker.isCoolifyProxyUsed) {
|
||||||
|
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
|
||||||
|
if (!portReachable) {
|
||||||
|
await startTraefikProxy(docker.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await createRemoteEngineConfiguration(docker.id)
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
// TCP Proxies
|
// TCP Proxies
|
||||||
const databasesWithPublicPort = await prisma.database.findMany({
|
const databasesWithPublicPort = await prisma.database.findMany({
|
||||||
where: { publicPort: { not: null } },
|
where: { publicPort: { not: null } },
|
||||||
@@ -113,9 +130,7 @@ async function cleanupPrismaEngines() {
|
|||||||
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
|
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
|
||||||
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
|
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function cleanupStorage() {
|
async function cleanupStorage() {
|
||||||
@@ -166,9 +181,7 @@ async function cleanupStorage() {
|
|||||||
lowDiskSpace = true;
|
lowDiskSpace = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
|
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
|
import { base64Encode, encrypt, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { day } from "../dayjs";
|
import { day } from "../dayjs";
|
||||||
|
|
||||||
@@ -89,6 +89,22 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
const phpVersions = [
|
const phpVersions = [
|
||||||
|
{
|
||||||
|
value: 'webdevops/php-apache:8.2',
|
||||||
|
label: 'webdevops/php-apache:8.2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'webdevops/php-nginx:8.2',
|
||||||
|
label: 'webdevops/php-nginx:8.2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'webdevops/php-apache:8.1',
|
||||||
|
label: 'webdevops/php-apache:8.1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'webdevops/php-nginx:8.1',
|
||||||
|
label: 'webdevops/php-nginx:8.1'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'webdevops/php-apache:8.0',
|
value: 'webdevops/php-apache:8.0',
|
||||||
label: 'webdevops/php-apache:8.0'
|
label: 'webdevops/php-apache:8.0'
|
||||||
@@ -145,6 +161,22 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
|
|||||||
value: 'webdevops/php-nginx:5.6',
|
value: 'webdevops/php-nginx:5.6',
|
||||||
label: 'webdevops/php-nginx:5.6'
|
label: 'webdevops/php-nginx:5.6'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'webdevops/php-apache:8.2-alpine',
|
||||||
|
label: 'webdevops/php-apache:8.2-alpine'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'webdevops/php-nginx:8.2-alpine',
|
||||||
|
label: 'webdevops/php-nginx:8.2-alpine'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'webdevops/php-apache:8.1-alpine',
|
||||||
|
label: 'webdevops/php-apache:8.1-alpine'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'webdevops/php-nginx:8.1-alpine',
|
||||||
|
label: 'webdevops/php-nginx:8.1-alpine'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'webdevops/php-apache:8.0-alpine',
|
value: 'webdevops/php-apache:8.0-alpine',
|
||||||
label: 'webdevops/php-apache:8.0-alpine'
|
label: 'webdevops/php-apache:8.0-alpine'
|
||||||
@@ -305,11 +337,11 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
|
|||||||
payload.baseImage = 'denoland/deno:latest';
|
payload.baseImage = 'denoland/deno:latest';
|
||||||
}
|
}
|
||||||
if (buildPack === 'php') {
|
if (buildPack === 'php') {
|
||||||
payload.baseImage = 'webdevops/php-apache:8.0-alpine';
|
payload.baseImage = 'webdevops/php-apache:8.2-alpine';
|
||||||
payload.baseImages = phpVersions;
|
payload.baseImages = phpVersions;
|
||||||
}
|
}
|
||||||
if (buildPack === 'laravel') {
|
if (buildPack === 'laravel') {
|
||||||
payload.baseImage = 'webdevops/php-apache:8.0-alpine';
|
payload.baseImage = 'webdevops/php-apache:8.2-alpine';
|
||||||
payload.baseBuildImage = 'node:18';
|
payload.baseBuildImage = 'node:18';
|
||||||
payload.baseBuildImages = nodeVersions;
|
payload.baseBuildImages = nodeVersions;
|
||||||
}
|
}
|
||||||
@@ -429,17 +461,32 @@ export const saveBuildLog = async ({
|
|||||||
buildId: string;
|
buildId: string;
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}): Promise<any> => {
|
}): Promise<any> => {
|
||||||
|
const { default: got } = await import('got')
|
||||||
|
|
||||||
if (line && typeof line === 'string' && line.includes('ghs_')) {
|
if (line && typeof line === 'string' && line.includes('ghs_')) {
|
||||||
const regex = /ghs_.*@/g;
|
const regex = /ghs_.*@/g;
|
||||||
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||||
}
|
}
|
||||||
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
||||||
if (isDev) console.debug(`[${applicationId}] ${addTimestamp}`);
|
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
|
||||||
return await prisma.buildLog.create({
|
|
||||||
data: {
|
if (isDev) {
|
||||||
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
|
console.debug(`[${applicationId}] ${addTimestamp}`);
|
||||||
}
|
}
|
||||||
});
|
try {
|
||||||
|
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
|
||||||
|
json: {
|
||||||
|
line: encrypt(line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch(error) {
|
||||||
|
return await prisma.buildLog.create({
|
||||||
|
data: {
|
||||||
|
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function copyBaseConfigurationFiles(
|
export async function copyBaseConfigurationFiles(
|
||||||
@@ -512,7 +559,6 @@ export async function copyBaseConfigurationFiles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,7 +571,6 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function buildImage({
|
export async function buildImage({
|
||||||
applicationId,
|
applicationId,
|
||||||
tag,
|
tag,
|
||||||
@@ -646,8 +691,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
|||||||
secrets,
|
secrets,
|
||||||
pullmergeRequestId
|
pullmergeRequestId
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
|
||||||
const isPnpm = checkPnpm(installCommand, buildCommand);
|
const isPnpm = checkPnpm(installCommand, buildCommand);
|
||||||
const Dockerfile: Array<string> = [];
|
const Dockerfile: Array<string> = [];
|
||||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||||
@@ -657,7 +700,10 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -691,7 +737,10 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default async function (data) {
|
|||||||
if (secrets.length > 0) {
|
if (secrets.length > 0) {
|
||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
|
// TODO: fix secrets
|
||||||
if (
|
if (
|
||||||
(pullmergeRequestId && secret.isPRMRSecret) ||
|
(pullmergeRequestId && secret.isPRMRSecret) ||
|
||||||
(!pullmergeRequestId && !secret.isPRMRSecret)
|
(!pullmergeRequestId && !secret.isPRMRSecret)
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ const createDockerfile = async (data, image): Promise<void> => {
|
|||||||
secrets.forEach((secret) => {
|
secrets.forEach((secret) => {
|
||||||
if (secret.isBuildSecret) {
|
if (secret.isBuildSecret) {
|
||||||
if (pullmergeRequestId) {
|
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}`);
|
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
|
|||||||
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
||||||
import { includeServices } from './services/common';
|
import { includeServices } from './services/common';
|
||||||
|
|
||||||
export const version = '3.8.6';
|
export const version = '3.10.4';
|
||||||
export const isDev = process.env.NODE_ENV === 'development';
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
const algorithm = 'aes-256-ctr';
|
const algorithm = 'aes-256-ctr';
|
||||||
@@ -45,7 +45,7 @@ export function getAPIUrl() {
|
|||||||
if (process.env.CODESANDBOX_HOST) {
|
if (process.env.CODESANDBOX_HOST) {
|
||||||
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`
|
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`
|
||||||
}
|
}
|
||||||
return isDev ? 'http://localhost:3001' : 'http://localhost:3000';
|
return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUIUrl() {
|
export function getUIUrl() {
|
||||||
@@ -75,13 +75,15 @@ export const asyncExecShellStream = async ({ debug, buildId, applicationId, comm
|
|||||||
return await new Promise(async (resolve, reject) => {
|
return await new Promise(async (resolve, reject) => {
|
||||||
const { execaCommand } = await import('execa')
|
const { execaCommand } = await import('execa')
|
||||||
const subprocess = execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine } })
|
const subprocess = execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine } })
|
||||||
if (debug) {
|
const errorLogs = []
|
||||||
|
const logs = []
|
||||||
subprocess.stdout.on('data', async (data) => {
|
subprocess.stdout.on('data', async (data) => {
|
||||||
const stdout = data.toString();
|
const stdout = data.toString();
|
||||||
const array = stdout.split('\n')
|
const array = stdout.split('\n')
|
||||||
for (const line of array) {
|
for (const line of array) {
|
||||||
if (line !== '\n' && line !== '') {
|
if (line !== '\n' && line !== '') {
|
||||||
await saveBuildLog({
|
logs.push(line.replace('\n', ''))
|
||||||
|
debug && await saveBuildLog({
|
||||||
line: `${line.replace('\n', '')}`,
|
line: `${line.replace('\n', '')}`,
|
||||||
buildId,
|
buildId,
|
||||||
applicationId
|
applicationId
|
||||||
@@ -94,6 +96,22 @@ export const asyncExecShellStream = async ({ debug, buildId, applicationId, comm
|
|||||||
const array = stderr.split('\n')
|
const array = stderr.split('\n')
|
||||||
for (const line of array) {
|
for (const line of array) {
|
||||||
if (line !== '\n' && line !== '') {
|
if (line !== '\n' && line !== '') {
|
||||||
|
errorLogs.push(line.replace('\n', ''))
|
||||||
|
debug && await saveBuildLog({
|
||||||
|
line: `${line.replace('\n', '')}`,
|
||||||
|
buildId,
|
||||||
|
applicationId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
subprocess.on('exit', async (code) => {
|
||||||
|
await asyncSleep(1000);
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(code)
|
||||||
|
} else {
|
||||||
|
if (!debug) {
|
||||||
|
for (const line of errorLogs) {
|
||||||
await saveBuildLog({
|
await saveBuildLog({
|
||||||
line: `${line.replace('\n', '')}`,
|
line: `${line.replace('\n', '')}`,
|
||||||
buildId,
|
buildId,
|
||||||
@@ -101,13 +119,6 @@ export const asyncExecShellStream = async ({ debug, buildId, applicationId, comm
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
subprocess.on('exit', async (code) => {
|
|
||||||
await asyncSleep(1000);
|
|
||||||
if (code === 0) {
|
|
||||||
resolve(code)
|
|
||||||
} else {
|
|
||||||
reject(code)
|
reject(code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -139,10 +150,10 @@ export const prisma = new PrismaClient({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// prisma.$on('query', (e) => {
|
// prisma.$on('query', (e) => {
|
||||||
// console.log({e})
|
// console.log({e})
|
||||||
// console.log('Query: ' + e.query)
|
// console.log('Query: ' + e.query)
|
||||||
// console.log('Params: ' + e.params)
|
// console.log('Params: ' + e.params)
|
||||||
// console.log('Duration: ' + e.duration + 'ms')
|
// console.log('Duration: ' + e.duration + 'ms')
|
||||||
// })
|
// })
|
||||||
export const base64Encode = (text: string): string => {
|
export const base64Encode = (text: string): string => {
|
||||||
return Buffer.from(text).toString('base64');
|
return Buffer.from(text).toString('base64');
|
||||||
@@ -193,7 +204,7 @@ export async function isDNSValid(hostname: any, domain: string): Promise<any> {
|
|||||||
const { isIP } = await import('is-ip');
|
const { isIP } = await import('is-ip');
|
||||||
const { DNSServers } = await listSettings();
|
const { DNSServers } = await listSettings();
|
||||||
if (DNSServers) {
|
if (DNSServers) {
|
||||||
dns.setServers([DNSServers]);
|
dns.setServers([...DNSServers.split(',')]);
|
||||||
}
|
}
|
||||||
let resolves = [];
|
let resolves = [];
|
||||||
try {
|
try {
|
||||||
@@ -302,7 +313,7 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P
|
|||||||
|
|
||||||
const { DNSServers } = await listSettings();
|
const { DNSServers } = await listSettings();
|
||||||
if (DNSServers) {
|
if (DNSServers) {
|
||||||
dns.setServers([DNSServers]);
|
dns.setServers([...DNSServers.split(',')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolves = [];
|
let resolves = [];
|
||||||
@@ -414,6 +425,12 @@ export const supportedDatabaseTypesAndVersions = [
|
|||||||
baseImageARM: 'couchdb',
|
baseImageARM: 'couchdb',
|
||||||
versions: ['3.2.2', '3.1.2', '2.3.1'],
|
versions: ['3.2.2', '3.1.2', '2.3.1'],
|
||||||
versionsARM: ['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 +456,6 @@ export async function getFreeSSHLocalPort(id: string): Promise<number | boolean>
|
|||||||
return Number(alreadyConfigured.sshLocalPort)
|
return Number(alreadyConfigured.sshLocalPort)
|
||||||
}
|
}
|
||||||
const range = generateRangeArray(minPort, maxPort)
|
const range = generateRangeArray(minPort, maxPort)
|
||||||
console.log({ ports })
|
|
||||||
const availablePorts = range.filter(port => !ports.map(p => p.sshLocalPort).includes(port))
|
const availablePorts = range.filter(port => !ports.map(p => p.sshLocalPort).includes(port))
|
||||||
for (const port of availablePorts) {
|
for (const port of availablePorts) {
|
||||||
const found = await isReachable(port, { host: 'localhost' })
|
const found = await isReachable(port, { host: 'localhost' })
|
||||||
@@ -458,20 +474,21 @@ export async function createRemoteEngineConfiguration(id: string) {
|
|||||||
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
|
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
|
||||||
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
|
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
|
||||||
// Needed for remote docker compose
|
// Needed for remote docker compose
|
||||||
const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(`ps ax | grep [s]sh-agent | grep ssh-agent.pid | grep -v grep | wc -l`)
|
const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(`ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`)
|
||||||
if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
|
if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
|
||||||
await asyncExecShell(`eval $(ssh-agent -sa /tmp/ssh-agent.pid)`)
|
try {
|
||||||
|
await fs.stat(`/tmp/coolify-ssh-agent.pid`)
|
||||||
|
await fs.rm(`/tmp/coolify-ssh-agent.pid`)
|
||||||
|
} catch (error) { }
|
||||||
|
await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`)
|
||||||
}
|
}
|
||||||
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh-add -q ${sshKeyFile}`)
|
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`)
|
||||||
|
|
||||||
const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`)
|
const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`)
|
||||||
if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
|
if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
|
||||||
try {
|
try {
|
||||||
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`)
|
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`)
|
||||||
|
} catch (error) { }
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
const config = sshConfig.parse('')
|
const config = sshConfig.parse('')
|
||||||
@@ -493,8 +510,26 @@ export async function createRemoteEngineConfiguration(id: string) {
|
|||||||
}
|
}
|
||||||
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config))
|
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> {
|
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) {
|
if (remoteEngine) {
|
||||||
await createRemoteEngineConfiguration(dockerId)
|
await createRemoteEngineConfiguration(dockerId)
|
||||||
engine = `ssh://${remoteIpAddress}`
|
engine = `ssh://${remoteIpAddress}`
|
||||||
@@ -509,14 +544,11 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId
|
|||||||
if (command.startsWith(`docker build --progress plain`)) {
|
if (command.startsWith(`docker build --progress plain`)) {
|
||||||
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
|
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
|
||||||
}
|
}
|
||||||
return await asyncExecShell(
|
return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })
|
||||||
`DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}`
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
export async function startTraefikProxy(id: string): Promise<void> {
|
export async function startTraefikProxy(id: string): Promise<void> {
|
||||||
const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } })
|
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();
|
const { id: settingsId, ipv4, ipv6 } = await listSettings();
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
@@ -600,7 +632,7 @@ export async function configureNetworkTraefikProxy(destination: any): Promise<vo
|
|||||||
export async function stopTraefikProxy(
|
export async function stopTraefikProxy(
|
||||||
id: string
|
id: string
|
||||||
): Promise<{ stdout: string; stderr: string } | Error> {
|
): 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({
|
await prisma.destinationDocker.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { isCoolifyProxyUsed: false }
|
data: { isCoolifyProxyUsed: false }
|
||||||
@@ -642,21 +674,20 @@ export function generatePassword({ length = 24, symbols = false, isHex = false }
|
|||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateDatabaseConfiguration(database: any, arch: string):
|
type DatabaseConfiguration = {
|
||||||
| {
|
volume: string;
|
||||||
volume: string;
|
image: string;
|
||||||
image: string;
|
command?: string;
|
||||||
command?: string;
|
ulimits: Record<string, unknown>;
|
||||||
ulimits: Record<string, unknown>;
|
privatePort: number;
|
||||||
privatePort: number;
|
environmentVariables: {
|
||||||
environmentVariables: {
|
MYSQL_DATABASE: string;
|
||||||
MYSQL_DATABASE: string;
|
MYSQL_PASSWORD: string;
|
||||||
MYSQL_PASSWORD: string;
|
MYSQL_ROOT_USER: string;
|
||||||
MYSQL_ROOT_USER: string;
|
MYSQL_USER: string;
|
||||||
MYSQL_USER: string;
|
MYSQL_ROOT_PASSWORD: string;
|
||||||
MYSQL_ROOT_PASSWORD: string;
|
};
|
||||||
};
|
}
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
volume: string;
|
volume: string;
|
||||||
image: string;
|
image: string;
|
||||||
@@ -691,22 +722,13 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
ulimits: Record<string, unknown>;
|
ulimits: Record<string, unknown>;
|
||||||
privatePort: number;
|
privatePort: number;
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
POSTGRESQL_POSTGRES_PASSWORD: string;
|
POSTGRES_PASSWORD?: string;
|
||||||
POSTGRESQL_USERNAME: string;
|
POSTGRES_USER?: string;
|
||||||
POSTGRESQL_PASSWORD: string;
|
POSTGRES_DB?: string;
|
||||||
POSTGRESQL_DATABASE: string;
|
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;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -730,7 +752,21 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
COUCHDB_PASSWORD: string;
|
COUCHDB_PASSWORD: string;
|
||||||
COUCHDB_USER: 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 {
|
const {
|
||||||
id,
|
id,
|
||||||
dbUser,
|
dbUser,
|
||||||
@@ -740,7 +776,6 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
defaultDatabase,
|
defaultDatabase,
|
||||||
version,
|
version,
|
||||||
type,
|
type,
|
||||||
settings: { appendOnly }
|
|
||||||
} = database;
|
} = database;
|
||||||
const baseImage = getDatabaseImage(type, arch);
|
const baseImage = getDatabaseImage(type, arch);
|
||||||
if (type === 'mysql') {
|
if (type === 'mysql') {
|
||||||
@@ -762,7 +797,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
} else if (type === 'mariadb') {
|
} else if (type === 'mariadb') {
|
||||||
const configuration = {
|
const configuration: DatabaseConfiguration = {
|
||||||
privatePort: 3306,
|
privatePort: 3306,
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
MARIADB_ROOT_USER: rootUser,
|
MARIADB_ROOT_USER: rootUser,
|
||||||
@@ -780,7 +815,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
} else if (type === 'mongodb') {
|
} else if (type === 'mongodb') {
|
||||||
const configuration = {
|
const configuration: DatabaseConfiguration = {
|
||||||
privatePort: 27017,
|
privatePort: 27017,
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
MONGODB_ROOT_USER: rootUser,
|
MONGODB_ROOT_USER: rootUser,
|
||||||
@@ -799,7 +834,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
} else if (type === 'postgresql') {
|
} else if (type === 'postgresql') {
|
||||||
const configuration = {
|
const configuration: DatabaseConfiguration = {
|
||||||
privatePort: 5432,
|
privatePort: 5432,
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
|
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
|
||||||
@@ -821,7 +856,8 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
} else if (type === 'redis') {
|
} else if (type === 'redis') {
|
||||||
const configuration = {
|
const { settings: { appendOnly } } = database;
|
||||||
|
const configuration: DatabaseConfiguration = {
|
||||||
privatePort: 6379,
|
privatePort: 6379,
|
||||||
command: undefined,
|
command: undefined,
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
@@ -838,7 +874,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
}
|
}
|
||||||
return configuration
|
return configuration
|
||||||
} else if (type === 'couchdb') {
|
} else if (type === 'couchdb') {
|
||||||
const configuration = {
|
const configuration: DatabaseConfiguration = {
|
||||||
privatePort: 5984,
|
privatePort: 5984,
|
||||||
environmentVariables: {
|
environmentVariables: {
|
||||||
COUCHDB_PASSWORD: dbUserPassword,
|
COUCHDB_PASSWORD: dbUserPassword,
|
||||||
@@ -852,6 +888,20 @@ export function generateDatabaseConfiguration(database: any, arch: string):
|
|||||||
configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
|
configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
|
||||||
}
|
}
|
||||||
return configuration
|
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) {
|
export function isARM(arch: string) {
|
||||||
@@ -974,9 +1024,14 @@ export const createDirectories = async ({
|
|||||||
}): Promise<{ workdir: string; repodir: string }> => {
|
}): Promise<{ workdir: string; repodir: string }> => {
|
||||||
const repodir = `/tmp/build-sources/${repository}/`;
|
const repodir = `/tmp/build-sources/${repository}/`;
|
||||||
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
|
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
|
||||||
|
let workdirFound = false;
|
||||||
|
try {
|
||||||
|
workdirFound = !!(await fs.stat(workdir));
|
||||||
|
} catch (error) { }
|
||||||
|
if (workdirFound) {
|
||||||
|
await asyncExecShell(`rm -fr ${workdir}`);
|
||||||
|
}
|
||||||
await asyncExecShell(`mkdir -p ${workdir}`);
|
await asyncExecShell(`mkdir -p ${workdir}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workdir,
|
workdir,
|
||||||
repodir
|
repodir
|
||||||
@@ -1018,7 +1073,7 @@ export async function stopTcpHttpProxy(
|
|||||||
const { id: dockerId } = destinationDocker;
|
const { id: dockerId } = destinationDocker;
|
||||||
let container = `${id}-${publicPort}`;
|
let container = `${id}-${publicPort}`;
|
||||||
if (forceName) container = forceName;
|
if (forceName) container = forceName;
|
||||||
const found = await checkContainer({ dockerId, container });
|
const { found } = await checkContainer({ dockerId, container });
|
||||||
try {
|
try {
|
||||||
if (found) {
|
if (found) {
|
||||||
return await executeDockerCmd({
|
return await executeDockerCmd({
|
||||||
@@ -1085,91 +1140,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) {
|
if (exposePort < 1024 || exposePort > 65535) {
|
||||||
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
|
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configuredPort) {
|
if (configuredPort) {
|
||||||
if (configuredPort !== exposePort) {
|
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()) {
|
if (availablePort.toString() !== exposePort.toString()) {
|
||||||
throw { status: 500, message: `Port ${exposePort} is already in use.` }
|
throw { status: 500, message: `Port ${exposePort} is already in use.` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
|
const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress);
|
||||||
if (availablePort.toString() !== exposePort.toString()) {
|
if (availablePort.toString() !== exposePort.toString()) {
|
||||||
throw { status: 500, message: `Port ${exposePort} is already in use.` }
|
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 { default: checkPort } = await import('is-port-reachable');
|
||||||
const applicationUsed = await (
|
if (remoteEngine) {
|
||||||
await prisma.application.findMany({
|
const applicationUsed = await (
|
||||||
where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
|
await prisma.application.findMany({
|
||||||
select: { exposePort: true }
|
where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
|
||||||
})
|
select: { exposePort: true }
|
||||||
).map((a) => a.exposePort);
|
})
|
||||||
const serviceUsed = await (
|
).map((a) => a.exposePort);
|
||||||
await prisma.service.findMany({
|
const serviceUsed = await (
|
||||||
where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
|
await prisma.service.findMany({
|
||||||
select: { exposePort: true }
|
where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
|
||||||
})
|
select: { exposePort: true }
|
||||||
).map((a) => a.exposePort);
|
})
|
||||||
const usedPorts = [...applicationUsed, ...serviceUsed];
|
).map((a) => a.exposePort);
|
||||||
if (usedPorts.includes(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
|
return false
|
||||||
}
|
}
|
||||||
const found = await checkPort(exposePort, { host: remoteIpAddress || 'localhost' });
|
|
||||||
if (!found) {
|
|
||||||
return exposePort
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
|
|
||||||
}
|
}
|
||||||
export function generateRangeArray(start, end) {
|
export function generateRangeArray(start, end) {
|
||||||
return Array.from({ length: (end - start) }, (v, k) => k + start);
|
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 { default: isReachable } = await import('is-port-reachable');
|
||||||
const data = await prisma.setting.findFirst();
|
const data = await prisma.setting.findFirst();
|
||||||
const { minPort, maxPort } = data;
|
const { minPort, maxPort } = data;
|
||||||
const dbUsed = await (
|
if (remoteEngine) {
|
||||||
await prisma.database.findMany({
|
const dbUsed = await (
|
||||||
where: { publicPort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
|
await prisma.database.findMany({
|
||||||
select: { publicPort: true }
|
where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
|
||||||
})
|
select: { publicPort: true }
|
||||||
).map((a) => a.publicPort);
|
})
|
||||||
const wpFtpUsed = await (
|
).map((a) => a.publicPort);
|
||||||
await prisma.wordpress.findMany({
|
const wpFtpUsed = await (
|
||||||
where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
|
await prisma.wordpress.findMany({
|
||||||
select: { ftpPublicPort: true }
|
where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
|
||||||
})
|
select: { ftpPublicPort: true }
|
||||||
).map((a) => a.ftpPublicPort);
|
})
|
||||||
const wpUsed = await (
|
).map((a) => a.ftpPublicPort);
|
||||||
await prisma.wordpress.findMany({
|
const wpUsed = await (
|
||||||
where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
|
await prisma.wordpress.findMany({
|
||||||
select: { mysqlPublicPort: true }
|
where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
|
||||||
})
|
select: { mysqlPublicPort: true }
|
||||||
).map((a) => a.mysqlPublicPort);
|
})
|
||||||
const minioUsed = await (
|
).map((a) => a.mysqlPublicPort);
|
||||||
await prisma.minio.findMany({
|
const minioUsed = await (
|
||||||
where: { publicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
|
await prisma.minio.findMany({
|
||||||
select: { publicPort: true }
|
where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
|
||||||
})
|
select: { publicPort: true }
|
||||||
).map((a) => a.publicPort);
|
})
|
||||||
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
|
).map((a) => a.publicPort);
|
||||||
const range = generateRangeArray(minPort, maxPort)
|
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
|
||||||
const availablePorts = range.filter(port => !usedPorts.includes(port))
|
const range = generateRangeArray(minPort, maxPort)
|
||||||
for (const port of availablePorts) {
|
const availablePorts = range.filter(port => !usedPorts.includes(port))
|
||||||
const found = await isReachable(port, { host: 'localhost' })
|
for (const port of availablePorts) {
|
||||||
if (!found) {
|
const found = await isReachable(port, { host: remoteIpAddress })
|
||||||
return port
|
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(
|
export async function startTraefikTCPProxy(
|
||||||
@@ -1181,7 +1295,7 @@ export async function startTraefikTCPProxy(
|
|||||||
): Promise<{ stdout: string; stderr: string } | Error> {
|
): Promise<{ stdout: string; stderr: string } | Error> {
|
||||||
const { network, id: dockerId, remoteEngine } = destinationDocker;
|
const { network, id: dockerId, remoteEngine } = destinationDocker;
|
||||||
const container = `${id}-${publicPort}`;
|
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();
|
const { ipv4, ipv6 } = await listSettings();
|
||||||
|
|
||||||
let dependentId = id;
|
let dependentId = id;
|
||||||
@@ -1249,7 +1363,6 @@ export async function startTraefikTCPProxy(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1309,6 +1422,9 @@ export function saveUpdateableFields(type: string, data: any) {
|
|||||||
temp = Boolean(temp)
|
temp = Boolean(temp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (k.isNumber && temp === '') {
|
||||||
|
temp = null
|
||||||
|
}
|
||||||
update[k.name] = temp
|
update[k.name] = temp
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1351,9 +1467,9 @@ export const getServiceMainPort = (service: string) => {
|
|||||||
export function makeLabelForServices(type) {
|
export function makeLabelForServices(type) {
|
||||||
return [
|
return [
|
||||||
'coolify.managed=true',
|
'coolify.managed=true',
|
||||||
`coolify.version = ${version}`,
|
`coolify.version=${version}`,
|
||||||
`coolify.type = service`,
|
`coolify.type=service`,
|
||||||
`coolify.service.type = ${type}`
|
`coolify.service.type=${type}`
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) {
|
export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) {
|
||||||
@@ -1434,15 +1550,13 @@ export function convertTolOldVolumeNames(type) {
|
|||||||
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
|
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
|
||||||
// Cleanup old coolify images
|
// Cleanup old coolify images
|
||||||
try {
|
try {
|
||||||
let { stdout: images } = await executeDockerCmd({ dockerId, command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs` })
|
let { stdout: images } = await executeDockerCmd({ dockerId, command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r` })
|
||||||
|
|
||||||
images = images.trim();
|
images = images.trim();
|
||||||
if (images) {
|
if (images) {
|
||||||
await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs` })
|
await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r` })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
//console.log(error);
|
|
||||||
}
|
|
||||||
if (lowDiskSpace || force) {
|
if (lowDiskSpace || force) {
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`);
|
if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`);
|
||||||
@@ -1450,37 +1564,40 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await executeDockerCmd({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` })
|
await executeDockerCmd({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` })
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
//console.log(error);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await executeDockerCmd({ dockerId, command: `docker image prune -f` })
|
await executeDockerCmd({ dockerId, command: `docker image prune -f` })
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
//console.log(error);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await executeDockerCmd({ dockerId, command: `docker image prune -a -f` })
|
await executeDockerCmd({ dockerId, command: `docker image prune -a -f` })
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
//console.log(error);
|
|
||||||
}
|
|
||||||
// Cleanup build caches
|
// Cleanup build caches
|
||||||
try {
|
try {
|
||||||
await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` })
|
await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` })
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
//console.log(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persistentVolumes(id, persistentStorage, config) {
|
export function persistentVolumes(id, persistentStorage, config) {
|
||||||
|
let volumeSet = new Set();
|
||||||
|
if (Object.keys(config).length > 0) {
|
||||||
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
if (value.volumes) {
|
||||||
|
for (const volume of value.volumes) {
|
||||||
|
volumeSet.add(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const volumesArray = Array.from(volumeSet);
|
||||||
const persistentVolume =
|
const persistentVolume =
|
||||||
persistentStorage?.map((storage) => {
|
persistentStorage?.map((storage) => {
|
||||||
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
let volumes = [...persistentVolume]
|
let volumes = [...persistentVolume]
|
||||||
if (config.volume) volumes = [config.volume, ...volumes]
|
if (volumesArray) volumes = [...volumesArray, ...volumes]
|
||||||
|
|
||||||
const composeVolumes = volumes.length > 0 && volumes.map((volume) => {
|
const composeVolumes = volumes.length > 0 && volumes.map((volume) => {
|
||||||
return {
|
return {
|
||||||
[`${volume.split(':')[0]}`]: {
|
[`${volume.split(':')[0]}`]: {
|
||||||
@@ -1489,16 +1606,11 @@ export function persistentVolumes(id, persistentStorage, config) {
|
|||||||
};
|
};
|
||||||
}) || []
|
}) || []
|
||||||
|
|
||||||
const volumeMounts = config.volume && Object.assign(
|
const volumeMounts = Object.assign(
|
||||||
{},
|
{},
|
||||||
{
|
|
||||||
[config.volume.split(':')[0]]: {
|
|
||||||
name: config.volume.split(':')[0]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...composeVolumes
|
...composeVolumes
|
||||||
) || {}
|
) || {}
|
||||||
return { volumes, volumeMounts }
|
return { volumeMounts }
|
||||||
}
|
}
|
||||||
export function defaultComposeConfiguration(network: string): any {
|
export function defaultComposeConfiguration(network: string): any {
|
||||||
return {
|
return {
|
||||||
@@ -1515,26 +1627,26 @@ export function defaultComposeConfiguration(network: string): any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function decryptApplication(application: any) {
|
export function decryptApplication(application: any) {
|
||||||
if (application) {
|
if (application) {
|
||||||
if (application?.gitSource?.githubApp?.clientSecret) {
|
if (application?.gitSource?.githubApp?.clientSecret) {
|
||||||
application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null;
|
application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null;
|
||||||
}
|
}
|
||||||
if (application?.gitSource?.githubApp?.webhookSecret) {
|
if (application?.gitSource?.githubApp?.webhookSecret) {
|
||||||
application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null;
|
application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null;
|
||||||
}
|
}
|
||||||
if (application?.gitSource?.githubApp?.privateKey) {
|
if (application?.gitSource?.githubApp?.privateKey) {
|
||||||
application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null;
|
application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null;
|
||||||
}
|
}
|
||||||
if (application?.gitSource?.gitlabApp?.appSecret) {
|
if (application?.gitSource?.gitlabApp?.appSecret) {
|
||||||
application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null;
|
application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null;
|
||||||
}
|
}
|
||||||
if (application?.secrets.length > 0) {
|
if (application?.secrets.length > 0) {
|
||||||
application.secrets = application.secrets.map((s: any) => {
|
application.secrets = application.secrets.map((s: any) => {
|
||||||
s.value = decrypt(s.value) || null
|
s.value = decrypt(s.value) || null
|
||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return application;
|
return application;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function formatLabelsOnDocker(data) {
|
|||||||
return container
|
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;
|
let containerFound = false;
|
||||||
try {
|
try {
|
||||||
const { stdout } = await executeDockerCmd({
|
const { stdout } = await executeDockerCmd({
|
||||||
@@ -21,10 +21,12 @@ export async function checkContainer({ dockerId, container, remove = false }: {
|
|||||||
command:
|
command:
|
||||||
`docker inspect --format '{{json .State}}' ${container}`
|
`docker inspect --format '{{json .State}}' ${container}`
|
||||||
});
|
});
|
||||||
|
containerFound = true
|
||||||
const parsedStdout = JSON.parse(stdout);
|
const parsedStdout = JSON.parse(stdout);
|
||||||
const status = parsedStdout.Status;
|
const status = parsedStdout.Status;
|
||||||
const isRunning = status === 'running';
|
const isRunning = status === 'running';
|
||||||
|
const isRestarting = status === 'restarting'
|
||||||
|
const isExited = status === 'exited'
|
||||||
if (status === 'created') {
|
if (status === 'created') {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId,
|
dockerId,
|
||||||
@@ -39,13 +41,23 @@ export async function checkContainer({ dockerId, container, remove = false }: {
|
|||||||
`docker rm ${container}`
|
`docker rm ${container}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isRunning) {
|
|
||||||
containerFound = true;
|
return {
|
||||||
}
|
found: containerFound,
|
||||||
|
status: {
|
||||||
|
isRunning,
|
||||||
|
isRestarting,
|
||||||
|
isExited
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Container not found
|
// Container not found
|
||||||
}
|
}
|
||||||
return containerFound;
|
return {
|
||||||
|
found: false
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {
|
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}` })
|
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export default async function ({
|
|||||||
branch,
|
branch,
|
||||||
buildId,
|
buildId,
|
||||||
privateSshKey,
|
privateSshKey,
|
||||||
customPort
|
customPort,
|
||||||
|
forPublic
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
workdir: string;
|
workdir: string;
|
||||||
@@ -21,11 +22,15 @@ export default async function ({
|
|||||||
repodir: string;
|
repodir: string;
|
||||||
privateSshKey: string;
|
privateSshKey: string;
|
||||||
customPort: number;
|
customPort: number;
|
||||||
|
forPublic: boolean;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
|
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
|
||||||
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
|
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({
|
await saveBuildLog({
|
||||||
line: `Cloning ${repository}:${branch} branch.`,
|
line: `Cloning ${repository}:${branch} branch.`,
|
||||||
@@ -33,9 +38,16 @@ export default async function ({
|
|||||||
applicationId
|
applicationId
|
||||||
});
|
});
|
||||||
|
|
||||||
await asyncExecShell(
|
if (forPublic) {
|
||||||
`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 .. `
|
await asyncExecShell(
|
||||||
);
|
`git clone -q -b ${branch} https://${url}/${repository}.git ${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`);
|
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
|
||||||
return commit.replace('\n', '');
|
return commit.replace('\n', '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,7 @@
|
|||||||
import { exec } from 'node:child_process'
|
|
||||||
import util from 'util';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import yaml from 'js-yaml';
|
|
||||||
import forge from 'node-forge';
|
|
||||||
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
|
||||||
import type { Config } from 'unique-names-generator';
|
|
||||||
import generator from 'generate-password';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import { promises as dns } from 'dns';
|
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
import os from 'os';
|
|
||||||
import sshConfig from 'ssh-config'
|
|
||||||
import { encrypt, generatePassword, prisma } from '../common';
|
import { encrypt, generatePassword, prisma } from '../common';
|
||||||
|
|
||||||
|
|
||||||
export const version = '3.8.2';
|
|
||||||
export const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
export const includeServices: any = {
|
export const includeServices: any = {
|
||||||
destinationDocker: true,
|
destinationDocker: true,
|
||||||
persistentStorage: true,
|
persistentStorage: true,
|
||||||
@@ -34,7 +18,9 @@ export const includeServices: any = {
|
|||||||
moodle: true,
|
moodle: true,
|
||||||
appwrite: true,
|
appwrite: true,
|
||||||
glitchTip: true,
|
glitchTip: true,
|
||||||
searxng: true
|
searxng: true,
|
||||||
|
weblate: true,
|
||||||
|
taiga: true
|
||||||
};
|
};
|
||||||
export async function configureServiceType({
|
export async function configureServiceType({
|
||||||
id,
|
id,
|
||||||
@@ -312,6 +298,58 @@ export async function configureServiceType({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (type === 'weblate') {
|
||||||
|
const adminPassword = encrypt(generatePassword({}))
|
||||||
|
const postgresqlUser = cuid();
|
||||||
|
const postgresqlPassword = encrypt(generatePassword({}));
|
||||||
|
const postgresqlDatabase = 'weblate';
|
||||||
|
await prisma.service.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
type,
|
||||||
|
weblate: {
|
||||||
|
create: {
|
||||||
|
adminPassword,
|
||||||
|
postgresqlHost: `${id}-postgresql`,
|
||||||
|
postgresqlPort: 5432,
|
||||||
|
postgresqlUser,
|
||||||
|
postgresqlPassword,
|
||||||
|
postgresqlDatabase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (type === 'taiga') {
|
||||||
|
const secretKey = encrypt(generatePassword({}))
|
||||||
|
const erlangSecret = encrypt(generatePassword({}))
|
||||||
|
const rabbitMQUser = cuid();
|
||||||
|
const djangoAdminUser = cuid();
|
||||||
|
const djangoAdminPassword = encrypt(generatePassword({}))
|
||||||
|
const rabbitMQPassword = encrypt(generatePassword({}))
|
||||||
|
const postgresqlUser = cuid();
|
||||||
|
const postgresqlPassword = encrypt(generatePassword({}));
|
||||||
|
const postgresqlDatabase = 'taiga';
|
||||||
|
await prisma.service.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
type,
|
||||||
|
taiga: {
|
||||||
|
create: {
|
||||||
|
secretKey,
|
||||||
|
erlangSecret,
|
||||||
|
djangoAdminUser,
|
||||||
|
djangoAdminPassword,
|
||||||
|
rabbitMQUser,
|
||||||
|
rabbitMQPassword,
|
||||||
|
postgresqlHost: `${id}-postgresql`,
|
||||||
|
postgresqlPort: 5432,
|
||||||
|
postgresqlUser,
|
||||||
|
postgresqlPassword,
|
||||||
|
postgresqlDatabase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await prisma.service.update({
|
await prisma.service.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -338,7 +376,8 @@ export async function removeService({ id }: { id: string }): Promise<void> {
|
|||||||
await prisma.moodle.deleteMany({ where: { serviceId: id } });
|
await prisma.moodle.deleteMany({ where: { serviceId: id } });
|
||||||
await prisma.appwrite.deleteMany({ where: { serviceId: id } });
|
await prisma.appwrite.deleteMany({ where: { serviceId: id } });
|
||||||
await prisma.searxng.deleteMany({ where: { serviceId: id } });
|
await prisma.searxng.deleteMany({ where: { serviceId: id } });
|
||||||
|
await prisma.weblate.deleteMany({ where: { serviceId: id } });
|
||||||
|
await prisma.taiga.deleteMany({ where: { serviceId: id } });
|
||||||
|
|
||||||
await prisma.service.delete({ where: { id } });
|
await prisma.service.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -599,6 +599,54 @@ export const glitchTip = [{
|
|||||||
isBoolean: false,
|
isBoolean: false,
|
||||||
isEncrypted: true
|
isEncrypted: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'emailSmtpHost',
|
||||||
|
isEditable: true,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emailSmtpPassword',
|
||||||
|
isEditable: true,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emailSmtpUseSsl',
|
||||||
|
isEditable: true,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: true,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emailSmtpUseSsl',
|
||||||
|
isEditable: true,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: true,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emailSmtpPort',
|
||||||
|
isEditable: true,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: true,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emailSmtpUser',
|
||||||
|
isEditable: true,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'defaultEmail',
|
name: 'defaultEmail',
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
@@ -624,7 +672,7 @@ export const glitchTip = [{
|
|||||||
isEncrypted: true
|
isEncrypted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'defaultFromEmail',
|
name: 'defaultEmailFrom',
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
isLowerCase: false,
|
isLowerCase: false,
|
||||||
isNumber: false,
|
isNumber: false,
|
||||||
@@ -687,4 +735,133 @@ export const searxng = [{
|
|||||||
isNumber: false,
|
isNumber: false,
|
||||||
isBoolean: false,
|
isBoolean: false,
|
||||||
isEncrypted: true
|
isEncrypted: true
|
||||||
|
}]
|
||||||
|
|
||||||
|
export const weblate = [{
|
||||||
|
name: 'adminPassword',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlHost',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlPort',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlUser',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlPassword',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlDatabase',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
}]
|
||||||
|
export const taiga = [{
|
||||||
|
name: 'secretKey',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'djangoAdminUser',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'djangoAdminPassword',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rabbitMQUser',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rabbitMQPassword',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlHost',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlPort',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlUser',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlPassword',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'postgresqlDatabase',
|
||||||
|
isEditable: false,
|
||||||
|
isLowerCase: false,
|
||||||
|
isNumber: false,
|
||||||
|
isBoolean: false,
|
||||||
|
isEncrypted: false
|
||||||
}]
|
}]
|
||||||
@@ -1,3 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Example of a supported version:
|
||||||
|
{
|
||||||
|
// Name used to identify the service internally
|
||||||
|
name: 'umami',
|
||||||
|
// Fancier name to show to the user
|
||||||
|
fancyName: 'Umami',
|
||||||
|
// Docker base image for the service
|
||||||
|
baseImage: 'ghcr.io/mikecao/umami',
|
||||||
|
// Optional: If there is any dependent image, you should list it here
|
||||||
|
images: [],
|
||||||
|
// Usable tags
|
||||||
|
versions: ['postgresql-latest'],
|
||||||
|
// Which tag is the recommended
|
||||||
|
recommendedVersion: 'postgresql-latest',
|
||||||
|
// Application's default port, Umami listens on 3000
|
||||||
|
ports: {
|
||||||
|
main: 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
export const supportedServiceTypesAndVersions = [
|
export const supportedServiceTypesAndVersions = [
|
||||||
{
|
{
|
||||||
name: 'plausibleanalytics',
|
name: 'plausibleanalytics',
|
||||||
@@ -116,7 +137,7 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
{
|
{
|
||||||
name: 'umami',
|
name: 'umami',
|
||||||
fancyName: 'Umami',
|
fancyName: 'Umami',
|
||||||
baseImage: 'ghcr.io/mikecao/umami',
|
baseImage: 'ghcr.io/umami-software/umami',
|
||||||
images: ['postgres:12-alpine'],
|
images: ['postgres:12-alpine'],
|
||||||
versions: ['postgresql-latest'],
|
versions: ['postgresql-latest'],
|
||||||
recommendedVersion: 'postgresql-latest',
|
recommendedVersion: 'postgresql-latest',
|
||||||
@@ -151,7 +172,7 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
fancyName: 'Appwrite',
|
fancyName: 'Appwrite',
|
||||||
baseImage: 'appwrite/appwrite',
|
baseImage: 'appwrite/appwrite',
|
||||||
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
|
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
|
||||||
versions: ['latest', '0.15.3'],
|
versions: ['latest', '1.0','0.15.3'],
|
||||||
recommendedVersion: '0.15.3',
|
recommendedVersion: '0.15.3',
|
||||||
ports: {
|
ports: {
|
||||||
main: 80
|
main: 80
|
||||||
@@ -190,4 +211,26 @@ export const supportedServiceTypesAndVersions = [
|
|||||||
main: 8080
|
main: 8080
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'weblate',
|
||||||
|
fancyName: 'Weblate',
|
||||||
|
baseImage: 'weblate/weblate',
|
||||||
|
images: ['postgres:14-alpine', 'redis:6-alpine'],
|
||||||
|
versions: ['latest'],
|
||||||
|
recommendedVersion: 'latest',
|
||||||
|
ports: {
|
||||||
|
main: 8080
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: 'taiga',
|
||||||
|
// fancyName: 'Taiga',
|
||||||
|
// baseImage: 'taigaio/taiga-front',
|
||||||
|
// images: ['postgres:12.3', 'rabbitmq:3.8-management-alpine', 'taigaio/taiga-back', 'taigaio/taiga-events', 'taigaio/taiga-protected'],
|
||||||
|
// versions: ['latest'],
|
||||||
|
// recommendedVersion: 'latest',
|
||||||
|
// ports: {
|
||||||
|
// main: 80
|
||||||
|
// }
|
||||||
|
// },
|
||||||
];
|
];
|
||||||
@@ -21,7 +21,6 @@ export default fp<FastifyJWTOptions>(async (fastify, opts) => {
|
|||||||
try {
|
try {
|
||||||
await request.jwtVerify()
|
await request.jwtVerify()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
|
||||||
reply.send(err)
|
reply.send(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,15 +3,24 @@ import crypto from 'node:crypto'
|
|||||||
import jsonwebtoken from 'jsonwebtoken';
|
import jsonwebtoken from 'jsonwebtoken';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { FastifyReply } from 'fastify';
|
import { FastifyReply } from 'fastify';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import csv from 'csvtojson';
|
||||||
|
|
||||||
import { day } from '../../../../lib/dayjs';
|
import { day } from '../../../../lib/dayjs';
|
||||||
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
||||||
import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
|
import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
|
||||||
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
||||||
import { scheduler } from '../../../../lib/scheduler';
|
|
||||||
|
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types';
|
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
|
||||||
import { OnlyId } from '../../../../types';
|
import { OnlyId } from '../../../../types';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
function filterObject(obj, callback) {
|
||||||
|
return Object.fromEntries(Object.entries(obj).
|
||||||
|
filter(([key, val]) => callback(val, key)));
|
||||||
|
}
|
||||||
|
|
||||||
export async function listApplications(request: FastifyRequest) {
|
export async function listApplications(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -67,14 +76,19 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
const { teamId } = request.user
|
const { teamId } = request.user
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isExited = false;
|
let isExited = false;
|
||||||
|
let isRestarting = false;
|
||||||
const application: any = await getApplicationFromDB(id, teamId);
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
if (application?.destinationDockerId) {
|
if (application?.destinationDockerId) {
|
||||||
isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
|
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
|
||||||
isExited = await isContainerExited(application.destinationDocker.id, id);
|
if (status?.found) {
|
||||||
|
isRunning = status.status.isRunning;
|
||||||
|
isExited = status.status.isExited;
|
||||||
|
isRestarting = status.status.isRestarting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isRunning,
|
isRunning,
|
||||||
|
isRestarting,
|
||||||
isExited,
|
isExited,
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -149,7 +163,9 @@ export async function getApplicationFromDB(id: string, teamId: string) {
|
|||||||
settings: true,
|
settings: true,
|
||||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||||
secrets: true,
|
secrets: true,
|
||||||
persistentStorage: true
|
persistentStorage: true,
|
||||||
|
connectedDatabase: true,
|
||||||
|
previewApplication: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!application) {
|
if (!application) {
|
||||||
@@ -176,32 +192,39 @@ export async function getApplicationFromDB(id: string, teamId: string) {
|
|||||||
}
|
}
|
||||||
export async function getApplicationFromDBWebhook(projectId: number, branch: string) {
|
export async function getApplicationFromDBWebhook(projectId: number, branch: string) {
|
||||||
try {
|
try {
|
||||||
let application = await prisma.application.findFirst({
|
let applications = await prisma.application.findMany({
|
||||||
where: { projectId, branch, settings: { autodeploy: true } },
|
where: { projectId, branch, settings: { autodeploy: true } },
|
||||||
include: {
|
include: {
|
||||||
destinationDocker: true,
|
destinationDocker: true,
|
||||||
settings: true,
|
settings: true,
|
||||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||||
secrets: true,
|
secrets: true,
|
||||||
persistentStorage: true
|
persistentStorage: true,
|
||||||
|
connectedDatabase: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!application) {
|
if (applications.length === 0) {
|
||||||
throw { status: 500, message: 'Application not configured.' }
|
throw { status: 500, message: 'Application not configured.' }
|
||||||
}
|
}
|
||||||
application = decryptApplication(application);
|
applications = applications.map((application: any) => {
|
||||||
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
|
application = decryptApplication(application);
|
||||||
application.buildPack
|
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
|
||||||
);
|
application.buildPack
|
||||||
|
);
|
||||||
|
|
||||||
// Set default build images
|
// Set default build images
|
||||||
if (!application.baseImage) {
|
if (!application.baseImage) {
|
||||||
application.baseImage = baseImage;
|
application.baseImage = baseImage;
|
||||||
}
|
}
|
||||||
if (!application.baseBuildImage) {
|
if (!application.baseBuildImage) {
|
||||||
application.baseBuildImage = baseBuildImage;
|
application.baseBuildImage = baseBuildImage;
|
||||||
}
|
}
|
||||||
return { ...application, baseBuildImages, baseImages };
|
application.baseBuildImages = baseBuildImages;
|
||||||
|
application.baseImages = baseImages;
|
||||||
|
return application
|
||||||
|
})
|
||||||
|
|
||||||
|
return applications;
|
||||||
|
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
@@ -229,15 +252,16 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
|||||||
denoOptions,
|
denoOptions,
|
||||||
baseImage,
|
baseImage,
|
||||||
baseBuildImage,
|
baseBuildImage,
|
||||||
deploymentType
|
deploymentType,
|
||||||
|
baseDatabaseBranch
|
||||||
} = request.body
|
} = request.body
|
||||||
if (port) port = Number(port);
|
if (port) port = Number(port);
|
||||||
if (exposePort) {
|
if (exposePort) {
|
||||||
exposePort = Number(exposePort);
|
exposePort = Number(exposePort);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { destinationDocker: { id: dockerId, remoteIpAddress } } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||||
if (exposePort) await checkExposedPort({ id, exposePort, dockerId, remoteIpAddress })
|
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
||||||
if (denoOptions) denoOptions = denoOptions.trim();
|
if (denoOptions) denoOptions = denoOptions.trim();
|
||||||
const defaultConfiguration = await setDefaultConfiguration({
|
const defaultConfiguration = await setDefaultConfiguration({
|
||||||
buildPack,
|
buildPack,
|
||||||
@@ -250,22 +274,43 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
|||||||
dockerFileLocation,
|
dockerFileLocation,
|
||||||
denoMainFile
|
denoMainFile
|
||||||
});
|
});
|
||||||
await prisma.application.update({
|
if (baseDatabaseBranch) {
|
||||||
where: { id },
|
await prisma.application.update({
|
||||||
data: {
|
where: { id },
|
||||||
name,
|
data: {
|
||||||
fqdn,
|
name,
|
||||||
exposePort,
|
fqdn,
|
||||||
pythonWSGI,
|
exposePort,
|
||||||
pythonModule,
|
pythonWSGI,
|
||||||
pythonVariable,
|
pythonModule,
|
||||||
denoOptions,
|
pythonVariable,
|
||||||
baseImage,
|
denoOptions,
|
||||||
baseBuildImage,
|
baseImage,
|
||||||
deploymentType,
|
baseBuildImage,
|
||||||
...defaultConfiguration
|
deploymentType,
|
||||||
}
|
...defaultConfiguration,
|
||||||
});
|
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await prisma.application.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
fqdn,
|
||||||
|
exposePort,
|
||||||
|
pythonWSGI,
|
||||||
|
pythonModule,
|
||||||
|
pythonVariable,
|
||||||
|
denoOptions,
|
||||||
|
baseImage,
|
||||||
|
baseBuildImage,
|
||||||
|
deploymentType,
|
||||||
|
...defaultConfiguration
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return reply.code(201).send();
|
return reply.code(201).send();
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
@@ -276,15 +321,15 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
|
|||||||
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
|
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot } = request.body
|
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching } = request.body
|
||||||
const isDouble = await checkDoubleBranch(branch, projectId);
|
// const isDouble = await checkDoubleBranch(branch, projectId);
|
||||||
if (isDouble && autodeploy) {
|
// if (isDouble && autodeploy) {
|
||||||
await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
|
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
|
||||||
throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' }
|
// throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' }
|
||||||
}
|
// }
|
||||||
await prisma.application.update({
|
await prisma.application.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot } } },
|
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching } } },
|
||||||
include: { destinationDocker: true }
|
include: { destinationDocker: true }
|
||||||
});
|
});
|
||||||
return reply.code(201).send();
|
return reply.code(201).send();
|
||||||
@@ -302,16 +347,127 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
|
|||||||
if (application?.destinationDockerId) {
|
if (application?.destinationDockerId) {
|
||||||
const container = `${id}-${pullmergeRequestId}`
|
const container = `${id}-${pullmergeRequestId}`
|
||||||
const { id: dockerId } = application.destinationDocker;
|
const { id: dockerId } = application.destinationDocker;
|
||||||
const found = await checkContainer({ dockerId, container });
|
const { found } = await checkContainer({ dockerId, container });
|
||||||
if (found) {
|
if (found) {
|
||||||
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
||||||
}
|
}
|
||||||
|
await prisma.previewApplication.deleteMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
}
|
}
|
||||||
return reply.code(201).send();
|
return reply.code(201).send();
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restartApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
const { teamId } = request.user
|
||||||
|
let application: any = await getApplicationFromDB(id, teamId);
|
||||||
|
if (application?.destinationDockerId) {
|
||||||
|
const buildId = cuid();
|
||||||
|
const { id: dockerId, network } = application.destinationDocker;
|
||||||
|
const { secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`PORT=${port}`
|
||||||
|
];
|
||||||
|
if (secrets.length > 0) {
|
||||||
|
secrets.forEach((secret) => {
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
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 {
|
||||||
|
if (!secret.isPRMRSecret) {
|
||||||
|
envs.push(`${secret.name}=${secret.value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { workdir } = await createDirectories({ repository, buildId });
|
||||||
|
const labels = []
|
||||||
|
let image = null
|
||||||
|
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` })
|
||||||
|
const containersArray = container.trim().split('\n');
|
||||||
|
for (const container of containersArray) {
|
||||||
|
const containerObj = formatLabelsOnDocker(container);
|
||||||
|
image = containerObj[0].Image
|
||||||
|
Object.keys(containerObj[0].Labels).forEach(function (key) {
|
||||||
|
if (key.startsWith('coolify')) {
|
||||||
|
labels.push(`${key}=${containerObj[0].Labels[key]}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let imageFound = false;
|
||||||
|
try {
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId,
|
||||||
|
command: `docker image inspect ${image}`
|
||||||
|
})
|
||||||
|
imageFound = true;
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
if (!imageFound) {
|
||||||
|
throw { status: 500, message: 'Image not found, cannot restart application.' }
|
||||||
|
}
|
||||||
|
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
||||||
|
|
||||||
|
let envFound = false;
|
||||||
|
try {
|
||||||
|
envFound = !!(await fs.stat(`${workdir}/.env`));
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
const volumes =
|
||||||
|
persistentStorage?.map((storage) => {
|
||||||
|
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
|
||||||
|
}${storage.path}`;
|
||||||
|
}) || [];
|
||||||
|
const composeVolumes = volumes.map((volume) => {
|
||||||
|
return {
|
||||||
|
[`${volume.split(':')[0]}`]: {
|
||||||
|
name: volume.split(':')[0]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const composeFile = {
|
||||||
|
version: '3.8',
|
||||||
|
services: {
|
||||||
|
[applicationId]: {
|
||||||
|
image,
|
||||||
|
container_name: applicationId,
|
||||||
|
volumes,
|
||||||
|
env_file: envFound ? [`${workdir}/.env`] : [],
|
||||||
|
labels,
|
||||||
|
depends_on: [],
|
||||||
|
expose: [port],
|
||||||
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||||
|
...defaultComposeConfiguration(network),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
[network]: {
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
volumes: Object.assign({}, ...composeVolumes)
|
||||||
|
};
|
||||||
|
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
|
||||||
|
return reply.code(201).send();
|
||||||
|
}
|
||||||
|
throw { status: 500, message: 'Application cannot be restarted.' }
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
@@ -319,7 +475,7 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
|
|||||||
const application: any = await getApplicationFromDB(id, teamId);
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
if (application?.destinationDockerId) {
|
if (application?.destinationDockerId) {
|
||||||
const { id: dockerId } = application.destinationDocker;
|
const { id: dockerId } = application.destinationDocker;
|
||||||
const found = await checkContainer({ dockerId, container: id });
|
const { found } = await checkContainer({ dockerId, container: id });
|
||||||
if (found) {
|
if (found) {
|
||||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||||
}
|
}
|
||||||
@@ -332,12 +488,14 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
|
|||||||
export async function deleteApplication(request: FastifyRequest<DeleteApplication>, reply: FastifyReply) {
|
export async function deleteApplication(request: FastifyRequest<DeleteApplication>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
|
const { force } = request.body
|
||||||
|
|
||||||
const { teamId } = request.user
|
const { teamId } = request.user
|
||||||
const application = await prisma.application.findUnique({
|
const application = await prisma.application.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: { destinationDocker: true }
|
include: { destinationDocker: true }
|
||||||
});
|
});
|
||||||
if (application?.destinationDockerId && application.destinationDocker?.network) {
|
if (!force && application?.destinationDockerId && application.destinationDocker?.network) {
|
||||||
const { stdout: containers } = await executeDockerCmd({
|
const { stdout: containers } = await executeDockerCmd({
|
||||||
dockerId: application.destinationDocker.id,
|
dockerId: application.destinationDocker.id,
|
||||||
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
|
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
|
||||||
@@ -356,6 +514,7 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
|
|||||||
await prisma.build.deleteMany({ where: { applicationId: id } });
|
await prisma.build.deleteMany({ where: { applicationId: id } });
|
||||||
await prisma.secret.deleteMany({ where: { applicationId: id } });
|
await prisma.secret.deleteMany({ where: { applicationId: id } });
|
||||||
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
|
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
|
||||||
|
await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } });
|
||||||
if (teamId === '0') {
|
if (teamId === '0') {
|
||||||
await prisma.application.deleteMany({ where: { id } });
|
await prisma.application.deleteMany({ where: { id } });
|
||||||
} else {
|
} else {
|
||||||
@@ -379,20 +538,22 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
|
|||||||
export async function checkDNS(request: FastifyRequest<CheckDNS>) {
|
export async function checkDNS(request: FastifyRequest<CheckDNS>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
|
|
||||||
let { exposePort, fqdn, forceSave, dualCerts } = request.body
|
let { exposePort, fqdn, forceSave, dualCerts } = request.body
|
||||||
|
if (!fqdn) {
|
||||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
return {}
|
||||||
|
} else {
|
||||||
|
fqdn = fqdn.toLowerCase();
|
||||||
|
}
|
||||||
if (exposePort) exposePort = Number(exposePort);
|
if (exposePort) exposePort = Number(exposePort);
|
||||||
|
|
||||||
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
const { destinationDocker: { engine, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||||
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
||||||
|
|
||||||
const found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
const found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
||||||
if (found) {
|
if (found) {
|
||||||
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
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) {
|
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
||||||
let hostname = request.hostname.split(':')[0];
|
let hostname = request.hostname.split(':')[0];
|
||||||
if (remoteEngine) hostname = remoteIpAddress;
|
if (remoteEngine) hostname = remoteIpAddress;
|
||||||
@@ -451,14 +612,14 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
|
|||||||
applicationId: id,
|
applicationId: id,
|
||||||
sourceBranch: branch,
|
sourceBranch: branch,
|
||||||
branch: application.branch,
|
branch: application.branch,
|
||||||
pullmergeRequestId: pullmergeRequestId.toString(),
|
pullmergeRequestId: pullmergeRequestId?.toString(),
|
||||||
forceRebuild,
|
forceRebuild,
|
||||||
destinationDockerId: application.destinationDocker?.id,
|
destinationDockerId: application.destinationDocker?.id,
|
||||||
gitSourceId: application.gitSource?.id,
|
gitSourceId: application.gitSource?.id,
|
||||||
githubAppId: application.gitSource?.githubApp?.id,
|
githubAppId: application.gitSource?.githubApp?.id,
|
||||||
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
||||||
status: 'queued',
|
status: 'queued',
|
||||||
type: 'manual'
|
type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -558,12 +719,12 @@ export async function saveRepository(request, reply) {
|
|||||||
data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } }
|
data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!isPublicRepository) {
|
// if (!isPublicRepository) {
|
||||||
const isDouble = await checkDoubleBranch(branch, projectId);
|
// const isDouble = await checkDoubleBranch(branch, projectId);
|
||||||
if (isDouble) {
|
// if (isDouble) {
|
||||||
await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } })
|
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return reply.code(201).send()
|
return reply.code(201).send()
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
@@ -612,6 +773,16 @@ export async function saveBuildPack(request, reply) {
|
|||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export async function saveConnectedDatabase(request, reply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
const { databaseId, type } = request.body
|
||||||
|
await prisma.application.update({ where: { id }, data: { connectedDatabase: { upsert: { create: { database: { connect: { id: databaseId } }, hostedDatabaseType: type }, update: { database: { connect: { id: databaseId } }, hostedDatabaseType: type } } } } })
|
||||||
|
return reply.code(201).send()
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSecrets(request: FastifyRequest<OnlyId>) {
|
export async function getSecrets(request: FastifyRequest<OnlyId>) {
|
||||||
try {
|
try {
|
||||||
@@ -639,7 +810,6 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
|
|||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
|
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
|
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
|
||||||
if (found) {
|
if (found) {
|
||||||
@@ -651,14 +821,24 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = encrypt(value.trim());
|
if (value) {
|
||||||
|
value = encrypt(value.trim());
|
||||||
|
}
|
||||||
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
|
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
|
||||||
|
|
||||||
if (found) {
|
if (found) {
|
||||||
await prisma.secret.updateMany({
|
if (!value && isPRMRSecret) {
|
||||||
where: { applicationId: id, name, isPRMRSecret },
|
await prisma.secret.deleteMany({
|
||||||
data: { value, isBuildSecret, isPRMRSecret }
|
where: { applicationId: id, name, isPRMRSecret }
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
await prisma.secret.updateMany({
|
||||||
|
where: { applicationId: id, name, isPRMRSecret },
|
||||||
|
data: { value, isBuildSecret, isPRMRSecret }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
await prisma.secret.create({
|
await prisma.secret.create({
|
||||||
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
|
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
|
||||||
@@ -725,6 +905,181 @@ export async function deleteStorage(request: FastifyRequest<DeleteStorage>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restartPreview(request: FastifyRequest<RestartPreviewApplication>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id, pullmergeRequestId } = request.params
|
||||||
|
const { teamId } = request.user
|
||||||
|
let application: any = await getApplicationFromDB(id, teamId);
|
||||||
|
if (application?.destinationDockerId) {
|
||||||
|
const buildId = cuid();
|
||||||
|
const { id: dockerId, network } = application.destinationDocker;
|
||||||
|
const { secrets, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`PORT=${port}`
|
||||||
|
];
|
||||||
|
if (secrets.length > 0) {
|
||||||
|
secrets.forEach((secret) => {
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
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 {
|
||||||
|
if (!secret.isPRMRSecret) {
|
||||||
|
envs.push(`${secret.name}=${secret.value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { workdir } = await createDirectories({ repository, buildId });
|
||||||
|
const labels = []
|
||||||
|
let image = null
|
||||||
|
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` })
|
||||||
|
const containersArray = container.trim().split('\n');
|
||||||
|
for (const container of containersArray) {
|
||||||
|
const containerObj = formatLabelsOnDocker(container);
|
||||||
|
image = containerObj[0].Image
|
||||||
|
Object.keys(containerObj[0].Labels).forEach(function (key) {
|
||||||
|
if (key.startsWith('coolify')) {
|
||||||
|
labels.push(`${key}=${containerObj[0].Labels[key]}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let imageFound = false;
|
||||||
|
try {
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId,
|
||||||
|
command: `docker image inspect ${image}`
|
||||||
|
})
|
||||||
|
imageFound = true;
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
if (!imageFound) {
|
||||||
|
throw { status: 500, message: 'Image not found, cannot restart application.' }
|
||||||
|
}
|
||||||
|
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
||||||
|
|
||||||
|
let envFound = false;
|
||||||
|
try {
|
||||||
|
envFound = !!(await fs.stat(`${workdir}/.env`));
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
const volumes =
|
||||||
|
persistentStorage?.map((storage) => {
|
||||||
|
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
|
||||||
|
}${storage.path}`;
|
||||||
|
}) || [];
|
||||||
|
const composeVolumes = volumes.map((volume) => {
|
||||||
|
return {
|
||||||
|
[`${volume.split(':')[0]}`]: {
|
||||||
|
name: volume.split(':')[0]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const composeFile = {
|
||||||
|
version: '3.8',
|
||||||
|
services: {
|
||||||
|
[`${applicationId}-${pullmergeRequestId}`]: {
|
||||||
|
image,
|
||||||
|
container_name: `${applicationId}-${pullmergeRequestId}`,
|
||||||
|
volumes,
|
||||||
|
env_file: envFound ? [`${workdir}/.env`] : [],
|
||||||
|
labels,
|
||||||
|
depends_on: [],
|
||||||
|
expose: [port],
|
||||||
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||||
|
...defaultComposeConfiguration(network),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
[network]: {
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
volumes: Object.assign({}, ...composeVolumes)
|
||||||
|
};
|
||||||
|
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` })
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` })
|
||||||
|
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
|
||||||
|
return reply.code(201).send();
|
||||||
|
}
|
||||||
|
throw { status: 500, message: 'Application cannot be restarted.' }
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function getPreviewStatus(request: FastifyRequest<RestartPreviewApplication>) {
|
||||||
|
try {
|
||||||
|
const { id, pullmergeRequestId } = request.params
|
||||||
|
const { teamId } = request.user
|
||||||
|
let isRunning = false;
|
||||||
|
let isExited = false;
|
||||||
|
let isRestarting = false;
|
||||||
|
let isBuilding = false
|
||||||
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
|
if (application?.destinationDockerId) {
|
||||||
|
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: `${id}-${pullmergeRequestId}` });
|
||||||
|
if (status?.found) {
|
||||||
|
isRunning = status.status.isRunning;
|
||||||
|
isExited = status.status.isExited;
|
||||||
|
isRestarting = status.status.isRestarting
|
||||||
|
}
|
||||||
|
const building = await prisma.build.findMany({ where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } })
|
||||||
|
isBuilding = building.length > 0
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isBuilding,
|
||||||
|
isRunning,
|
||||||
|
isRestarting,
|
||||||
|
isExited,
|
||||||
|
};
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function loadPreviews(request: FastifyRequest<OnlyId>) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
||||||
|
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
||||||
|
if (stdout === '') {
|
||||||
|
throw { status: 500, message: 'No previews found.' }
|
||||||
|
}
|
||||||
|
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
|
||||||
|
|
||||||
|
const jsonContainers = containers
|
||||||
|
.map((container) =>
|
||||||
|
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
||||||
|
)
|
||||||
|
.filter((container) => {
|
||||||
|
return container.pullmergeRequestId && container.applicationId === id;
|
||||||
|
});
|
||||||
|
for (const container of jsonContainers) {
|
||||||
|
const found = await prisma.previewApplication.findMany({ where: { applicationId: container.applicationId, pullmergeRequestId: container.pullmergeRequestId } })
|
||||||
|
if (found.length === 0) {
|
||||||
|
await prisma.previewApplication.create({
|
||||||
|
data: {
|
||||||
|
pullmergeRequestId: container.pullmergeRequestId,
|
||||||
|
sourceBranch: container.branch,
|
||||||
|
customDomain: container.fqdn,
|
||||||
|
application: { connect: { id: container.applicationId } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
previews: await prisma.previewApplication.findMany({ where: { applicationId: id } })
|
||||||
|
}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
@@ -740,26 +1095,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
|||||||
|
|
||||||
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
||||||
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
||||||
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
|
||||||
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
|
||||||
if (stdout === '') {
|
|
||||||
return {
|
|
||||||
containers: [],
|
|
||||||
applicationSecrets: [],
|
|
||||||
PRMRSecrets: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
|
|
||||||
|
|
||||||
const jsonContainers = containers
|
|
||||||
.map((container) =>
|
|
||||||
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
|
||||||
)
|
|
||||||
.filter((container) => {
|
|
||||||
return container.pullmergeRequestId && container.applicationId === id;
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
containers: jsonContainers,
|
|
||||||
applicationSecrets: applicationSecrets.sort((a, b) => {
|
applicationSecrets: applicationSecrets.sort((a, b) => {
|
||||||
return ('' + a.name).localeCompare(b.name);
|
return ('' + a.name).localeCompare(b.name);
|
||||||
}),
|
}),
|
||||||
@@ -768,7 +1104,6 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
console.log({ status, message })
|
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -812,7 +1147,7 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
|
|||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
export async function getBuilds(request: FastifyRequest<GetBuilds>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
let { buildId, skip = 0 } = request.query
|
let { buildId, skip = 0 } = request.query
|
||||||
@@ -829,17 +1164,15 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
|||||||
builds = await prisma.build.findMany({
|
builds = await prisma.build.findMany({
|
||||||
where: { applicationId: id },
|
where: { applicationId: id },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: 5,
|
take: 5 + skip
|
||||||
skip
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
builds = builds.map((build) => {
|
builds = builds.map((build) => {
|
||||||
const updatedAt = day(build.updatedAt).utc();
|
if (build.status === 'running') {
|
||||||
build.took = updatedAt.diff(day(build.createdAt)) / 1000;
|
build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0);
|
||||||
build.since = updatedAt.fromNow();
|
}
|
||||||
return build;
|
return build
|
||||||
});
|
})
|
||||||
return {
|
return {
|
||||||
builds,
|
builds,
|
||||||
buildCount
|
buildCount
|
||||||
@@ -851,18 +1184,50 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
|||||||
|
|
||||||
export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
|
export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
|
||||||
try {
|
try {
|
||||||
const { buildId } = request.params
|
// TODO: Fluentbit could still hold the logs, so we need to check if the logs are done
|
||||||
|
const { buildId, id } = request.params
|
||||||
let { sequence = 0 } = request.query
|
let { sequence = 0 } = request.query
|
||||||
if (typeof sequence !== 'number') {
|
if (typeof sequence !== 'number') {
|
||||||
sequence = Number(sequence)
|
sequence = Number(sequence)
|
||||||
}
|
}
|
||||||
let logs = await prisma.buildLog.findMany({
|
let file = `/app/logs/${id}_buildlog_${buildId}.csv`
|
||||||
where: { buildId, time: { gt: sequence } },
|
if (isDev) {
|
||||||
orderBy: { time: 'asc' }
|
file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv`
|
||||||
});
|
}
|
||||||
const data = await prisma.build.findFirst({ where: { id: buildId } });
|
const data = await prisma.build.findFirst({ where: { id: buildId } });
|
||||||
|
const createdAt = day(data.createdAt).utc();
|
||||||
|
try {
|
||||||
|
await fs.stat(file)
|
||||||
|
} catch (error) {
|
||||||
|
let logs = await prisma.buildLog.findMany({
|
||||||
|
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.map(log => {
|
||||||
|
log.time = Number(log.time)
|
||||||
|
return log
|
||||||
|
}),
|
||||||
|
fromDb: true,
|
||||||
|
took: day().diff(createdAt) / 1000,
|
||||||
|
status: data?.status || 'queued'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fileLogs = (await fs.readFile(file)).toString()
|
||||||
|
let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs)
|
||||||
|
let logs = decryptedLogs.map(log => {
|
||||||
|
const parsed = {
|
||||||
|
time: log['field1'],
|
||||||
|
line: decrypt(log['field2'] + '","' + log['field3'])
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}).filter(log => log.time > sequence)
|
||||||
return {
|
return {
|
||||||
logs,
|
logs,
|
||||||
|
fromDb: false,
|
||||||
|
took: day().diff(createdAt) / 1000,
|
||||||
status: data?.status || 'queued'
|
status: data?.status || 'queued'
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -937,4 +1302,59 @@ export async function cancelDeployment(request: FastifyRequest<CancelDeployment>
|
|||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function createdBranchDatabase(database: any, baseDatabaseBranch: string, pullmergeRequestId: string) {
|
||||||
|
try {
|
||||||
|
if (!baseDatabaseBranch) return
|
||||||
|
const { id, type, destinationDockerId, rootUser, rootUserPassword, dbUser } = database;
|
||||||
|
if (destinationDockerId) {
|
||||||
|
if (type === 'postgresql') {
|
||||||
|
const decryptedRootUserPassword = decrypt(rootUserPassword);
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump`
|
||||||
|
})
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"`
|
||||||
|
})
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump`
|
||||||
|
})
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function removeBranchDatabase(database: any, pullmergeRequestId: string) {
|
||||||
|
try {
|
||||||
|
const { id, type, destinationDockerId, rootUser, rootUserPassword } = database;
|
||||||
|
if (destinationDockerId) {
|
||||||
|
if (type === 'postgresql') {
|
||||||
|
const decryptedRootUserPassword = decrypt(rootUserPassword);
|
||||||
|
// Terminate all connections to the database
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"`
|
||||||
|
})
|
||||||
|
|
||||||
|
await executeDockerCmd({
|
||||||
|
dockerId: destinationDockerId,
|
||||||
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { OnlyId } from '../../../../types';
|
import { OnlyId } from '../../../../types';
|
||||||
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, 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, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
|
||||||
|
|
||||||
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.addHook('onRequest', async (request) => {
|
fastify.addHook('onRequest', async (request) => {
|
||||||
@@ -19,6 +19,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
|
|
||||||
fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request));
|
fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request));
|
||||||
|
|
||||||
|
fastify.post<OnlyId>('/:id/restart', async (request, reply) => await restartApplication(request, reply));
|
||||||
fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply));
|
fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply));
|
||||||
fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply));
|
fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply));
|
||||||
|
|
||||||
@@ -36,9 +37,12 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
fastify.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request));
|
fastify.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
|
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
|
||||||
|
fastify.post<OnlyId>('/:id/previews/load', async (request) => await loadPreviews(request));
|
||||||
|
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
|
||||||
|
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
|
||||||
|
|
||||||
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
||||||
fastify.get<GetBuildLogs>('/:id/logs/build', async (request) => await getBuildLogs(request));
|
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
|
||||||
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
|
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
|
||||||
|
|
||||||
fastify.get('/:id/usage', async (request) => await getUsage(request))
|
fastify.get('/:id/usage', async (request) => await getUsage(request))
|
||||||
@@ -54,6 +58,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
fastify.get('/:id/configuration/buildpack', async (request) => await getBuildPack(request));
|
fastify.get('/:id/configuration/buildpack', async (request) => await getBuildPack(request));
|
||||||
fastify.post('/:id/configuration/buildpack', async (request, reply) => await saveBuildPack(request, reply));
|
fastify.post('/:id/configuration/buildpack', async (request, reply) => await saveBuildPack(request, reply));
|
||||||
|
|
||||||
|
fastify.post('/:id/configuration/database', async (request, reply) => await saveConnectedDatabase(request, reply));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request));
|
fastify.get<OnlyId>('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request));
|
||||||
fastify.post<OnlyId>('/:id/configuration/sshkey', async (request, reply) => await saveGitLabSSHKey(request, reply));
|
fastify.post<OnlyId>('/:id/configuration/sshkey', async (request, reply) => await saveGitLabSSHKey(request, reply));
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,17 @@ export interface SaveApplication extends OnlyId {
|
|||||||
denoOptions: string,
|
denoOptions: string,
|
||||||
baseImage: string,
|
baseImage: string,
|
||||||
baseBuildImage: string,
|
baseBuildImage: string,
|
||||||
deploymentType: string
|
deploymentType: string,
|
||||||
|
baseDatabaseBranch: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface SaveApplicationSettings extends OnlyId {
|
export interface SaveApplicationSettings extends OnlyId {
|
||||||
Querystring: { domain: string; };
|
Querystring: { domain: string; };
|
||||||
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; };
|
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean };
|
||||||
}
|
}
|
||||||
export interface DeleteApplication extends OnlyId {
|
export interface DeleteApplication extends OnlyId {
|
||||||
Querystring: { domain: string; };
|
Querystring: { domain: string; };
|
||||||
|
Body: { force: boolean }
|
||||||
}
|
}
|
||||||
export interface CheckDomain extends OnlyId {
|
export interface CheckDomain extends OnlyId {
|
||||||
Querystring: { domain: string; };
|
Querystring: { domain: string; };
|
||||||
@@ -87,7 +89,7 @@ export interface GetApplicationLogs extends OnlyId {
|
|||||||
since: number,
|
since: number,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface GetBuildLogs extends OnlyId {
|
export interface GetBuilds extends OnlyId {
|
||||||
Querystring: {
|
Querystring: {
|
||||||
buildId: string
|
buildId: string
|
||||||
skip: number,
|
skip: number,
|
||||||
@@ -95,6 +97,7 @@ export interface GetBuildLogs extends OnlyId {
|
|||||||
}
|
}
|
||||||
export interface GetBuildIdLogs {
|
export interface GetBuildIdLogs {
|
||||||
Params: {
|
Params: {
|
||||||
|
id: string,
|
||||||
buildId: string
|
buildId: string
|
||||||
},
|
},
|
||||||
Querystring: {
|
Querystring: {
|
||||||
@@ -124,4 +127,10 @@ export interface StopPreviewApplication extends OnlyId {
|
|||||||
Body: {
|
Body: {
|
||||||
pullmergeRequestId: string | null,
|
pullmergeRequestId: string | null,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export interface RestartPreviewApplication {
|
||||||
|
Params: {
|
||||||
|
id: string,
|
||||||
|
pullmergeRequestId: string | null,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
version,
|
version,
|
||||||
whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true',
|
whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true',
|
||||||
whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON,
|
whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON,
|
||||||
|
isRegistrationEnabled: settings.isRegistrationEnabled,
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import type { FastifyRequest } from 'fastify';
|
|||||||
import { FastifyReply } from 'fastify';
|
import { FastifyReply } from 'fastify';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import fs from 'fs/promises';
|
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 { day } from '../../../../lib/dayjs';
|
||||||
|
|
||||||
import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import { SaveDatabaseType } from './types';
|
import type { DeleteDatabase, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveDatabaseType, SaveVersion } from './types';
|
||||||
|
|
||||||
export async function listDatabases(request: FastifyRequest) {
|
export async function listDatabases(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -61,16 +61,18 @@ export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||||
include: { destinationDocker: true, settings: true }
|
include: { destinationDocker: true, settings: true }
|
||||||
});
|
});
|
||||||
const { destinationDockerId, destinationDocker } = database;
|
if (database) {
|
||||||
if (destinationDockerId) {
|
const { destinationDockerId, destinationDocker } = database;
|
||||||
try {
|
if (destinationDockerId) {
|
||||||
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
|
try {
|
||||||
|
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
|
||||||
|
|
||||||
if (JSON.parse(stdout).Running) {
|
if (JSON.parse(stdout).Running) {
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -92,15 +94,14 @@ export async function getDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
if (!database) {
|
if (!database) {
|
||||||
throw { status: 404, message: 'Database not found.' }
|
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.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||||
const configuration = generateDatabaseConfiguration(database, arch);
|
const configuration = generateDatabaseConfiguration(database, settings.arch);
|
||||||
const settings = await listSettings();
|
|
||||||
return {
|
return {
|
||||||
privatePort: configuration?.privatePort,
|
privatePort: configuration?.privatePort,
|
||||||
database,
|
database,
|
||||||
versions: await getDatabaseVersions(database.type, arch),
|
versions: await getDatabaseVersions(database.type, settings.arch),
|
||||||
settings
|
settings
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -167,6 +168,7 @@ export async function saveDatabaseDestination(request: FastifyRequest<SaveDataba
|
|||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const { destinationId } = request.body;
|
const { destinationId } = request.body;
|
||||||
|
|
||||||
|
const { arch } = await listSettings();
|
||||||
await prisma.database.update({
|
await prisma.database.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { destinationDocker: { connect: { id: destinationId } } }
|
data: { destinationDocker: { connect: { id: destinationId } } }
|
||||||
@@ -181,7 +183,7 @@ export async function saveDatabaseDestination(request: FastifyRequest<SaveDataba
|
|||||||
|
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
if (type && version) {
|
if (type && version) {
|
||||||
const baseImage = getDatabaseImage(type);
|
const baseImage = getDatabaseImage(type, arch);
|
||||||
executeDockerCmd({ dockerId, command: `docker pull ${baseImage}:${version}` })
|
executeDockerCmd({ dockerId, command: `docker pull ${baseImage}:${version}` })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +221,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
|
|
||||||
const database = await prisma.database.findFirst({
|
const database = await prisma.database.findFirst({
|
||||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
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();
|
const { arch } = await listSettings();
|
||||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||||
@@ -229,7 +231,8 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
destinationDockerId,
|
destinationDockerId,
|
||||||
destinationDocker,
|
destinationDocker,
|
||||||
publicPort,
|
publicPort,
|
||||||
settings: { isPublic }
|
settings: { isPublic },
|
||||||
|
databaseSecret
|
||||||
} = database;
|
} = database;
|
||||||
const { privatePort, command, environmentVariables, image, volume, ulimits } =
|
const { privatePort, command, environmentVariables, image, volume, ulimits } =
|
||||||
generateDatabaseConfiguration(database, arch);
|
generateDatabaseConfiguration(database, arch);
|
||||||
@@ -239,7 +242,11 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
|
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
|
||||||
|
|
||||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||||
|
if (databaseSecret.length > 0) {
|
||||||
|
databaseSecret.forEach((secret) => {
|
||||||
|
environmentVariables[secret.name] = decrypt(secret.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
const composeFile: ComposeFile = {
|
const composeFile: ComposeFile = {
|
||||||
version: '3.8',
|
version: '3.8',
|
||||||
services: {
|
services: {
|
||||||
@@ -247,20 +254,11 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
container_name: id,
|
container_name: id,
|
||||||
image,
|
image,
|
||||||
command,
|
command,
|
||||||
networks: [network],
|
|
||||||
environment: environmentVariables,
|
environment: environmentVariables,
|
||||||
volumes: [volume],
|
volumes: [volume],
|
||||||
ulimits,
|
ulimits,
|
||||||
labels,
|
labels,
|
||||||
restart: 'always',
|
...defaultComposeConfiguration(network),
|
||||||
deploy: {
|
|
||||||
restart_policy: {
|
|
||||||
condition: 'on-failure',
|
|
||||||
delay: '5s',
|
|
||||||
max_attempts: 3,
|
|
||||||
window: '120s'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
networks: {
|
networks: {
|
||||||
@@ -270,28 +268,16 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
|||||||
},
|
},
|
||||||
volumes: {
|
volumes: {
|
||||||
[volumeName]: {
|
[volumeName]: {
|
||||||
external: true
|
name: volumeName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||||
try {
|
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
|
||||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` })
|
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||||
} catch (error) {
|
return {};
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
@@ -360,21 +346,25 @@ export async function getDatabaseLogs(request: FastifyRequest<GetDatabaseLogs>)
|
|||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function deleteDatabase(request: FastifyRequest<OnlyId>) {
|
export async function deleteDatabase(request: FastifyRequest<DeleteDatabase>) {
|
||||||
try {
|
try {
|
||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
|
const { force } = request.body;
|
||||||
const database = await prisma.database.findFirst({
|
const database = await prisma.database.findFirst({
|
||||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||||
include: { destinationDocker: true, settings: true }
|
include: { destinationDocker: true, settings: true }
|
||||||
});
|
});
|
||||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
if (!force) {
|
||||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||||
if (database.destinationDockerId) {
|
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||||
const everStarted = await stopDatabaseContainer(database);
|
if (database.destinationDockerId) {
|
||||||
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
|
const everStarted = await stopDatabaseContainer(database);
|
||||||
|
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
|
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
|
||||||
|
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
|
||||||
await prisma.database.delete({ where: { id } });
|
await prisma.database.delete({ where: { id } });
|
||||||
return {}
|
return {}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -435,10 +425,10 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
|
|||||||
|
|
||||||
let publicPort = null
|
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) {
|
if (isPublic) {
|
||||||
publicPort = await getFreePublicPort(id, dockerId);
|
publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
|
||||||
}
|
}
|
||||||
await prisma.database.update({
|
await prisma.database.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -470,4 +460,69 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
|
|||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
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 { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import type { SaveDatabaseType } from './types';
|
|
||||||
|
import type { DeleteDatabase, SaveDatabaseType, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveVersion } from './types';
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.addHook('onRequest', async (request) => {
|
fastify.addHook('onRequest', async (request) => {
|
||||||
@@ -13,12 +14,16 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
|
|
||||||
fastify.get<OnlyId>('/:id', async (request) => await getDatabase(request));
|
fastify.get<OnlyId>('/:id', async (request) => await getDatabase(request));
|
||||||
fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply));
|
fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply));
|
||||||
fastify.delete<OnlyId>('/:id', async (request) => await deleteDatabase(request));
|
fastify.delete<DeleteDatabase>('/:id', async (request) => await deleteDatabase(request));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/:id/status', async (request) => await getDatabaseStatus(request));
|
fastify.get<OnlyId>('/:id/status', async (request) => await getDatabaseStatus(request));
|
||||||
|
|
||||||
fastify.post<SaveDatabaseSettings>('/:id/settings', async (request) => await saveDatabaseSettings(request));
|
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.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));
|
||||||
fastify.post<SaveDatabaseType>('/:id/configuration/type', async (request, reply) => await saveDatabaseType(request, reply));
|
fastify.post<SaveDatabaseType>('/:id/configuration/type', async (request, reply) => await saveDatabaseType(request, reply));
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,54 @@ import type { OnlyId } from "../../../../types";
|
|||||||
|
|
||||||
export interface SaveDatabaseType extends OnlyId {
|
export interface SaveDatabaseType extends OnlyId {
|
||||||
Body: { type: string }
|
Body: { type: string }
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export async function listDestinations(request: FastifyRequest<ListDestinations>
|
|||||||
destinations
|
destinations
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
console.log({ status, message })
|
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +113,6 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
console.log({ status, message })
|
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +160,6 @@ export async function startProxy(request: FastifyRequest<Proxy>) {
|
|||||||
await startTraefikProxy(id);
|
await startTraefikProxy(id);
|
||||||
return {}
|
return {}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
console.log({ status, message })
|
|
||||||
await stopTraefikProxy(id);
|
await stopTraefikProxy(id);
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
@@ -205,23 +202,21 @@ export async function assignSSHKey(request: FastifyRequest) {
|
|||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) {
|
export async function verifyRemoteDockerEngine(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
await createRemoteEngineConfiguration(id);
|
await createRemoteEngineConfiguration(id);
|
||||||
|
const { remoteIpAddress, remoteUser, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } })
|
||||||
const { remoteIpAddress, remoteUser, network } = await prisma.destinationDocker.findFirst({ where: { id } })
|
|
||||||
const host = `ssh://${remoteUser}@${remoteIpAddress}`
|
const host = `ssh://${remoteUser}@${remoteIpAddress}`
|
||||||
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
|
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
|
||||||
|
|
||||||
if (!stdout) {
|
if (!stdout) {
|
||||||
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
|
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
|
||||||
}
|
}
|
||||||
const { stdout:coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`);
|
const { stdout: coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`);
|
||||||
|
|
||||||
if (!coolifyNetwork) {
|
if (!coolifyNetwork) {
|
||||||
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
|
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
|
||||||
}
|
}
|
||||||
|
if (isCoolifyProxyUsed) await startTraefikProxy(id);
|
||||||
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
|
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
|
||||||
return reply.code(201).send()
|
return reply.code(201).send()
|
||||||
|
|
||||||
@@ -234,7 +229,7 @@ export async function getDestinationStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const destination = await prisma.destinationDocker.findUnique({ where: { id } })
|
const destination = await prisma.destinationDocker.findUnique({ where: { id } })
|
||||||
const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy' })
|
const { found: isRunning } = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true })
|
||||||
return {
|
return {
|
||||||
isRunning
|
isRunning
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
|
|
||||||
fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request));
|
fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request));
|
||||||
|
|
||||||
fastify.post('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply));
|
fastify.post<OnlyId>('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default root;
|
export default root;
|
||||||
|
|||||||
@@ -1,50 +1,64 @@
|
|||||||
import os from 'node:os';
|
import axios from "axios";
|
||||||
import osu from 'node-os-utils';
|
import { compareVersions } from "compare-versions";
|
||||||
import axios from 'axios';
|
import cuid from "cuid";
|
||||||
import { compareVersions } from 'compare-versions';
|
import bcrypt from "bcryptjs";
|
||||||
import cuid from 'cuid';
|
import {
|
||||||
import bcrypt from 'bcryptjs';
|
asyncExecShell,
|
||||||
import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common';
|
asyncSleep,
|
||||||
import { supportedServiceTypesAndVersions } from '../../../lib/services/supportedVersions';
|
cleanupDockerStorage,
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
errorHandler,
|
||||||
import type { Login, Update } from '.';
|
isDev,
|
||||||
import type { GetCurrentUser } from './types';
|
listSettings,
|
||||||
|
prisma,
|
||||||
|
uniqueName,
|
||||||
|
version,
|
||||||
|
} from "../../../lib/common";
|
||||||
|
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
|
||||||
|
import { scheduler } from "../../../lib/scheduler";
|
||||||
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
|
import type { Login, Update } from ".";
|
||||||
|
import type { GetCurrentUser } from "./types";
|
||||||
|
|
||||||
export async function hashPassword(password: string): Promise<string> {
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
const saltRounds = 15;
|
const saltRounds = 15;
|
||||||
return bcrypt.hash(password, saltRounds);
|
return bcrypt.hash(password, saltRounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupManually() {
|
export async function cleanupManually(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
const destination = await prisma.destinationDocker.findFirst({ where: { engine: '/var/run/docker.sock' } })
|
const { serverId } = request.body;
|
||||||
await cleanupDockerStorage(destination.id, true, true)
|
const destination = await prisma.destinationDocker.findUnique({
|
||||||
return {}
|
where: { id: serverId },
|
||||||
|
});
|
||||||
|
await cleanupDockerStorage(destination.id, true, true);
|
||||||
|
return {};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function checkUpdate(request: FastifyRequest) {
|
export async function checkUpdate(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
const isStaging = request.hostname === 'staging.coolify.io'
|
const isStaging =
|
||||||
|
request.hostname === "staging.coolify.io" ||
|
||||||
|
request.hostname === "arm.coolify.io";
|
||||||
const currentVersion = version;
|
const currentVersion = version;
|
||||||
const { data: versions } = await axios.get(
|
const { data: versions } = await axios.get(
|
||||||
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
|
`https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}`
|
||||||
);
|
);
|
||||||
const latestVersion = versions['coolify'].main.version
|
const latestVersion = versions["coolify"].main.version;
|
||||||
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
||||||
if (isStaging) {
|
if (isStaging) {
|
||||||
return {
|
return {
|
||||||
isUpdateAvailable: true,
|
isUpdateAvailable: true,
|
||||||
latestVersion: 'next'
|
latestVersion: "next",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1,
|
isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1,
|
||||||
latestVersion
|
latestVersion,
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,168 +66,184 @@ export async function update(request: FastifyRequest<Update>) {
|
|||||||
const { latestVersion } = request.body;
|
const { latestVersion } = request.body;
|
||||||
try {
|
try {
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
const { isAutoUpdateEnabled } = (await prisma.setting.findFirst()) || {
|
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
|
||||||
isAutoUpdateEnabled: false
|
|
||||||
};
|
|
||||||
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
|
||||||
await asyncExecShell(`env | grep COOLIFY > .env`);
|
await asyncExecShell(`env | grep COOLIFY > .env`);
|
||||||
await asyncExecShell(
|
await asyncExecShell(
|
||||||
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
|
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
|
||||||
);
|
);
|
||||||
await asyncExecShell(
|
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"`
|
`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 coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
|
||||||
);
|
);
|
||||||
return {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
console.log(latestVersion);
|
|
||||||
await asyncSleep(2000);
|
await asyncSleep(2000);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function resetQueue(request: FastifyRequest<any>) {
|
||||||
|
try {
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
if (teamId === "0") {
|
||||||
|
await prisma.build.updateMany({
|
||||||
|
where: { status: { in: ["queued", "running"] } },
|
||||||
|
data: { status: "canceled" },
|
||||||
|
});
|
||||||
|
scheduler.workers.get("deployApplication").postMessage("cancel");
|
||||||
|
}
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function restartCoolify(request: FastifyRequest<any>) {
|
export async function restartCoolify(request: FastifyRequest<any>) {
|
||||||
try {
|
try {
|
||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
if (teamId === '0') {
|
if (teamId === "0") {
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
await asyncExecShell(`docker restart coolify`);
|
asyncExecShell(`docker restart coolify`);
|
||||||
return {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
console.log('Restarting Coolify')
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw { status: 500, message: 'You are not authorized to restart Coolify.' };
|
throw {
|
||||||
} catch ({ status, message }) {
|
status: 500,
|
||||||
return errorHandler({ status, message })
|
message: "You are not authorized to restart Coolify.",
|
||||||
}
|
|
||||||
}
|
|
||||||
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 }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showDashboard(request: FastifyRequest) {
|
export async function showDashboard(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
const userId = request.user.userId;
|
const userId = request.user.userId;
|
||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
const applications = await prisma.application.findMany({
|
const applications = await prisma.application.findMany({
|
||||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
|
||||||
include: { settings: true }
|
include: { settings: true, destinationDocker: true, teams: true },
|
||||||
});
|
});
|
||||||
const databases = await prisma.database.findMany({
|
const databases = await prisma.database.findMany({
|
||||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
|
||||||
include: { settings: true }
|
include: { settings: true, destinationDocker: true, teams: true },
|
||||||
});
|
});
|
||||||
const services = await prisma.service.findMany({
|
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();
|
const settings = await listSettings();
|
||||||
return {
|
return {
|
||||||
applications,
|
applications,
|
||||||
databases,
|
databases,
|
||||||
services,
|
services,
|
||||||
|
gitSources,
|
||||||
|
destinations,
|
||||||
settings,
|
settings,
|
||||||
};
|
};
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(request: FastifyRequest<Login>, reply: FastifyReply) {
|
export async function login(
|
||||||
|
request: FastifyRequest<Login>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
if (request.user) {
|
if (request.user) {
|
||||||
return reply.redirect('/dashboard');
|
return reply.redirect("/dashboard");
|
||||||
} else {
|
} else {
|
||||||
const { email, password, isLogin } = request.body || {};
|
const { email, password, isLogin } = request.body || {};
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
throw { status: 500, message: 'Email and password are required.' };
|
throw { status: 500, message: "Email and password are required." };
|
||||||
}
|
}
|
||||||
const users = await prisma.user.count();
|
const users = await prisma.user.count();
|
||||||
const userFound = await prisma.user.findUnique({
|
const userFound = await prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
include: { teams: true, permission: true },
|
include: { teams: true, permission: true },
|
||||||
rejectOnNotFound: false
|
rejectOnNotFound: false,
|
||||||
});
|
});
|
||||||
if (!userFound && isLogin) {
|
if (!userFound && isLogin) {
|
||||||
throw { status: 500, message: 'User not found.' };
|
throw { status: 500, message: "User not found." };
|
||||||
}
|
}
|
||||||
const { isRegistrationEnabled, id } = await prisma.setting.findFirst()
|
const { isRegistrationEnabled, id } = await prisma.setting.findFirst();
|
||||||
let uid = cuid();
|
let uid = cuid();
|
||||||
let permission = 'read';
|
let permission = "read";
|
||||||
let isAdmin = false;
|
let isAdmin = false;
|
||||||
|
|
||||||
if (users === 0) {
|
if (users === 0) {
|
||||||
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
|
await prisma.setting.update({
|
||||||
uid = '0';
|
where: { id },
|
||||||
|
data: { isRegistrationEnabled: false },
|
||||||
|
});
|
||||||
|
uid = "0";
|
||||||
}
|
}
|
||||||
if (userFound) {
|
if (userFound) {
|
||||||
if (userFound.type === 'email') {
|
if (userFound.type === "email") {
|
||||||
if (userFound.password === 'RESETME') {
|
if (userFound.password === "RESETME") {
|
||||||
const hashedPassword = await hashPassword(password);
|
const hashedPassword = await hashPassword(password);
|
||||||
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
|
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
|
||||||
if (userFound.id === '0') {
|
if (userFound.id === "0") {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { email: userFound.email },
|
where: { email: userFound.email },
|
||||||
data: { password: 'RESETME' }
|
data: { password: "RESETME" },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { email: userFound.email },
|
where: { email: userFound.email },
|
||||||
data: { password: 'RESETTIMEOUT' }
|
data: { password: "RESETTIMEOUT" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw {
|
throw {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: 'Password reset link has expired. Please request a new one.'
|
message:
|
||||||
|
"Password reset link has expired. Please request a new one.",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { email: userFound.email },
|
where: { email: userFound.email },
|
||||||
data: { password: hashedPassword }
|
data: { password: hashedPassword },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
userId: userFound.id,
|
userId: userFound.id,
|
||||||
teamId: userFound.id,
|
teamId: userFound.id,
|
||||||
permission: userFound.permission,
|
permission: userFound.permission,
|
||||||
isAdmin: true
|
isAdmin: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMatch = await bcrypt.compare(password, userFound.password);
|
const passwordMatch = await bcrypt.compare(
|
||||||
|
password,
|
||||||
|
userFound.password
|
||||||
|
);
|
||||||
if (!passwordMatch) {
|
if (!passwordMatch) {
|
||||||
throw {
|
throw {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: 'Wrong password or email address.'
|
message: "Wrong password or email address.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
uid = userFound.id;
|
uid = userFound.id;
|
||||||
isAdmin = true;
|
isAdmin = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
permission = 'owner';
|
permission = "owner";
|
||||||
isAdmin = true;
|
isAdmin = true;
|
||||||
if (!isRegistrationEnabled) {
|
if (!isRegistrationEnabled) {
|
||||||
throw {
|
throw {
|
||||||
status: 404,
|
status: 404,
|
||||||
message: 'Registration disabled by administrator.'
|
message: "Registration disabled by administrator.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const hashedPassword = await hashPassword(password);
|
const hashedPassword = await hashPassword(password);
|
||||||
@@ -223,17 +253,17 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
|
|||||||
id: uid,
|
id: uid,
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
type: 'email',
|
type: "email",
|
||||||
teams: {
|
teams: {
|
||||||
create: {
|
create: {
|
||||||
id: uid,
|
id: uid,
|
||||||
name: uniqueName(),
|
name: uniqueName(),
|
||||||
destinationDocker: { connect: { network: 'coolify' } }
|
destinationDocker: { connect: { network: "coolify" } },
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
permission: { create: { teamId: uid, permission: 'owner' } }
|
permission: { create: { teamId: uid, permission: "owner" } },
|
||||||
},
|
},
|
||||||
include: { teams: true }
|
include: { teams: true },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await prisma.user.create({
|
await prisma.user.create({
|
||||||
@@ -241,16 +271,16 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
|
|||||||
id: uid,
|
id: uid,
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
type: 'email',
|
type: "email",
|
||||||
teams: {
|
teams: {
|
||||||
create: {
|
create: {
|
||||||
id: uid,
|
id: uid,
|
||||||
name: uniqueName()
|
name: uniqueName(),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
permission: { create: { teamId: uid, permission: 'owner' } }
|
permission: { create: { teamId: uid, permission: "owner" } },
|
||||||
},
|
},
|
||||||
include: { teams: true }
|
include: { teams: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,18 +288,21 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
|
|||||||
userId: uid,
|
userId: uid,
|
||||||
teamId: uid,
|
teamId: uid,
|
||||||
permission,
|
permission,
|
||||||
isAdmin
|
isAdmin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fastify) {
|
export async function getCurrentUser(
|
||||||
let token = null
|
request: FastifyRequest<GetCurrentUser>,
|
||||||
const { teamId } = request.query
|
fastify
|
||||||
|
) {
|
||||||
|
let token = null;
|
||||||
|
const { teamId } = request.query;
|
||||||
try {
|
try {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: request.user.userId }
|
where: { id: request.user.userId },
|
||||||
})
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw "User not found";
|
throw "User not found";
|
||||||
}
|
}
|
||||||
@@ -280,20 +313,20 @@ export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fa
|
|||||||
try {
|
try {
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: { id: request.user.userId, teams: { some: { id: teamId } } },
|
where: { id: request.user.userId, teams: { some: { id: teamId } } },
|
||||||
include: { teams: true, permission: true }
|
include: { teams: true, permission: true },
|
||||||
})
|
});
|
||||||
if (user) {
|
if (user) {
|
||||||
const permission = user.permission.find(p => p.teamId === teamId).permission
|
const permission = user.permission.find(
|
||||||
|
(p) => p.teamId === teamId
|
||||||
|
).permission;
|
||||||
const payload = {
|
const payload = {
|
||||||
...request.user,
|
...request.user,
|
||||||
teamId,
|
teamId,
|
||||||
permission: permission || null,
|
permission: permission || null,
|
||||||
isAdmin: permission === 'owner' || permission === 'admin'
|
isAdmin: permission === "owner" || permission === "admin",
|
||||||
|
};
|
||||||
}
|
token = fastify.jwt.sign(payload);
|
||||||
token = fastify.jwt.sign(payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// No new token -> not switching teams
|
// No new token -> not switching teams
|
||||||
}
|
}
|
||||||
@@ -302,6 +335,6 @@ export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fa
|
|||||||
settings: await prisma.setting.findFirst(),
|
settings: await prisma.setting.findFirst(),
|
||||||
supportedServiceTypesAndVersions,
|
supportedServiceTypesAndVersions,
|
||||||
token,
|
token,
|
||||||
...request.user
|
...request.user,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
|
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
|
||||||
import { GetCurrentUser } from './types';
|
import { GetCurrentUser } from './types';
|
||||||
|
|
||||||
export interface Update {
|
export interface Update {
|
||||||
@@ -43,17 +43,17 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
onRequest: [fastify.authenticate]
|
onRequest: [fastify.authenticate]
|
||||||
}, async (request) => await showDashboard(request));
|
}, async (request) => await showDashboard(request));
|
||||||
|
|
||||||
fastify.get('/usage', {
|
|
||||||
onRequest: [fastify.authenticate]
|
|
||||||
}, async () => await showUsage());
|
|
||||||
|
|
||||||
fastify.post('/internal/restart', {
|
fastify.post('/internal/restart', {
|
||||||
onRequest: [fastify.authenticate]
|
onRequest: [fastify.authenticate]
|
||||||
}, async (request) => await restartCoolify(request));
|
}, async (request) => await restartCoolify(request));
|
||||||
|
|
||||||
|
fastify.post('/internal/resetQueue', {
|
||||||
|
onRequest: [fastify.authenticate]
|
||||||
|
}, async (request) => await resetQueue(request));
|
||||||
|
|
||||||
fastify.post('/internal/cleanup', {
|
fastify.post('/internal/cleanup', {
|
||||||
onRequest: [fastify.authenticate]
|
onRequest: [fastify.authenticate]
|
||||||
}, async () => await cleanupManually());
|
}, async (request) => await cleanupManually(request));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default root;
|
export default root;
|
||||||
|
|||||||
116
apps/api/src/routes/api/v1/servers/handlers.ts
Normal file
116
apps/api/src/routes/api/v1/servers/handlers.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
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 } }}, 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}')]` })
|
||||||
|
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,0,0],
|
||||||
|
usage: cpuUsage,
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
14
apps/api/src/routes/api/v1/servers/index.ts
Normal file
14
apps/api/src/routes/api/v1/servers/index.ts
Normal 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;
|
||||||
27
apps/api/src/routes/api/v1/servers/types.ts
Normal file
27
apps/api/src/routes/api/v1/servers/types.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import bcrypt from 'bcryptjs';
|
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 { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceFromDB, getContainerUsage,isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM, defaultComposeConfiguration, checkExposedPort } from '../../../../lib/common';
|
|
||||||
import { day } from '../../../../lib/dayjs';
|
import { day } from '../../../../lib/dayjs';
|
||||||
import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker';
|
import { checkContainer, isContainerExited } from '../../../../lib/docker';
|
||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
|
|
||||||
import type { OnlyId } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types';
|
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
|
||||||
import { defaultServiceConfigurations } from '../../../../lib/services';
|
|
||||||
import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions';
|
import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions';
|
||||||
import { configureServiceType, removeService } from '../../../../lib/services/common';
|
import { configureServiceType, removeService } from '../../../../lib/services/common';
|
||||||
|
|
||||||
@@ -45,13 +43,17 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
|
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let isExited = false
|
let isExited = false
|
||||||
|
let isRestarting = false;
|
||||||
const service = await getServiceFromDB({ id, teamId });
|
const service = await getServiceFromDB({ id, teamId });
|
||||||
const { destinationDockerId, settings } = service;
|
const { destinationDockerId, settings } = service;
|
||||||
|
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
isRunning = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
|
const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
|
||||||
isExited = await isContainerExited(service.destinationDocker.id, id);
|
if (status?.found) {
|
||||||
|
isRunning = status.status.isRunning;
|
||||||
|
isExited = status.status.isExited;
|
||||||
|
isRestarting = status.status.isRestarting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isRunning,
|
isRunning,
|
||||||
@@ -72,6 +74,7 @@ export async function getService(request: FastifyRequest<OnlyId>) {
|
|||||||
throw { status: 404, message: 'Service not found.' }
|
throw { status: 404, message: 'Service not found.' }
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
settings: await listSettings(),
|
||||||
service
|
service
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
@@ -234,7 +237,7 @@ export async function checkService(request: FastifyRequest<CheckService>) {
|
|||||||
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
|
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
|
||||||
if (exposePort) exposePort = Number(exposePort);
|
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({});
|
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
||||||
|
|
||||||
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
||||||
@@ -249,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) {
|
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
||||||
let hostname = request.hostname.split(':')[0];
|
let hostname = request.hostname.split(':')[0];
|
||||||
if (remoteEngine) hostname = remoteIpAddress;
|
if (remoteEngine) hostname = remoteIpAddress;
|
||||||
@@ -269,7 +272,6 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
|
|||||||
if (exposePort) exposePort = Number(exposePort);
|
if (exposePort) exposePort = Number(exposePort);
|
||||||
|
|
||||||
type = fixType(type)
|
type = fixType(type)
|
||||||
|
|
||||||
const update = saveUpdateableFields(type, request.body[type])
|
const update = saveUpdateableFields(type, request.body[type])
|
||||||
const data = {
|
const data = {
|
||||||
fqdn,
|
fqdn,
|
||||||
@@ -400,17 +402,33 @@ export async function deleteServiceStorage(request: FastifyRequest<DeleteService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setSettingsService(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) {
|
export async function setSettingsService(request: FastifyRequest<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { type } = request.params
|
const { type } = request.params
|
||||||
if (type === 'wordpress') {
|
if (type === 'wordpress') {
|
||||||
return await setWordpressSettings(request, reply)
|
return await setWordpressSettings(request, reply)
|
||||||
}
|
}
|
||||||
|
if (type === 'glitchtip') {
|
||||||
|
return await setGlitchTipSettings(request, reply)
|
||||||
|
}
|
||||||
throw `Service type ${type} not supported.`
|
throw `Service type ${type} not supported.`
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function setGlitchTipSettings(request: FastifyRequest<SetGlitchTipSettings>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params
|
||||||
|
const { enableOpenUserRegistration, emailSmtpUseSsl, emailSmtpUseTls } = request.body
|
||||||
|
await prisma.glitchTip.update({
|
||||||
|
where: { serviceId: id },
|
||||||
|
data: { enableOpenUserRegistration, emailSmtpUseSsl, emailSmtpUseTls }
|
||||||
|
});
|
||||||
|
return reply.code(201).send()
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message })
|
||||||
|
}
|
||||||
|
}
|
||||||
async function setWordpressSettings(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) {
|
async function setWordpressSettings(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
@@ -438,7 +456,7 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
|
|||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId: destinationDocker.id,
|
dockerId: destinationDocker.id,
|
||||||
command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'`
|
command: `docker exec ${id}-postgresql psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
|
||||||
})
|
})
|
||||||
return await reply.code(201).send()
|
return await reply.code(201).send()
|
||||||
}
|
}
|
||||||
@@ -458,7 +476,7 @@ export async function cleanupPlausibleLogs(request: FastifyRequest<OnlyId>, repl
|
|||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId: destinationDocker.id,
|
dockerId: destinationDocker.id,
|
||||||
command: `docker exec ${id}-clickhouse sh -c "/usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\""`
|
command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"`
|
||||||
})
|
})
|
||||||
return await reply.code(201).send()
|
return await reply.code(201).send()
|
||||||
}
|
}
|
||||||
@@ -471,9 +489,9 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
|||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const { ftpEnabled } = request.body;
|
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 ftpUser = cuid();
|
||||||
let ftpPassword = generatePassword({});
|
let ftpPassword = generatePassword({});
|
||||||
@@ -540,17 +558,14 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isRunning = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
|
const { found: isRunning } = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId: destinationDocker.id,
|
dockerId: destinationDocker.id,
|
||||||
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
|
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
console.log(error);
|
|
||||||
//
|
|
||||||
}
|
|
||||||
const volumes = [
|
const volumes = [
|
||||||
`${id}-wordpress-data:/home/${ftpUser}/wordpress`,
|
`${id}-wordpress-data:/home/${ftpUser}/wordpress`,
|
||||||
`${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
|
`${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
|
||||||
@@ -629,9 +644,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
|||||||
await asyncExecShell(
|
await asyncExecShell(
|
||||||
`rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
|
`rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) { }
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
} from './handlers';
|
} from './handlers';
|
||||||
|
|
||||||
import type { OnlyId } from '../../../../types';
|
import type { OnlyId } from '../../../../types';
|
||||||
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types';
|
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
|
||||||
import { startService, stopService } from '../../../../lib/services/handlers';
|
import { startService, stopService } from '../../../../lib/services/handlers';
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
@@ -71,7 +71,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
|
|
||||||
fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request));
|
fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request));
|
||||||
fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request));
|
fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request));
|
||||||
fastify.post<ServiceStartStop & SetWordpressSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));
|
fastify.post<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));
|
||||||
|
|
||||||
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
|
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
|
||||||
fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply));
|
fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply));
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ export interface ActivateWordpressFtp extends OnlyId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SetGlitchTipSettings extends OnlyId {
|
||||||
|
Body: {
|
||||||
|
enableOpenUserRegistration: boolean,
|
||||||
|
emailSmtpUseSsl: boolean,
|
||||||
|
emailSmtpUseTls: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
fqdn,
|
fqdn,
|
||||||
|
isAPIDebuggingEnabled,
|
||||||
isRegistrationEnabled,
|
isRegistrationEnabled,
|
||||||
dualCerts,
|
dualCerts,
|
||||||
minPort,
|
minPort,
|
||||||
@@ -39,7 +40,7 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
|
|||||||
const { id } = await listSettings();
|
const { id } = await listSettings();
|
||||||
await prisma.setting.update({
|
await prisma.setting.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers }
|
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled }
|
||||||
});
|
});
|
||||||
if (fqdn) {
|
if (fqdn) {
|
||||||
await prisma.setting.update({ where: { id }, data: { fqdn } });
|
await prisma.setting.update({ where: { id }, data: { fqdn } });
|
||||||
@@ -57,7 +58,7 @@ export async function deleteDomain(request: FastifyRequest<DeleteDomain>, reply:
|
|||||||
const { fqdn } = request.body
|
const { fqdn } = request.body
|
||||||
const { DNSServers } = await listSettings();
|
const { DNSServers } = await listSettings();
|
||||||
if (DNSServers) {
|
if (DNSServers) {
|
||||||
dns.setServers([DNSServers]);
|
dns.setServers([...DNSServers.split(',')]);
|
||||||
}
|
}
|
||||||
let ip;
|
let ip;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { OnlyId } from "../../../../types"
|
|||||||
export interface SaveSettings {
|
export interface SaveSettings {
|
||||||
Body: {
|
Body: {
|
||||||
fqdn: string,
|
fqdn: string,
|
||||||
|
isAPIDebuggingEnabled: boolean,
|
||||||
isRegistrationEnabled: boolean,
|
isRegistrationEnabled: boolean,
|
||||||
dualCerts: boolean,
|
dualCerts: boolean,
|
||||||
minPort: number,
|
minPort: number,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common";
|
import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
|
||||||
import { checkContainer, removeContainer } from "../../../lib/docker";
|
import { checkContainer, removeContainer } from "../../../lib/docker";
|
||||||
import { scheduler } from "../../../lib/scheduler";
|
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
|
||||||
import { getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
|
|
||||||
|
|
||||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import type { GitHubEvents, InstallGithub } from "./types";
|
import type { GitHubEvents, InstallGithub } from "./types";
|
||||||
@@ -67,7 +66,6 @@ export async function configureGitHubApp(request, reply) {
|
|||||||
}
|
}
|
||||||
export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> {
|
export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const buildId = cuid();
|
|
||||||
const allowedGithubEvents = ['push', 'pull_request'];
|
const allowedGithubEvents = ['push', 'pull_request'];
|
||||||
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
|
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
|
||||||
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
|
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
|
||||||
@@ -87,126 +85,170 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
|||||||
if (!projectId || !branch) {
|
if (!projectId || !branch) {
|
||||||
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
|
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
|
||||||
}
|
}
|
||||||
const applicationFound = await getApplicationFromDBWebhook(projectId, branch);
|
const applicationsFound = await getApplicationFromDBWebhook(projectId, branch);
|
||||||
if (applicationFound) {
|
if (applicationsFound && applicationsFound.length > 0) {
|
||||||
const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret || null;
|
for (const application of applicationsFound) {
|
||||||
//@ts-ignore
|
const buildId = cuid();
|
||||||
const hmac = crypto.createHmac('sha256', webhookSecret);
|
const webhookSecret = application.gitSource.githubApp.webhookSecret || null;
|
||||||
const digest = Buffer.from(
|
|
||||||
'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
if (!isDev) {
|
|
||||||
const checksum = Buffer.from(githubSignature, 'utf8');
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
|
const hmac = crypto.createHmac('sha256', webhookSecret);
|
||||||
throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' }
|
const digest = Buffer.from(
|
||||||
};
|
'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'),
|
||||||
}
|
'utf8'
|
||||||
|
);
|
||||||
|
if (!isDev) {
|
||||||
if (githubEvent === 'push') {
|
const checksum = Buffer.from(githubSignature, 'utf8');
|
||||||
if (!applicationFound.configHash) {
|
//@ts-ignore
|
||||||
const configHash = crypto
|
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
|
||||||
//@ts-ignore
|
throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' }
|
||||||
.createHash('sha256')
|
};
|
||||||
.update(
|
|
||||||
JSON.stringify({
|
|
||||||
buildPack: applicationFound.buildPack,
|
|
||||||
port: applicationFound.port,
|
|
||||||
exposePort: applicationFound.exposePort,
|
|
||||||
installCommand: applicationFound.installCommand,
|
|
||||||
buildCommand: applicationFound.buildCommand,
|
|
||||||
startCommand: applicationFound.startCommand
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.digest('hex');
|
|
||||||
await prisma.application.updateMany({
|
|
||||||
where: { branch, projectId },
|
|
||||||
data: { configHash }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await prisma.application.update({
|
|
||||||
where: { id: applicationFound.id },
|
|
||||||
data: { updatedAt: new Date() }
|
|
||||||
});
|
|
||||||
await prisma.build.create({
|
|
||||||
data: {
|
|
||||||
id: buildId,
|
|
||||||
applicationId: applicationFound.id,
|
|
||||||
destinationDockerId: applicationFound.destinationDocker.id,
|
|
||||||
gitSourceId: applicationFound.gitSource.id,
|
|
||||||
githubAppId: applicationFound.gitSource.githubApp?.id,
|
|
||||||
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
|
|
||||||
status: 'queued',
|
|
||||||
type: 'webhook_commit'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
message: 'Queued. Thank you!'
|
|
||||||
};
|
|
||||||
} else if (githubEvent === 'pull_request') {
|
|
||||||
const pullmergeRequestId = body.number.toString();
|
|
||||||
const pullmergeRequestAction = body.action;
|
|
||||||
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
|
|
||||||
if (!allowedActions.includes(pullmergeRequestAction)) {
|
|
||||||
throw { status: 500, message: 'Action not allowed.' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (applicationFound.settings.previews) {
|
if (githubEvent === 'push') {
|
||||||
if (applicationFound.destinationDockerId) {
|
if (!application.configHash) {
|
||||||
const isRunning = await checkContainer(
|
const configHash = crypto
|
||||||
{
|
//@ts-ignore
|
||||||
dockerId: applicationFound.destinationDocker.id,
|
.createHash('sha256')
|
||||||
container: applicationFound.id
|
.update(
|
||||||
}
|
JSON.stringify({
|
||||||
);
|
buildPack: application.buildPack,
|
||||||
if (!isRunning) {
|
port: application.port,
|
||||||
throw { status: 500, message: 'Application not running.' }
|
exposePort: application.exposePort,
|
||||||
}
|
installCommand: application.installCommand,
|
||||||
}
|
buildCommand: application.buildCommand,
|
||||||
if (
|
startCommand: application.startCommand
|
||||||
pullmergeRequestAction === 'opened' ||
|
})
|
||||||
pullmergeRequestAction === 'reopened' ||
|
)
|
||||||
pullmergeRequestAction === 'synchronize'
|
.digest('hex');
|
||||||
) {
|
|
||||||
await prisma.application.update({
|
await prisma.application.update({
|
||||||
where: { id: applicationFound.id },
|
where: { id: application.id },
|
||||||
data: { updatedAt: new Date() }
|
data: { configHash }
|
||||||
});
|
});
|
||||||
await prisma.build.create({
|
|
||||||
data: {
|
|
||||||
id: buildId,
|
|
||||||
pullmergeRequestId,
|
|
||||||
sourceBranch,
|
|
||||||
applicationId: applicationFound.id,
|
|
||||||
destinationDockerId: applicationFound.destinationDocker.id,
|
|
||||||
gitSourceId: applicationFound.gitSource.id,
|
|
||||||
githubAppId: applicationFound.gitSource.githubApp?.id,
|
|
||||||
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
|
|
||||||
status: 'queued',
|
|
||||||
type: 'webhook_pr'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: 'Queued. Thank you!'
|
|
||||||
};
|
|
||||||
} else if (pullmergeRequestAction === 'closed') {
|
|
||||||
if (applicationFound.destinationDockerId) {
|
|
||||||
const id = `${applicationFound.id}-${pullmergeRequestId}`;
|
|
||||||
await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
message: 'Removed preview. Thank you!'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw { status: 500, message: 'Pull request previews are not enabled.' }
|
await prisma.application.update({
|
||||||
|
where: { id: application.id },
|
||||||
|
data: { updatedAt: new Date() }
|
||||||
|
});
|
||||||
|
await prisma.build.create({
|
||||||
|
data: {
|
||||||
|
id: buildId,
|
||||||
|
applicationId: application.id,
|
||||||
|
destinationDockerId: application.destinationDocker.id,
|
||||||
|
gitSourceId: application.gitSource.id,
|
||||||
|
githubAppId: application.gitSource.githubApp?.id,
|
||||||
|
gitlabAppId: application.gitSource.gitlabApp?.id,
|
||||||
|
status: 'queued',
|
||||||
|
type: 'webhook_commit'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`Webhook for ${application.name} queued.`)
|
||||||
|
|
||||||
|
} else if (githubEvent === 'pull_request') {
|
||||||
|
const pullmergeRequestId = body.number.toString();
|
||||||
|
const pullmergeRequestAction = body.action;
|
||||||
|
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
|
||||||
|
if (!allowedActions.includes(pullmergeRequestAction)) {
|
||||||
|
throw { status: 500, message: 'Action not allowed.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (application.settings.previews) {
|
||||||
|
if (application.destinationDockerId) {
|
||||||
|
const { found: isRunning } = await checkContainer(
|
||||||
|
{
|
||||||
|
dockerId: application.destinationDocker.id,
|
||||||
|
container: application.id
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!isRunning) {
|
||||||
|
throw { status: 500, message: 'Application not running.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
pullmergeRequestAction === 'opened' ||
|
||||||
|
pullmergeRequestAction === 'reopened' ||
|
||||||
|
pullmergeRequestAction === 'synchronize'
|
||||||
|
) {
|
||||||
|
|
||||||
|
await prisma.application.update({
|
||||||
|
where: { id: application.id },
|
||||||
|
data: { updatedAt: new Date() }
|
||||||
|
});
|
||||||
|
let previewApplicationId = undefined
|
||||||
|
if (pullmergeRequestId) {
|
||||||
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
|
if (foundPreviewApplications.length > 0) {
|
||||||
|
previewApplicationId = foundPreviewApplications[0].id
|
||||||
|
} else {
|
||||||
|
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
|
||||||
|
const previewApplication = await prisma.previewApplication.create({
|
||||||
|
data: {
|
||||||
|
pullmergeRequestId,
|
||||||
|
sourceBranch,
|
||||||
|
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
|
||||||
|
application: { connect: { id: application.id } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
previewApplicationId = previewApplication.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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,
|
||||||
|
pullmergeRequestId,
|
||||||
|
previewApplicationId,
|
||||||
|
sourceBranch,
|
||||||
|
applicationId: application.id,
|
||||||
|
destinationDockerId: application.destinationDocker.id,
|
||||||
|
gitSourceId: application.gitSource.id,
|
||||||
|
githubAppId: application.gitSource.githubApp?.id,
|
||||||
|
gitlabAppId: application.gitSource.gitlabApp?.id,
|
||||||
|
status: 'queued',
|
||||||
|
type: 'webhook_pr'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Queued. Thank you!'
|
||||||
|
};
|
||||||
|
} else if (pullmergeRequestAction === 'closed') {
|
||||||
|
if (application.destinationDockerId) {
|
||||||
|
const id = `${application.id}-${pullmergeRequestId}`;
|
||||||
|
try {
|
||||||
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
|
if (foundPreviewApplications.length > 0) {
|
||||||
|
for (const preview of foundPreviewApplications) {
|
||||||
|
await prisma.previewApplication.delete({ where: { id: preview.id } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message: 'PR closed. Thank you!'
|
||||||
|
};
|
||||||
|
// if (application?.connectedDatabase?.databaseId) {
|
||||||
|
// const databaseId = application.connectedDatabase.databaseId;
|
||||||
|
// const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
||||||
|
// if (database) {
|
||||||
|
// await removeBranchDatabase(database, pullmergeRequestId);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw { status: 500, message: 'Not handled event.' }
|
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import axios from "axios";
|
|||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { errorHandler, getAPIUrl, isDev, listSettings, prisma } from "../../../lib/common";
|
import { errorHandler, getAPIUrl, getDomain, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
|
||||||
import { checkContainer, removeContainer } from "../../../lib/docker";
|
import { checkContainer, removeContainer } from "../../../lib/docker";
|
||||||
import { scheduler } from "../../../lib/scheduler";
|
|
||||||
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
|
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
|
||||||
|
|
||||||
import type { ConfigureGitLabApp, GitLabEvents } from "./types";
|
import type { ConfigureGitLabApp, GitLabEvents } from "./types";
|
||||||
@@ -30,7 +29,7 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
|
|||||||
});
|
});
|
||||||
const { data } = await axios.post(`${htmlUrl}/oauth/token`, params)
|
const { data } = await axios.post(`${htmlUrl}/oauth/token`, params)
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
return reply.redirect(`${getAPIUrl()}/webhooks/success?token=${data.access_token}`)
|
return reply.redirect(`${getUIUrl()}/webhooks/success?token=${data.access_token}`)
|
||||||
}
|
}
|
||||||
return reply.redirect(`/webhooks/success?token=${data.access_token}`)
|
return reply.redirect(`/webhooks/success?token=${data.access_token}`)
|
||||||
} catch ({ status, message, ...other }) {
|
} catch ({ status, message, ...other }) {
|
||||||
@@ -40,63 +39,60 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
|
|||||||
export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
||||||
const { object_kind: objectKind, ref, project_id } = request.body
|
const { object_kind: objectKind, ref, project_id } = request.body
|
||||||
try {
|
try {
|
||||||
const buildId = cuid();
|
|
||||||
|
|
||||||
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
|
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
|
||||||
|
|
||||||
const webhookToken = request.headers['x-gitlab-token'];
|
const webhookToken = request.headers['x-gitlab-token'];
|
||||||
if (!webhookToken) {
|
if (!webhookToken && !isDev) {
|
||||||
throw { status: 500, message: 'Invalid webhookToken.' }
|
throw { status: 500, message: 'Invalid webhookToken.' }
|
||||||
}
|
}
|
||||||
if (objectKind === 'push') {
|
if (objectKind === 'push') {
|
||||||
const projectId = Number(project_id);
|
const projectId = Number(project_id);
|
||||||
const branch = ref.split('/')[2];
|
const branch = ref.split('/')[2];
|
||||||
const applicationFound = await getApplicationFromDBWebhook(projectId, branch);
|
const applicationsFound = await getApplicationFromDBWebhook(projectId, branch);
|
||||||
if (applicationFound) {
|
if (applicationsFound && applicationsFound.length > 0) {
|
||||||
if (!applicationFound.configHash) {
|
for (const application of applicationsFound) {
|
||||||
const configHash = crypto
|
const buildId = cuid();
|
||||||
.createHash('sha256')
|
if (!application.configHash) {
|
||||||
.update(
|
const configHash = crypto
|
||||||
JSON.stringify({
|
.createHash('sha256')
|
||||||
buildPack: applicationFound.buildPack,
|
.update(
|
||||||
port: applicationFound.port,
|
JSON.stringify({
|
||||||
exposePort: applicationFound.exposePort,
|
buildPack: application.buildPack,
|
||||||
installCommand: applicationFound.installCommand,
|
port: application.port,
|
||||||
buildCommand: applicationFound.buildCommand,
|
exposePort: application.exposePort,
|
||||||
startCommand: applicationFound.startCommand
|
installCommand: application.installCommand,
|
||||||
})
|
buildCommand: application.buildCommand,
|
||||||
)
|
startCommand: application.startCommand
|
||||||
.digest('hex');
|
})
|
||||||
await prisma.application.updateMany({
|
)
|
||||||
where: { branch, projectId },
|
.digest('hex');
|
||||||
data: { configHash }
|
await prisma.application.update({
|
||||||
|
where: { id: application.id },
|
||||||
|
data: { configHash }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await prisma.application.update({
|
||||||
|
where: { id: application.id },
|
||||||
|
data: { updatedAt: new Date() }
|
||||||
|
});
|
||||||
|
await prisma.build.create({
|
||||||
|
data: {
|
||||||
|
id: buildId,
|
||||||
|
applicationId: application.id,
|
||||||
|
destinationDockerId: application.destinationDocker.id,
|
||||||
|
gitSourceId: application.gitSource.id,
|
||||||
|
githubAppId: application.gitSource.githubApp?.id,
|
||||||
|
gitlabAppId: application.gitSource.gitlabApp?.id,
|
||||||
|
status: 'queued',
|
||||||
|
type: 'webhook_commit'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await prisma.application.update({
|
|
||||||
where: { id: applicationFound.id },
|
|
||||||
data: { updatedAt: new Date() }
|
|
||||||
});
|
|
||||||
await prisma.build.create({
|
|
||||||
data: {
|
|
||||||
id: buildId,
|
|
||||||
applicationId: applicationFound.id,
|
|
||||||
destinationDockerId: applicationFound.destinationDocker.id,
|
|
||||||
gitSourceId: applicationFound.gitSource.id,
|
|
||||||
githubAppId: applicationFound.gitSource.githubApp?.id,
|
|
||||||
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
|
|
||||||
status: 'queued',
|
|
||||||
type: 'webhook_commit'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: 'Queued. Thank you!'
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} else if (objectKind === 'merge_request') {
|
} else if (objectKind === 'merge_request') {
|
||||||
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, iid: pullmergeRequestId }, project: { id } } = request.body
|
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body
|
||||||
|
const pullmergeRequestId = request.body.object_attributes.iid.toString();
|
||||||
const projectId = Number(id);
|
const projectId = Number(id);
|
||||||
if (!allowedActions.includes(action)) {
|
if (!allowedActions.includes(action)) {
|
||||||
throw { status: 500, message: 'Action not allowed.' }
|
throw { status: 500, message: 'Action not allowed.' }
|
||||||
@@ -105,64 +101,93 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
|||||||
throw { status: 500, message: 'Draft MR, do nothing.' }
|
throw { status: 500, message: 'Draft MR, do nothing.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationFound = await getApplicationFromDBWebhook(projectId, targetBranch);
|
const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch);
|
||||||
if (applicationFound) {
|
if (applicationsFound && applicationsFound.length > 0) {
|
||||||
if (applicationFound.settings.previews) {
|
for (const application of applicationsFound) {
|
||||||
if (applicationFound.destinationDockerId) {
|
const buildId = cuid();
|
||||||
const isRunning = await checkContainer(
|
if (application.settings.previews) {
|
||||||
{
|
if (application.destinationDockerId) {
|
||||||
dockerId: applicationFound.destinationDocker.id,
|
const { found: isRunning } = await checkContainer(
|
||||||
container: applicationFound.id
|
{
|
||||||
|
dockerId: application.destinationDocker.id,
|
||||||
|
container: application.id
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!isRunning) {
|
||||||
|
throw { status: 500, message: 'Application not running.' }
|
||||||
}
|
}
|
||||||
);
|
|
||||||
if (!isRunning) {
|
|
||||||
throw { status: 500, message: 'Application not running.' }
|
|
||||||
}
|
}
|
||||||
}
|
if (!isDev && application.gitSource.gitlabApp.webhookToken !== webhookToken) {
|
||||||
if (!isDev && applicationFound.gitSource.gitlabApp.webhookToken !== webhookToken) {
|
throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' }
|
||||||
throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' }
|
}
|
||||||
}
|
if (
|
||||||
if (
|
action === 'opened' ||
|
||||||
action === 'opened' ||
|
action === 'reopen' ||
|
||||||
action === 'reopen' ||
|
action === 'open' ||
|
||||||
action === 'open' ||
|
action === 'update'
|
||||||
action === 'update'
|
) {
|
||||||
) {
|
await prisma.application.update({
|
||||||
await prisma.application.update({
|
where: { id: application.id },
|
||||||
where: { id: applicationFound.id },
|
data: { updatedAt: new Date() }
|
||||||
data: { updatedAt: new Date() }
|
});
|
||||||
});
|
let previewApplicationId = undefined
|
||||||
await prisma.build.create({
|
if (pullmergeRequestId) {
|
||||||
data: {
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
id: buildId,
|
if (foundPreviewApplications.length > 0) {
|
||||||
pullmergeRequestId,
|
previewApplicationId = foundPreviewApplications[0].id
|
||||||
sourceBranch,
|
} else {
|
||||||
applicationId: applicationFound.id,
|
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
|
||||||
destinationDockerId: applicationFound.destinationDocker.id,
|
const previewApplication = await prisma.previewApplication.create({
|
||||||
gitSourceId: applicationFound.gitSource.id,
|
data: {
|
||||||
githubAppId: applicationFound.gitSource.githubApp?.id,
|
pullmergeRequestId,
|
||||||
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
|
sourceBranch,
|
||||||
status: 'queued',
|
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
|
||||||
type: 'webhook_mr'
|
application: { connect: { id: application.id } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
previewApplicationId = previewApplication.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
await prisma.build.create({
|
||||||
return {
|
data: {
|
||||||
message: 'Queued. Thank you!'
|
id: buildId,
|
||||||
};
|
pullmergeRequestId,
|
||||||
} else if (action === 'close') {
|
previewApplicationId,
|
||||||
if (applicationFound.destinationDockerId) {
|
sourceBranch,
|
||||||
const id = `${applicationFound.id}-${pullmergeRequestId}`;
|
applicationId: application.id,
|
||||||
await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
|
destinationDockerId: application.destinationDocker.id,
|
||||||
|
gitSourceId: application.gitSource.id,
|
||||||
|
githubAppId: application.gitSource.githubApp?.id,
|
||||||
|
gitlabAppId: application.gitSource.gitlabApp?.id,
|
||||||
|
status: 'queued',
|
||||||
|
type: 'webhook_mr'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message: 'Queued. Thank you!'
|
||||||
|
};
|
||||||
|
} else if (action === 'close') {
|
||||||
|
if (application.destinationDockerId) {
|
||||||
|
const id = `${application.id}-${pullmergeRequestId}`;
|
||||||
|
try {
|
||||||
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||||
|
if (foundPreviewApplications.length > 0) {
|
||||||
|
for (const preview of foundPreviewApplications) {
|
||||||
|
await prisma.previewApplication.delete({ where: { id: preview.id } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message: 'MR closed. Thank you!'
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
message: 'Removed preview. Thank you!'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw { status: 500, message: 'Merge request previews are not enabled.' }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw { status: 500, message: 'Not handled event.' }
|
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { errorHandler, getDomain, isDev, prisma, executeDockerCmd } from "../../
|
|||||||
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
|
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
|
||||||
import { includeServices } from "../../../lib/services/common";
|
import { includeServices } from "../../../lib/services/common";
|
||||||
import { TraefikOtherConfiguration } from "./types";
|
import { TraefikOtherConfiguration } from "./types";
|
||||||
|
import { OnlyId } from "../../../types";
|
||||||
|
|
||||||
function configureMiddleware(
|
function configureMiddleware(
|
||||||
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type },
|
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type },
|
||||||
@@ -11,7 +12,7 @@ function configureMiddleware(
|
|||||||
if (isHttps) {
|
if (isHttps) {
|
||||||
traefik.http.routers[id] = {
|
traefik.http.routers[id] = {
|
||||||
entrypoints: ['web'],
|
entrypoints: ['web'],
|
||||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
middlewares: ['redirect-to-https']
|
middlewares: ['redirect-to-https']
|
||||||
};
|
};
|
||||||
@@ -25,11 +26,34 @@ function configureMiddleware(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (type === 'appwrite') {
|
||||||
|
traefik.http.routers[`${id}-realtime`] = {
|
||||||
|
entrypoints: ['websecure'],
|
||||||
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/v1/realtime\`)`,
|
||||||
|
service: `${`${id}-realtime`}`,
|
||||||
|
tls: {
|
||||||
|
domains: {
|
||||||
|
main: `${domain}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
middlewares: []
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
traefik.http.services[`${id}-realtime`] = {
|
||||||
|
loadbalancer: {
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: `http://${container}-realtime:${port}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
if (isDualCerts) {
|
if (isDualCerts) {
|
||||||
traefik.http.routers[`${id}-secure`] = {
|
traefik.http.routers[`${id}-secure`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
certresolver: 'letsencrypt'
|
certresolver: 'letsencrypt'
|
||||||
@@ -40,7 +64,7 @@ function configureMiddleware(
|
|||||||
if (isWWW) {
|
if (isWWW) {
|
||||||
traefik.http.routers[`${id}-secure-www`] = {
|
traefik.http.routers[`${id}-secure-www`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`www.${nakedDomain}\`)`,
|
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
certresolver: 'letsencrypt'
|
certresolver: 'letsencrypt'
|
||||||
@@ -49,7 +73,7 @@ function configureMiddleware(
|
|||||||
};
|
};
|
||||||
traefik.http.routers[`${id}-secure`] = {
|
traefik.http.routers[`${id}-secure`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`${nakedDomain}\`)`,
|
rule: `Host(\`${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
domains: {
|
domains: {
|
||||||
@@ -62,7 +86,7 @@ function configureMiddleware(
|
|||||||
} else {
|
} else {
|
||||||
traefik.http.routers[`${id}-secure-www`] = {
|
traefik.http.routers[`${id}-secure-www`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`www.${nakedDomain}\`)`,
|
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
domains: {
|
domains: {
|
||||||
@@ -73,7 +97,7 @@ function configureMiddleware(
|
|||||||
};
|
};
|
||||||
traefik.http.routers[`${id}-secure`] = {
|
traefik.http.routers[`${id}-secure`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`${domain}\`)`,
|
rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
certresolver: 'letsencrypt'
|
certresolver: 'letsencrypt'
|
||||||
@@ -86,14 +110,14 @@ function configureMiddleware(
|
|||||||
} else {
|
} else {
|
||||||
traefik.http.routers[id] = {
|
traefik.http.routers[id] = {
|
||||||
entrypoints: ['web'],
|
entrypoints: ['web'],
|
||||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
middlewares: []
|
middlewares: []
|
||||||
};
|
};
|
||||||
|
|
||||||
traefik.http.routers[`${id}-secure`] = {
|
traefik.http.routers[`${id}-secure`] = {
|
||||||
entrypoints: ['websecure'],
|
entrypoints: ['websecure'],
|
||||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||||
service: `${id}`,
|
service: `${id}`,
|
||||||
tls: {
|
tls: {
|
||||||
domains: {
|
domains: {
|
||||||
@@ -112,6 +136,23 @@ function configureMiddleware(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (type === 'appwrite') {
|
||||||
|
traefik.http.routers[`${id}-realtime`] = {
|
||||||
|
entrypoints: ['web'],
|
||||||
|
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/v1/realtime\`)`,
|
||||||
|
service: `${id}-realtime`,
|
||||||
|
middlewares: []
|
||||||
|
};
|
||||||
|
traefik.http.services[`${id}-realtime`] = {
|
||||||
|
loadbalancer: {
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: `http://${container}-realtime:${port}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDualCerts) {
|
if (!isDualCerts) {
|
||||||
if (isWWW) {
|
if (isWWW) {
|
||||||
@@ -490,7 +531,7 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remoteTraefikConfiguration(request: FastifyRequest) {
|
export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
try {
|
try {
|
||||||
const traefik = {
|
const traefik = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
|
import { OnlyId } from '../../../types';
|
||||||
import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers';
|
import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers';
|
||||||
import { TraefikOtherConfiguration } from './types';
|
import { TraefikOtherConfiguration } from './types';
|
||||||
|
|
||||||
@@ -6,7 +7,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply));
|
fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply));
|
||||||
fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request));
|
fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request));
|
||||||
|
|
||||||
fastify.get('/remote/:id', async (request) => remoteTraefikConfiguration(request));
|
fastify.get<OnlyId>('/remote/:id', async (request) => remoteTraefikConfiguration(request));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default root;
|
export default root;
|
||||||
|
|||||||
@@ -1,39 +1,4 @@
|
|||||||
export interface OnlyId {
|
export interface OnlyId {
|
||||||
Params: { id: string },
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
apps/i18n/.env.example
Normal file
4
apps/i18n/.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
WEBLATE_INSTANCE_URL=http://localhost
|
||||||
|
WEBLATE_COMPONENT_NAME=coolify
|
||||||
|
WEBLATE_TOKEN=
|
||||||
|
TRANSLATION_DIR=
|
||||||
1
apps/i18n/.gitignore
vendored
Normal file
1
apps/i18n/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
locales/*
|
||||||
63
apps/i18n/index.mjs
Normal file
63
apps/i18n/index.mjs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config()
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import Gettext from 'node-gettext'
|
||||||
|
import { po } from 'gettext-parser'
|
||||||
|
import got from 'got';
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const weblateInstanceURL = process.env.WEBLATE_INSTANCE_URL;
|
||||||
|
const weblateComponentName = process.env.WEBLATE_COMPONENT_NAME
|
||||||
|
const token = process.env.WEBLATE_TOKEN;
|
||||||
|
|
||||||
|
const translationsDir = process.env.TRANSLATION_DIR;
|
||||||
|
const translationsPODir = './locales';
|
||||||
|
const locales = []
|
||||||
|
const domain = 'locale'
|
||||||
|
|
||||||
|
const translations = await got(`${weblateInstanceURL}/api/components/${weblateComponentName}/glossary/translations/?format=json`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Token ${token}`
|
||||||
|
}
|
||||||
|
}).json()
|
||||||
|
for (const translation of translations.results) {
|
||||||
|
const code = translation.language_code
|
||||||
|
locales.push(code)
|
||||||
|
|
||||||
|
const fileUrl = translation.file_url.replace('=json', '=po')
|
||||||
|
const file = await got(fileUrl, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Token ${token}`
|
||||||
|
}
|
||||||
|
}).text()
|
||||||
|
fs.writeFileSync(path.join(__dirname, translationsPODir, domain + '-' + code + '.po'), file)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const gt = new Gettext()
|
||||||
|
|
||||||
|
locales.forEach((locale) => {
|
||||||
|
let json = {}
|
||||||
|
const fileName = `${domain}-${locale}.po`
|
||||||
|
const translationsFilePath = path.join(translationsPODir, fileName)
|
||||||
|
const translationsContent = fs.readFileSync(translationsFilePath)
|
||||||
|
|
||||||
|
const parsedTranslations = po.parse(translationsContent)
|
||||||
|
const a = gt.gettext(parsedTranslations)
|
||||||
|
for (const [key, value] of Object.entries(a)) {
|
||||||
|
if (key === 'translations') {
|
||||||
|
for (const [key1, value1] of Object.entries(value)) {
|
||||||
|
if (key1 !== '') {
|
||||||
|
for (const [key2, value2] of Object.entries(value1)) {
|
||||||
|
json[value2.msgctxt] = value2.msgstr[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(`${translationsDir}/${locale}.json`, JSON.stringify(json))
|
||||||
|
})
|
||||||
15
apps/i18n/package.json
Normal file
15
apps/i18n/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "i18n-converter",
|
||||||
|
"description": "Convert Weblate translations to sveltekit-i18n",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"scripts": {
|
||||||
|
"translate": "node index.mjs"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"node-gettext": "3.0.0",
|
||||||
|
"gettext-parser": "6.0.0",
|
||||||
|
"got": "12.3.1",
|
||||||
|
"dotenv": "16.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,33 +14,39 @@
|
|||||||
"format": "prettier --write --plugin-search-dir=. ."
|
"format": "prettier --write --plugin-search-dir=. ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@floating-ui/dom": "1.0.1",
|
||||||
"@playwright/test": "1.25.1",
|
"@playwright/test": "1.25.1",
|
||||||
|
"@popperjs/core": "2.11.6",
|
||||||
"@sveltejs/kit": "1.0.0-next.405",
|
"@sveltejs/kit": "1.0.0-next.405",
|
||||||
"@types/js-cookie": "3.0.2",
|
"@types/js-cookie": "3.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.35.1",
|
"@typescript-eslint/eslint-plugin": "5.36.1",
|
||||||
"@typescript-eslint/parser": "5.35.1",
|
"@typescript-eslint/parser": "5.36.1",
|
||||||
"autoprefixer": "10.4.8",
|
"autoprefixer": "10.4.8",
|
||||||
"eslint": "8.22.0",
|
"classnames": "2.3.1",
|
||||||
|
"eslint": "8.23.0",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-svelte3": "4.0.0",
|
"eslint-plugin-svelte3": "4.0.0",
|
||||||
|
"flowbite": "1.5.2",
|
||||||
|
"flowbite-svelte": "0.26.2",
|
||||||
"postcss": "8.4.16",
|
"postcss": "8.4.16",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
"prettier-plugin-svelte": "2.7.0",
|
"prettier-plugin-svelte": "2.7.0",
|
||||||
"svelte": "3.49.0",
|
"svelte": "3.50.0",
|
||||||
"svelte-check": "2.8.1",
|
"svelte-check": "2.9.0",
|
||||||
"svelte-preprocess": "4.10.7",
|
"svelte-preprocess": "4.10.7",
|
||||||
"tailwindcss": "3.1.8",
|
"tailwindcss": "3.1.8",
|
||||||
"tailwindcss-scrollbar": "0.1.0",
|
"tailwindcss-scrollbar": "0.1.0",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "4.7.4",
|
"typescript": "4.8.2",
|
||||||
"vite": "3.0.5"
|
"vite": "3.1.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dayjs": "1.11.5",
|
||||||
"@sveltejs/adapter-static": "1.0.0-next.39",
|
"@sveltejs/adapter-static": "1.0.0-next.39",
|
||||||
"@tailwindcss/typography": "^0.5.4",
|
"@tailwindcss/typography": "^0.5.7",
|
||||||
"cuid": "2.1.8",
|
"cuid": "2.1.8",
|
||||||
"daisyui": "2.24.0",
|
"daisyui": "2.24.2",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"p-limit": "4.0.0",
|
"p-limit": "4.0.0",
|
||||||
"svelte-select": "4.4.7",
|
"svelte-select": "4.4.7",
|
||||||
|
|||||||
@@ -83,4 +83,8 @@ export function handlerNotFoundLoad(error: any, url: URL) {
|
|||||||
status: 500,
|
status: 500,
|
||||||
error: new Error(`Could not load ${url}`)
|
error: new Error(`Could not load ${url}`)
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRndInteger(min: number, max: number) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
||||||
32
apps/ui/src/lib/components/DocLink.svelte
Normal file
32
apps/ui/src/lib/components/DocLink.svelte
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Tooltip from './Tooltip.svelte';
|
||||||
|
export let url = 'https://docs.coollabs.io';
|
||||||
|
let id =
|
||||||
|
'cool-' +
|
||||||
|
url
|
||||||
|
.split('')
|
||||||
|
.map((c) => c.charCodeAt(0).toString(16).padStart(2, '0'))
|
||||||
|
.join('')
|
||||||
|
.slice(-16);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a {id} href={url} target="_blank" class="icons inline-block text-pink-500 cursor-pointer text-xs">
|
||||||
|
<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="M6 4h11a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-11a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1m3 0v18"
|
||||||
|
/>
|
||||||
|
<line x1="13" y1="8" x2="15" y2="8" />
|
||||||
|
<line x1="13" y1="12" x2="15" y2="12" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
|
||||||
@@ -1,6 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let text: string;
|
import { onMount } from 'svelte';
|
||||||
export let customClass = 'max-w-[24rem]';
|
|
||||||
|
import Tooltip from './Tooltip.svelte';
|
||||||
|
export let explanation = '';
|
||||||
|
let id: any;
|
||||||
|
let self: any;
|
||||||
|
onMount(() => {
|
||||||
|
id = `info-${self.offsetLeft}-${self.offsetTop}`;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-2 text-xs text-stone-400 {customClass}">{@html text}</div>
|
<div {id} class="inline-block mx-2 text-pink-500 cursor-pointer" bind:this={self}>
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="18"
|
||||||
|
shape-rendering="geometricPrecision"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="18"
|
||||||
|
><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path
|
||||||
|
d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
|
||||||
|
/><circle cx="12" cy="17" r=".5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{#if id}
|
||||||
|
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
import Explaner from './Explainer.svelte';
|
||||||
|
import Tooltip from './Tooltip.svelte';
|
||||||
|
|
||||||
|
export let id: any;
|
||||||
export let setting: any;
|
export let setting: any;
|
||||||
export let title: any;
|
export let title: any;
|
||||||
export let description: any;
|
export let description: any;
|
||||||
@@ -8,22 +10,17 @@
|
|||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let dataTooltip: any = null;
|
export let dataTooltip: any = null;
|
||||||
export let loading = false;
|
export let loading = false;
|
||||||
|
let triggeredBy = `#${id}`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center py-4 pr-8">
|
<div class="flex items-center py-4 pr-8">
|
||||||
<div class="flex w-96 flex-col">
|
<div class="flex w-96 flex-col">
|
||||||
<div class="text-xs font-bold text-stone-100 md:text-base">{title}</div>
|
<div class="text-xs font-bold text-stone-100 md:text-base">
|
||||||
<Explainer text={description} />
|
{title}<Explaner explanation={description} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class:text-center={isCenter} class="flex justify-center">
|
||||||
class:tooltip-right={dataTooltip}
|
|
||||||
class:tooltip-primary={dataTooltip}
|
|
||||||
class:tooltip={dataTooltip}
|
|
||||||
class:text-center={isCenter}
|
|
||||||
data-tip={dataTooltip}
|
|
||||||
class="flex justify-center"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
on:click
|
on:click
|
||||||
aria-pressed="false"
|
aria-pressed="false"
|
||||||
@@ -32,6 +29,7 @@
|
|||||||
class:bg-green-600={!loading && setting}
|
class:bg-green-600={!loading && setting}
|
||||||
class:bg-stone-700={!loading && !setting}
|
class:bg-stone-700={!loading && !setting}
|
||||||
class:bg-yellow-500={loading}
|
class:bg-yellow-500={loading}
|
||||||
|
{id}
|
||||||
>
|
>
|
||||||
<span class="sr-only">Use setting</span>
|
<span class="sr-only">Use setting</span>
|
||||||
<span
|
<span
|
||||||
@@ -72,3 +70,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if dataTooltip}
|
||||||
|
<Tooltip {triggeredBy} placement="top">{dataTooltip}</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|||||||
6
apps/ui/src/lib/components/SimpleExplainer.svelte
Normal file
6
apps/ui/src/lib/components/SimpleExplainer.svelte
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let text: string;
|
||||||
|
export let customClass = 'max-w-[24rem]';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-2 text-xs text-stone-400 {customClass}">{@html text}</div>
|
||||||
8
apps/ui/src/lib/components/Tooltip.svelte
Normal file
8
apps/ui/src/lib/components/Tooltip.svelte
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Tooltip } from 'flowbite-svelte';
|
||||||
|
export let placement = 'bottom';
|
||||||
|
export let color = 'bg-coollabs text-left';
|
||||||
|
export let triggeredBy = '#tooltip-default';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tooltip {triggeredBy} {placement} arrow={false} {color} style="custom"><slot /></Tooltip>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
import { addToast, appSession, features } from '$lib/store';
|
import { addToast, appSession, features } from '$lib/store';
|
||||||
import { asyncSleep, errorNotification } from '$lib/common';
|
import { asyncSleep, errorNotification } from '$lib/common';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import Tooltip from './Tooltip.svelte';
|
||||||
|
|
||||||
let isUpdateAvailable = false;
|
let isUpdateAvailable = false;
|
||||||
let updateStatus: any = {
|
let updateStatus: any = {
|
||||||
@@ -16,7 +17,6 @@
|
|||||||
updateStatus.loading = true;
|
updateStatus.loading = true;
|
||||||
try {
|
try {
|
||||||
if (dev) {
|
if (dev) {
|
||||||
console.log(`updating to ${latestVersion}`);
|
|
||||||
await asyncSleep(4000);
|
await asyncSleep(4000);
|
||||||
return window.location.reload();
|
return window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -76,14 +76,14 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-4 py-2">
|
<div class="py-2">
|
||||||
{#if $appSession.teamId === '0'}
|
{#if $appSession.teamId === '0'}
|
||||||
{#if isUpdateAvailable}
|
{#if isUpdateAvailable}
|
||||||
<button
|
<button
|
||||||
|
id="update"
|
||||||
disabled={updateStatus.success === false}
|
disabled={updateStatus.success === false}
|
||||||
on:click={update}
|
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"
|
class="icons bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
|
||||||
data-tip="Update Available!"
|
|
||||||
>
|
>
|
||||||
{#if updateStatus.loading}
|
{#if updateStatus.loading}
|
||||||
<svg
|
<svg
|
||||||
@@ -184,6 +184,7 @@
|
|||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</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}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
export let server: any;
|
||||||
let usage = {
|
let usage = {
|
||||||
cpu: {
|
cpu: {
|
||||||
load: [0, 0, 0],
|
load: [0, 0, 0],
|
||||||
@@ -20,8 +21,7 @@
|
|||||||
let usageInterval: any;
|
let usageInterval: any;
|
||||||
let loading = {
|
let loading = {
|
||||||
usage: false,
|
usage: false,
|
||||||
cleanup: false,
|
cleanup: false
|
||||||
restart: false
|
|
||||||
};
|
};
|
||||||
import { addToast, appSession } from '$lib/store';
|
import { addToast, appSession } from '$lib/store';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
@@ -30,29 +30,11 @@
|
|||||||
async function getStatus() {
|
async function getStatus() {
|
||||||
if (loading.usage) return;
|
if (loading.usage) return;
|
||||||
loading.usage = true;
|
loading.usage = true;
|
||||||
const data = await get('/usage');
|
const data = await get(`/servers/usage/${server.id}?remoteEngine=${server.remoteEngine}`);
|
||||||
usage = data.usage;
|
usage = data.usage;
|
||||||
loading.usage = false;
|
loading.usage = false;
|
||||||
}
|
}
|
||||||
async function restartCoolify() {
|
|
||||||
const sure = confirm(
|
|
||||||
'Are you sure you would like to restart Coolify? Currently running deployments will be stopped and restarted.'
|
|
||||||
);
|
|
||||||
if (sure) {
|
|
||||||
loading.restart = true;
|
|
||||||
try {
|
|
||||||
await post(`/internal/restart`, {});
|
|
||||||
addToast({
|
|
||||||
type: 'success',
|
|
||||||
message: 'Coolify restarted successfully. It will take a moment.'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
} finally {
|
|
||||||
loading.restart = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
clearInterval(usageInterval);
|
clearInterval(usageInterval);
|
||||||
});
|
});
|
||||||
@@ -71,7 +53,7 @@
|
|||||||
async function manuallyCleanupStorage() {
|
async function manuallyCleanupStorage() {
|
||||||
try {
|
try {
|
||||||
loading.cleanup = true;
|
loading.cleanup = true;
|
||||||
await post('/internal/cleanup', {});
|
await post('/internal/cleanup', { serverId: server.id });
|
||||||
return addToast({
|
return addToast({
|
||||||
message: 'Cleanup done.',
|
message: 'Cleanup done.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
@@ -84,45 +66,66 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full relative p-5 ">
|
||||||
<div class="flex lg:flex-row flex-col gap-4">
|
{#if loading.usage}
|
||||||
<h1 class="title lg:text-3xl">Hardware Details</h1>
|
<span class="indicator-item badge bg-yellow-500 badge-sm" />
|
||||||
<div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0">
|
{:else}
|
||||||
{#if $appSession.teamId === '0'}
|
<span class="indicator-item badge bg-success badge-sm" />
|
||||||
<button
|
{/if}
|
||||||
on:click={manuallyCleanupStorage}
|
{#if server.remoteEngine}
|
||||||
class:loading={loading.cleanup}
|
<div
|
||||||
class="btn btn-sm">Cleanup Storage</button
|
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"
|
||||||
>
|
>
|
||||||
<button
|
BETA
|
||||||
on:click={restartCoolify}
|
|
||||||
class:loading={loading.restart}
|
|
||||||
class="btn btn-sm bg-red-600 hover:bg-red-500">Restart Coolify</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</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
|
||||||
|
disabled={loading.cleanup}
|
||||||
|
on:click={manuallyCleanupStorage}
|
||||||
|
class:loading={loading.cleanup}
|
||||||
|
class:bg-coollabs={!loading.cleanup}
|
||||||
|
class="btn btn-sm">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>
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
<div class="grid grid-flow-col gap-4 grid-rows-3 justify-start lg:justify-center lg:grid-rows-1">
|
<div class="grid grid-flow-col gap-4 grid-rows-3 justify-start lg:justify-center lg:grid-rows-1">
|
||||||
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
|
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total Memory</div>
|
<div class="stat-title">Total Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{(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>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Used Memory</div>
|
<div class="stat-title">Used Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{(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>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Free Memory</div>
|
<div class="stat-title">Free Memory</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.memory.freeMemPercentage}<span class="text-sm">%</span>
|
{(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,42 +133,42 @@
|
|||||||
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
|
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total CPU</div>
|
<div class="stat-title">Total CPU</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.cpu.count}
|
{usage?.cpu?.count}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">CPU Usage</div>
|
<div class="stat-title">CPU Usage</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.cpu.usage}<span class="text-sm">%</span>
|
{usage?.cpu?.usage}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Load Average (5,10,30mins)</div>
|
<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 text-white">{usage?.cpu?.load}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
|
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Total Disk</div>
|
<div class="stat-title">Total Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk.totalGb}<span class="text-sm">GB</span>
|
{usage?.disk?.totalGb}<span class="text-sm">GB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Used Disk</div>
|
<div class="stat-title">Used Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk.usedGb}<span class="text-sm">GB</span>
|
{usage?.disk?.usedGb}<span class="text-sm">GB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-title">Free Disk</div>
|
<div class="stat-title">Free Disk</div>
|
||||||
<div class="stat-value text-2xl">
|
<div class="stat-value text-2xl text-white">
|
||||||
{usage?.disk.freePercentage}<span class="text-sm">%</span>
|
{usage?.disk?.freePercentage}<span class="text-sm">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,4 +16,6 @@
|
|||||||
<Icons.Redis {isAbsolute} />
|
<Icons.Redis {isAbsolute} />
|
||||||
{:else if type === 'couchdb'}
|
{:else if type === 'couchdb'}
|
||||||
<Icons.CouchDB {isAbsolute} />
|
<Icons.CouchDB {isAbsolute} />
|
||||||
|
{:else if type === 'edgedb'}
|
||||||
|
<Icons.EdgeDB {isAbsolute} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
22
apps/ui/src/lib/components/svg/databases/EdgeDB.svelte
Normal file
22
apps/ui/src/lib/components/svg/databases/EdgeDB.svelte
Normal 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
|
||||||
|
>
|
||||||
@@ -6,5 +6,6 @@ export { default as MongoDB } from './MongoDB.svelte';
|
|||||||
export { default as MySQL } from './MySQL.svelte';
|
export { default as MySQL } from './MySQL.svelte';
|
||||||
export { default as PostgreSQL } from './PostgreSQL.svelte';
|
export { default as PostgreSQL } from './PostgreSQL.svelte';
|
||||||
export { default as Redis } from './Redis.svelte';
|
export { default as Redis } from './Redis.svelte';
|
||||||
|
export { default as EdgeDB } from './EdgeDB.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 127 74"
|
viewBox="0 0 127 74"
|
||||||
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'}
|
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
><path
|
><path
|
||||||
d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z"
|
d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z"
|
||||||
|
|||||||
@@ -40,4 +40,6 @@
|
|||||||
<Icons.GlitchTip {isAbsolute} />
|
<Icons.GlitchTip {isAbsolute} />
|
||||||
{:else if type === 'searxng'}
|
{:else if type === 'searxng'}
|
||||||
<Icons.Searxng {isAbsolute} />
|
<Icons.Searxng {isAbsolute} />
|
||||||
|
{:else if type === 'weblate'}
|
||||||
|
<Icons.Weblate {isAbsolute} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
61
apps/ui/src/lib/components/svg/services/Weblate.svelte
Normal file
61
apps/ui/src/lib/components/svg/services/Weblate.svelte
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let isAbsolute = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-12 h-12 mx-auto'}
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 300 300"
|
||||||
|
><linearGradient
|
||||||
|
id="a"
|
||||||
|
x1=".3965"
|
||||||
|
x2="98.808"
|
||||||
|
y1="55.253"
|
||||||
|
y2="55.253"
|
||||||
|
gradientTransform="scale(.98308 1.0172)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
><stop stop-color="#00d2e6" offset="0" /><stop
|
||||||
|
stop-color="#2eccaa"
|
||||||
|
offset="1"
|
||||||
|
/></linearGradient
|
||||||
|
><linearGradient
|
||||||
|
id="b"
|
||||||
|
x1="49.017"
|
||||||
|
x2="99.793"
|
||||||
|
y1="137.89"
|
||||||
|
y2="113.96"
|
||||||
|
gradientTransform="scale(1.1631 .8598)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
><stop stop-opacity="0" offset="0" /><stop offset=".51413" /><stop
|
||||||
|
stop-opacity="0"
|
||||||
|
offset="1"
|
||||||
|
/></linearGradient
|
||||||
|
><linearGradient
|
||||||
|
id="c"
|
||||||
|
x1="201.82"
|
||||||
|
x2="103.58"
|
||||||
|
y1="57.649"
|
||||||
|
y2="57.649"
|
||||||
|
gradientTransform="scale(.98308 1.0172)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
><stop stop-color="#1fa385" offset="0" /><stop
|
||||||
|
stop-color="#2eccaa"
|
||||||
|
offset="1"
|
||||||
|
/></linearGradient
|
||||||
|
><g transform="translate(50,76)" fill-rule="evenodd"
|
||||||
|
><path
|
||||||
|
d="m127.25 111.61c-2.8884-0.0145-5.7666-0.6024-8.4797-1.7847-6.1117-2.6626-11.493-7.6912-15.872-14.495 1.2486-2.2193 2.3738-4.5173 3.3784-6.8535 4.4051-10.243 6.5-21.46 6.6607-32.593-0.0233-0.22082-0.0416-0.44244-0.0552-0.66483l-0.0121-0.57132c-0.01-4.3654-0.67459-8.7898-2.1767-12.909-1.7304-4.7458-4.4887-9.4955-8.865-11.348-0.79519-0.33595-1.6316-0.47701-2.4642-0.45737-5.5049-10.289-5.6799-20.149 0-29.537 0.10115 0 0.20619 3.9293e-4 0.30734 0.001179 6.7012 0.07387 13.34 2.1418 19.021 5.7536 15.469 9.835 23.182 29.001 23.352 47.818 2e-3 0.22083-3.9e-4 0.44126-7e-3 0.66169h0.0868c-0.0226 19.887-4.8049 40.054-14.875 56.979zm-34.3 31.216c-14.448 5.9425-31.228 5.6236-45.549-1.025-16.476-7.6476-29.065-22.512-36.818-39.479-13.262-29.022-13.566-63.715-0.98815-93.182 9.4458 3.7788 17.845-2.2397 17.845-2.2397s-0.01945 9.2605 8.9478 13.905c-9.2007 21.556-8.979 47.167 0.2412 68.173 4.4389 10.107 11.22 19.519 20.619 24.842 3.3547 1.8996 7.041 3.126 10.833 3.5862 0.01404 0.0219 0.02808 0.0439 0.04214 0.0658 6.6965 10.449 15.132 19.157 24.828 25.354z"
|
||||||
|
fill="url(#a)"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
/><path
|
||||||
|
d="m127.24 111.61c-2.8869-0.0151-5.7636-0.60296-8.4755-1.7846-6.1127-2.663-11.495-7.6928-15.874-14.498 1.2494-2.2205 2.3754-4.5198 3.3806-6.8572 1.3282-3.0884 2.4463-6.2648 3.3644-9.501 2.128-7.4978 30.382 2.0181 26.072 14.371-2.2239 6.373-5.0394 12.509-8.4675 18.27zm-34.302 31.212c-14.446 5.9396-31.224 5.6198-45.543-1.0278-16.476-7.6476 0.44739-33.303 9.8465-27.981 3.3533 1.8988 7.0378 3.125 10.828 3.5856 0.01567 0.0245 0.03135 0.049 0.04704 0.0735 6.695 10.447 15.128 19.153 24.821 25.349z"
|
||||||
|
fill="url(#b)"
|
||||||
|
opacity=".3"
|
||||||
|
/><path
|
||||||
|
d="m56.762 54.628c-0.0066-0.22043-0.0093-0.44086-7e-3 -0.66169 0.17001-18.817 7.8827-37.983 23.352-47.818 5.6811-3.6118 12.32-5.6798 19.021-5.7536 0.10115-7.8585e-4 0.20619-0.001179 0.30734-0.001179v29.537c-0.83254-0.01965-1.669 0.12141-2.4642 0.45737-4.3763 1.8523-7.1345 6.602-8.865 11.348-1.5021 4.1191-2.1669 8.5434-2.1767 12.909l-0.01206 0.57132c-0.01362 0.2224-0.0319 0.44401-0.05524 0.66483 0.16067 11.134 2.2556 22.35 6.6607 32.593 4.9334 11.472 12.775 22.025 23.847 26.849 8.3526 3.6397 17.612 2.7811 25.182-1.5057 9.3991-5.3226 16.18-14.734 20.619-24.842 9.2202-21.006 9.4419-46.617 0.24121-68.173 8.9673-4.6444 8.9478-13.905 8.9478-13.905s8.3993 6.0185 17.845 2.2397c12.578 29.466 12.274 64.16-0.98815 93.182-7.7535 16.967-20.343 31.831-36.818 39.479-14.667 6.809-31.913 6.9792-46.591 0.58389-13.19-5.7489-23.918-16.106-31.637-28.15-11.179-17.443-16.472-38.678-16.496-59.604z"
|
||||||
|
fill="url(#c)"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
/></g
|
||||||
|
></svg
|
||||||
|
>
|
||||||
@@ -16,4 +16,5 @@ export { default as Fider } from './Fider.svelte';
|
|||||||
export { default as Appwrite } from './Appwrite.svelte';
|
export { default as Appwrite } from './Appwrite.svelte';
|
||||||
export { default as Moodle } from './Moodle.svelte';
|
export { default as Moodle } from './Moodle.svelte';
|
||||||
export { default as GlitchTip } from './GlitchTip.svelte';
|
export { default as GlitchTip } from './GlitchTip.svelte';
|
||||||
export { default as Searxng } from './Searxng.svelte';
|
export { default as Searxng } from './Searxng.svelte';
|
||||||
|
export { default as Weblate } from './Weblate.svelte';
|
||||||
7
apps/ui/src/lib/dayjs.ts
Normal file
7
apps/ui/src/lib/dayjs.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import utc from 'dayjs/plugin/utc.js';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export { dayjs as day };
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
"expose_a_port": "Expose a port",
|
"expose_a_port": "Expose a port",
|
||||||
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
|
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
|
||||||
"debug_logs": "Debug Logs",
|
"debug_logs": "Debug Logs",
|
||||||
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs.",
|
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-settings font-bold'>Sensitive information</span> could be visible and saved in logs.",
|
||||||
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
|
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
|
||||||
"no_applications_found": "No applications found",
|
"no_applications_found": "No applications found",
|
||||||
"secret__batch_dot_env": "Paste .env file",
|
"secret__batch_dot_env": "Paste .env file",
|
||||||
@@ -275,7 +275,7 @@
|
|||||||
"application_id": "Application ID",
|
"application_id": "Application ID",
|
||||||
"group_name": "Group Name",
|
"group_name": "Group Name",
|
||||||
"oauth_id": "OAuth ID",
|
"oauth_id": "OAuth ID",
|
||||||
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application.",
|
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-settings' >in the URL</span> of your GitLab OAuth Application.",
|
||||||
"register_oauth_gitlab": "Register new OAuth application on GitLab",
|
"register_oauth_gitlab": "Register new OAuth application on GitLab",
|
||||||
"gitlab": {
|
"gitlab": {
|
||||||
"self_hosted": "Instance-wide application (self-hosted)",
|
"self_hosted": "Instance-wide application (self-hosted)",
|
||||||
@@ -306,7 +306,7 @@
|
|||||||
"change_language": "Change Language",
|
"change_language": "Change Language",
|
||||||
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
|
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
|
||||||
"domain_removed": "Domain removed",
|
"domain_removed": "Domain removed",
|
||||||
"ssl_explainer": "If you specify <span class='text-settings font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings font-bold'>WARNING:</span> If you change an already set domain, it will brake webhooks and other integrations! You need to manually update them.",
|
"ssl_explainer": "If you specify <span class='text-settings font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings font-bold'>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.",
|
||||||
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
|
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
|
||||||
"registration_allowed": "Registration allowed?",
|
"registration_allowed": "Registration allowed?",
|
||||||
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
|
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import cuid from 'cuid';
|
|||||||
import { writable, readable, type Writable } from 'svelte/store';
|
import { writable, readable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
interface AppSession {
|
interface AppSession {
|
||||||
|
isRegistrationEnabled: boolean;
|
||||||
ipv4: string | null,
|
ipv4: string | null,
|
||||||
ipv6: string | null,
|
ipv6: string | null,
|
||||||
version: string | null,
|
version: string | null,
|
||||||
@@ -25,8 +26,11 @@ interface AddToast {
|
|||||||
message: string,
|
message: string,
|
||||||
timeout?: number | undefined
|
timeout?: number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const search: any = writable('')
|
||||||
export const loginEmail: Writable<string | undefined> = writable()
|
export const loginEmail: Writable<string | undefined> = writable()
|
||||||
export const appSession: Writable<AppSession> = writable({
|
export const appSession: Writable<AppSession> = writable({
|
||||||
|
isRegistrationEnabled: false,
|
||||||
ipv4: null,
|
ipv4: null,
|
||||||
ipv6: null,
|
ipv6: null,
|
||||||
version: null,
|
version: null,
|
||||||
@@ -45,10 +49,31 @@ export const appSession: Writable<AppSession> = writable({
|
|||||||
supportedServiceTypesAndVersions: []
|
supportedServiceTypesAndVersions: []
|
||||||
});
|
});
|
||||||
export const disabledButton: Writable<boolean> = writable(false);
|
export const disabledButton: Writable<boolean> = writable(false);
|
||||||
|
export const isDeploymentEnabled: Writable<boolean> = writable(false);
|
||||||
|
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
|
||||||
|
return (
|
||||||
|
isAdmin &&
|
||||||
|
(application.fqdn || application.settings.isBot) &&
|
||||||
|
application.gitSource &&
|
||||||
|
application.repository &&
|
||||||
|
application.destinationDocker &&
|
||||||
|
application.buildPack
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any) {
|
||||||
|
return (
|
||||||
|
isAdmin &&
|
||||||
|
service.fqdn &&
|
||||||
|
service.destinationDocker &&
|
||||||
|
service.version &&
|
||||||
|
service.type
|
||||||
|
);
|
||||||
|
}
|
||||||
export const status: Writable<any> = writable({
|
export const status: Writable<any> = writable({
|
||||||
application: {
|
application: {
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
isExited: false,
|
isExited: false,
|
||||||
|
isRestarting: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
initialLoading: true
|
initialLoading: true
|
||||||
},
|
},
|
||||||
@@ -62,7 +87,8 @@ export const status: Writable<any> = writable({
|
|||||||
isRunning: false,
|
isRunning: false,
|
||||||
isExited: false,
|
isExited: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
initialLoading: true
|
initialLoading: true,
|
||||||
|
isPublic: false
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -130,4 +156,6 @@ export const addToast = (toast: AddToast) => {
|
|||||||
let t: any = { ...defaults, ...toast }
|
let t: any = { ...defaults, ...toast }
|
||||||
if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout)
|
if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout)
|
||||||
toasts.update((all: any) => [t, ...all])
|
toasts.update((all: any) => [t, ...all])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const selectedBuildId: any = writable(null)
|
||||||
150
apps/ui/src/routes/_NewResource.svelte
Normal file
150
apps/ui/src/routes/_NewResource.svelte
Normal 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>
|
||||||
@@ -66,12 +66,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let baseSettings: any;
|
export let baseSettings: any;
|
||||||
export let supportedServiceTypesAndVersions: any;
|
export let supportedServiceTypesAndVersions: any;
|
||||||
|
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
|
||||||
$appSession.ipv4 = baseSettings.ipv4;
|
$appSession.ipv4 = baseSettings.ipv4;
|
||||||
$appSession.ipv6 = baseSettings.ipv6;
|
$appSession.ipv6 = baseSettings.ipv6;
|
||||||
$appSession.version = baseSettings.version;
|
$appSession.version = baseSettings.version;
|
||||||
$appSession.whiteLabeled = baseSettings.whiteLabeled;
|
$appSession.whiteLabeled = baseSettings.whiteLabeled;
|
||||||
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
|
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
|
||||||
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions
|
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
|
||||||
|
|
||||||
export let userId: string;
|
export let userId: string;
|
||||||
export let teamId: string;
|
export let teamId: string;
|
||||||
@@ -88,6 +89,7 @@
|
|||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import { appSession } from '$lib/store';
|
import { appSession } from '$lib/store';
|
||||||
import Toasts from '$lib/components/Toasts.svelte';
|
import Toasts from '$lib/components/Toasts.svelte';
|
||||||
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
|
||||||
if (userId) $appSession.userId = userId;
|
if (userId) $appSession.userId = userId;
|
||||||
if (teamId) $appSession.teamId = teamId;
|
if (teamId) $appSession.teamId = teamId;
|
||||||
@@ -132,16 +134,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
|
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
|
||||||
<a
|
<a
|
||||||
|
id="dashboard"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
href="/"
|
href="/"
|
||||||
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200 hover:text-white"
|
class="icons hover:text-white"
|
||||||
class:text-white={$page.url.pathname === '/'}
|
class:text-pink-500={$page.url.pathname === '/'}
|
||||||
class:bg-coolgray-500={$page.url.pathname === '/'}
|
class:bg-coolgray-500={$page.url.pathname === '/'}
|
||||||
data-tip="Dashboard"
|
class:bg-coolgray-200={!($page.url.pathname === '/')}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-8 w-8"
|
class="h-9 w-9"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -156,166 +159,53 @@
|
|||||||
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
|
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<div class="border-t border-stone-700" />
|
{#if $appSession.teamId === '0'}
|
||||||
|
<a
|
||||||
<a
|
id="servers"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
href="/applications"
|
href="/servers"
|
||||||
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200"
|
class="icons hover:text-white"
|
||||||
class:text-applications={$page.url.pathname.startsWith('/applications') ||
|
class:text-sky-500={$page.url.pathname === '/servers'}
|
||||||
$page.url.pathname.startsWith('/new/application')}
|
class:bg-coolgray-500={$page.url.pathname === '/servers'}
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
|
class:bg-coolgray-200={!($page.url.pathname === '/servers')}
|
||||||
$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"
|
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<svg
|
||||||
<rect x="4" y="4" width="6" height="6" rx="1" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<rect x="4" y="14" width="6" height="6" rx="1" />
|
class="w-8 h-8 mx-auto"
|
||||||
<rect x="14" y="14" width="6" height="6" rx="1" />
|
viewBox="0 0 24 24"
|
||||||
<line x1="14" y1="7" x2="20" y2="7" />
|
stroke-width="1.5"
|
||||||
<line x1="17" y1="4" x2="17" y2="10" />
|
stroke="currentColor"
|
||||||
</svg>
|
fill="none"
|
||||||
</a>
|
stroke-linecap="round"
|
||||||
<a
|
stroke-linejoin="round"
|
||||||
sveltekit:prefetch
|
>
|
||||||
href="/sources"
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200"
|
<rect x="3" y="4" width="18" height="8" rx="3" />
|
||||||
class:text-sources={$page.url.pathname.startsWith('/sources') ||
|
<rect x="3" y="12" width="18" height="8" rx="3" />
|
||||||
$page.url.pathname.startsWith('/new/source')}
|
<line x1="7" y1="8" x2="7" y2="8.01" />
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
|
<line x1="7" y1="16" x2="7" y2="16.01" />
|
||||||
$page.url.pathname.startsWith('/new/source')}
|
</svg>
|
||||||
data-tip="Git Sources"
|
</a>
|
||||||
>
|
{/if}
|
||||||
<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
|
|
||||||
sveltekit:prefetch
|
|
||||||
href="/destinations"
|
|
||||||
class="icons tooltip tooltip-primary tooltip-right 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
|
|
||||||
sveltekit:prefetch
|
|
||||||
href="/databases"
|
|
||||||
class="icons tooltip tooltip-primary tooltip-right 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
|
|
||||||
sveltekit:prefetch
|
|
||||||
href="/services"
|
|
||||||
class="icons tooltip tooltip-primary tooltip-right 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
|
||||||
|
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
|
|
||||||
<UpdateAvailable />
|
<UpdateAvailable />
|
||||||
<div class="flex flex-col space-y-2 py-2">
|
<div class="flex flex-col space-y-2 py-2">
|
||||||
<a
|
<a
|
||||||
|
id="iam"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
href="/iam"
|
href="/iam"
|
||||||
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200"
|
class="icons bg-coolgray-200"
|
||||||
class:text-iam={$page.url.pathname.startsWith('/iam')}
|
class:text-iam={$page.url.pathname.startsWith('/iam')}
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
|
class:bg-coolgray-500={$page.url.pathname === '/iam'}
|
||||||
data-tip="IAM"
|
class:bg-coolgray-200={!($page.url.pathname === '/iam')}
|
||||||
><svg
|
><svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-8 w-8"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
class="h-9 w-9"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -329,18 +219,21 @@
|
|||||||
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
id="settings"
|
||||||
sveltekit:prefetch
|
sveltekit:prefetch
|
||||||
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
|
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
|
||||||
class="icons tooltip tooltip-primary tooltip-right bg-coolgray-200"
|
class="icons bg-coolgray-200"
|
||||||
class:text-settings={$page.url.pathname.startsWith('/settings')}
|
class:text-settings={$page.url.pathname.startsWith('/settings')}
|
||||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
|
class:bg-coolgray-500={$page.url.pathname === '/settings'}
|
||||||
data-tip="Settings"
|
class:bg-coolgray-200={!($page.url.pathname === '/settings')}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-8 w-8"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
class="h-9 w-9"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -354,15 +247,14 @@
|
|||||||
<circle cx="12" cy="12" r="3" />
|
<circle cx="12" cy="12" r="3" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black"
|
||||||
<div
|
>Settings</Tooltip
|
||||||
class="icons tooltip tooltip-primary tooltip-right 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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="ml-1 h-7 w-7"
|
class="ml-1 h-8 w-8"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -377,6 +269,8 @@
|
|||||||
<path d="M7 12h14l-3 -3m0 6l3 -3" />
|
<path d="M7 12h14l-3 -3m0 6l3 -3" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
|
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
|
||||||
>
|
>
|
||||||
@@ -396,7 +290,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<main>
|
<main>
|
||||||
<div class="px-20">
|
<div class={$appSession.userId ? 'pl-14 lg:pl-20' : null}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
export let isNewSecret = false;
|
export let isNewSecret = false;
|
||||||
export let isPRMRSecret = false;
|
export let isPRMRSecret = false;
|
||||||
export let PRMRSecret: any = {};
|
export let PRMRSecret: any = {};
|
||||||
|
|
||||||
if (isPRMRSecret) value = PRMRSecret.value;
|
if (isPRMRSecret) value = PRMRSecret.value;
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
@@ -39,7 +38,15 @@
|
|||||||
|
|
||||||
async function createSecret(isNew: any) {
|
async function createSecret(isNew: any) {
|
||||||
try {
|
try {
|
||||||
if (!name || !value) return;
|
if (isNew) {
|
||||||
|
if (!name || !value) return;
|
||||||
|
}
|
||||||
|
if (value === undefined && isPRMRSecret) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value === '' && !isPRMRSecret) {
|
||||||
|
throw new Error('Value is required.')
|
||||||
|
}
|
||||||
await saveSecret({
|
await saveSecret({
|
||||||
isNew,
|
isNew,
|
||||||
name,
|
name,
|
||||||
@@ -65,7 +72,6 @@
|
|||||||
}
|
}
|
||||||
dispatch('refresh');
|
dispatch('refresh');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +115,6 @@
|
|||||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||||
isPasswordField={true}
|
isPasswordField={true}
|
||||||
bind:value
|
bind:value
|
||||||
required
|
|
||||||
placeholder="J$#@UIO%HO#$U%H"
|
placeholder="J$#@UIO%HO#$U%H"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -131,7 +136,7 @@
|
|||||||
class:translate-x-0={!isBuildSecret}
|
class:translate-x-0={!isBuildSecret}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
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-0={isBuildSecret}
|
||||||
class:opacity-100={!isBuildSecret}
|
class:opacity-100={!isBuildSecret}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
|
||||||
|
|
||||||
export let setting: any;
|
|
||||||
export let title: any;
|
|
||||||
export let description: any;
|
|
||||||
export let isCenter = true;
|
|
||||||
export let disabled = false;
|
|
||||||
export let dataTooltip: any = null;
|
|
||||||
export let loading = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex items-center py-4 pr-8">
|
|
||||||
<div class="flex w-96 flex-col">
|
|
||||||
<div class="text-xs font-bold text-stone-100 md:text-base">{title}</div>
|
|
||||||
<Explainer text={description} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class:tooltip={dataTooltip}
|
|
||||||
class:text-center={isCenter}
|
|
||||||
data-tip={dataTooltip}
|
|
||||||
class="flex justify-center"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
on:click
|
|
||||||
aria-pressed="false"
|
|
||||||
class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
|
|
||||||
class:opacity-50={disabled || loading}
|
|
||||||
class:bg-green-600={!loading && setting}
|
|
||||||
class:bg-stone-700={!loading && !setting}
|
|
||||||
class:bg-yellow-500={loading}
|
|
||||||
>
|
|
||||||
<span class="sr-only">Use setting</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={setting}
|
|
||||||
class:translate-x-0={!setting}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
|
||||||
class:opacity-0={setting}
|
|
||||||
class:opacity-100={!setting}
|
|
||||||
class:animate-spin={loading}
|
|
||||||
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={setting}
|
|
||||||
class:opacity-0={!setting}
|
|
||||||
class:animate-spin={loading}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -60,60 +60,90 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store';
|
import {
|
||||||
|
appSession,
|
||||||
|
status,
|
||||||
|
location,
|
||||||
|
setLocation,
|
||||||
|
addToast,
|
||||||
|
isDeploymentEnabled,
|
||||||
|
checkIfDeploymentEnabledApplications,
|
||||||
|
selectedBuildId
|
||||||
|
} from '$lib/store';
|
||||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||||
import Loading from '$lib/components/Loading.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
|
||||||
let loading = false;
|
|
||||||
let statusInterval: any;
|
let statusInterval: any;
|
||||||
$disabledButton =
|
let forceDelete = false;
|
||||||
!$appSession.isAdmin ||
|
|
||||||
(!application.fqdn && !application.settings.isBot) ||
|
|
||||||
!application.gitSource ||
|
|
||||||
!application.repository ||
|
|
||||||
!application.destinationDocker ||
|
|
||||||
!application.buildPack;
|
|
||||||
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
|
|
||||||
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
|
|
||||||
async function handleDeploySubmit(forceRebuild = false) {
|
async function handleDeploySubmit(forceRebuild = false) {
|
||||||
|
if (!$isDeploymentEnabled) return;
|
||||||
try {
|
try {
|
||||||
const { buildId } = await post(`/applications/${id}/deploy`, { ...application, forceRebuild });
|
const { buildId } = await post(`/applications/${id}/deploy`, {
|
||||||
|
...application,
|
||||||
|
forceRebuild
|
||||||
|
});
|
||||||
addToast({
|
addToast({
|
||||||
message: $t('application.deployment_queued'),
|
message: $t('application.deployment_queued'),
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
|
$selectedBuildId = buildId;
|
||||||
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
|
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
||||||
} else {
|
replaceState: true
|
||||||
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
});
|
||||||
replaceState: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteApplication(name: string) {
|
async function deleteApplication(name: string, force: boolean) {
|
||||||
const sure = confirm($t('application.confirm_to_delete', { name }));
|
const sure = confirm($t('application.confirm_to_delete', { name }));
|
||||||
if (sure) {
|
if (sure) {
|
||||||
loading = true;
|
$status.application.initialLoading = true;
|
||||||
try {
|
try {
|
||||||
await del(`/applications/${id}`, { id });
|
await del(`/applications/${id}`, { id, force });
|
||||||
return await goto(`/applications`);
|
return await window.location.assign(`/`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
|
||||||
|
forceDelete = true;
|
||||||
|
}
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
$status.application.initialLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function restartApplication() {
|
||||||
|
try {
|
||||||
|
$status.application.initialLoading = true;
|
||||||
|
$status.application.loading = true;
|
||||||
|
await post(`/applications/${id}/restart`, {});
|
||||||
|
addToast({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Restart successful.'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
$status.application.initialLoading = false;
|
||||||
|
$status.application.loading = false;
|
||||||
|
await getStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
async function stopApplication() {
|
async function stopApplication() {
|
||||||
try {
|
try {
|
||||||
loading = true;
|
$status.application.initialLoading = true;
|
||||||
|
// $status.application.loading = true;
|
||||||
await post(`/applications/${id}/stop`, {});
|
await post(`/applications/${id}/stop`, {});
|
||||||
return window.location.reload();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
$status.application.initialLoading = false;
|
||||||
|
// $status.application.loading = false;
|
||||||
|
await getStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function getStatus() {
|
async function getStatus() {
|
||||||
@@ -122,19 +152,26 @@
|
|||||||
const data = await get(`/applications/${id}/status`);
|
const data = await get(`/applications/${id}/status`);
|
||||||
$status.application.isRunning = data.isRunning;
|
$status.application.isRunning = data.isRunning;
|
||||||
$status.application.isExited = data.isExited;
|
$status.application.isExited = data.isExited;
|
||||||
|
$status.application.isRestarting = data.isRestarting;
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
$status.application.initialLoading = false;
|
$status.application.initialLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
$status.application.initialLoading = true;
|
$status.application.initialLoading = true;
|
||||||
|
$status.application.isRunning = false;
|
||||||
|
$status.application.isExited = false;
|
||||||
|
$status.application.isRestarting = false;
|
||||||
|
$status.application.loading = false;
|
||||||
$location = null;
|
$location = null;
|
||||||
|
$isDeploymentEnabled = false;
|
||||||
clearInterval(statusInterval);
|
clearInterval(statusInterval);
|
||||||
});
|
});
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
setLocation(application, settings);
|
setLocation(application, settings);
|
||||||
$status.application.isRunning = false;
|
$status.application.isRunning = false;
|
||||||
$status.application.isExited = false;
|
$status.application.isExited = false;
|
||||||
|
$status.application.isRestarting = false;
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
if (
|
if (
|
||||||
application.gitSourceId &&
|
application.gitSourceId &&
|
||||||
@@ -152,371 +189,424 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="nav-side">
|
<nav class="nav-side">
|
||||||
{#if loading}
|
{#if $location}
|
||||||
<Loading fullscreen cover />
|
<a
|
||||||
{:else}
|
id="open"
|
||||||
{#if $location}
|
href={$location}
|
||||||
<a
|
target="_blank"
|
||||||
href={$location}
|
class="icons flex items-center bg-transparent text-sm"
|
||||||
target="_blank"
|
><svg
|
||||||
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
><svg
|
class="h-6 w-6"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24"
|
||||||
class="h-6 w-6"
|
stroke-width="1.5"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
stroke-width="1.5"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke-linecap="round"
|
||||||
fill="none"
|
stroke-linejoin="round"
|
||||||
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
|
|
||||||
>
|
>
|
||||||
<div class="border border-coolgray-500 h-8" />
|
<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" />
|
||||||
{/if}
|
<line x1="10" y1="14" x2="20" y2="4" />
|
||||||
|
<polyline points="15 4 20 4 20 9" />
|
||||||
{#if $status.application.isExited}
|
</svg></a
|
||||||
<a
|
>
|
||||||
href={!$disabledButton ? `/applications/${id}/logs` : null}
|
<Tooltip triggeredBy="#open">Open</Tooltip>
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center text-error"
|
|
||||||
data-tip="Application exited with an 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>
|
|
||||||
{/if}
|
|
||||||
{#if $status.application.initialLoading}
|
|
||||||
<button
|
|
||||||
class="icons tooltip-bottom 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.application.isRunning}
|
|
||||||
<button
|
|
||||||
on:click={stopApplication}
|
|
||||||
type="submit"
|
|
||||||
disabled={$disabledButton}
|
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-error"
|
|
||||||
data-tip={$appSession.isAdmin
|
|
||||||
? $t('application.stop_application')
|
|
||||||
: $t('application.permission_denied_stop_application')}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<form on:submit|preventDefault={() => handleDeploySubmit(true)}>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={$disabledButton}
|
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2"
|
|
||||||
data-tip={$appSession.isAdmin
|
|
||||||
? 'Force Rebuild Application'
|
|
||||||
: 'You do not have permission to rebuild application.'}
|
|
||||||
>
|
|
||||||
<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="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
|
|
||||||
transform="rotate(-45 12 12)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{:else}
|
|
||||||
<form on:submit|preventDefault={() => handleDeploySubmit(false)}>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={$disabledButton}
|
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-success"
|
|
||||||
data-tip={$appSession.isAdmin
|
|
||||||
? 'Deploy'
|
|
||||||
: 'You do not have permission to deploy application.'}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</form>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="border border-coolgray-500 h-8" />
|
<div class="border border-coolgray-500 h-8" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $status.application.isExited || $status.application.isRestarting}
|
||||||
<a
|
<a
|
||||||
href={!$disabledButton ? `/applications/${id}` : null}
|
id="applicationerror"
|
||||||
|
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
|
||||||
|
class="icons bg-transparent text-sm flex items-center text-error"
|
||||||
sveltekit:prefetch
|
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
|
<svg
|
||||||
disabled={$disabledButton}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
|
class="w-6 h-6"
|
||||||
data-tip="Configurations"
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentcolor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<svg
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path
|
||||||
class="h-6 w-6"
|
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"
|
||||||
viewBox="0 0 24 24"
|
/>
|
||||||
stroke-width="1.5"
|
<line x1="12" y1="8" x2="12" y2="12" />
|
||||||
stroke="currentColor"
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||||
fill="none"
|
</svg>
|
||||||
stroke-linecap="round"
|
</a>
|
||||||
stroke-linejoin="round"
|
<Tooltip triggeredBy="#applicationerror">Application exited or restarting!</Tooltip>
|
||||||
>
|
<button
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
id="stop"
|
||||||
<rect x="4" y="8" width="4" height="4" />
|
on:click={stopApplication}
|
||||||
<line x1="6" y1="4" x2="6" y2="8" />
|
type="submit"
|
||||||
<line x1="6" y1="12" x2="6" y2="20" />
|
disabled={!$isDeploymentEnabled}
|
||||||
<rect x="10" y="14" width="4" height="4" />
|
class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
|
||||||
<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
|
|
||||||
>
|
>
|
||||||
<a
|
<svg
|
||||||
href={!$disabledButton ? `/applications/${id}/secrets` : null}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
sveltekit:prefetch
|
class="w-6 h-6"
|
||||||
class="hover:text-pink-500 rounded"
|
viewBox="0 0 24 24"
|
||||||
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
|
stroke-width="1.5"
|
||||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
|
stroke="currentColor"
|
||||||
>
|
fill="none"
|
||||||
<button
|
stroke-linecap="round"
|
||||||
disabled={$disabledButton}
|
stroke-linejoin="round"
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
|
|
||||||
data-tip="Secrets"
|
|
||||||
>
|
>
|
||||||
<svg
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||||
class="w-6 h-6"
|
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||||
viewBox="0 0 24 24"
|
</svg>
|
||||||
stroke-width="1.5"
|
</button>
|
||||||
stroke="currentColor"
|
<Tooltip triggeredBy="#stop">Stop</Tooltip>
|
||||||
fill="none"
|
{/if}
|
||||||
stroke-linecap="round"
|
{#if $status.application.initialLoading}
|
||||||
stroke-linejoin="round"
|
<button
|
||||||
>
|
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
|
||||||
<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
|
|
||||||
>
|
>
|
||||||
<a
|
<svg
|
||||||
href={!$disabledButton ? `/applications/${id}/storages` : null}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
sveltekit:prefetch
|
class="h-6 w-6"
|
||||||
class="hover:text-pink-500 rounded"
|
viewBox="0 0 24 24"
|
||||||
class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`}
|
stroke-width="1.5"
|
||||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`}
|
stroke="currentColor"
|
||||||
>
|
fill="none"
|
||||||
<button
|
stroke-linecap="round"
|
||||||
disabled={$disabledButton}
|
stroke-linejoin="round"
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
|
|
||||||
data-tip="Persistent Storages"
|
|
||||||
>
|
>
|
||||||
<svg
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||||
class="w-6 h-6"
|
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
||||||
viewBox="0 0 24 24"
|
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
||||||
stroke-width="1.5"
|
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
||||||
stroke="currentColor"
|
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||||
fill="none"
|
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||||
stroke-linecap="round"
|
</svg>
|
||||||
stroke-linejoin="round"
|
</button>
|
||||||
>
|
{:else if $status.application.isRunning}
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<button
|
||||||
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
id="stop"
|
||||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
on:click={stopApplication}
|
||||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
type="submit"
|
||||||
</svg>
|
disabled={!$isDeploymentEnabled}
|
||||||
</button></a
|
class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
|
||||||
>
|
>
|
||||||
{#if !application.settings.isBot}
|
<svg
|
||||||
<a
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
href={!$disabledButton ? `/applications/${id}/previews` : null}
|
class="w-6 h-6"
|
||||||
sveltekit:prefetch
|
viewBox="0 0 24 24"
|
||||||
class="hover:text-orange-500 rounded"
|
stroke-width="1.5"
|
||||||
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
|
stroke="currentColor"
|
||||||
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<button
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
disabled={$disabledButton}
|
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
|
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||||
data-tip="Previews"
|
</svg>
|
||||||
>
|
</button>
|
||||||
<svg
|
<Tooltip triggeredBy="#stop">Stop</Tooltip>
|
||||||
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" />
|
|
||||||
<circle cx="7" cy="18" r="2" />
|
|
||||||
<circle cx="7" cy="6" r="2" />
|
|
||||||
<circle cx="17" cy="12" r="2" />
|
|
||||||
<line x1="7" y1="8" x2="7" y2="16" />
|
|
||||||
<path d="M7 8a4 4 0 0 0 4 4h4" />
|
|
||||||
</svg></button
|
|
||||||
></a
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
<div class="border border-coolgray-500 h-8" />
|
|
||||||
<a
|
|
||||||
href={!$disabledButton && $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}
|
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
|
|
||||||
data-tip={$t('application.logs')}
|
|
||||||
>
|
|
||||||
<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
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={!$disabledButton ? `/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 tooltip tooltip-primary tooltip-bottom text-sm"
|
|
||||||
data-tip="Build Logs"
|
|
||||||
>
|
|
||||||
<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="19" cy="13" r="2" />
|
|
||||||
<circle cx="4" cy="17" r="2" />
|
|
||||||
<circle cx="13" cy="17" r="2" />
|
|
||||||
<line x1="13" y1="19" x2="4" y2="19" />
|
|
||||||
<line x1="4" y1="15" x2="13" y2="15" />
|
|
||||||
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
|
|
||||||
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
|
|
||||||
<path d="M19 11v-7l-6 7" />
|
|
||||||
</svg>
|
|
||||||
</button></a
|
|
||||||
>
|
|
||||||
<div class="border border-coolgray-500 h-8" />
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click={() => deleteApplication(application.name)}
|
id="restart"
|
||||||
|
on:click={restartApplication}
|
||||||
|
type="submit"
|
||||||
|
disabled={!$isDeploymentEnabled}
|
||||||
|
class="icons bg-transparent text-sm flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<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="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
||||||
|
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={() => handleDeploySubmit(true)}>
|
||||||
|
<button
|
||||||
|
id="forceredeploy"
|
||||||
|
type="submit"
|
||||||
|
disabled={!$isDeploymentEnabled}
|
||||||
|
class="icons bg-transparent text-sm flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<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="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
|
||||||
|
transform="rotate(-45 12 12)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Tooltip triggeredBy="#forceredeploy">Force redeploy (without cache)</Tooltip>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<form on:submit|preventDefault={() => handleDeploySubmit(false)}>
|
||||||
|
<button
|
||||||
|
id="deploy"
|
||||||
|
type="submit"
|
||||||
|
disabled={!$isDeploymentEnabled}
|
||||||
|
class="icons bg-transparent text-sm flex items-center space-x-2 text-success"
|
||||||
|
>
|
||||||
|
<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="#deploy">Deploy</Tooltip>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="border border-coolgray-500 h-8" />
|
||||||
|
<a
|
||||||
|
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={!$isDeploymentEnabled}
|
||||||
|
id="configurations"
|
||||||
|
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" />
|
||||||
|
<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="#configurations">Configurations</Tooltip>
|
||||||
|
<a
|
||||||
|
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 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>
|
||||||
|
<a
|
||||||
|
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
|
||||||
|
id="persistentstorages"
|
||||||
|
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" />
|
||||||
|
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||||
|
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
</svg>
|
||||||
|
</button></a
|
||||||
|
>
|
||||||
|
<Tooltip triggeredBy="#persistentstorages">Persistent Storages</Tooltip>
|
||||||
|
{#if !application.settings.isBot}
|
||||||
|
<a
|
||||||
|
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 id="previews" 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" />
|
||||||
|
<circle cx="7" cy="18" r="2" />
|
||||||
|
<circle cx="7" cy="6" r="2" />
|
||||||
|
<circle cx="17" cy="12" r="2" />
|
||||||
|
<line x1="7" y1="8" x2="7" y2="16" />
|
||||||
|
<path d="M7 8a4 4 0 0 0 4 4h4" />
|
||||||
|
</svg></button
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
<Tooltip triggeredBy="#previews">Previews</Tooltip>
|
||||||
|
{/if}
|
||||||
|
<div class="border border-coolgray-500 h-8" />
|
||||||
|
<a
|
||||||
|
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
|
||||||
|
id="applicationlogs"
|
||||||
|
disabled={!$isDeploymentEnabled || !$status.application.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="#applicationlogs">Application Logs</Tooltip>
|
||||||
|
<a
|
||||||
|
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 id="buildlogs" disabled={!$isDeploymentEnabled} 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" />
|
||||||
|
<circle cx="19" cy="13" r="2" />
|
||||||
|
<circle cx="4" cy="17" r="2" />
|
||||||
|
<circle cx="13" cy="17" r="2" />
|
||||||
|
<line x1="13" y1="19" x2="4" y2="19" />
|
||||||
|
<line x1="4" y1="15" x2="13" y2="15" />
|
||||||
|
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
|
||||||
|
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
|
||||||
|
<path d="M19 11v-7l-6 7" />
|
||||||
|
</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}
|
||||||
|
class:bg-red-600={$appSession.isAdmin}
|
||||||
|
class:hover:bg-red-500={$appSession.isAdmin}
|
||||||
|
class="icons bg-transparent text-sm"
|
||||||
|
>
|
||||||
|
Force Delete
|
||||||
|
</button>
|
||||||
|
<Tooltip triggeredBy="#forcedelete">Force Delete</Tooltip>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
id="delete"
|
||||||
|
on:click={() => deleteApplication(application.name, false)}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!$appSession.isAdmin}
|
disabled={!$appSession.isAdmin}
|
||||||
class:hover:text-red-500={$appSession.isAdmin}
|
class:hover:text-red-500={$appSession.isAdmin}
|
||||||
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
|
class="icons bg-transparent text-sm"
|
||||||
data-tip={$appSession.isAdmin
|
|
||||||
? $t('application.delete_application')
|
|
||||||
: $t('application.permission_denied_delete_application')}
|
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</button>
|
</button>
|
||||||
|
<Tooltip triggeredBy="#delete">Delete</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -95,19 +95,19 @@
|
|||||||
async function isBranchAlreadyUsed(event: any) {
|
async function isBranchAlreadyUsed(event: any) {
|
||||||
selected.branch = event.detail.value;
|
selected.branch = event.detail.value;
|
||||||
try {
|
try {
|
||||||
const data = await get(
|
// const data = await get(
|
||||||
`/applications/${id}/configuration/repository?repository=${selected.repository}&branch=${selected.branch}`
|
// `/applications/${id}/configuration/repository?repository=${selected.repository}&branch=${selected.branch}`
|
||||||
);
|
// );
|
||||||
if (data.used) {
|
// if (data.used) {
|
||||||
const sure = confirm($t('application.configuration.branch_already_in_use'));
|
// const sure = confirm($t('application.configuration.branch_already_in_use'));
|
||||||
if (sure) {
|
// if (sure) {
|
||||||
selected.autodeploy = false;
|
// selected.autodeploy = false;
|
||||||
showSave = true;
|
// showSave = true;
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
showSave = false;
|
// showSave = false;
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
showSave = true;
|
showSave = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showSave = false;
|
showSave = false;
|
||||||
|
|||||||
@@ -169,10 +169,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function selectBranch(event: any) {
|
|
||||||
selected.branch = event.detail;
|
|
||||||
isBranchAlreadyUsed();
|
|
||||||
}
|
|
||||||
async function loadBranches(page: number = 1) {
|
async function loadBranches(page: number = 1) {
|
||||||
let perPage = 100;
|
let perPage = 100;
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -199,21 +195,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isBranchAlreadyUsed() {
|
async function isBranchAlreadyUsed(event) {
|
||||||
|
selected.branch = event.detail;
|
||||||
try {
|
try {
|
||||||
const data = await get(
|
// const data = await get(
|
||||||
`/applications/${id}/configuration/repository?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
|
// `/applications/${id}/configuration/repository?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
|
||||||
);
|
// );
|
||||||
if (data.used) {
|
// if (data.used) {
|
||||||
const sure = confirm($t('application.configuration.branch_already_in_use'));
|
// const sure = confirm($t('application.configuration.branch_already_in_use'));
|
||||||
if (sure) {
|
// if (sure) {
|
||||||
autodeploy = false;
|
// autodeploy = false;
|
||||||
showSave = true;
|
// showSave = true;
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
showSave = false;
|
// showSave = false;
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
showSave = true;
|
showSave = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
@@ -227,9 +224,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function setWebhook(url: any, webhookToken: any) {
|
async function setWebhook(url: any, webhookToken: any) {
|
||||||
const host = dev
|
const host = dev ? getWebhookUrl('gitlab') : `${window.location.origin}/webhooks/gitlab/events`;
|
||||||
? getWebhookUrl('gitlab')
|
|
||||||
: `${window.location.origin}/webhooks/gitlab/events`;
|
|
||||||
try {
|
try {
|
||||||
await post(
|
await post(
|
||||||
url,
|
url,
|
||||||
@@ -294,17 +289,15 @@
|
|||||||
);
|
);
|
||||||
await post(updateDeployKeyIdUrl, { deployKeyId: id });
|
await post(updateDeployKeyIdUrl, { deployKeyId: id });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
|
||||||
} finally {
|
|
||||||
loading.save = false;
|
loading.save = false;
|
||||||
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setWebhook(webhookUrl, webhookToken);
|
await setWebhook(webhookUrl, webhookToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
|
||||||
} finally {
|
|
||||||
loading.save = false;
|
loading.save = false;
|
||||||
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `/applications/${id}/configuration/repository`;
|
const url = `/applications/${id}/configuration/repository`;
|
||||||
@@ -317,11 +310,11 @@
|
|||||||
autodeploy,
|
autodeploy,
|
||||||
webhookToken
|
webhookToken
|
||||||
});
|
});
|
||||||
|
loading.save = false;
|
||||||
return await goto(from || `/applications/${id}/configuration/buildpack`);
|
return await goto(from || `/applications/${id}/configuration/buildpack`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
|
||||||
} finally {
|
|
||||||
loading.save = false;
|
loading.save = false;
|
||||||
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
@@ -396,7 +389,7 @@
|
|||||||
showIndicator={!loading.branches}
|
showIndicator={!loading.branches}
|
||||||
isWaiting={loading.branches}
|
isWaiting={loading.branches}
|
||||||
isDisabled={loading.branches || !selected.project}
|
isDisabled={loading.branches || !selected.project}
|
||||||
on:select={selectBranch}
|
on:select={isBranchAlreadyUsed}
|
||||||
on:clear={() => {
|
on:clear={() => {
|
||||||
showSave = false;
|
showSave = false;
|
||||||
selected.branch = null;
|
selected.branch = null;
|
||||||
@@ -425,7 +418,7 @@
|
|||||||
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
|
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="w-40 bg-green-600"
|
class="btn btn-sm w-40 bg-green-600"
|
||||||
on:click|stopPropagation|preventDefault={() => window.location.reload()}
|
on:click|stopPropagation|preventDefault={() => window.location.reload()}
|
||||||
>
|
>
|
||||||
Try again
|
Try again
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
import Select from 'svelte-select';
|
import Select from 'svelte-select';
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@
|
|||||||
async function loadBranches() {
|
async function loadBranches() {
|
||||||
try {
|
try {
|
||||||
loading.branches = true;
|
loading.branches = true;
|
||||||
|
publicRepositoryLink = publicRepositoryLink.trim();
|
||||||
const protocol = publicRepositoryLink.split(':')[0];
|
const protocol = publicRepositoryLink.split(':')[0];
|
||||||
const gitUrl = publicRepositoryLink.replace('http://', '').replace('https://', '');
|
const gitUrl = publicRepositoryLink.replace('http://', '').replace('https://', '');
|
||||||
|
|
||||||
@@ -164,7 +163,6 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<input
|
<input
|
||||||
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
||||||
class="text-xs"
|
|
||||||
bind:value={publicRepositoryLink}
|
bind:value={publicRepositoryLink}
|
||||||
/>
|
/>
|
||||||
{#if branchSelectOptions.length > 0}
|
{#if branchSelectOptions.length > 0}
|
||||||
@@ -193,7 +191,5 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Explainer
|
|
||||||
text="Examples:<br><br>https://github.com/coollabsio/nodejs-example<br>https://github.com/coollabsio/nodejs-example/tree/main<br>https://gitlab.com/aleveha/fastify-example<br>https://gitlab.com/aleveha/fastify-example/-/tree/master<br><br>Only works with Github.com and Gitlab.com."
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import { appSession } from '$lib/store';
|
import { appSession } from '$lib/store';
|
||||||
import PublicRepository from './_PublicRepository.svelte';
|
import PublicRepository from './_PublicRepository.svelte';
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
import DocLink from '$lib/components/DocLink.svelte';
|
||||||
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
const from = $page.url.searchParams.get('from');
|
const from = $page.url.searchParams.get('from');
|
||||||
@@ -192,7 +192,9 @@ import Explainer from '$lib/components/Explainer.svelte';
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="title py-4">Public Repository</div>
|
<div class="flex items-center">
|
||||||
|
<div class="title py-4">Public Repository</div>
|
||||||
<PublicRepository />
|
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
|
||||||
|
</div>
|
||||||
|
<PublicRepository />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,15 +31,24 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import Select from 'svelte-select';
|
import Select from 'svelte-select';
|
||||||
|
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
|
||||||
import { get, post } from '$lib/api';
|
import { get, post } from '$lib/api';
|
||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
import { browser } from '$app/env';
|
import {
|
||||||
import { addToast, appSession, disabledButton, setLocation, status } from '$lib/store';
|
addToast,
|
||||||
|
appSession,
|
||||||
|
checkIfDeploymentEnabledApplications,
|
||||||
|
setLocation,
|
||||||
|
status,
|
||||||
|
isDeploymentEnabled,
|
||||||
|
features
|
||||||
|
} from '$lib/store';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
|
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
|
||||||
import Setting from './_Setting.svelte';
|
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;
|
const { id } = $page.params;
|
||||||
|
|
||||||
$: isDisabled =
|
$: isDisabled =
|
||||||
@@ -63,7 +72,9 @@
|
|||||||
let dualCerts = application.settings.dualCerts;
|
let dualCerts = application.settings.dualCerts;
|
||||||
let autodeploy = application.settings.autodeploy;
|
let autodeploy = application.settings.autodeploy;
|
||||||
let isBot = application.settings.isBot;
|
let isBot = application.settings.isBot;
|
||||||
|
let isDBBranching = application.settings.isDBBranching;
|
||||||
|
|
||||||
|
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
|
||||||
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||||
let isNonWWWDomainOK = false;
|
let isNonWWWDomainOK = false;
|
||||||
let isWWWDomainOK = false;
|
let isWWWDomainOK = false;
|
||||||
@@ -159,8 +170,12 @@
|
|||||||
if ($status.application.isRunning) return;
|
if ($status.application.isRunning) return;
|
||||||
isBot = !isBot;
|
isBot = !isBot;
|
||||||
application.settings.isBot = isBot;
|
application.settings.isBot = isBot;
|
||||||
|
application.fqdn = null;
|
||||||
setLocation(application, settings);
|
setLocation(application, settings);
|
||||||
}
|
}
|
||||||
|
if (name === 'isDBBranching') {
|
||||||
|
isDBBranching = !isDBBranching;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await post(`/applications/${id}/settings`, {
|
await post(`/applications/${id}/settings`, {
|
||||||
previews,
|
previews,
|
||||||
@@ -168,6 +183,7 @@
|
|||||||
dualCerts,
|
dualCerts,
|
||||||
isBot,
|
isBot,
|
||||||
autodeploy,
|
autodeploy,
|
||||||
|
isDBBranching,
|
||||||
branch: application.branch,
|
branch: application.branch,
|
||||||
projectId: application.projectId
|
projectId: application.projectId
|
||||||
});
|
});
|
||||||
@@ -191,11 +207,16 @@
|
|||||||
if (name === 'isBot') {
|
if (name === 'isBot') {
|
||||||
isBot = !isBot;
|
isBot = !isBot;
|
||||||
}
|
}
|
||||||
|
if (name === 'isDBBranching') {
|
||||||
|
isDBBranching = !isDBBranching;
|
||||||
|
}
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (loading || (!application.fqdn && !isBot)) return;
|
if (loading) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
|
||||||
@@ -208,16 +229,17 @@
|
|||||||
dualCerts,
|
dualCerts,
|
||||||
exposePort: application.exposePort
|
exposePort: application.exposePort
|
||||||
}));
|
}));
|
||||||
await post(`/applications/${id}`, { ...application });
|
await post(`/applications/${id}`, { ...application, baseDatabaseBranch });
|
||||||
setLocation(application, settings);
|
setLocation(application, settings);
|
||||||
$disabledButton = false;
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
|
|
||||||
forceSave = false;
|
forceSave = false;
|
||||||
|
|
||||||
addToast({
|
addToast({
|
||||||
message: 'Configuration saved.',
|
message: 'Configuration saved.',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
|
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
|
||||||
forceSave = true;
|
forceSave = true;
|
||||||
@@ -281,6 +303,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
|
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
|
||||||
<a
|
<a
|
||||||
|
id="git"
|
||||||
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
|
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="w-10"
|
class="w-10"
|
||||||
@@ -321,6 +344,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
<Tooltip triggeredBy="#git">Open on Git</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -426,7 +450,7 @@
|
|||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 items-center pb-8">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<label for="destination" class="text-base font-bold text-stone-100"
|
<label for="destination" class="text-base font-bold text-stone-100"
|
||||||
>{$t('application.destination')}</label
|
>{$t('application.destination')}</label
|
||||||
>
|
>
|
||||||
@@ -440,10 +464,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
||||||
<div class="grid grid-cols-2 items-center pb-8">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<label for="baseBuildImage" class="text-base font-bold text-stone-100"
|
<label for="baseBuildImage" class="text-base font-bold text-stone-100"
|
||||||
>{$t('application.base_build_image')}</label
|
>{$t('application.base_build_image')}
|
||||||
>
|
<Explainer
|
||||||
|
explanation={application.buildPack === 'laravel'
|
||||||
|
? 'For building frontend assets with webpack.'
|
||||||
|
: 'Image that will be used during the build process.'}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="custom-select-wrapper">
|
<div class="custom-select-wrapper">
|
||||||
<Select
|
<Select
|
||||||
@@ -457,17 +486,13 @@
|
|||||||
isClearable={false}
|
isClearable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if application.buildPack === 'laravel'}
|
|
||||||
<Explainer text="For building frontend assets with webpack." />
|
|
||||||
{:else}
|
|
||||||
<Explainer text={$t('application.base_build_image_explainer')} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if application.buildPack !== 'docker'}
|
{#if application.buildPack !== 'docker'}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<label for="baseImage" class="text-base font-bold text-stone-100"
|
<label for="baseImage" class="text-base font-bold text-stone-100"
|
||||||
>{$t('application.base_image')}</label
|
>{$t('application.base_image')}
|
||||||
|
<Explainer explanation={'Image that will be used for the deployment.'} /></label
|
||||||
>
|
>
|
||||||
<div class="custom-select-wrapper">
|
<div class="custom-select-wrapper">
|
||||||
<Select
|
<Select
|
||||||
@@ -481,13 +506,15 @@
|
|||||||
isClearable={false}
|
isClearable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Explainer text={$t('application.base_image_explainer')} />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')}
|
{#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')}
|
||||||
<div class="grid grid-cols-2 items-center pb-8">
|
<div class="grid grid-cols-2 items-center pb-8">
|
||||||
<label for="deploymentType" class="text-base font-bold text-stone-100"
|
<label for="deploymentType" class="text-base font-bold text-stone-100"
|
||||||
>Deployment Type</label
|
>Deployment Type
|
||||||
|
<Explainer
|
||||||
|
explanation={"Defines how to deploy your application. <br><br><span class='text-green-500 font-bold'>Static</span> is for static websites, <span class='text-green-500 font-bold'>node</span> is for server-side applications."}
|
||||||
|
/></label
|
||||||
>
|
>
|
||||||
<div class="custom-select-wrapper">
|
<div class="custom-select-wrapper">
|
||||||
<Select
|
<Select
|
||||||
@@ -501,11 +528,48 @@
|
|||||||
isClearable={false}
|
isClearable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Explainer
|
|
||||||
text="Defines how to deploy your application. <br><br><span class='text-green-500 font-bold'>Static</span> is for static websites, <span class='text-green-500 font-bold'>node</span> is for server-side applications."
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $features.beta}
|
||||||
|
{#if !application.settings.isBot && !application.settings.isPublicRepository}
|
||||||
|
<div class="grid grid-cols-2 items-center">
|
||||||
|
<Setting
|
||||||
|
id="isDBBranching"
|
||||||
|
isCenter={false}
|
||||||
|
bind:setting={isDBBranching}
|
||||||
|
on:click={() => changeSettings('isDBBranching')}
|
||||||
|
title="Enable DB Branching"
|
||||||
|
description="Enable DB Branching"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if isDBBranching}
|
||||||
|
<button
|
||||||
|
on:click|stopPropagation|preventDefault={() =>
|
||||||
|
goto(`/applications/${id}/configuration/database`)}
|
||||||
|
class="btn btn-sm">Configure Connected Database</button
|
||||||
|
>
|
||||||
|
{#if application.connectedDatabase}
|
||||||
|
<div class="grid grid-cols-2 items-center">
|
||||||
|
<label for="baseImage" class="text-base font-bold text-stone-100"
|
||||||
|
>Base Database
|
||||||
|
<Explainer
|
||||||
|
explanation={'The name of the database that will be used as base when branching.'}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name="baseDatabaseBranch"
|
||||||
|
required
|
||||||
|
id="baseDatabaseBranch"
|
||||||
|
bind:value={baseDatabaseBranch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-center bg-green-600 rounded">
|
||||||
|
Connected to {application.connectedDatabase.databaseId}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
<div class="flex space-x-1 py-5 font-bold">
|
||||||
<div class="title">{$t('application.application')}</div>
|
<div class="title">{$t('application.application')}</div>
|
||||||
@@ -513,35 +577,41 @@
|
|||||||
<div class="grid grid-flow-row gap-2 px-10">
|
<div class="grid grid-flow-row gap-2 px-10">
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<Setting
|
<Setting
|
||||||
|
id="isBot"
|
||||||
isCenter={false}
|
isCenter={false}
|
||||||
bind:setting={isBot}
|
bind:setting={isBot}
|
||||||
on:click={() => changeSettings('isBot')}
|
on:click={() => changeSettings('isBot')}
|
||||||
title="Is your application a bot?"
|
title="Is your application a bot?"
|
||||||
description="You can deploy applications without domains. <br>You can also make them to listen on <span class='text-green-500 font-bold'>IP:EXPOSEDPORT</span> as well.<br></Setting><br>Useful to host <span class='text-green-500 font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming connection.</span>"
|
description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>"
|
||||||
disabled={$status.application.isRunning}
|
disabled={$status.application.isRunning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-2 items-center">
|
||||||
|
<Setting
|
||||||
|
id="dualCerts"
|
||||||
|
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||||
|
disabled={$status.application.isRunning}
|
||||||
|
isCenter={false}
|
||||||
|
bind:setting={dualCerts}
|
||||||
|
title={$t('application.ssl_www_and_non_www')}
|
||||||
|
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
|
||||||
|
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{#if !isBot}
|
{#if !isBot}
|
||||||
<div class="grid grid-cols-2">
|
<div class="grid grid-cols-2 items-center pb-8">
|
||||||
<div class="flex-col">
|
<label for="fqdn" class="text-base font-bold text-stone-100"
|
||||||
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
|
>{$t('application.url_fqdn')}
|
||||||
>{$t('application.url_fqdn')}</label
|
<Explainer
|
||||||
>
|
explanation={"If you specify <span class='text-settings font-bold'>https</span>, the application will be accessible only over https.<br>SSL certificate will be generated automatically.<br><br>If you specify <span class='text-settings font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-settings font-bold'>You must set your DNS to point to the server IP in advance.</span>"}
|
||||||
{#if browser && window.location.hostname === 'demo.coolify.io'}
|
/>
|
||||||
<Explainer
|
</label>
|
||||||
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<Explainer text={$t('application.https_explainer')} />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
readonly={isDisabled}
|
readonly={isDisabled}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
bind:this={domainEl}
|
|
||||||
name="fqdn"
|
name="fqdn"
|
||||||
id="fqdn"
|
id="fqdn"
|
||||||
required
|
|
||||||
bind:value={application.fqdn}
|
bind:value={application.fqdn}
|
||||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||||
placeholder="eg: https://coollabs.io"
|
placeholder="eg: https://coollabs.io"
|
||||||
@@ -582,17 +652,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 items-center pb-8">
|
|
||||||
<Setting
|
|
||||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
|
||||||
disabled={$status.application.isRunning}
|
|
||||||
isCenter={false}
|
|
||||||
bind:setting={dualCerts}
|
|
||||||
title={$t('application.ssl_www_and_non_www')}
|
|
||||||
description={$t('application.ssl_explainer')}
|
|
||||||
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if application.buildPack === 'python'}
|
{#if application.buildPack === 'python'}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
@@ -645,7 +704,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if !staticDeployments.includes(application.buildPack)}
|
{#if !staticDeployments.includes(application.buildPack)}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
|
<label for="port" class="text-base font-bold text-stone-100"
|
||||||
|
>{$t('forms.port')}
|
||||||
|
<Explainer explanation={'The port your application listens on.'} /></label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
readonly={!$appSession.isAdmin}
|
readonly={!$appSession.isAdmin}
|
||||||
@@ -654,11 +716,14 @@
|
|||||||
bind:value={application.port}
|
bind:value={application.port}
|
||||||
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
|
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
|
||||||
/>
|
/>
|
||||||
<Explainer text={'The port your application listens on.'} />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center pb-8">
|
||||||
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
|
<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.'}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
readonly={!$appSession.isAdmin && !$status.application.isRunning}
|
readonly={!$appSession.isAdmin && !$status.application.isRunning}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
@@ -667,12 +732,9 @@
|
|||||||
bind:value={application.exposePort}
|
bind:value={application.exposePort}
|
||||||
placeholder="12345"
|
placeholder="12345"
|
||||||
/>
|
/>
|
||||||
<Explainer
|
|
||||||
text={'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.'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{#if !notNodeDeployments.includes(application.buildPack)}
|
{#if !notNodeDeployments.includes(application.buildPack)}
|
||||||
<div class="grid grid-cols-2 items-center pt-4">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<label for="installCommand" class="text-base font-bold text-stone-100"
|
<label for="installCommand" class="text-base font-bold text-stone-100"
|
||||||
>{$t('application.install_command')}</label
|
>{$t('application.install_command')}</label
|
||||||
>
|
>
|
||||||
@@ -698,7 +760,7 @@
|
|||||||
placeholder="{$t('forms.default')}: yarn build"
|
placeholder="{$t('forms.default')}: yarn build"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center pb-8">
|
||||||
<label for="startCommand" class="text-base font-bold text-stone-100"
|
<label for="startCommand" class="text-base font-bold text-stone-100"
|
||||||
>{$t('application.start_command')}</label
|
>{$t('application.start_command')}</label
|
||||||
>
|
>
|
||||||
@@ -715,7 +777,9 @@
|
|||||||
{#if application.buildPack === 'docker'}
|
{#if application.buildPack === 'docker'}
|
||||||
<div class="grid grid-cols-2 items-center pt-4">
|
<div class="grid grid-cols-2 items-center pt-4">
|
||||||
<label for="dockerFileLocation" class="text-base font-bold text-stone-100"
|
<label for="dockerFileLocation" class="text-base font-bold text-stone-100"
|
||||||
>Dockerfile Location</label
|
>Dockerfile Location <Explainer
|
||||||
|
explanation={"Should be absolute path, like <span class='text-settings font-bold'>/data/Dockerfile</span> or <span class='text-settings font-bold'>/Dockerfile.</span>"}
|
||||||
|
/></label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
@@ -725,9 +789,6 @@
|
|||||||
bind:value={application.dockerFileLocation}
|
bind:value={application.dockerFileLocation}
|
||||||
placeholder="default: /Dockerfile"
|
placeholder="default: /Dockerfile"
|
||||||
/>
|
/>
|
||||||
<Explainer
|
|
||||||
text="Should be absolute path, like <span class='text-green-500 font-bold'>/data/Dockerfile</span> or <span class='text-green-500 font-bold'>/Dockerfile.</span>"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if application.buildPack === 'deno'}
|
{#if application.buildPack === 'deno'}
|
||||||
@@ -743,7 +804,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<label for="denoOptions" class="text-base font-bold text-stone-100">Arguments</label>
|
<label for="denoOptions" class="text-base font-bold text-stone-100"
|
||||||
|
>Arguments <Explainer
|
||||||
|
explanation={"List of arguments to pass to <span class='text-settings font-bold'>deno run</span> command. Could include permissions, configurations files, etc."}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
readonly={!$appSession.isAdmin}
|
readonly={!$appSession.isAdmin}
|
||||||
@@ -752,18 +817,17 @@
|
|||||||
bind:value={application.denoOptions}
|
bind:value={application.denoOptions}
|
||||||
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
|
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
|
||||||
/>
|
/>
|
||||||
<Explainer
|
|
||||||
text="List of arguments to pass to <span class='text-green-500 font-bold'>deno run</span> command. Could include permissions, configurations files, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if application.buildPack !== 'laravel' && application.buildPack !== 'heroku'}
|
{#if application.buildPack !== 'laravel' && application.buildPack !== 'heroku'}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<div class="flex-col">
|
<div class="flex-col">
|
||||||
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
|
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
|
||||||
>{$t('forms.base_directory')}</label
|
>{$t('forms.base_directory')}
|
||||||
|
<Explainer
|
||||||
|
explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."}
|
||||||
|
/></label
|
||||||
>
|
>
|
||||||
<Explainer text={$t('application.directory_to_use_explainer')} />
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
@@ -779,9 +843,11 @@
|
|||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<div class="flex-col">
|
<div class="flex-col">
|
||||||
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
|
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
|
||||||
>{$t('forms.publish_directory')}</label
|
>{$t('forms.publish_directory')}
|
||||||
|
<Explainer
|
||||||
|
explanation={"Directory containing all the assets for deployment. <br> For example: <span class='text-settings font-bold'>dist</span>,<span class='text-settings font-bold'>_site</span> or <span class='text-settings font-bold'>public</span>."}
|
||||||
|
/></label
|
||||||
>
|
>
|
||||||
<Explainer text={$t('application.publish_directory_explainer')} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@@ -804,6 +870,7 @@
|
|||||||
{#if !application.settings.isPublicRepository}
|
{#if !application.settings.isPublicRepository}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<Setting
|
<Setting
|
||||||
|
id="autodeploy"
|
||||||
isCenter={false}
|
isCenter={false}
|
||||||
bind:setting={autodeploy}
|
bind:setting={autodeploy}
|
||||||
on:click={() => changeSettings('autodeploy')}
|
on:click={() => changeSettings('autodeploy')}
|
||||||
@@ -812,9 +879,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !application.settings.isBot}
|
{#if !application.settings.isBot && !application.settings.isPublicRepository}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<Setting
|
<Setting
|
||||||
|
id="previews"
|
||||||
isCenter={false}
|
isCenter={false}
|
||||||
bind:setting={previews}
|
bind:setting={previews}
|
||||||
on:click={() => changeSettings('previews')}
|
on:click={() => changeSettings('previews')}
|
||||||
@@ -825,6 +893,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="grid grid-cols-2 items-center">
|
<div class="grid grid-cols-2 items-center">
|
||||||
<Setting
|
<Setting
|
||||||
|
id="debug"
|
||||||
isCenter={false}
|
isCenter={false}
|
||||||
bind:setting={debug}
|
bind:setting={debug}
|
||||||
on:click={() => changeSettings('debug')}
|
on:click={() => changeSettings('debug')}
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let buildId: any;
|
|
||||||
|
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
import Loading from '$lib/components/Loading.svelte';
|
|
||||||
import { get, post } from '$lib/api';
|
import { get, post } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
import { day } from '$lib/dayjs';
|
||||||
|
import { selectedBuildId } from '$lib/store';
|
||||||
|
|
||||||
let logs: any = [];
|
let logs: any = [];
|
||||||
let loading = true;
|
|
||||||
let currentStatus: any;
|
let currentStatus: any;
|
||||||
let streamInterval: any;
|
let streamInterval: any;
|
||||||
let followingBuild: any;
|
let followingBuild: any;
|
||||||
let followingInterval: any;
|
let followingInterval: any;
|
||||||
let logsEl: any;
|
let logsEl: any;
|
||||||
|
let fromDb = false;
|
||||||
let cancelInprogress = false;
|
let cancelInprogress = false;
|
||||||
|
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
@@ -39,14 +38,18 @@
|
|||||||
}
|
}
|
||||||
async function streamLogs(sequence = 0) {
|
async function streamLogs(sequence = 0) {
|
||||||
try {
|
try {
|
||||||
let { logs: responseLogs, status } = await get(
|
let {
|
||||||
`/applications/${id}/logs/build/${buildId}?sequence=${sequence}`
|
logs: responseLogs,
|
||||||
);
|
status,
|
||||||
|
fromDb: from
|
||||||
|
} = await get(`/applications/${id}/logs/build/${$selectedBuildId}?sequence=${sequence}`);
|
||||||
|
|
||||||
currentStatus = status;
|
currentStatus = status;
|
||||||
logs = logs.concat(
|
logs = logs.concat(
|
||||||
responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
|
responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
|
||||||
);
|
);
|
||||||
loading = false;
|
fromDb = from;
|
||||||
|
|
||||||
streamInterval = setInterval(async () => {
|
streamInterval = setInterval(async () => {
|
||||||
if (status !== 'running' && status !== 'queued') {
|
if (status !== 'running' && status !== 'queued') {
|
||||||
clearInterval(streamInterval);
|
clearInterval(streamInterval);
|
||||||
@@ -55,21 +58,21 @@
|
|||||||
const nextSequence = logs[logs.length - 1]?.time || 0;
|
const nextSequence = logs[logs.length - 1]?.time || 0;
|
||||||
try {
|
try {
|
||||||
const data = await get(
|
const data = await get(
|
||||||
`/applications/${id}/logs/build/${buildId}?sequence=${nextSequence}`
|
`/applications/${id}/logs/build/${$selectedBuildId}?sequence=${nextSequence}`
|
||||||
);
|
);
|
||||||
status = data.status;
|
status = data.status;
|
||||||
currentStatus = status;
|
currentStatus = status;
|
||||||
|
fromDb = data.fromDb;
|
||||||
|
|
||||||
logs = logs.concat(
|
logs = logs.concat(
|
||||||
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
|
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
|
||||||
);
|
);
|
||||||
dispatch('updateBuildStatus', { status });
|
dispatch('updateBuildStatus', { status, took: data.took });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,11 +81,10 @@
|
|||||||
try {
|
try {
|
||||||
cancelInprogress = true;
|
cancelInprogress = true;
|
||||||
await post(`/applications/${id}/cancel`, {
|
await post(`/applications/${id}/cancel`, {
|
||||||
buildId,
|
buildId: $selectedBuildId,
|
||||||
applicationId: id
|
applicationId: id
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,84 +98,86 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading}
|
<div class="relative ">
|
||||||
<Loading />
|
{#if currentStatus === 'running'}
|
||||||
{:else}
|
<LoadingLogs />
|
||||||
<div class="relative ">
|
{/if}
|
||||||
{#if currentStatus === 'running'}
|
{#if currentStatus === 'queued'}
|
||||||
<LoadingLogs />
|
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
|
||||||
{/if}
|
{:else}
|
||||||
{#if currentStatus === 'queued'}
|
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||||
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
|
<button
|
||||||
{:else}
|
id="follow"
|
||||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
on:click={followBuild}
|
||||||
|
class="bg-transparent btn btn-sm btn-link hover:text-green-500 hover:bg-coolgray-500"
|
||||||
|
class:text-green-500={followingBuild}
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<line x1="8" y1="12" x2="12" y2="16" />
|
||||||
|
<line x1="12" y1="8" x2="12" y2="16" />
|
||||||
|
<line x1="16" y1="12" x2="12" y2="16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
|
||||||
|
{#if currentStatus === 'running'}
|
||||||
<button
|
<button
|
||||||
on:click={followBuild}
|
id="cancel"
|
||||||
class="bg-transparent btn btn-sm btn-link tooltip tooltip-primary tooltip-bottom hover:text-green-500 hover:bg-coolgray-500"
|
on:click={cancelBuild}
|
||||||
data-tip="Follow logs"
|
class:animation-spin={cancelInprogress}
|
||||||
class:text-green-500={followingBuild}
|
class="bg-transparent btn btn-sm btn-link hover:text-red-500 hover:bg-coolgray-500"
|
||||||
>
|
>
|
||||||
<svg
|
{#if cancelInprogress}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
Cancelling...
|
||||||
class="w-6 h-6"
|
{:else}
|
||||||
viewBox="0 0 24 24"
|
<svg
|
||||||
stroke-width="1.5"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke="currentColor"
|
class="w-6 h-6"
|
||||||
fill="none"
|
viewBox="0 0 24 24"
|
||||||
stroke-linecap="round"
|
stroke-width="1.5"
|
||||||
stroke-linejoin="round"
|
stroke="currentColor"
|
||||||
>
|
fill="none"
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
stroke-linecap="round"
|
||||||
<circle cx="12" cy="12" r="9" />
|
stroke-linejoin="round"
|
||||||
<line x1="8" y1="12" x2="12" y2="16" />
|
>
|
||||||
<line x1="12" y1="8" x2="12" y2="16" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<line x1="16" y1="12" x2="12" y2="16" />
|
<circle cx="12" cy="12" r="9" />
|
||||||
</svg>
|
<path d="M10 10l4 4m0 -4l-4 4" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if currentStatus === 'running'}
|
<Tooltip triggeredBy="#cancel">Cancel build</Tooltip>
|
||||||
<button
|
|
||||||
on:click={cancelBuild}
|
|
||||||
class:animation-spin={cancelInprogress}
|
|
||||||
class="bg-transparent btn btn-sm btn-link hover:text-red-500 hover:bg-coolgray-500 tooltip tooltip-primary tooltip-bottom"
|
|
||||||
data-tip="Cancel build"
|
|
||||||
>
|
|
||||||
{#if cancelInprogress}
|
|
||||||
Cancelling...
|
|
||||||
{:else}
|
|
||||||
<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" />
|
|
||||||
<circle cx="12" cy="12" r="9" />
|
|
||||||
<path d="M10 10l4 4m0 -4l-4 4" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if logs.length > 0}
|
|
||||||
<div
|
|
||||||
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
|
||||||
bind:this={logsEl}
|
|
||||||
>
|
|
||||||
{#each logs as log}
|
|
||||||
<div>{log.line + '\n'}</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
|
||||||
>
|
|
||||||
No logs found.
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if logs.length > 0}
|
||||||
|
<div
|
||||||
|
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||||
|
bind:this={logsEl}
|
||||||
|
>
|
||||||
|
{#each logs as log}
|
||||||
|
{#if fromDb}
|
||||||
|
<div>{log.line + '\n'}</div>
|
||||||
|
{:else}
|
||||||
|
<div>[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||||
|
>
|
||||||
|
No logs found.
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
|||||||
@@ -23,50 +23,45 @@
|
|||||||
export let application: any;
|
export let application: any;
|
||||||
export let buildCount: any;
|
export let buildCount: any;
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import { addToast, selectedBuildId } from '$lib/store';
|
||||||
import BuildLog from './_BuildLog.svelte';
|
import BuildLog from './_BuildLog.svelte';
|
||||||
import { get } from '$lib/api';
|
import { get, post } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import { changeQueryParams, dateOptions, errorNotification } from '$lib/common';
|
import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common';
|
||||||
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
import { day } from '$lib/dayjs';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
const { id } = $page.params;
|
||||||
|
|
||||||
let buildId: any;
|
let loadBuildLogsInterval: any = null;
|
||||||
|
|
||||||
let skip = 0;
|
let skip = 0;
|
||||||
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
|
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
|
||||||
const { id } = $page.params;
|
|
||||||
let preselectedBuildId = $page.url.searchParams.get('buildId');
|
let preselectedBuildId = $page.url.searchParams.get('buildId');
|
||||||
if (preselectedBuildId) buildId = preselectedBuildId;
|
if (preselectedBuildId) $selectedBuildId = preselectedBuildId;
|
||||||
|
|
||||||
async function updateBuildStatus({ detail }: { detail: any }) {
|
onMount(async () => {
|
||||||
const { status } = detail;
|
getBuildLogs();
|
||||||
if (status !== 'running') {
|
loadBuildLogsInterval = setInterval(() => {
|
||||||
try {
|
getBuildLogs();
|
||||||
const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`);
|
}, 2000);
|
||||||
builds = builds.filter((build: any) => {
|
|
||||||
if (build.id === data.builds[0].id) {
|
});
|
||||||
build.status = data.builds[0].status;
|
onDestroy(() => {
|
||||||
build.took = data.builds[0].took;
|
clearInterval(loadBuildLogsInterval);
|
||||||
build.since = data.builds[0].since;
|
});
|
||||||
}
|
async function getBuildLogs() {
|
||||||
return build;
|
const response = await get(`/applications/${$page.params.id}/logs/build?skip=${skip}`);
|
||||||
});
|
builds = response.builds;
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
builds = builds.filter((build: any) => {
|
|
||||||
if (build.id === buildId) build.status = status;
|
|
||||||
return build;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMoreBuilds() {
|
async function loadMoreBuilds() {
|
||||||
if (buildCount >= skip) {
|
if (buildCount >= skip) {
|
||||||
skip = skip + 5;
|
skip = skip + 5;
|
||||||
noMoreBuilds = buildCount >= skip;
|
noMoreBuilds = buildCount <= skip;
|
||||||
try {
|
try {
|
||||||
const data = await get(`/applications/${id}/logs/build?skip=${skip}`);
|
const data = await get(`/applications/${id}/logs/build?skip=${skip}`);
|
||||||
builds = builds.concat(data.builds);
|
builds = data.builds
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
@@ -76,8 +71,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function loadBuild(build: any) {
|
function loadBuild(build: any) {
|
||||||
buildId = build;
|
$selectedBuildId = build;
|
||||||
return changeQueryParams(buildId);
|
return changeQueryParams($selectedBuildId);
|
||||||
|
}
|
||||||
|
async function resetQueue() {
|
||||||
|
const sure = confirm(
|
||||||
|
'It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? '
|
||||||
|
);
|
||||||
|
if (sure) {
|
||||||
|
try {
|
||||||
|
await post(`/internal/resetQueue`, {});
|
||||||
|
addToast({
|
||||||
|
message: 'Queue reset done.',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
await asyncSleep(500);
|
||||||
|
return window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
return errorNotification(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function generateBadgeColors(status: string) {
|
||||||
|
if (status === 'failed') {
|
||||||
|
return 'text-red-500';
|
||||||
|
} else if (status === 'running') {
|
||||||
|
return 'text-yellow-300';
|
||||||
|
} else if (status === 'success') {
|
||||||
|
return 'text-green-500';
|
||||||
|
} else if (status === 'canceled') {
|
||||||
|
return 'text-orange-500';
|
||||||
|
} else {
|
||||||
|
return 'text-white';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -134,60 +160,75 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
|
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
|
||||||
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
|
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
|
||||||
|
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}
|
||||||
|
>Reset Build Queue</button
|
||||||
|
>
|
||||||
<div class="top-4 md:sticky">
|
<div class="top-4 md:sticky">
|
||||||
{#each builds as build, index (build.id)}
|
{#each builds as build, index (build.id)}
|
||||||
<div
|
<div
|
||||||
data-tip={new Intl.DateTimeFormat('default', dateOptions).format(
|
id={`building-${build.id}`}
|
||||||
new Date(build.createdAt)
|
|
||||||
) + `\n${build.status}`}
|
|
||||||
on:click={() => loadBuild(build.id)}
|
on:click={() => loadBuild(build.id)}
|
||||||
class:rounded-tr={index === 0}
|
class:rounded-tr={index === 0}
|
||||||
class:rounded-br={index === builds.length - 1}
|
class:rounded-br={index === builds.length - 1}
|
||||||
class="tooltip tooltip-primary tooltip-top flex cursor-pointer items-center justify-center border-l-2 py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl"
|
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-300 hover:shadow-xl"
|
||||||
class:bg-coolgray-400={buildId === build.id}
|
class:bg-coolgray-200={$selectedBuildId === build.id}
|
||||||
class:border-red-500={build.status === 'failed'}
|
|
||||||
class:border-orange-500={build.status === 'canceled'}
|
|
||||||
class:border-green-500={build.status === 'success'}
|
|
||||||
class:border-yellow-500={build.status === 'running'}
|
|
||||||
>
|
>
|
||||||
<div class="flex-col px-2">
|
<div class="flex-col px-2 text-center min-w-[10rem]">
|
||||||
<div class="text-sm font-bold">
|
<div class="text-sm font-bold">
|
||||||
{build.branch || application.branch}
|
{build.branch || application.branch}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
{build.type}
|
{build.type}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class={`badge badge-sm text-xs uppercase rounded bg-coolgray-300 border-none font-bold ${generateBadgeColors(
|
||||||
|
build.status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{build.status}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-48 text-center text-xs">
|
<div class="w-48 text-center text-xs">
|
||||||
{#if build.status === 'running'}
|
{#if build.status === 'running'}
|
||||||
<div class="font-bold">{$t('application.build.running')}</div>
|
|
||||||
{:else if build.status === 'queued'}
|
|
||||||
<div class="font-bold">{$t('application.build.queued')}</div>
|
|
||||||
{:else}
|
|
||||||
<div>{build.since}</div>
|
|
||||||
<div>
|
<div>
|
||||||
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span>
|
<span class="font-bold text-xl"
|
||||||
|
>{build.elapsed}s</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else if build.status !== 'queued'}
|
||||||
|
<div>{day(build.updatedAt).utc().fromNow()}</div>
|
||||||
|
<div>
|
||||||
|
{$t('application.build.finished_in')}
|
||||||
|
<span class="font-bold"
|
||||||
|
>{day(build.updatedAt).utc().diff(day(build.createdAt)) / 1000}s</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Tooltip triggeredBy={`#building-${build.id}`}
|
||||||
|
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
|
||||||
|
`\n`}</Tooltip
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if !noMoreBuilds}
|
{#if !noMoreBuilds}
|
||||||
{#if buildCount > 5}
|
{#if buildCount > 5}
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2 pb-10">
|
||||||
<button disabled={noMoreBuilds} class=" btn btn-sm w-full" on:click={loadMoreBuilds}
|
<button
|
||||||
>{$t('application.build.load_more')}</button
|
disabled={noMoreBuilds}
|
||||||
|
class=" btn btn-sm w-full text-xs"
|
||||||
|
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 md:w-96">
|
<div class="flex-1 md:w-96">
|
||||||
{#if buildId}
|
{#if $selectedBuildId}
|
||||||
{#key buildId}
|
{#key $selectedBuildId}
|
||||||
<svelte:component this={BuildLog} {buildId} on:updateBuildStatus={updateBuildStatus} />
|
<svelte:component this={BuildLog} />
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
||||||
import { onMount, onDestroy } from '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 application: any = {};
|
||||||
let logsLoading = false;
|
let logsLoading = false;
|
||||||
let loadLogsInterval: any = null;
|
let loadLogsInterval: any = null;
|
||||||
@@ -15,7 +17,13 @@
|
|||||||
let followingLogs: any;
|
let followingLogs: any;
|
||||||
let logsEl: any;
|
let logsEl: any;
|
||||||
let position = 0;
|
let position = 0;
|
||||||
|
if (
|
||||||
|
!$status.application.isExited &&
|
||||||
|
!$status.application.isRestarting &&
|
||||||
|
!$status.application.isRunning
|
||||||
|
) {
|
||||||
|
goto(`/applications/${$page.params.id}/`, { replaceState: true });
|
||||||
|
}
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const response = await get(`/applications/${id}`);
|
const response = await get(`/applications/${id}`);
|
||||||
@@ -38,7 +46,6 @@
|
|||||||
logs = data.logs;
|
logs = data.logs;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
} finally {
|
} finally {
|
||||||
logsLoading = false;
|
logsLoading = false;
|
||||||
@@ -146,9 +153,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex justify-end sticky top-0 p-1 mx-1">
|
<div class="flex justify-end sticky top-0 p-1 mx-1">
|
||||||
<button
|
<button
|
||||||
|
id="follow"
|
||||||
on:click={followBuild}
|
on:click={followBuild}
|
||||||
class="bg-transparent btn btn-sm btn-link tooltip tooltip-primary tooltip-bottom"
|
class="bg-transparent btn btn-sm btn-link"
|
||||||
data-tip="Follow logs"
|
|
||||||
class:text-green-500={followingLogs}
|
class:text-green-500={followingLogs}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -168,6 +175,7 @@
|
|||||||
<line x1="16" y1="12" x2="12" y2="16" />
|
<line x1="16" y1="12" x2="12" y2="16" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user