Compare commits

...

87 Commits

Author SHA1 Message Date
Andras Bacsai
b80a519b80 updates 2023-09-05 11:31:55 +02:00
Andras Bacsai
2fa7ffc931 updates 2023-09-05 11:25:16 +02:00
Andras Bacsai
4abec14a21 updates 2023-09-05 11:24:42 +02:00
Andras Bacsai
18d0623011 force volume prune 2023-08-16 15:27:10 +02:00
Andras Bacsai
aa634c78d1 fix seed js 2023-08-16 15:26:36 +02:00
Andras Bacsai
a2d4373104 cleanup volumes as well 2023-08-16 15:26:33 +02:00
Andras Bacsai
702e16d643 rename rollback to upgrade 2023-08-14 09:23:33 +02:00
Andras Bacsai
3b25c8f96b fix: docker compose env file 2023-08-12 00:10:14 +02:00
Andras Bacsai
1c8c567791 fix: env variables in compose deplyoments 2023-07-27 12:40:58 +02:00
Andras Bacsai
807a3c9d66 fix: n8n double mount
version++
2023-07-26 10:38:36 +02:00
Andras Bacsai
2abd7bd7bb copy to persisten storage 2023-07-25 12:38:36 +02:00
Andras Bacsai
343957ab8b update backups 2023-07-25 12:33:05 +02:00
Andras Bacsai
49261308f7 update webhook 2023-07-25 12:00:31 +02:00
Andras Bacsai
d037409237 updates 2023-07-25 11:40:27 +02:00
Andras Bacsai
338cbf62a1 fix: encrypt decrypt 2023-07-25 10:48:31 +02:00
Andras Bacsai
4c51bffc7b save db backup on seed 2023-07-20 21:41:47 +02:00
Andras Bacsai
fd98ba8812 auto update every hour 2023-07-20 16:37:24 +02:00
Andras Bacsai
930251e9c8 autoupdate fixed 2023-07-20 16:19:54 +02:00
Andras Bacsai
7cd441266a remove console log 2023-07-20 14:52:32 +02:00
Andras Bacsai
990fb8ec15 fix 2023-07-20 14:52:16 +02:00
Andras Bacsai
3fe982b2f4 Merge pull request #1052 from f-kawamura/bugfix-http-git-source
[Bug] Added support for HTTP source URLs in Git source
2023-07-20 13:48:34 +02:00
Andras Bacsai
9dd874e959 Merge pull request #1147 from martijnmichel/v3
Update serviceFields.ts
2023-07-20 13:46:52 +02:00
Andras Bacsai
b91368223b update plausible docs 2023-07-20 13:39:29 +02:00
Andras Bacsai
139670372b updates for templates 2023-07-20 13:29:03 +02:00
Andras Bacsai
1c0769ad75 update tags + only download on service view 2023-07-20 13:12:54 +02:00
Andras Bacsai
e6cbcf98cb fix: cleanup plausible 2023-07-20 13:06:19 +02:00
martijnmichel
64b0481055 Update serviceFields.ts 2023-07-20 08:19:51 +02:00
Andras Bacsai
ce15161926 Merge pull request #1144 from coollabsio/feature/implement-basic-auth-handling
fix: traefik config + ui + api
2023-07-18 15:37:57 +02:00
Andras Bacsai
4003d4d894 Merge pull request #1071 from pascal-klesse/feature/implement-basic-auth-handling
feat: Implement basic auth for applications
2023-07-18 15:37:35 +02:00
Andras Bacsai
6e011025a7 fix: traefik config + ui + api 2023-07-18 15:34:05 +02:00
Andras Bacsai
6c0544adb2 Merge branch 'v2' into feature/implement-basic-auth-handling 2023-07-18 14:48:02 +02:00
Andras Bacsai
8e4f7c9065 remove console.log 2023-07-18 14:44:46 +02:00
Andras Bacsai
e71f890b54 Merge pull request #1084 from Geczy/main
add trycatch
2023-07-18 14:44:32 +02:00
Andras Bacsai
4dc35dea97 Merge pull request #1119 from jenishngl/patch-1
Fixing the help link - source.svelte line 263
2023-07-18 14:40:33 +02:00
Andras Bacsai
b63dfb4bcd feat: backup databases 2023-07-18 14:36:54 +02:00
Andras Bacsai
b2ffd9183b fix: set connection string on publicity change 2023-07-18 13:01:10 +02:00
Andras Bacsai
5cb0bcfd9b fix: increase query time for new services etc 2023-07-18 09:50:05 +02:00
Jenish J
1fbcfcaf74 Merge branch 'v3' into patch-1 2023-07-17 20:15:27 +05:30
Andras Bacsai
3ba44a1e23 backup db file 2023-07-17 15:17:18 +02:00
Andras Bacsai
de4efbb555 update GH actions 2023-07-17 14:36:33 +02:00
Andras Bacsai
6f443680f3 typo 2023-07-17 14:02:51 +02:00
Andras Bacsai
23b22e5ca8 wip: reencrypt everything 2023-07-17 13:50:26 +02:00
Andras Bacsai
1bba747ce5 update 2023-07-15 09:50:29 +02:00
Andras Bacsai
9ebfc6646e wip 2023-07-14 23:15:43 +02:00
Andras Bacsai
055ff6dbbd updates 2023-07-14 22:30:40 +02:00
Andras Bacsai
6430e7b288 fix seed 2023-07-14 22:19:52 +02:00
Andras Bacsai
87b0050161 update seed 2023-07-14 22:11:59 +02:00
Andras Bacsai
369d8b408d version ++ 2023-07-14 21:36:28 +02:00
Andras Bacsai
505abc592c fix: gh actions 2023-07-14 21:33:30 +02:00
Andras Bacsai
c9df812258 testing seeder 2023-07-14 21:30:08 +02:00
Andras Bacsai
0bfcf6b66f fix: gh actions 2023-07-14 21:06:26 +02:00
Andras Bacsai
67853acabd docs link update 2023-07-14 20:58:54 +02:00
Jenish J
79c98657b1 Update source.svelte line 263 - Correcting the help link
Updated to the correct help link, as the old link was not pointing to the correct section in the docs.coollabs.io page
2023-06-28 20:45:03 +05:30
Geczy
d1be7e44af add trycatch 2023-05-25 13:39:57 -04:00
Andras Bacsai
33b853b981 Merge pull request #1081 from coollabsio/next
v3.12.32
2023-05-25 09:14:17 +02:00
Andras Bacsai
e6063fb93a fix: force delete stucked destinations 2023-05-24 21:55:24 +02:00
Andras Bacsai
f30f23af59 fix: restart storage volumes 2023-05-24 20:51:33 +02:00
Andras Bacsai
d4798a3b22 fix: more aggressive cleanup 2023-05-24 20:34:40 +02:00
Pascal Klesse
eefc2a3d0e remove TODO 2023-05-15 09:33:03 +02:00
Pascal Klesse
d14ca724e9 feat: Implement basic auth for applications 2023-05-15 09:27:49 +02:00
f-kawamura
7b05aaffc3 fix: Added support for HTTP source URLs in Git source. Currently only support HTTPS 2023-04-24 16:12:47 +09:00
Andras Bacsai
f3beb5d8db Merge pull request #1045 from coollabsio/next
v3.12.31
2023-04-19 08:53:20 +02:00
Andras Bacsai
e86b916415 fix: application logs duplicate 2023-04-19 08:50:55 +02:00
Andras Bacsai
e14cc6f2f0 remove assignment for issues 2023-04-18 14:52:26 +02:00
Andras Bacsai
8c1eb94401 fix: remove git is not necessary for docker bp 2023-04-18 14:51:31 +02:00
Andras Bacsai
29fa421945 feat: add custom version/tag 2023-04-18 14:46:40 +02:00
Andras Bacsai
7cfe98d988 fix: fail build if no application found. 2023-04-18 14:32:29 +02:00
Andras Bacsai
e2314c350b update lock file 2023-04-18 14:27:54 +02:00
Andras Bacsai
3713b33578 Update templates
fix: non string inputs in templates
2023-04-18 14:22:46 +02:00
Andras Bacsai
e007a773fd Merge pull request #1030 from coollabsio/next
v3.12.30
2023-04-03 10:40:27 +02:00
Andras Bacsai
e2821118eb fix. removing git 2023-04-03 10:21:08 +02:00
Andras Bacsai
4c8e73ac86 fix: harder to remove destinations and sources 2023-04-03 09:55:13 +02:00
Andras Bacsai
cb980fb814 fix: docker compose generator 2023-04-03 08:59:48 +02:00
Andras Bacsai
41c84e3642 Merge pull request #1001 from coollabsio/next
v3.12.29
2023-03-20 13:57:01 +01:00
Andras Bacsai
2bad98424f switch back to aarch-runners 2023-03-20 13:49:41 +01:00
Andras Bacsai
bc6b1e2dea fix: remove .git dir from final image 2023-03-20 13:05:53 +01:00
Andras Bacsai
911c15d1be update versions 2023-03-20 12:44:45 +01:00
Andras Bacsai
f79d570870 fix: gitea 2023-03-20 12:28:23 +01:00
Andras Bacsai
7fffa9fba5 Merge branch 'main' into next 2023-03-20 12:05:21 +01:00
Andras Bacsai
cbd634fb99 Update README.md 2023-03-17 15:31:00 +01:00
Andras Bacsai
7ae7436d4f Update staging-release.yml 2023-03-17 15:27:16 +01:00
Andras Bacsai
641bada100 ignore dockerhub releases 2023-03-16 13:54:58 +01:00
Andras Bacsai
3416d8d88e only arm 2023-03-16 13:42:19 +01:00
Andras Bacsai
0bb503368b concurrency 2023-03-16 13:37:49 +01:00
Andras Bacsai
ac3a77c3c7 no qemu 2023-03-16 13:35:04 +01:00
Andras Bacsai
79b4178d76 vcpu increase 2023-03-16 13:32:48 +01:00
Andras Bacsai
42a61296d7 test buildjet 2023-03-16 13:29:13 +01:00
68 changed files with 8092 additions and 7017 deletions

View File

@@ -2,9 +2,6 @@ name: 🐞 Bug report
description: Create a bug report to help us improve coolify description: Create a bug report to help us improve coolify
title: "[Bug]: " title: "[Bug]: "
labels: [Bug] labels: [Bug]
assignees:
- andrasbacsai
- vasani-arpit
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -2,9 +2,6 @@ name: 🛠️ Feature request
description: Suggest an idea to improve coolify description: Suggest an idea to improve coolify
title: '[Feature]: ' title: '[Feature]: '
labels: [Enhancement] labels: [Enhancement]
assignees:
- andrasbacsai
- vasani-arpit
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -1,111 +0,0 @@
name: Production Release to DockerHub
on:
release:
types: [released]
jobs:
arm64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
- 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:${{steps.package-version.outputs.current-version}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
amd64:
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}}
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
aarch64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
- 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/aarch64
push: true
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-aarch64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-aarch64,mode=max
merge-manifest:
runs-on: ubuntu-latest
needs: [amd64, arm64, aarch64]
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 buildx imagetools create --append coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --append coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64 --tag coollabsio/coolify:${{steps.package-version.outputs.current-version}}
docker buildx imagetools create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --tag coollabsio/coolify:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -14,6 +14,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: "v3"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -44,6 +46,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: "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
@@ -95,7 +99,6 @@ jobs:
- name: Create & publish manifest - name: Create & publish manifest
run: | run: |
docker buildx imagetools create --append ${{ fromJSON(steps.meta.outputs.json).tags[0] }}-aarch64 --tag ${{ fromJSON(steps.meta.outputs.json).tags[0] }} docker buildx imagetools create --append ${{ fromJSON(steps.meta.outputs.json).tags[0] }}-aarch64 --tag ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:

View File

@@ -1,110 +0,0 @@
name: Release Candidate to ghcr.io
on:
release:
types: [prereleased]
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify"
jobs:
amd64:
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 ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
aarch64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/aarch64
push: true
tags: ${{ steps.meta.outputs.tags }}-aarch64
labels: ${{ steps.meta.outputs.labels }}
merge-manifest:
runs-on: ubuntu-latest
needs: [amd64, aarch64]
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 ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ steps.meta.outputs.tags }}-aarch64 --tag ${{ steps.meta.outputs.tags }}
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -1,86 +0,0 @@
name: Staging Release to DockerHub
on:
push:
branches:
- "next"
jobs:
arm64:
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: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:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: coollabsio/coolify:next
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
merge-manifest:
runs-on: ubuntu-latest
needs: [arm64, amd64]
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 buildx imagetools create --append coollabsio/coolify:next-arm64 --tag coollabsio/coolify:next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -1,23 +1,23 @@
name: Staging Release to ghcr.io name: Staging Release to ghcr.io
concurrency:
group: staging_environment
cancel-in-progress: true
on: on:
push: push:
branches-ignore: branches:
- "main" - "v3"
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify" IMAGE_NAME: "coollabsio/coolify"
jobs: jobs:
amd64: amd64:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: "next" ref: "v3"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io - name: Login to ghcr.io
@@ -40,14 +40,13 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
aarch64: aarch64:
runs-on: [self-hosted, arm64] runs-on:
group: aarch-runners
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: "next" ref: "v3"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io - name: Login to ghcr.io

11
.vscode/settings.json vendored
View File

@@ -18,5 +18,14 @@
"ts", "ts",
"json" "json"
], ],
"i18n-ally.extract.autoDetect": true "i18n-ally.extract.autoDetect": true,
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true
},
"hide-files.files": []
} }

View File

@@ -24,7 +24,7 @@ ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.27.0 ARG PACK_VERSION=0.27.0
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3 RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3 vim
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/ RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION} RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION} RUN npm install -g npm@${PNPM_VERSION}

View File

@@ -16,7 +16,7 @@ If you have a new service / build pack you would like to add, raise an idea [her
## How to install ## How to install
For more details goto the [docs](https://docs.coollabs.io/coolify/installation). For more details goto the [docs](https://docs.coollabs.io/coolify-v3/installation).
Installation is automated with the following command: Installation is automated with the following command:
@@ -79,9 +79,9 @@ Deploy your resource to:
### Services ### Services
- [Appwrite](https://appwrite.io) - [Appwrite](https://appwrite.io)
- [WordPress](https://docs.coollabs.io/coolify/services/wordpress) - [WordPress](https://docs.coollabs.io/coolify-v3/services/wordpress)
- [Ghost](https://ghost.org) - [Ghost](https://ghost.org)
- [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics) - [Plausible Analytics](https://docs.coollabs.io/coolify-v3/services/plausible-analytics)
- [NocoDB](https://nocodb.com) - [NocoDB](https://nocodb.com)
- [VSCode Server](https://github.com/cdr/code-server) - [VSCode Server](https://github.com/cdr/code-server)
- [MinIO](https://min.io) - [MinIO](https://min.io)
@@ -100,7 +100,7 @@ Deploy your resource to:
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai) - Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai) - Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai) - Twitter: [@andrasbacsai](https://twitter.com/heyandras)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io) - Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://coollabs.io/discord) - Discord: [Invitation](https://coollabs.io/discord)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -26,8 +26,6 @@
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.2.1", "@ladjs/graceful": "3.2.1",
"@prisma/client": "4.8.1", "@prisma/client": "4.8.1",
"@sentry/node": "7.30.0",
"@sentry/tracing": "7.30.0",
"axe": "11.2.1", "axe": "11.2.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bree": "9.1.3", "bree": "9.1.3",

View File

@@ -0,0 +1,29 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "basicAuthPw" TEXT;
ALTER TABLE "Application" ADD COLUMN "basicAuthUser" TEXT;
-- 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,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"isHttp2" BOOLEAN NOT NULL DEFAULT false,
"basicAuth" 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", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -135,6 +135,8 @@ model Application {
dockerRegistryId String? dockerRegistryId String?
dockerRegistryImageName String? dockerRegistryImageName String?
simpleDockerfile String? simpleDockerfile String?
basicAuthUser String?
basicAuthPw String?
persistentStorage ApplicationPersistentStorage[] persistentStorage ApplicationPersistentStorage[]
secrets Secret[] secrets Secret[]
@@ -187,6 +189,7 @@ model ApplicationSettings {
isDBBranching Boolean @default(false) isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false) isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false) isHttp2 Boolean @default(false)
basicAuth 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])

View File

@@ -12,7 +12,7 @@ async function main() {
await prisma.setting.create({ await prisma.setting.create({
data: { data: {
id: '0', id: '0',
arch: process.arch, arch: process.arch
} }
}); });
} else { } else {
@@ -81,16 +81,295 @@ async function main() {
}); });
} }
// Set new preview secrets // Set new preview secrets
const secrets = await prisma.secret.findMany({ where: { isPRMRSecret: false } }) const secrets = await prisma.secret.findMany({ where: { isPRMRSecret: false } });
if (secrets.length > 0) { if (secrets.length > 0) {
for (const secret of secrets) { for (const secret of secrets) {
const previewSecrets = await prisma.secret.findMany({ where: { applicationId: secret.applicationId, name: secret.name, isPRMRSecret: true } }) const previewSecrets = await prisma.secret.findMany({
where: { applicationId: secret.applicationId, name: secret.name, isPRMRSecret: true }
});
if (previewSecrets.length === 0) { if (previewSecrets.length === 0) {
await prisma.secret.create({ data: { ...secret, id: undefined, isPRMRSecret: true } }) await prisma.secret.create({ data: { ...secret, id: undefined, isPRMRSecret: true } });
} }
} }
} }
} }
async function reEncryptSecrets() {
const { execaCommand } = await import('execa');
const image = await execaCommand("docker inspect coolify --format '{{ .Config.Image }}'", {
shell: true
});
const version = image.stdout.split(':')[1] ?? null;
const date = new Date().getTime();
let backupfile = `/app/db/prod.db_${date}`;
if (version) {
backupfile = `/app/db/prod.db_${version}_${date}`;
}
await execaCommand('env | grep "^COOLIFY" | sort > .env', {
shell: true
});
const secretOld = process.env['COOLIFY_SECRET_KEY'];
let secretNew = process.env['COOLIFY_SECRET_KEY_BETTER'];
if (!secretNew) {
console.log('No COOLIFY_SECRET_KEY_BETTER found... Generating new one...');
const { stdout: newKey } = await execaCommand(
'openssl rand -base64 1024 | sha256sum | base64 | head -c 32',
{ shell: true }
);
secretNew = newKey;
}
if (secretOld !== secretNew) {
console.log(`Backup database to ${backupfile}.`);
await execaCommand(`cp /app/db/prod.db ${backupfile}`, { shell: true });
console.log(
'Secrets (COOLIFY_SECRET_KEY & COOLIFY_SECRET_KEY_BETTER) are different, so re-encrypting everything...'
);
await execaCommand(`sed -i '/COOLIFY_SECRET_KEY=/d' .env`, { shell: true });
await execaCommand(`sed -i '/COOLIFY_SECRET_KEY_BETTER=/d' .env`, { shell: true });
await execaCommand(`echo "COOLIFY_SECRET_KEY=${secretNew}" >> .env`, { shell: true });
await execaCommand('echo "COOLIFY_SECRET_KEY_BETTER=' + secretNew + '" >> .env ', {
shell: true
});
await execaCommand(`echo "COOLIFY_SECRET_KEY_OLD_${date}=${secretOld}" >> .env`, {
shell: true
});
const transactions = [];
const secrets = await prisma.secret.findMany();
if (secrets.length > 0) {
for (const secret of secrets) {
try {
const value = decrypt(secret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.secret.update({
where: { id: secret.id },
data: { value: newValue }
})
);
} catch (e) {
console.log(e);
}
}
}
const serviceSecrets = await prisma.serviceSecret.findMany();
if (serviceSecrets.length > 0) {
for (const secret of serviceSecrets) {
try {
const value = decrypt(secret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.serviceSecret.update({
where: { id: secret.id },
data: { value: newValue }
})
);
} catch (e) {
console.log(e);
}
}
}
const gitlabApps = await prisma.gitlabApp.findMany();
if (gitlabApps.length > 0) {
for (const gitlabApp of gitlabApps) {
try {
const value = decrypt(gitlabApp.privateSshKey, secretOld);
const newValue = encrypt(value, secretNew);
const appSecret = decrypt(gitlabApp.appSecret, secretOld);
const newAppSecret = encrypt(appSecret, secretNew);
transactions.push(
prisma.gitlabApp.update({
where: { id: gitlabApp.id },
data: { privateSshKey: newValue, appSecret: newAppSecret }
})
);
} catch (e) {
console.log(e);
}
}
}
const githubApps = await prisma.githubApp.findMany();
if (githubApps.length > 0) {
for (const githubApp of githubApps) {
try {
const clientSecret = decrypt(githubApp.clientSecret, secretOld);
const newClientSecret = encrypt(clientSecret, secretNew);
const webhookSecret = decrypt(githubApp.webhookSecret, secretOld);
const newWebhookSecret = encrypt(webhookSecret, secretNew);
const privateKey = decrypt(githubApp.privateKey, secretOld);
const newPrivateKey = encrypt(privateKey, secretNew);
transactions.push(
prisma.githubApp.update({
where: { id: githubApp.id },
data: {
clientSecret: newClientSecret,
webhookSecret: newWebhookSecret,
privateKey: newPrivateKey
}
})
);
} catch (e) {
console.log(e);
}
}
}
const databases = await prisma.database.findMany();
if (databases.length > 0) {
for (const database of databases) {
try {
const dbUserPassword = decrypt(database.dbUserPassword, secretOld);
const newDbUserPassword = encrypt(dbUserPassword, secretNew);
const rootUserPassword = decrypt(database.rootUserPassword, secretOld);
const newRootUserPassword = encrypt(rootUserPassword, secretNew);
transactions.push(
prisma.database.update({
where: { id: database.id },
data: {
dbUserPassword: newDbUserPassword,
rootUserPassword: newRootUserPassword
}
})
);
} catch (e) {
console.log(e);
}
}
}
const databaseSecrets = await prisma.databaseSecret.findMany();
if (databaseSecrets.length > 0) {
for (const databaseSecret of databaseSecrets) {
try {
const value = decrypt(databaseSecret.value, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.databaseSecret.update({
where: { id: databaseSecret.id },
data: { value: newValue }
})
);
} catch (e) {
console.log(e);
}
}
}
const wordpresses = await prisma.wordpress.findMany();
if (wordpresses.length > 0) {
for (const wordpress of wordpresses) {
try {
const value = decrypt(wordpress.ftpHostKey, secretOld);
const newValue = encrypt(value, secretNew);
const ftpHostKeyPrivate = decrypt(wordpress.ftpHostKeyPrivate, secretOld);
const newFtpHostKeyPrivate = encrypt(ftpHostKeyPrivate, secretNew);
let newFtpPassword = undefined;
if (wordpress.ftpPassword != null) {
const ftpPassword = decrypt(wordpress.ftpPassword, secretOld);
newFtpPassword = encrypt(ftpPassword, secretNew);
}
transactions.push(
prisma.wordpress.update({
where: { id: wordpress.id },
data: {
ftpHostKey: newValue,
ftpHostKeyPrivate: newFtpHostKeyPrivate,
ftpPassword: newFtpPassword
}
})
);
} catch (e) {
console.log(e);
}
}
}
const sshKeys = await prisma.sshKey.findMany();
if (sshKeys.length > 0) {
for (const key of sshKeys) {
try {
const value = decrypt(key.privateKey, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.sshKey.update({
where: { id: key.id },
data: {
privateKey: newValue
}
})
);
} catch (e) {
console.log(e);
}
}
}
const dockerRegistries = await prisma.dockerRegistry.findMany();
if (dockerRegistries.length > 0) {
for (const registry of dockerRegistries) {
try {
const value = decrypt(registry.password, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.dockerRegistry.update({
where: { id: registry.id },
data: {
password: newValue
}
})
);
} catch (e) {
console.log(e);
}
}
}
const certificates = await prisma.certificate.findMany();
if (certificates.length > 0) {
for (const certificate of certificates) {
try {
const value = decrypt(certificate.key, secretOld);
const newValue = encrypt(value, secretNew);
transactions.push(
prisma.certificate.update({
where: { id: certificate.id },
data: {
key: newValue
}
})
);
} catch (e) {
console.log(e);
}
}
}
await prisma.$transaction(transactions);
} else {
console.log('secrets are the same, so no need to re-encrypt');
}
}
const encrypt = (text, secret) => {
if (text && secret) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, secret, iv);
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
return JSON.stringify({
iv: iv.toString('hex'),
content: encrypted.toString('hex')
});
}
};
const decrypt = (hashString, secret) => {
if (hashString && secret) {
const hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv(algorithm, secret, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([
decipher.update(Buffer.from(hash.content, 'hex')),
decipher.final()
]);
if (/<2F>/.test(decrpyted.toString())) {
throw new Error('Invalid secret. Skipping...');
}
return decrpyted.toString();
}
};
main() main()
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
@@ -99,15 +378,11 @@ main()
.finally(async () => { .finally(async () => {
await prisma.$disconnect(); await prisma.$disconnect();
}); });
reEncryptSecrets()
const encrypt = (text) => { .catch((e) => {
if (text) { console.error(e);
const iv = crypto.randomBytes(16); process.exit(1);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv); })
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); .finally(async () => {
return JSON.stringify({ await prisma.$disconnect();
iv: iv.toString('hex'), });
content: encrypted.toString('hex')
});
}
};

View File

@@ -1,14 +1,19 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import serve from '@fastify/static';
import env from '@fastify/env';
import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import cookie from '@fastify/cookie';
import cors from '@fastify/cors';
import env from '@fastify/env';
import multipart from '@fastify/multipart';
import serve from '@fastify/static';
import Fastify from 'fastify';
import socketIO from 'fastify-socket.io'; import socketIO from 'fastify-socket.io';
import path, { join } from 'path';
import socketIOServer from './realtime'; import socketIOServer from './realtime';
import Graceful from '@ladjs/graceful';
import { compareVersions } from 'compare-versions';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
import { import {
cleanupDockerStorage, cleanupDockerStorage,
createRemoteEngineConfiguration, createRemoteEngineConfiguration,
@@ -18,26 +23,20 @@ import {
isDev, isDev,
listSettings, listSettings,
prisma, prisma,
sentryDSN,
startTraefikProxy, startTraefikProxy,
startTraefikTCPProxy, startTraefikTCPProxy,
version version
} from './lib/common'; } from './lib/common';
import { scheduler } from './lib/scheduler';
import { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful';
import yaml from 'js-yaml';
import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { checkContainer } from './lib/docker'; import { checkContainer } from './lib/docker';
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib'; import { scheduler } from './lib/scheduler';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers'; import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
import * as Sentry from '@sentry/node';
declare module 'fastify' { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
config: { config: {
COOLIFY_APP_ID: string; COOLIFY_APP_ID: string;
COOLIFY_SECRET_KEY: string; COOLIFY_SECRET_KEY: string;
COOLIFY_SECRET_KEY_BETTER: string | null;
COOLIFY_DATABASE_URL: string; COOLIFY_DATABASE_URL: string;
COOLIFY_IS_ON: string; COOLIFY_IS_ON: string;
COOLIFY_WHITE_LABELED: string; COOLIFY_WHITE_LABELED: string;
@@ -67,6 +66,10 @@ const host = '0.0.0.0';
COOLIFY_SECRET_KEY: { COOLIFY_SECRET_KEY: {
type: 'string' type: 'string'
}, },
COOLIFY_SECRET_KEY_BETTER: {
type: 'string',
default: null
},
COOLIFY_DATABASE_URL: { COOLIFY_DATABASE_URL: {
type: 'string', type: 'string',
default: 'file:../db/dev.db' default: 'file:../db/dev.db'
@@ -164,7 +167,7 @@ const host = '0.0.0.0';
// autoUpdater // autoUpdater
setInterval(async () => { setInterval(async () => {
await autoUpdater(); await autoUpdater();
}, 60000 * 15); }, 60000 * 60);
// cleanupStorage // cleanupStorage
setInterval(async () => { setInterval(async () => {
@@ -185,17 +188,17 @@ const host = '0.0.0.0';
// Refresh and check templates // Refresh and check templates
setInterval(async () => { setInterval(async () => {
await refreshTemplates(); await refreshTemplates();
}, 60000); }, 60000 * 10);
setInterval(async () => { setInterval(async () => {
await refreshTags(); await refreshTags();
}, 60000); }, 60000 * 10);
setInterval( setInterval(
async () => { async () => {
await migrateServicesToNewTemplate(); await migrateServicesToNewTemplate();
}, },
isDev ? 10000 : 60000 isDev ? 10000 : 60000 * 10
); );
setInterval(async () => { setInterval(async () => {
@@ -206,7 +209,9 @@ const host = '0.0.0.0';
getTagsTemplates(), getTagsTemplates(),
getArch(), getArch(),
getIPAddress(), getIPAddress(),
configureRemoteDockers() configureRemoteDockers(),
refreshTemplates(),
refreshTags()
// cleanupStuckedContainers() // cleanupStuckedContainers()
]); ]);
} catch (error) { } catch (error) {
@@ -230,7 +235,7 @@ async function getIPAddress() {
console.log(`Getting public IPv6 address...`); console.log(`Getting public IPv6 address...`);
await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } }); await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } });
} }
} catch (error) {} } catch (error) { }
} }
async function getTagsTemplates() { async function getTagsTemplates() {
const { default: got } = await import('got'); const { default: got } = await import('got');
@@ -242,7 +247,7 @@ async function getTagsTemplates() {
if (await fs.stat('./testTemplate.yaml')) { if (await fs.stat('./testTemplate.yaml')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8')); templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
} }
} catch (error) {} } catch (error) { }
try { try {
if (await fs.stat('./testTags.json')) { if (await fs.stat('./testTags.json')) {
const testTags = await fs.readFile('./testTags.json', 'utf8'); const testTags = await fs.readFile('./testTags.json', 'utf8');
@@ -250,7 +255,7 @@ async function getTagsTemplates() {
tags = JSON.stringify(JSON.parse(tags).concat(JSON.parse(testTags))); tags = JSON.stringify(JSON.parse(tags).concat(JSON.parse(testTags)));
} }
} }
} catch (error) {} } catch (error) { }
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates))); await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates)));
await fs.writeFile('./tags.json', tags); await fs.writeFile('./tags.json', tags);
@@ -276,9 +281,6 @@ async function initServer() {
if (settings.doNotTrack === true) { if (settings.doNotTrack === true) {
console.log('[000] Telemetry disabled...'); console.log('[000] Telemetry disabled...');
} else { } else {
if (settings.sentryDSN !== sentryDSN) {
await prisma.setting.update({ where: { id: '0' }, data: { sentryDSN } });
}
// Initialize Sentry // Initialize Sentry
// Sentry.init({ // Sentry.init({
// dsn: sentryDSN, // dsn: sentryDSN,
@@ -293,7 +295,7 @@ async function initServer() {
try { try {
console.log(`[001] Initializing server...`); console.log(`[001] Initializing server...`);
await executeCommand({ command: `docker network create --attachable coolify` }); await executeCommand({ command: `docker network create --attachable coolify` });
} catch (error) {} } catch (error) { }
try { try {
console.log(`[002] Cleanup stucked builds...`); console.log(`[002] Cleanup stucked builds...`);
const isOlder = compareVersions('3.8.1', version); const isOlder = compareVersions('3.8.1', version);
@@ -303,10 +305,10 @@ async function initServer() {
data: { status: 'failed' } data: { status: 'failed' }
}); });
} }
} catch (error) {} } catch (error) { }
try { try {
console.log('[003] Cleaning up old build sources under /tmp/build-sources/...'); console.log('[003] Cleaning up old build sources under /tmp/build-sources/...');
await fs.rm('/tmp/build-sources', { recursive: true, force: true }); if (!isDev) await fs.rm('/tmp/build-sources', { recursive: true, force: true });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@@ -319,7 +321,7 @@ async function getArch() {
console.log(`Getting architecture...`); console.log(`Getting architecture...`);
await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } }); await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } });
} }
} catch (error) {} } catch (error) { }
} }
async function cleanupStuckedContainers() { async function cleanupStuckedContainers() {
@@ -402,14 +404,21 @@ async function autoUpdater() {
if (!isDev) { if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) { if (isAutoUpdateEnabled) {
await executeCommand({ command: `docker pull ghcr.io/coollabsio/coolify:${latestVersion}` }); let image = `ghcr.io/coollabsio/coolify:${latestVersion}`;
await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }); try {
await executeCommand({ command: `docker pull ${image}` });
} catch (error) {
image = `coollabsio/coolify:${latestVersion}`;
await executeCommand({ command: `docker pull ${image}` });
}
await executeCommand({ shell: true, command: `ls .env || env | grep "^COOLIFY" | sort > .env` });
await executeCommand({ await executeCommand({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
}); });
await executeCommand({ await executeCommand({
shell: true, shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ghcr.io/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"` command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep "^COOLIFY" | sort > .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 {
@@ -475,7 +484,7 @@ async function checkProxies() {
} }
try { try {
await createRemoteEngineConfiguration(docker.id); await createRemoteEngineConfiguration(docker.id);
} catch (error) {} } catch (error) { }
} }
} }
// TCP Proxies // TCP Proxies
@@ -514,7 +523,7 @@ async function checkProxies() {
// await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000); // await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
// } // }
// } // }
} catch (error) {} } catch (error) { }
} }
async function copySSLCertificates() { async function copySSLCertificates() {
@@ -546,7 +555,11 @@ async function copySSLCertificates() {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} finally { } finally {
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` }); try {
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` });
} catch (e) {
console.log(e);
}
} }
} }
@@ -604,53 +617,54 @@ async function cleanupStorage() {
if (!destination.remoteVerified) continue; if (!destination.remoteVerified) continue;
enginesDone.add(destination.remoteIpAddress); enginesDone.add(destination.remoteIpAddress);
} }
let lowDiskSpace = false; await cleanupDockerStorage(destination.id);
try { // let lowDiskSpace = false;
let stdout = null; // try {
if (!isDev) { // let stdout = null;
const output = await executeCommand({ // if (!isDev) {
dockerId: destination.id, // const output = await executeCommand({
command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`, // dockerId: destination.id,
shell: true // command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`,
}); // shell: true
stdout = output.stdout; // });
} else { // stdout = output.stdout;
const output = await executeCommand({ // } else {
command: `df -kPT /` // const output = await executeCommand({
}); // command: `df -kPT /`
stdout = output.stdout; // });
} // stdout = output.stdout;
let lines = stdout.trim().split('\n'); // }
let header = lines[0]; // let lines = stdout.trim().split('\n');
let regex = // let header = lines[0];
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g; // let regex =
const boundaries = []; // /^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
let match; // const boundaries = [];
// let match;
while ((match = regex.exec(header))) { // while ((match = regex.exec(header))) {
boundaries.push(match[0].length); // boundaries.push(match[0].length);
} // }
boundaries[boundaries.length - 1] = -1; // boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => { // const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => { // const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line; // const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary); // line = line.slice(boundary);
return column.trim(); // return column.trim();
}); // });
return { // return {
capacity: Number.parseInt(cl[5], 10) / 100 // capacity: Number.parseInt(cl[5], 10) / 100
}; // };
}); // });
if (data.length > 0) { // if (data.length > 0) {
const { capacity } = data[0]; // const { capacity } = data[0];
if (capacity > 0.8) { // if (capacity > 0.8) {
lowDiskSpace = true; // lowDiskSpace = true;
} // }
} // }
} catch (error) {} // } catch (error) {}
if (lowDiskSpace) { // if (lowDiskSpace) {
await cleanupDockerStorage(destination.id); // await cleanupDockerStorage(destination.id);
} // }
} }
} }

View File

@@ -69,7 +69,15 @@ import * as buildpacks from '../lib/buildPacks';
teams: true teams: true
} }
}); });
if (!application) {
await prisma.build.update({
where: { id: queueBuild.id },
data: {
status: 'failed'
}
});
throw new Error('Application not found');
}
let { let {
id: buildId, id: buildId,
type, type,
@@ -111,7 +119,7 @@ import * as buildpacks from '../lib/buildPacks';
.replace('-app', '')}:${storage.path}`; .replace('-app', '')}:${storage.path}`;
} }
if (storage.hostPath) { if (storage.hostPath) {
return `${storage.hostPath}:${storage.path}` return `${storage.hostPath}:${storage.path}`;
} }
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
@@ -163,17 +171,24 @@ import * as buildpacks from '../lib/buildPacks';
port: exposePort ? `${exposePort}:${port}` : port port: exposePort ? `${exposePort}:${port}` : port
}); });
try { try {
const composeVolumes = volumes.filter(v => { const composeVolumes = volumes
if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) { .filter((v) => {
return v; if (
} !v.startsWith('.') &&
}).map((volume) => { !v.startsWith('..') &&
return { !v.startsWith('/') &&
[`${volume.split(':')[0]}`]: { !v.startsWith('~')
name: volume.split(':')[0] ) {
return v;
} }
}; })
}); .map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = { const composeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -240,7 +255,7 @@ import * as buildpacks from '../lib/buildPacks';
applicationId: application.id applicationId: application.id
}); });
} }
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
return; return;
} }
try { try {
@@ -263,7 +278,7 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error.stderr, buildId, applicationId }); await saveBuildLog({ line: error.stderr, buildId, applicationId });
} }
} finally { } finally {
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ await prisma.build.update({
where: { id: buildId }, where: { id: buildId },
data: { status: 'success' } data: { status: 'success' }
@@ -389,14 +404,14 @@ import * as buildpacks from '../lib/buildPacks';
.replace('-app', '')}:${storage.path}`; .replace('-app', '')}:${storage.path}`;
} }
if (storage.hostPath) { if (storage.hostPath) {
return `${storage.hostPath}:${storage.path}` return `${storage.hostPath}:${storage.path}`;
} }
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
try { try {
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration); dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration);
} catch (error) { } } catch (error) {}
let deployNeeded = true; let deployNeeded = true;
let destinationType; let destinationType;
@@ -463,7 +478,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) {}
if (!pullmergeRequestId) { if (!pullmergeRequestId) {
if (configHash !== currentHash) { if (configHash !== currentHash) {
@@ -504,8 +519,9 @@ import * as buildpacks from '../lib/buildPacks';
try { try {
await executeCommand({ await executeCommand({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker ${location ? `--config ${location}` : '' command: `docker ${
} pull ${imageName}:${customTag}` location ? `--config ${location}` : ''
} pull ${imageName}:${customTag}`
}); });
imageFoundRemotely = true; imageFoundRemotely = true;
} catch (error) { } catch (error) {
@@ -668,8 +684,9 @@ import * as buildpacks from '../lib/buildPacks';
try { try {
const { stdout: containers } = await executeCommand({ const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId, dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId command: `docker ps -a --filter 'label=com.docker.compose.service=${
}' --format {{.ID}}` pullmergeRequestId ? imageId : applicationId
}' --format {{.ID}}`
}); });
if (containers) { if (containers) {
const containerArray = containers.split('\n'); const containerArray = containers.split('\n');
@@ -701,17 +718,24 @@ import * as buildpacks from '../lib/buildPacks';
await saveDockerRegistryCredentials({ url, username, password, workdir }); await saveDockerRegistryCredentials({ url, username, password, workdir });
} }
try { try {
const composeVolumes = volumes.filter(v => { const composeVolumes = volumes
if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) { .filter((v) => {
return v; if (
} !v.startsWith('.') &&
}).map((volume) => { !v.startsWith('..') &&
return { !v.startsWith('/') &&
[`${volume.split(':')[0]}`]: { !v.startsWith('~')
name: volume.split(':')[0] ) {
return v;
} }
}; })
}); .map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = { const composeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -782,7 +806,7 @@ import * as buildpacks from '../lib/buildPacks';
applicationId: application.id applicationId: application.id
}); });
} }
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
return; return;
} }
try { try {
@@ -803,7 +827,7 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error.stderr, buildId, applicationId }); await saveBuildLog({ line: error.stderr, buildId, applicationId });
} }
} finally { } finally {
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
} }
}); });

View File

@@ -86,19 +86,20 @@ export async function migrateServicesToNewTemplate() {
if (template.variables) { if (template.variables) {
if (template.variables.length > 0) { if (template.variables.length > 0) {
for (const variable of template.variables) { for (const variable of template.variables) {
const { defaultValue } = variable; let { defaultValue } = variable;
defaultValue = defaultValue.toString();
const regex = /^\$\$.*\((\d+)\)$/g; const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined const length = Number(regex.exec(defaultValue)?.[1]) || undefined
if (variable.defaultValue.startsWith('$$generate_password')) { if (defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length }); variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) { } else if (defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true }); variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) { } else if (defaultValue.startsWith('$$generate_username')) {
variable.value = cuid(); variable.value = cuid();
} else if (variable.defaultValue.startsWith('$$generate_token')) { } else if (defaultValue.startsWith('$$generate_token')) {
variable.value = generateToken() variable.value = generateToken()
} else { } else {
variable.value = variable.defaultValue || ''; variable.value = defaultValue || '';
} }
} }
} }

View File

@@ -804,6 +804,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
} }
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push('RUN rm -fr .git');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });
} }
@@ -821,6 +822,7 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
} }
Dockerfile.push(`COPY *.json *.mix.js /app/`); Dockerfile.push(`COPY *.json *.mix.js /app/`);
Dockerfile.push(`COPY resources /app/resources`); Dockerfile.push(`COPY resources /app/resources`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`RUN yarn install && yarn production`); Dockerfile.push(`RUN yarn install && yarn production`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });
@@ -842,6 +844,7 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
Dockerfile.push('RUN cargo install cargo-chef'); Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`); Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json'); Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
Dockerfile.push('RUN rm -fr .git');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });
} }

View File

@@ -19,7 +19,9 @@ export default async function (data) {
dockerComposeConfiguration, dockerComposeConfiguration,
dockerComposeFileLocation dockerComposeFileLocation
} = data; } = data;
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`; const baseDir = `${workdir}${baseDirectory}`;
const envFile = `${baseDir}/.env`;
const fileYaml = `${baseDir}${dockerComposeFileLocation}`;
const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8'); const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8');
const dockerComposeYaml = yaml.load(dockerComposeRaw); const dockerComposeYaml = yaml.load(dockerComposeRaw);
if (!dockerComposeYaml.services) { if (!dockerComposeYaml.services) {
@@ -31,7 +33,7 @@ export default async function (data) {
envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)]; envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)];
buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)]; buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)];
} }
await fs.writeFile(envFile, envs.join('\n'));
const composeVolumes = []; const composeVolumes = [];
if (volumes.length > 0) { if (volumes.length > 0) {
for (const volume of volumes) { for (const volume of volumes) {
@@ -47,30 +49,41 @@ export default async function (data) {
for (let [key, value] of Object.entries(dockerComposeYaml.services)) { for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}`; value['container_name'] = `${applicationId}-${key}`;
let environment = typeof value['environment'] === 'undefined' ? [] : value['environment']; if (value['env_file']) {
if (Object.keys(environment).length > 0) { delete value['env_file'];
environment = Object.entries(environment).map(([key, value]) => `${key}=${value}`);
} }
value['environment'] = [...environment, ...envs]; value['env_file'] = [envFile];
// let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'];
// let finalEnvs = [...envs];
// if (Object.keys(environment).length > 0) {
// for (const arg of Object.keys(environment)) {
// const [key, _] = arg.split('=');
// if (finalEnvs.filter((env) => env.startsWith(key)).length === 0) {
// finalEnvs.push(arg);
// }
// }
// }
// value['environment'] = [...finalEnvs];
let build = typeof value['build'] === 'undefined' ? [] : value['build']; let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (typeof build === 'string') { if (typeof build === 'string') {
build = { context: build }; build = { context: build };
} }
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args']; const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
let finalArgs = [...buildEnvs]; let finalBuildArgs = [...buildEnvs];
if (Object.keys(buildArgs).length > 0) { if (Object.keys(buildArgs).length > 0) {
for (const arg of buildArgs) { for (const arg of Object.keys(buildArgs)) {
const [key, _] = arg.split('='); const [key, _] = arg.split('=');
if (finalArgs.filter((env) => env.startsWith(key)).length === 0) { if (finalBuildArgs.filter((env) => env.startsWith(key)).length === 0) {
finalArgs.push(arg); finalBuildArgs.push(arg);
} }
} }
} }
if (build.length > 0 || buildArgs.length > 0) { if (build.length > 0 || buildArgs.length > 0) {
value['build'] = { value['build'] = {
...build, ...build,
args: finalArgs args: finalBuildArgs
}; };
} }
@@ -87,7 +100,10 @@ export default async function (data) {
v.startsWith('~') || v.startsWith('~') ||
v.startsWith('$PWD') v.startsWith('$PWD')
) { ) {
v = v.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~'); v = v
.replace(/^\./, `~`)
.replace(/^\.\./, '~')
.replace(/^\$PWD/, '~');
} else { } else {
if (!path) { if (!path) {
path = v; path = v;
@@ -110,10 +126,10 @@ export default async function (data) {
source.startsWith('~') || source.startsWith('~') ||
source.startsWith('$PWD') source.startsWith('$PWD')
) { ) {
source = source
source = source.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~'); .replace(/^\./, `~`)
console.log({source}) .replace(/^\.\./, '~')
.replace(/^\$PWD/, '~');
} else { } else {
if (!target) { if (!target) {
target = source; target = source;
@@ -125,7 +141,6 @@ export default async function (data) {
return `${source}:${target}${mode ? ':' + mode : ''}`; return `${source}:${target}${mode ? ':' + mode : ''}`;
} }
}); });
} }
if (volumes.length > 0) { if (volumes.length > 0) {
@@ -136,16 +151,20 @@ export default async function (data) {
if (dockerComposeConfiguration[key]?.port) { if (dockerComposeConfiguration[key]?.port) {
value['expose'] = [dockerComposeConfiguration[key].port]; value['expose'] = [dockerComposeConfiguration[key].port];
} }
if (value['networks']?.length > 0) { value['networks'] = [network];
value['networks'].forEach((network) => { if (value['build']?.network) {
networks[network] = { delete value['build']['network'];
name: network
};
});
value['networks'] = [...(value['networks'] || ''), network];
} else {
value['networks'] = [network];
} }
// if (value['networks']?.length > 0) {
// value['networks'].forEach((network) => {
// networks[network] = {
// name: network
// };
// });
// value['networks'] = [...(value['networks'] || ''), network];
// } else {
// value['networks'] = [network];
// }
dockerComposeYaml.services[key] = { dockerComposeYaml.services[key] = {
...dockerComposeYaml.services[key], ...dockerComposeYaml.services[key],

View File

@@ -36,6 +36,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN deno cache ${denoMainFile}`); Dockerfile.push(`RUN deno cache ${denoMainFile}`);
Dockerfile.push(`ENV NO_COLOR true`); Dockerfile.push(`ENV NO_COLOR true`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`); Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -12,6 +12,7 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -30,6 +30,7 @@ const createDockerfile = async (data, image): Promise<void> => {
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json` `COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json`
); );
Dockerfile.push(`COPY --chown=application:application . ./`); Dockerfile.push(`COPY --chown=application:application . ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -13,7 +13,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -36,6 +36,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
} else if (deploymentType === 'static') { } else if (deploymentType === 'static') {
@@ -43,6 +44,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
} }

View File

@@ -34,6 +34,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
} }
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -36,6 +36,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
} else if (deploymentType === 'static') { } else if (deploymentType === 'static') {
@@ -43,6 +44,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
} }

View File

@@ -28,6 +28,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
} }
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -52,7 +52,7 @@ const createDockerfile = async (data, image): Promise<void> => {
} else { } else {
Dockerfile.push(`CMD python ${pythonModule}`); Dockerfile.push(`CMD python ${pythonModule}`);
} }
Dockerfile.push('RUN rm -fr .git');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -12,6 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -20,6 +20,7 @@ const createDockerfile = async (data, image, name): Promise<void> => {
); );
Dockerfile.push(`RUN update-ca-certificates`); Dockerfile.push(`RUN update-ca-certificates`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ["/app/${name}"]`); Dockerfile.push(`CMD ["/app/${name}"]`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -38,6 +38,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -12,6 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -12,6 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -1,6 +1,5 @@
import { exec } from 'node:child_process';
import util from 'util';
import fs from 'fs/promises'; import fs from 'fs/promises';
import fsNormal from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import forge from 'node-forge'; import forge from 'node-forge';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
@@ -8,7 +7,6 @@ import type { Config } from 'unique-names-generator';
import generator from 'generate-password'; import generator from 'generate-password';
import crypto from 'crypto'; import crypto from 'crypto';
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
import * as Sentry from '@sentry/node';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import os from 'os'; import os from 'os';
import * as SSHConfig from 'ssh-config/src/ssh-config'; import * as SSHConfig from 'ssh-config/src/ssh-config';
@@ -18,13 +16,13 @@ import { day } from './dayjs';
import { saveBuildLog } from './buildPacks/common'; import { saveBuildLog } from './buildPacks/common';
import { scheduler } from './scheduler'; import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa'; import type { ExecaChildProcess } from 'execa';
import { FastifyReply } from 'fastify';
export const version = '3.12.28'; export const version = '3.12.39';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
export const proxyPort = process.env.COOLIFY_PROXY_PORT; export const proxyPort = process.env.COOLIFY_PROXY_PORT;
export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT; export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT;
export const sentryDSN =
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
const customConfig: Config = { const customConfig: Config = {
dictionaries: [adjectives, colors, animals], dictionaries: [adjectives, colors, animals],
@@ -172,13 +170,19 @@ export const base64Encode = (text: string): string => {
export const base64Decode = (text: string): string => { export const base64Decode = (text: string): string => {
return Buffer.from(text, 'base64').toString('ascii'); return Buffer.from(text, 'base64').toString('ascii');
}; };
export const getSecretKey = () => {
if (process.env['COOLIFY_SECRET_KEY_BETTER']) {
return process.env['COOLIFY_SECRET_KEY_BETTER'];
}
return process.env['COOLIFY_SECRET_KEY'];
};
export const decrypt = (hashString: string) => { export const decrypt = (hashString: string) => {
if (hashString) { if (hashString) {
try { try {
const hash = JSON.parse(hashString); const hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv( const decipher = crypto.createDecipheriv(
algorithm, algorithm,
process.env['COOLIFY_SECRET_KEY'], getSecretKey(),
Buffer.from(hash.iv, 'hex') Buffer.from(hash.iv, 'hex')
); );
const decrpyted = Buffer.concat([ const decrpyted = Buffer.concat([
@@ -195,7 +199,7 @@ export const decrypt = (hashString: string) => {
export const encrypt = (text: string) => { export const encrypt = (text: string) => {
if (text) { if (text) {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv); const cipher = crypto.createCipheriv(algorithm, getSecretKey(), iv);
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]); const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
return JSON.stringify({ return JSON.stringify({
iv: iv.toString('hex'), iv: iv.toString('hex'),
@@ -402,8 +406,8 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'MongoDB', fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb', baseImage: 'bitnami/mongodb',
baseImageARM: 'mongo', baseImageARM: 'mongo',
versions: ['5.0', '4.4', '4.2'], versions: ['6.0', '5.0', '4.4', '4.2'],
versionsARM: ['5.0', '4.4', '4.2'] versionsARM: ['6.0', '5.0', '4.4', '4.2']
}, },
{ {
name: 'mysql', name: 'mysql',
@@ -418,16 +422,16 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'MariaDB', fancyName: 'MariaDB',
baseImage: 'bitnami/mariadb', baseImage: 'bitnami/mariadb',
baseImageARM: 'mariadb', baseImageARM: 'mariadb',
versions: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'], versions: ['10.11', '10.10', '10.9', '10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'],
versionsARM: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'] versionsARM: ['10.11', '10.10', '10.9', '10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2']
}, },
{ {
name: 'postgresql', name: 'postgresql',
fancyName: 'PostgreSQL', fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql', baseImage: 'bitnami/postgresql',
baseImageARM: 'postgres', baseImageARM: 'postgres',
versions: ['14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'], versions: ['15.2.0', '14.7.0', '14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'],
versionsARM: ['14.5', '13.8', '12.12', '11.17', '10.22'] versionsARM: ['15.2', '14.7', '14.5', '13.8', '12.12', '11.17', '10.22']
}, },
{ {
name: 'redis', name: 'redis',
@@ -442,14 +446,14 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'CouchDB', fancyName: 'CouchDB',
baseImage: 'bitnami/couchdb', baseImage: 'bitnami/couchdb',
baseImageARM: 'couchdb', baseImageARM: 'couchdb',
versions: ['3.2.2', '3.1.2', '2.3.1'], versions: ['3.3.1', '3.2.2', '3.1.2', '2.3.1'],
versionsARM: ['3.2.2', '3.1.2', '2.3.1'] versionsARM: ['3.3', '3.2.2', '3.1.2', '2.3.1']
}, },
{ {
name: 'edgedb', name: 'edgedb',
fancyName: 'EdgeDB', fancyName: 'EdgeDB',
baseImage: 'edgedb/edgedb', baseImage: 'edgedb/edgedb',
versions: ['latest', '2.1', '2.0', '1.4'] versions: ['latest', '2.9', '2.8', '2.7']
} }
]; ];
@@ -579,7 +583,8 @@ export async function executeCommand({
stream = false, stream = false,
buildId, buildId,
applicationId, applicationId,
debug debug,
timeout = 0
}: { }: {
command: string; command: string;
sshCommand?: boolean; sshCommand?: boolean;
@@ -589,6 +594,7 @@ export async function executeCommand({
buildId?: string; buildId?: string;
applicationId?: string; applicationId?: string;
debug?: boolean; debug?: boolean;
timeout?: number;
}): Promise<ExecaChildProcess<string>> { }): Promise<ExecaChildProcess<string>> {
const { execa, execaCommand } = await import('execa'); const { execa, execaCommand } = await import('execa');
const { parse } = await import('shell-quote'); const { parse } = await import('shell-quote');
@@ -613,20 +619,26 @@ export async function executeCommand({
} }
if (sshCommand) { if (sshCommand) {
if (shell) { if (shell) {
return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`); return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`, {
timeout
});
} }
return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs]); return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs], {
timeout
});
} }
if (stream) { if (stream) {
return await new Promise(async (resolve, reject) => { return await new Promise(async (resolve, reject) => {
let subprocess = null; let subprocess = null;
if (shell) { if (shell) {
subprocess = execaCommand(command, { subprocess = execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
}); });
} else { } else {
subprocess = execa(dockerCommand, dockerArgs, { subprocess = execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
}); });
} }
const logs = []; const logs = [];
@@ -680,19 +692,26 @@ export async function executeCommand({
} else { } else {
if (shell) { if (shell) {
return await execaCommand(command, { return await execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
}); });
} else { } else {
return await execa(dockerCommand, dockerArgs, { return await execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
}); });
} }
} }
} else { } else {
if (shell) { if (shell) {
return execaCommand(command, { shell: true }); return execaCommand(command, {
shell: true,
timeout
});
} }
return await execa(dockerCommand, dockerArgs); return await execa(dockerCommand, dockerArgs, {
timeout
});
} }
} }
@@ -826,7 +845,7 @@ export function generateToken() {
{ {
nbf: Math.floor(Date.now() / 1000) - 30 nbf: Math.floor(Date.now() / 1000) - 30
}, },
process.env['COOLIFY_SECRET_KEY'] getSecretKey()
); );
} }
export function generatePassword({ export function generatePassword({
@@ -1013,7 +1032,7 @@ export function generateDatabaseConfiguration(database: any): DatabaseConfigurat
ulimits: {} ulimits: {}
}; };
if (isARM()) { if (isARM()) {
configuration.volume = `${id}-${type}-data:/var/lib/postgresql`; configuration.volume = `${id}-${type}-data:/var/lib/postgresql/data`;
configuration.environmentVariables = { configuration.environmentVariables = {
POSTGRES_PASSWORD: dbUserPassword, POSTGRES_PASSWORD: dbUserPassword,
POSTGRES_USER: dbUser, POSTGRES_USER: dbUser,
@@ -1663,9 +1682,6 @@ export function errorHandler({
if (message.includes('Unique constraint failed')) { if (message.includes('Unique constraint failed')) {
message = 'This data is unique and already exists. Please try again with a different value.'; message = 'This data is unique and already exists. Please try again with a different value.';
} }
if (type === 'normal') {
Sentry.captureException(message);
}
throw { status, message }; throw { status, message };
} }
export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> { export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> {
@@ -1746,7 +1762,7 @@ export function convertTolOldVolumeNames(type) {
} }
} }
export async function cleanupDockerStorage(dockerId) { export async function cleanupDockerStorage(dockerId, volumes = false) {
// Cleanup images that are not used by any container // Cleanup images that are not used by any container
try { try {
await executeCommand({ dockerId, command: `docker image prune -af` }); await executeCommand({ dockerId, command: `docker image prune -af` });
@@ -1764,6 +1780,11 @@ export async function cleanupDockerStorage(dockerId) {
try { try {
await executeCommand({ dockerId, command: `docker builder prune -af` }); await executeCommand({ dockerId, command: `docker builder prune -af` });
} catch (error) { } } catch (error) { }
if (volumes) {
try {
await executeCommand({ dockerId, command: `docker volume prune -af` });
} catch (error) { }
}
} }
export function persistentVolumes(id, persistentStorage, config) { export function persistentVolumes(id, persistentStorage, config) {
@@ -1926,3 +1947,49 @@ export function generateSecrets(
} }
return envs; return envs;
} }
export async function backupDatabaseNow(database, reply) {
const backupFolder = '/tmp'
const fileName = `${database.id}-${new Date().getTime()}.gz`
const backupFileName = `${backupFolder}/${fileName}`
const backupStorageFilename = `/app/backups/${fileName}`
let command = null
switch (database?.type) {
case 'postgresql':
command = `docker exec ${database.id} sh -c "PGPASSWORD=${database.rootUserPassword} pg_dumpall -U postgres | gzip > ${backupFileName}"`
break;
case 'mongodb':
command = `docker exec ${database.id} sh -c "mongodump --archive=${backupFileName} --gzip --username=${database.rootUser} --password=${database.rootUserPassword}"`
break;
case 'mysql':
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
break;
case 'mariadb':
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
break;
case 'couchdb':
command = `docker exec ${database.id} sh -c "tar -czvf ${backupFileName} /bitnami/couchdb/data"`
break;
default:
return;
}
await executeCommand({
dockerId: database.destinationDockerId,
command,
});
const copyCommand = `docker cp ${database.id}:${backupFileName} ${backupFileName}`
await executeCommand({
dockerId: database.destinationDockerId,
command: copyCommand
});
await executeCommand({
dockerId: database.destinationDockerId,
command: `docker cp ${database.id}:${backupFileName} /app/backups/`
});
const stream = fsNormal.createReadStream(backupFileName);
reply.header('Content-Type', 'application/octet-stream');
reply.header('Content-Disposition', `attachment; filename=${fileName}`);
reply.header('Content-Length', fsNormal.statSync(backupFileName).size);
reply.header('Content-Transfer-Encoding', 'binary');
return reply.send(stream)
}

View File

@@ -624,7 +624,7 @@ export const glitchTip = [{
isEncrypted: false isEncrypted: false
}, },
{ {
name: 'emailSmtpUseSsl', name: 'emailSmtpUseTls',
isEditable: true, isEditable: true,
isLowerCase: false, isLowerCase: false,
isNumber: false, isNumber: false,

View File

@@ -1,33 +1,37 @@
import fp from 'fastify-plugin' import fp from 'fastify-plugin';
import fastifyJwt, { FastifyJWTOptions } from '@fastify/jwt' import fastifyJwt, { FastifyJWTOptions } from '@fastify/jwt';
declare module "@fastify/jwt" { declare module '@fastify/jwt' {
interface FastifyJWT { interface FastifyJWT {
user: { user: {
userId: string, userId: string;
teamId: string, teamId: string;
permission: string, permission: string;
isAdmin: boolean isAdmin: boolean;
} };
} }
} }
export default fp<FastifyJWTOptions>(async (fastify, opts) => { export default fp<FastifyJWTOptions>(async (fastify, opts) => {
fastify.register(fastifyJwt, { let secretKey = fastify.config.COOLIFY_SECRET_KEY_BETTER;
secret: fastify.config.COOLIFY_SECRET_KEY if (!secretKey) {
}) secretKey = fastify.config.COOLIFY_SECRET_KEY;
}
fastify.register(fastifyJwt, {
secret: secretKey
});
fastify.decorate("authenticate", async function (request, reply) { fastify.decorate('authenticate', async function (request, reply) {
try { try {
await request.jwtVerify() await request.jwtVerify();
} catch (err) { } catch (err) {
reply.send(err) reply.send(err);
} }
}) });
}) });
declare module 'fastify' { declare module 'fastify' {
export interface FastifyInstance { export interface FastifyInstance {
authenticate(): Promise<void> authenticate(): Promise<void>;
} }
} }

View File

@@ -398,7 +398,9 @@ export async function saveApplication(
dockerComposeFileLocation, dockerComposeFileLocation,
dockerComposeConfiguration, dockerComposeConfiguration,
simpleDockerfile, simpleDockerfile,
dockerRegistryImageName dockerRegistryImageName,
basicAuthPw,
basicAuthUser,
} = request.body; } = request.body;
if (port) port = Number(port); if (port) port = Number(port);
if (exposePort) { if (exposePort) {
@@ -453,6 +455,8 @@ export async function saveApplication(
dockerComposeConfiguration, dockerComposeConfiguration,
simpleDockerfile, simpleDockerfile,
dockerRegistryImageName, dockerRegistryImageName,
basicAuthPw,
basicAuthUser,
...defaultConfiguration, ...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
} }
@@ -476,6 +480,8 @@ export async function saveApplication(
dockerComposeFileLocation, dockerComposeFileLocation,
dockerComposeConfiguration, dockerComposeConfiguration,
simpleDockerfile, simpleDockerfile,
basicAuthPw,
basicAuthUser,
dockerRegistryImageName, dockerRegistryImageName,
...defaultConfiguration ...defaultConfiguration
} }
@@ -499,12 +505,11 @@ export async function saveApplicationSettings(
previews, previews,
dualCerts, dualCerts,
autodeploy, autodeploy,
branch,
projectId,
isBot, isBot,
isDBBranching, isDBBranching,
isCustomSSL, isCustomSSL,
isHttp2 isHttp2,
basicAuth,
} = request.body; } = request.body;
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
@@ -519,7 +524,8 @@ export async function saveApplicationSettings(
isBot, isBot,
isDBBranching, isDBBranching,
isCustomSSL, isCustomSSL,
isHttp2 isHttp2,
basicAuth,
} }
} }
}, },
@@ -640,8 +646,7 @@ export async function restartApplication(
const volumes = const volumes =
persistentStorage?.map((storage) => { persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : '' return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}${storage.path}`;
}) || []; }) || [];
const composeVolumes = volumes.map((volume) => { const composeVolumes = volumes.map((volume) => {
return { return {

View File

@@ -28,6 +28,8 @@ export interface SaveApplication extends OnlyId {
dockerComposeConfiguration: string; dockerComposeConfiguration: string;
simpleDockerfile: string; simpleDockerfile: string;
dockerRegistryImageName: string; dockerRegistryImageName: string;
basicAuthPw: string;
basicAuthUser: string;
}; };
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
@@ -43,6 +45,7 @@ export interface SaveApplicationSettings extends OnlyId {
isDBBranching: boolean; isDBBranching: boolean;
isCustomSSL: boolean; isCustomSSL: boolean;
isHttp2: boolean; isHttp2: boolean;
basicAuth: boolean;
}; };
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {

View File

@@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { import {
ComposeFile, ComposeFile,
backupDatabaseNow,
createDirectories, createDirectories,
decrypt, decrypt,
defaultComposeConfiguration, defaultComposeConfiguration,
@@ -302,7 +303,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
databaseSecret databaseSecret
} = database; } = database;
const { privatePort, command, environmentVariables, image, volume, ulimits } = const { privatePort, command, environmentVariables, image, volume, ulimits } =
generateDatabaseConfiguration(database, arch); generateDatabaseConfiguration(database);
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const volumeName = volume.split(':')[0]; const volumeName = volume.split(':')[0];
@@ -351,6 +352,21 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
return errorHandler({ status, message }); return errorHandler({ status, message });
} }
} }
export async function backupDatabase(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
return await backupDatabaseNow(database, reply);
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function stopDatabase(request: FastifyRequest<OnlyId>) { export async function stopDatabase(request: FastifyRequest<OnlyId>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers'; import { backupDatabase, cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
@@ -39,6 +39,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<OnlyId>('/:id/start', async (request) => await startDatabase(request)); fastify.post<OnlyId>('/:id/start', async (request) => await startDatabase(request));
fastify.post<OnlyId>('/:id/stop', async (request) => await stopDatabase(request)); fastify.post<OnlyId>('/:id/stop', async (request) => await stopDatabase(request));
fastify.post<OnlyId>('/:id/backup', async (request, reply) => await backupDatabase(request, reply));
}; };
export default root; export default root;

View File

@@ -1,279 +1,384 @@
import type { FastifyRequest } from 'fastify'; import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import sshConfig from 'ssh-config' import {
import fs from 'fs/promises' errorHandler,
import os from 'os'; executeCommand,
listSettings,
import { createRemoteEngineConfiguration, decrypt, errorHandler, executeCommand, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common'; prisma,
startTraefikProxy,
stopTraefikProxy
} from '../../../../lib/common';
import { checkContainer } from '../../../../lib/docker'; import { checkContainer } from '../../../../lib/docker';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types'; import type {
CheckDestination,
ListDestinations,
NewDestination,
Proxy,
SaveDestinationSettings
} from './types';
import { removeService } from '../../../../lib/services/common';
export async function listDestinations(request: FastifyRequest<ListDestinations>) { export async function listDestinations(request: FastifyRequest<ListDestinations>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { onlyVerified = false } = request.query const { onlyVerified = false } = request.query;
let destinations = [] let destinations = [];
if (teamId === '0') { if (teamId === '0') {
destinations = await prisma.destinationDocker.findMany({ include: { teams: true } }); destinations = await prisma.destinationDocker.findMany({ include: { teams: true } });
} else { } else {
destinations = await prisma.destinationDocker.findMany({ destinations = await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId } } }, where: { teams: { some: { id: teamId } } },
include: { teams: true } include: { teams: true }
}); });
} }
if (onlyVerified) { if (onlyVerified) {
destinations = destinations.filter(destination => destination.engine || (destination.remoteEngine && destination.remoteVerified)) destinations = destinations.filter(
} (destination) =>
return { destination.engine || (destination.remoteEngine && destination.remoteVerified)
destinations );
} }
} catch ({ status, message }) { return {
return errorHandler({ status, message }) destinations
} };
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function checkDestination(request: FastifyRequest<CheckDestination>) { export async function checkDestination(request: FastifyRequest<CheckDestination>) {
try { try {
const { network } = request.body; const { network } = request.body;
const found = await prisma.destinationDocker.findFirst({ where: { network } }); const found = await prisma.destinationDocker.findFirst({ where: { network } });
if (found) { if (found) {
throw { throw {
message: `Network already exists: ${network}` message: `Network already exists: ${network}`
}; };
} }
return {} return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function getDestination(request: FastifyRequest<OnlyId>) { export async function getDestination(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params;
const teamId = request.user?.teamId; const teamId = request.user?.teamId;
const destination = await prisma.destinationDocker.findFirst({ const destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { sshKey: true, application: true, service: true, database: true } include: { sshKey: true, application: true, service: true, database: true }
}); });
if (!destination && id !== 'new') { if (!destination && id !== 'new') {
throw { status: 404, message: `Destination not found.` }; throw { status: 404, message: `Destination not found.` };
} }
const settings = await listSettings(); const settings = await listSettings();
const payload = { const payload = {
destination, destination,
settings settings
}; };
return { return {
...payload ...payload
}; };
} catch ({ status, message }) {
} catch ({ status, message }) { return errorHandler({ status, message });
return errorHandler({ status, message }) }
}
} }
export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) { export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { id } = request.params const { id } = request.params;
let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } =
if (id === 'new') { request.body;
if (engine) { if (id === 'new') {
const { stdout } = await await executeCommand({ command: `docker network ls --filter 'name=^${network}$' --format '{{json .}}'` }); if (engine) {
if (stdout === '') { const { stdout } = await await executeCommand({
await await executeCommand({ command: `docker network create --attachable ${network}` }); command: `docker network ls --filter 'name=^${network}$' --format '{{json .}}'`
} });
await prisma.destinationDocker.create({ if (stdout === '') {
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed } await await executeCommand({ command: `docker network create --attachable ${network}` });
}); }
const destinations = await prisma.destinationDocker.findMany({ where: { engine } }); await prisma.destinationDocker.create({
const destination = destinations.find((destination) => destination.network === network); data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
if (destinations.length > 0) { });
const proxyConfigured = destinations.find( const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true const destination = destinations.find((destination) => destination.network === network);
); if (destinations.length > 0) {
if (proxyConfigured) { const proxyConfigured = destinations.find(
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed; (destination) =>
} destination.network !== network && destination.isCoolifyProxyUsed === true
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } }); );
} if (proxyConfigured) {
if (isCoolifyProxyUsed) { isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
await startTraefikProxy(destination.id); }
} await prisma.destinationDocker.updateMany({
return reply.code(201).send({ id: destination.id }); where: { engine },
} else { data: { isCoolifyProxyUsed }
const destination = await prisma.destinationDocker.create({ });
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress, remoteUser, remotePort: Number(remotePort) } }
}); if (isCoolifyProxyUsed) {
return reply.code(201).send({ id: destination.id }) await startTraefikProxy(destination.id);
} }
} else { return reply.code(201).send({ id: destination.id });
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } }); } else {
return reply.code(201).send(); const destination = await prisma.destinationDocker.create({
} data: {
name,
} catch ({ status, message }) { teams: { connect: { id: teamId } },
return errorHandler({ status, message }) engine,
} network,
isCoolifyProxyUsed,
remoteEngine: true,
remoteIpAddress,
remoteUser,
remotePort: Number(remotePort)
}
});
return reply.code(201).send({ id: destination.id });
}
} else {
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
return reply.code(201).send();
}
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function forceDeleteDestination(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params;
const services = await prisma.service.findMany({ where: { destinationDockerId: id } });
for (const service of services) {
await removeService({ id: service.id });
}
const applications = await prisma.application.findMany({ where: { destinationDockerId: id } });
for (const application of applications) {
await prisma.applicationSettings.deleteMany({ where: { application: { id: application.id } } });
await prisma.buildLog.deleteMany({ where: { applicationId: application.id } });
await prisma.build.deleteMany({ where: { applicationId: application.id } });
await prisma.secret.deleteMany({ where: { applicationId: application.id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: application.id } });
await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: application.id } });
await prisma.previewApplication.deleteMany({ where: { applicationId: application.id } });
}
const databases = await prisma.database.findMany({ where: { destinationDockerId: id } });
for (const database of databases) {
await prisma.databaseSettings.deleteMany({ where: { databaseId: database.id } });
await prisma.databaseSecret.deleteMany({ where: { databaseId: database.id } });
await prisma.database.delete({ where: { id: database.id } });
}
await prisma.destinationDocker.delete({ where: { id } });
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function deleteDestination(request: FastifyRequest<OnlyId>) { export async function deleteDestination(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params;
const { network, remoteVerified, engine, isCoolifyProxyUsed } = await prisma.destinationDocker.findUnique({ where: { id } }); const appFound = await prisma.application.findFirst({ where: { destinationDockerId: id } });
if (isCoolifyProxyUsed) { const serviceFound = await prisma.service.findFirst({ where: { destinationDockerId: id } });
if (engine || remoteVerified) { const databaseFound = await prisma.database.findFirst({ where: { destinationDockerId: id } });
const { stdout: found } = await executeCommand({ if (appFound || serviceFound || databaseFound) {
dockerId: id, throw {
command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'` message: `Destination is in use.<br>Remove all applications, services and databases using this destination first.`
}) };
if (found) { }
await executeCommand({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` }) const { network, remoteVerified, engine, isCoolifyProxyUsed } =
await executeCommand({ dockerId: id, command: `docker network rm ${network}` }) await prisma.destinationDocker.findUnique({ where: { id } });
} if (isCoolifyProxyUsed) {
} if (engine || remoteVerified) {
} const { stdout: found } = await executeCommand({
await prisma.destinationDocker.delete({ where: { id } }); dockerId: id,
return {} command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'`
} catch ({ status, message }) { });
return errorHandler({ status, message }) if (found) {
} await executeCommand({
dockerId: id,
command: `docker network disconnect ${network} coolify-proxy`
});
await executeCommand({ dockerId: id, command: `docker network rm ${network}` });
}
}
}
await prisma.destinationDocker.delete({ where: { id } });
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function saveDestinationSettings(request: FastifyRequest<SaveDestinationSettings>) { export async function saveDestinationSettings(request: FastifyRequest<SaveDestinationSettings>) {
try { try {
const { engine, isCoolifyProxyUsed } = request.body; const { engine, isCoolifyProxyUsed } = request.body;
await prisma.destinationDocker.updateMany({ await prisma.destinationDocker.updateMany({
where: { engine }, where: { engine },
data: { isCoolifyProxyUsed } data: { isCoolifyProxyUsed }
}); });
return { return {
status: 202 status: 202
} };
// 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 startProxy(request: FastifyRequest<Proxy>) { export async function startProxy(request: FastifyRequest<Proxy>) {
const { id } = request.params const { id } = request.params;
try { try {
await startTraefikProxy(id); await startTraefikProxy(id);
return {} return {};
} catch ({ status, message }) { } catch ({ status, message }) {
await stopTraefikProxy(id); await stopTraefikProxy(id);
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function stopProxy(request: FastifyRequest<Proxy>) { export async function stopProxy(request: FastifyRequest<Proxy>) {
const { id } = request.params const { id } = request.params;
try { try {
await stopTraefikProxy(id); await stopTraefikProxy(id);
return {} return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function restartProxy(request: FastifyRequest<Proxy>) { export async function restartProxy(request: FastifyRequest<Proxy>) {
const { id } = request.params const { id } = request.params;
try { try {
await stopTraefikProxy(id); await stopTraefikProxy(id);
await startTraefikProxy(id); await startTraefikProxy(id);
await prisma.destinationDocker.update({ await prisma.destinationDocker.update({
where: { id }, where: { id },
data: { isCoolifyProxyUsed: true } data: { isCoolifyProxyUsed: true }
}); });
return {} return {};
} catch ({ status, message }) { } catch ({ status, message }) {
await prisma.destinationDocker.update({ await prisma.destinationDocker.update({
where: { id }, where: { id },
data: { isCoolifyProxyUsed: false } data: { isCoolifyProxyUsed: false }
}); });
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function assignSSHKey(request: FastifyRequest) { export async function assignSSHKey(request: FastifyRequest) {
try { try {
const { id: sshKeyId } = request.body; const { id: sshKeyId } = request.body;
const { id } = request.params; const { id } = request.params;
await prisma.destinationDocker.update({ where: { id }, data: { sshKey: { connect: { id: sshKeyId } } } }) await prisma.destinationDocker.update({
return {} where: { id },
} catch ({ status, message }) { data: { sshKey: { connect: { id: sshKeyId } } }
return errorHandler({ status, message }) });
} return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function verifyRemoteDockerEngineFn(id: string) { export async function verifyRemoteDockerEngineFn(id: string) {
const { remoteIpAddress, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } }) const { remoteIpAddress, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst(
const daemonJson = `daemon-${id}.json` { where: { id } }
try { );
await executeCommand({ sshCommand: true, command: `docker network inspect ${network}`, dockerId: id }); const daemonJson = `daemon-${id}.json`;
} catch (error) { try {
await executeCommand({ command: `docker network create --attachable ${network}`, dockerId: id }); await executeCommand({
} sshCommand: true,
command: `docker network inspect ${network}`,
dockerId: id
});
} catch (error) {
await executeCommand({
command: `docker network create --attachable ${network}`,
dockerId: id
});
}
try { try {
await executeCommand({ sshCommand: true, command: `docker network inspect coolify-infra`, dockerId: id }); await executeCommand({
} catch (error) { sshCommand: true,
await executeCommand({ command: `docker network create --attachable coolify-infra`, dockerId: id }); command: `docker network inspect coolify-infra`,
} dockerId: id
});
} catch (error) {
await executeCommand({
command: `docker network create --attachable coolify-infra`,
dockerId: id
});
}
if (isCoolifyProxyUsed) await startTraefikProxy(id); if (isCoolifyProxyUsed) await startTraefikProxy(id);
let isUpdated = false; let isUpdated = false;
let daemonJsonParsed = { let daemonJsonParsed = {
"live-restore": true, 'live-restore': true,
"features": { features: {
"buildkit": true buildkit: true
} }
}; };
try { try {
const { stdout: daemonJson } = await executeCommand({ sshCommand: true, dockerId: id, command: `cat /etc/docker/daemon.json` }); const { stdout: daemonJson } = await executeCommand({
daemonJsonParsed = JSON.parse(daemonJson); sshCommand: true,
if (!daemonJsonParsed['live-restore'] || daemonJsonParsed['live-restore'] !== true) { dockerId: id,
isUpdated = true; command: `cat /etc/docker/daemon.json`
daemonJsonParsed['live-restore'] = true });
daemonJsonParsed = JSON.parse(daemonJson);
} if (!daemonJsonParsed['live-restore'] || daemonJsonParsed['live-restore'] !== true) {
if (!daemonJsonParsed?.features?.buildkit) { isUpdated = true;
isUpdated = true; daemonJsonParsed['live-restore'] = true;
daemonJsonParsed.features = { }
buildkit: true if (!daemonJsonParsed?.features?.buildkit) {
} isUpdated = true;
} daemonJsonParsed.features = {
} catch (error) { buildkit: true
isUpdated = true; };
} }
try { } catch (error) {
if (isUpdated) { isUpdated = true;
await executeCommand({ shell: true, command: `echo '${JSON.stringify(daemonJsonParsed, null, 2)}' > /tmp/${daemonJson}` }) }
await executeCommand({ dockerId: id, command: `scp /tmp/${daemonJson} ${remoteIpAddress}-remote:/etc/docker/daemon.json` }); try {
await executeCommand({ command: `rm /tmp/${daemonJson}` }) if (isUpdated) {
await executeCommand({ sshCommand: true, dockerId: id, command: `systemctl restart docker` }); await executeCommand({
} shell: true,
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } }) command: `echo '${JSON.stringify(daemonJsonParsed, null, 2)}' > /tmp/${daemonJson}`
} catch (error) { });
throw new Error('Error while verifying remote docker engine') await executeCommand({
} dockerId: id,
command: `scp /tmp/${daemonJson} ${remoteIpAddress}-remote:/etc/docker/daemon.json`
});
await executeCommand({ command: `rm /tmp/${daemonJson}` });
await executeCommand({ sshCommand: true, dockerId: id, command: `systemctl restart docker` });
}
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } });
} catch (error) {
console.log(error)
throw new Error('Error while verifying remote docker engine');
}
} }
export async function verifyRemoteDockerEngine(request: FastifyRequest<OnlyId>, reply: FastifyReply) { export async function verifyRemoteDockerEngine(
const { id } = request.params; request: FastifyRequest<OnlyId>,
try { reply: FastifyReply
await verifyRemoteDockerEngineFn(id); ) {
return reply.code(201).send() const { id } = request.params;
} catch ({ status, message }) { try {
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: false } }) await verifyRemoteDockerEngineFn(id);
return errorHandler({ status, message }) return reply.code(201).send();
} } catch ({ status, message }) {
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: false } });
return errorHandler({ status, message });
}
} }
export async function getDestinationStatus(request: FastifyRequest<OnlyId>) { 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 { found: isRunning } = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true }) const { found: isRunning } = await checkContainer({
return { dockerId: destination.id,
isRunning container: 'coolify-proxy',
} remove: true
} catch ({ status, message }) { });
return errorHandler({ status, message }) return {
} isRunning
};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { assignSSHKey, checkDestination, deleteDestination, getDestination, getDestinationStatus, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy, verifyRemoteDockerEngine } from './handlers'; import { assignSSHKey, checkDestination, deleteDestination, forceDeleteDestination, getDestination, getDestinationStatus, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy, verifyRemoteDockerEngine } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types'; import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types';
@@ -14,6 +14,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id', async (request) => await getDestination(request)); fastify.get<OnlyId>('/:id', async (request) => await getDestination(request));
fastify.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply)); fastify.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteDestination(request)); fastify.delete<OnlyId>('/:id', async (request) => await deleteDestination(request));
fastify.delete<OnlyId>('/:id/force', async (request) => await forceDeleteDestination(request));
fastify.get<OnlyId>('/:id/status', async (request) => await getDestinationStatus(request)); fastify.get<OnlyId>('/:id/status', async (request) => await getDestinationStatus(request));
fastify.post<SaveDestinationSettings>('/:id/settings', async (request) => await saveDestinationSettings(request)); fastify.post<SaveDestinationSettings>('/:id/settings', async (request) => await saveDestinationSettings(request));

View File

@@ -12,7 +12,6 @@ import {
prisma, prisma,
uniqueName, uniqueName,
version, version,
sentryDSN,
executeCommand executeCommand
} from '../../../lib/common'; } from '../../../lib/common';
import { scheduler } from '../../../lib/scheduler'; import { scheduler } from '../../../lib/scheduler';
@@ -20,8 +19,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import type { Login, Update } from '.'; import type { Login, Update } from '.';
import type { GetCurrentUser } from './types'; import type { GetCurrentUser } from './types';
export async function hashPassword(password: string): Promise<string> { export async function hashPassword(password: string, saltRounds = 15): Promise<string> {
const saltRounds = 15;
return bcrypt.hash(password, saltRounds); return bcrypt.hash(password, saltRounds);
} }
@@ -59,7 +57,7 @@ export async function cleanupManually(request: FastifyRequest) {
const destination = await prisma.destinationDocker.findUnique({ const destination = await prisma.destinationDocker.findUnique({
where: { id: serverId } where: { id: serverId }
}); });
await cleanupDockerStorage(destination.id); await cleanupDockerStorage(destination.id, true);
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -78,7 +76,7 @@ export async function refreshTags() {
tags = JSON.parse(tags).concat(JSON.parse(testTags)); tags = JSON.parse(tags).concat(JSON.parse(testTags));
} }
} }
} catch (error) {} } catch (error) { }
await fs.writeFile('./tags.json', tags); await fs.writeFile('./tags.json', tags);
} else { } else {
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text(); const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text();
@@ -103,7 +101,7 @@ export async function refreshTemplates() {
if (await fs.stat('./testTemplate.yaml')) { if (await fs.stat('./testTemplate.yaml')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8')); templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
} }
} catch (error) {} } catch (error) { }
const response = await fs.readFile('./devTemplates.yaml', 'utf8'); const response = await fs.readFile('./devTemplates.yaml', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response))); await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response)));
} else { } else {
@@ -164,13 +162,13 @@ export async function update(request: FastifyRequest<Update>) {
await executeCommand({ command: `docker pull ${image}` }); await executeCommand({ command: `docker pull ${image}` });
} }
await executeCommand({ shell: true, command: `env | grep COOLIFY > .env` }); await executeCommand({ shell: true, command: `ls .env || env | grep "^COOLIFY" | sort > .env` });
await executeCommand({ await executeCommand({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
}); });
await executeCommand({ await executeCommand({
shell: true, shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /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"` command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep "^COOLIFY" | sort > .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 {
@@ -452,7 +450,6 @@ export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fa
}); });
return { return {
settings: await prisma.setting.findUnique({ where: { id: '0' } }), settings: await prisma.setting.findUnique({ where: { id: '0' } }),
sentryDSN,
pendingInvitations, pendingInvitations,
token, token,
...request.user ...request.user

View File

@@ -50,6 +50,7 @@ import type {
SetWordpressSettings SetWordpressSettings
} from './types'; } from './types';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import { refreshTags, refreshTemplates } from '../handlers';
export async function listServices(request: FastifyRequest) { export async function listServices(request: FastifyRequest) {
try { try {
@@ -412,22 +413,23 @@ export async function saveServiceType(
if (foundTemplate.variables) { if (foundTemplate.variables) {
if (foundTemplate.variables.length > 0) { if (foundTemplate.variables.length > 0) {
for (const variable of foundTemplate.variables) { for (const variable of foundTemplate.variables) {
const { defaultValue } = variable; let { defaultValue } = variable;
defaultValue = defaultValue.toString();
const regex = /^\$\$.*\((\d+)\)$/g; const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined; const length = Number(regex.exec(defaultValue)?.[1]) || undefined;
if (variable.defaultValue.startsWith('$$generate_password')) { if (defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length }); variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) { } else if (defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true }); variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) { } else if (defaultValue.startsWith('$$generate_username')) {
variable.value = cuid(); variable.value = cuid();
} else if (variable.defaultValue.startsWith('$$generate_token')) { } else if (defaultValue.startsWith('$$generate_token')) {
variable.value = generateToken(); variable.value = generateToken();
} else { } else {
variable.value = variable.defaultValue || ''; variable.value = defaultValue || '';
} }
const foundVariableSomewhereElse = foundTemplate.variables.find((v) => const foundVariableSomewhereElse = foundTemplate.variables.find((v) =>
v.defaultValue.includes(variable.id) v.defaultValue.toString().includes(variable.id)
); );
if (foundVariableSomewhereElse) { if (foundVariableSomewhereElse) {
foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll( foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll(
@@ -475,7 +477,7 @@ export async function saveServiceType(
const [volumeName, path] = volume.split(':'); const [volumeName, path] = volume.split(':');
if (!volumeName.startsWith('/')) { if (!volumeName.startsWith('/')) {
const found = await prisma.servicePersistentStorage.findFirst({ const found = await prisma.servicePersistentStorage.findFirst({
where: { volumeName, serviceId: id } where: { volumeName, serviceId: id, path }
}); });
if (!found) { if (!found) {
await prisma.servicePersistentStorage.create({ await prisma.servicePersistentStorage.create({
@@ -746,7 +748,10 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting; let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting;
if (value) { if (value) {
if (changed) { if (changed) {
await prisma.serviceSetting.update({ where: { id: settingId }, data: { value: value.replace(/\n/, "\\n") } }); await prisma.serviceSetting.update({
where: { id: settingId },
data: { value: value.replace(/\n/, '\\n') }
});
} }
if (isNew) { if (isNew) {
if (!variableName) { if (!variableName) {
@@ -981,11 +986,22 @@ export async function cleanupPlausibleLogs(request: FastifyRequest<OnlyId>, repl
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { destinationDockerId, destinationDocker } = await getServiceFromDB({ id, teamId }); const { destinationDockerId, destinationDocker } = await getServiceFromDB({ id, teamId });
if (destinationDockerId) { if (destinationDockerId) {
await executeCommand({ const logTables = await executeCommand({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
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.{};\"`, command: `docker exec ${id}-clickhouse clickhouse-client -q "SELECT name FROM system.tables;"`,
shell: true shell: false
}); });
if (logTables.stdout !== '') {
const tables = logTables.stdout.split('\n').filter((t) => t.includes('_log'));
for (const table of tables) {
console.log(`Truncating table ${table}`)
await executeCommand({
dockerId: destinationDocker.id,
command: `docker exec ${id}-clickhouse clickhouse-client -q "TRUNCATE TABLE system.${table};"`,
shell: false
});
}
}
return await reply.code(201).send(); return await reply.code(201).send();
} }
throw { status: 500, message: 'Could cleanup logs.' }; throw { status: 500, message: 'Could cleanup logs.' };

View File

@@ -1,235 +1,312 @@
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
import { X509Certificate } from 'node:crypto'; import { X509Certificate } from 'node:crypto';
import * as Sentry from '@sentry/node';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, executeCommand, getDomain, isDev, isDNSValid, isDomainConfigured, listSettings, prisma, sentryDSN, version } from '../../../../lib/common'; import {
import { AddDefaultRegistry, CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey, SetDefaultRegistry } from './types'; checkDomainsIsValidInDNS,
decrypt,
encrypt,
errorHandler,
executeCommand,
getDomain,
isDev,
isDNSValid,
isDomainConfigured,
listSettings,
prisma
} from '../../../../lib/common';
import {
AddDefaultRegistry,
CheckDNS,
CheckDomain,
DeleteDomain,
OnlyIdInBody,
SaveSettings,
SaveSSHKey,
SetDefaultRegistry
} from './types';
export async function listAllSettings(request: FastifyRequest) { export async function listAllSettings(request: FastifyRequest) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const settings = await listSettings(); const settings = await listSettings();
const sshKeys = await prisma.sshKey.findMany({ where: { team: { id: teamId } } }) const sshKeys = await prisma.sshKey.findMany({ where: { team: { id: teamId } } });
let registries = await prisma.dockerRegistry.findMany({ where: { team: { id: teamId } } }) let registries = await prisma.dockerRegistry.findMany({ where: { team: { id: teamId } } });
registries = registries.map((registry) => { registries = registries.map((registry) => {
if (registry.password) { if (registry.password) {
registry.password = decrypt(registry.password) registry.password = decrypt(registry.password);
} }
return registry return registry;
}) });
const unencryptedKeys = [] const unencryptedKeys = [];
if (sshKeys.length > 0) { if (sshKeys.length > 0) {
for (const key of sshKeys) { for (const key of sshKeys) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt }) unencryptedKeys.push({
} id: key.id,
} name: key.name,
const certificates = await prisma.certificate.findMany({ where: { team: { id: teamId } } }) privateKey: decrypt(key.privateKey),
let cns = []; createdAt: key.createdAt
for (const certificate of certificates) { });
const x509 = new X509Certificate(certificate.cert); }
cns.push({ commonName: x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''), id: certificate.id, createdAt: certificate.createdAt }) }
} const certificates = await prisma.certificate.findMany({ where: { team: { id: teamId } } });
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push({
commonName: x509.subject
.split('\n')
.find((s) => s.startsWith('CN='))
.replace('CN=', ''),
id: certificate.id,
createdAt: certificate.createdAt
});
}
return { return {
settings, settings,
certificates: cns, certificates: cns,
sshKeys: unencryptedKeys, sshKeys: unencryptedKeys,
registries registries
} };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function saveSettings(request: FastifyRequest<SaveSettings>, reply: FastifyReply) { export async function saveSettings(request: FastifyRequest<SaveSettings>, reply: FastifyReply) {
try { try {
let { let {
previewSeparator, previewSeparator,
numberOfDockerImagesKeptLocally, numberOfDockerImagesKeptLocally,
doNotTrack, doNotTrack,
fqdn, fqdn,
isAPIDebuggingEnabled, isAPIDebuggingEnabled,
isRegistrationEnabled, isRegistrationEnabled,
dualCerts, dualCerts,
minPort, minPort,
maxPort, maxPort,
isAutoUpdateEnabled, isAutoUpdateEnabled,
isDNSCheckEnabled, isDNSCheckEnabled,
DNSServers, DNSServers,
proxyDefaultRedirect proxyDefaultRedirect
} = request.body } = request.body;
const { id, previewSeparator: SetPreviewSeparator } = await listSettings(); const { id, previewSeparator: SetPreviewSeparator } = await listSettings();
if (numberOfDockerImagesKeptLocally) { if (numberOfDockerImagesKeptLocally) {
numberOfDockerImagesKeptLocally = Number(numberOfDockerImagesKeptLocally) numberOfDockerImagesKeptLocally = Number(numberOfDockerImagesKeptLocally);
} }
if (previewSeparator == '') { if (previewSeparator == '') {
previewSeparator = '.' previewSeparator = '.';
} }
if (SetPreviewSeparator != previewSeparator) { if (SetPreviewSeparator != previewSeparator) {
const applications = await prisma.application.findMany({ where: { previewApplication: { some: { id: { not: undefined } } } }, include: { previewApplication: true } }) const applications = await prisma.application.findMany({
for (const application of applications) { where: { previewApplication: { some: { id: { not: undefined } } } },
for (const preview of application.previewApplication) { include: { previewApplication: true }
const { protocol } = new URL(preview.customDomain) });
const { pullmergeRequestId } = preview for (const application of applications) {
const { fqdn } = application for (const preview of application.previewApplication) {
const newPreviewDomain = `${protocol}//${pullmergeRequestId}${previewSeparator}${getDomain(fqdn)}` const { protocol } = new URL(preview.customDomain);
await prisma.previewApplication.update({ where: { id: preview.id }, data: { customDomain: newPreviewDomain } }) const { pullmergeRequestId } = preview;
} const { fqdn } = application;
} const newPreviewDomain = `${protocol}//${pullmergeRequestId}${previewSeparator}${getDomain(
} fqdn
)}`;
await prisma.previewApplication.update({
where: { id: preview.id },
data: { customDomain: newPreviewDomain }
});
}
}
}
await prisma.setting.update({ await prisma.setting.update({
where: { id }, where: { id },
data: { previewSeparator, numberOfDockerImagesKeptLocally, doNotTrack, isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled } data: {
}); previewSeparator,
if (fqdn) { numberOfDockerImagesKeptLocally,
await prisma.setting.update({ where: { id }, data: { fqdn } }); doNotTrack,
} isRegistrationEnabled,
await prisma.setting.update({ where: { id }, data: { proxyDefaultRedirect } }); dualCerts,
if (minPort && maxPort) { isAutoUpdateEnabled,
await prisma.setting.update({ where: { id }, data: { minPort, maxPort } }); isDNSCheckEnabled,
} DNSServers,
if (doNotTrack === false) { isAPIDebuggingEnabled
// Sentry.init({ }
// dsn: sentryDSN, });
// environment: isDev ? 'development' : 'production', if (fqdn) {
// release: version await prisma.setting.update({ where: { id }, data: { fqdn } });
// }); }
// console.log('Sentry initialized') await prisma.setting.update({ where: { id }, data: { proxyDefaultRedirect } });
} if (minPort && maxPort) {
return reply.code(201).send() await prisma.setting.update({ where: { id }, data: { minPort, maxPort } });
} catch ({ status, message }) { }
return errorHandler({ status, message }) if (doNotTrack === false) {
} // Sentry.init({
// dsn: sentryDSN,
// environment: isDev ? 'development' : 'production',
// release: version
// });
// console.log('Sentry initialized')
}
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function deleteDomain(request: FastifyRequest<DeleteDomain>, reply: FastifyReply) { export async function deleteDomain(request: FastifyRequest<DeleteDomain>, reply: FastifyReply) {
try { try {
const { fqdn } = request.body const { fqdn } = request.body;
const { DNSServers } = await listSettings(); const { DNSServers } = await listSettings();
if (DNSServers) { if (DNSServers) {
dns.setServers([...DNSServers.split(',')]); dns.setServers([...DNSServers.split(',')]);
} }
let ip; let ip;
try { try {
ip = await dns.resolve(fqdn); ip = await dns.resolve(fqdn);
} catch (error) { } catch (error) {
// Do not care. // Do not care.
} }
await prisma.setting.update({ where: { fqdn }, data: { fqdn: null } }); await prisma.setting.update({ where: { fqdn }, data: { fqdn: null } });
return reply.redirect(302, ip ? `http://${ip[0]}:3000/settings` : undefined) return reply.redirect(302, ip ? `http://${ip[0]}:3000/settings` : undefined);
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function checkDomain(request: FastifyRequest<CheckDomain>) { export async function checkDomain(request: FastifyRequest<CheckDomain>) {
try { try {
const { id } = request.params; const { id } = request.params;
let { fqdn, forceSave, dualCerts, isDNSCheckEnabled } = request.body let { fqdn, forceSave, dualCerts, isDNSCheckEnabled } = request.body;
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
const found = await isDomainConfigured({ id, fqdn }); const found = await isDomainConfigured({ id, fqdn });
if (found) { if (found) {
throw { message: "Domain already configured" }; throw { message: 'Domain already configured' };
} }
if (isDNSCheckEnabled && !forceSave && !isDev) { if (isDNSCheckEnabled && !forceSave && !isDev) {
const hostname = request.hostname.split(':')[0] const hostname = request.hostname.split(':')[0];
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
} }
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function checkDNS(request: FastifyRequest<CheckDNS>) { export async function checkDNS(request: FastifyRequest<CheckDNS>) {
try { try {
const { domain } = request.params; const { domain } = request.params;
await isDNSValid(request.hostname, domain); await isDNSValid(request.hostname, domain);
return {} return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: FastifyReply) { export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: FastifyReply) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { privateKey, name } = request.body; const { privateKey, name } = request.body;
const found = await prisma.sshKey.findMany({ where: { name } }) const found = await prisma.sshKey.findMany({ where: { name } });
if (found.length > 0) { if (found.length > 0) {
throw { throw {
message: "Name already used. Choose another one please." message: 'Name already used. Choose another one please.'
} };
} }
const encryptedSSHKey = encrypt(privateKey) const encryptedSSHKey = encrypt(privateKey);
await prisma.sshKey.create({ data: { name, privateKey: encryptedSSHKey, team: { connect: { id: teamId } } } }) await prisma.sshKey.create({
return reply.code(201).send() data: { name, privateKey: encryptedSSHKey, team: { connect: { id: teamId } } }
} catch ({ status, message }) { });
return errorHandler({ status, message }) return reply.code(201).send();
} } catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) { export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { id } = request.body; const { id } = request.body;
await prisma.sshKey.deleteMany({ where: { id, teamId } }) await prisma.sshKey.deleteMany({ where: { id, teamId } });
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 deleteCertificates(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) { export async function deleteCertificates(
try { request: FastifyRequest<OnlyIdInBody>,
const teamId = request.user.teamId; reply: FastifyReply
const { id } = request.body; ) {
await executeCommand({ command: `docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`, shell: true }) try {
await prisma.certificate.deleteMany({ where: { id, teamId } }) const teamId = request.user.teamId;
return reply.code(201).send() const { id } = request.body;
} catch ({ status, message }) { await executeCommand({
return errorHandler({ status, message }) command: `docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`,
} shell: true
});
await prisma.certificate.deleteMany({ where: { id, teamId } });
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function setDockerRegistry(request: FastifyRequest<SetDefaultRegistry>, reply: FastifyReply) { export async function setDockerRegistry(
try { request: FastifyRequest<SetDefaultRegistry>,
const teamId = request.user.teamId; reply: FastifyReply
const { id, username, password } = request.body; ) {
try {
const teamId = request.user.teamId;
const { id, username, password } = request.body;
let encryptedPassword = '' let encryptedPassword = '';
if (password) encryptedPassword = encrypt(password) if (password) encryptedPassword = encrypt(password);
if (teamId === '0') { if (teamId === '0') {
await prisma.dockerRegistry.update({ where: { id }, data: { username, password: encryptedPassword } }) await prisma.dockerRegistry.update({
} else { where: { id },
await prisma.dockerRegistry.updateMany({ where: { id, teamId }, data: { username, password: encryptedPassword } }) data: { username, password: encryptedPassword }
} });
return reply.code(201).send() } else {
} catch ({ status, message }) { await prisma.dockerRegistry.updateMany({
return errorHandler({ status, message }) where: { id, teamId },
} data: { username, password: encryptedPassword }
});
}
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function addDockerRegistry(request: FastifyRequest<AddDefaultRegistry>, reply: FastifyReply) { export async function addDockerRegistry(
try { request: FastifyRequest<AddDefaultRegistry>,
const teamId = request.user.teamId; reply: FastifyReply
const { name, url, username, password } = request.body; ) {
try {
const teamId = request.user.teamId;
const { name, url, username, password } = request.body;
let encryptedPassword = '' let encryptedPassword = '';
if (password) encryptedPassword = encrypt(password) if (password) encryptedPassword = encrypt(password);
await prisma.dockerRegistry.create({ data: { name, url, username, password: encryptedPassword, team: { connect: { id: teamId } } } }) await prisma.dockerRegistry.create({
data: { name, url, username, password: encryptedPassword, team: { connect: { id: teamId } } }
});
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 deleteDockerRegistry(
request: FastifyRequest<OnlyIdInBody>,
reply: FastifyReply
) {
try {
const teamId = request.user.teamId;
const { id } = request.body;
await prisma.application.updateMany({
where: { dockerRegistryId: id },
data: { dockerRegistryId: null }
});
await prisma.dockerRegistry.deleteMany({ where: { id, teamId } });
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function deleteDockerRegistry(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.body;
await prisma.application.updateMany({ where: { dockerRegistryId: id }, data: { dockerRegistryId: null } })
await prisma.dockerRegistry.deleteMany({ where: { id, teamId } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,191 +1,231 @@
import cuid from 'cuid'; import cuid from 'cuid';
import type { FastifyRequest } from 'fastify'; import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { decrypt, encrypt, errorHandler, prisma } from '../../../../lib/common'; import { decrypt, encrypt, errorHandler, prisma } from '../../../../lib/common';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import { CheckGitLabOAuthId, SaveGitHubSource, SaveGitLabSource } from './types'; import { CheckGitLabOAuthId, SaveGitHubSource, SaveGitLabSource } from './types';
export async function listSources(request: FastifyRequest) { export async function listSources(request: FastifyRequest) {
try { try {
const teamId = request.user?.teamId; const teamId = request.user?.teamId;
const sources = await prisma.gitSource.findMany({ const sources = await prisma.gitSource.findMany({
where: { OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] }, where: {
include: { teams: true, githubApp: true, gitlabApp: true } OR: [
}); { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
return { { isSystemWide: true }
sources ]
} },
} catch ({ status, message }) { include: { teams: true, githubApp: true, gitlabApp: true }
return errorHandler({ status, message }) });
} return {
sources
};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function saveSource(request, reply) { export async function saveSource(request, reply) {
try { try {
const { id } = request.params const { id } = request.params;
let { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } = request.body let { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } = request.body;
if (customPort) customPort = Number(customPort) if (customPort) customPort = Number(customPort);
await prisma.gitSource.update({ await prisma.gitSource.update({
where: { id }, where: { id },
data: { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } data: { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide }
}); });
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 getSource(request: FastifyRequest<OnlyId>) { export async function getSource(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params;
const { teamId } = request.user const { teamId } = request.user;
const settings = await prisma.setting.findFirst({}); const settings = await prisma.setting.findFirst({});
if (id === 'new') { if (id === 'new') {
return { return {
source: { source: {
name: null, name: null,
type: null, type: null,
htmlUrl: null, htmlUrl: null,
apiUrl: null, apiUrl: null,
organization: null, organization: null,
customPort: 22, customPort: 22,
customUser: 'git', customUser: 'git'
}, },
settings settings
} };
} }
const source = await prisma.gitSource.findFirst({ const source = await prisma.gitSource.findFirst({
where: { id, OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] }, where: {
include: { githubApp: true, gitlabApp: true } id,
}); OR: [
if (!source) { { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
throw { status: 404, message: 'Source not found.' } { isSystemWide: true }
} ]
},
include: { githubApp: true, gitlabApp: true }
});
if (!source) {
throw { status: 404, message: 'Source not found.' };
}
if (source?.githubApp?.clientSecret) if (source?.githubApp?.clientSecret)
source.githubApp.clientSecret = decrypt(source.githubApp.clientSecret); source.githubApp.clientSecret = decrypt(source.githubApp.clientSecret);
if (source?.githubApp?.webhookSecret) if (source?.githubApp?.webhookSecret)
source.githubApp.webhookSecret = decrypt(source.githubApp.webhookSecret); source.githubApp.webhookSecret = decrypt(source.githubApp.webhookSecret);
if (source?.githubApp?.privateKey) source.githubApp.privateKey = decrypt(source.githubApp.privateKey); if (source?.githubApp?.privateKey)
if (source?.gitlabApp?.appSecret) source.gitlabApp.appSecret = decrypt(source.gitlabApp.appSecret); source.githubApp.privateKey = decrypt(source.githubApp.privateKey);
if (source?.gitlabApp?.appSecret)
source.gitlabApp.appSecret = decrypt(source.gitlabApp.appSecret);
return { return {
source, source,
settings settings
}; };
} catch ({ status, message }) {
} catch ({ status, message }) { return errorHandler({ status, message });
return errorHandler({ status, message }) }
}
} }
export async function deleteSource(request) { export async function deleteSource(request) {
try { try {
const { id } = request.params const { id } = request.params;
const source = await prisma.gitSource.delete({ const gitAppFound = await prisma.application.findFirst({ where: { gitSourceId: id } });
where: { id }, if (gitAppFound) {
include: { githubApp: true, gitlabApp: true } throw {
}); status: 400,
if (source.githubAppId) { message: 'This source is used by an application. Please remove the application first.'
await prisma.githubApp.delete({ where: { id: source.githubAppId } }); };
} }
if (source.gitlabAppId) { const source = await prisma.gitSource.delete({
await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } }); where: { id },
} include: { githubApp: true, gitlabApp: true }
return {} });
} catch ({ status, message }) { if (source.githubAppId) {
return errorHandler({ status, message }) await prisma.githubApp.delete({ where: { id: source.githubAppId } });
} }
if (source.gitlabAppId) {
await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } });
}
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function saveGitHubSource(request: FastifyRequest<SaveGitHubSource>) { export async function saveGitHubSource(request: FastifyRequest<SaveGitHubSource>) {
try { try {
const { teamId } = request.user const { teamId } = request.user;
const { id } = request.params const { id } = request.params;
let { name, htmlUrl, apiUrl, organization, customPort, isSystemWide } = request.body let { name, htmlUrl, apiUrl, organization, customPort, isSystemWide } = request.body;
if (customPort) customPort = Number(customPort) if (customPort) customPort = Number(customPort);
if (id === 'new') { if (id === 'new') {
const newId = cuid() const newId = cuid();
await prisma.gitSource.create({ await prisma.gitSource.create({
data: { data: {
id: newId, id: newId,
name, name,
htmlUrl, htmlUrl,
apiUrl, apiUrl,
organization, organization,
customPort, customPort,
isSystemWide, isSystemWide,
type: 'github', type: 'github',
teams: { connect: { id: teamId } } teams: { connect: { id: teamId } }
} }
}); });
return { return {
id: newId id: newId
} };
} }
throw { status: 500, message: 'Wrong request.' } throw { status: 500, message: 'Wrong request.' };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }
export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>) { export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>) {
try { try {
const { id } = request.params const { id } = request.params;
const { teamId } = request.user const { teamId } = request.user;
let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName, customPort, customUser } = let {
request.body type,
name,
htmlUrl,
apiUrl,
oauthId,
appId,
appSecret,
groupName,
customPort,
customUser
} = request.body;
if (oauthId) oauthId = Number(oauthId); if (oauthId) oauthId = Number(oauthId);
if (customPort) customPort = Number(customPort) if (customPort) customPort = Number(customPort);
const encryptedAppSecret = encrypt(appSecret); const encryptedAppSecret = encrypt(appSecret);
if (id === 'new') { if (id === 'new') {
const newId = cuid() const newId = cuid();
await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, customPort, customUser, teams: { connect: { id: teamId } } } }); await prisma.gitSource.create({
await prisma.gitlabApp.create({ data: {
data: { id: newId,
teams: { connect: { id: teamId } }, type,
appId, apiUrl,
oauthId, htmlUrl,
groupName, name,
appSecret: encryptedAppSecret, customPort,
gitSource: { connect: { id: newId } } customUser,
} teams: { connect: { id: teamId } }
}); }
return { });
status: 201, await prisma.gitlabApp.create({
id: newId data: {
} teams: { connect: { id: teamId } },
} else { appId,
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name, customPort, customUser } }); oauthId,
await prisma.gitlabApp.update({ groupName,
where: { id }, appSecret: encryptedAppSecret,
data: { gitSource: { connect: { id: newId } }
appId, }
oauthId, });
groupName, return {
appSecret: encryptedAppSecret, status: 201,
} id: newId
}); };
} } else {
return { status: 201 }; await prisma.gitSource.update({
where: { id },
} catch ({ status, message }) { data: { type, apiUrl, htmlUrl, name, customPort, customUser }
return errorHandler({ status, message }) });
} await prisma.gitlabApp.update({
where: { id },
data: {
appId,
oauthId,
groupName,
appSecret: encryptedAppSecret
}
});
}
return { status: 201 };
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function checkGitLabOAuthID(request: FastifyRequest<CheckGitLabOAuthId>) { export async function checkGitLabOAuthID(request: FastifyRequest<CheckGitLabOAuthId>) {
try { try {
const { oauthId } = request.body const { oauthId } = request.body;
const found = await prisma.gitlabApp.findFirst({ where: { oauthId: Number(oauthId) } }); const found = await prisma.gitlabApp.findFirst({ where: { oauthId: Number(oauthId) } });
if (found) { if (found) {
throw { status: 500, message: 'OAuthID already configured in Coolify.' } throw { status: 500, message: 'OAuthID already configured in Coolify.' };
} }
return {} return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }

View File

@@ -1,8 +1,9 @@
import { FastifyRequest } from 'fastify'; import { FastifyRequest } from 'fastify';
import { errorHandler, getDomain, isDev, prisma, executeCommand } from '../../../lib/common'; import { errorHandler, executeCommand, getDomain, isDev, prisma } from '../../../lib/common';
import { getTemplates } from '../../../lib/services'; import { getTemplates } from '../../../lib/services';
import { OnlyId } from '../../../types'; import { OnlyId } from '../../../types';
import { parseAndFindServiceTemplates } from '../../api/v1/services/handlers'; import { parseAndFindServiceTemplates } from '../../api/v1/services/handlers';
import { hashPassword } from '../../api/v1/handlers';
function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps = false) { function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps = false) {
if (isHttp2) { if (isHttp2) {
@@ -39,7 +40,7 @@ function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps
} }
}; };
} }
function generateRouters( async function generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
@@ -48,20 +49,22 @@ function generateRouters(
isWWW, isWWW,
isDualCerts, isDualCerts,
isCustomSSL, isCustomSSL,
isHttp2 = false isHttp2 = false,
) { httpBasicAuth = null,
let rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`; }) {
let ruleWWW = `Host(\`www.${nakedDomain}\`)${ const rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : '' const ruleWWW = `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''
}`; }`;
let http: any = {
const http: any = {
entrypoints: ['web'], entrypoints: ['web'],
rule, rule,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
middlewares: [] middlewares: []
}; };
let https: any = { const https: any = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule, rule,
service: `${serviceId}`, service: `${serviceId}`,
@@ -71,14 +74,14 @@ function generateRouters(
}, },
middlewares: [] middlewares: []
}; };
let httpWWW: any = { const httpWWW: any = {
entrypoints: ['web'], entrypoints: ['web'],
rule: ruleWWW, rule: ruleWWW,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
middlewares: [] middlewares: []
}; };
let httpsWWW: any = { const httpsWWW: any = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: ruleWWW, rule: ruleWWW,
service: `${serviceId}`, service: `${serviceId}`,
@@ -97,6 +100,10 @@ function generateRouters(
httpsWWW.middlewares.push('redirect-to-non-www'); httpsWWW.middlewares.push('redirect-to-non-www');
delete https.tls; delete https.tls;
delete httpsWWW.tls; delete httpsWWW.tls;
if (httpBasicAuth) {
http.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 3. http + www only // 3. http + www only
@@ -108,6 +115,10 @@ function generateRouters(
https.middlewares.push('redirect-to-www'); https.middlewares.push('redirect-to-www');
delete https.tls; delete https.tls;
delete httpsWWW.tls; delete httpsWWW.tls;
if (httpBasicAuth) {
httpWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 5. https + non-www only // 5. https + non-www only
if (isHttps && !isWWW) { if (isHttps && !isWWW) {
@@ -136,6 +147,10 @@ function generateRouters(
}; };
} }
} }
if (httpBasicAuth) {
https.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 6. https + www only // 6. https + www only
if (isHttps && isWWW) { if (isHttps && isWWW) {
@@ -145,6 +160,11 @@ function generateRouters(
http.middlewares.push('redirect-to-www'); http.middlewares.push('redirect-to-www');
https.middlewares.push('redirect-to-www'); https.middlewares.push('redirect-to-www');
} }
if (httpBasicAuth) {
httpsWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
if (isCustomSSL) { if (isCustomSSL) {
if (isDualCerts) { if (isDualCerts) {
https.tls = true; https.tls = true;
@@ -166,23 +186,23 @@ function generateRouters(
} }
} }
if (isHttp2) { if (isHttp2) {
let http2 = { const http2 = {
...http, ...http,
service: `${serviceId}-http2`, service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
}; };
let http2WWW = { const http2WWW = {
...httpWWW, ...httpWWW,
service: `${serviceId}-http2`, service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
}; };
let https2 = { const https2 = {
...https, ...https,
service: `${serviceId}-http2`, service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
}; };
let https2WWW = { const https2WWW = {
...httpsWWW, ...httpsWWW,
service: `${serviceId}-http2`, service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
@@ -198,14 +218,17 @@ function generateRouters(
[`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW } [`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW }
}; };
} }
return {
const result = {
[`${serviceId}-${pathPrefix}`]: { ...http }, [`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-secure`]: { ...https }, [`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW }, [`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW } [`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }
}; };
return result;
} }
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote: boolean = false) { export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote = false) {
const traefik = { const traefik = {
tls: { tls: {
certificates: [] certificates: []
@@ -298,7 +321,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
}); });
} }
let parsedCertificates = []; const parsedCertificates = [];
for (const certificate of certificates) { for (const certificate of certificates) {
parsedCertificates.push({ parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`, certFile: `${sslpath}/${certificate.id}-cert.pem`,
@@ -369,7 +392,10 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
dockerComposeConfiguration, dockerComposeConfiguration,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings settings,
basicAuthUser,
basicAuthPw,
settings: { basicAuth: isBasicAuthEnabled }
} = application; } = application;
if (!destinationDockerId) { if (!destinationDockerId) {
continue; continue;
@@ -382,6 +408,14 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
) { ) {
continue; continue;
} }
let httpBasicAuth = null;
if (basicAuthUser && basicAuthPw && isBasicAuthEnabled) {
httpBasicAuth = {
basicAuth: {
users: [basicAuthUser + ':' + await hashPassword(basicAuthPw, 1)]
}
};
}
if (buildPack === 'compose') { if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration)); const services = Object.entries(JSON.parse(dockerComposeConfiguration));
if (services.length > 0) { if (services.length > 0) {
@@ -404,27 +438,33 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL,
) httpBasicAuth
})
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
...generateServices(serviceId, containerId, port) ...generateServices(serviceId, containerId, port)
}; };
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
} }
} }
} }
continue; continue;
} }
const { previews, dualCerts, isCustomSSL, isHttp2 } = settings; const { previews, dualCerts, isCustomSSL, isHttp2, basicAuth } = settings;
const { network, id: dockerId } = destinationDocker; const { network, id: dockerId } = destinationDocker;
if (!fqdn) { if (!fqdn) {
continue; continue;
@@ -437,22 +477,28 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${id}-${port || 'default'}`; const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL, isCustomSSL,
isHttp2 isHttp2,
) httpBasicAuth
})
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
...generateServices(serviceId, id, port, isHttp2, isHttps) ...generateServices(serviceId, id, port, isHttp2, isHttps)
}; };
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
if (previews) { if (previews) {
const { stdout } = await executeCommand({ const { stdout } = await executeCommand({
dockerId, dockerId,
@@ -466,29 +512,35 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
.map((c) => c.replace(/"/g, '')); .map((c) => c.replace(/"/g, ''));
if (containers.length > 0) { if (containers.length > 0) {
for (const container of containers) { for (const container of containers) {
const previewDomain = `${container.split('-')[1]}${ const previewDomain = `${container.split('-')[1]}${coolifySettings.previewSeparator
coolifySettings.previewSeparator }${domain}`;
}${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, ''); const nakedDomain = previewDomain.replace(/^www\./, '');
const pathPrefix = '/'; const pathPrefix = '/';
const serviceId = `${container}-${port || 'default'}`; const serviceId = `${container}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
previewDomain, domain: previewDomain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL,
) isHttp2: false,
httpBasicAuth
})
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
...generateServices(serviceId, container, port, isHttp2) ...generateServices(serviceId, container, port, isHttp2)
}; };
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
} }
} }
} }
@@ -542,7 +594,10 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
if (isDomainAndProxyConfiguration.length > 0) { if (isDomainAndProxyConfiguration.length > 0) {
const template: any = await parseAndFindServiceTemplates(service, null, true); const template: any = await parseAndFindServiceTemplates(service, null, true);
const { proxy } = template.services[oneService] || found.services[oneService]; const { proxy } = template.services[oneService] || found.services[oneService];
for (let configuration of proxy) { for (const configuration of proxy) {
if (configuration.hostPort) {
continue;
}
if (configuration.domain) { if (configuration.domain) {
const setting = serviceSetting.find( const setting = serviceSetting.find(
(a) => a.variableName === configuration.domain (a) => a.variableName === configuration.domain
@@ -579,16 +634,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${oneService}-${port || 'default'}`; const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL,
) })
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
@@ -616,16 +671,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${oneService}-${port || 'default'}`; const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL
) })
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
@@ -657,16 +712,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${id}-${port || 'default'}`; const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL
) })
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,

View File

@@ -4,9 +4,9 @@ import { proxyConfiguration, otherProxyConfiguration } from './handlers';
import { OtherProxyConfiguration } from './types'; import { OtherProxyConfiguration } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/main.json', async (request, reply) => proxyConfiguration(request, false)); fastify.get<OnlyId>('/main.json', async (request) => proxyConfiguration(request, false));
fastify.get<OnlyId>('/remote/:id', async (request) => proxyConfiguration(request, true)); fastify.get<OnlyId>('/remote/:id', async (request) => proxyConfiguration(request, true));
fastify.get<OtherProxyConfiguration>('/other.json', async (request, reply) => fastify.get<OtherProxyConfiguration>('/other.json', async (request) =>
otherProxyConfiguration(request) otherProxyConfiguration(request)
); );
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -42,8 +42,6 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@sentry/svelte": "7.21.1",
"@sentry/tracing": "7.21.1",
"@sveltejs/adapter-static": "1.0.0-next.48", "@sveltejs/adapter-static": "1.0.0-next.48",
"@tailwindcss/typography": "0.5.8", "@tailwindcss/typography": "0.5.8",
"cuid": "2.1.8", "cuid": "2.1.8",

View File

@@ -1,13 +1,10 @@
import * as Sentry from '@sentry/svelte';
export async function handle({ event, resolve }) { export async function handle({ event, resolve }) {
const response = await resolve(event, { ssr: false }); const response = await resolve(event, { ssr: false });
return response; return response;
} }
export const handleError = ({ error, event }) => { export const handleError = ({ error, event }) => {
Sentry.captureException(error, { event }); return {
message: 'Whoops!',
return { code: error?.code ?? 'UNKNOWN'
message: 'Whoops!', };
code: error?.code ?? 'UNKNOWN' };
};
};

View File

@@ -1,5 +1,6 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { dashify } from './common';
export function getAPIUrl() { export function getAPIUrl() {
if (GITPOD_WORKSPACE_URL) { if (GITPOD_WORKSPACE_URL) {
@@ -72,17 +73,19 @@ async function send({
...headers ...headers
}; };
} }
if (token && !path.startsWith('https://')) {
if (token && !path.startsWith('https://') && !path.startsWith('http://')) {
opts.headers = { opts.headers = {
...opts.headers, ...opts.headers,
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}; };
} }
if (!path.startsWith('https://')) {
if (!path.startsWith('https://') && !path.startsWith('http://')) {
path = `/api/v1${path}`; path = `/api/v1${path}`;
} }
if (dev && !path.startsWith('https://')) { if (dev && !path.startsWith('https://') && !path.startsWith('http://')) {
path = `${getAPIUrl()}${path}`; path = `${getAPIUrl()}${path}`;
} }
if (method === 'POST' && data && !opts.body) { if (method === 'POST' && data && !opts.body) {
@@ -100,6 +103,14 @@ async function send({
responseData = await response.json(); responseData = await response.json();
} else if (contentType?.indexOf('text/plain') !== -1) { } else if (contentType?.indexOf('text/plain') !== -1) {
responseData = await response.text(); responseData = await response.text();
} else if (contentType?.indexOf('application/octet-stream') !== -1) {
responseData = await response.blob();
const fileName = dashify(data.id + '-' + data.name)
const fileLink = document.createElement('a');
fileLink.href = URL.createObjectURL(new Blob([responseData]))
fileLink.download = fileName + '.gz';
fileLink.click();
fileLink.remove();
} else { } else {
return {}; return {};
} }

View File

@@ -196,6 +196,9 @@
"domain_fqdn": "Domain (FQDN)", "domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>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-white '>You must set your DNS to point to the server IP in advance.</span>", "https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>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-white '>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?", "ssl_www_and_non_www": "Generate SSL for www and non-www?",
"basic_auth": "Basic Auth",
"basic_auth_user": "User",
"basic_auth_pw": "Password",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.", "ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command", "install_command": "Install Command",
"build_command": "Build Command", "build_command": "Build Command",

View File

@@ -65,7 +65,6 @@
<script lang="ts"> <script lang="ts">
export let settings: any; export let settings: any;
export let sentryDSN: any;
export let baseSettings: any; export let baseSettings: any;
export let pendingInvitations: any = 0; export let pendingInvitations: any = 0;
@@ -98,10 +97,6 @@
import Toasts from '$lib/components/Toasts.svelte'; import Toasts from '$lib/components/Toasts.svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import LocalePicker from '$lib/components/LocalePicker.svelte';
import * as Sentry from '@sentry/svelte';
import { BrowserTracing } from '@sentry/tracing';
import { dev } from '$app/env';
if (userId) $appSession.userId = userId; if (userId) $appSession.userId = userId;
if (teamId) $appSession.teamId = teamId; if (teamId) $appSession.teamId = teamId;
@@ -293,7 +288,7 @@
</a> </a>
<a <a
id="documentation" id="documentation"
href="https://docs.coollabs.io/coolify/" href="https://docs.coollabs.io/coolify-v3/"
target="_blank" target="_blank"
rel="noreferrer external" rel="noreferrer external"
class="icons hover:text-info" class="icons hover:text-info"
@@ -384,7 +379,7 @@
</div> </div>
<div class="drawer-side"> <div class="drawer-side">
<label for="main-drawer" class="drawer-overlay w-full" /> <label for="main-drawer" class="drawer-overlay w-full" />
<ul class="menu bg-coolgray-200 w-60 p-2 space-y-3 pt-4 "> <ul class="menu bg-coolgray-200 w-60 p-2 space-y-3 pt-4">
<li> <li>
<a <a
class="no-underline icons hover:text-white hover:bg-pink-500" class="no-underline icons hover:text-white hover:bg-pink-500"
@@ -498,7 +493,7 @@
<li> <li>
<a <a
class="no-underline icons hover:text-white hover:bg-info" class="no-underline icons hover:text-white hover:bg-info"
href="https://docs.coollabs.io/coolify/" href="https://docs.coollabs.io/coolify-v3/"
target="_blank" target="_blank"
rel="noreferrer external" rel="noreferrer external"
> >

View File

@@ -255,12 +255,12 @@
{/if} {/if}
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<div class="title py-4 pr-4">Public Repository from Git</div> <div class="title py-4 pr-4">Public Repository from Git</div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository-from-git" /> <DocLink url="https://docs.coollabs.io/coolify-v3/applications/#public-repository-from-git" />
</div> </div>
<PublicRepository /> <PublicRepository />
<div class="flex flex-row items-center pt-10"> <div class="flex flex-row items-center pt-10">
<div class="title py-4 pr-4">Simple Dockerfile <Beta /></div> <div class="title py-4 pr-4">Simple Dockerfile <Beta /></div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#dockerfile" /> <DocLink url="https://docs.coollabs.io/coolify-v3/applications/#simple-dockerfile" />
</div> </div>
<div class="mx-auto max-w-screen-2xl"> <div class="mx-auto max-w-screen-2xl">
<form class="flex flex-col" on:submit|preventDefault={handleDockerImage}> <form class="flex flex-col" on:submit|preventDefault={handleDockerImage}>

View File

@@ -29,27 +29,28 @@
export let application: any; export let application: any;
export let settings: any; export let settings: any;
import yaml from 'js-yaml'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { get, getAPIUrl, post } from '$lib/api'; import { get, getAPIUrl, post } from '$lib/api';
import cuid from 'cuid'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Beta from '$lib/components/Beta.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { import {
addToast, addToast,
appSession, appSession,
checkIfDeploymentEnabledApplications, checkIfDeploymentEnabledApplications,
setLocation, features,
status,
isDeploymentEnabled, isDeploymentEnabled,
features setLocation,
status
} from '$lib/store'; } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; import cuid from 'cuid';
import Setting from '$lib/components/Setting.svelte'; import yaml from 'js-yaml';
import Explainer from '$lib/components/Explainer.svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import Select from 'svelte-select';
import Beta from '$lib/components/Beta.svelte';
import { saveForm } from './utils'; import { saveForm } from './utils';
const { id } = $page.params; const { id } = $page.params;
@@ -77,6 +78,7 @@
let isCustomSSL = application.settings?.isCustomSSL; let isCustomSSL = application.settings?.isCustomSSL;
let autodeploy = application.settings?.autodeploy; let autodeploy = application.settings?.autodeploy;
let isBot = application.settings?.isBot; let isBot = application.settings?.isBot;
let basicAuth = application.settings?.basicAuth;
let isDBBranching = application.settings?.isDBBranching; let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl; let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings.isHttp2; let isHttp2 = application.settings.isHttp2;
@@ -186,6 +188,9 @@
if (name === 'isCustomSSL') { if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL; isCustomSSL = !isCustomSSL;
} }
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isBot') { if (name === 'isBot') {
if ($status.application.overallStatus !== 'stopped') return; if ($status.application.overallStatus !== 'stopped') return;
isBot = !isBot; isBot = !isBot;
@@ -210,7 +215,8 @@
isCustomSSL, isCustomSSL,
isHttp2, isHttp2,
branch: application.branch, branch: application.branch,
projectId: application.projectId projectId: application.projectId,
basicAuth
}); });
return addToast({ return addToast({
message: $t('application.settings_saved'), message: $t('application.settings_saved'),
@@ -232,6 +238,9 @@
if (name === 'isBot') { if (name === 'isBot') {
isBot = !isBot; isBot = !isBot;
} }
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isDBBranching') { if (name === 'isDBBranching') {
isDBBranching = !isDBBranching; isDBBranching = !isDBBranching;
} }
@@ -272,6 +281,7 @@
} }
} }
} }
console.log(application);
await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration); await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration);
setLocation(application, settings); setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications(application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
@@ -498,7 +508,7 @@
<div class="title font-bold pb-3">General</div> <div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"
type="submit" type="submit"
class:loading={loading.save} class:loading={loading.save}
class:bg-orange-600={forceSave} class:bg-orange-600={forceSave}
@@ -751,7 +761,7 @@
on:click={() => !isDisabled && changeSettings('dualCerts')} on:click={() => !isDisabled && changeSettings('dualCerts')}
/> />
</div> </div>
{#if isHttps && application.buildPack !== 'compose'} {#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4"> <div class="grid grid-cols-2 items-center pb-4">
<Setting <Setting
@@ -774,6 +784,46 @@
on:click={() => changeSettings('isHttp2')} on:click={() => changeSettings('isHttp2')}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center">
<Setting
id="basicAuth"
isCenter={false}
bind:setting={basicAuth}
title={$t('application.basic_auth')}
description="Activate basic authentication for your application. <br>Useful if you want to protect your application with a password. <br><br>Use the <span class='font-bold text-settings'>username</span> and <span class='font-bold text-settings'>password</span> fields to set the credentials."
on:click={() => changeSettings('basicAuth')}
/>
</div>
{#if basicAuth}
<div class="grid grid-cols-2 items-center">
<label for="basicAuthUser">{$t('application.basic_auth_user')}</label>
<input
bind:this={fqdnEl}
class="w-full"
required={!application.settings?.basicAuth}
name="basicAuthUser"
id="basicAuthUser"
class:border={!application.settings?.basicAuth && !application.basicAuthUser}
class:border-red-500={!application.settings?.basicAuth &&
!application.basicAuthUser}
bind:value={application.basicAuthUser}
placeholder="eg: admin"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="basicAuthPw">{$t('application.basic_auth_pw')}</label>
<CopyPasswordField
bind:this={fqdnEl}
isPasswordField={true}
required={!application.settings?.basicAuth}
name="basicAuthPw"
id="basicAuthPw"
bind:value={application.basicAuthPw}
placeholder="**********"
/>
</div>
{/if}
{/if} {/if}
</div> </div>
{#if isSimpleDockerfile} {#if isSimpleDockerfile}
@@ -782,7 +832,7 @@
</div> </div>
<div class="grid grid-flow-row gap-2 px-4 pr-5"> <div class="grid grid-flow-row gap-2 px-4 pr-5">
<div class="grid grid-cols-2 items-center pt-4"> <div class="grid grid-cols-2 items-center pt-4">
<label for="simpleDockerfile">Dockerfile</label> <label for="simpleDockerfile">Dockerfile</label>
<div class="flex gap-2"> <div class="flex gap-2">
<textarea <textarea

View File

@@ -51,16 +51,22 @@
async function loadLogs() { async function loadLogs() {
if (logsLoading) return; if (logsLoading) return;
try { try {
const newLogs: any = await get( const since = lastLog?.split(' ')[0] || 0;
`/applications/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}` const newLogs: any = await get(`/applications/${id}/logs/${selectedService}?since=${since}`);
);
if (newLogs.noContainer) { if (newLogs.noContainer) {
noContainer = true; noContainer = true;
} else { } else {
noContainer = false; noContainer = false;
} }
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) { if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs); if (since === 0) {
logs = logs.concat(newLogs.logs);
} else {
const newParsedLogs = newLogs.logs.filter((log: any) => {
return log !== logs[logs.length - 1];
});
logs = logs.concat(newParsedLogs);
}
lastLog = newLogs.logs[newLogs.logs.length - 1]; lastLog = newLogs.logs[newLogs.logs.length - 1];
} }
} catch (error) { } catch (error) {
@@ -135,7 +141,7 @@
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2"> <div class="flex justify-start sticky space-x-2 pb-2">
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}> <button on:click={followBuild} class="btn btn-sm" class:bg-coollabs={followingLogs}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2" class="w-6 h-6 mr-2"

View File

@@ -13,17 +13,19 @@
import Redis from './_Redis.svelte'; import Redis from './_Redis.svelte';
import CouchDb from './_CouchDb.svelte'; import CouchDb from './_CouchDb.svelte';
import EdgeDB from './_EdgeDB.svelte'; import EdgeDB from './_EdgeDB.svelte';
import { post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { addToast, appSession, status } from '$lib/store'; import { addToast, appSession, status } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
let loading = { let loading = {
main: false, main: false,
public: false public: false,
backup: false
}; };
let publicUrl = ''; let publicUrl = '';
let appendOnly = database.settings.appendOnly; let appendOnly = database.settings.appendOnly;
@@ -109,6 +111,7 @@
if ($status.database.isPublic) { if ($status.database.isPublic) {
database.publicPort = publicPort; database.publicPort = publicPort;
} }
generateUrl();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@@ -130,6 +133,22 @@
loading.main = false; loading.main = false;
} }
} }
async function backupDatabase() {
try {
loading.backup = true;
addToast({
message:
'Backup will be downloaded soon and saved to /var/lib/docker/volumes/coolify-local-backup/_data/ on the host system.',
type: 'success',
timeout: 15000
});
return await post(`/databases/${id}/backup`, { id, name: database.name });
} catch (error) {
return errorNotification(error);
} finally {
loading.backup = false;
}
}
</script> </script>
<div class="mx-auto max-w-6xl p-4"> <div class="mx-auto max-w-6xl p-4">
@@ -144,6 +163,19 @@
class:bg-databases={!loading.main} class:bg-databases={!loading.main}
disabled={loading.main}>{$t('forms.save')}</button disabled={loading.main}>{$t('forms.save')}</button
> >
{#if database.type !== 'redis' && database.type !== 'edgedb'}
{#if $status.database.isRunning}
<button
class="btn btn-sm"
on:click={backupDatabase}
class:loading={loading.backup}
class:bg-databases={!loading.backup}
disabled={loading.backup}>Backup Database</button
>
{:else}
<button disabled class="btn btn-sm">Backup Database (start the database)</button>
{/if}
{/if}
{/if} {/if}
</div> </div>
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2"> <div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">

View File

@@ -34,14 +34,20 @@
customClass="max-w-[32rem]" customClass="max-w-[32rem]"
text="Remote Docker Engines are using <span class='text-white font-bold'>SSH</span> to communicate with the remote docker engine. text="Remote Docker Engines are using <span class='text-white font-bold'>SSH</span> to communicate with the remote docker engine.
You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker. You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker.
<br>See <a class='text-white' href='https://docs.coollabs.io/coolify/destinations#remote-docker-engine' target='blank'>docs</a> for more details." <br>See <a class='text-white' href='https://docs.coollabs.io-v3/coolify/destinations#remote-docker-engine' target='blank'>docs</a> for more details."
/> />
</div> </div>
<div class="flex justify-center px-6 pb-8"> <div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0"> <div
class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0"
>
<div class="title font-bold">{$t('forms.configuration')}</div> <div class="title font-bold">{$t('forms.configuration')}</div>
<button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading} <button
type="submit"
class="btn btn-sm bg-destinations w-full lg:w-fit"
class:loading
disabled={loading}
>{loading >{loading
? payload.isCoolifyProxyUsed ? payload.isCoolifyProxyUsed
? $t('destination.new.saving_and_configuring_proxy') ? $t('destination.new.saving_and_configuring_proxy')

View File

@@ -75,6 +75,25 @@
} }
} }
} }
async function forceDeleteDestination(destination: any) {
let sure = confirm($t('application.confirm_to_delete', { name: destination.name }));
if (sure) {
sure = confirm(
'Are you REALLY sure? This will delete all resources associated with this destination, but not on the destination (server) itself. You will have manually delete everything on the server afterwards.'
);
if (sure) {
sure = confirm('REALLY?');
if (sure) {
try {
await del(`/destinations/${destination.id}/force`, { id: destination.id });
return await goto('/', { replaceState: true });
} catch (error) {
return errorNotification(error);
}
}
}
}
}
function deletable() { function deletable() {
if (!isDestinationDeletable) { if (!isDestinationDeletable) {
return 'Please delete all resources before deleting this.'; return 'Please delete all resources before deleting this.';
@@ -88,7 +107,7 @@
</script> </script>
{#if $page.params.id !== 'new'} {#if $page.params.id !== 'new'}
<nav class="header lg:flex-row flex-col-reverse"> <nav class="header lg:flex-row flex-col-reverse gap-2">
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0"> <div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
<div class="flex flex-col items-center justify-center title"> <div class="flex flex-col items-center justify-center title">
{#if $page.url.pathname === `/destinations/${$page.params.id}`} {#if $page.url.pathname === `/destinations/${$page.params.id}`}
@@ -111,6 +130,16 @@
> >
<Tooltip triggeredBy="#delete">{deletable()}</Tooltip> <Tooltip triggeredBy="#delete">{deletable()}</Tooltip>
</div> </div>
<div class="flex flex-row flex-wrap justify-center lg:justify-start lg:py-0 items-center">
<button
id="forceDelete"
on:click={() => forceDeleteDestination(destination)}
type="submit"
disabled={!$appSession.isAdmin && isDestinationDeletable}
class="icons bg-transparent text-sm text-red-500"><DeleteIcon /></button
>
<Tooltip triggeredBy="#forceDelete">Force Delete</Tooltip>
</div>
</nav> </nav>
{/if} {/if}
<slot /> <slot />

View File

@@ -43,6 +43,7 @@
return true; return true;
} }
}); });
let customVersion: string;
$: isDisabled = $: isDisabled =
!$appSession.isAdmin || !$appSession.isAdmin ||
$status.service.overallStatus === 'degraded' || $status.service.overallStatus === 'degraded' ||
@@ -111,6 +112,7 @@
setLocation(service); setLocation(service);
forceSave = false; forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices(service); $isDeploymentEnabled = checkIfDeploymentEnabledServices(service);
customVersion = null;
return addToast({ return addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'
@@ -196,26 +198,12 @@
async function selectTag(event: any) { async function selectTag(event: any) {
service.version = event.detail.value; service.version = event.detail.value;
} }
async function setCustomVersion() {
service.version = customVersion;
}
onMount(async () => { onMount(async () => {
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
service.fqdn = `http://${cuid()}.demo.coolify.io`; service.fqdn = `http://${cuid()}.demo.coolify.io`;
// if (service.type === 'wordpress') {
// service.wordpress.mysqlDatabase = 'db';
// }
// if (service.type === 'plausibleanalytics') {
// service.plausibleAnalytics.email = 'noreply@demo.com';
// service.plausibleAnalytics.username = 'admin';
// }
// if (service.type === 'minio') {
// service.minio.apiFqdn = `http://${cuid()}.demo.coolify.io`;
// }
// if (service.type === 'ghost') {
// service.ghost.mariadbDatabase = 'db';
// }
// if (service.type === 'fider') {
// service.fider.emailNoreply = 'noreply@demo.com';
// }
// await handleSubmit();
} }
}); });
</script> </script>
@@ -224,7 +212,7 @@
<form id="saveForm" on:submit|preventDefault={handleSubmit}> <form id="saveForm" on:submit|preventDefault={handleSubmit}>
<div class="mx-auto w-full"> <div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 ">General</div> <div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
@@ -289,6 +277,7 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="version">Version / Tag</label> <label for="version">Version / Tag</label>
<div class="flex gap-2">
{#if tags.tags?.length > 0} {#if tags.tags?.length > 0}
<div class="custom-select-wrapper w-full"> <div class="custom-select-wrapper w-full">
<Select <Select
@@ -303,9 +292,11 @@
isClearable={false} isClearable={false}
/> />
</div> </div>
{:else} {:else}
<input class="w-full border-red-500" disabled placeholder="Error getting tags..." /> <input class="w-full border-red-500" disabled placeholder="Error getting tags..." />
{/if} {/if}
<input class="w-full" placeholder="Custom version" on:change={setCustomVersion} bind:value={customVersion} />
</div>
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">

View File

@@ -56,23 +56,23 @@
async function rollback() { async function rollback() {
if (rollbackVersion) { if (rollbackVersion) {
const sure = confirm(`Are you sure you want rollback Coolify to ${rollbackVersion}?`); const sure = confirm(`Are you sure you want upgrade Coolify to ${rollbackVersion}?`);
if (sure) { if (sure) {
try { try {
loading.rollback = true; loading.rollback = true;
console.log('loading.rollback', loading.rollback); console.log('loading.rollback', loading.rollback);
if (dev) { if (dev) {
console.log('rolling back to', rollbackVersion); console.log('Upgrading to ', rollbackVersion);
await asyncSleep(4000); await asyncSleep(4000);
return window.location.reload(); return window.location.reload();
} else { } else {
addToast({ addToast({
message: 'Rollback started...', message: 'Upgrade started...',
type: 'success' type: 'success'
}); });
await post(`/update`, { type: 'update', latestVersion: rollbackVersion }); await post(`/update`, { type: 'update', latestVersion: rollbackVersion });
addToast({ addToast({
message: 'Rollback completed.<br><br>Waiting for the new version to start...', message: 'Upgrade completed.<br><br>Waiting for the new version to start...',
type: 'success' type: 'success'
}); });
@@ -381,12 +381,12 @@
/> />
</div> </div>
<div class="grid grid-cols-4 items-center"> <div class="grid grid-cols-4 items-center pb-12">
<div class="col-span-2"> <div class="col-span-2">
Rollback Coolify to a specific version Upgrade Coolify to a specific version
<Explainer <Explainer
position="dropdown-bottom" position="dropdown-bottom"
explanation="You can rollback to a specific version of Coolify. This will not affect your current running resources.<br><br><a href='https://github.com/coollabsio/coolify/releases' target='_blank'>See available versions</a>" explanation="You can upgrade to a specific version of Coolify. This will not affect your current running resources, but could cause issues if you downgrade to an older version where the database layout was different..<br><br><a href='https://github.com/coollabsio/coolify/releases' target='_blank'>See available versions</a>"
/> />
</div> </div>
<input <input
@@ -401,7 +401,7 @@
class:loading={loading.rollback} class:loading={loading.rollback}
class="btn btn-primary ml-2" class="btn btn-primary ml-2"
disabled={!rollbackVersion || loading.rollback} disabled={!rollbackVersion || loading.rollback}
on:click|preventDefault|stopPropagation={rollback}>Rollback</button on:click|preventDefault|stopPropagation={rollback}>Upgrade</button
> >
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">

View File

@@ -52,7 +52,7 @@
appSecret: source.gitlabApp.appSecret, appSecret: source.gitlabApp.appSecret,
groupName: source.gitlabApp.groupName, groupName: source.gitlabApp.groupName,
customPort: source.customPort, customPort: source.customPort,
customUser: source.customUser, customUser: source.customUser
}); });
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
if (from) { if (from) {
@@ -169,8 +169,8 @@
<div class="grid grid-flow-row gap-2 lg:px-10"> <div class="grid grid-flow-row gap-2 lg:px-10">
{#if !source.gitlabAppId} {#if !source.gitlabAppId}
<a <a
href="https://docs.coollabs.io/coolify/sources#how-to-integrate-with-gitlab" href="https://docs.coollabs.io-v3/coolify/sources#how-to-integrate-with-gitlab"
class="font-bold " class="font-bold"
target="_blank noreferrer" target="_blank noreferrer"
rel="noopener noreferrer">Documentation and detailed instructions.</a rel="noopener noreferrer">Documentation and detailed instructions.</a
> >

View File

@@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.12.28", "version": "3.12.39",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {
@@ -32,7 +32,7 @@
"build:api": "NODE_ENV=production pnpm run --filter api build", "build:api": "NODE_ENV=production pnpm run --filter api build",
"build:ui": "NODE_ENV=production pnpm run --filter ui build", "build:ui": "NODE_ENV=production pnpm run --filter ui build",
"dockerlogin": "echo $DOCKER_PASS | docker login --username=$DOCKER_USER --password-stdin", "dockerlogin": "echo $DOCKER_PASS | docker login --username=$DOCKER_USER --password-stdin",
"release:staging:amd": "docker build -t ghcr.io/coollabsio/coolify:next . && docker push ghcr.io/coollabsio/coolify:next", "release:staging:amd": "docker build -t ghcr.io/coollabsio/coolify:v3 . && docker push ghcr.io/coollabsio/coolify:v3",
"release:local": "rm -fr ./local-serve && mkdir ./local-serve && pnpm build && cp -Rp apps/api/build/* ./local-serve && cp -Rp apps/ui/build/ ./local-serve/public && cp -Rp apps/api/prisma/ ./local-serve/prisma && cp -Rp apps/api/package.json ./local-serve && env | grep '^COOLIFY_' > ./local-serve/.env && cd ./local-serve && pnpm install . && pnpm start" "release:local": "rm -fr ./local-serve && mkdir ./local-serve && pnpm build && cp -Rp apps/api/build/* ./local-serve && cp -Rp apps/ui/build/ ./local-serve/public && cp -Rp apps/api/prisma/ ./local-serve/prisma && cp -Rp apps/api/package.json ./local-serve && env | grep '^COOLIFY_' > ./local-serve/.env && cd ./local-serve && pnpm install . && pnpm start"
}, },
"devDependencies": { "devDependencies": {
@@ -50,4 +50,4 @@
"open-source", "open-source",
"coolify" "coolify"
] ]
} }

3563
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff