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
title: "[Bug]: "
labels: [Bug]
assignees:
- andrasbacsai
- vasani-arpit
body:
- type: markdown
attributes:

View File

@@ -2,9 +2,6 @@ name: 🛠️ Feature request
description: Suggest an idea to improve coolify
title: '[Feature]: '
labels: [Enhancement]
assignees:
- andrasbacsai
- vasani-arpit
body:
- type: markdown
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:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "v3"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
@@ -44,6 +46,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "v3"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
@@ -95,7 +99,6 @@ jobs:
- name: Create & publish manifest
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 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
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
concurrency:
group: staging_environment
cancel-in-progress: true
on:
push:
branches-ignore:
- "main"
branches:
- "v3"
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify"
jobs:
amd64:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
ref: "v3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io
@@ -40,14 +40,13 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
aarch64:
runs-on: [self-hosted, arm64]
runs-on:
group: aarch-runners
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
ref: "v3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io

11
.vscode/settings.json vendored
View File

@@ -18,5 +18,14 @@
"ts",
"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
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 npm --no-update-notifier --no-fund --global install pnpm@${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
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:
@@ -79,9 +79,9 @@ Deploy your resource to:
### Services
- [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)
- [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)
- [VSCode Server](https://github.com/cdr/code-server)
- [MinIO](https://min.io)
@@ -100,7 +100,7 @@ Deploy your resource to:
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@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)
- 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",
"@ladjs/graceful": "3.2.1",
"@prisma/client": "4.8.1",
"@sentry/node": "7.30.0",
"@sentry/tracing": "7.30.0",
"axe": "11.2.1",
"bcryptjs": "2.4.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?
dockerRegistryImageName String?
simpleDockerfile String?
basicAuthUser String?
basicAuthPw String?
persistentStorage ApplicationPersistentStorage[]
secrets Secret[]
@@ -187,6 +189,7 @@ model ApplicationSettings {
isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false)
basicAuth Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])

View File

@@ -12,7 +12,7 @@ async function main() {
await prisma.setting.create({
data: {
id: '0',
arch: process.arch,
arch: process.arch
}
});
} else {
@@ -81,16 +81,295 @@ async function main() {
});
}
// 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) {
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) {
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()
.catch((e) => {
console.error(e);
@@ -99,15 +378,11 @@ main()
.finally(async () => {
await prisma.$disconnect();
});
const encrypt = (text) => {
if (text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
return JSON.stringify({
iv: iv.toString('hex'),
content: encrypted.toString('hex')
});
}
};
reEncryptSecrets()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

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 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 path, { join } from 'path';
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 {
cleanupDockerStorage,
createRemoteEngineConfiguration,
@@ -18,26 +23,20 @@ import {
isDev,
listSettings,
prisma,
sentryDSN,
startTraefikProxy,
startTraefikTCPProxy,
version
} 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 { 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 * as Sentry from '@sentry/node';
declare module 'fastify' {
interface FastifyInstance {
config: {
COOLIFY_APP_ID: string;
COOLIFY_SECRET_KEY: string;
COOLIFY_SECRET_KEY_BETTER: string | null;
COOLIFY_DATABASE_URL: string;
COOLIFY_IS_ON: string;
COOLIFY_WHITE_LABELED: string;
@@ -67,6 +66,10 @@ const host = '0.0.0.0';
COOLIFY_SECRET_KEY: {
type: 'string'
},
COOLIFY_SECRET_KEY_BETTER: {
type: 'string',
default: null
},
COOLIFY_DATABASE_URL: {
type: 'string',
default: 'file:../db/dev.db'
@@ -164,7 +167,7 @@ const host = '0.0.0.0';
// autoUpdater
setInterval(async () => {
await autoUpdater();
}, 60000 * 15);
}, 60000 * 60);
// cleanupStorage
setInterval(async () => {
@@ -185,17 +188,17 @@ const host = '0.0.0.0';
// Refresh and check templates
setInterval(async () => {
await refreshTemplates();
}, 60000);
}, 60000 * 10);
setInterval(async () => {
await refreshTags();
}, 60000);
}, 60000 * 10);
setInterval(
async () => {
await migrateServicesToNewTemplate();
},
isDev ? 10000 : 60000
isDev ? 10000 : 60000 * 10
);
setInterval(async () => {
@@ -206,7 +209,9 @@ const host = '0.0.0.0';
getTagsTemplates(),
getArch(),
getIPAddress(),
configureRemoteDockers()
configureRemoteDockers(),
refreshTemplates(),
refreshTags()
// cleanupStuckedContainers()
]);
} catch (error) {
@@ -230,7 +235,7 @@ async function getIPAddress() {
console.log(`Getting public IPv6 address...`);
await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } });
}
} catch (error) {}
} catch (error) { }
}
async function getTagsTemplates() {
const { default: got } = await import('got');
@@ -242,7 +247,7 @@ async function getTagsTemplates() {
if (await fs.stat('./testTemplate.yaml')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
}
} catch (error) {}
} catch (error) { }
try {
if (await fs.stat('./testTags.json')) {
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)));
}
}
} catch (error) {}
} catch (error) { }
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates)));
await fs.writeFile('./tags.json', tags);
@@ -276,9 +281,6 @@ async function initServer() {
if (settings.doNotTrack === true) {
console.log('[000] Telemetry disabled...');
} else {
if (settings.sentryDSN !== sentryDSN) {
await prisma.setting.update({ where: { id: '0' }, data: { sentryDSN } });
}
// Initialize Sentry
// Sentry.init({
// dsn: sentryDSN,
@@ -293,7 +295,7 @@ async function initServer() {
try {
console.log(`[001] Initializing server...`);
await executeCommand({ command: `docker network create --attachable coolify` });
} catch (error) {}
} catch (error) { }
try {
console.log(`[002] Cleanup stucked builds...`);
const isOlder = compareVersions('3.8.1', version);
@@ -303,10 +305,10 @@ async function initServer() {
data: { status: 'failed' }
});
}
} catch (error) {}
} catch (error) { }
try {
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) {
console.log(error);
}
@@ -319,7 +321,7 @@ async function getArch() {
console.log(`Getting architecture...`);
await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } });
}
} catch (error) {}
} catch (error) { }
}
async function cleanupStuckedContainers() {
@@ -402,14 +404,21 @@ async function autoUpdater() {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await executeCommand({ command: `docker pull ghcr.io/coollabsio/coolify:${latestVersion}` });
await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` });
let image = `ghcr.io/coollabsio/coolify:${latestVersion}`;
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({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
});
await executeCommand({
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 {
@@ -475,7 +484,7 @@ async function checkProxies() {
}
try {
await createRemoteEngineConfiguration(docker.id);
} catch (error) {}
} catch (error) { }
}
}
// TCP Proxies
@@ -514,7 +523,7 @@ async function checkProxies() {
// await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
// }
// }
} catch (error) {}
} catch (error) { }
}
async function copySSLCertificates() {
@@ -546,7 +555,11 @@ async function copySSLCertificates() {
} catch (error) {
console.log(error);
} 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;
enginesDone.add(destination.remoteIpAddress);
}
let lowDiskSpace = false;
try {
let stdout = null;
if (!isDev) {
const output = await executeCommand({
dockerId: destination.id,
command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`,
shell: true
});
stdout = output.stdout;
} else {
const output = await executeCommand({
command: `df -kPT /`
});
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
await cleanupDockerStorage(destination.id);
// let lowDiskSpace = false;
// try {
// let stdout = null;
// if (!isDev) {
// const output = await executeCommand({
// dockerId: destination.id,
// command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`,
// shell: true
// });
// stdout = output.stdout;
// } else {
// const output = await executeCommand({
// command: `df -kPT /`
// });
// stdout = output.stdout;
// }
// let lines = stdout.trim().split('\n');
// let header = lines[0];
// let regex =
// /^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
// const boundaries = [];
// let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
// while ((match = regex.exec(header))) {
// boundaries.push(match[0].length);
// }
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) {}
if (lowDiskSpace) {
await cleanupDockerStorage(destination.id);
}
// boundaries[boundaries.length - 1] = -1;
// const data = lines.slice(1).map((line) => {
// const cl = boundaries.map((boundary) => {
// const column = boundary > 0 ? line.slice(0, boundary) : line;
// line = line.slice(boundary);
// return column.trim();
// });
// return {
// capacity: Number.parseInt(cl[5], 10) / 100
// };
// });
// if (data.length > 0) {
// const { capacity } = data[0];
// if (capacity > 0.8) {
// lowDiskSpace = true;
// }
// }
// } catch (error) {}
// if (lowDiskSpace) {
// await cleanupDockerStorage(destination.id);
// }
}
}

View File

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

View File

@@ -86,19 +86,20 @@ export async function migrateServicesToNewTemplate() {
if (template.variables) {
if (template.variables.length > 0) {
for (const variable of template.variables) {
const { defaultValue } = variable;
let { defaultValue } = variable;
defaultValue = defaultValue.toString();
const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
if (variable.defaultValue.startsWith('$$generate_password')) {
if (defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
} else if (defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) {
} else if (defaultValue.startsWith('$$generate_username')) {
variable.value = cuid();
} else if (variable.defaultValue.startsWith('$$generate_token')) {
} else if (defaultValue.startsWith('$$generate_token')) {
variable.value = generateToken()
} 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 ${buildCommand}`);
Dockerfile.push('RUN rm -fr .git');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
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 resources /app/resources`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`RUN yarn install && yarn production`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
@@ -842,6 +844,7 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
Dockerfile.push('RUN cargo install cargo-chef');
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 rm -fr .git');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
}

View File

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

View File

@@ -36,6 +36,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN deno cache ${denoMainFile}`);
Dockerfile.push(`ENV NO_COLOR true`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`);
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')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
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`
);
Dockerfile.push(`COPY --chown=application:application . ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
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(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`);
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(`RUN ${installCommand}`);
Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`);
} 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 --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE 80`);
}

View File

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

View File

@@ -52,7 +52,7 @@ const createDockerfile = async (data, image): Promise<void> => {
} else {
Dockerfile.push(`CMD python ${pythonModule}`);
}
Dockerfile.push('RUN rm -fr .git');
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')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
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(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ["/app/${name}"]`);
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')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
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')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
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')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
}
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`);
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 fsNormal from 'fs';
import yaml from 'js-yaml';
import forge from 'node-forge';
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 crypto from 'crypto';
import { promises as dns } from 'dns';
import * as Sentry from '@sentry/node';
import { PrismaClient } from '@prisma/client';
import os from 'os';
import * as SSHConfig from 'ssh-config/src/ssh-config';
@@ -18,13 +16,13 @@ import { day } from './dayjs';
import { saveBuildLog } from './buildPacks/common';
import { scheduler } from './scheduler';
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 proxyPort = process.env.COOLIFY_PROXY_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 customConfig: Config = {
dictionaries: [adjectives, colors, animals],
@@ -172,13 +170,19 @@ export const base64Encode = (text: string): string => {
export const base64Decode = (text: string): string => {
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) => {
if (hashString) {
try {
const hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv(
algorithm,
process.env['COOLIFY_SECRET_KEY'],
getSecretKey(),
Buffer.from(hash.iv, 'hex')
);
const decrpyted = Buffer.concat([
@@ -195,7 +199,7 @@ export const decrypt = (hashString: string) => {
export const encrypt = (text: string) => {
if (text) {
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()]);
return JSON.stringify({
iv: iv.toString('hex'),
@@ -402,8 +406,8 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
baseImageARM: 'mongo',
versions: ['5.0', '4.4', '4.2'],
versionsARM: ['5.0', '4.4', '4.2']
versions: ['6.0', '5.0', '4.4', '4.2'],
versionsARM: ['6.0', '5.0', '4.4', '4.2']
},
{
name: 'mysql',
@@ -418,16 +422,16 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'MariaDB',
baseImage: 'bitnami/mariadb',
baseImageARM: 'mariadb',
versions: ['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']
versions: ['10.11', '10.10', '10.9', '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',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
baseImageARM: 'postgres',
versions: ['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']
versions: ['15.2.0', '14.7.0', '14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'],
versionsARM: ['15.2', '14.7', '14.5', '13.8', '12.12', '11.17', '10.22']
},
{
name: 'redis',
@@ -442,14 +446,14 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'CouchDB',
baseImage: 'bitnami/couchdb',
baseImageARM: 'couchdb',
versions: ['3.2.2', '3.1.2', '2.3.1'],
versionsARM: ['3.2.2', '3.1.2', '2.3.1']
versions: ['3.3.1', '3.2.2', '3.1.2', '2.3.1'],
versionsARM: ['3.3', '3.2.2', '3.1.2', '2.3.1']
},
{
name: 'edgedb',
fancyName: '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,
buildId,
applicationId,
debug
debug,
timeout = 0
}: {
command: string;
sshCommand?: boolean;
@@ -589,6 +594,7 @@ export async function executeCommand({
buildId?: string;
applicationId?: string;
debug?: boolean;
timeout?: number;
}): Promise<ExecaChildProcess<string>> {
const { execa, execaCommand } = await import('execa');
const { parse } = await import('shell-quote');
@@ -613,20 +619,26 @@ export async function executeCommand({
}
if (sshCommand) {
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) {
return await new Promise(async (resolve, reject) => {
let subprocess = null;
if (shell) {
subprocess = execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
});
} else {
subprocess = execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
});
}
const logs = [];
@@ -680,19 +692,26 @@ export async function executeCommand({
} else {
if (shell) {
return await execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
});
} else {
return await execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
});
}
}
} else {
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
},
process.env['COOLIFY_SECRET_KEY']
getSecretKey()
);
}
export function generatePassword({
@@ -1013,7 +1032,7 @@ export function generateDatabaseConfiguration(database: any): DatabaseConfigurat
ulimits: {}
};
if (isARM()) {
configuration.volume = `${id}-${type}-data:/var/lib/postgresql`;
configuration.volume = `${id}-${type}-data:/var/lib/postgresql/data`;
configuration.environmentVariables = {
POSTGRES_PASSWORD: dbUserPassword,
POSTGRES_USER: dbUser,
@@ -1663,9 +1682,6 @@ export function errorHandler({
if (message.includes('Unique constraint failed')) {
message = 'This data is unique and already exists. Please try again with a different value.';
}
if (type === 'normal') {
Sentry.captureException(message);
}
throw { status, message };
}
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
try {
await executeCommand({ dockerId, command: `docker image prune -af` });
@@ -1764,6 +1780,11 @@ export async function cleanupDockerStorage(dockerId) {
try {
await executeCommand({ dockerId, command: `docker builder prune -af` });
} catch (error) { }
if (volumes) {
try {
await executeCommand({ dockerId, command: `docker volume prune -af` });
} catch (error) { }
}
}
export function persistentVolumes(id, persistentStorage, config) {
@@ -1926,3 +1947,49 @@ export function generateSecrets(
}
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
},
{
name: 'emailSmtpUseSsl',
name: 'emailSmtpUseTls',
isEditable: true,
isLowerCase: false,
isNumber: false,

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import fs from 'fs/promises';
import {
ComposeFile,
backupDatabaseNow,
createDirectories,
decrypt,
defaultComposeConfiguration,
@@ -302,7 +303,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
databaseSecret
} = database;
const { privatePort, command, environmentVariables, image, volume, ulimits } =
generateDatabaseConfiguration(database, arch);
generateDatabaseConfiguration(database);
const network = destinationDockerId && destinationDocker.network;
const volumeName = volume.split(':')[0];
@@ -351,6 +352,21 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
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>) {
try {
const teamId = request.user.teamId;

View File

@@ -1,5 +1,5 @@
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';
@@ -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/stop', async (request) => await stopDatabase(request));
fastify.post<OnlyId>('/:id/backup', async (request, reply) => await backupDatabase(request, reply));
};
export default root;

View File

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

View File

@@ -1,5 +1,5 @@
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 { 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.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply));
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.post<SaveDestinationSettings>('/:id/settings', async (request) => await saveDestinationSettings(request));

View File

@@ -12,7 +12,6 @@ import {
prisma,
uniqueName,
version,
sentryDSN,
executeCommand
} from '../../../lib/common';
import { scheduler } from '../../../lib/scheduler';
@@ -20,8 +19,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import type { Login, Update } from '.';
import type { GetCurrentUser } from './types';
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15;
export async function hashPassword(password: string, saltRounds = 15): Promise<string> {
return bcrypt.hash(password, saltRounds);
}
@@ -59,7 +57,7 @@ export async function cleanupManually(request: FastifyRequest) {
const destination = await prisma.destinationDocker.findUnique({
where: { id: serverId }
});
await cleanupDockerStorage(destination.id);
await cleanupDockerStorage(destination.id, true);
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
@@ -78,7 +76,7 @@ export async function refreshTags() {
tags = JSON.parse(tags).concat(JSON.parse(testTags));
}
}
} catch (error) {}
} catch (error) { }
await fs.writeFile('./tags.json', tags);
} else {
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')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
}
} catch (error) {}
} catch (error) { }
const response = await fs.readFile('./devTemplates.yaml', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response)));
} else {
@@ -164,13 +162,13 @@ export async function update(request: FastifyRequest<Update>) {
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({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
});
await executeCommand({
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 {};
} else {
@@ -452,7 +450,6 @@ export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fa
});
return {
settings: await prisma.setting.findUnique({ where: { id: '0' } }),
sentryDSN,
pendingInvitations,
token,
...request.user

View File

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

View File

@@ -1,235 +1,312 @@
import { promises as dns } from 'dns';
import { X509Certificate } from 'node:crypto';
import * as Sentry from '@sentry/node';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, executeCommand, getDomain, isDev, isDNSValid, isDomainConfigured, listSettings, prisma, sentryDSN, version } from '../../../../lib/common';
import { AddDefaultRegistry, CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey, SetDefaultRegistry } from './types';
import {
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) {
try {
const teamId = request.user.teamId;
const settings = await listSettings();
const sshKeys = await prisma.sshKey.findMany({ where: { team: { id: teamId } } })
let registries = await prisma.dockerRegistry.findMany({ where: { team: { id: teamId } } })
registries = registries.map((registry) => {
if (registry.password) {
registry.password = decrypt(registry.password)
}
return registry
})
const unencryptedKeys = []
if (sshKeys.length > 0) {
for (const key of sshKeys) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.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 })
}
try {
const teamId = request.user.teamId;
const settings = await listSettings();
const sshKeys = await prisma.sshKey.findMany({ where: { team: { id: teamId } } });
let registries = await prisma.dockerRegistry.findMany({ where: { team: { id: teamId } } });
registries = registries.map((registry) => {
if (registry.password) {
registry.password = decrypt(registry.password);
}
return registry;
});
const unencryptedKeys = [];
if (sshKeys.length > 0) {
for (const key of sshKeys) {
unencryptedKeys.push({
id: key.id,
name: key.name,
privateKey: decrypt(key.privateKey),
createdAt: key.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 {
settings,
certificates: cns,
sshKeys: unencryptedKeys,
registries
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
return {
settings,
certificates: cns,
sshKeys: unencryptedKeys,
registries
};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function saveSettings(request: FastifyRequest<SaveSettings>, reply: FastifyReply) {
try {
let {
previewSeparator,
numberOfDockerImagesKeptLocally,
doNotTrack,
fqdn,
isAPIDebuggingEnabled,
isRegistrationEnabled,
dualCerts,
minPort,
maxPort,
isAutoUpdateEnabled,
isDNSCheckEnabled,
DNSServers,
proxyDefaultRedirect
} = request.body
const { id, previewSeparator: SetPreviewSeparator } = await listSettings();
if (numberOfDockerImagesKeptLocally) {
numberOfDockerImagesKeptLocally = Number(numberOfDockerImagesKeptLocally)
}
if (previewSeparator == '') {
previewSeparator = '.'
}
if (SetPreviewSeparator != previewSeparator) {
const applications = await prisma.application.findMany({ where: { previewApplication: { some: { id: { not: undefined } } } }, include: { previewApplication: true } })
for (const application of applications) {
for (const preview of application.previewApplication) {
const { protocol } = new URL(preview.customDomain)
const { pullmergeRequestId } = preview
const { fqdn } = application
const newPreviewDomain = `${protocol}//${pullmergeRequestId}${previewSeparator}${getDomain(fqdn)}`
await prisma.previewApplication.update({ where: { id: preview.id }, data: { customDomain: newPreviewDomain } })
}
}
}
try {
let {
previewSeparator,
numberOfDockerImagesKeptLocally,
doNotTrack,
fqdn,
isAPIDebuggingEnabled,
isRegistrationEnabled,
dualCerts,
minPort,
maxPort,
isAutoUpdateEnabled,
isDNSCheckEnabled,
DNSServers,
proxyDefaultRedirect
} = request.body;
const { id, previewSeparator: SetPreviewSeparator } = await listSettings();
if (numberOfDockerImagesKeptLocally) {
numberOfDockerImagesKeptLocally = Number(numberOfDockerImagesKeptLocally);
}
if (previewSeparator == '') {
previewSeparator = '.';
}
if (SetPreviewSeparator != previewSeparator) {
const applications = await prisma.application.findMany({
where: { previewApplication: { some: { id: { not: undefined } } } },
include: { previewApplication: true }
});
for (const application of applications) {
for (const preview of application.previewApplication) {
const { protocol } = new URL(preview.customDomain);
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({
where: { id },
data: { previewSeparator, numberOfDockerImagesKeptLocally, doNotTrack, isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled }
});
if (fqdn) {
await prisma.setting.update({ where: { id }, data: { fqdn } });
}
await prisma.setting.update({ where: { id }, data: { proxyDefaultRedirect } });
if (minPort && maxPort) {
await prisma.setting.update({ where: { id }, data: { minPort, maxPort } });
}
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 })
}
await prisma.setting.update({
where: { id },
data: {
previewSeparator,
numberOfDockerImagesKeptLocally,
doNotTrack,
isRegistrationEnabled,
dualCerts,
isAutoUpdateEnabled,
isDNSCheckEnabled,
DNSServers,
isAPIDebuggingEnabled
}
});
if (fqdn) {
await prisma.setting.update({ where: { id }, data: { fqdn } });
}
await prisma.setting.update({ where: { id }, data: { proxyDefaultRedirect } });
if (minPort && maxPort) {
await prisma.setting.update({ where: { id }, data: { minPort, maxPort } });
}
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) {
try {
const { fqdn } = request.body
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([...DNSServers.split(',')]);
}
let ip;
try {
ip = await dns.resolve(fqdn);
} catch (error) {
// Do not care.
}
await prisma.setting.update({ where: { fqdn }, data: { fqdn: null } });
return reply.redirect(302, ip ? `http://${ip[0]}:3000/settings` : undefined)
} catch ({ status, message }) {
return errorHandler({ status, message })
}
try {
const { fqdn } = request.body;
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([...DNSServers.split(',')]);
}
let ip;
try {
ip = await dns.resolve(fqdn);
} catch (error) {
// Do not care.
}
await prisma.setting.update({ where: { fqdn }, data: { fqdn: null } });
return reply.redirect(302, ip ? `http://${ip[0]}:3000/settings` : undefined);
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function checkDomain(request: FastifyRequest<CheckDomain>) {
try {
const { id } = request.params;
let { fqdn, forceSave, dualCerts, isDNSCheckEnabled } = request.body
if (fqdn) fqdn = fqdn.toLowerCase();
const found = await isDomainConfigured({ id, fqdn });
if (found) {
throw { message: "Domain already configured" };
}
if (isDNSCheckEnabled && !forceSave && !isDev) {
const hostname = request.hostname.split(':')[0]
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
}
return {};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
try {
const { id } = request.params;
let { fqdn, forceSave, dualCerts, isDNSCheckEnabled } = request.body;
if (fqdn) fqdn = fqdn.toLowerCase();
const found = await isDomainConfigured({ id, fqdn });
if (found) {
throw { message: 'Domain already configured' };
}
if (isDNSCheckEnabled && !forceSave && !isDev) {
const hostname = request.hostname.split(':')[0];
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
}
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function checkDNS(request: FastifyRequest<CheckDNS>) {
try {
const { domain } = request.params;
await isDNSValid(request.hostname, domain);
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
try {
const { domain } = request.params;
await isDNSValid(request.hostname, domain);
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { privateKey, name } = request.body;
const found = await prisma.sshKey.findMany({ where: { name } })
if (found.length > 0) {
throw {
message: "Name already used. Choose another one please."
}
}
const encryptedSSHKey = encrypt(privateKey)
await prisma.sshKey.create({ data: { name, privateKey: encryptedSSHKey, team: { connect: { id: teamId } } } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
try {
const teamId = request.user.teamId;
const { privateKey, name } = request.body;
const found = await prisma.sshKey.findMany({ where: { name } });
if (found.length > 0) {
throw {
message: 'Name already used. Choose another one please.'
};
}
const encryptedSSHKey = encrypt(privateKey);
await prisma.sshKey.create({
data: { name, privateKey: encryptedSSHKey, team: { connect: { id: teamId } } }
});
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.body;
await prisma.sshKey.deleteMany({ where: { id, teamId } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
try {
const teamId = request.user.teamId;
const { id } = request.body;
await prisma.sshKey.deleteMany({ where: { id, teamId } });
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function deleteCertificates(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
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 })
await prisma.certificate.deleteMany({ where: { id, teamId } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
export async function deleteCertificates(
request: FastifyRequest<OnlyIdInBody>,
reply: FastifyReply
) {
try {
const teamId = request.user.teamId;
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
});
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) {
try {
const teamId = request.user.teamId;
const { id, username, password } = request.body;
export async function setDockerRegistry(
request: FastifyRequest<SetDefaultRegistry>,
reply: FastifyReply
) {
try {
const teamId = request.user.teamId;
const { id, username, password } = request.body;
let encryptedPassword = ''
if (password) encryptedPassword = encrypt(password)
let encryptedPassword = '';
if (password) encryptedPassword = encrypt(password);
if (teamId === '0') {
await prisma.dockerRegistry.update({ where: { id }, data: { username, password: encryptedPassword } })
} else {
await prisma.dockerRegistry.updateMany({ where: { id, teamId }, data: { username, password: encryptedPassword } })
}
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
if (teamId === '0') {
await prisma.dockerRegistry.update({
where: { id },
data: { username, password: encryptedPassword }
});
} else {
await prisma.dockerRegistry.updateMany({
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) {
try {
const teamId = request.user.teamId;
const { name, url, username, password } = request.body;
export async function addDockerRegistry(
request: FastifyRequest<AddDefaultRegistry>,
reply: FastifyReply
) {
try {
const teamId = request.user.teamId;
const { name, url, username, password } = request.body;
let encryptedPassword = ''
if (password) encryptedPassword = encrypt(password)
await prisma.dockerRegistry.create({ data: { name, url, username, password: encryptedPassword, team: { connect: { id: teamId } } } })
let encryptedPassword = '';
if (password) encryptedPassword = encrypt(password);
await prisma.dockerRegistry.create({
data: { name, url, username, password: encryptedPassword, team: { connect: { id: teamId } } }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
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 });
}
}
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 type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { decrypt, encrypt, errorHandler, prisma } from '../../../../lib/common';
import { OnlyId } from '../../../../types';
import { CheckGitLabOAuthId, SaveGitHubSource, SaveGitLabSource } from './types';
export async function listSources(request: FastifyRequest) {
try {
const teamId = request.user?.teamId;
const sources = await prisma.gitSource.findMany({
where: { OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] },
include: { teams: true, githubApp: true, gitlabApp: true }
});
return {
sources
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
try {
const teamId = request.user?.teamId;
const sources = await prisma.gitSource.findMany({
where: {
OR: [
{ teams: { some: { id: teamId === '0' ? undefined : teamId } } },
{ isSystemWide: true }
]
},
include: { teams: true, githubApp: true, gitlabApp: true }
});
return {
sources
};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function saveSource(request, reply) {
try {
const { id } = request.params
let { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } = request.body
if (customPort) customPort = Number(customPort)
await prisma.gitSource.update({
where: { id },
data: { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
try {
const { id } = request.params;
let { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } = request.body;
if (customPort) customPort = Number(customPort);
await prisma.gitSource.update({
where: { id },
data: { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide }
});
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function getSource(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const { teamId } = request.user
const settings = await prisma.setting.findFirst({});
try {
const { id } = request.params;
const { teamId } = request.user;
const settings = await prisma.setting.findFirst({});
if (id === 'new') {
return {
source: {
name: null,
type: null,
htmlUrl: null,
apiUrl: null,
organization: null,
customPort: 22,
customUser: 'git',
},
settings
}
}
if (id === 'new') {
return {
source: {
name: null,
type: null,
htmlUrl: null,
apiUrl: null,
organization: null,
customPort: 22,
customUser: 'git'
},
settings
};
}
const source = await prisma.gitSource.findFirst({
where: { id, OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] },
include: { githubApp: true, gitlabApp: true }
});
if (!source) {
throw { status: 404, message: 'Source not found.' }
}
const source = await prisma.gitSource.findFirst({
where: {
id,
OR: [
{ teams: { some: { id: teamId === '0' ? undefined : teamId } } },
{ isSystemWide: true }
]
},
include: { githubApp: true, gitlabApp: true }
});
if (!source) {
throw { status: 404, message: 'Source not found.' };
}
if (source?.githubApp?.clientSecret)
source.githubApp.clientSecret = decrypt(source.githubApp.clientSecret);
if (source?.githubApp?.webhookSecret)
source.githubApp.webhookSecret = decrypt(source.githubApp.webhookSecret);
if (source?.githubApp?.privateKey) source.githubApp.privateKey = decrypt(source.githubApp.privateKey);
if (source?.gitlabApp?.appSecret) source.gitlabApp.appSecret = decrypt(source.gitlabApp.appSecret);
if (source?.githubApp?.clientSecret)
source.githubApp.clientSecret = decrypt(source.githubApp.clientSecret);
if (source?.githubApp?.webhookSecret)
source.githubApp.webhookSecret = decrypt(source.githubApp.webhookSecret);
if (source?.githubApp?.privateKey)
source.githubApp.privateKey = decrypt(source.githubApp.privateKey);
if (source?.gitlabApp?.appSecret)
source.gitlabApp.appSecret = decrypt(source.gitlabApp.appSecret);
return {
source,
settings
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
return {
source,
settings
};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function deleteSource(request) {
try {
const { id } = request.params
const source = await prisma.gitSource.delete({
where: { id },
include: { githubApp: true, gitlabApp: true }
});
if (source.githubAppId) {
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 })
}
try {
const { id } = request.params;
const gitAppFound = await prisma.application.findFirst({ where: { gitSourceId: id } });
if (gitAppFound) {
throw {
status: 400,
message: 'This source is used by an application. Please remove the application first.'
};
}
const source = await prisma.gitSource.delete({
where: { id },
include: { githubApp: true, gitlabApp: true }
});
if (source.githubAppId) {
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>) {
try {
const { teamId } = request.user
try {
const { teamId } = request.user;
const { id } = request.params
let { name, htmlUrl, apiUrl, organization, customPort, isSystemWide } = request.body
const { id } = request.params;
let { name, htmlUrl, apiUrl, organization, customPort, isSystemWide } = request.body;
if (customPort) customPort = Number(customPort)
if (id === 'new') {
const newId = cuid()
await prisma.gitSource.create({
data: {
id: newId,
name,
htmlUrl,
apiUrl,
organization,
customPort,
isSystemWide,
type: 'github',
teams: { connect: { id: teamId } }
}
});
return {
id: newId
}
}
throw { status: 500, message: 'Wrong request.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
if (customPort) customPort = Number(customPort);
if (id === 'new') {
const newId = cuid();
await prisma.gitSource.create({
data: {
id: newId,
name,
htmlUrl,
apiUrl,
organization,
customPort,
isSystemWide,
type: 'github',
teams: { connect: { id: teamId } }
}
});
return {
id: newId
};
}
throw { status: 500, message: 'Wrong request.' };
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>) {
try {
const { id } = request.params
const { teamId } = request.user
let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName, customPort, customUser } =
request.body
try {
const { id } = request.params;
const { teamId } = request.user;
let {
type,
name,
htmlUrl,
apiUrl,
oauthId,
appId,
appSecret,
groupName,
customPort,
customUser
} = request.body;
if (oauthId) oauthId = Number(oauthId);
if (customPort) customPort = Number(customPort)
const encryptedAppSecret = encrypt(appSecret);
if (oauthId) oauthId = Number(oauthId);
if (customPort) customPort = Number(customPort);
const encryptedAppSecret = encrypt(appSecret);
if (id === 'new') {
const newId = cuid()
await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, customPort, customUser, teams: { connect: { id: teamId } } } });
await prisma.gitlabApp.create({
data: {
teams: { connect: { id: teamId } },
appId,
oauthId,
groupName,
appSecret: encryptedAppSecret,
gitSource: { connect: { id: newId } }
}
});
return {
status: 201,
id: newId
}
} else {
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name, customPort, customUser } });
await prisma.gitlabApp.update({
where: { id },
data: {
appId,
oauthId,
groupName,
appSecret: encryptedAppSecret,
}
});
}
return { status: 201 };
} catch ({ status, message }) {
return errorHandler({ status, message })
}
if (id === 'new') {
const newId = cuid();
await prisma.gitSource.create({
data: {
id: newId,
type,
apiUrl,
htmlUrl,
name,
customPort,
customUser,
teams: { connect: { id: teamId } }
}
});
await prisma.gitlabApp.create({
data: {
teams: { connect: { id: teamId } },
appId,
oauthId,
groupName,
appSecret: encryptedAppSecret,
gitSource: { connect: { id: newId } }
}
});
return {
status: 201,
id: newId
};
} else {
await prisma.gitSource.update({
where: { id },
data: { type, apiUrl, htmlUrl, name, customPort, customUser }
});
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>) {
try {
const { oauthId } = request.body
const found = await prisma.gitlabApp.findFirst({ where: { oauthId: Number(oauthId) } });
if (found) {
throw { status: 500, message: 'OAuthID already configured in Coolify.' }
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
try {
const { oauthId } = request.body;
const found = await prisma.gitlabApp.findFirst({ where: { oauthId: Number(oauthId) } });
if (found) {
throw { status: 500, message: 'OAuthID already configured in Coolify.' };
}
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}

View File

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

View File

@@ -4,9 +4,9 @@ import { proxyConfiguration, otherProxyConfiguration } from './handlers';
import { OtherProxyConfiguration } from './types';
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<OtherProxyConfiguration>('/other.json', async (request, reply) =>
fastify.get<OtherProxyConfiguration>('/other.json', async (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",
"dependencies": {
"@sentry/svelte": "7.21.1",
"@sentry/tracing": "7.21.1",
"@sveltejs/adapter-static": "1.0.0-next.48",
"@tailwindcss/typography": "0.5.8",
"cuid": "2.1.8",

View File

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

View File

@@ -1,5 +1,6 @@
import { dev } from '$app/env';
import Cookies from 'js-cookie';
import { dashify } from './common';
export function getAPIUrl() {
if (GITPOD_WORKSPACE_URL) {
@@ -72,17 +73,19 @@ async function send({
...headers
};
}
if (token && !path.startsWith('https://')) {
if (token && !path.startsWith('https://') && !path.startsWith('http://')) {
opts.headers = {
...opts.headers,
Authorization: `Bearer ${token}`
};
}
if (!path.startsWith('https://')) {
if (!path.startsWith('https://') && !path.startsWith('http://')) {
path = `/api/v1${path}`;
}
if (dev && !path.startsWith('https://')) {
if (dev && !path.startsWith('https://') && !path.startsWith('http://')) {
path = `${getAPIUrl()}${path}`;
}
if (method === 'POST' && data && !opts.body) {
@@ -100,6 +103,14 @@ async function send({
responseData = await response.json();
} else if (contentType?.indexOf('text/plain') !== -1) {
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 {
return {};
}

View File

@@ -196,6 +196,9 @@
"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>",
"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.",
"install_command": "Install Command",
"build_command": "Build Command",

View File

@@ -65,7 +65,6 @@
<script lang="ts">
export let settings: any;
export let sentryDSN: any;
export let baseSettings: any;
export let pendingInvitations: any = 0;
@@ -98,10 +97,6 @@
import Toasts from '$lib/components/Toasts.svelte';
import Tooltip from '$lib/components/Tooltip.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 (teamId) $appSession.teamId = teamId;
@@ -293,7 +288,7 @@
</a>
<a
id="documentation"
href="https://docs.coollabs.io/coolify/"
href="https://docs.coollabs.io/coolify-v3/"
target="_blank"
rel="noreferrer external"
class="icons hover:text-info"
@@ -384,7 +379,7 @@
</div>
<div class="drawer-side">
<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>
<a
class="no-underline icons hover:text-white hover:bg-pink-500"
@@ -498,7 +493,7 @@
<li>
<a
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"
rel="noreferrer external"
>

View File

@@ -255,12 +255,12 @@
{/if}
<div class="flex flex-row items-center">
<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>
<PublicRepository />
<div class="flex flex-row items-center pt-10">
<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 class="mx-auto max-w-screen-2xl">
<form class="flex flex-col" on:submit|preventDefault={handleDockerImage}>

View File

@@ -29,27 +29,28 @@
export let application: any;
export let settings: any;
import yaml from 'js-yaml';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import Select from 'svelte-select';
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 {
addToast,
appSession,
checkIfDeploymentEnabledApplications,
setLocation,
status,
features,
isDeploymentEnabled,
features
setLocation,
status
} from '$lib/store';
import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation';
import Beta from '$lib/components/Beta.svelte';
import cuid from 'cuid';
import yaml from 'js-yaml';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { saveForm } from './utils';
const { id } = $page.params;
@@ -77,6 +78,7 @@
let isCustomSSL = application.settings?.isCustomSSL;
let autodeploy = application.settings?.autodeploy;
let isBot = application.settings?.isBot;
let basicAuth = application.settings?.basicAuth;
let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings.isHttp2;
@@ -186,6 +188,9 @@
if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL;
}
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isBot') {
if ($status.application.overallStatus !== 'stopped') return;
isBot = !isBot;
@@ -210,7 +215,8 @@
isCustomSSL,
isHttp2,
branch: application.branch,
projectId: application.projectId
projectId: application.projectId,
basicAuth
});
return addToast({
message: $t('application.settings_saved'),
@@ -232,6 +238,9 @@
if (name === 'isBot') {
isBot = !isBot;
}
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
@@ -272,6 +281,7 @@
}
}
}
console.log(application);
await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration);
setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
@@ -498,7 +508,7 @@
<div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin}
<button
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary"
type="submit"
class:loading={loading.save}
class:bg-orange-600={forceSave}
@@ -751,7 +761,7 @@
on:click={() => !isDisabled && changeSettings('dualCerts')}
/>
</div>
{#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
@@ -774,6 +784,46 @@
on:click={() => changeSettings('isHttp2')}
/>
</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}
</div>
{#if isSimpleDockerfile}
@@ -782,7 +832,7 @@
</div>
<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>
<div class="flex gap-2">
<textarea

View File

@@ -51,16 +51,22 @@
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs: any = await get(
`/applications/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}`
);
const since = lastLog?.split(' ')[0] || 0;
const newLogs: any = await get(`/applications/${id}/logs/${selectedService}?since=${since}`);
if (newLogs.noContainer) {
noContainer = true;
} else {
noContainer = false;
}
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];
}
} catch (error) {
@@ -135,7 +141,7 @@
{:else}
<div class="relative w-full">
<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
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"

View File

@@ -13,17 +13,19 @@
import Redis from './_Redis.svelte';
import CouchDb from './_CouchDb.svelte';
import EdgeDB from './_EdgeDB.svelte';
import { post } from '$lib/api';
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { addToast, appSession, status } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params;
let loading = {
main: false,
public: false
public: false,
backup: false
};
let publicUrl = '';
let appendOnly = database.settings.appendOnly;
@@ -109,6 +111,7 @@
if ($status.database.isPublic) {
database.publicPort = publicPort;
}
generateUrl();
} catch (error) {
return errorNotification(error);
} finally {
@@ -130,6 +133,22 @@
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>
<div class="mx-auto max-w-6xl p-4">
@@ -144,6 +163,19 @@
class:bg-databases={!loading.main}
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}
</div>
<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]"
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.
<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 class="flex justify-center px-6 pb-8">
<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>
<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
? payload.isCoolifyProxyUsed
? $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() {
if (!isDestinationDeletable) {
return 'Please delete all resources before deleting this.';
@@ -88,7 +107,7 @@
</script>
{#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-col items-center justify-center title">
{#if $page.url.pathname === `/destinations/${$page.params.id}`}
@@ -111,6 +130,16 @@
>
<Tooltip triggeredBy="#delete">{deletable()}</Tooltip>
</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>
{/if}
<slot />

View File

@@ -43,6 +43,7 @@
return true;
}
});
let customVersion: string;
$: isDisabled =
!$appSession.isAdmin ||
$status.service.overallStatus === 'degraded' ||
@@ -111,6 +112,7 @@
setLocation(service);
forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices(service);
customVersion = null;
return addToast({
message: 'Configuration saved.',
type: 'success'
@@ -196,26 +198,12 @@
async function selectTag(event: any) {
service.version = event.detail.value;
}
async function setCustomVersion() {
service.version = customVersion;
}
onMount(async () => {
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
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>
@@ -224,7 +212,7 @@
<form id="saveForm" on:submit|preventDefault={handleSubmit}>
<div class="mx-auto w-full">
<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}
<button
type="submit"
@@ -289,6 +277,7 @@
</div>
<div class="grid grid-cols-2 items-center">
<label for="version">Version / Tag</label>
<div class="flex gap-2">
{#if tags.tags?.length > 0}
<div class="custom-select-wrapper w-full">
<Select
@@ -303,9 +292,11 @@
isClearable={false}
/>
</div>
{:else}
{:else}
<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 class="grid grid-cols-2 items-center">

View File

@@ -56,23 +56,23 @@
async function rollback() {
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) {
try {
loading.rollback = true;
console.log('loading.rollback', loading.rollback);
if (dev) {
console.log('rolling back to', rollbackVersion);
console.log('Upgrading to ', rollbackVersion);
await asyncSleep(4000);
return window.location.reload();
} else {
addToast({
message: 'Rollback started...',
message: 'Upgrade started...',
type: 'success'
});
await post(`/update`, { type: 'update', latestVersion: rollbackVersion });
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'
});
@@ -381,12 +381,12 @@
/>
</div>
<div class="grid grid-cols-4 items-center">
<div class="grid grid-cols-4 items-center pb-12">
<div class="col-span-2">
Rollback Coolify to a specific version
Upgrade Coolify to a specific version
<Explainer
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>
<input
@@ -401,7 +401,7 @@
class:loading={loading.rollback}
class="btn btn-primary ml-2"
disabled={!rollbackVersion || loading.rollback}
on:click|preventDefault|stopPropagation={rollback}>Rollback</button
on:click|preventDefault|stopPropagation={rollback}>Upgrade</button
>
</div>
<div class="grid grid-cols-2 items-center">

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.12.28",
"version": "3.12.39",
"license": "Apache-2.0",
"repository": "github:coollabsio/coolify",
"scripts": {
@@ -32,7 +32,7 @@
"build:api": "NODE_ENV=production pnpm run --filter api build",
"build:ui": "NODE_ENV=production pnpm run --filter ui build",
"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"
},
"devDependencies": {
@@ -50,4 +50,4 @@
"open-source",
"coolify"
]
}
}

3563
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff