Compare commits

..

175 Commits

Author SHA1 Message Date
Andras Bacsai
f55b861849 fix: cleanup 2022-12-09 14:32:22 +01:00
Andras Bacsai
adf82c04ad Merge pull request #747 from coollabsio/next
v3.12.0
2022-12-09 14:29:33 +01:00
Andras Bacsai
1b80956fe8 fix: public db icon on dashboard 2022-12-09 14:08:21 +01:00
Andras Bacsai
de9da8caf9 fix 2022-12-09 13:59:43 +01:00
Andras Bacsai
967f42dd89 add shell to some cmds 2022-12-09 13:46:06 +01:00
Andras Bacsai
95e8b29fa2 fix: wrong port in case of docker compose 2022-12-09 11:21:25 +01:00
Andras Bacsai
2e3c815e53 fix: delete resource on dashboard 2022-12-07 15:27:26 +01:00
Andras Bacsai
132707caa7 fix: rde 2022-12-07 14:46:12 +01:00
Andras Bacsai
0dad616c38 fixes 2022-12-07 13:45:56 +01:00
Andras Bacsai
c0882dffde Merge pull request #766 from twisttaan/feat/name-label
feat(api): name label
2022-12-07 13:45:11 +01:00
Andras Bacsai
5e082c647c fixes 2022-12-07 12:17:06 +01:00
Tristan Camejo
285c3c2f5d feat(api): name label 2022-12-07 00:38:08 +00:00
Andras Bacsai
dcb29a80fe fix 2022-12-06 10:29:14 +01:00
Andras Bacsai
b45ad19732 fix: security hole 2022-12-06 10:27:51 +01:00
Andras Bacsai
f12d453b5f backups... backups everywhere 2022-12-02 14:34:06 +01:00
Andras Bacsai
8a00b711be add pocketbase 2022-12-02 10:00:27 +01:00
Andras Bacsai
56204efc7a update workflow 2022-12-02 09:44:29 +01:00
Andras Bacsai
da638c270f infra: pocketbase release 2022-12-02 09:41:22 +01:00
Andras Bacsai
ad4b974274 fix: turn off autodeploy for simpledockerfiles 2022-12-01 16:50:54 +01:00
Andras Bacsai
943a05edcc fixes 2022-12-01 16:29:38 +01:00
Andras Bacsai
1a28e65e50 feat: revert to remote image 2022-12-01 15:51:18 +01:00
Andras Bacsai
cd3af7fa39 fix: failed builds should not push images 2022-12-01 15:05:21 +01:00
Andras Bacsai
8ccb0c88db feat: able to push image to docker registry 2022-12-01 14:39:02 +01:00
Andras Bacsai
127880cf8d schema prettify 2022-12-01 13:29:45 +01:00
Andras Bacsai
2e56086661 feat: simpleDockerfile deployment 2022-12-01 12:58:45 +01:00
Andras Bacsai
a129be0dbd fixes 2022-12-01 10:23:43 +01:00
Andras Bacsai
12c0760cb3 fixes 2022-12-01 09:51:56 +01:00
Andras Bacsai
9d3ed85ffd haha 2022-11-30 15:50:45 +01:00
Andras Bacsai
850d57d0d2 fix text haha 2022-11-30 15:49:39 +01:00
Andras Bacsai
7981bec1ed text changes 2022-11-30 15:47:54 +01:00
Andras Bacsai
76373a8597 feat: save application data before deploying 2022-11-30 15:40:27 +01:00
Andras Bacsai
9913e7b70b feat: specific git commit deployment
feat: revert to specific image
fix: no system wide docker registries
2022-11-30 15:22:07 +01:00
Andras Bacsai
a08bb25bfa fix: static for arm 2022-11-30 11:45:39 +01:00
Andras Bacsai
28ec164bc2 fix: update PR/MRs with new previewSeparator 2022-11-30 11:36:05 +01:00
Andras Bacsai
3d5ea8629c fix: apache on arm 2022-11-30 11:18:19 +01:00
Andras Bacsai
4aaf59d034 update templates and tags 2022-11-30 11:07:44 +01:00
Andras Bacsai
14850476c7 feat: able to host static/php sites on arm 2022-11-30 11:00:03 +01:00
Andras Bacsai
bf5b6170fa remove console log 2022-11-29 15:47:25 +01:00
Andras Bacsai
6f91591448 fix: webhook previewseparator 2022-11-29 15:45:18 +01:00
Andras Bacsai
3c723bcba2 fix: remove sentry before migration 2022-11-29 15:13:05 +01:00
Andras Bacsai
e7dd13cffa fix: git checkout 2022-11-29 15:10:34 +01:00
Andras Bacsai
ad91630faa fix: remove beta from systemwide git 2022-11-29 15:05:31 +01:00
Andras Bacsai
57f746b584 fix: login error 2022-11-29 14:55:40 +01:00
Andras Bacsai
a55720091c fix: prevent webhook errors to be logged 2022-11-29 14:50:24 +01:00
Andras Bacsai
b461635834 debug 2022-11-29 14:44:53 +01:00
Andras Bacsai
1375580651 fix: migrations 2022-11-29 14:01:19 +01:00
Andras Bacsai
3d20433ad1 feat: sentry frontend 2022-11-29 13:59:03 +01:00
Andras Bacsai
58447c6456 update migration 2022-11-29 13:39:00 +01:00
Andras Bacsai
c6273e9177 feat: custom previewseparator 2022-11-29 13:29:11 +01:00
Andras Bacsai
ffdc158d44 fix: only visible with publicrepo 2022-11-29 13:13:04 +01:00
Andras Bacsai
876c81fad8 fix: ui 2022-11-29 13:00:44 +01:00
Andras Bacsai
028ee6d7b1 feat: deploy specific commit for apps
feat: keep number of images locally to revert quickly
2022-11-29 11:47:20 +01:00
Andras Bacsai
ec00548f1b feat: system wide git out of beta 2022-11-29 10:53:05 +01:00
Andras Bacsai
c4dc03e4a8 Merge pull request #700 from ThallesP/main
feature: initial support for specific git commit
2022-11-29 10:52:21 +01:00
Andras Bacsai
3a510a77ec Merge branch 'next' into main 2022-11-29 10:50:00 +01:00
Andras Bacsai
98a785fced tags 2022-11-29 10:36:19 +01:00
Andras Bacsai
c48654160d fixes 2022-11-29 10:35:56 +01:00
Andras Bacsai
55b80132c4 fixes 2022-11-29 09:43:28 +01:00
Andras Bacsai
1f0c168936 fixes 2022-11-29 09:42:36 +01:00
Andras Bacsai
6715bc750f Merge pull request #721 from gabrielengel/g-i18n
Starting translations work
2022-11-29 09:24:52 +01:00
Andras Bacsai
04a48a626b Merge pull request #746 from gabrielengel/refactor-servers
Componentization of /servers and /sources (depends on badges merge)
2022-11-29 09:22:08 +01:00
Andras Bacsai
2f9f0da7c6 Merge pull request #745 from gabrielengel/new-badges
New Badges components: destination, public, status, teams
2022-11-29 09:21:30 +01:00
Andras Bacsai
513c4f9e29 fixes 2022-11-29 09:19:10 +01:00
Andras Bacsai
3f078517a0 fix: dnt 2022-11-28 14:29:14 +01:00
Andras Bacsai
37036f0fca fix: sentry dsn update 2022-11-28 13:57:18 +01:00
Andras Bacsai
5789aadb5c feat: do not track in settings 2022-11-28 13:55:49 +01:00
Andras Bacsai
a768ed718a update sentry 2022-11-28 12:56:43 +01:00
Andras Bacsai
9c6092f31f fix: seed 2022-11-28 12:53:44 +01:00
Andras Bacsai
40d294a247 feat: add default sentry 2022-11-28 12:02:10 +01:00
Andras Bacsai
72844e4edc feat: save doNotTrackData to db 2022-11-28 11:48:38 +01:00
Andras Bacsai
db0a71125a version++ 2022-11-28 11:28:54 +01:00
Andras Bacsai
da244af39d fixes 2022-11-28 11:27:03 +01:00
Andras Bacsai
067f502d3c feat: custom docker compose file location in repo 2022-11-28 10:21:11 +01:00
Andras Bacsai
fffc6b1e4e feat: docker registries working 2022-11-25 15:44:11 +01:00
Andras Bacsai
9121c6a078 fix: 0 destinations redirect after creation 2022-11-25 15:43:59 +01:00
Andras Bacsai
9c4e581d8b feat: use registry for building 2022-11-25 14:29:01 +01:00
Andras Bacsai
dfadd31f46 Merge pull request #748 from zarxor/main
Typing error in CONTRIBUTION.md
2022-11-25 13:08:16 +01:00
Johan Boström
0cfa6fff43 Typing error in CONTRIBUTION.md 2022-11-23 21:00:01 +01:00
Andras Bacsai
d61671c1a0 wip 2022-11-23 15:44:30 +01:00
Andras Bacsai
d4f10a9af3 feat: custom/private docker registries 2022-11-23 14:39:30 +01:00
Andras Bacsai
03861af893 fix: nope in database strings 2022-11-23 13:40:10 +01:00
Andras Bacsai
ae531c445d fix: remove hardcoded sentry dsn 2022-11-23 13:39:16 +01:00
Andras Bacsai
4b26aeef9a fix: remote haproxy password/etc 2022-11-23 13:39:16 +01:00
Andras Bacsai
1e47b79b50 chore: version++ 2022-11-23 13:39:16 +01:00
Andras Bacsai
0c223dcec4 Merge pull request #698 from themarkwill/fix/errorInBaseApi
fix: Accept logged and not logged user in /base
2022-11-23 13:36:39 +01:00
Andras Bacsai
0f4536c3d3 Merge pull request #744 from coollabsio/next
v3.11.13
2022-11-23 13:08:13 +01:00
Andras Bacsai
f43c584463 prettify 2022-11-23 13:07:45 +01:00
Gabriel Engel
91c558ec83 Componentization of /servers and /sources 2022-11-23 08:17:03 -03:00
Gabriel Engel
9d45ab3246 New Badges components: destination, public, status, teams + container/status 2022-11-23 07:52:59 -03:00
Andras Bacsai
34ff6eb567 fix: load logs after build failed 2022-11-23 11:51:19 +01:00
Andras Bacsai
8793c00438 fix: mounts 2022-11-23 11:48:31 +01:00
Andras Bacsai
d7981d5c3e fix: logs 2022-11-23 11:48:04 +01:00
Andras Bacsai
bcaae3b67b debug off
fix: logging
2022-11-23 11:37:52 +01:00
Andras Bacsai
046d9f9597 debug 2022-11-23 11:24:15 +01:00
Andras Bacsai
81bd0301d2 fix: hasura admin secret 2022-11-23 11:18:25 +01:00
Andras Bacsai
530e7e494f fix: storage for compose bp + debug on 2022-11-23 10:57:52 +01:00
Andras Bacsai
d402fd5690 fix: move debug log settings to build logs 2022-11-23 10:28:36 +01:00
Andras Bacsai
eebec3b92f fix: escape % in secrets 2022-11-23 10:17:09 +01:00
Andras Bacsai
211c6585fa chore: version++ 2022-11-22 13:17:25 +01:00
Andras Bacsai
e1b5c40ca0 update templates 2022-11-22 13:17:09 +01:00
Andras Bacsai
747a9b521b fix: wrong icons on dashboard 2022-11-22 13:16:47 +01:00
Andras Bacsai
c2d72ad309 Merge pull request #742 from coollabsio/next
v3.11.12
2022-11-22 11:20:40 +01:00
Andras Bacsai
596181b622 update packages 2022-11-22 10:55:52 +01:00
Andras Bacsai
77c5270e1e chore: version++ 2022-11-22 10:47:21 +01:00
Andras Bacsai
a663c14df8 fix: exposed ports 2022-11-22 10:47:02 +01:00
Andras Bacsai
3bd9f00268 Merge pull request #741 from coollabsio/next
v3.11.11
2022-11-21 22:03:07 +01:00
Andras Bacsai
1aadda735d fix: webhook traefik 2022-11-21 21:58:07 +01:00
Andras Bacsai
12035208e2 fix: replace $$generate vars 2022-11-21 21:54:21 +01:00
Andras Bacsai
df8a9f673c fix: gh actions 2022-11-18 14:49:20 +01:00
Andras Bacsai
aa5c8a2c56 fix: gh actions 2022-11-18 14:48:31 +01:00
Andras Bacsai
a84540e6bb fix: gitea icon is svg 2022-11-18 14:47:23 +01:00
Andras Bacsai
fb91b64063 Merge pull request #730 from quiint/patch-1
Create Gitea icon
2022-11-18 14:45:01 +01:00
Andras Bacsai
94cc77ebca feat: only show expose if no proxy conf defined in template 2022-11-18 14:33:58 +01:00
Andras Bacsai
aac6981304 fix: no variables in template
feat: hostPort proxy conf from template
2022-11-18 14:28:05 +01:00
Andras Bacsai
ca05828b68 ga fixes 2022-11-18 11:21:41 +01:00
Andras Bacsai
8ec6b4c59c ga fixes 2022-11-18 11:19:15 +01:00
Andras Bacsai
f1be5f5341 ga fixes 2022-11-18 11:17:04 +01:00
Andras Bacsai
714c264002 fluentbit github release 2022-11-18 11:07:52 +01:00
Andras Bacsai
eca58097ef Merge pull request #733 from coollabsio/next
v3.11.10
2022-11-16 14:24:54 +01:00
Andras Bacsai
281146e22b chore: version++ 2022-11-16 12:46:29 +00:00
Andras Bacsai
f3a19a5d02 fix: wrong template/type 2022-11-16 12:40:44 +00:00
Andras Bacsai
9b9b6937f4 fix: local dev api/ws urls 2022-11-16 12:40:28 +00:00
Andras Bacsai
f54c0b7dff fix: isBot issue 2022-11-15 19:13:46 +00:00
Quiint
36c58ad286 Create gitea.svg 2022-11-14 09:54:46 -05:00
Andras Bacsai
a67f633259 Merge pull request #726 from coollabsio/next
v3.11.8
2022-11-14 14:24:52 +01:00
Andras Bacsai
f39a607c1a fix: default icon for new services 2022-11-14 13:54:06 +01:00
Andras Bacsai
0cc67ed2e5 update embeded templates 2022-11-14 13:46:17 +01:00
Andras Bacsai
5f8402c645 Merge pull request #727 from ksmithdev/main
Create keycloak.png
2022-11-14 12:59:29 +01:00
Andras Bacsai
3ab87cd11e ui: reload compose loading 2022-11-14 11:53:53 +01:00
Andras Bacsai
d5620d305d fix: ports for services 2022-11-14 11:49:32 +01:00
Andras Bacsai
35ebc5e842 fix: empty secrets on UI 2022-11-14 11:37:36 +01:00
Andras Bacsai
66276be1d2 fix: volume names for undefined volume names in compose 2022-11-14 11:26:12 +01:00
Andras Bacsai
47c0d522db chore: version++ 2022-11-14 11:00:25 +01:00
Andras Bacsai
b654883d1a ui: fixes 2022-11-14 10:59:19 +01:00
Andras Bacsai
b4f9d29129 fix: application persistent storage things 2022-11-14 10:40:28 +01:00
Andras Bacsai
bec6b961f3 fix: docker compose persistent volumes 2022-11-14 09:11:02 +01:00
Kyle Smith
2ce8f34306 Create keycloak.png 2022-11-11 14:03:05 -05:00
Andras Bacsai
30d1ae59ec revert: revert: revert 2022-11-11 14:25:02 +01:00
Andras Bacsai
ac7d4e3645 fix: getTemplates 2022-11-11 14:19:42 +01:00
Andras Bacsai
868c4001f6 gh action: revert 2022-11-11 14:17:53 +01:00
Andras Bacsai
e99c44d967 gh actions: update prod release flow 2022-11-11 13:41:02 +01:00
Andras Bacsai
48a877f160 Merge pull request #725 from coollabsio/next
v3.11.7
2022-11-11 13:33:57 +01:00
Andras Bacsai
cea894a8bd fix: dashboard error 2022-11-11 13:28:37 +01:00
Andras Bacsai
087e7b9311 Merge pull request #724 from coollabsio/next
v3.11.6
2022-11-11 11:58:46 +01:00
Andras Bacsai
39ba498293 ui: fix 2022-11-11 10:39:01 +01:00
Andras Bacsai
fe7390bd4d fix: update on mobile 2022-11-11 10:38:30 +01:00
Andras Bacsai
75af551435 ui: secrets on apps 2022-11-11 09:33:45 +01:00
Andras Bacsai
ae2d3ebb48 fix: no tags error 2022-11-11 09:25:02 +01:00
Andras Bacsai
5ff6c53715 Merge pull request #723 from coollabsio/next
v3.11.5
2022-11-11 08:28:37 +01:00
Andras Bacsai
3c94723b23 fix: show rollback button loading 2022-11-10 15:43:28 +01:00
Andras Bacsai
c6a2e3e328 update tags 2022-11-10 15:34:33 +01:00
Andras Bacsai
2dc5e10878 update tags 2022-11-10 15:33:57 +01:00
Andras Bacsai
4086dfcf56 rename lavalink 2022-11-10 15:32:13 +01:00
Andras Bacsai
7937c2bab0 Merge pull request #717 from kaname-png/next
chore: add jda icon for lavalink service
2022-11-10 15:31:37 +01:00
Andras Bacsai
5ffa8e9936 update templates 2022-11-10 15:29:44 +01:00
Andras Bacsai
c431cee517 fix: wp + mysql on arm 2022-11-10 15:01:03 +01:00
Andras Bacsai
375f17e728 debug 2022-11-10 14:52:37 +01:00
Andras Bacsai
d3f658c874 Readme fix 2022-11-10 14:17:20 +01:00
Andras Bacsai
5e340a4cdd fix: expose ports for services 2022-11-10 14:13:58 +01:00
Andras Bacsai
409a5b9f99 fix: n8n and weblate icon 2022-11-10 14:08:02 +01:00
Andras Bacsai
fba305020b fix: for rollback 2022-11-10 14:00:01 +01:00
Andras Bacsai
bd4ce3ac45 feat: rollback coolify 2022-11-10 13:57:34 +01:00
Gabriel Engel
733de60f7c Starting translations work 2022-11-09 19:27:03 -03:00
Andras Bacsai
c365a44e01 Merge pull request #719 from coollabsio/next
v3.11.4
2022-11-09 14:20:23 +01:00
Andras Bacsai
e94f450bf0 fix: doc links 2022-11-09 13:50:29 +01:00
Andras Bacsai
d5efc9ddde chore: version++ 2022-11-09 13:50:20 +01:00
Andras Bacsai
68895ba4a5 fix: variable replacements 2022-11-09 13:50:11 +01:00
Andras Bacsai
139aa7a0fc Merge pull request #718 from coollabsio/next
v3.11.3
2022-11-09 13:05:58 +01:00
Andras Bacsai
4955157e13 fix: compose webhooks fixed 2022-11-09 13:02:42 +01:00
Kaname
f2dd5cc75e chore: add jda icon for lavalink service 2022-11-08 12:39:41 -06:00
Andras Bacsai
2ad634dbc6 refactor: code 2022-11-08 15:51:07 +01:00
Andras Bacsai
de13f65a24 fix: umami template 2022-11-08 15:23:18 +01:00
ThallesP
e038865693 feature: add default to latest commit and support for gitlab 2022-10-25 13:51:10 -03:00
ThallesP
dfd29dc37a feature: initial support for specific git commit 2022-10-25 13:26:03 -03:00
The Mark
4448b86b93 fix: Accept logged and not logged user in /base 2022-10-23 13:31:24 -04:00
139 changed files with 8478 additions and 2334 deletions

View File

@@ -0,0 +1,93 @@
name: fluent-bit-release
on:
push:
paths:
- "others/fluentbit"
- ".github/workflows/fluent-bit-release.yml"
branches:
- next
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: Build and push
uses: docker/build-push-action@v2
with:
context: others/fluentbit/
platforms: linux/arm64
push: true
tags: coollabsio/coolify-fluent-bit:1.0.0-arm64
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: Build and push
uses: docker/build-push-action@v3
with:
context: others/fluentbit/
platforms: linux/amd64
push: true
tags: coollabsio/coolify-fluent-bit:1.0.0-amd64
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: Build and push
uses: docker/build-push-action@v2
with:
context: others/fluentbit/
platforms: linux/aarch64
push: true
tags: coollabsio/coolify-fluent-bit:1.0.0-aarch64
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: Create & publish manifest
run: |
docker manifest create coollabsio/coolify-fluent-bit:1.0.0 --amend coollabsio/coolify-fluent-bit:1.0.0-amd64 --amend coollabsio/coolify-fluent-bit:1.0.0-arm64 --amend coollabsio/coolify-fluent-bit:1.0.0-aarch64
docker manifest push coollabsio/coolify-fluent-bit:1.0.0

View File

@@ -0,0 +1,93 @@
name: pocketbase-release
on:
push:
paths:
- "others/pocketbase"
- ".github/workflows/pocketbase-release.yml"
branches:
- next
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: Build and push
uses: docker/build-push-action@v2
with:
context: others/pocketbase/
platforms: linux/arm64
push: true
tags: coollabsio/pocketbase:0.8.0-arm64
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: Build and push
uses: docker/build-push-action@v3
with:
context: others/pocketbase/
platforms: linux/amd64
push: true
tags: coollabsio/pocketbase:0.8.0-amd64
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: Build and push
uses: docker/build-push-action@v2
with:
context: others/pocketbase/
platforms: linux/aarch64
push: true
tags: coollabsio/pocketbase:0.8.0-aarch64
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: Create & publish manifest
run: |
docker manifest create coollabsio/pocketbase:0.8.0 --amend coollabsio/pocketbase:0.8.0-amd64 --amend coollabsio/pocketbase:0.8.0-arm64 --amend coollabsio/pocketbase:0.8.0-aarch64
docker manifest push coollabsio/pocketbase:0.8.0

View File

@@ -2,6 +2,12 @@ name: staging-release
on:
push:
paths:
- "**"
- "!others/fluentbit"
- "!others/pocketbase"
- "!.github/workflows/fluent-bit-release.yml"
- "!.github/workflows/pocketbase-release.yml"
branches:
- next

5
.gitignore vendored
View File

@@ -10,8 +10,11 @@ package
dist
client
apps/api/db/*.db
local-serve
apps/api/db/migration.db-journal
apps/api/core*
apps/backup/backups/*
!apps/backup/backups/.gitkeep
logs
others/certificates
backups/*
!backups/.gitkeep

21
.vscode/settings.json vendored
View File

@@ -1,11 +1,22 @@
{
"i18n-ally.localesPaths": ["src/lib/locales"],
"i18n-ally.localesPaths": [
"src/lib/locales"
],
"i18n-ally.keystyle": "nested",
"i18n-ally.extract.ignoredByFiles": {
"src\\routes\\__layout.svelte": ["Coolify", "coolLabs logo"]
"src\\routes\\__layout.svelte": [
"Coolify",
"coolLabs logo"
]
},
"i18n-ally.sourceLanguage": "en",
"i18n-ally.enabledFrameworks": ["svelte"],
"i18n-ally.enabledParsers": ["js", "ts", "json"],
"i18n-ally.enabledFrameworks": [
"svelte"
],
"i18n-ally.enabledParsers": [
"js",
"ts",
"json"
],
"i18n-ally.extract.autoDetect": true
}
}

View File

@@ -34,7 +34,7 @@ You'll need a set of skills to [get started](docs/contribution/GettingStarted.md
```sh
# Or... Copy and paste commands bellow:
cp apps/api/.env.example apps/api.env
cp apps/api/.env.example apps/api/.env
pnpm install
pnpm db:push
pnpm db:seed

View File

@@ -77,6 +77,7 @@ Deploy your resource to:
<a href="https://redis.io"><svg style="width:40px;height:40px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ><defs ><path id="a" d="m45.536 38.764c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.813s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z" /><path id="b" d="m45.536 28.733c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.935c2.332-.837 3.14-.867 5.126-.14s12.35 4.853 14.312 5.57 2.037 1.31.024 2.36z" /></defs ><g transform="matrix(.848327 0 0 .848327 -7.883573 -9.449691)" ><use fill="#a41e11" xlink:href="#a" /><path d="m45.536 34.95c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.936c2.332-.836 3.14-.867 5.126-.14s12.35 4.852 14.31 5.582 2.037 1.31.024 2.36z" fill="#d82c20" /><use fill="#a41e11" xlink:href="#a" y="-6.218" /><use fill="#d82c20" xlink:href="#b" /><path d="m45.536 26.098c-2.013 1.05-12.44 5.337-14.66 6.495s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.815s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z" fill="#a41e11" /><use fill="#d82c20" xlink:href="#b" y="-6.449" /><g fill="#fff" ><path d="m29.096 20.712-1.182-1.965-3.774-.34 2.816-1.016-.845-1.56 2.636 1.03 2.486-.814-.672 1.612 2.534.95-3.268.34zm-6.296 3.912 8.74-1.342-2.64 3.872z" /><ellipse cx="20.444" cy="21.402" rx="4.672" ry="1.811" /></g ><path d="m42.132 21.138-5.17 2.042-.004-4.087z" fill="#7a0c00" /><path d="m36.963 23.18-.56.22-5.166-2.042 5.723-2.264z" fill="#ad2115" /></g ></svg ></a>
### Services
- [Appwrite](https://appwrite.io)
- [WordPress](https://docs.coollabs.io/coolify/services/wordpress)
- [Ghost](https://ghost.org)
@@ -93,23 +94,39 @@ Deploy your resource to:
- [Fider](https://fider.io)
- [Hasura](https://hasura.io)
- [GlitchTip](https://glitchtip.com)
## Migration from v1
A fresh installation is necessary. v2 and v3 are not compatible with v1.
- And more...
## Support
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://coollabs.io/discord)
## Development Contributions
---
Coolify is developed under the Apache License and you can help to make it grow &rarr; [Start coding!](./CONTRIBUTION.md)
## ⚗️ Expertise Contributions
## Financial Contributors
Coolify is developed under the [Apache License](./LICENSE) and you can help to make it grow.
Our community will be glad to have you on board!
Learn how to contribute to Coolify as as ...
&rarr; [👩🏾‍💻 Software developer](./CONTRIBUTION.md)
&rarr; [🧑🏻‍🏫 Translator](./docs/contribution/Translating.md)
<!--
&rarr; 🧑🏽‍🎨 Designer
&rarr; 🙋‍♀️ Community Managemer
&rarr; 🧙🏻‍♂️ Text Content Creator
&rarr; 👨🏼‍🎤 Video Content Creator
-->
---
## 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]

View File

@@ -1,10 +1,9 @@
COOLIFY_APP_ID=local-dev
# 32 bits long secret key
COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON=docker
COOLIFY_WHITE_LABELED=false
COOLIFY_WHITE_LABELED_ICON=
COOLIFY_AUTO_UPDATE=
COOLIFY_APP_ID=local-dev
# 32 bits long secret key
COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_IS_ON=docker
COOLIFY_WHITE_LABELED=false
COOLIFY_WHITE_LABELED_ICON=
COOLIFY_AUTO_UPDATE=

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,379 @@
- templateVersion: 1.0.0
defaultVersion: '0.8.0'
documentation: https://pocketbase.io/docs/
type: pocketbase
name: Pocketbase
description: "Open Source realtime backend in 1 file"
services:
$$id:
image: coollabsio/pocketbase:$$core_version
volumes:
- $$id-data:/app/pb_data
ports:
- "8080"
- templateVersion: 1.0.0
defaultVersion: 1.5.0-rc.0
documentation: https://plausible.io/doc/
type: plausibleanalytics-arm
name: Plausible Analytics (ARM)
description: A lightweight and open-source website analytics tool.
labels:
- analytics
- statistics
- plausible
- gdpr
- no-cookie
- google analytics
services:
$$id:
name: Plausible Analytics
command: >-
sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate
&& /entrypoint.sh db init-admin && /entrypoint.sh run"
depends_on:
- $$id-postgresql
- $$id-clickhouse
image: plausible/analytics:$$core_version
environment:
- ADMIN_USER_EMAIL=$$config_admin_user_email
- ADMIN_USER_NAME=$$config_admin_user_name
- ADMIN_USER_PWD=$$secret_admin_user_pwd
- BASE_URL=$$config_base_url
- SECRET_KEY_BASE=$$secret_secret_key_base
- DISABLE_AUTH=$$config_disable_auth
- DISABLE_REGISTRATION=$$config_disable_registration
- DATABASE_URL=$$secret_database_url
- CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url
ports:
- "8000"
$$id-postgresql:
name: PostgreSQL
image: postgres:14-alpine
volumes:
- $$id-postgresql-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=$$secret_postgres_password
- POSTGRES_USER=$$config_postgres_user
- POSTGRES_DB=$$config_postgres_db
$$id-clickhouse:
name: Clickhouse
volumes:
- $$id-clickhouse-data:/var/lib/clickhouse
image: clickhouse/clickhouse-server:22.6-alpine
ulimits:
nofile:
soft: 262144
hard: 262144
files:
- location: /etc/clickhouse-server/users.d/logging.xml
content: >-
<yandex><logger><level>warning</level><console>true</console></logger><query_thread_log
remove="remove"/><query_log remove="remove"/><text_log
remove="remove"/><trace_log remove="remove"/><metric_log
remove="remove"/><asynchronous_metric_log
remove="remove"/><session_log remove="remove"/><part_log
remove="remove"/></yandex>
- location: /etc/clickhouse-server/config.d/logging.xml
content: >-
<yandex><profiles><default><log_queries>0</log_queries><log_query_threads>0</log_query_threads></default></profiles></yandex>
- location: /docker-entrypoint-initdb.d/init.query
content: CREATE DATABASE IF NOT EXISTS plausible;
- location: /docker-entrypoint-initdb.d/init-db.sh
content: >-
clickhouse client --queries-file
/docker-entrypoint-initdb.d/init.query
variables:
- id: $$config_base_url
name: BASE_URL
label: Base URL
defaultValue: $$generate_fqdn
description: >-
You must set this to the FQDN of the Plausible Analytics instance. This is
used to generate the links to the Plausible Analytics instance.
- id: $$secret_database_url
name: DATABASE_URL
label: Database URL for PostgreSQL
defaultValue: >-
postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db
description: ""
- id: $$secret_clickhouse_database_url
name: CLICKHOUSE_DATABASE_URL
label: Database URL for Clickhouse
defaultValue: http://$$id-clickhouse:8123/plausible
description: ""
- id: $$config_admin_user_email
name: ADMIN_USER_EMAIL
label: Admin Email Address
defaultValue: admin@example.com
description: This is the admin email. Please change it.
- id: $$config_admin_user_name
name: ADMIN_USER_NAME
label: Admin User Name
defaultValue: $$generate_username
description: This is the admin username. Please change it.
- id: $$secret_admin_user_pwd
name: ADMIN_USER_PWD
label: Admin User Password
defaultValue: $$generate_password
description: This is the admin password. Please change it.
showOnConfiguration: true
- id: $$secret_secret_key_base
name: SECRET_KEY_BASE
label: Secret Key Base
defaultValue: $$generate_hex(64)
description: ""
- id: $$config_disable_auth
name: DISABLE_AUTH
label: Disable Authentication
defaultValue: "false"
description: ""
- id: $$config_disable_registration
name: DISABLE_REGISTRATION
label: Disable Registration
defaultValue: "true"
description: ""
- id: $$config_postgres_user
main: $$id-postgresql
name: POSTGRES_USER
label: PostgreSQL Username
defaultValue: postgresql
description: ""
- id: $$secret_postgres_password
main: $$id-postgresql
name: POSTGRES_PASSWORD
label: PostgreSQL Password
defaultValue: $$generate_password
description: ""
showOnConfiguration: true
- id: $$config_postgres_db
main: $$id-postgresql
name: POSTGRES_DB
label: PostgreSQL Database
defaultValue: plausible
description: ""
- id: $$config_scriptName
name: SCRIPT_NAME
label: Custom Script Name
defaultValue: plausible.js
description: This is the default script name.
- templateVersion: 1.0.0
defaultVersion: "1.17"
documentation: https://docs.gitea.io
type: gitea
name: Gitea
description: Gitea is a community managed lightweight code hosting solution written in Go.
labels:
- storage
- git
services:
$$id:
name: Gitea
documentation: https://docs.gitea.io
image: gitea/gitea:$$core_version
volumes:
- $$id-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
- USER_UID=1000
- USER_GID=1000
- DOMAIN=$$config_domain
- SSH_DOMAIN=$$config_ssh_domain
- ROOT_URL=$$config_root_url
- SECRET_KEY=$$secret_secret_key
- INTERNAL_TOKEN=$$secret_internal_token
- SSH_PORT=22
- START_SSH_SERVER=$$config_start_ssh_server
ports:
- "3000"
- "22"
proxy:
- port: "22"
hostPort: $$config_hostport_ssh
variables:
- id: $$config_hostport_ssh
name: SSH_PORT
label: SSH Port
defaultValue: "8022"
description: ""
required: true
- id: $$config_domain
name: DOMAIN
label: Domain
defaultValue: $$generate_domain
description: ""
- id: $$config_ssh_domain
name: SSH_DOMAIN
label: SSH Domain
defaultValue: $$generate_domain
description: ""
- id: $$config_start_ssh_server
name: START_SSH_SERVER
label: Start SSH Server
defaultValue: "true"
description: ""
- id: $$config_root_url
name: ROOT_URL
label: Root URL of Gitea
defaultValue: $$generate_fqdn_slash
description: ""
- id: $$secret_secret_key
name: SECRET_KEY
label: Secret Key
defaultValue: $$generate_hex(32)
description: ""
- id: $$secret_internal_token
name: INTERNAL_TOKEN
label: Internal JWT Token
defaultValue: $$generate_token
description: ""
- templateVersion: 1.0.0
defaultVersion: "20.0"
documentation: https://www.keycloak.org/documentation
type: keycloak
name: Keycloak
description: "Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more."
labels:
- authentication
- authorization
- oidconnect
- saml2
services:
$$id:
name: Keycloak
command: start --db=postgres --features=token-exchange --import-realm
depends_on:
- $$id-postgresql
image: "quay.io/keycloak/keycloak:$$core_version"
volumes:
- $$id-import:/opt/keycloak/data/import
environment:
- KC_HEALTH_ENABLED=true
- KC_PROXY=edge
- KC_DB=postgres
- KC_HOSTNAME=$$config_keycloak_domain
- KEYCLOAK_ADMIN=$$config_admin_user
- KEYCLOAK_ADMIN_PASSWORD=$$secret_keycloak_admin_password
- KC_DB_PASSWORD=$$secret_postgres_password
- KC_DB_USERNAME=$$config_postgres_user
- KC_DB_URL=$$secret_keycloak_database_url
ports:
- "8080"
$$id-postgresql:
name: PostgreSQL
depends_on: []
image: "postgres:14-alpine"
volumes:
- "$$id-postgresql-data:/var/lib/postgresql/data"
environment:
- POSTGRES_USER=$$config_postgres_user
- POSTGRES_PASSWORD=$$secret_postgres_password
- POSTGRES_DB=$$config_postgres_db
ports: []
variables:
- id: $$config_keycloak_domain
name: KEYCLOAK_DOMAIN
label: Keycloak Domain
defaultValue: $$generate_domain
description: ""
- id: $$secret_keycloak_database_url
name: KEYCLOAK_DATABASE_URL
label: Keycloak Database Url
defaultValue: >-
jdbc:postgresql://$$id-postgresql:5432/$$config_postgres_db
description: ""
- id: $$config_admin_user
name: KEYCLOAK_ADMIN
label: Keycloak Admin User
defaultValue: $$generate_username
description: ""
- id: $$secret_keycloak_admin_password
name: KEYCLOAK_ADMIN_PASSWORD
label: Keycloak Admin Password
defaultValue: $$generate_password
description: ""
showOnConfiguration: true
- id: $$config_postgres_user
main: $$id-postgresql
name: POSTGRES_USER
label: PostgreSQL User
defaultValue: $$generate_username
description: ""
- id: $$secret_postgres_password
main: $$id-postgresql
name: POSTGRES_PASSWORD
label: PostgreSQL Password
defaultValue: $$generate_password
description: ""
showOnConfiguration: true
- id: $$config_postgres_db
main: $$id-postgresql
name: POSTGRES_DB
label: PostgreSQL Database
defaultValue: keycloak
description: ""
- templateVersion: 1.0.0
defaultVersion: v3.6
documentation: https://github.com/freyacodes/Lavalink
description: Standalone audio sending node based on Lavaplayer.
type: lavalink
name: Lavalink
labels:
- discord
- discord bot
- audio
- lavalink
- jda
services:
$$id:
name: Lavalink
image: fredboat/lavalink:$$core_version
environment: []
volumes:
- $$id-lavalink:/lavalink
ports:
- "2333"
files:
- location: /opt/Lavalink/application.yml
content: >-
server:
port: $$config_port
address: 0.0.0.0
lavalink:
server:
password: "$$secret_password"
sources:
youtube: true
bandcamp: true
soundcloud: true
twitch: true
vimeo: true
http: true
local: false
logging:
file:
path: ./logs/
level:
root: INFO
lavalink: INFO
logback:
rollingpolicy:
max-file-size: 1GB
max-history: 30
variables:
- id: $$config_port
name: PORT
label: Port
defaultValue: "2333"
required: true
- id: $$secret_password
name: PASSWORD
label: Password
defaultValue: $$generate_password
required: true
- templateVersion: 1.0.0
defaultVersion: v1.8.6
documentation: https://docs.appsmith.com/getting-started/setup/instance-configuration/
@@ -1606,7 +1982,7 @@
- HASURA_GRAPHQL_ENABLE_CONSOLE=$$config_hasura_graphql_enable_console
- >-
HASURA_GRAPHQL_METADATA_DATABASE_URL=$$secret_hasura_graphql_metadata_database_url
- HASURA_GRAPHQL_ADMIN_PASSWORD=$$secret_hasura_graphql_admin_password
- HASURA_GRAPHQL_ADMIN_SECRET=$$secret_hasura_graphql_admin_secret
ports:
- "8080"
$$id-postgresql:
@@ -1632,8 +2008,8 @@
defaultValue: >-
postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db
description: ""
- id: $$secret_hasura_graphql_admin_password
name: HASURA_GRAPHQL_ADMIN_PASSWORD
- id: $$secret_hasura_graphql_admin_secret
name: HASURA_GRAPHQL_ADMIN_SECRET
label: Hasura Admin Password
defaultValue: $$generate_password
description: ""
@@ -1664,7 +2040,6 @@
services:
$$id:
name: Umami
documentation: "Official docs are [here](https://umami.is/docs/getting-started)"
depends_on:
- $$id-postgresql
image: "ghcr.io/umami-software/umami:$$core_version"
@@ -1678,7 +2053,213 @@
- "3000"
$$id-postgresql:
name: PostgreSQL
documentation: "Official docs are [here](https://umami.is/docs/getting-started)"
depends_on: []
image: "postgres:12-alpine"
volumes:
- "$$id-postgresql-data:/var/lib/postgresql/data"
environment:
- POSTGRES_USER=$$config_postgres_user
- POSTGRES_PASSWORD=$$secret_postgres_password
- POSTGRES_DB=$$config_postgres_db
ports: []
files:
- location: /docker-entrypoint-initdb.d/schema.postgresql.sql
content: |2-
-- CreateTable
CREATE TABLE "account" (
"user_id" SERIAL NOT NULL,
"username" VARCHAR(255) NOT NULL,
"password" VARCHAR(60) NOT NULL,
"is_admin" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("user_id")
);
-- CreateTable
CREATE TABLE "event" (
"event_id" SERIAL NOT NULL,
"website_id" INTEGER NOT NULL,
"session_id" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"url" VARCHAR(500) NOT NULL,
"event_type" VARCHAR(50) NOT NULL,
"event_value" VARCHAR(50) NOT NULL,
PRIMARY KEY ("event_id")
);
-- CreateTable
CREATE TABLE "pageview" (
"view_id" SERIAL NOT NULL,
"website_id" INTEGER NOT NULL,
"session_id" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"url" VARCHAR(500) NOT NULL,
"referrer" VARCHAR(500),
PRIMARY KEY ("view_id")
);
-- CreateTable
CREATE TABLE "session" (
"session_id" SERIAL NOT NULL,
"session_uuid" UUID NOT NULL,
"website_id" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"hostname" VARCHAR(100),
"browser" VARCHAR(20),
"os" VARCHAR(20),
"device" VARCHAR(20),
"screen" VARCHAR(11),
"language" VARCHAR(35),
"country" CHAR(2),
PRIMARY KEY ("session_id")
);
-- CreateTable
CREATE TABLE "website" (
"website_id" SERIAL NOT NULL,
"website_uuid" UUID NOT NULL,
"user_id" INTEGER NOT NULL,
"name" VARCHAR(100) NOT NULL,
"domain" VARCHAR(500),
"share_id" VARCHAR(64),
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("website_id")
);
-- CreateIndex
CREATE UNIQUE INDEX "account.username_unique" ON "account"("username");
-- CreateIndex
CREATE INDEX "event_created_at_idx" ON "event"("created_at");
-- CreateIndex
CREATE INDEX "event_session_id_idx" ON "event"("session_id");
-- CreateIndex
CREATE INDEX "event_website_id_idx" ON "event"("website_id");
-- CreateIndex
CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at");
-- CreateIndex
CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id");
-- CreateIndex
CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at");
-- CreateIndex
CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id");
-- CreateIndex
CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at");
-- CreateIndex
CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid");
-- CreateIndex
CREATE INDEX "session_created_at_idx" ON "session"("created_at");
-- CreateIndex
CREATE INDEX "session_website_id_idx" ON "session"("website_id");
-- CreateIndex
CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid");
-- CreateIndex
CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id");
-- CreateIndex
CREATE INDEX "website_user_id_idx" ON "website"("user_id");
-- AddForeignKey
ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);
variables:
- id: $$secret_database_url
name: DATABASE_URL
label: Database URL for PostgreSQL
defaultValue: >-
postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db
description: ""
- id: $$secret_hash_salt
name: HASH_SALT
label: Hash Salt
defaultValue: $$generate_hex(64)
description: ""
- id: $$config_database_type
name: DATABASE_TYPE
label: Database Type
defaultValue: "postgresql"
description: ""
- id: $$config_postgres_user
name: POSTGRES_USER
label: PostgreSQL User
defaultValue: $$generate_username
description: ""
- id: $$secret_postgres_password
name: POSTGRES_PASSWORD
label: PostgreSQL Password
defaultValue: $$generate_password
description: ""
- id: $$config_postgres_db
name: POSTGRES_DB
label: PostgreSQL Database
defaultValue: umami
description: ""
- id: $$secret_admin_password
name: ADMIN_PASSWORD
label: Initial Admin Password
defaultValue: $$generate_password
description: ""
showOnConfiguration: true
- templateVersion: 1.0.0
ignore: true
defaultVersion: postgresql-v1.38.0
documentation: https://umami.is/docs/getting-started
type: umami
name: Umami
subname: (PostgreSQL)
description: >-
A simple, easy to use, self-hosted web analytics solution.
services:
$$id:
name: Umami
depends_on:
- $$id-postgresql
image: "ghcr.io/umami-software/umami:$$core_version"
volumes: []
environment:
- ADMIN_PASSWORD=$$secret_admin_password
- DATABASE_URL=$$secret_database_url
- DATABASE_TYPE=$$config_database_type
- HASH_SALT=$$secret_hash_salt
ports:
- "3000"
$$id-postgresql:
name: PostgreSQL
depends_on: []
image: "postgres:12-alpine"
volumes:
@@ -1871,7 +2452,7 @@
services:
$$id:
name: MeiliSearch
documentation: "https://docs.meilisearch.com/"
documentation: https://docs.meilisearch.com/
depends_on: []
image: "getmeili/meilisearch:$$core_version"
volumes:
@@ -1893,7 +2474,8 @@
- templateVersion: 1.0.0
ignore: true
defaultVersion: latest
documentation: https://ghost.org/resources/
documentation: https://docs.ghost.org
arch: amd64
type: ghost-mariadb
name: Ghost
subname: (MariaDB)
@@ -1905,7 +2487,6 @@
services:
$$id:
name: Ghost
documentation: "Taken from https://docs.ghost.org/"
depends_on:
- $$id-mariadb
image: "bitnami/ghost:$$core_version"
@@ -2011,7 +2592,7 @@
description: ""
- templateVersion: 1.0.0
defaultVersion: "5.22"
documentation: https://ghost.org/resources/
documentation: https://docs.ghost.org
type: ghost-only
name: Ghost
subname: (without Database)
@@ -2020,7 +2601,6 @@
services:
$$id:
name: Ghost
documentation: "Taken from https://docs.ghost.org/"
image: "ghost:$$core_version"
volumes:
- "$$id-ghost:/var/lib/ghost/content"
@@ -2076,7 +2656,7 @@
required: true
- templateVersion: 1.0.0
defaultVersion: "5.22"
documentation: https://ghost.org/resources/
documentation: https://docs.ghost.org
type: ghost-mysql
name: Ghost
subname: (MySQL)
@@ -2085,7 +2665,6 @@
services:
$$id:
name: Ghost
documentation: "Taken from https://docs.ghost.org/"
depends_on:
- $$id-mysql
image: "ghost:$$core_version"
@@ -2166,7 +2745,6 @@
services:
$$id:
name: WordPress
documentation: " Taken from https://docs.docker.com/compose/wordpress/"
depends_on:
- $$id-mysql
image: "wordpress:$$core_version"
@@ -2184,7 +2762,7 @@
name: MySQL
depends_on: []
image: "bitnami/mysql:5.7"
imageArm: "mysql:5.7"
imageArm: "mysql:8.0"
volumes:
- "$$id-mysql-data:/bitnami/mysql/data"
volumesArm:
@@ -2257,7 +2835,6 @@
services:
$$id:
name: WordPress
documentation: "Taken from https://docs.docker.com/compose/wordpress/"
image: "wordpress:$$core_version"
volumes:
- "$$id-wordpress-data:/var/www/html"
@@ -2331,7 +2908,6 @@
services:
$$id:
name: VSCode Server
documentation: "Taken from https://github.com/coder/code-server/. "
depends_on: []
image: "codercom/code-server:$$core_version"
volumes:
@@ -2363,7 +2939,6 @@
$$id:
name: MinIO
command: "server /data --console-address :9001"
documentation: "Taken from https://docs.min.io/docs/minio-docker-quickstart-guide.html"
depends_on: []
image: "minio/minio:$$core_version"
volumes:
@@ -2423,7 +2998,6 @@
$$id:
name: Fider
image: "getfider/fider:$$core_version"
documentation: "Taken from https://hub.docker.com/r/getfider/fider/"
depends_on:
- $$id-postgresql
environment:
@@ -2443,7 +3017,6 @@
- "3000"
$$id-postgresql:
name: PostgreSQL
documentation: "Taken from https://hub.docker.com/r/getfider/fider/"
depends_on: []
image: "postgres:12-alpine"
volumes:
@@ -2546,7 +3119,6 @@
services:
$$id:
name: N8n
documentation: "Taken from https://hub.docker.com/r/n8nio/n8n"
depends_on: []
image: "n8nio/n8n:$$core_version"
volumes:
@@ -2566,6 +3138,7 @@
- templateVersion: 1.0.0
defaultVersion: stable
documentation: https://plausible.io/doc/
arch: amd64
type: plausibleanalytics
name: Plausible Analytics
description: A lightweight and open-source website analytics tool.
@@ -2579,7 +3152,6 @@
services:
$$id:
name: Plausible Analytics
documentation: "Taken from https://plausible.io/"
command: >-
sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db
migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"
@@ -2601,8 +3173,7 @@
- "8000"
$$id-postgresql:
name: PostgreSQL
documentation: "Taken from https://plausible.io/"
image: "bitnami/postgresql:13.2.0"
image: "bitnami/postgresql:13"
volumes:
- "$$id-postgresql-data:/bitnami/postgresql"
environment:
@@ -2611,10 +3182,9 @@
- POSTGRESQL_DATABASE=$$config_postgresql_database
$$id-clickhouse:
name: Clickhouse
documentation: "Taken from https://plausible.io/"
volumes:
- "$$id-clickhouse-data:/var/lib/clickhouse"
image: "yandex/clickhouse-server:21.3.2.5"
image: "clickhouse/clickhouse-server:22.6-alpine"
ulimits:
nofile:
soft: 262144

View File

@@ -1,7 +1,11 @@
{
"watch": ["src"],
"ignore": ["src/**/*.test.ts"],
"ext": "ts,mjs,json,graphql",
"exec": "rimraf build && esbuild `find src \\( -name '*.ts' \\)` --minify=true --platform=node --outdir=build --format=cjs && node build",
"legacyWatch": true
}
"watch": [
"src"
],
"ignore": [
"src/**/*.test.ts"
],
"ext": "ts,mjs,json,graphql",
"exec": "rimraf build && esbuild `find src \\( -name '*.ts' \\)` --platform=node --outdir=build --format=cjs && node build",
"legacyWatch": true
}

View File

@@ -9,62 +9,67 @@
"db:studio": "prisma studio",
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"dev": "nodemon",
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --minify=true --platform=node --outdir=build --format=cjs",
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs",
"format": "prettier --write 'src/**/*.{js,ts,json,md}'",
"lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .",
"start": "NODE_ENV=production pnpm prisma migrate deploy && pnpm prisma generate && pnpm prisma db seed && node index.js"
},
"dependencies": {
"@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.4.1",
"@fastify/autoload": "5.5.0",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.1.1",
"@fastify/cors": "8.2.0",
"@fastify/env": "4.1.0",
"@fastify/jwt": "6.3.2",
"@fastify/jwt": "6.3.3",
"@fastify/multipart": "7.3.0",
"@fastify/static": "6.5.0",
"@fastify/static": "6.5.1",
"@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2",
"@prisma/client": "4.5.0",
"@prisma/client": "4.6.1",
"@sentry/node": "7.21.1",
"@sentry/tracing": "7.21.1",
"axe": "11.0.0",
"bcryptjs": "2.4.3",
"bree": "9.1.2",
"cabin": "9.1.2",
"cabin": "11.0.1",
"compare-versions": "5.0.1",
"csv-parse": "5.3.1",
"csv-parse": "5.3.2",
"csvtojson": "2.0.10",
"cuid": "2.1.8",
"dayjs": "1.11.6",
"dockerode": "3.3.4",
"dotenv-extended": "2.9.0",
"execa": "6.1.0",
"fastify": "4.9.2",
"fastify": "4.10.2",
"fastify-plugin": "4.3.0",
"fastify-socket.io": "4.0.0",
"generate-password": "1.7.0",
"got": "12.5.2",
"got": "12.5.3",
"is-ip": "5.0.0",
"is-port-reachable": "4.0.0",
"js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1",
"minimist": "^1.2.7",
"node-forge": "1.3.1",
"node-os-utils": "1.3.7",
"p-all": "4.0.0",
"p-throttle": "5.0.0",
"prisma": "4.5.0",
"prisma": "4.6.1",
"public-ip": "6.0.1",
"pump": "3.0.0",
"shell-quote": "^1.7.4",
"socket.io": "4.5.3",
"ssh-config": "4.1.6",
"strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1"
},
"devDependencies": {
"@types/node": "18.11.6",
"@types/node": "18.11.9",
"@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.41.0",
"@typescript-eslint/parser": "5.41.0",
"esbuild": "0.15.12",
"eslint": "8.26.0",
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
"esbuild": "0.15.15",
"eslint": "8.28.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.20",
@@ -72,7 +77,7 @@
"rimraf": "3.0.2",
"tsconfig-paths": "4.1.0",
"types-fastify-socket.io": "0.0.1",
"typescript": "4.8.4"
"typescript": "4.9.3"
},
"prisma": {
"seed": "node prisma/seed.js"

View File

@@ -0,0 +1,45 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"isAPIDebuggingEnabled" BOOLEAN DEFAULT false,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"proxyPassword" TEXT NOT NULL,
"proxyUser" TEXT NOT NULL,
"proxyHash" TEXT,
"proxyDefaultRedirect" TEXT,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"DNSServers" TEXT,
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"ipv4" TEXT,
"ipv6" TEXT,
"arch" TEXT,
"concurrentBuilds" INTEGER NOT NULL DEFAULT 1,
"applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false
);
INSERT INTO "new_Setting" ("DNSServers", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "DNSServers", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
CREATE TABLE "new_ApplicationPersistentStorage" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"path" TEXT NOT NULL,
"oldPath" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationPersistentStorage_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationPersistentStorage" ("applicationId", "createdAt", "id", "path", "updatedAt") SELECT "applicationId", "createdAt", "id", "path", "updatedAt" FROM "ApplicationPersistentStorage";
DROP TABLE "ApplicationPersistentStorage";
ALTER TABLE "new_ApplicationPersistentStorage" RENAME TO "ApplicationPersistentStorage";
CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_path_key" ON "ApplicationPersistentStorage"("applicationId", "path");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,37 @@
/*
Warnings:
- You are about to drop the column `proxyHash` on the `Setting` table. All the data in the column will be lost.
- You are about to drop the column `proxyPassword` on the `Setting` table. All the data in the column will be lost.
- You are about to drop the column `proxyUser` on the `Setting` table. All the data in the column will be lost.
*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"DNSServers" TEXT,
"ipv4" TEXT,
"ipv6" TEXT,
"arch" TEXT,
"concurrentBuilds" INTEGER NOT NULL DEFAULT 1,
"applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false,
"proxyDefaultRedirect" TEXT,
"isAPIDebuggingEnabled" BOOLEAN DEFAULT false,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,59 @@
-- CreateTable
CREATE TABLE "DockerRegistry" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"url" TEXT NOT NULL,
"username" TEXT,
"password" TEXT,
"isSystemWide" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"teamId" TEXT,
CONSTRAINT "DockerRegistry_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Application" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"fqdn" TEXT,
"repository" TEXT,
"configHash" TEXT,
"branch" TEXT,
"buildPack" TEXT,
"projectId" INTEGER,
"port" INTEGER,
"exposePort" INTEGER,
"installCommand" TEXT,
"buildCommand" TEXT,
"startCommand" TEXT,
"baseDirectory" TEXT,
"publishDirectory" TEXT,
"deploymentType" TEXT,
"phpModules" TEXT,
"pythonWSGI" TEXT,
"pythonModule" TEXT,
"pythonVariable" TEXT,
"dockerFileLocation" TEXT,
"denoMainFile" TEXT,
"denoOptions" TEXT,
"dockerComposeFile" TEXT,
"dockerComposeFileLocation" TEXT,
"dockerComposeConfiguration" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"destinationDockerId" TEXT,
"gitSourceId" TEXT,
"baseImage" TEXT,
"baseBuildImage" TEXT,
"dockerRegistryId" TEXT NOT NULL DEFAULT '0',
CONSTRAINT "Application_gitSourceId_fkey" FOREIGN KEY ("gitSourceId") REFERENCES "GitSource" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Application_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Application_dockerRegistryId_fkey" FOREIGN KEY ("dockerRegistryId") REFERENCES "DockerRegistry" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Application" ("baseBuildImage", "baseDirectory", "baseImage", "branch", "buildCommand", "buildPack", "configHash", "createdAt", "denoMainFile", "denoOptions", "deploymentType", "destinationDockerId", "dockerComposeConfiguration", "dockerComposeFile", "dockerComposeFileLocation", "dockerFileLocation", "exposePort", "fqdn", "gitSourceId", "id", "installCommand", "name", "phpModules", "port", "projectId", "publishDirectory", "pythonModule", "pythonVariable", "pythonWSGI", "repository", "startCommand", "updatedAt") SELECT "baseBuildImage", "baseDirectory", "baseImage", "branch", "buildCommand", "buildPack", "configHash", "createdAt", "denoMainFile", "denoOptions", "deploymentType", "destinationDockerId", "dockerComposeConfiguration", "dockerComposeFile", "dockerComposeFileLocation", "dockerFileLocation", "exposePort", "fqdn", "gitSourceId", "id", "installCommand", "name", "phpModules", "port", "projectId", "publishDirectory", "pythonModule", "pythonVariable", "pythonWSGI", "repository", "startCommand", "updatedAt" FROM "Application";
DROP TABLE "Application";
ALTER TABLE "new_Application" RENAME TO "Application";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,30 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"DNSServers" TEXT,
"ipv4" TEXT,
"ipv6" TEXT,
"arch" TEXT,
"concurrentBuilds" INTEGER NOT NULL DEFAULT 1,
"applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false,
"proxyDefaultRedirect" TEXT,
"doNotTrack" BOOLEAN NOT NULL DEFAULT false,
"isAPIDebuggingEnabled" BOOLEAN DEFAULT false,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,60 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"DNSServers" TEXT,
"ipv4" TEXT,
"ipv6" TEXT,
"arch" TEXT,
"concurrentBuilds" INTEGER NOT NULL DEFAULT 1,
"applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false,
"proxyDefaultRedirect" TEXT,
"doNotTrack" BOOLEAN NOT NULL DEFAULT false,
"isAPIDebuggingEnabled" BOOLEAN NOT NULL DEFAULT false,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", coalesce("isAPIDebuggingEnabled", false) AS "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
CREATE TABLE "new_GlitchTip" (
"id" TEXT NOT NULL PRIMARY KEY,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"secretKeyBase" TEXT,
"defaultEmail" TEXT NOT NULL,
"defaultUsername" TEXT NOT NULL,
"defaultPassword" TEXT NOT NULL,
"defaultEmailFrom" TEXT NOT NULL DEFAULT 'glitchtip@domain.tdl',
"emailSmtpHost" TEXT DEFAULT 'domain.tdl',
"emailSmtpPort" INTEGER DEFAULT 25,
"emailSmtpUser" TEXT,
"emailSmtpPassword" TEXT,
"emailSmtpUseTls" BOOLEAN NOT NULL DEFAULT false,
"emailSmtpUseSsl" BOOLEAN NOT NULL DEFAULT false,
"emailBackend" TEXT,
"mailgunApiKey" TEXT,
"sendgridApiKey" TEXT,
"enableOpenUserRegistration" BOOLEAN NOT NULL DEFAULT true,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "GlitchTip_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_GlitchTip" ("createdAt", "defaultEmail", "defaultEmailFrom", "defaultPassword", "defaultUsername", "emailBackend", "emailSmtpHost", "emailSmtpPassword", "emailSmtpPort", "emailSmtpUseSsl", "emailSmtpUseTls", "emailSmtpUser", "enableOpenUserRegistration", "id", "mailgunApiKey", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "secretKeyBase", "sendgridApiKey", "serviceId", "updatedAt") SELECT "createdAt", "defaultEmail", "defaultEmailFrom", "defaultPassword", "defaultUsername", "emailBackend", "emailSmtpHost", "emailSmtpPassword", "emailSmtpPort", coalesce("emailSmtpUseSsl", false) AS "emailSmtpUseSsl", coalesce("emailSmtpUseTls", false) AS "emailSmtpUseTls", "emailSmtpUser", "enableOpenUserRegistration", "id", "mailgunApiKey", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "secretKeyBase", "sendgridApiKey", "serviceId", "updatedAt" FROM "GlitchTip";
DROP TABLE "GlitchTip";
ALTER TABLE "new_GlitchTip" RENAME TO "GlitchTip";
CREATE UNIQUE INDEX "GlitchTip_serviceId_key" ON "GlitchTip"("serviceId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Setting" ADD COLUMN "sentryDSN" TEXT;

View File

@@ -0,0 +1,31 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"DNSServers" TEXT NOT NULL DEFAULT '1.1.1.1,8.8.8.8',
"ipv4" TEXT,
"ipv6" TEXT,
"arch" TEXT,
"concurrentBuilds" INTEGER NOT NULL DEFAULT 1,
"applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false,
"proxyDefaultRedirect" TEXT,
"doNotTrack" BOOLEAN NOT NULL DEFAULT false,
"sentryDSN" TEXT,
"isAPIDebuggingEnabled" BOOLEAN NOT NULL DEFAULT false,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT true,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "sentryDSN", "updatedAt") SELECT coalesce("DNSServers", '1.1.1.1,8.8.8.8') AS "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "sentryDSN", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,33 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"DNSServers" TEXT NOT NULL DEFAULT '1.1.1.1,8.8.8.8',
"ipv4" TEXT,
"ipv6" TEXT,
"arch" TEXT,
"concurrentBuilds" INTEGER NOT NULL DEFAULT 1,
"applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false,
"numberOfDockerImagesKeptLocally" INTEGER NOT NULL DEFAULT 3,
"proxyDefaultRedirect" TEXT,
"doNotTrack" BOOLEAN NOT NULL DEFAULT false,
"sentryDSN" TEXT,
"previewSeparator" TEXT NOT NULL DEFAULT '.',
"isAPIDebuggingEnabled" BOOLEAN NOT NULL DEFAULT false,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT true,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "numberOfDockerImagesKeptLocally", "proxyDefaultRedirect", "sentryDSN", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "numberOfDockerImagesKeptLocally", "proxyDefaultRedirect", "sentryDSN", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "gitCommitHash" TEXT;

View File

@@ -0,0 +1,66 @@
/*
Warnings:
- You are about to drop the column `isSystemWide` on the `DockerRegistry` table. All the data in the column will be lost.
*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_DockerRegistry" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"url" TEXT NOT NULL,
"username" TEXT,
"password" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"teamId" TEXT,
CONSTRAINT "DockerRegistry_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_DockerRegistry" ("createdAt", "id", "name", "password", "teamId", "updatedAt", "url", "username") SELECT "createdAt", "id", "name", "password", "teamId", "updatedAt", "url", "username" FROM "DockerRegistry";
DROP TABLE "DockerRegistry";
ALTER TABLE "new_DockerRegistry" RENAME TO "DockerRegistry";
CREATE TABLE "new_Application" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"fqdn" TEXT,
"repository" TEXT,
"configHash" TEXT,
"branch" TEXT,
"buildPack" TEXT,
"projectId" INTEGER,
"port" INTEGER,
"exposePort" INTEGER,
"installCommand" TEXT,
"buildCommand" TEXT,
"startCommand" TEXT,
"baseDirectory" TEXT,
"publishDirectory" TEXT,
"deploymentType" TEXT,
"phpModules" TEXT,
"pythonWSGI" TEXT,
"pythonModule" TEXT,
"pythonVariable" TEXT,
"dockerFileLocation" TEXT,
"denoMainFile" TEXT,
"denoOptions" TEXT,
"dockerComposeFile" TEXT,
"dockerComposeFileLocation" TEXT,
"dockerComposeConfiguration" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"destinationDockerId" TEXT,
"gitSourceId" TEXT,
"gitCommitHash" TEXT,
"baseImage" TEXT,
"baseBuildImage" TEXT,
"dockerRegistryId" TEXT,
CONSTRAINT "Application_gitSourceId_fkey" FOREIGN KEY ("gitSourceId") REFERENCES "GitSource" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Application_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Application_dockerRegistryId_fkey" FOREIGN KEY ("dockerRegistryId") REFERENCES "DockerRegistry" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Application" ("baseBuildImage", "baseDirectory", "baseImage", "branch", "buildCommand", "buildPack", "configHash", "createdAt", "denoMainFile", "denoOptions", "deploymentType", "destinationDockerId", "dockerComposeConfiguration", "dockerComposeFile", "dockerComposeFileLocation", "dockerFileLocation", "dockerRegistryId", "exposePort", "fqdn", "gitCommitHash", "gitSourceId", "id", "installCommand", "name", "phpModules", "port", "projectId", "publishDirectory", "pythonModule", "pythonVariable", "pythonWSGI", "repository", "startCommand", "updatedAt") SELECT "baseBuildImage", "baseDirectory", "baseImage", "branch", "buildCommand", "buildPack", "configHash", "createdAt", "denoMainFile", "denoOptions", "deploymentType", "destinationDockerId", "dockerComposeConfiguration", "dockerComposeFile", "dockerComposeFileLocation", "dockerFileLocation", "dockerRegistryId", "exposePort", "fqdn", "gitCommitHash", "gitSourceId", "id", "installCommand", "name", "phpModules", "port", "projectId", "publishDirectory", "pythonModule", "pythonVariable", "pythonWSGI", "repository", "startCommand", "updatedAt" FROM "Application";
DROP TABLE "Application";
ALTER TABLE "new_Application" RENAME TO "Application";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "simpleDockerfile" TEXT;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "dockerRegistryImageName" TEXT;

View File

@@ -19,27 +19,29 @@ model Certificate {
}
model Setting {
id String @id @default(cuid())
fqdn String? @unique
isAPIDebuggingEnabled Boolean? @default(false)
isRegistrationEnabled Boolean @default(false)
dualCerts Boolean @default(false)
minPort Int @default(9000)
maxPort Int @default(9100)
proxyPassword String
proxyUser String
proxyHash String?
proxyDefaultRedirect String?
isAutoUpdateEnabled Boolean @default(false)
isDNSCheckEnabled Boolean @default(true)
DNSServers String?
isTraefikUsed Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipv4 String?
ipv6 String?
arch String?
concurrentBuilds Int @default(1)
id String @id @default(cuid())
fqdn String? @unique
dualCerts Boolean @default(false)
minPort Int @default(9000)
maxPort Int @default(9100)
DNSServers String @default("1.1.1.1,8.8.8.8")
ipv4 String?
ipv6 String?
arch String?
concurrentBuilds Int @default(1)
applicationStoragePathMigrationFinished Boolean @default(false)
numberOfDockerImagesKeptLocally Int @default(3)
proxyDefaultRedirect String?
doNotTrack Boolean @default(false)
sentryDSN String?
previewSeparator String @default(".")
isAPIDebuggingEnabled Boolean @default(false)
isRegistrationEnabled Boolean @default(true)
isAutoUpdateEnabled Boolean @default(false)
isDNSCheckEnabled Boolean @default(true)
isTraefikUsed Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
@@ -82,6 +84,7 @@ model Team {
service Service[]
users User[]
certificate Certificate[]
dockerRegistry DockerRegistry[]
}
model TeamInvitation {
@@ -95,7 +98,7 @@ model TeamInvitation {
}
model Application {
id String @id @default(cuid())
id String @id @default(cuid())
name String
fqdn String?
repository String?
@@ -121,20 +124,26 @@ model Application {
dockerComposeFile String?
dockerComposeFileLocation String?
dockerComposeConfiguration String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
destinationDockerId String?
gitSourceId String?
gitCommitHash String?
baseImage String?
baseBuildImage String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
persistentStorage ApplicationPersistentStorage[]
settings ApplicationSettings?
secrets Secret[]
teams Team[]
connectedDatabase ApplicationConnectedDatabase?
previewApplication PreviewApplication[]
dockerRegistryId String?
dockerRegistryImageName String?
simpleDockerfile String?
persistentStorage ApplicationPersistentStorage[]
secrets Secret[]
teams Team[]
connectedDatabase ApplicationConnectedDatabase?
previewApplication PreviewApplication[]
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
dockerRegistry DockerRegistry? @relation(fields: [dockerRegistryId], references: [id])
}
model PreviewApplication {
@@ -186,6 +195,7 @@ model ApplicationPersistentStorage {
id String @id @default(cuid())
applicationId String
path String
oldPath Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])
@@ -294,6 +304,19 @@ model SshKey {
destinationDocker DestinationDocker[]
}
model DockerRegistry {
id String @id @default(cuid())
name String
url String
username String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
teamId String?
team Team? @relation(fields: [teamId], references: [id])
application Application[]
}
model GitSource {
id String @id @default(cuid())
name String
@@ -624,8 +647,8 @@ model GlitchTip {
emailSmtpPort Int? @default(25)
emailSmtpUser String?
emailSmtpPassword String?
emailSmtpUseTls Boolean? @default(false)
emailSmtpUseSsl Boolean? @default(false)
emailSmtpUseTls Boolean @default(false)
emailSmtpUseSsl Boolean @default(false)
emailBackend String?
mailgunApiKey String?
sendgridApiKey String?

View File

@@ -1,18 +1,8 @@
const dotEnvExtended = require('dotenv-extended');
dotEnvExtended.load();
const crypto = require('crypto');
const generator = require('generate-password');
const cuid = require('cuid');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
function generatePassword(length = 24) {
return generator.generate({
length,
numbers: true,
strict: true
});
}
const algorithm = 'aes-256-ctr';
async function main() {
@@ -21,11 +11,8 @@ async function main() {
if (!settingsFound) {
await prisma.setting.create({
data: {
isRegistrationEnabled: true,
proxyPassword: encrypt(generatePassword()),
proxyUser: cuid(),
id: '0',
arch: process.arch,
DNSServers: '1.1.1.1,8.8.8.8'
}
});
} else {
@@ -34,11 +21,11 @@ async function main() {
id: settingsFound.id
},
data: {
isTraefikUsed: true,
proxyHash: null
id: '0'
}
});
}
// Create local docker engine
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine: '/var/run/docker.sock' }
});
@@ -55,23 +42,18 @@ async function main() {
// Set auto-update based on env variable
const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true';
const settings = await prisma.setting.findFirst({});
if (settings) {
await prisma.setting.update({
where: {
id: settings.id
},
data: {
isAutoUpdateEnabled
}
});
}
await prisma.setting.update({
where: {
id: '0'
},
data: {
isAutoUpdateEnabled
}
});
// Create public github source
const github = await prisma.gitSource.findFirst({
where: { htmlUrl: 'https://github.com', forPublic: true }
});
const gitlab = await prisma.gitSource.findFirst({
where: { htmlUrl: 'https://gitlab.com', forPublic: true }
});
if (!github) {
await prisma.gitSource.create({
data: {
@@ -83,6 +65,10 @@ async function main() {
}
});
}
// Create public gitlab source
const gitlab = await prisma.gitSource.findFirst({
where: { htmlUrl: 'https://gitlab.com', forPublic: true }
});
if (!gitlab) {
await prisma.gitSource.create({
data: {

View File

@@ -9,7 +9,7 @@ import autoLoad from '@fastify/autoload';
import socketIO from 'fastify-socket.io'
import socketIOServer from './realtime'
import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, encrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common';
import { cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeCommand, generateDatabaseConfiguration, 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'
@@ -17,16 +17,15 @@ import yaml from 'js-yaml'
import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { checkContainer } from './lib/docker';
import { migrateServicesToNewTemplate } from './lib';
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
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_DATABASE_URL: string,
COOLIFY_SENTRY_DSN: string,
COOLIFY_IS_ON: string,
COOLIFY_WHITE_LABELED: string,
COOLIFY_WHITE_LABELED_ICON: string | null,
@@ -37,6 +36,7 @@ declare module 'fastify' {
const port = isDev ? 3001 : 3000;
const host = '0.0.0.0';
(async () => {
const settings = await prisma.setting.findFirst()
const fastify = Fastify({
@@ -58,10 +58,6 @@ const host = '0.0.0.0';
type: 'string',
default: 'file:../db/dev.db'
},
COOLIFY_SENTRY_DSN: {
type: 'string',
default: null
},
COOLIFY_IS_ON: {
type: 'string',
default: 'docker'
@@ -114,7 +110,6 @@ const host = '0.0.0.0';
origin: isDev ? "*" : ''
}
})
// To detect allowed origins
// fastify.addHook('onRequest', async (request, reply) => {
// console.log(request.headers.host)
@@ -142,7 +137,8 @@ const host = '0.0.0.0';
await socketIOServer(fastify)
console.log(`Coolify's API is listening on ${host}:${port}`);
migrateServicesToNewTemplate()
migrateServicesToNewTemplate();
await migrateApplicationPersistentStorage();
await initServer();
const graceful = new Graceful({ brees: [scheduler] });
@@ -181,7 +177,7 @@ const host = '0.0.0.0';
setInterval(async () => {
await migrateServicesToNewTemplate()
}, 60000)
}, isDev ? 10000 : 60000)
setInterval(async () => {
await copySSLCertificates();
@@ -206,14 +202,14 @@ async function getIPAddress() {
try {
const settings = await listSettings();
if (!settings.ipv4) {
console.log(`Getting public IPv4 address...`);
const ipv4 = await publicIpv4({ timeout: 2000 })
console.log(`Getting public IPv4 address...`);
await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } })
}
if (!settings.ipv6) {
console.log(`Getting public IPv6 address...`);
const ipv6 = await publicIpv6({ timeout: 2000 })
console.log(`Getting public IPv6 address...`);
await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } })
}
@@ -227,13 +223,13 @@ async function getTagsTemplates() {
const tags = await fs.readFile('./devTags.json', 'utf8')
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates)))
await fs.writeFile('./tags.json', tags)
console.log('Tags and templates loaded in dev mode...')
console.log('[004] Tags and templates loaded in dev mode...')
} else {
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text()
const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text()
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response)))
await fs.writeFile('/app/tags.json', tags)
console.log('Tags and templates loaded...')
console.log('[004] Tags and templates loaded...')
}
} catch (error) {
@@ -242,16 +238,44 @@ async function getTagsTemplates() {
}
}
async function initServer() {
const appId = process.env['COOLIFY_APP_ID'];
const settings = await prisma.setting.findUnique({ where: { id: '0' } })
try {
console.log(`Initializing server...`);
await asyncExecShell(`docker network create --attachable coolify`);
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,
// environment: isDev ? 'development' : 'production',
// release: version
// });
// console.log('[000] Sentry initialized...')
}
} catch (error) {
console.error(error)
}
try {
console.log(`[001] Initializing server...`);
await executeCommand({ command: `docker network create --attachable coolify` });
} catch (error) { }
try {
console.log(`[002] Cleanup stucked builds...`);
const isOlder = compareVersions('3.8.1', version);
if (isOlder === 1) {
await prisma.build.updateMany({ where: { status: { in: ['running', 'queued'] } }, data: { status: 'failed' } });
}
} 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 })
} catch (error) {
console.log(error)
}
}
async function getArch() {
@@ -299,14 +323,10 @@ async function autoUpdater() {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
);
await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` })
await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .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 coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` })
}
} else {
console.log('Updating (not really in dev mode).');
@@ -327,8 +347,8 @@ async function checkFluentBit() {
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true });
if (!found) {
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` });
await executeCommand({ command: `docker compose up -d fluent-bit` });
}
}
} catch (error) {
@@ -438,25 +458,25 @@ async function copySSLCertificates() {
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` })
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeCommand({ command: `scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/` })
await executeCommand({ sshCommand: true, shell: true, dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeCommand({ sshCommand: true, dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeCommand({ sshCommand: true, dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
await executeCommand({ command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`, shell: true })
await executeCommand({ command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeCommand({ command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
@@ -474,12 +494,13 @@ async function cleanupStorage() {
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
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 asyncExecShell(
`df -kPT /`
);
const output = await executeCommand({
command:
`df -kPT /`
});
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');

View File

@@ -3,8 +3,8 @@ import crypto from 'crypto';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common';
import { createDirectories, decrypt, defaultComposeConfiguration, executeDockerCmd, getDomain, prisma, decryptApplication } from '../lib/common';
import { copyBaseConfigurationFiles, makeLabelForSimpleDockerfile, makeLabelForStandaloneApplication, saveBuildLog, saveDockerRegistryCredentials, setDefaultConfiguration } from '../lib/buildPacks/common';
import { createDirectories, decrypt, defaultComposeConfiguration, getDomain, prisma, decryptApplication, isDev, pushToRegistry, executeCommand } from '../lib/common';
import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks';
@@ -37,57 +37,257 @@ import * as buildpacks from '../lib/buildPacks';
for (const queueBuild of queuedBuilds) {
actions.push(async () => {
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { dockerRegistry: true, destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
let { id: buildId, type, gitSourceId, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild
application = decryptApplication(application)
if (!gitSourceId && application.simpleDockerfile) {
const {
id: applicationId,
destinationDocker,
destinationDockerId,
secrets,
port,
persistentStorage,
exposePort,
simpleDockerfile,
dockerRegistry
} = application
const { workdir } = await createDirectories({ repository: applicationId, buildId });
try {
if (queueBuild.status === 'running') {
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
}
const volumes =
persistentStorage?.map((storage) => {
if (storage.oldPath) {
return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app', '')}:${storage.path}`;
}
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || [];
if (destinationDockerId) {
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
try {
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}`
})
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` })
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` })
}
}
}
} catch (error) {
//
}
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile);
if (dockerRegistry) {
const { url, username, password } = dockerRegistry
await saveDockerRegistryCredentials({ url, username, password, workdir })
}
const labels = makeLabelForSimpleDockerfile({
applicationId,
type,
port: exposePort ? `${exposePort}:${port}` : port,
});
try {
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[applicationId]: {
build: {
context: workdir,
},
image: `${applicationId}:${buildId}`,
container_name: applicationId,
volumes,
labels,
env_file: envFound ? [`${workdir}/.env`] : [],
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(destinationDocker.network),
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeCommand({ debug: true, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
throw new Error(error);
}
}
} catch (error) {
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
if (error !== 1) {
await saveBuildLog({ line: error, buildId, applicationId: application.id });
}
if (error instanceof Error) {
await saveBuildLog({ line: error.message, buildId, applicationId: application.id });
}
await fs.rm(workdir, { recursive: true, force: true });
return;
}
try {
if (application.dockerRegistryImageName) {
const customTag = application.dockerRegistryImageName.split(':')[1] || buildId;
const imageName = application.dockerRegistryImageName.split(':')[0];
await saveBuildLog({ line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, buildId, applicationId: application.id });
await pushToRegistry(application, workdir, buildId, imageName, customTag)
await saveBuildLog({ line: "Success", buildId, applicationId: application.id });
}
} catch (error) {
if (error.stdout) {
await saveBuildLog({ line: error.stdout, buildId, applicationId });
}
if (error.stderr) {
await saveBuildLog({ line: error.stderr, buildId, applicationId });
}
} finally {
await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
}
return;
}
const originalApplicationId = application.id
const {
id: applicationId,
name,
destinationDocker,
destinationDockerId,
gitSource,
configHash,
fqdn,
projectId,
secrets,
phpModules,
settings,
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
exposePort,
baseImage,
baseBuildImage,
deploymentType,
gitCommitHash,
dockerRegistry
} = application
let {
branch,
repository,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
dockerFileLocation,
dockerComposeFileLocation,
dockerComposeConfiguration,
denoMainFile
} = application
let imageId = applicationId;
let domain = getDomain(fqdn);
let location = null;
let tag = null;
let customTag = null;
let imageName = null;
let imageFoundLocally = false;
let imageFoundRemotely = false;
if (pullmergeRequestId) {
const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } })
if (previewApplications.length > 0) {
previewApplicationId = previewApplications[0].id
}
// Previews, we need to get the source branch and set subdomain
branch = sourceBranch;
domain = `${pullmergeRequestId}.${domain}`;
imageId = `${applicationId}-${pullmergeRequestId}`;
repository = sourceRepository || repository;
}
const usableApplicationId = previewApplicationId || originalApplicationId
const { workdir, repodir } = await createDirectories({ repository, buildId });
try {
if (queueBuild.status === 'running') {
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
}
const {
id: applicationId,
name,
destinationDocker,
destinationDockerId,
gitSource,
configHash,
fqdn,
projectId,
secrets,
phpModules,
settings,
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
exposePort,
baseImage,
baseBuildImage,
deploymentType,
} = application
let {
branch,
repository,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
dockerFileLocation,
dockerComposeConfiguration,
denoMainFile
} = application
const currentHash = crypto
.createHash('sha256')
.update(
@@ -113,20 +313,21 @@ import * as buildpacks from '../lib/buildPacks';
)
.digest('hex');
const { debug } = settings;
let imageId = applicationId;
let domain = getDomain(fqdn);
if (!debug) {
await saveBuildLog({
line: `Debug logging is disabled. Enable it above if necessary!`,
buildId,
applicationId
});
}
const volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
}${storage.path}`;
if (storage.oldPath) {
return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app', '')}:${storage.path}`;
}
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || [];
// Previews, we need to get the source branch and set subdomain
if (pullmergeRequestId) {
branch = sourceBranch;
domain = `${pullmergeRequestId}.${domain}`;
imageId = `${applicationId}-${pullmergeRequestId}`;
repository = sourceRepository || repository;
}
try {
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration)
@@ -139,7 +340,7 @@ import * as buildpacks from '../lib/buildPacks';
}
if (destinationType === 'docker') {
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
const { workdir, repodir } = await createDirectories({ repository, buildId });
const configuration = await setDefaultConfiguration(application);
buildPack = configuration.buildPack;
@@ -150,6 +351,7 @@ import * as buildpacks from '../lib/buildPacks';
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory || '';
dockerFileLocation = configuration.dockerFileLocation;
dockerComposeFileLocation = configuration.dockerComposeFileLocation;
denoMainFile = configuration.denoMainFile;
const commit = await importers[gitSource.type]({
applicationId,
@@ -159,6 +361,8 @@ import * as buildpacks from '../lib/buildPacks';
githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id,
customPort: gitSource.customPort,
gitCommitHash,
configuration,
repository,
branch,
buildId,
@@ -172,10 +376,21 @@ import * as buildpacks from '../lib/buildPacks';
if (!commit) {
throw new Error('No commit found?');
}
let tag = commit.slice(0, 7);
tag = commit.slice(0, 7);
if (pullmergeRequestId) {
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
}
if (application.dockerRegistryImageName) {
imageName = application.dockerRegistryImageName.split(':')[0]
customTag = application.dockerRegistryImageName.split(':')[1] || tag
} else {
customTag = tag
imageName = applicationId;
}
if (pullmergeRequestId) {
customTag = `${customTag}-${pullmergeRequestId}`;
}
try {
await prisma.build.update({ where: { id: buildId }, data: { commit } });
@@ -185,7 +400,7 @@ import * as buildpacks from '../lib/buildPacks';
if (configHash !== currentHash) {
deployNeeded = true;
if (configHash) {
await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
await saveBuildLog({ line: 'Configuration changed', buildId, applicationId });
}
} else {
deployNeeded = false;
@@ -194,16 +409,33 @@ import * as buildpacks from '../lib/buildPacks';
deployNeeded = true;
}
let imageFound = false;
try {
await executeDockerCmd({
await executeCommand({
dockerId: destinationDocker.id,
command: `docker image inspect ${applicationId}:${tag}`
})
imageFound = true;
imageFoundLocally = true;
} catch (error) {
//
}
if (dockerRegistry) {
const { url, username, password } = dockerRegistry
location = await saveDockerRegistryCredentials({ url, username, password, workdir })
}
try {
await executeCommand({
dockerId: destinationDocker.id,
command: `docker ${location ? `--config ${location}` : ''} pull ${imageName}:${customTag}`
})
imageFoundRemotely = true;
} catch (error) {
//
}
let imageFound = `${applicationId}:${tag}`
if (imageFoundRemotely) {
imageFound = `${imageName}:${customTag}`
}
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
const labels = makeLabelForStandaloneApplication({
applicationId,
@@ -224,7 +456,7 @@ import * as buildpacks from '../lib/buildPacks';
publishDirectory
});
if (forceRebuild) deployNeeded = true
if (!imageFound || deployNeeded) {
if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) {
if (buildpacks[buildPack])
await buildpacks[buildPack]({
dockerId: destinationDocker.id,
@@ -258,6 +490,7 @@ import * as buildpacks from '../lib/buildPacks';
pythonVariable,
dockerFileLocation,
dockerComposeConfiguration,
dockerComposeFileLocation,
denoMainFile,
denoOptions,
baseImage,
@@ -269,26 +502,37 @@ import * as buildpacks from '../lib/buildPacks';
throw new Error(`Build pack ${buildPack} not found.`);
}
} else {
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
if (imageFoundRemotely || deployNeeded) {
await saveBuildLog({ line: `Container image ${imageFound} found in Docker Registry - reuising it`, buildId, applicationId });
} else {
if (imageFoundLocally || deployNeeded) {
await saveBuildLog({ line: `Container image ${imageFound} found locally - reuising it`, buildId, applicationId });
}
}
}
if (buildPack === 'compose') {
try {
await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}`
})
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` })
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` })
}
}
}
} catch (error) {
//
}
try {
await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
console.log({ debug })
await executeCommand({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
await prisma.application.update({
where: { id: applicationId },
@@ -310,14 +554,19 @@ import * as buildpacks from '../lib/buildPacks';
} else {
try {
await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}`
})
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` })
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` })
}
}
}
} catch (error) {
//
}
@@ -341,6 +590,10 @@ import * as buildpacks from '../lib/buildPacks';
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
if (dockerRegistry) {
const { url, username, password } = dockerRegistry
await saveDockerRegistryCredentials({ url, username, password, workdir })
}
let envFound = false;
try {
@@ -349,7 +602,6 @@ import * as buildpacks from '../lib/buildPacks';
//
}
try {
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
@@ -361,7 +613,7 @@ import * as buildpacks from '../lib/buildPacks';
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
image: imageFound,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
@@ -380,8 +632,8 @@ import * as buildpacks from '../lib/buildPacks';
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
await executeCommand({ debug, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
@@ -395,16 +647,14 @@ import * as buildpacks from '../lib/buildPacks';
}
throw new Error(error);
}
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
}
}
}
catch (error) {
} catch (error) {
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
@@ -417,6 +667,29 @@ import * as buildpacks from '../lib/buildPacks';
if (error !== 1) {
await saveBuildLog({ line: error, buildId, applicationId: application.id });
}
if (error instanceof Error) {
await saveBuildLog({ line: error.message, buildId, applicationId: application.id });
}
await fs.rm(workdir, { recursive: true, force: true });
return;
}
try {
if (application.dockerRegistryImageName && (!imageFoundRemotely || forceRebuild)) {
await saveBuildLog({ line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, buildId, applicationId: application.id });
await pushToRegistry(application, workdir, tag, imageName, customTag)
await saveBuildLog({ line: "Success", buildId, applicationId: application.id });
}
} catch (error) {
if (error.stdout) {
await saveBuildLog({ line: error.stdout, buildId, applicationId });
}
if (error.stderr) {
await saveBuildLog({ line: error.stderr, buildId, applicationId });
}
} finally {
await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
}
});
}

View File

@@ -1,7 +1,33 @@
import cuid from "cuid";
import { decrypt, encrypt, fixType, generatePassword, prisma } from "./lib/common";
import { decrypt, encrypt, fixType, generatePassword, generateToken, prisma } from "./lib/common";
import { getTemplates } from "./lib/services";
export async function migrateApplicationPersistentStorage() {
const settings = await prisma.setting.findFirst()
if (settings) {
const { id: settingsId, applicationStoragePathMigrationFinished } = settings
try {
if (!applicationStoragePathMigrationFinished) {
const applications = await prisma.application.findMany({ include: { persistentStorage: true } });
for (const application of applications) {
if (application.persistentStorage && application.persistentStorage.length > 0 && application?.buildPack !== 'docker') {
for (const storage of application.persistentStorage) {
let { id, path } = storage
if (!path.startsWith('/app')) {
path = `/app${path}`
await prisma.applicationPersistentStorage.update({ where: { id }, data: { path, oldPath: true } })
}
}
}
}
}
} catch (error) {
console.log(error)
} finally {
await prisma.setting.update({ where: { id: settingsId }, data: { applicationStoragePathMigrationFinished: true } })
}
}
}
export async function migrateServicesToNewTemplate() {
// This function migrates old hardcoded services to the new template based services
try {
@@ -57,39 +83,42 @@ export async function migrateServicesToNewTemplate() {
} catch (error) {
console.log(error)
}
if (template.variables.length > 0) {
if (template.variables) {
if (template.variables.length > 0) {
for (const variable of template.variables) {
const { defaultValue } = variable;
const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
if (variable.defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) {
variable.value = cuid();
} else if (variable.defaultValue.startsWith('$$generate_token')) {
variable.value = generateToken()
} else {
variable.value = variable.defaultValue || '';
}
}
}
for (const variable of template.variables) {
const { defaultValue } = variable;
const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
if (variable.defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) {
variable.value = cuid();
} else {
variable.value = variable.defaultValue || '';
}
}
}
for (const variable of template.variables) {
if (variable.id.startsWith('$$secret_')) {
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSecret.create({
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
})
}
if (variable.id.startsWith('$$secret_')) {
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSecret.create({
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
})
}
}
if (variable.id.startsWith('$$config_')) {
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSetting.create({
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
})
}
if (variable.id.startsWith('$$config_')) {
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSetting.create({
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
})
}
}
}
}
@@ -221,7 +250,7 @@ async function hasura(service: any, template: any) {
const { id } = service
const secrets = [
`HASURA_GRAPHQL_ADMIN_PASSWORD@@@${graphQLAdminPassword}`,
`HASURA_GRAPHQL_ADMIN_SECRET@@@${graphQLAdminPassword}`,
`HASURA_GRAPHQL_METADATA_DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
]
@@ -238,7 +267,6 @@ async function hasura(service: any, template: any) {
async function umami(service: any, template: any) {
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, umamiAdminPassword, hashSalt } = service.umami
const { id } = service
const secrets = [
`HASH_SALT@@@${hashSalt}`,
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
@@ -439,7 +467,6 @@ async function plausibleAnalytics(service: any, template: any) {
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { plausibleAnalytics: { disconnect: true } } })
}
async function migrateSettings(settings: any[], service: any, template: any) {
for (const setting of settings) {
try {
@@ -457,9 +484,9 @@ async function migrateSettings(settings: any[], service: any, template: any) {
variableName = `$$config_${name.toLowerCase()}`
}
// console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name, 'variableName: ', variableName)
await prisma.serviceSetting.findFirst({ where: { name: minio, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name: minio, value, variableName, service: { connect: { id: service.id } } } })
} catch(error) {
} catch (error) {
console.log(error)
}
}
@@ -474,7 +501,7 @@ async function migrateSecrets(secrets: any[], service: any) {
}
// console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name)
await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } })
} catch(error) {
} catch (error) {
console.log(error)
}
}
@@ -500,4 +527,4 @@ async function createVolumes(service: any, template: any) {
// console.log('Creating volume', volumeName, path, containerId, 'for service', service.id, ', service name:', service.name)
await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: service.id } }) || await prisma.servicePersistentStorage.create({ data: { volumeName, path, containerId, predefined: true, service: { connect: { id: service.id } } } })
}
}
}

View File

@@ -1,4 +1,4 @@
import { base64Encode, encrypt, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
import { base64Encode, decrypt, encrypt, executeCommand, generateTimestamp, getDomain, isARM, isDev, prisma, version } from "../common";
import { promises as fs } from 'fs';
import { day } from "../dayjs";
@@ -52,6 +52,14 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
{
value: 'webdevops/apache:alpine',
label: 'webdevops/apache:alpine'
},
{
value: 'nginx:alpine',
label: 'nginx:alpine'
},
{
value: 'httpd:alpine',
label: 'httpd:alpine (Apache)'
}
];
const rustVersions = [
@@ -214,8 +222,20 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
label: 'webdevops/php-apache:7.1-alpine'
},
{
value: 'webdevops/php-nginx:7.1-alpine',
label: 'webdevops/php-nginx:7.1-alpine'
value: 'php:8.1-fpm',
label: 'php:8.1-fpm'
},
{
value: 'php:8.0-fpm',
label: 'php:8.0-fpm'
},
{
value: 'php:8.1-fpm-alpine',
label: 'php:8.1-fpm-alpine'
},
{
value: 'php:8.0-fpm-alpine',
label: 'php:8.0-fpm-alpine'
}
];
const pythonVersions = [
@@ -306,8 +326,8 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
};
if (nodeBased.includes(buildPack)) {
if (deploymentType === 'static') {
payload.baseImage = 'webdevops/nginx:alpine';
payload.baseImages = staticVersions;
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
payload.baseImages = isARM(process.arch) ? staticVersions.filter((version) => !version.value.includes('webdevops')) : staticVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
} else {
@@ -318,8 +338,8 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
}
}
if (staticApps.includes(buildPack)) {
payload.baseImage = 'webdevops/nginx:alpine';
payload.baseImages = staticVersions;
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
payload.baseImages = isARM(process.arch) ? staticVersions.filter((version) => !version.value.includes('webdevops')) : staticVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
}
@@ -337,12 +357,12 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
payload.baseImage = 'denoland/deno:latest';
}
if (buildPack === 'php') {
payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseImages = phpVersions;
payload.baseImage = isARM(process.arch) ? 'php:8.1-fpm-alpine' : 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM(process.arch) ? phpVersions.filter((version) => !version.value.includes('webdevops')) : phpVersions
}
if (buildPack === 'laravel') {
payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseImages = phpVersions;
payload.baseImage = isARM(process.arch) ? 'php:8.1-fpm-alpine' : 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM(process.arch) ? phpVersions.filter((version) => !version.value.includes('webdevops')) : phpVersions
payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions;
}
@@ -363,6 +383,7 @@ export const setDefaultConfiguration = async (data: any) => {
publishDirectory,
baseDirectory,
dockerFileLocation,
dockerComposeFileLocation,
denoMainFile
} = data;
//@ts-ignore
@@ -392,6 +413,12 @@ export const setDefaultConfiguration = async (data: any) => {
} else {
dockerFileLocation = '/Dockerfile';
}
if (dockerComposeFileLocation) {
if (!dockerComposeFileLocation.startsWith('/')) dockerComposeFileLocation = `/${dockerComposeFileLocation}`;
if (dockerComposeFileLocation.endsWith('/')) dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1);
} else {
dockerComposeFileLocation = '/Dockerfile';
}
if (!denoMainFile) {
denoMainFile = 'main.ts';
}
@@ -405,6 +432,7 @@ export const setDefaultConfiguration = async (data: any) => {
publishDirectory,
baseDirectory,
dockerFileLocation,
dockerComposeFileLocation,
denoMainFile
};
};
@@ -461,8 +489,16 @@ export const saveBuildLog = async ({
buildId: string;
applicationId: string;
}): Promise<any> => {
if (buildId === 'undefined' || buildId === 'null' || !buildId) return;
if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return;
const { default: got } = await import('got')
if (typeof line === 'object' && line) {
if (line.shortMessage) {
line = line.shortMessage + '\n' + line.stderr;
} else {
line = JSON.stringify(line);
}
}
if (line && typeof line === 'string' && line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
@@ -558,6 +594,7 @@ export async function copyBaseConfigurationFiles(
`
);
}
// TODO: Add more configuration files for other buildpacks, like apache2, etc.
} catch (error) {
throw new Error(error);
}
@@ -571,6 +608,29 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma
);
}
export async function saveDockerRegistryCredentials({ url, username, password, workdir }) {
if (!username || !password) {
return null
}
let decryptedPassword = decrypt(password);
const location = `${workdir}/.docker`;
try {
await fs.mkdir(`${workdir}/.docker`);
} catch (error) {
console.log(error);
}
const payload = JSON.stringify({
"auths": {
[url]: {
"auth": Buffer.from(`${username}:${decryptedPassword}`).toString('base64')
}
}
})
await fs.writeFile(`${location}/config.json`, payload)
return location
}
export async function buildImage({
applicationId,
tag,
@@ -583,33 +643,36 @@ export async function buildImage({
commit
}) {
if (isCache) {
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
await saveBuildLog({ line: `Building cache image...`, buildId, applicationId });
} else {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
}
if (!debug) {
await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the features tab.`,
buildId,
applicationId
});
await saveBuildLog({ line: `Building production image...`, buildId, applicationId });
}
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` })
let location = null
const { dockerRegistry } = await prisma.application.findUnique({ where: { id: applicationId }, select: { dockerRegistry: true } })
if (dockerRegistry) {
const { url, username, password } = dockerRegistry
location = await saveDockerRegistryCredentials({ url, username, password, workdir })
}
await executeCommand({ stream: true, debug, buildId, applicationId, dockerId, command: `docker ${location ? `--config ${location}` : ''} build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` })
const { status } = await prisma.build.findUnique({ where: { id: buildId } })
if (status === 'canceled') {
throw new Error('Deployment canceled.')
}
if (isCache) {
await saveBuildLog({ line: `Building cache image successful.`, buildId, applicationId });
} else {
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
throw new Error('Canceled.')
}
}
export function makeLabelForSimpleDockerfile({ applicationId, port, type }) {
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.applicationId=${applicationId}`,
`coolify.type=standalone-application`
];
}
export function makeLabelForStandaloneApplication({
applicationId,
fqdn,
@@ -638,6 +701,7 @@ export function makeLabelForStandaloneApplication({
`coolify.version=${version}`,
`coolify.applicationId=${applicationId}`,
`coolify.type=standalone-application`,
`coolify.name=${name}`,
`coolify.configuration=${base64Encode(
JSON.stringify({
applicationId,

View File

@@ -1,100 +1,126 @@
import { promises as fs } from 'fs';
import { defaultComposeConfiguration, executeDockerCmd } from '../common';
import { buildImage, saveBuildLog } from './common';
import { defaultComposeConfiguration, executeCommand } from '../common';
import { saveBuildLog } from './common';
import yaml from 'js-yaml';
export default async function (data) {
let {
applicationId,
debug,
buildId,
dockerId,
network,
volumes,
labels,
workdir,
baseDirectory,
secrets,
pullmergeRequestId,
port,
dockerComposeConfiguration
} = data
const fileYml = `${workdir}${baseDirectory}/docker-compose.yml`;
const fileYaml = `${workdir}${baseDirectory}/docker-compose.yaml`;
let dockerComposeRaw = null;
let isYml = false;
try {
dockerComposeRaw = await fs.readFile(`${fileYml}`, 'utf8')
isYml = true
} catch (error) { }
try {
dockerComposeRaw = await fs.readFile(`${fileYaml}`, 'utf8')
} catch (error) { }
let {
applicationId,
debug,
buildId,
dockerId,
network,
volumes,
labels,
workdir,
baseDirectory,
secrets,
pullmergeRequestId,
dockerComposeConfiguration,
dockerComposeFileLocation
} = data;
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8');
const dockerComposeYaml = yaml.load(dockerComposeRaw);
if (!dockerComposeYaml.services) {
throw 'No Services found in docker-compose file.';
}
const envs = [];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret);
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
const composeVolumes = [];
if (volumes.length > 0) {
for (const volume of volumes) {
let [v, path] = volume.split(':');
composeVolumes[v] = {
name: v
};
}
}
if (!dockerComposeRaw) {
throw ('docker-compose.yml or docker-compose.yaml are not found!');
}
const dockerComposeYaml = yaml.load(dockerComposeRaw)
if (!dockerComposeYaml.services) {
throw 'No Services found in docker-compose file.'
}
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
let networks = {}
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}`
value['env_file'] = envFound ? [`${workdir}/.env`] : []
value['labels'] = labels
value['volumes'] = volumes
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]
dockerComposeYaml.services[key] = { ...dockerComposeYaml.services[key], restart: defaultComposeConfiguration(network).restart, deploy: defaultComposeConfiguration(network).deploy }
}
dockerComposeYaml['volumes'] = Object.assign({ ...dockerComposeYaml['volumes'] }, ...composeVolumes)
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } })
await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml));
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` })
await saveBuildLog({ line: 'Pulling images from Compose file.', buildId, applicationId });
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain` })
await saveBuildLog({ line: 'Building images from Compose file.', buildId, applicationId });
let networks = {};
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}`;
value['env_file'] = envFound ? [`${workdir}/.env`] : [];
value['labels'] = labels;
// TODO: If we support separated volume for each service, we need to add it here
if (value['volumes']?.length > 0) {
value['volumes'] = value['volumes'].map((volume) => {
let [v, path, permission] = volume.split(':');
if (!path) {
path = v;
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
composeVolumes[v] = {
name: v
};
return `${v}:${path}${permission ? ':' + permission : ''}`;
});
}
if (volumes.length > 0) {
for (const volume of volumes) {
value['volumes'].push(volume);
}
}
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];
dockerComposeYaml.services[key] = {
...dockerComposeYaml.services[key],
restart: defaultComposeConfiguration(network).restart,
deploy: defaultComposeConfiguration(network).deploy
};
}
if (Object.keys(composeVolumes).length > 0) {
dockerComposeYaml['volumes'] = { ...composeVolumes };
}
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } });
await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml));
await executeCommand({
debug,
buildId,
applicationId,
dockerId,
command: `docker compose --project-directory ${workdir} pull`
});
await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId });
await executeCommand({
debug,
buildId,
applicationId,
dockerId,
command: `docker compose --project-directory ${workdir} build --progress plain`
});
await saveBuildLog({ line: 'Building images from Compose file...', buildId, applicationId });
}

View File

@@ -20,7 +20,11 @@ export default async function (data) {
.toString()
.trim()
.split('\n');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `LABEL coolify.buildId=${buildId}`);
}
});
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
@@ -28,11 +32,9 @@ export default async function (data) {
(pullmergeRequestId && secret.isPRMRSecret) ||
(!pullmergeRequestId && !secret.isPRMRSecret)
) {
Dockerfile.unshift(`ARG ${secret.name}=${secret.value}`);
Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `ARG ${secret.name}`);
Dockerfile.splice(index + 1, 0, `ARG ${secret.name}=${secret.value}`);
}
});
}

View File

@@ -1,17 +1,16 @@
import { executeDockerCmd, prisma } from "../common"
import { executeCommand } from "../common"
import { saveBuildLog } from "./common";
export default async function (data: any): Promise<void> {
const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory, baseImage } = data
try {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
await executeDockerCmd({
await saveBuildLog({ line: `Building production image...`, buildId, applicationId });
await executeCommand({
buildId,
debug,
dockerId,
command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder ${baseImage}`
})
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
} catch (error) {
throw error;
}

View File

@@ -1,6 +1,6 @@
import { promises as fs } from 'fs';
import TOML from '@iarna/toml';
import { asyncExecShell } from '../common';
import { executeCommand } from '../common';
import { buildCacheImageWithCargo, buildImage } from './common';
const createDockerfile = async (data, image, name): Promise<void> => {
@@ -28,7 +28,7 @@ const createDockerfile = async (data, image, name): Promise<void> => {
export default async function (data) {
try {
const { workdir, baseImage, baseBuildImage } = data;
const { stdout: cargoToml } = await asyncExecShell(`cat ${workdir}/Cargo.toml`);
const { stdout: cargoToml } = await executeCommand({ command: `cat ${workdir}/Cargo.toml` });
const parsedToml: any = TOML.parse(cargoToml);
const name = parsedToml.package.name;
await buildCacheImageWithCargo(data, baseBuildImage);

View File

@@ -18,7 +18,11 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
if (baseImage?.includes('httpd')) {
Dockerfile.push('WORKDIR /usr/local/apache2/htdocs/');
} else {
Dockerfile.push('WORKDIR /app');
}
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {

View File

@@ -8,18 +8,20 @@ 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 sshConfig from 'ssh-config';
import jsonwebtoken from 'jsonwebtoken';
import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs';
import { saveBuildLog } from './buildPacks/common';
import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common';
import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa';
export const version = '3.11.2';
export const version = '3.12.0';
export const isDev = process.env.NODE_ENV === 'development';
export const sentryDSN = 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
const algorithm = 'aes-256-ctr';
const customConfig: Config = {
dictionaries: [adjectives, colors, animals],
@@ -28,9 +30,6 @@ const customConfig: Config = {
length: 3
};
export const defaultProxyImage = `coolify-haproxy-alpine:latest`;
export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`;
export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`;
export const defaultTraefikImage = `traefik:v2.8`;
export function getAPIUrl() {
if (process.env.GITPOD_WORKSPACE_URL) {
@@ -65,7 +64,6 @@ const otherTraefikEndpoint = isDev
: 'http://coolify:3000/webhooks/traefik/other.json';
export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
export const asyncExecShell = util.promisify(exec);
export const asyncExecShellStream = async ({
debug,
buildId,
@@ -305,7 +303,7 @@ export async function isDomainConfigured({
export async function getContainerUsage(dockerId: string, container: string): Promise<any> {
try {
const { stdout } = await executeDockerCmd({
const { stdout } = await executeCommand({
dockerId,
command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"`
});
@@ -510,36 +508,13 @@ export async function createRemoteEngineConfiguration(id: string) {
remoteUser
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
// Needed for remote docker compose
// const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(
// `ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`
// );
// if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
// try {
// await fs.stat(`/tmp/coolify-ssh-agent.pid`);
// await fs.rm(`/tmp/coolify-ssh-agent.pid`);
// } catch (error) { }
// await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`);
// }
// await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`);
// const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(
// `ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`
// );
// if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
// try {
// await asyncExecShell(
// `SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`
// );
// } catch (error) { }
// }
const config = sshConfig.parse('');
const Host = `${remoteIpAddress}-remote`
try {
await asyncExecShell(`ssh-keygen -R ${Host}`);
await asyncExecShell(`ssh-keygen -R ${remoteIpAddress}`);
await asyncExecShell(`ssh-keygen -R localhost:${localPort}`);
await executeCommand({ command: `ssh-keygen -R ${Host}` });
await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
} catch (error) { }
@@ -568,56 +543,130 @@ export async function createRemoteEngineConfiguration(id: string) {
}
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config));
}
export async function executeSSHCmd({ dockerId, command }) {
const { execaCommand } = await import('execa')
let { remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId)
}
if (process.env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose')
export async function executeCommand({ command, dockerId = null, sshCommand = false, shell = false, stream = false, buildId, applicationId, debug }: { command: string, sshCommand?: boolean, shell?: boolean, stream?: boolean, dockerId?: string, buildId?: string, applicationId?: string, debug?: boolean }): Promise<ExecaChildProcess<string>> {
const { execa, execaCommand } = await import('execa')
const { parse } = await import('shell-quote')
const parsedCommand = parse(command);
const dockerCommand = parsedCommand[0];
const dockerArgs = parsedCommand.slice(1);
if (dockerId) {
let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId);
engine = `ssh://${remoteIpAddress}-remote`;
} else {
engine = 'unix:///var/run/docker.sock';
}
}
return await execaCommand(`ssh ${remoteIpAddress}-remote ${command}`)
}
export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise<any> {
const { execaCommand } = await import('execa')
let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId);
engine = `ssh://${remoteIpAddress}-remote`;
if (process.env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose');
}
}
if (sshCommand) {
if (shell) {
return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`);
}
return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs]);
}
if (stream) {
return await new Promise(async (resolve, reject) => {
let subprocess = null;
if (shell) {
subprocess = execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
});
} else {
subprocess = execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
});
}
const logs = [];
subprocess.stdout.on('data', async (data) => {
const stdout = data.toString();
const array = stdout.split('\n');
for (const line of array) {
if (line !== '\n' && line !== '') {
const log = {
line: `${line.replace('\n', '')}`,
buildId,
applicationId
}
logs.push(log);
if (debug) {
await saveBuildLog(log);
}
}
}
});
subprocess.stderr.on('data', async (data) => {
const stderr = data.toString();
const array = stderr.split('\n');
for (const line of array) {
if (line !== '\n' && line !== '') {
const log = {
line: `${line.replace('\n', '')}`,
buildId,
applicationId
}
logs.push(log);
if (debug) {
await saveBuildLog(log);
}
}
}
});
subprocess.on('exit', async (code) => {
if (code === 0) {
resolve(code);
} else {
if (!debug) {
for (const log of logs) {
await saveBuildLog(log);
}
}
reject(code);
}
});
})
} else {
if (shell) {
return await execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
});
} else {
return await execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
});
}
}
} else {
engine = 'unix:///var/run/docker.sock';
}
if (process.env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose');
if (shell) {
return execaCommand(command, { shell: true });
}
return await execa(dockerCommand, dockerArgs);
}
if (command.startsWith(`docker build`) || command.startsWith(`pack build`) || command.startsWith(`docker compose build`)) {
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
}
return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })
}
export async function startTraefikProxy(id: string): Promise<void> {
const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } })
const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true });
const { id: settingsId, ipv4, ipv6 } = await listSettings();
if (!found) {
const { stdout: coolifyNetwork } = await executeDockerCmd({
const { stdout: coolifyNetwork } = await executeCommand({
dockerId: id,
command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`
});
if (!coolifyNetwork) {
await executeDockerCmd({
await executeCommand({
dockerId: id,
command: `docker network create --attachable coolify-infra`
});
}
const { stdout: Config } = await executeDockerCmd({
const { stdout: Config } = await executeCommand({
dockerId: id,
command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
});
@@ -632,7 +681,7 @@ export async function startTraefikProxy(id: string): Promise<void> {
}
traefikUrl = `${ip}/webhooks/traefik/remote/${id}`;
}
await executeDockerCmd({
await executeCommand({
dockerId: id,
command: `docker run --restart always \
--add-host 'host.docker.internal:host-gateway' \
@@ -657,7 +706,6 @@ export async function startTraefikProxy(id: string): Promise<void> {
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
--log.level=error`
});
await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } });
await prisma.destinationDocker.update({
where: { id },
data: { isCoolifyProxyUsed: true }
@@ -681,13 +729,13 @@ export async function startTraefikProxy(id: string): Promise<void> {
export async function configureNetworkTraefikProxy(destination: any): Promise<void> {
const { id } = destination;
const { stdout: networks } = await executeDockerCmd({
const { stdout: networks } = await executeCommand({
dockerId: id,
command: `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'`
});
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
if (!configuredNetworks.includes(destination.network)) {
await executeDockerCmd({
await executeCommand({
dockerId: destination.id,
command: `docker network connect ${destination.network} coolify-proxy`
});
@@ -702,13 +750,12 @@ export async function stopTraefikProxy(
where: { id },
data: { isCoolifyProxyUsed: false }
});
const { id: settingsId } = await prisma.setting.findFirst({});
await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } });
try {
if (found) {
await executeDockerCmd({
await executeCommand({
dockerId: id,
command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy`
command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy`,
shell: true
});
}
} catch (error) {
@@ -717,11 +764,14 @@ export async function stopTraefikProxy(
}
export async function listSettings(): Promise<any> {
const settings = await prisma.setting.findFirst({});
if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword);
return settings;
return await prisma.setting.findUnique({ where: { id: '0' } });
}
export function generateToken() {
return jsonwebtoken.sign({
nbf: Math.floor(Date.now() / 1000) - 30,
}, process.env['COOLIFY_SECRET_KEY'])
}
export function generatePassword({
length = 24,
symbols = false,
@@ -972,7 +1022,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data
}
}
export function isARM(arch: string) {
if (arch === 'arm' || arch === 'arm64') {
if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') {
return true;
}
return false;
@@ -1073,6 +1123,7 @@ export async function makeLabelForStandaloneDatabase({ id, image, volume }) {
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.type=standalone-database`,
`coolify.name=${database.name}`,
`coolify.configuration=${base64Encode(
JSON.stringify({
version,
@@ -1090,7 +1141,7 @@ export const createDirectories = async ({
repository: string;
buildId: string;
}): Promise<{ workdir: string; repodir: string }> => {
repository = repository.replaceAll(' ', '')
if (repository) repository = repository.replaceAll(' ', '')
const repodir = `/tmp/build-sources/${repository}/`;
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
let workdirFound = false;
@@ -1098,9 +1149,9 @@ export const createDirectories = async ({
workdirFound = !!(await fs.stat(workdir));
} catch (error) { }
if (workdirFound) {
await asyncExecShell(`rm -fr ${workdir}`);
await executeCommand({ command: `rm -fr ${workdir}` });
}
await asyncExecShell(`mkdir -p ${workdir}`);
await executeCommand({ command: `mkdir -p ${workdir}` });
return {
workdir,
repodir
@@ -1116,7 +1167,7 @@ export async function stopDatabaseContainer(database: any): Promise<boolean> {
} = database;
if (destinationDockerId) {
try {
const { stdout } = await executeDockerCmd({
const { stdout } = await executeCommand({
dockerId,
command: `docker inspect --format '{{json .State}}' ${id}`
});
@@ -1144,9 +1195,10 @@ export async function stopTcpHttpProxy(
const { found } = await checkContainer({ dockerId, container });
try {
if (found) {
return await executeDockerCmd({
return await executeCommand({
dockerId,
command: `docker stop -t 0 ${container} && docker rm ${container}`
command: `docker stop -t 0 ${container} && docker rm ${container}`,
shell: true
});
}
} catch (error) {
@@ -1168,34 +1220,34 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) {
} = database;
if (destinationDockerId) {
if (type === 'mysql') {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
});
} else if (type === 'mariadb') {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"`
});
} else if (type === 'postgresql') {
if (isRoot) {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"`
});
} else {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
});
}
} else if (type === 'mongodb') {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
});
} else if (type === 'redis') {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
});
@@ -1369,7 +1421,7 @@ export async function startTraefikTCPProxy(
});
try {
if (foundDependentContainer && !found) {
const { stdout: Config } = await executeDockerCmd({
const { stdout: Config } = await executeCommand({
dockerId,
command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
});
@@ -1416,16 +1468,17 @@ export async function startTraefikTCPProxy(
}
};
await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy));
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker compose -f /tmp/docker-compose-${id}.yaml up -d`
});
await fs.rm(`/tmp/docker-compose-${id}.yaml`);
}
if (!foundDependentContainer && found) {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker stop -t 0 ${container} && docker rm ${container}`
command: `docker stop -t 0 ${container} && docker rm ${container}`,
shell: true
});
}
} catch (error) {
@@ -1485,12 +1538,17 @@ export function makeLabelForServices(type) {
}
export function errorHandler({
status = 500,
message = 'Unknown error.'
message = 'Unknown error.',
type = 'normal'
}: {
status: number;
message: string | any;
type?: string | null;
}) {
if (message.message) message = message.message;
if (type === 'normal') {
Sentry.captureException(message);
}
throw { status, message };
}
export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> {
@@ -1529,9 +1587,9 @@ export async function stopBuild(buildId, applicationId) {
scheduler.workers.get('deployApplication').postMessage('cancel');
}
await cleanupDB(buildId, applicationId);
return reject(new Error('Deployment canceled.'));
return reject(new Error('Canceled.'));
}
const { stdout: buildContainers } = await executeDockerCmd({
const { stdout: buildContainers } = await executeCommand({
dockerId,
command: `docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'`
});
@@ -1562,7 +1620,7 @@ async function cleanupDB(buildId: string, applicationId: string) {
if (data?.status === 'queued' || data?.status === 'running') {
await prisma.build.update({ where: { id: buildId }, data: { status: 'canceled' } });
}
await saveBuildLog({ line: 'Deployment canceled.', buildId, applicationId });
await saveBuildLog({ line: 'Canceled.', buildId, applicationId });
}
export function convertTolOldVolumeNames(type) {
@@ -1574,36 +1632,60 @@ export function convertTolOldVolumeNames(type) {
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
// Cleanup old coolify images
try {
let { stdout: images } = await executeDockerCmd({
let { stdout: images } = await executeCommand({
dockerId,
command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r`
command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r`,
shell: true
});
images = images.trim();
if (images) {
await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r` });
await executeCommand({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r`, shell: true });
}
} catch (error) { }
if (lowDiskSpace || force) {
// if (isDev) {
// if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`);
// return;
// }
// Cleanup images that are not used
try {
await executeDockerCmd({
await executeCommand({ dockerId, command: `docker image prune -f` });
} catch (error) { }
const { numberOfDockerImagesKeptLocally } = await prisma.setting.findUnique({ where: { id: '0' } })
const { stdout: images } = await executeCommand({
dockerId,
command: `docker images|grep -v "<none>"|grep -v REPOSITORY|awk '{print $1, $2}'`,
shell: true
});
const imagesArray = images.trim().replaceAll(' ', ':').split('\n');
const imagesSet = new Set(imagesArray.map((image) => image.split(':')[0]));
let deleteImage = []
for (const image of imagesSet) {
let keepImage = []
for (const image2 of imagesArray) {
if (image2.startsWith(image)) {
if (keepImage.length >= numberOfDockerImagesKeptLocally) {
deleteImage.push(image2)
} else {
keepImage.push(image2)
}
}
}
}
for (const image of deleteImage) {
await executeCommand({ dockerId, command: `docker image rm -f ${image}` });
}
// Prune coolify managed containers
try {
await executeCommand({
dockerId,
command: `docker container prune -f --filter "label=coolify.managed=true"`
});
} catch (error) { }
try {
await executeDockerCmd({ dockerId, command: `docker image prune -f` });
} catch (error) { }
try {
await executeDockerCmd({ dockerId, command: `docker image prune -a -f` });
} catch (error) { }
// Cleanup build caches
try {
await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` });
await executeCommand({ dockerId, command: `docker builder prune -a -f` });
} catch (error) { }
}
}
@@ -1614,7 +1696,7 @@ export function persistentVolumes(id, persistentStorage, config) {
for (const [key, value] of Object.entries(config)) {
if (value.volumes) {
for (const volume of value.volumes) {
if (!volume.startsWith('/var/run/docker.sock')) {
if (!volume.startsWith('/')) {
volumeSet.add(volume);
}
}
@@ -1685,3 +1767,17 @@ export function decryptApplication(application: any) {
return application;
}
}
export async function pushToRegistry(application: any, workdir: string, tag: string, imageName: string, customTag: string) {
const location = `${workdir}/.docker`
const tagCommand = `docker tag ${application.id}:${tag} ${imageName}:${customTag}`
const pushCommand = `docker --config ${location} push ${imageName}:${customTag}`
await executeCommand({
dockerId: application.destinationDockerId,
command: tagCommand
})
await executeCommand({
dockerId: application.destinationDockerId,
command: pushCommand
})
}

View File

@@ -1,4 +1,4 @@
import { executeDockerCmd } from './common';
import { executeCommand } from './common';
export function formatLabelsOnDocker(data) {
return data.trim().split('\n').map(a => JSON.parse(a)).map((container) => {
@@ -16,7 +16,7 @@ export function formatLabelsOnDocker(data) {
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> {
let containerFound = false;
try {
const { stdout } = await executeDockerCmd({
const { stdout } = await executeCommand({
dockerId,
command:
`docker inspect --format '{{json .State}}' ${container}`
@@ -28,27 +28,26 @@ export async function checkContainer({ dockerId, container, remove = false }: {
const isRestarting = status === 'restarting'
const isExited = status === 'exited'
if (status === 'created') {
await executeDockerCmd({
await executeCommand({
dockerId,
command:
`docker rm ${container}`
});
}
if (remove && status === 'exited') {
await executeDockerCmd({
await executeCommand({
dockerId,
command:
`docker rm ${container}`
});
}
return {
found: containerFound,
status: {
isRunning,
isRestarting,
isExited
}
};
} catch (err) {
@@ -63,7 +62,7 @@ export async function checkContainer({ dockerId, container, remove = false }: {
export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {
let isExited = false;
try {
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` })
const { stdout } = await executeCommand({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` })
if (stdout.trim() === 'exited') {
isExited = true;
}
@@ -82,13 +81,13 @@ export async function removeContainer({
dockerId: string;
}): Promise<void> {
try {
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
const { stdout } = await executeCommand({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
if (JSON.parse(stdout).Running) {
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` })
await executeCommand({ dockerId, command: `docker rm ${id}` })
}
if (JSON.parse(stdout).Status === 'exited') {
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
await executeCommand({ dockerId, command: `docker rm ${id}` })
}
} catch (error) {
throw error;

View File

@@ -1,7 +1,7 @@
import jsonwebtoken from 'jsonwebtoken';
import { saveBuildLog } from '../buildPacks/common';
import { asyncExecShell, decrypt, prisma } from '../common';
import { decrypt, executeCommand, prisma } from '../common';
export default async function ({
applicationId,
@@ -9,6 +9,7 @@ export default async function ({
githubAppId,
repository,
apiUrl,
gitCommitHash,
htmlUrl,
branch,
buildId,
@@ -20,6 +21,7 @@ export default async function ({
githubAppId: string;
repository: string;
apiUrl: string;
gitCommitHash?: string;
htmlUrl: string;
branch: string;
buildId: string;
@@ -28,16 +30,24 @@ export default async function ({
}): Promise<string> {
const { default: got } = await import('got')
const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
if (forPublic) {
await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`,
line: `Cloning ${repository}:${branch}...`,
buildId,
applicationId
});
await asyncExecShell(
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. `
);
if (gitCommitHash) {
await saveBuildLog({
line: `Checking out ${gitCommitHash} commit...`,
buildId,
applicationId
});
}
await executeCommand({
command:
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `,
shell: true
});
} else {
const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
@@ -62,15 +72,23 @@ export default async function ({
})
.json();
await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`,
line: `Cloning ${repository}:${branch}...`,
buildId,
applicationId
});
await asyncExecShell(
`git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. `
);
if (gitCommitHash) {
await saveBuildLog({
line: `Checking out ${gitCommitHash} commit...`,
buildId,
applicationId
});
}
await executeCommand({
command:
`git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `,
shell: true
});
}
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
const { stdout: commit } = await executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true });
return commit.replace('\n', '');
}

View File

@@ -1,11 +1,12 @@
import { saveBuildLog } from "../buildPacks/common";
import { asyncExecShell } from "../common";
import { executeCommand } from "../common";
export default async function ({
applicationId,
workdir,
repodir,
htmlUrl,
gitCommitHash,
repository,
branch,
buildId,
@@ -20,34 +21,43 @@ export default async function ({
branch: string;
buildId: string;
repodir: string;
gitCommitHash: string;
privateSshKey: string;
customPort: number;
forPublic: boolean;
}): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
if (!forPublic) {
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
await executeCommand({ command: `echo '${privateSshKey}' > ${repodir}/id.rsa`, shell: true });
await executeCommand({ command: `chmod 600 ${repodir}/id.rsa` });
}
await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`,
line: `Cloning ${repository}:${branch}...`,
buildId,
applicationId
});
if (gitCommitHash) {
await saveBuildLog({
line: `Checking out ${gitCommitHash} commit...`,
buildId,
applicationId
});
}
if (forPublic) {
await asyncExecShell(
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
await executeCommand({
command:
`git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true
}
);
} else {
await asyncExecShell(
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
await executeCommand({
command:
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true
}
);
}
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
const { stdout: commit } = await executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true });
return commit.replace('\n', '');
}

View File

@@ -9,7 +9,7 @@ Bree.extend(TSBree);
const options: any = {
defaultExtension: 'js',
logger: new Cabin(),
logger: false,
// logger: false,
// workerMessageHandler: async ({ name, message }) => {
// if (name === 'deployApplication' && message?.deploying) {

View File

@@ -1,145 +1,20 @@
import { isDev } from "./common";
import { isARM, isDev } from "./common";
import fs from 'fs/promises';
export async function getTemplates() {
let templates: any = [];
if (isDev) {
templates = JSON.parse(await (await fs.readFile('./templates.json')).toString())
} else {
templates = JSON.parse(await (await fs.readFile('/app/templates.json')).toString())
const templatePath = isDev ? './templates.json' : '/app/templates.json';
const open = await fs.open(templatePath, 'r');
try {
let data = await open.readFile({ encoding: 'utf-8' });
let jsonData = JSON.parse(data)
if (isARM(process.arch)) {
jsonData = jsonData.filter(d => d.arch !== 'amd64')
}
return jsonData;
} catch (error) {
return []
} finally {
await open?.close()
}
// if (!isDev) {
// templates.push({
// "templateVersion": "1.0.0",
// "defaultVersion": "latest",
// "name": "Test-Fake-Service",
// "description": "",
// "services": {
// "$$id": {
// "name": "Test-Fake-Service",
// "depends_on": [
// "$$id-postgresql",
// "$$id-redis"
// ],
// "image": "weblate/weblate:$$core_version",
// "volumes": [
// "$$id-data:/app/data",
// ],
// "environment": [
// `POSTGRES_SECRET=$$secret_postgres_secret`,
// `WEBLATE_SITE_DOMAIN=$$config_weblate_site_domain`,
// `WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password`,
// `POSTGRES_PASSWORD=$$secret_postgres_password`,
// `POSTGRES_USER=$$config_postgres_user`,
// `POSTGRES_DATABASE=$$config_postgres_db`,
// `POSTGRES_HOST=$$id-postgresql`,
// `POSTGRES_PORT=5432`,
// `REDIS_HOST=$$id-redis`,
// ],
// "ports": [
// "8080"
// ]
// },
// "$$id-postgresql": {
// "name": "PostgreSQL",
// "depends_on": [],
// "image": "postgres:14-alpine",
// "volumes": [
// "$$id-postgresql-data:/var/lib/postgresql/data",
// ],
// "environment": [
// "POSTGRES_USER=$$config_postgres_user",
// "POSTGRES_PASSWORD=$$secret_postgres_password",
// "POSTGRES_DB=$$config_postgres_db",
// ],
// "ports": []
// },
// "$$id-redis": {
// "name": "Redis",
// "depends_on": [],
// "image": "redis:7-alpine",
// "volumes": [
// "$$id-redis-data:/data",
// ],
// "environment": [],
// "ports": [],
// }
// },
// "variables": [
// {
// "id": "$$config_weblate_site_domain",
// "main": "$$id",
// "name": "WEBLATE_SITE_DOMAIN",
// "label": "Weblate Domain",
// "defaultValue": "$$generate_domain",
// "description": "",
// },
// {
// "id": "$$secret_weblate_admin_password",
// "main": "$$id",
// "name": "WEBLATE_ADMIN_PASSWORD",
// "label": "Weblate Admin Password",
// "defaultValue": "$$generate_password",
// "description": "",
// "extras": {
// "isVisibleOnUI": true,
// }
// },
// {
// "id": "$$secret_weblate_admin_password2",
// "name": "WEBLATE_ADMIN_PASSWORD2",
// "label": "Weblate Admin Password2",
// "defaultValue": "$$generate_password",
// "description": "",
// },
// {
// "id": "$$config_postgres_user",
// "main": "$$id-postgresql",
// "name": "POSTGRES_USER",
// "label": "PostgreSQL User",
// "defaultValue": "$$generate_username",
// "description": "",
// },
// {
// "id": "$$secret_postgres_password",
// "main": "$$id-postgresql",
// "name": "POSTGRES_PASSWORD",
// "label": "PostgreSQL Password",
// "defaultValue": "$$generate_password(32)",
// "description": "",
// },
// {
// "id": "$$secret_postgres_password_hex32",
// "name": "POSTGRES_PASSWORD_hex32",
// "label": "PostgreSQL Password hex32",
// "defaultValue": "$$generate_hex(32)",
// "description": "",
// },
// {
// "id": "$$config_postgres_something_hex32",
// "name": "POSTGRES_SOMETHING_HEX32",
// "label": "PostgreSQL Something hex32",
// "defaultValue": "$$generate_hex(32)",
// "description": "",
// },
// {
// "id": "$$config_postgres_db",
// "main": "$$id-postgresql",
// "name": "POSTGRES_DB",
// "label": "PostgreSQL Database",
// "defaultValue": "weblate",
// "description": "",
// },
// {
// "id": "$$secret_postgres_secret",
// "name": "POSTGRES_SECRET",
// "label": "PostgreSQL Secret",
// "defaultValue": "",
// "description": "",
// },
// ]
// })
// }
return templates
}
const compareSemanticVersions = (a: string, b: string) => {
const a1 = a.split('.');
@@ -155,16 +30,22 @@ const compareSemanticVersions = (a: string, b: string) => {
return b1.length - a1.length;
};
export async function getTags(type: string) {
if (type) {
let tags: any = [];
if (isDev) {
tags = JSON.parse(await (await fs.readFile('./tags.json')).toString())
} else {
tags = JSON.parse(await (await fs.readFile('/app/tags.json')).toString())
try {
if (type) {
const tagsPath = isDev ? './tags.json' : '/app/tags.json';
const data = await fs.readFile(tagsPath, 'utf8')
let tags = JSON.parse(data)
if (tags) {
tags = tags.find((tag: any) => tag.name.includes(type))
tags.tags = tags.tags.sort(compareSemanticVersions).reverse();
return tags
}
}
tags = tags.find((tag: any) => tag.name.includes(type))
tags.tags = tags.tags.sort(compareSemanticVersions).reverse();
return tags
} catch (error) {
return []
}
return []
}

View File

@@ -1,5 +1,5 @@
import { prisma } from '../common';
import { decrypt, prisma } from '../common';
export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
@@ -22,4 +22,18 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.taiga.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } });
}
export async function verifyAndDecryptServiceSecrets(id: string) {
const secrets = await prisma.serviceSecret.findMany({ where: { serviceId: id } })
let decryptedSecrets = secrets.map(secret => {
const { name, value } = secret
if (value) {
let rawValue = decrypt(value)
rawValue = rawValue.replaceAll(/\$/gi, '$$$')
return { name, value: rawValue }
}
return { name, value }
})
return decryptedSecrets
}

View File

@@ -2,11 +2,12 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import path from 'path';
import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeDockerCmd, getServiceFromDB, isARM, makeLabelForServices, persistentVolumes, prisma, stopTcpHttpProxy } from '../common';
import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeCommand, getServiceFromDB, isARM, makeLabelForServices, persistentVolumes, prisma, stopTcpHttpProxy } from '../common';
import { parseAndFindServiceTemplates } from '../../routes/api/v1/services/handlers';
import { ServiceStartStop } from '../../routes/api/v1/services/types';
import { OnlyId } from '../../types';
import { verifyAndDecryptServiceSecrets } from './common';
export async function stopService(request: FastifyRequest<ServiceStartStop>) {
try {
@@ -14,14 +15,19 @@ export async function stopService(request: FastifyRequest<ServiceStartStop>) {
const teamId = request.user.teamId;
const { destinationDockerId } = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}`
})
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` })
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` })
}
}
}
return {}
}
throw { status: 500, message: 'Could not stop containers.' }
@@ -34,7 +40,7 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const arm = isARM(service.arch)
const arm = isARM(service.arch);
const { type, destinationDockerId, destinationDocker, persistentStorage, exposePort } =
service;
@@ -65,15 +71,17 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
}
}
}
const secrets = await prisma.serviceSecret.findMany({ where: { serviceId: id } })
const secrets = await verifyAndDecryptServiceSecrets(id)
for (const secret of secrets) {
const { name, value } = secret
if (value) {
const foundEnv = !!template.services[s].environment?.find(env => env.startsWith(`${name}=`))
const foundNewEnv = !!newEnvironments?.find(env => env.startsWith(`${name}=`))
if (foundEnv && !foundNewEnv) {
newEnvironments.push(`${name}=${decrypt(value)}`)
newEnvironments.push(`${name}=${value}`)
}
if (!foundEnv && !foundNewEnv && s === id) {
newEnvironments.push(`${name}=${value}`)
}
}
}
@@ -103,15 +111,34 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
}
}
}
let ports = []
if (template.services[s].proxy?.length > 0) {
for (const proxy of template.services[s].proxy) {
if (proxy.hostPort) {
ports.push(`${proxy.hostPort}:${proxy.port}`)
}
}
} else {
if (template.services[s].ports?.length === 1) {
for (const port of template.services[s].ports) {
if (exposePort) {
ports.push(`${exposePort}:${port}`)
}
}
}
}
let image = template.services[s].image
if (arm && template.services[s].imageArm) {
image = template.services[s].imageArm
}
config[s] = {
container_name: s,
build: template.services[s].build || undefined,
command: template.services[s].command,
entrypoint: template.services[s]?.entrypoint,
image: arm ? template.services[s].imageArm : template.services[s].image,
image,
expose: template.services[s].ports,
...(exposePort ? { ports: [`${exposePort}:${exposePort}`] } : {}),
ports: ports.length > 0 ? ports : undefined,
volumes: Array.from(volumes),
environment: newEnvironments,
depends_on: template.services[s]?.depends_on,
@@ -121,7 +148,6 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
labels: makeLabelForServices(type),
...defaultComposeConfiguration(network),
}
// Generate files for builds
if (template.services[s]?.files?.length > 0) {
if (!config[s].build) {
@@ -161,21 +187,37 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
// Workaround: Stop old minio proxies
if (service.type === 'minio') {
try {
await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: destinationDocker.id,
command:
`docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container stop -t 0`
`docker container ls -a --filter 'name=${id}-' --format {{.ID}}`
});
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` })
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` })
}
}
}
} catch (error) { }
try {
await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: destinationDocker.id,
command:
`docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container rm -f`
`docker container ls -a --filter 'name=${id}-' --format {{.ID}}`
});
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` })
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` })
}
}
}
} catch (error) { }
}
return {}
} catch ({ status, message }) {
@@ -185,16 +227,16 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
async function startServiceContainers(fastify, id, teamId, dockerId, composeFileDestination) {
try {
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Pulling images...' })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
} catch (error) { }
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Building images...' })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Creating containers...' })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Starting containers...' })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} start` })
await asyncSleep(1000);
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` })
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 0 })
}
export async function migrateAppwriteDB(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
@@ -206,7 +248,7 @@ export async function migrateAppwriteDB(request: FastifyRequest<OnlyId>, reply:
destinationDocker,
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeDockerCmd({
await executeCommand({
dockerId: destinationDocker.id,
command: `docker exec ${id} migrate`
})

View File

@@ -7,12 +7,12 @@ import yaml from 'js-yaml';
import csv from 'csvtojson';
import { day } from '../../../../lib/dayjs';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { saveDockerRegistryCredentials, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeCommand, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, formatLabelsOnDocker, removeContainer } from '../../../../lib/docker';
import type { FastifyRequest } from 'fastify';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds, RestartApplication } from './types';
import { OnlyId } from '../../../../types';
function filterObject(obj, callback) {
@@ -78,7 +78,7 @@ export async function cleanupUnconfiguredApplications(request: FastifyRequest<an
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
if (application?.destinationDockerId && application.destinationDocker?.network) {
const { stdout: containers } = await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: application.destinationDocker.id,
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${application.id} --format '{{json .}}'`
})
@@ -113,7 +113,7 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: application.destinationDocker.id,
command:
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
@@ -241,7 +241,8 @@ export async function getApplicationFromDB(id: string, teamId: string) {
secrets: true,
persistentStorage: true,
connectedDatabase: true,
previewApplication: true
previewApplication: true,
dockerRegistry: true
}
});
if (!application) {
@@ -280,7 +281,7 @@ export async function getApplicationFromDBWebhook(projectId: number, branch: str
}
});
if (applications.length === 0) {
throw { status: 500, message: 'Application not configured.' }
throw { status: 500, message: 'Application not configured.', type: 'webhook' }
}
applications = applications.map((application: any) => {
application = decryptApplication(application);
@@ -302,8 +303,8 @@ export async function getApplicationFromDBWebhook(projectId: number, branch: str
return applications;
} catch ({ status, message }) {
return errorHandler({ status, message })
} catch ({ status, message, type }) {
return errorHandler({ status, message, type })
}
}
export async function saveApplication(request: FastifyRequest<SaveApplication>, reply: FastifyReply) {
@@ -326,13 +327,16 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
dockerFileLocation,
denoMainFile,
denoOptions,
gitCommitHash,
baseImage,
baseBuildImage,
deploymentType,
baseDatabaseBranch,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration
dockerComposeConfiguration,
simpleDockerfile,
dockerRegistryImageName
} = request.body
if (port) port = Number(port);
if (exposePort) {
@@ -350,6 +354,7 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
publishDirectory,
baseDirectory,
dockerFileLocation,
dockerComposeFileLocation,
denoMainFile
});
if (baseDatabaseBranch) {
@@ -364,11 +369,14 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
pythonVariable,
denoOptions,
baseImage,
gitCommitHash,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
simpleDockerfile,
dockerRegistryImageName,
...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
}
@@ -382,6 +390,7 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
exposePort,
pythonWSGI,
pythonModule,
gitCommitHash,
pythonVariable,
denoOptions,
baseImage,
@@ -390,6 +399,8 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
simpleDockerfile,
dockerRegistryImageName,
...defaultConfiguration
}
});
@@ -438,16 +449,17 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
}
}
export async function restartApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
export async function restartApplication(request: FastifyRequest<RestartApplication>, reply: FastifyReply) {
try {
const { id } = request.params
const { imageId = null } = request.body
const { teamId } = request.user
let application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const buildId = cuid();
const { id: dockerId, network } = application.destinationDocker;
const { secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
const { dockerRegistry, secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
let location = null;
const envs = [
`PORT=${port}`
];
@@ -470,28 +482,48 @@ export async function restartApplication(request: FastifyRequest<OnlyId>, reply:
const { workdir } = await createDirectories({ repository, buildId });
const labels = []
let image = null
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` })
const containersArray = container.trim().split('\n');
for (const container of containersArray) {
const containerObj = formatLabelsOnDocker(container);
image = containerObj[0].Image
Object.keys(containerObj[0].Labels).forEach(function (key) {
if (key.startsWith('coolify')) {
labels.push(`${key}=${containerObj[0].Labels[key]}`)
}
})
if (imageId) {
image = imageId
} else {
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` })
const containersArray = container.trim().split('\n');
for (const container of containersArray) {
const containerObj = formatLabelsOnDocker(container);
image = containerObj[0].Image
Object.keys(containerObj[0].Labels).forEach(function (key) {
if (key.startsWith('coolify')) {
labels.push(`${key}=${containerObj[0].Labels[key]}`)
}
})
}
}
let imageFound = false;
if (dockerRegistry) {
const { url, username, password } = dockerRegistry
location = await saveDockerRegistryCredentials({ url, username, password, workdir })
}
let imageFoundLocally = false;
try {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker image inspect ${image}`
})
imageFound = true;
imageFoundLocally = true;
} catch (error) {
//
}
if (!imageFound) {
let imageFoundRemotely = false;
try {
await executeCommand({
dockerId,
command: `docker ${location ? `--config ${location}` : ''} pull ${image}`
})
imageFoundRemotely = true;
} catch (error) {
//
}
if (!imageFoundLocally && !imageFoundRemotely) {
throw { status: 500, message: 'Image not found, cannot restart application.' }
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
@@ -537,9 +569,14 @@ export async function restartApplication(request: FastifyRequest<OnlyId>, reply:
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
try {
await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` })
await executeCommand({ dockerId, command: `docker rm ${id}` })
} catch (error) {
//
}
await executeCommand({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
return reply.code(201).send();
}
throw { status: 500, message: 'Application cannot be restarted.' }
@@ -555,7 +592,7 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
if (application?.destinationDockerId) {
const { id: dockerId } = application.destinationDocker;
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: application.destinationDocker.id,
command:
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
@@ -590,7 +627,7 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
include: { destinationDocker: true }
});
if (!force && application?.destinationDockerId && application.destinationDocker?.network) {
const { stdout: containers } = await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: application.destinationDocker.id,
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
})
@@ -676,6 +713,47 @@ export async function getUsage(request) {
return errorHandler({ status, message })
}
}
export async function getDockerImages(request) {
try {
const { id } = request.params
const teamId = request.user?.teamId;
const application: any = await getApplicationFromDB(id, teamId);
let imagesAvailables = [];
try {
const { stdout } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}' | grep -i ${id} | grep -v cache`, shell: true });
const { stdout: runningImage } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}` });
const images = stdout.trim().split('\n');
for (const image of images) {
const [repository, tag, createdAt] = image.split('#');
if (tag.includes('-')) {
continue;
}
const [year, time] = createdAt.split(' ');
imagesAvailables.push({
repository,
tag,
createdAt: day(year + time).unix()
})
}
imagesAvailables = imagesAvailables.sort((a, b) => b.tag - a.tag);
return {
imagesAvailables,
runningImage
}
} catch (error) {
return {
imagesAvailables,
}
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getUsageByContainer(request) {
try {
@@ -718,22 +796,37 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
await prisma.application.update({ where: { id }, data: { configHash } });
}
await prisma.application.update({ where: { id }, data: { updatedAt: new Date() } });
await prisma.build.create({
data: {
id: buildId,
applicationId: id,
sourceBranch: branch,
branch: application.branch,
pullmergeRequestId: pullmergeRequestId?.toString(),
forceRebuild,
destinationDockerId: application.destinationDocker?.id,
gitSourceId: application.gitSource?.id,
githubAppId: application.gitSource?.githubApp?.id,
gitlabAppId: application.gitSource?.gitlabApp?.id,
status: 'queued',
type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
}
});
if (application.gitSourceId) {
await prisma.build.create({
data: {
id: buildId,
applicationId: id,
sourceBranch: branch,
branch: application.branch,
pullmergeRequestId: pullmergeRequestId?.toString(),
forceRebuild,
destinationDockerId: application.destinationDocker?.id,
gitSourceId: application.gitSource?.id,
githubAppId: application.gitSource?.githubApp?.id,
gitlabAppId: application.gitSource?.gitlabApp?.id,
status: 'queued',
type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
}
});
} else {
await prisma.build.create({
data: {
id: buildId,
applicationId: id,
branch: 'latest',
forceRebuild,
destinationDockerId: application.destinationDocker?.id,
status: 'queued',
type: 'manual'
}
});
}
return {
buildId
};
@@ -748,20 +841,28 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
export async function saveApplicationSource(request: FastifyRequest<SaveApplicationSource>, reply: FastifyReply) {
try {
const { id } = request.params
const { gitSourceId, forPublic, type } = request.body
const { gitSourceId, forPublic, type, simpleDockerfile } = request.body
if (forPublic) {
const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } });
await prisma.application.update({
where: { id },
data: { gitSource: { connect: { id: publicGit.id } } }
});
} else {
}
if (simpleDockerfile) {
await prisma.application.update({
where: { id },
data: { simpleDockerfile, settings: { update: { autodeploy: false } } }
});
}
if (gitSourceId) {
await prisma.application.update({
where: { id },
data: { gitSource: { connect: { id: gitSourceId } } }
});
}
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -819,7 +920,7 @@ export async function saveRepository(request, reply) {
let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body
repository = repository.toLowerCase();
projectId = Number(projectId);
if (webhookToken) {
await prisma.application.update({
@@ -864,11 +965,11 @@ export async function getBuildPack(request) {
const teamId = request.user?.teamId;
const application: any = await getApplicationFromDB(id, teamId);
return {
type: application.gitSource.type,
type: application.gitSource?.type || 'dockerRegistry',
projectId: application.projectId,
repository: application.repository,
branch: application.branch,
apiUrl: application.gitSource.apiUrl,
apiUrl: application.gitSource?.apiUrl || null,
isPublicRepository: application.settings.isPublicRepository
}
} catch ({ status, message }) {
@@ -876,6 +977,16 @@ export async function getBuildPack(request) {
}
}
export async function saveRegistry(request, reply) {
try {
const { id } = request.params
const { registryId } = request.body
await prisma.application.update({ where: { id }, data: { dockerRegistry: { connect: { id: registryId } } } });
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveBuildPack(request, reply) {
try {
const { id } = request.params
@@ -1072,7 +1183,7 @@ export async function restartPreview(request: FastifyRequest<RestartPreviewAppli
const { workdir } = await createDirectories({ repository, buildId });
const labels = []
let image = null
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` })
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` })
const containersArray = container.trim().split('\n');
for (const container of containersArray) {
const containerObj = formatLabelsOnDocker(container);
@@ -1085,7 +1196,7 @@ export async function restartPreview(request: FastifyRequest<RestartPreviewAppli
}
let imageFound = false;
try {
await executeDockerCmd({
await executeCommand({
dockerId,
command: `docker image inspect ${image}`
})
@@ -1139,9 +1250,9 @@ export async function restartPreview(request: FastifyRequest<RestartPreviewAppli
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` })
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
await executeCommand({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` })
await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` })
await executeCommand({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
return reply.code(201).send();
}
throw { status: 500, message: 'Application cannot be restarted.' }
@@ -1182,7 +1293,7 @@ export async function loadPreviews(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
const { stdout } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
if (stdout === '') {
throw { status: 500, message: 'No previews found.' }
}
@@ -1257,7 +1368,7 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
if (destinationDockerId) {
try {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const { stdout, stderr } = await executeCommand({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)
@@ -1448,19 +1559,19 @@ export async function createdBranchDatabase(database: any, baseDatabaseBranch: s
if (destinationDockerId) {
if (type === 'postgresql') {
const decryptedRootUserPassword = decrypt(rootUserPassword);
await executeDockerCmd({
await executeCommand({
dockerId: destinationDockerId,
command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump`
})
await executeDockerCmd({
await executeCommand({
dockerId: destinationDockerId,
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"`
})
await executeDockerCmd({
await executeCommand({
dockerId: destinationDockerId,
command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump`
})
await executeDockerCmd({
await executeCommand({
dockerId: destinationDockerId,
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"`
})
@@ -1479,12 +1590,12 @@ export async function removeBranchDatabase(database: any, pullmergeRequestId: st
if (type === 'postgresql') {
const decryptedRootUserPassword = decrypt(rootUserPassword);
// Terminate all connections to the database
await executeDockerCmd({
await executeCommand({
dockerId: destinationDockerId,
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"`
})
await executeDockerCmd({
await executeCommand({
dockerId: destinationDockerId,
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"`
})

View File

@@ -1,8 +1,8 @@
import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, getUsageByContainer, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getDockerImages, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, getUsageByContainer, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRegistry, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartApplication, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
@@ -21,7 +21,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request));
fastify.post<OnlyId>('/:id/restart', async (request, reply) => await restartApplication(request, reply));
fastify.post<RestartApplication>('/:id/restart', async (request, reply) => await restartApplication(request, reply));
fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply));
fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply));
@@ -45,7 +45,6 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
// fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
fastify.get<GetApplicationLogs>('/:id/logs/:containerId', async (request) => await getApplicationLogs(request));
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
@@ -53,6 +52,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/:id/usage', async (request) => await getUsage(request))
fastify.get('/:id/usage/:containerId', async (request) => await getUsageByContainer(request))
fastify.get('/:id/images', async (request) => await getDockerImages(request))
fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request))
fastify.post<CancelDeployment>('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply));
@@ -64,6 +65,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/:id/configuration/buildpack', async (request) => await getBuildPack(request));
fastify.post('/:id/configuration/buildpack', async (request, reply) => await saveBuildPack(request, reply));
fastify.post('/:id/configuration/registry', async (request, reply) => await saveRegistry(request, reply));
fastify.post('/:id/configuration/database', async (request, reply) => await saveConnectedDatabase(request, reply));
fastify.get<OnlyId>('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request));

View File

@@ -19,12 +19,15 @@ export interface SaveApplication extends OnlyId {
denoMainFile: string,
denoOptions: string,
baseImage: string,
gitCommitHash: string,
baseBuildImage: string,
deploymentType: string,
baseDatabaseBranch: string,
dockerComposeFile: string,
dockerComposeFileLocation: string,
dockerComposeConfiguration: string
dockerComposeConfiguration: string,
simpleDockerfile: string,
dockerRegistryImageName: string
}
}
export interface SaveApplicationSettings extends OnlyId {
@@ -55,7 +58,7 @@ export interface GetImages {
Body: { buildPack: string, deploymentType: string }
}
export interface SaveApplicationSource extends OnlyId {
Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string }
Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string, simpleDockerfile?: string }
}
export interface CheckRepository extends OnlyId {
Querystring: { repository: string, branch: string }
@@ -140,4 +143,12 @@ export interface RestartPreviewApplication {
id: string,
pullmergeRequestId: string | null,
}
}
export interface RestartApplication {
Params: {
id: string,
},
Body: {
imageId: string | null,
}
}

View File

@@ -2,13 +2,20 @@ import { FastifyPluginAsync } from 'fastify';
import { errorHandler, listSettings, version } from '../../../../lib/common';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
try {
await request.jwtVerify()
} catch(error) {
return
}
});
fastify.get('/', async (request) => {
const teamId = request.user?.teamId;
const settings = await listSettings()
try {
return {
ipv4: teamId ? settings.ipv4 : 'nope',
ipv6: teamId ? settings.ipv6 : 'nope',
ipv4: teamId ? settings.ipv4 : null,
ipv6: teamId ? settings.ipv6 : null,
version,
whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true',
whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON,

View File

@@ -3,7 +3,7 @@ import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import yaml from 'js-yaml';
import fs from 'fs/promises';
import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeCommand, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
import type { OnlyId } from '../../../../types';
@@ -89,7 +89,7 @@ export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) {
const { destinationDockerId, destinationDocker } = database;
if (destinationDockerId) {
try {
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
const { stdout } = await executeCommand({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
if (JSON.parse(stdout).Running) {
isRunning = true;
@@ -208,7 +208,7 @@ export async function saveDatabaseDestination(request: FastifyRequest<SaveDataba
if (destinationDockerId) {
if (type && version) {
const baseImage = getDatabaseImage(type, arch);
executeDockerCmd({ dockerId, command: `docker pull ${baseImage}:${version}` })
executeCommand({ dockerId, command: `docker pull ${baseImage}:${version}` })
}
}
return reply.code(201).send({})
@@ -298,7 +298,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
await executeCommand({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
return {};
@@ -347,7 +347,7 @@ export async function getDatabaseLogs(request: FastifyRequest<GetDatabaseLogs>)
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
const { stdout, stderr } = await executeCommand({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)

View File

@@ -4,7 +4,7 @@ import sshConfig from 'ssh-config'
import fs from 'fs/promises'
import os from 'os';
import { asyncExecShell, createRemoteEngineConfiguration, decrypt, errorHandler, executeDockerCmd, executeSSHCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { createRemoteEngineConfiguration, decrypt, errorHandler, executeCommand, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { checkContainer } from '../../../../lib/docker';
import type { OnlyId } from '../../../../types';
@@ -79,9 +79,9 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body
if (id === 'new') {
if (engine) {
const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`);
const { stdout } = await await executeCommand({ command: `docker network ls --filter 'name=^${network}$' --format '{{json .}}'` });
if (stdout === '') {
await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network create --attachable ${network}`);
await await executeCommand({ command: `docker network create --attachable ${network}` });
}
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
@@ -103,7 +103,7 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
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 }
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress, remoteUser, remotePort: Number(remotePort) }
});
return reply.code(201).send({ id: destination.id })
}
@@ -122,13 +122,13 @@ export async function deleteDestination(request: FastifyRequest<OnlyId>) {
const { network, remoteVerified, engine, isCoolifyProxyUsed } = await prisma.destinationDocker.findUnique({ where: { id } });
if (isCoolifyProxyUsed) {
if (engine || remoteVerified) {
const { stdout: found } = await executeDockerCmd({
const { stdout: found } = await executeCommand({
dockerId: id,
command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'`
})
if (found) {
await executeDockerCmd({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` })
await executeDockerCmd({ dockerId: id, command: `docker network rm ${network}` })
await executeCommand({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` })
await executeCommand({ dockerId: id, command: `docker network rm ${network}` })
}
}
}
@@ -203,22 +203,31 @@ export async function assignSSHKey(request: FastifyRequest) {
}
}
export async function verifyRemoteDockerEngineFn(id: string) {
await createRemoteEngineConfiguration(id);
const { remoteIpAddress, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } })
const host = `ssh://${remoteIpAddress}-remote`
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
if (!stdout) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
const { stdout: coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`);
if (!coolifyNetwork) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
}
if (isCoolifyProxyUsed) await startTraefikProxy(id);
const daemonJson = `daemon-${id}.json`
try {
const { stdout: daemonJson } = await executeSSHCmd({ dockerId: id, command: `cat /etc/docker/daemon.json` });
let daemonJsonParsed = JSON.parse(daemonJson);
let isUpdated = false;
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 });
}
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
@@ -230,21 +239,19 @@ export async function verifyRemoteDockerEngineFn(id: string) {
buildkit: true
}
}
if (isUpdated) {
await executeSSHCmd({ dockerId: id, command: `echo '${JSON.stringify(daemonJsonParsed)}' > /etc/docker/daemon.json` });
await executeSSHCmd({ dockerId: id, command: `systemctl restart docker` });
}
} catch (error) {
const daemonJsonParsed = {
"live-restore": true,
"features": {
"buildkit": true
}
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 executeSSHCmd({ dockerId: id, command: `echo '${JSON.stringify(daemonJsonParsed)}' > /etc/docker/daemon.json` });
await executeSSHCmd({ dockerId: id, command: `systemctl restart docker` });
} finally {
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
} catch (error) {
throw new Error('Error while verifying remote docker engine')
}
}
export async function verifyRemoteDockerEngine(request: FastifyRequest<OnlyId>, reply: FastifyReply) {

View File

@@ -4,7 +4,6 @@ import bcrypt from "bcryptjs";
import fs from 'fs/promises';
import yaml from 'js-yaml';
import {
asyncExecShell,
asyncSleep,
cleanupDockerStorage,
errorHandler,
@@ -13,6 +12,8 @@ import {
prisma,
uniqueName,
version,
sentryDSN,
executeCommand,
} from "../../../lib/common";
import { scheduler } from "../../../lib/scheduler";
import type { FastifyReply, FastifyRequest } from "fastify";
@@ -24,6 +25,35 @@ export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, saltRounds);
}
export async function backup(request: FastifyRequest) {
try {
const { backupData } = request.params;
let std = null;
const [id, backupType, type, zipped, storage] = backupData.split(':')
console.log(id, backupType, type, zipped, storage)
const database = await prisma.database.findUnique({ where: { id } })
if (database) {
// await executeDockerCmd({
// dockerId: database.destinationDockerId,
// command: `docker pull coollabsio/backup:latest`,
// })
std = await executeCommand({
dockerId: database.destinationDockerId,
command: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v coolify-local-backup:/app/backups -e CONTAINERS_TO_BACKUP="${backupData}" coollabsio/backup`
})
}
if (std.stdout) {
return std.stdout;
}
if (std.stderr) {
return std.stderr;
}
return 'nope';
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function cleanupManually(request: FastifyRequest) {
try {
const { serverId } = request.body;
@@ -110,14 +140,10 @@ export async function update(request: FastifyRequest<Update>) {
try {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
);
await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` });
await executeCommand({ shell: true, command: `env | grep COOLIFY > .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 coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` });
return {};
} else {
await asyncSleep(2000);
@@ -146,7 +172,7 @@ export async function restartCoolify(request: FastifyRequest<any>) {
const teamId = request.user.teamId;
if (teamId === "0") {
if (!isDev) {
asyncExecShell(`docker restart coolify`);
await executeCommand({ command: `docker restart coolify` });
return {};
} else {
return {};
@@ -189,7 +215,7 @@ export async function showDashboard(request: FastifyRequest) {
let foundUnconfiguredApplication = false;
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") {
if (((!application.buildPack || !application.branch) && !application.simpleDockerfile) || !application.destinationDockerId || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") {
foundUnconfiguredApplication = true
}
}
@@ -398,7 +424,8 @@ export async function getCurrentUser(
}
const pendingInvitations = await prisma.teamInvitation.findMany({ where: { uid: request.user.userId } })
return {
settings: await prisma.setting.findFirst(),
settings: await prisma.setting.findUnique({ where: { id: "0" } }),
sentryDSN,
pendingInvitations,
token,
...request.user,

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify';
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify, backup } from './handlers';
import { GetCurrentUser } from './types';
export interface Update {
@@ -52,6 +52,10 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post('/internal/cleanup', {
onRequest: [fastify.authenticate]
}, async (request) => await cleanupManually(request));
// fastify.get('/internal/backup/:backupData', {
// onRequest: [fastify.authenticate]
// }, async (request) => await backup(request));
};
export default root;

View File

@@ -1,5 +1,5 @@
import type { FastifyRequest } from 'fastify';
import { errorHandler, executeDockerCmd, prisma, createRemoteEngineConfiguration, executeSSHCmd } from '../../../../lib/common';
import { errorHandler, prisma, executeCommand } from '../../../../lib/common';
import os from 'node:os';
import osu from 'node-os-utils';
@@ -71,10 +71,10 @@ export async function showUsage(request: FastifyRequest) {
let { remoteEngine } = request.query
remoteEngine = remoteEngine === 'true' ? true : false
if (remoteEngine) {
const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` })
const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` })
const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` })
const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` })
const { stdout: stats } = await executeCommand({ sshCommand: true, dockerId: id, command: `vmstat -s` })
const { stdout: disks } = await executeCommand({ sshCommand: true, shell: true, dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` })
const { stdout: cpus } = await executeCommand({ sshCommand: true, dockerId: id, command: `nproc --all` })
const { stdout: cpuUsage } = await executeCommand({ sshCommand: true, shell: true, dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` })
const parsed: any = parseFromText(stats)
return {
usage: {

View File

@@ -4,7 +4,7 @@ import yaml from 'js-yaml';
import bcrypt from 'bcryptjs';
import cuid from 'cuid';
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common';
import { prisma, uniqueName, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, checkDomainsIsValidInDNS, checkExposedPort, listSettings, generateToken, executeCommand } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
import { checkContainer, } from '../../../../lib/docker';
import { removeService } from '../../../../lib/services/common';
@@ -48,14 +48,19 @@ export async function cleanupUnconfiguredServices(request: FastifyRequest) {
for (const service of services) {
if (!service.fqdn) {
if (service.destinationDockerId) {
await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: service.destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: service.destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}`
})
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({ dockerId: service.destinationDockerId, command: `docker stop -t 0 ${container}` })
await executeCommand({ dockerId: service.destinationDockerId, command: `docker rm --force ${container}` })
}
}
}
}
await removeService({ id: service.id });
}
@@ -73,55 +78,61 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
const { destinationDockerId, settings } = service;
let payload = {}
if (destinationDockerId) {
const { stdout: containers } = await executeDockerCmd({
const { stdout: containers } = await executeCommand({
dockerId: service.destinationDocker.id,
command:
`docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
const templates = await getTemplates();
let template = templates.find(t => t.type === service.type);
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', service.id));
for (const container of containersArray) {
let isRunning = false;
let isExited = false;
let isRestarting = false;
let isExcluded = false;
const containerObj = JSON.parse(container);
const exclude = template.services[containerObj.Names]?.exclude;
if (exclude) {
if (containers) {
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
const templates = await getTemplates();
let template = templates.find(t => t.type === service.type);
const templateStr = JSON.stringify(template)
if (templateStr) {
template = JSON.parse(templateStr.replaceAll('$$id', service.id));
}
for (const container of containersArray) {
let isRunning = false;
let isExited = false;
let isRestarting = false;
let isExcluded = false;
const containerObj = JSON.parse(container);
const exclude = template?.services[containerObj.Names]?.exclude;
if (exclude) {
payload[containerObj.Names] = {
status: {
isExcluded: true,
isRunning: false,
isExited: false,
isRestarting: false,
}
}
continue;
}
const status = containerObj.State
if (status === 'running') {
isRunning = true;
}
if (status === 'exited') {
isExited = true;
}
if (status === 'restarting') {
isRestarting = true;
}
payload[containerObj.Names] = {
status: {
isExcluded: true,
isRunning: false,
isExited: false,
isRestarting: false,
isExcluded,
isRunning,
isExited,
isRestarting
}
}
continue;
}
const status = containerObj.State
if (status === 'running') {
isRunning = true;
}
if (status === 'exited') {
isExited = true;
}
if (status === 'restarting') {
isRestarting = true;
}
payload[containerObj.Names] = {
status: {
isExcluded,
isRunning,
isExited,
isRestarting
}
}
}
}
}
return payload
} catch ({ status, message }) {
@@ -149,18 +160,24 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
}
}
parsedTemplate[realKey] = {
value,
name,
documentation: value.documentation || foundTemplate.documentation || 'https://docs.coollabs.io',
image: value.image,
files: value?.files,
environment: [],
fqdns: [],
hostPorts: [],
proxy: {}
}
if (value.environment?.length > 0) {
for (const env of value.environment) {
let [envKey, ...envValue] = env.split('=')
envValue = envValue.join("=")
const variable = foundTemplate.variables.find(v => v.name === envKey) || foundTemplate.variables.find(v => v.id === envValue)
let variable = null
if (foundTemplate?.variables) {
variable = foundTemplate?.variables.find(v => v.name === envKey) || foundTemplate?.variables.find(v => v.id === envValue)
}
if (variable) {
const id = variable.id.replaceAll('$$', '')
const label = variable?.label
@@ -186,15 +203,24 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
if (value?.proxy && value.proxy.length > 0) {
for (const proxyValue of value.proxy) {
if (proxyValue.domain) {
const variable = foundTemplate.variables.find(v => v.id === proxyValue.domain)
const variable = foundTemplate?.variables.find(v => v.id === proxyValue.domain)
if (variable) {
const { id, name, label, description, defaultValue, required = false } = variable
const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id , variableName: proxyValue.domain } })
const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id, variableName: proxyValue.domain } })
parsedTemplate[realKey].fqdns.push(
{ id, name, value: found?.value || '', label, description, defaultValue, required }
)
}
}
if (proxyValue.hostPort) {
const variable = foundTemplate?.variables.find(v => v.id === proxyValue.hostPort)
if (variable) {
const { id, name, label, description, defaultValue, required = false } = variable
const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id, variableName: proxyValue.hostPort } })
parsedTemplate[realKey].hostPorts.push(
{ id, name, value: found?.value || '', label, description, defaultValue, required }
)
}
}
}
}
@@ -208,7 +234,7 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
strParsedTemplate = strParsedTemplate.replaceAll('$$id', service.id)
strParsedTemplate = strParsedTemplate.replaceAll('$$core_version', service.version || foundTemplate.defaultVersion)
// replace $$fqdn
// replace $$workdir
if (workdir) {
strParsedTemplate = strParsedTemplate.replaceAll('$$workdir', workdir)
}
@@ -217,15 +243,17 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
if (service.serviceSetting.length > 0) {
for (const setting of service.serviceSetting) {
const { value, variableName } = setting
const regex = new RegExp(`\\$\\$config_${variableName.replace('$$config_', '')}\\"`, 'gi')
const regex = new RegExp(`\\$\\$config_${variableName.replace('$$config_', '')}\"`, 'gi')
if (value === '$$generate_fqdn') {
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + "\"" || '' + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '"' || '' + '"')
} else if (value === '$$generate_fqdn_slash') {
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '/' + '"')
} else if (value === '$$generate_domain') {
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn) + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn) + '"')
} else if (service.destinationDocker?.network && value === '$$generate_network') {
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network + '"')
} else {
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + '"')
}
}
}
@@ -233,15 +261,16 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
// replace $$secret
if (service.serviceSecret.length > 0) {
for (const secret of service.serviceSecret) {
const { name, value } = secret
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\\"`, 'gi')
const regex = new RegExp(`\\$\\$secret_${name}\\"`, 'gi')
let { name, value } = secret
name = name.toLowerCase()
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\"`, 'gi')
const regex = new RegExp(`\\$\\$secret_${name}\"`, 'gi')
if (value) {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"") + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + '"')
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"") + '"')
} else {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '' + '"')
strParsedTemplate = strParsedTemplate.replaceAll(regex, '' + '"')
}
}
}
@@ -291,42 +320,46 @@ export async function saveServiceType(request: FastifyRequest<SaveServiceType>,
let foundTemplate = templates.find(t => fixType(t.type) === fixType(type))
if (foundTemplate) {
foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id))
if (foundTemplate.variables.length > 0) {
if (foundTemplate.variables) {
if (foundTemplate.variables.length > 0) {
for (const variable of foundTemplate.variables) {
const { defaultValue } = variable;
const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
if (variable.defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) {
variable.value = cuid();
} else if (variable.defaultValue.startsWith('$$generate_token')) {
variable.value = generateToken()
} else {
variable.value = variable.defaultValue || '';
}
const foundVariableSomewhereElse = foundTemplate.variables.find(v => v.defaultValue.includes(variable.id))
if (foundVariableSomewhereElse) {
foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll(variable.id, variable.value)
}
}
}
for (const variable of foundTemplate.variables) {
const { defaultValue } = variable;
const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
if (variable.defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) {
variable.value = cuid();
} else {
variable.value = variable.defaultValue || '';
}
const foundVariableSomewhereElse = foundTemplate.variables.find(v => v.defaultValue.includes(variable.id))
if (foundVariableSomewhereElse) {
foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll(variable.id, variable.value)
}
}
}
for (const variable of foundTemplate.variables) {
if (variable.id.startsWith('$$secret_')) {
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSecret.create({
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
})
}
if (variable.id.startsWith('$$secret_')) {
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSecret.create({
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
})
}
}
if (variable.id.startsWith('$$config_')) {
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSetting.create({
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
})
}
if (variable.id.startsWith('$$config_')) {
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSetting.create({
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
})
}
}
}
}
@@ -418,7 +451,7 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
if (destinationDockerId) {
try {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const { stdout, stderr } = await executeCommand({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)
@@ -532,7 +565,7 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
}
if (isNew) {
if (!variableName) {
variableName = foundTemplate.variables.find(v => v.name === name).id
variableName = foundTemplate?.variables.find(v => v.name === name).id
}
await prisma.serviceSetting.create({ data: { name, value, variableName, service: { connect: { id } } } })
}
@@ -724,7 +757,7 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
if (destinationDockerId) {
const databaseUrl = serviceSecret.find((secret) => secret.name === 'DATABASE_URL');
if (databaseUrl) {
await executeDockerCmd({
await executeCommand({
dockerId: destinationDocker.id,
command: `docker exec ${id}-postgresql psql -H ${databaseUrl.value} -c "UPDATE users SET email_verified = true;"`
})
@@ -745,9 +778,10 @@ export async function cleanupPlausibleLogs(request: FastifyRequest<OnlyId>, repl
destinationDocker,
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeDockerCmd({
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.{};\"`
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
})
return await reply.code(201).send()
}
@@ -787,36 +821,42 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
if (user) ftpUser = user;
if (savedPassword) ftpPassword = decrypt(savedPassword);
const { stdout: password } = await asyncExecShell(
`echo ${ftpPassword} | openssl passwd -1 -stdin`
// TODO: rewrite these to usable without shell
const { stdout: password } = await executeCommand({
command:
`echo ${ftpPassword} | openssl passwd -1 -stdin`,
shell: true
}
);
if (destinationDockerId) {
try {
await fs.stat(hostkeyDir);
} catch (error) {
await asyncExecShell(`mkdir -p ${hostkeyDir}`);
await executeCommand({ command: `mkdir -p ${hostkeyDir}` });
}
if (!ftpHostKey) {
await asyncExecShell(
`ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519`
await executeCommand({
command:
`ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519`
}
);
const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`);
const { stdout: ftpHostKey } = await executeCommand({ command: `cat ${hostkeyDir}/${id}.ed25519` });
await prisma.wordpress.update({
where: { serviceId: id },
data: { ftpHostKey: encrypt(ftpHostKey) }
});
} else {
await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`);
await executeCommand({ command: `echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`, shell: true });
}
if (!ftpHostKeyPrivate) {
await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`);
const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`);
await executeCommand({ command: `ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa` });
const { stdout: ftpHostKeyPrivate } = await executeCommand({ command: `cat ${hostkeyDir}/${id}.rsa` });
await prisma.wordpress.update({
where: { serviceId: id },
data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) }
});
} else {
await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`);
await executeCommand({ command: `echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`, shell: true });
}
await prisma.wordpress.update({
@@ -831,9 +871,10 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
try {
const { found: isRunning } = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
if (isRunning) {
await executeDockerCmd({
await executeCommand({
dockerId: destinationDocker.id,
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`,
shell: true
})
}
} catch (error) { }
@@ -877,9 +918,9 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
`${hostkeyDir}/${id}.sh`,
`#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key\nuserdel -f xfs\nchown -R 33:33 /home/${ftpUser}/wordpress/`
);
await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`);
await executeCommand({ command: `chmod +x ${hostkeyDir}/${id}.sh` });
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
await executeDockerCmd({
await executeCommand({
dockerId: destinationDocker.id,
command: `docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
})
@@ -896,9 +937,10 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
data: { ftpPublicPort: null }
});
try {
await executeDockerCmd({
await executeCommand({
dockerId: destinationDocker.id,
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`,
shell: true
})
} catch (error) {
@@ -912,8 +954,10 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
return errorHandler({ status, message })
} finally {
try {
await asyncExecShell(
`rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
await executeCommand({
command:
`rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
}
);
} catch (error) { }

View File

@@ -1,9 +1,9 @@
import { promises as dns } from 'dns';
import { X509Certificate } from 'node:crypto';
import * as Sentry from '@sentry/node';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDev, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
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';
export async function listAllSettings(request: FastifyRequest) {
@@ -11,6 +11,13 @@ export async function listAllSettings(request: FastifyRequest) {
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) {
@@ -27,7 +34,8 @@ export async function listAllSettings(request: FastifyRequest) {
return {
settings,
certificates: cns,
sshKeys: unencryptedKeys
sshKeys: unencryptedKeys,
registries
}
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -35,7 +43,10 @@ export async function listAllSettings(request: FastifyRequest) {
}
export async function saveSettings(request: FastifyRequest<SaveSettings>, reply: FastifyReply) {
try {
const {
let {
previewSeparator,
numberOfDockerImagesKeptLocally,
doNotTrack,
fqdn,
isAPIDebuggingEnabled,
isRegistrationEnabled,
@@ -47,10 +58,29 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
DNSServers,
proxyDefaultRedirect
} = request.body
const { id } = await listSettings();
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: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled, }
data: { previewSeparator, numberOfDockerImagesKeptLocally, doNotTrack, isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled }
});
if (fqdn) {
await prisma.setting.update({ where: { id }, data: { fqdn } });
@@ -59,6 +89,14 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
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 })
@@ -91,7 +129,7 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
if (fqdn) fqdn = fqdn.toLowerCase();
const found = await isDomainConfigured({ id, fqdn });
if (found) {
throw "Domain already configured";
throw { message: "Domain already configured" };
}
if (isDNSCheckEnabled && !forceSave && !isDev) {
const hostname = request.hostname.split(':')[0]
@@ -131,8 +169,9 @@ export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: Fas
}
export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.body;
await prisma.sshKey.delete({ where: { id } })
await prisma.sshKey.deleteMany({ where: { id, teamId } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -141,9 +180,54 @@ export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply:
export async function deleteCertificates(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.body;
await asyncExecShell(`docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`)
await prisma.certificate.delete({ where: { id } })
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;
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 })
}
}
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 } } } })
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

@@ -2,8 +2,8 @@ import { FastifyPluginAsync } from 'fastify';
import { X509Certificate } from 'node:crypto';
import { encrypt, errorHandler, prisma } from '../../../../lib/common';
import { checkDNS, checkDomain, deleteCertificates, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
import { addDockerRegistry, checkDNS, checkDomain, deleteCertificates, deleteDockerRegistry, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey, setDockerRegistry } from './handlers';
import { AddDefaultRegistry, CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey, SetDefaultRegistry } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
@@ -20,6 +20,10 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<OnlyIdInBody>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
fastify.post<SetDefaultRegistry>('/registry', async (request, reply) => await setDockerRegistry(request, reply));
fastify.post<AddDefaultRegistry>('/registry/new', async (request, reply) => await addDockerRegistry(request, reply));
fastify.delete<OnlyIdInBody>('/registry', async (request, reply) => await deleteDockerRegistry(request, reply));
fastify.post('/upload', async (request) => {
try {
const teamId = request.user.teamId;
@@ -53,7 +57,6 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
});
fastify.delete<OnlyIdInBody>('/certificate', async (request, reply) => await deleteCertificates(request, reply))
// fastify.get('/certificates', async (request) => await getCertificates(request))
};
export default root;

View File

@@ -2,6 +2,9 @@ import { OnlyId } from "../../../../types"
export interface SaveSettings {
Body: {
previewSeparator: string,
numberOfDockerImagesKeptLocally: number,
doNotTrack: boolean,
fqdn: string,
isAPIDebuggingEnabled: boolean,
isRegistrationEnabled: boolean,
@@ -21,30 +24,46 @@ export interface DeleteDomain {
}
export interface CheckDomain extends OnlyId {
Body: {
fqdn: string,
forceSave: boolean,
dualCerts: boolean,
isDNSCheckEnabled: boolean,
fqdn: string,
forceSave: boolean,
dualCerts: boolean,
isDNSCheckEnabled: boolean,
}
}
export interface CheckDNS {
Params: {
domain: string,
domain: string,
}
}
export interface SaveSSHKey {
Body: {
privateKey: string,
privateKey: string,
name: string
}
}
export interface DeleteSSHKey {
Body: {
id: string
id: string
}
}
export interface OnlyIdInBody {
Body: {
id: string
}
}
}
export interface SetDefaultRegistry {
Body: {
id: string
username: string
password: string
}
}
export interface AddDefaultRegistry {
Body: {
url: string
name: string
username: string
password: string
}
}

View File

@@ -37,9 +37,7 @@ export async function getSource(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const { teamId } = request.user
const settings = await prisma.setting.findFirst({});
if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword);
if (id === 'new') {
return {

View File

@@ -71,7 +71,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
const githubSignature = request.headers['x-hub-signature-256']?.toString().toLowerCase();
if (!allowedGithubEvents.includes(githubEvent)) {
throw { status: 500, message: 'Event not allowed.' }
throw { status: 500, message: 'Event not allowed.', type: 'webhook' }
}
if (githubEvent === 'ping') {
return { pong: 'cool' }
@@ -89,9 +89,10 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
branch = body.pull_request.base.ref
}
if (!projectId || !branch) {
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!', type: 'webhook' }
}
const applicationsFound = await getApplicationFromDBWebhook(projectId, branch);
const settings = await prisma.setting.findUnique({ where: { id: '0' } });
if (applicationsFound && applicationsFound.length > 0) {
for (const application of applicationsFound) {
const buildId = cuid();
@@ -106,7 +107,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
const checksum = Buffer.from(githubSignature, 'utf8');
//@ts-ignore
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' }
throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?', type: 'webhook' }
};
}
@@ -156,7 +157,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
const sourceBranch = body.pull_request.head.ref
const sourceRepository = body.pull_request.head.repo.full_name
if (!allowedActions.includes(pullmergeRequestAction)) {
throw { status: 500, message: 'Action not allowed.' }
throw { status: 500, message: 'Action not allowed.', type: 'webhook' }
}
if (application.settings.previews) {
@@ -168,7 +169,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
}
);
if (!isRunning) {
throw { status: 500, message: 'Application not running.' }
throw { status: 500, message: 'Application not running.', type: 'webhook' }
}
}
if (
@@ -192,7 +193,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
data: {
pullmergeRequestId,
sourceBranch,
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
customDomain: `${protocol}${pullmergeRequestId}${settings.previewSeparator}${getDomain(application.fqdn)}`,
application: { connect: { id: application.id } }
}
})
@@ -257,8 +258,8 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
}
}
}
} catch ({ status, message }) {
return errorHandler({ status, message })
} catch ({ status, message, type }) {
return errorHandler({ status, message, type })
}
}

View File

@@ -44,8 +44,9 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
const webhookToken = request.headers['x-gitlab-token'];
if (!webhookToken && !isDev) {
throw { status: 500, message: 'Invalid webhookToken.' }
throw { status: 500, message: 'Invalid webhookToken.', type: 'webhook' }
}
const settings = await prisma.setting.findUnique({ where: { id: '0' } });
if (objectKind === 'push') {
const projectId = Number(project_id);
const branch = ref.split('/')[2];
@@ -95,10 +96,10 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
const pullmergeRequestId = request.body.object_attributes.iid.toString();
const projectId = Number(id);
if (!allowedActions.includes(action)) {
throw { status: 500, message: 'Action not allowed.' }
throw { status: 500, message: 'Action not allowed.', type: 'webhook' }
}
if (isDraft) {
throw { status: 500, message: 'Draft MR, do nothing.' }
throw { status: 500, message: 'Draft MR, do nothing.', type: 'webhook' }
}
const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch);
if (applicationsFound && applicationsFound.length > 0) {
@@ -113,11 +114,11 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
}
);
if (!isRunning) {
throw { status: 500, message: 'Application not running.' }
throw { status: 500, message: 'Application not running.', type: 'webhook' }
}
}
if (!isDev && application.gitSource.gitlabApp.webhookToken !== webhookToken) {
throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' }
throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!', type: 'webhook' }
}
if (
action === 'opened' ||
@@ -140,7 +141,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
data: {
pullmergeRequestId,
sourceBranch,
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
customDomain: `${protocol}${pullmergeRequestId}${settings.previewSeparator}${getDomain(application.fqdn)}`,
application: { connect: { id: application.id } }
}
})
@@ -188,7 +189,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
}
}
}
} catch ({ status, message }) {
return errorHandler({ status, message })
} catch ({ status, message, type }) {
return errorHandler({ status, message, type })
}
}

View File

@@ -1,5 +1,5 @@
import { FastifyRequest } from "fastify";
import { errorHandler, getDomain, isDev, prisma, executeDockerCmd, fixType } from "../../../lib/common";
import { errorHandler, getDomain, isDev, prisma, executeCommand } from "../../../lib/common";
import { getTemplates } from "../../../lib/services";
import { OnlyId } from "../../../types";
@@ -171,8 +171,8 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
};
try {
const { id = null } = request.params;
const settings = await prisma.setting.findFirst();
if (settings.isTraefikUsed && settings.proxyDefaultRedirect) {
const coolifySettings = await prisma.setting.findFirst();
if (coolifySettings.isTraefikUsed && coolifySettings.proxyDefaultRedirect) {
traefik.http.routers['catchall-http'] = {
entrypoints: ["web"],
rule: "HostRegexp(`{catchall:.*}`)",
@@ -190,7 +190,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
traefik.http.middlewares['redirect-regexp'] = {
redirectregex: {
regex: '(.*)',
replacement: settings.proxyDefaultRedirect,
replacement: coolifySettings.proxyDefaultRedirect,
permanent: false
}
}
@@ -263,10 +263,12 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const runningContainers = {}
applications.forEach((app) => dockerIds.add(app.destinationDocker.id));
for (const dockerId of dockerIds) {
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` })
const containersArray = container.trim().split('\n');
if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` })
if (container) {
const containersArray = container.trim().split('\n');
if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray
}
}
}
for (const application of applications) {
@@ -287,11 +289,10 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
if (
!runningContainers[destinationDockerId] ||
runningContainers[destinationDockerId].length === 0 ||
!runningContainers[destinationDockerId].includes(id)
runningContainers[destinationDockerId].filter((container) => container.startsWith(id)).length === 0
) {
continue
}
if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration))
if (services.length > 0) {
@@ -333,20 +334,22 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) }
if (previews) {
const { stdout } = await executeDockerCmd({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` })
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
const previewDomain = `${container.split('-')[1]}.${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, '');
const pathPrefix = '/'
const serviceId = `${container}-${port || 'default'}`
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, previewDomain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) }
const { stdout } = await executeCommand({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` })
if (stdout) {
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
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(serviceId, previewDomain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) }
}
}
}
}
@@ -360,10 +363,12 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const runningContainers = {}
services.forEach((app) => dockerIds.add(app.destinationDocker.id));
for (const dockerId of dockerIds) {
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` })
const containersArray = container.trim().split('\n');
if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` })
if (container) {
const containersArray = container.trim().split('\n');
if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray
}
}
}
for (const service of services) {
@@ -396,8 +401,8 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
}
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
for (const oneService of Object.keys(found.services)) {
const isProxyConfiguration = found.services[oneService].proxy;
if (isProxyConfiguration) {
const isDomainConfiguration = found?.services[oneService]?.proxy?.filter(p => p.domain) ?? [];
if (isDomainConfiguration.length > 0) {
const { proxy } = found.services[oneService];
for (let configuration of proxy) {
if (configuration.domain) {
@@ -432,20 +437,24 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
}
} else {
if (found.services[oneService].ports && found.services[oneService].ports.length > 0) {
let port = found.services[oneService].ports[0]
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port')
if (foundPortVariable) {
port = foundPortVariable.value
for (let [index, port] of found.services[oneService].ports.entries()) {
if (port == 22) continue;
if (index === 0) {
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port')
if (foundPortVariable) {
port = foundPortVariable.value
}
}
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const pathPrefix = '/'
const isCustomSSL = false
const serviceId = `${oneService}-${port || 'default'}`
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) }
}
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const pathPrefix = '/'
const isCustomSSL = false
const serviceId = `${oneService}-${port || 'default'}`
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) }
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
node_modules
backup/*

27
apps/backup/Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
ARG PNPM_VERSION=7.17.1
FROM node:18-slim as build
WORKDIR /app
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
COPY ./package*.json .
RUN pnpm install -p
COPY . .
# Production build
FROM node:18-slim
ARG DOCKER_VERSION=20.10.18
ARG TARGETPLATFORM
ENV NODE_ENV production
WORKDIR /app
RUN apt update && apt -y install curl
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker
RUN chmod +x /usr/bin/docker
COPY --from=minio/mc:latest /usr/bin/mc /usr/bin/mc
COPY --from=build /app/ .
ENV CHECKPOINT_DISABLE=1
CMD node /app/src/index.mjs

View File

24
apps/backup/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "backup",
"version": "0.0.1",
"description": "",
"author": "Andras Bacsai",
"license": "Apache-2.0",
"main": "index.mjs",
"type": "module",
"scripts": {
"start": "NODE_ENV=production node src/index.mjs",
"dev": "pnpm cleanup && NODE_ENV=development node src/index.mjs",
"build": "docker build -t backup .",
"test": "pnpm build && docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock -v /root/devel/coolify/apps/backup/backups:/app/backups -e CONTAINERS_TO_BACKUP='clatmhc6000008lvb5a5tnvsk:database:mysql:local' backup",
"cleanup": "rm -rf backups/*"
},
"keywords": [],
"dependencies": {
"@aws-sdk/client-s3": "^3.222.0",
"@aws-sdk/lib-storage": "^3.222.0",
"cuid": "2.1.8",
"dotenv": "16.0.3",
"zx": "7.1.1"
}
}

126
apps/backup/src/index.mjs Normal file
View File

@@ -0,0 +1,126 @@
import * as dotenv from 'dotenv';
dotenv.config()
import 'zx/globals';
import cuid from 'cuid';
import { S3, PutObjectCommand } from "@aws-sdk/client-s3";
import fs from 'fs';
const isDev = process.env.NODE_ENV === 'development'
$.verbose = !!isDev
if (!process.env.CONTAINERS_TO_BACKUP && !isDev) {
console.log(chalk.red(`No containers to backup!`))
process.exit(1)
}
const mysqlGzipLocal = 'clb6c9ue4000a8lputdd5g1cl:database:mysql:gzip:local';
const mysqlRawLocal = 'clb6c9ue4000a8lputdd5g1cl:database:mysql:raw:local';
const postgresqlGzipLocal = 'clb6c15yi00008lpuezop7cy0:database:postgresql:gzip:local';
const postgresqlRawLocal = 'clb6c15yi00008lpuezop7cy0:database:postgresql:raw:local';
const minio = 'clb6c9ue4000a8lputdd5g1cl:database:mysql:gzip:minio|http|min.arm.coolify.io|backups|<access_key>|<secret_key>';
const digitalOcean = 'clb6c9ue4000a8lputdd5g1cl:database:mysql:gzip:do|https|fra1.digitaloceanspaces.com|backups|<access_key>|<secret_key>';
const devContainers = [mysqlGzipLocal, mysqlRawLocal, postgresqlGzipLocal, postgresqlRawLocal]
const containers = isDev
? devContainers
: process.env.CONTAINERS_TO_BACKUP.split(',')
const backup = async (container) => {
const id = cuid()
const [name, backupType, type, zipped, storage] = container.split(':')
const directory = `backups`;
const filename = zipped === 'raw'
? `${name}-${type}-${backupType}-${new Date().getTime()}.sql`
: `${name}-${type}-${backupType}-${new Date().getTime()}.tgz`
const backup = `${directory}/${filename}`;
try {
await $`docker inspect ${name.split(' ')[0]}`.quiet()
if (backupType === 'database') {
if (type === 'mysql') {
console.log(chalk.blue(`Backing up ${name}:${type}...`))
const { stdout: rootPassword } = await $`docker exec ${name} printenv MYSQL_ROOT_PASSWORD`.quiet()
if (zipped === 'raw') {
await $`docker exec ${name} sh -c "exec mysqldump --all-databases -uroot -p${rootPassword.trim()}" > ${backup}`
} else if (zipped === 'gzip') {
await $`docker exec ${name} sh -c "exec mysqldump --all-databases -uroot -p${rootPassword.trim()}" | gzip > ${backup}`
}
}
if (type === 'postgresql') {
console.log(chalk.blue(`Backing up ${name}:${type}...`))
const { stdout: userPassword } = await $`docker exec ${name} printenv POSTGRES_PASSWORD`
const { stdout: user } = await $`docker exec ${name} printenv POSTGRES_USER`
if (zipped === 'raw') {
await $`docker exec ${name} sh -c "exec pg_dumpall -c -U${user.trim()}" -W${userPassword.trim()}> ${backup}`
} else if (zipped === 'gzip') {
await $`docker exec ${name} sh -c "exec pg_dumpall -c -U${user.trim()}" -W${userPassword.trim()} | gzip > ${backup}`
}
}
const [storageType, ...storageArgs] = storage.split('|')
if (storageType !== 'local') {
let s3Protocol, s3Url, s3Bucket, s3Key, s3Secret = null
if (storageArgs.length > 0) {
[s3Protocol, s3Url, s3Bucket, s3Key, s3Secret] = storageArgs
}
if (storageType === 'minio') {
if (!s3Protocol || !s3Url || !s3Bucket || !s3Key || !s3Secret) {
console.log(chalk.red(`Invalid storage arguments for ${name}:${type}!`))
return
}
await $`mc alias set ${id} ${s3Protocol}://${s3Url} ${s3Key} ${s3Secret}`
await $`mc stat ${id}`
await $`mc cp ${backup} ${id}/${s3Bucket}`
await $`rm ${backup}`
await $`mc alias rm ${id}`
} else if (storageType === 'do') {
if (!s3Protocol || !s3Url || !s3Bucket || !s3Key || !s3Secret) {
console.log(chalk.red(`Invalid storage arguments for ${name}:${type}!`))
return
}
console.log({ s3Protocol, s3Url, s3Bucket, s3Key, s3Secret })
console.log(chalk.blue(`Uploading ${name}:${type} to DigitalOcean Spaces...`))
const readstream = fs.createReadStream(backup)
const bucketParams = {
Bucket: s3Bucket,
Key: filename,
Body: readstream
};
const s3Client = new S3({
forcePathStyle: false,
endpoint: `${s3Protocol}://${s3Url}`,
region: "us-east-1",
credentials: {
accessKeyId: s3Key,
secretAccessKey: s3Secret
},
});
try {
const data = await s3Client.send(new PutObjectCommand(bucketParams));
console.log(chalk.green("Successfully uploaded backup: " +
bucketParams.Bucket +
"/" +
bucketParams.Key
)
);
return data;
} catch (err) {
console.log("Error", err);
}
}
}
}
console.log(chalk.green(`Backup of ${name}:${type} complete!`))
} catch (error) {
console.log(chalk.red(`Backup of ${name}:${type} failed!`))
console.log(chalk.red(error))
}
}
const promises = []
for (const container of containers) {
// await backup(container);
promises.push(backup(container))
}
await Promise.all(promises)

View File

@@ -14,38 +14,40 @@
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@floating-ui/dom": "1.0.3",
"@playwright/test": "1.27.1",
"@floating-ui/dom": "1.0.6",
"@playwright/test": "1.28.0",
"@popperjs/core": "2.11.6",
"@sveltejs/kit": "1.0.0-next.405",
"@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.41.0",
"@typescript-eslint/parser": "5.41.0",
"autoprefixer": "10.4.12",
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
"autoprefixer": "10.4.13",
"classnames": "2.3.2",
"eslint": "8.26.0",
"eslint": "8.28.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0",
"flowbite": "1.5.3",
"flowbite-svelte": "0.27.11",
"postcss": "8.4.18",
"flowbite": "1.5.4",
"flowbite-svelte": "0.28.0",
"postcss": "8.4.19",
"prettier": "2.7.1",
"prettier-plugin-svelte": "2.8.0",
"svelte": "3.52.0",
"prettier-plugin-svelte": "2.8.1",
"svelte": "3.53.1",
"svelte-check": "2.9.2",
"svelte-preprocess": "4.10.7",
"tailwindcss": "3.2.1",
"tailwindcss": "3.2.4",
"tailwindcss-scrollbar": "0.1.0",
"tslib": "2.4.0",
"typescript": "4.8.4",
"vite": "3.2.0"
"tslib": "2.4.1",
"typescript": "4.9.3",
"vite": "3.2.4"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "1.0.0-next.46",
"@tailwindcss/typography": "0.5.7",
"@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",
"daisyui": "2.33.0",
"daisyui": "2.41.0",
"dayjs": "1.11.6",
"js-cookie": "3.0.1",
"js-yaml": "4.1.0",

View File

@@ -1,4 +1,13 @@
import * as Sentry from '@sentry/svelte';
export async function handle({ event, resolve }) {
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'
};
};

View File

@@ -11,7 +11,7 @@ export function getAPIUrl() {
return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
}
return dev
? 'http://localhost:3001'
? `http://${window.location.hostname}:3001`
: 'http://localhost:3000';
}
export function getWebhookUrl(type: string) {

View File

@@ -3,6 +3,8 @@ import { addToast } from '$lib/store';
export const asyncSleep = (delay: number) =>
new Promise((resolve) => setTimeout(resolve, delay));
export let initials = (str:string) => (str||'').split(' ').map( (wrd) => wrd[0]).join('')
export function errorNotification(error: any | { message: string }): void {
if (error.message) {
if (error.message === 'Cannot read properties of undefined (reading \'postMessage\')') {
@@ -87,4 +89,4 @@ export function handlerNotFoundLoad(error: any, url: URL) {
export function getRndInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}

View File

@@ -0,0 +1,4 @@
<nav class="header justify-between px-2 mb-5 lg:px-10">
<slot />
<slot name="actions" />
</nav>

View File

@@ -15,7 +15,7 @@
export let placeholder = '';
export let inputStyle = '';
let disabledClass = 'bg-coolback disabled:bg-coolblack w-full';
let disabledClass = 'input input-primary bg-coolback disabled:bg-coolblack w-full';
let isHttps = browser && window.location.protocol === 'https:';
function copyToClipboard() {

View File

@@ -0,0 +1,11 @@
<script>
import { locale, locales } from '$lib/translations';
</script>
<div>
<select bind:value={$locale} class="w-14">
{#each $locales as l}
<option value={l}>{l}</option>
{/each}
</select>
</div>

View File

@@ -1,7 +1,14 @@
<script lang="ts">
import { dev } from '$app/env';
import { get, post } from '$lib/api';
import { addToast, appSession, features, updateLoading, isUpdateAvailable } from '$lib/store';
import {
addToast,
appSession,
features,
updateLoading,
isUpdateAvailable,
latestVersion
} from '$lib/store';
import { asyncSleep, errorNotification } from '$lib/common';
import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte';
@@ -11,15 +18,17 @@
loading: false,
success: null
};
let latestVersion = 'latest';
async function update() {
updateStatus.loading = true;
try {
if (dev) {
await asyncSleep(4000);
localStorage.setItem('lastVersion', $appSession.version);
await asyncSleep(1000);
updateStatus.loading = false;
return window.location.reload();
} else {
await post(`/update`, { type: 'update', latestVersion });
localStorage.setItem('lastVersion', $appSession.version);
await post(`/update`, { type: 'update', latestVersion: $latestVersion });
addToast({
message: 'Update completed.<br><br>Waiting for the new version to start...',
type: 'success'
@@ -62,7 +71,7 @@
$updateLoading = true;
const data = await get(`/update`);
if (overrideVersion || data?.isUpdateAvailable) {
latestVersion = overrideVersion || data.latestVersion;
$latestVersion = overrideVersion || data.latestVersion;
if (overrideVersion) {
$isUpdateAvailable = true;
} else {
@@ -91,7 +100,7 @@
{#if updateStatus.loading}
<svg
xmlns="http://www.w3.org/2000/svg"
class="lds-heart h-8 w-8"
class="lds-heart h-8 w-8 mx-auto"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"

View File

@@ -0,0 +1,14 @@
<script>
import Tooltip from '../Tooltip.svelte';
import { initials } from '$lib/common';
export let name;
export let thingId;
let id = 'destination' + thingId;
</script>
{#if (name || '').length > 0}
<span class="badge rounded uppercase text-xs " {id}>
{initials(name)}
</span>
<Tooltip triggeredBy="#{id}" placement="right">{name}</Tooltip>
{/if}

View File

@@ -0,0 +1,19 @@
<div title="Public">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
</div>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { getStatus } from '$lib/container/status';
import { onDestroy, onMount } from 'svelte';
export let thing: any;
let getting = getStatus(thing);
let refreshing: any;
let status: any;
// AutoUpdates Status every 5 seconds
onMount(() => {
refreshing = setInterval(() => {
getStatus(thing).then((r) => (status = r));
}, 5000);
});
onDestroy(() => {
clearInterval(refreshing);
});
</script>
{#await getting}
<span class="badge badge-lg rounded uppercase">...</span>
{:then status}
<span class="badge badge-lg rounded uppercase badge-status-{status}">
{status}
</span>
{/await}

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import Tooltip from '../Tooltip.svelte';
import { initials } from '$lib/common';
export let teams: any;
export let thing: any;
let id = 'teams' + thing.id;
</script>
<span>
{#each teams as team}
<a href={`/iam/teams/${team.id}`} {id} class="no-underline">
Team: {initials(team.name)}
</a>
<Tooltip triggeredBy="#{id}" placement="right" color="bg-destinations">{team.name}</Tooltip>
{/each}
</span>

View File

@@ -0,0 +1,5 @@
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:md:grid-cols-3 xl:grid-cols-4 p-4 lg:px-10"
>
<slot />
</div>

View File

@@ -42,4 +42,6 @@
<Icons.Heroku {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'compose'}
<Icons.Compose {isAbsolute} />
{:else if application.simpleDockerfile}
<Icons.Docker {isAbsolute} />
{/if}

View File

@@ -0,0 +1,26 @@
<script>
export let isAbsolute=false;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'absolute top-0 left-0 -m-2 h-12 w-12 text-sky-500' : 'mx-auto w-8 h-8 text-sky-500'}
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>

View File

@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-2 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>

After

Width:  |  Height:  |  Size: 505 B

View File

@@ -1,9 +1,12 @@
<script lang="ts">
export let type: string;
export let isAbsolute = false;
let fallback = '/icons/default.png';
const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback);
let extension = 'png';
let svgs = [
'pocketbase',
'gitea',
'languagetool',
'meilisearch',
'n8n',
@@ -31,8 +34,14 @@
function generateClass() {
switch (name) {
case 'n8n':
if (isAbsolute) {
return 'w-12 h-12 absolute -m-9 -mt-12';
}
return 'w-12 h-12 -mt-3';
case 'weblate':
if (isAbsolute) {
return 'w-12 h-12 absolute -m-9 -mt-12';
}
return 'w-12 h-12 -mt-3';
default:
return isAbsolute ? 'w-10 h-10 absolute -m-4 -mt-9 left-0' : 'w-10 h-10';
@@ -41,5 +50,10 @@
</script>
{#if name}
<img class={generateClass()} src={`/icons/${name}.${extension}`} alt={`Icon of ${name}`} />
<img
class={generateClass()}
src={`/icons/${name}.${extension}`}
on:error={handleError}
alt={`Icon of ${name}`}
/>
{/if}

View File

@@ -0,0 +1,11 @@
<svg viewBox="0 0 128 128" class="h-10 w-10">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,21 @@
<svg viewBox="0 0 128 128" class="h-10 w-10">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1009 B

View File

@@ -0,0 +1,73 @@
//
// Maps Container ID x Operation Status
//
// Example response of $status => {'123asdf': 'degraded', '124asdf': 'running'}
import { writable, get as getStore } from 'svelte/store';
import { get } from '$lib/api';
export let containerStatus = writable({});
let PERMITED_STATUS = ['loading', 'running', 'healthy', 'building', 'degraded', 'stopped', 'error'];
// refreshStatus([{id}])
export async function refreshStatus(list: Array<any>) {
for (const item of list) {
setStatus(item.id, 'loading');
getStatus(item, true);
}
}
export async function getStatus(resource: any, force: boolean = false) {
const { id, buildPack, dualCerts, engine, simpleDockerfile } = resource;
let newStatus = 'stopped';
// Already set and we're not forcing
if (getStore(containerStatus)[id] && !force) return getStore(containerStatus)[id];
try {
if (buildPack || simpleDockerfile) { // Application
const response = await get(`/applications/${id}/status`);
newStatus = parseApplicationsResponse(response);
} else if (typeof dualCerts !== 'undefined') { // Service
const response = await get(`/services/${id}/status`);
newStatus = parseServiceResponse(response);
} else if (typeof engine !== 'undefined') { // Destination/Server
const response = await get(`/destinations/${id}/status`);
newStatus = response.isRunning ? 'running' : 'stopped';
} else { // Database
const response = await get(`/databases/${id}/status`);
newStatus = response.isRunning ? 'running' : 'stopped';
}
} catch (error) {
newStatus = 'error';
}
setStatus(id, newStatus);
// console.log("GOT:", id, newStatus)
return newStatus
}
const setStatus = (thingId, newStatus) => {
if (!PERMITED_STATUS.includes(newStatus))
throw (`Change to ${newStatus} is not permitted. Try: ${PERMITED_STATUS.join(', ')}`);
containerStatus.update(n => Object.assign(n, { thingId: newStatus }));
};
// -- Response Parsing
function parseApplicationsResponse(list: Array<any>) {
if (list.length === 0) return 'stopped';
if (list.length === 1) return list[0].status.isRunning ? 'running' : 'stopped';
return allWorking(list.map((el: any) => el.status.isRunning))
}
function parseServiceResponse(response: any) {
if (Object.keys(response).length === 0) return 'stopped';
let list = Object.keys(response).map((el) => el.status.isRunning)
return allWorking(list) ? 'running' : 'degraded'
}
function allWorking(list: Array<any>) {
return list.reduce((acum: boolean, res: boolean) => acum && res) ? 'running' : 'degraded';
}

View File

@@ -1,4 +1,7 @@
{
"fr": "Français",
"pt": "Português",
"es": "Espanhol",
"ko": "Korean",
"en": "English"
}

View File

@@ -159,7 +159,7 @@
"storage_saved": "Storage saved.",
"storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/app/example</span> in the container as <span class='text-settings '>/app</span> is <span class='text-settings '>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/example</span> between deployments.<br><br>Your application's data is copied to <span class='text-settings '>/app</span> inside the container, you can preserve data under it as well, like <span class='text-settings '>/app/db</span>.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
},
"deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",

View File

@@ -0,0 +1,341 @@
{
"layout":{
"update_done":"Actualización completada.",
"wait_new_version_startup":"Esperando que comience la nueva versión.",
"new_version":"Nueva versión accesible. Recargando.",
"switch_to_a_different_team":"Cambia a otro equipo.",
"update_available":"Actualización disponible"
},
"error":{
"you_can_find_your_way_back":"Puedes encontrar tu camino de vuelta",
"here":"Aquí.",
"you_are_lost":"¡Estás perdido! ¡Pero no tengas miedo!"
},
"index":{
"dashboard":"Dashboard",
"applications":"Aplicaciones",
"destinations":"Destinos",
"git_sources":"Fuentes Git",
"databases":"Bases de datos",
"services":"Servicios",
"teams":"Equipos",
"not_implemented_yet":"Aún no se ha aplicado",
"database":"Base de datos",
"settings":"Ajustes",
"global_settings":"Ajustes mundiales",
"secret":"Secret",
"team":"Equipo",
"logout":"Cerrar sesión"
},
"login":{
"already_logged_in":"Ya se ha registrado.",
"authenticating":"Autenticando.",
"login":"Iniciar sesión"
},
"forms":{
"password":"Contraseña",
"email":"Dirección de correo electrónico",
"passwords_not_match":"Las contraseñas no coinciden.",
"password_again":"Contraseña de nuevo",
"save":"Guardar",
"saving":"Salvando.",
"name":"Nombre",
"value":"Valor",
"action":"Acciones",
"is_required":"es necesario.",
"add":"Añadir",
"set":"Set",
"remove":"Retirar",
"path":"Camino",
"confirm_continue":"¿Estás seguro de continuar?",
"must_be_stopped_to_modify":"Debe ser detenido para modificar.",
"port":"Puerto",
"default":"Por defecto",
"base_directory":"Base Directory",
"publish_directory":"Publish Directory",
"generated_automatically_after_start":"Generado automáticamente después del inicio",
"roots_password":"La contraseña de Root",
"root_user":"Usuario raíz",
"eg":"eg",
"user":"Usuario",
"loading":"Carga.",
"version":"Versión",
"host":"Host",
"already_used_for":"##########################################################################################################################################################################################################################################################",
"configuration":"Configuración",
"engine":"Motor",
"network":"Red",
"ip_address":"Dirección IP",
"ssh_private_key":"SSH Clave privada",
"type":"Tipo",
"html_url":"URL",
"api_url":"API",
"organization":"Organización",
"new_password":"Nueva contraseña",
"super_secure_new_password":"Super seguro nueva contraseña",
"submit":"Submit",
"default_email_address":"Dirección de correo electrónico predeterminada",
"default_password":"Contraseña predeterminada",
"username":"Nombre de usuario",
"root_db_user":"Root DB Usuario",
"root_db_password":"Root DB Contraseña",
"api_port":"API Port",
"verifying":"Verificación",
"verify_emails_without_smtp":"Verificar correos electrónicos sin SMTP",
"extra_config":"Extra Config",
"select_a_service":"Seleccione un Servicio",
"select_a_service_version":"Seleccione una versión de servicio",
"removing":"Retirándose.",
"remove_domain":"Eliminar el dominio",
"public_port_range":"Public Port Range",
"public_port_range_explainer":"Puertos utilizados para exponer bases de datos/servicios/servicios internos. Añádalos a su cortafuegos (si es aplicable).Seguido se indicará una gama de puertos, por ejemplo: tachuelas clase='text-settings '9000-9100 seg/span",
"no_actions_available":"No se dispone de medidas",
"admin_api_key":"Clave de API de Admin"
},
"register":{
"register":"Registro",
"registering":"Registro.",
"first_user":"Está registrando al primer usuario. Será el administrador de tu instancia de Coolify."
},
"reset":{
"reset_password":"Reset",
"invalid_secret_key":"Una llave secreta inválida.",
"secret_key":"Secret Key",
"find_path_secret_key":"Puedes encontrarlo en ~coolify/.env (COOLIFY_SECRET_KEY)"
},
"application":{
"configuration":{
"buildpack":{
"choose_this_one":"Elige esta."
},
"branch_already_in_use":"Esta rama ya es utilizada por otra aplicación. Webhooks no funcionará en este caso para ambas aplicaciones. ¿Seguro que quieres usarlo?",
"no_repositories_configured":"No hay repositorios configurados para su aplicación Git.",
"configure_it_now":"Configure ahora",
"loading_repositories":"Carga de repositorios ...",
"select_a_repository":"Seleccione un repositorio",
"loading_branches":"Cargando ramas ...",
"select_a_repository_first":"Por favor seleccione un repositorio primero",
"select_a_branch":"Por favor seleccione una rama",
"loading_groups":"Grupos de carga.",
"select_a_group":"Seleccione un grupo",
"loading_projects":"Cargando proyectos.",
"select_a_project":"Seleccione un proyecto",
"no_projects_found":"No se han encontrado proyectos",
"no_branches_found":"No hay ramas encontradas",
"configure_build_pack":"Configure Build Pack",
"scanning_repository_suggest_build_pack":"Repositorio de exploración para sugerir un paquete de construcción para usted.",
"found_lock_file":"encontrado archivo de bloqueo para {{packageManager}}.Seguido de comandos predefinidos.",
"configure_destination":"Configurar Destino",
"no_configurable_destination":"No hay destino configurable encontrado",
"select_a_repository_project":"Seleccione un Repositorio / Proyecto",
"select_a_git_source":"Seleccione una fuente de Git",
"no_configurable_git":"No se encontró una fuente de Git configurable",
"configuration_missing":"Falta de configuración"
},
"build":{
"queued_waiting_exec":"Queued and waiting for execution.",
"build_logs_of":"Construir registros de",
"running":"Corriendo",
"queued":"Queued",
"finished_in":"Terminado en",
"load_more":"Carga más",
"no_logs":"No hay registros encontrados",
"waiting_logs":"Esperando los registros."
},
"preview":{
"need_during_buildtime":"¿Necesitas durante el tiempo de construcción?",
"setup_secret_app_first":"Puede añadir secretos a las implementaciones PR/MR. Por favor, agregue secretos a la aplicación primero. √≠br]Useful for creating יspan class='text-settings 'staging won/span environments.",
"values_overwriting_app_secrets":"Estos valores sobrescriben los secretos de aplicación en las implementaciones PR/MR. Útil para la creación de clase 0'text-settings 'estaging significan ambientes / paño.",
"redeploy":"Redistribución",
"no_previews_available":"No hay vistas previas disponibles"
},
"secrets":{
"secret_saved":"Secreto salvado.",
"use_isbuildsecret":"Use isBuildSecret",
"secrets_for":"Secretos para"
},
"storage":{
"path_is_required":"Se requiere camino.",
"storage_saved":"Almacenamiento guardado.",
"storage_updated":"Almacenamiento actualizado.",
"storage_deleted":"Almacenamiento eliminado.",
"persistent_storage_explainer":"Puede especificar cualquier carpeta que desee ser persistente a través de las implementaciones.Seguido de la clase='text-settings 'ejemplo observado/span significa que preservará <span class='text-settings 'app/example observado/span en el contenedor como ierespan class='text-settings 'appcanta/span es Гspan clase='text-setting directory Esto es útil para almacenar datos tales como una clase de =span='text-settings √≥'database (SQLite) obtenidos/spanilo o a יspan class='text-settings 'cache made/span."
},
"deployment_queued":"Despliegue apagado.",
"confirm_to_delete":"¿Estás seguro de que te gustaría borrar?",
"stop_application":"Stop Application",
"permission_denied_stop_application":"Usted no tiene permiso para detener la aplicación.",
"rebuild_application":"Rebuild Application",
"permission_denied_rebuild_application":"No tienes permiso para reconstruir la aplicación.",
"build_and_start_application":"Despliegue",
"permission_denied_build_and_start_application":"Usted no tiene permiso para implementar la aplicación.",
"configurations":"Configuraciones",
"secret":"Secretos",
"persistent_storage":"Almacenamiento persistente",
"previews":"Avances",
"logs":"Registros de aplicaciones",
"build_logs":"Logs de construcción",
"delete_application":"Suprimir",
"permission_denied_delete_application":"Usted no tiene permiso para borrar esta aplicación",
"domain_already_in_use":"El dominio ya se utiliza.",
"dns_not_set_error":"DNS no se establece correctamente ni se propogó para {{domain}}.Seguido manualmente indicando su configuración DNS.",
"domain_required":"El dominio es necesario.",
"settings_saved":"Configuración guardada.",
"dns_not_set_partial_error":"DNS not set",
"domain_not_valid":"No puede resolver el dominio o no apunta a la dirección IP del servidor. Por favor, compruebe su configuración de DNS e inténtelo de nuevo.",
"git_source":"Fuente de Git",
"git_repository":"Repositorio Git",
"build_pack":"Paquete de construcción",
"base_image":"Imagen de despliegue",
"base_image_explainer":"Imagen que se utilizará para el despliegue.",
"base_build_image":"Construir imagen",
"base_build_image_explainer":"Imagen que se utilizará durante el proceso de construcción.",
"destination":"Destino",
"application":"Aplicación",
"url_fqdn":"URL (FQDN)",
"domain_fqdn":"Dominio (FQDN)",
"https_explainer":"Si especificas יspan class='text-settings 'https seleccionado/span, la aplicación será accesible sólo en https. Certificado SSL se generará para usted.Seguidobr títuloSi especificas יspan class='text-settings 'www dañado/span, la aplicación será redireccionada (302) de non-www y viceversa.Seguradobr acordadobr título Para modificar el dominio, primero debe detener la aplicación. Debe establecer su DNS para apuntar al servidor IP con antelación.",
"ssl_www_and_non_www":"Generar SSL para www y non-www?",
"ssl_explainer":"Generará certificados tanto para www como para no www. Necesitas tener las entradas de DNS de la clase 0'' text-settings' inteligenteboth DNS seleccionadas/span título con antelación.Seguridad si esperas tener visitantes en ambos.",
"install_command":"Instalar el Comando",
"build_command":"Mando de construcción",
"start_command":"Comando de Inicio",
"directory_to_use_explainer":"Directorio para usar como base para todos los comandos. Podría ser útil con la clase 0'text-settings 'monorepos obtenidos / span.",
"publish_directory_explainer":"Directorio que contiene todos los activos para el despliegue. Por ejemplo: \"Clasificación de texto\": \"Clasificación de texto\": \"Clasificación de texto\": \"Clasificación de texto\": \"Clasificación de texto\" (p. ej., p. ej.:",
"features":"Características",
"enable_automatic_deployment":"Permitir el despliegue automático",
"enable_auto_deploy_webhooks":"Permitir el despliegue automático a través de los dispositivos web.",
"enable_mr_pr_previews":"Habilitar las previsiones MR/PR",
"expose_a_port":"Exponga un puerto",
"enable_preview_deploy_mr_pr_requests":"Permitir despliegues de previsualización de las solicitudes de tira o fusión.",
"debug_logs":"Debug Logs",
"enable_debug_log_during_build":"Activar registros de depuración durante fase de construcción.Seguidobr contactos clase='text-settings .'Informaciones positivas realizadas/span título podría ser visible y guardado en registros.",
"cant_activate_auto_deploy_without_repo":"No puede activar implementaciones automáticas hasta que sólo una aplicación se defina para este repositorio / rama.",
"no_applications_found":"No se han encontrado aplicaciones",
"secret__batch_dot_env":"Paste .env file",
"batch_secrets":"Batch añade secretos"
},
"general":"General",
"database":{
"default_database":"Base de datos predeterminada",
"generated_automatically_after_set_to_public":"Generado automáticamente después de establecerse en público",
"connection_string":"Conexión",
"set_public":"Ponlo en público",
"warning_database_public":"Su base de datos será accesible en Internet. ¡Seguridad en este caso!",
"change_append_only_mode":"Cambiar el apéndice sólo modo",
"warning_append_only":"Útil si desea restaurar los datos redis de una copia de seguridad. Se requiere el reinicio de la base de datos.",
"select_database_type":"Seleccione un tipo de base",
"select_database_version":"Seleccione una versión de base de datos",
"confirm_stop":"¿Seguro que te gustaría parar?",
"stop_database":"Para.",
"permission_denied_stop_database":"No tienes permiso para detener la base de datos.",
"start_database":"Comienzo",
"permission_denied_start_database":"No tienes permiso para iniciar la base de datos.",
"delete_database":"Suprimir",
"permission_denied_delete_database":"Usted no tiene permiso para eliminar una base de datos",
"no_databases_found":"No se encontraron bases de datos",
"logs":"Logs"
},
"destination":{
"delete_destination":"Suprimir",
"permission_denied_delete_destination":"Usted no tiene permiso para eliminar este destino",
"add_to_coolify":"Añadir a Coolify",
"coolify_proxy_stopped":"Coolify Proxy paró!",
"coolify_proxy_started":"Coolify Proxy comenzó!",
"confirm_restart_proxy":"¿Seguro que quieres reiniciar el proxy? Todo será reconfigurado en ~10 segundos.",
"coolify_proxy_restarting":"Enfríe el reinicio de Proxy.",
"restarting_please_wait":"Reiniciando. Por favor, espere.",
"force_restart_proxy":"Fuerza restart proxy",
"use_coolify_proxy":"¿Uso Coolify Proxy?",
"no_destination_found":"No hay destino encontrado",
"new_error_network_already_exists":"Red {{network}} ya configurado para otro equipo!",
"new":{
"saving_and_configuring_proxy":"Salvando.",
"install_proxy":"Esto instalará un proxy en el destino para permitirle acceder a sus aplicaciones y servicios sin ninguna configuración manual (recomendada para Docker).",
"add_new_destination":"Añadir nuevo destino",
"predefined_destinations":"Destinos predefinidos"
}
},
"sources":{
"local_docker":"Local Docker",
"remote_docker":"Remoto Docker",
"organization_explainer":"Rellene si desea utilizar una organización como su Fuente Git. De lo contrario su usuario será utilizado."
},
"source":{
"new":{
"git_source":"Agregar nueva fuente de Git",
"official_providers":"Proveedores oficiales"
},
"no_git_sources_found":"No hay fuentes de git",
"delete_git_source":"Suprimir",
"permission_denied":"Usted no tiene permiso para eliminar una Fuente Git",
"create_new_app":"Crear nuevo {{name}} App",
"change_app_settings":"Cambio {{name}} Configuración de la aplicación",
"install_repositories":"Instalar los depósitos",
"application_id":"ID de aplicación",
"group_name":"Nombre del grupo",
"oauth_id":"OAuth ID",
"oauth_id_explainer":"El ID OAuth es el identificador único de la aplicación GitLab. Puedes encontrarlo itspan class=' text-settings' √in la URL seleccionada/span título de tu aplicación GitLab OAuth.",
"register_oauth_gitlab":"Registrar nueva aplicación OAuth en GitLab",
"gitlab":{
"self_hosted":"Aplicación a nivel de instalación (auto hospedada)",
"user_owned":"Aplicación de propiedad de usuario",
"group_owned":"Solicitud de propiedad de un grupo",
"gitlab_application_type":"GitLab Application Tipo",
"already_configured":"GitLab La aplicación ya está configurada."
},
"github":{
"redirecting":"Redirección a Github."
}
},
"services":{
"all_email_verified":"Todos los correos electrónicos son verificados. Puedes entrar ahora.",
"generate_www_non_www_ssl":"Generará certificados tanto para www como para no www. Necesitas tener las entradas de DNS de la clase='text-settings' inteligenteboth DNS seleccionadas/span título con antelación."
},
"service":{
"stop_service":"Para.",
"permission_denied_stop_service":"No tienes permiso para detener el servicio.",
"start_service":"Comienzo",
"permission_denied_start_service":"No tienes permiso para empezar el servicio.",
"delete_service":"Suprimir",
"permission_denied_delete_service":"Usted no tiene permiso para borrar un servicio.",
"no_service":"No se han encontrado servicios",
"logs":"Logs"
},
"setting":{
"change_language":"Cambiar idioma",
"permission_denied":"No tienes permiso para hacer esto. \\nAsk un administrador para modificar sus permisos.",
"domain_removed":"El dominio eliminado",
"ssl_explainer":"Si especificas יspan class='text-settings' confianzahttps realizadas/span, Coolify será accesible sólo en https. Certificado SSL se generará para usted.Se indicará que si especificas <span class='text-settings 'www identificado/span, Coolify será redireccionado (302) de no-www y viceversa. Si cambia un dominio ya establecido, romperá webhooks y otras integraciones! Necesita actualizarlos manualmente.",
"must_remove_domain_before_changing":"Debe eliminar el dominio antes de que pueda cambiar esta configuración.",
"registration_allowed":"¿Se permite la inscripción?",
"registration_allowed_explainer":"Permitir nuevos registros a la solicitud. Se ha apagado después del primer registro.",
"coolify_proxy_settings":"Enfriar los ajustes Proxy",
"credential_stat_explainer":"Credenciales para \"href=\"{{link}} target=\"_blank\"(s) asignados/página.",
"auto_update_enabled":"¿Actualización automática activada?",
"auto_update_enabled_explainer":"Activar actualizaciones automáticas para enfriar. Se hará automáticamente detrás de las escenas, si no hay proceso de construcción funcionando.",
"generate_www_non_www_ssl":"Generará certificados tanto para www como para no www. Necesitas tener las entradas de la clase='' texto-settings' inteligenteboth DNS seleccionadas/span titulada con antelación.",
"is_dns_check_enabled":"¿El cheque DNS está habilitado?",
"is_dns_check_enabled_explainer":"Puede deshabilitar el cheque DNS antes de crear certificados SSL.Seguido se indica que el ajuste es útil cuando Coolify está detrás de un proxy o túnel inverso."
},
"team":{
"pending_invitations":"Invitaciones pendientes",
"accept":"Aceptar",
"delete":"Suprimir",
"member":"miembros(s)",
"root":"(root)",
"invited_with_permissions":"Invitado a las \"clase de texto\"(s)(s)(s)(s)(s)(s)(s)(s)(s)(s)(s))(s))(s))(sp))(sp.",
"members":"Miembros",
"root_team_explainer":"Este es el equipo de la clase de la clase 0'text-red-500 'raíz seleccionada/span. Esto significa que los miembros de este grupo pueden gestionar la configuración de instancia amplia y tener todos los priviliges en Coolify (imagina como usuario raíz en Linux.)",
"permission":"Permiso",
"you":"Tú",
"promote_to":"Promover a {{grade}}",
"revoke_invitation":"Revocar la invitación",
"pending_invitation":"Invitación pendiente",
"invite_new_member":"Invitar nuevo miembro",
"send_invitation":"Enviar invitación",
"invite_only_register_explainer":"Sólo puedes invitar a usuarios registrados.",
"admin":"Admin",
"read":"Leer"
}
}

View File

@@ -0,0 +1,341 @@
{
"layout":{
"update_done":"업데이트가 완료되었습니다.",
"wait_new_version_startup":"새 버전이 시작되기를 기다리는 중...",
"new_version":"새 버전을 사용할 수 있습니다. 새로고침 중...",
"switch_to_a_different_team":"다른팀으로 갈아타세요...",
"update_available":"업데이트 가능"
},
"error":{
"you_can_find_your_way_back":"돌아갈 길을 찾을 수 있다",
"here":"여기",
"you_are_lost":"앗 길을 잃으셨군요! 그러나 두려워하지 마십시오!"
},
"index":{
"dashboard":"계기반",
"applications":"애플리케이션",
"destinations":"목적지",
"git_sources":"힘내 소스",
"databases":"데이터베이스",
"services":"서비스",
"teams":"팀",
"not_implemented_yet":"아직 구현되지 않음",
"database":"데이터 베이스",
"settings":"설정",
"global_settings":"전역 설정",
"secret":"비밀",
"team":"팀",
"logout":"로그 아웃"
},
"login":{
"already_logged_in":"이미 로그인...",
"authenticating":"인증 중...",
"login":"로그인"
},
"forms":{
"password":"비밀번호",
"email":"이메일 주소",
"passwords_not_match":"비밀번호가 일치하지 않습니다.",
"password_again":"비밀번호를 다시",
"save":"구하다",
"saving":"절약...",
"name":"이름",
"value":"값",
"action":"행위",
"is_required":"필요합니다.",
"add":"추가하다",
"set":"세트",
"remove":"제거하다",
"path":"길",
"confirm_continue":"계속하시겠습니까?",
"must_be_stopped_to_modify":"수정하려면 중지해야 합니다.",
"port":"포트",
"default":"기본",
"base_directory":"기본 디렉토리",
"publish_directory":"디렉토리 게시",
"generated_automatically_after_start":"시작 후 자동으로 생성됨",
"roots_password":"루트의 비밀번호",
"root_user":"루트 사용자",
"eg":"예",
"user":"사용자",
"loading":"로드 중...",
"version":"버전",
"host":"주최자",
"already_used_for":"<span class=\"text-red-500\">{{type}}</span>이(가) 이미 사용됨",
"configuration":"구성",
"engine":"엔진",
"network":"회로망",
"ip_address":"IP 주소",
"ssh_private_key":"SSH 개인 키",
"type":"유형",
"html_url":"HTML URL",
"api_url":"API URL",
"organization":"조직",
"new_password":"새 비밀번호",
"super_secure_new_password":"매우 안전한 새 비밀번호",
"submit":"제출하다",
"default_email_address":"기본 이메일 주소",
"default_password":"기본 비밀번호",
"username":"사용자 이름",
"root_db_user":"루트 DB 사용자",
"root_db_password":"루트 DB 비밀번호",
"api_port":"API 포트",
"verifying":"확인 중",
"verify_emails_without_smtp":"SMTP 없이 이메일 확인",
"extra_config":"추가 구성",
"select_a_service":"서비스 선택",
"select_a_service_version":"서비스 버전 선택",
"removing":"풀이...",
"remove_domain":"도메인 제거",
"public_port_range":"공용 포트 범위",
"public_port_range_explainer":"데이터베이스/서비스/내부 서비스를 노출하는 데 사용되는 포트입니다.<br> 방화벽에 추가합니다(해당되는 경우).<br><br>포트 범위를 지정할 수 있습니다(예: <span class='text-settings '>). 9000-9100</span>",
"no_actions_available":"사용 가능한 작업이 없습니다.",
"admin_api_key":"관리 API 키"
},
"register":{
"register":"등록하다",
"registering":"등록 중...",
"first_user":"첫 번째 사용자를 등록하고 있습니다. Coolify 인스턴스의 관리자가 됩니다."
},
"reset":{
"reset_password":"초기화",
"invalid_secret_key":"잘못된 비밀 키입니다.",
"secret_key":"비밀 키",
"find_path_secret_key":"~/coolify/.env(COOLIFY_SECRET_KEY)에서 찾을 수 있습니다."
},
"application":{
"configuration":{
"buildpack":{
"choose_this_one":"이걸 선택..."
},
"branch_already_in_use":"이 분기는 이미 다른 응용 프로그램에서 사용하고 있습니다. 이 경우 두 애플리케이션 모두에 대해 Webhook이 작동하지 않습니다. 사용하시겠습니까?",
"no_repositories_configured":"Git 애플리케이션에 대해 구성된 저장소가 없습니다.",
"configure_it_now":"지금 구성",
"loading_repositories":"저장소 로드 중...",
"select_a_repository":"저장소를 선택하십시오",
"loading_branches":"브랜치 로드 중...",
"select_a_repository_first":"먼저 저장소를 선택하십시오",
"select_a_branch":"지점을 선택해 주세요",
"loading_groups":"그룹 로드 중...",
"select_a_group":"그룹을 선택하세요.",
"loading_projects":"프로젝트 로드 중...",
"select_a_project":"프로젝트를 선택하세요.",
"no_projects_found":"프로젝트를 찾을 수 없습니다.",
"no_branches_found":"지점을 찾을 수 없습니다",
"configure_build_pack":"빌드 팩 구성",
"scanning_repository_suggest_build_pack":"빌드 팩을 제안하기 위해 저장소를 검색하는 중...",
"found_lock_file":"{{packageManager}}에 대한 잠금 파일을 찾았습니다.<br>사전 정의된 명령 명령에 사용합니다.",
"configure_destination":"대상 구성",
"no_configurable_destination":"구성 가능한 대상을 찾을 수 없습니다.",
"select_a_repository_project":"리포지토리/프로젝트 선택",
"select_a_git_source":"Git 소스 선택",
"no_configurable_git":"구성 가능한 Git 소스를 찾을 수 없습니다.",
"configuration_missing":"구성 누락"
},
"build":{
"queued_waiting_exec":"큐에 넣고 실행을 기다리고 있습니다.",
"build_logs_of":"빌드 로그",
"running":"달리기",
"queued":"대기 중",
"finished_in":"완료",
"load_more":"더 찾아보기",
"no_logs":"로그를 찾을 수 없습니다.",
"waiting_logs":"로그를 기다리는 중..."
},
"preview":{
"need_during_buildtime":"빌드 시간에 필요하십니까?",
"setup_secret_app_first":"PR/MR 배포에 비밀을 추가할 수 있습니다. 먼저 응용 프로그램에 비밀을 추가하십시오. <br><span class='text-settings '>스테이징</span> 환경을 만드는 데 유용합니다.",
"values_overwriting_app_secrets":"이러한 값은 PR/MR 배포에서 애플리케이션 비밀을 덮어씁니다. <span class='text-settings '>스테이징</span> 환경을 만드는 데 유용합니다.",
"redeploy":"재배포",
"no_previews_available":"사용 가능한 미리보기가 없습니다."
},
"secrets":{
"secret_saved":"비밀이 저장되었습니다.",
"use_isbuildsecret":"isBuildSecret 사용",
"secrets_for":"비밀"
},
"storage":{
"path_is_required":"경로는 필수 항목입니다.",
"storage_saved":"저장용량이 저장되었습니다.",
"storage_updated":"스토리지가 업데이트되었습니다.",
"storage_deleted":"스토리지가 삭제되었습니다.",
"persistent_storage_explainer":"배포 간에 유지하려는 모든 폴더를 지정할 수 있습니다.<br><span class='text-settings '>/example</span>은 <span class='text-settings '>/app/를 보존함을 의미합니다. <span class='text-settings '>/app</span>과 같은 컨테이너의 example</span>은 애플리케이션의 <span class='text-settings '>루트 디렉토리</span>입니다.<br> <br><span class='text-settings '>데이터베이스(SQLite)</span> 또는 <span class='text-settings '>캐시</span>와 같은 데이터를 저장하는 데 유용합니다."
},
"deployment_queued":"배포가 대기 중입니다.",
"confirm_to_delete":"'{{name}}'을(를) 삭제하시겠습니까?",
"stop_application":"애플리케이션 중지",
"permission_denied_stop_application":"애플리케이션을 중지할 권한이 없습니다.",
"rebuild_application":"애플리케이션 재구축",
"permission_denied_rebuild_application":"애플리케이션을 다시 빌드할 권한이 없습니다.",
"build_and_start_application":"배포",
"permission_denied_build_and_start_application":"애플리케이션을 배포할 권한이 없습니다.",
"configurations":"구성",
"secret":"비밀",
"persistent_storage":"영구 스토리지",
"previews":"미리보기",
"logs":"애플리케이션 로그",
"build_logs":"빌드 로그",
"delete_application":"삭제",
"permission_denied_delete_application":"이 애플리케이션을 삭제할 권한이 없습니다.",
"domain_already_in_use":"도메인 {{domain}}은(는) 이미 사용 중입니다.",
"dns_not_set_error":"DNS가 올바르게 설정되지 않았거나 {{domain}}에 대해 전파되었습니다.<br><br>DNS 설정을 확인하십시오.",
"domain_required":"도메인은 필수 항목입니다.",
"settings_saved":"구성이 저장되었습니다.",
"dns_not_set_partial_error":"DNS가 설정되지 않았습니다.",
"domain_not_valid":"도메인을 확인할 수 없거나 서버 IP 주소를 가리키지 않습니다.<br><br>DNS 구성을 확인하고 다시 시도하십시오.",
"git_source":"힘내 소스",
"git_repository":"Git 저장소",
"build_pack":"빌드 팩",
"base_image":"배포 이미지",
"base_image_explainer":"배포에 사용할 이미지입니다.",
"base_build_image":"빌드 이미지",
"base_build_image_explainer":"빌드 프로세스 중에 사용될 이미지입니다.",
"destination":"목적지",
"application":"신청",
"url_fqdn":"URL(FQDN)",
"domain_fqdn":"도메인(FQDN)",
"https_explainer":"<span class='text-settings '>https</span>를 지정하면 https를 통해서만 애플리케이션에 액세스할 수 있습니다. SSL 인증서가 생성됩니다.<br><span class='text-settings '>www</span>를 지정하면 애플리케이션이 www가 아닌 ​​곳에서 리디렉션(302)되거나 그 반대의 경우도 마찬가지입니다.<br>< br>도메인을 수정하려면 먼저 애플리케이션을 중지해야 합니다.<br><br><span class='text-white '>미리 DNS가 서버 IP를 가리키도록 설정해야 합니다.</span>",
"ssl_www_and_non_www":"www 및 www가 없는 SSL을 생성하시겠습니까?",
"ssl_explainer":"www 및 non-www 모두에 대한 인증서를 생성합니다. <br>미리 <span class=' text-settings'>두 DNS 항목</span>을 설정해야 합니다.<br><br>두 DNS 항목 모두에 방문자가 있을 것으로 예상되는 경우 유용합니다.",
"install_command":"설치 명령",
"build_command":"빌드 명령",
"start_command":"시작 명령",
"directory_to_use_explainer":"모든 명령의 기반으로 사용할 디렉토리입니다.<br><span class='text-settings '>monorepos</span>와 함께 유용할 수 있습니다.",
"publish_directory_explainer":"배포를 위한 모든 자산이 포함된 디렉터리입니다. <br> 예: <span class='text-settings '>dist</span>,<span class='text-settings '>_site</span> 또는 <span class='text-settings '>public< /스팬>.",
"features":"특징",
"enable_automatic_deployment":"자동 배포 활성화",
"enable_auto_deploy_webhooks":"웹훅을 통한 자동 배포를 활성화합니다.",
"enable_mr_pr_previews":"MR/PR 미리보기 활성화",
"expose_a_port":"포트 노출",
"enable_preview_deploy_mr_pr_requests":"끌어오기 또는 병합 요청에서 미리보기 배포를 활성화합니다.",
"debug_logs":"디버그 로그",
"enable_debug_log_during_build":"빌드 단계에서 디버그 로그를 활성화합니다.<br><span class='text-settings '>민감한 정보</span>가 표시되고 로그에 저장될 수 있습니다.",
"cant_activate_auto_deploy_without_repo":"이 리포지토리/분기에 대해 하나의 애플리케이션만 정의될 때까지 자동 배포를 활성화할 수 없습니다.",
"no_applications_found":"애플리케이션을 찾을 수 없습니다.",
"secret__batch_dot_env":".env 파일 붙여넣기",
"batch_secrets":"일괄 추가 비밀"
},
"general":"일반적인",
"database":{
"default_database":"기본 데이터베이스",
"generated_automatically_after_set_to_public":"public으로 설정 후 자동 생성",
"connection_string":"연결 문자열",
"set_public":"공개 설정",
"warning_database_public":"인터넷을 통해 데이터베이스에 연결할 수 있습니다. <br>이 경우 보안을 심각하게 생각하십시오!",
"change_append_only_mode":"추가 전용 모드 변경",
"warning_append_only":"백업에서 redis 데이터를 복원하려는 경우에 유용합니다.<br><span class=' text-white'>데이터베이스를 다시 시작해야 합니다.</span>",
"select_database_type":"데이터베이스 유형 선택",
"select_database_version":"데이터베이스 버전 선택",
"confirm_stop":"{{name}}을(를) 중지하시겠습니까?",
"stop_database":"중지",
"permission_denied_stop_database":"데이터베이스를 중지할 권한이 없습니다.",
"start_database":"시작",
"permission_denied_start_database":"데이터베이스를 시작할 권한이 없습니다.",
"delete_database":"삭제",
"permission_denied_delete_database":"데이터베이스를 삭제할 권한이 없습니다.",
"no_databases_found":"데이터베이스를 찾을 수 없습니다.",
"logs":"로그"
},
"destination":{
"delete_destination":"삭제",
"permission_denied_delete_destination":"이 목적지를 삭제할 권한이 없습니다.",
"add_to_coolify":"Coolify에 추가",
"coolify_proxy_stopped":"Coolify 프록시가 중지되었습니다!",
"coolify_proxy_started":"Coolify 프록시가 시작되었습니다!",
"confirm_restart_proxy":"프록시를 다시 시작하시겠습니까? 모든 것이 ~10초 안에 재구성됩니다.",
"coolify_proxy_restarting":"Coolify 프록시 다시 시작 중...",
"restarting_please_wait":"다시 시작 중입니다... 잠시만 기다려 주십시오...",
"force_restart_proxy":"강제 재시작 프록시",
"use_coolify_proxy":"Coolify 프록시를 사용하시겠습니까?",
"no_destination_found":"목적지를 찾을 수 없습니다",
"new_error_network_already_exists":"다른 팀에 대해 네트워크 {{network}}이(가) 이미 구성되었습니다!",
"new":{
"saving_and_configuring_proxy":"절약...",
"install_proxy":"그러면 수동 구성 없이 애플리케이션과 서비스에 액세스할 수 있도록 대상에 프록시가 설치됩니다(Docker에 권장됨).<br><br>데이터베이스에는 자체 프록시가 있습니다.",
"add_new_destination":"새 목적지 추가",
"predefined_destinations":"사전 정의된 목적지"
}
},
"sources":{
"local_docker":"로컬 도커",
"remote_docker":"원격 도커",
"organization_explainer":"조직을 Git 소스로 사용하려면 입력하십시오. 그렇지 않으면 사용자가 사용됩니다."
},
"source":{
"new":{
"git_source":"새 Git 소스 추가",
"official_providers":"공식 제공업체"
},
"no_git_sources_found":"git 소스를 찾을 수 없습니다.",
"delete_git_source":"삭제",
"permission_denied":"Git 소스를 삭제할 권한이 없습니다.",
"create_new_app":"새 {{name}} 앱 만들기",
"change_app_settings":"{{name}} 앱 설정 변경",
"install_repositories":"저장소 설치",
"application_id":"애플리케이션 ID",
"group_name":"그룹 이름",
"oauth_id":"인증 ID",
"oauth_id_explainer":"OAuth ID는 GitLab 애플리케이션의 고유 식별자입니다. <br>GitLab OAuth 애플리케이션의 <span class=' text-settings' >URL</span>에서 찾을 수 있습니다.",
"register_oauth_gitlab":"GitLab에 새 OAuth 애플리케이션 등록",
"gitlab":{
"self_hosted":"인스턴스 전체 애플리케이션(자체 호스팅)",
"user_owned":"사용자 소유 애플리케이션",
"group_owned":"그룹 소유 애플리케이션",
"gitlab_application_type":"GitLab 애플리케이션 유형",
"already_configured":"GitLab 앱이 이미 구성되어 있습니다."
},
"github":{
"redirecting":"Github으로 리디렉션 중..."
}
},
"services":{
"all_email_verified":"모든 이메일이 확인되었습니다. 지금 로그인할 수 있습니다.",
"generate_www_non_www_ssl":"www 및 non-www 모두에 대한 인증서를 생성합니다. <br>미리 <span class='text-settings'>두 DNS 항목</span>을 설정해야 합니다.<br><br>서비스를 다시 시작해야 합니다."
},
"service":{
"stop_service":"중지",
"permission_denied_stop_service":"서비스를 중지할 권한이 없습니다.",
"start_service":"시작",
"permission_denied_start_service":"서비스를 시작할 권한이 없습니다.",
"delete_service":"삭제",
"permission_denied_delete_service":"서비스를 삭제할 권한이 없습니다.",
"no_service":"서비스를 찾을 수 없습니다.",
"logs":"로그"
},
"setting":{
"change_language":"언어 변경",
"permission_denied":"이 작업을 수행할 권한이 없습니다. \\n관리자에게 권한 수정을 요청하세요.",
"domain_removed":"도메인이 삭제됨",
"ssl_explainer":"<span class='text-settings'>https</span>를 지정하면 Coolify는 https를 통해서만 액세스할 수 있습니다. SSL 인증서가 자동으로 생성됩니다.<br><span class='text-settings '>www</span>를 지정하면 Coolify가 www가 아닌 ​​곳에서 리디렉션(302)되거나 그 반대의 경우도 마찬가지입니다.<br><br ><span class='text-settings '>경고:</span> 이미 설정된 도메인을 변경하면 웹훅 및 기타 통합이 중단됩니다! 수동으로 업데이트해야 합니다.",
"must_remove_domain_before_changing":"이 설정을 변경하려면 먼저 도메인을 제거해야 합니다.",
"registration_allowed":"등록이 허용됩니까?",
"registration_allowed_explainer":"애플리케이션에 대한 추가 등록을 허용합니다. <br>최초 등록 후에는 꺼져 있습니다.",
"coolify_proxy_settings":"Coolify 프록시 설정",
"credential_stat_explainer":"<a class=\"text-white \" href=\"{{link}}\" target=\"_blank\">통계</a> 페이지에 대한 자격 증명입니다.",
"auto_update_enabled":"자동 업데이트가 활성화되었습니까?",
"auto_update_enabled_explainer":"Coolify에 대한 자동 업데이트를 활성화합니다. 실행 중인 빌드 프로세스가 없는 경우 배후에서 자동으로 수행됩니다.",
"generate_www_non_www_ssl":"www 및 non-www 모두에 대한 인증서를 생성합니다. <br>미리 <span class=' text-settings'>두 DNS 항목</span>을 설정해야 합니다.",
"is_dns_check_enabled":"DNS 확인이 활성화되었습니까?",
"is_dns_check_enabled_explainer":"SSL 인증서를 생성하기 전에 DNS 확인을 비활성화할 수 있습니다.<br><br>Coolify가 역방향 프록시 또는 터널 뒤에 있을 때 비활성화하는 것이 유용합니다."
},
"team":{
"pending_invitations":"대기 중인 초대",
"accept":"수용하다",
"delete":"삭제",
"member":"회원",
"root":"(뿌리)",
"invited_with_permissions":"<span class=\" text-rose-600\">{{permission}}</span> 권한으로 <span class=\" text-settings\">{{teamName}}</span>에 초대되었습니다.",
"members":"회원",
"root_team_explainer":"<span class='text-red-500 '>루트</span> 팀입니다. 즉, 이 그룹의 구성원은 인스턴스 전체 설정을 관리하고 Coolify의 모든 권한을 가질 수 있습니다(Linux의 루트 사용자와 같은 경우).",
"permission":"허가",
"you":"너",
"promote_to":"{{grade}}(으)로 승격",
"revoke_invitation":"초대 취소",
"pending_invitation":"대기 중인 초대",
"invite_new_member":"새 회원 초대",
"send_invitation":"초대장을 보내다",
"invite_only_register_explainer":"등록된 사용자만 초대할 수 있습니다.",
"admin":"관리자",
"read":"읽다"
}
}

View File

@@ -0,0 +1,341 @@
{
"layout":{
"update_done":"Atualização completa.",
"wait_new_version_startup":"Aguardando a nova versão iniciar...",
"new_version":"Nova versão acessível. Recarregando...",
"switch_to_a_different_team":"Mudar para uma equipa diferente...",
"update_available":"Atualização disponível"
},
"error":{
"you_can_find_your_way_back":"Você pode encontrar o seu caminho de volta",
"here":"aqui",
"you_are_lost":"Ooops você está perdido! Mas não tenha medo!"
},
"index":{
"dashboard":"Painel",
"applications":"Formulários",
"destinations":"Destinos",
"git_sources":"Fontes Git",
"databases":"Bancos de dados",
"services":"Serviços",
"teams":"Equipes",
"not_implemented_yet":"Ainda não implementado",
"database":"Base de dados",
"settings":"Definições",
"global_settings":"Configurações globais",
"secret":"Segredo",
"team":"Equipe",
"logout":"Sair"
},
"login":{
"already_logged_in":"Já logado...",
"authenticating":"Autenticando...",
"login":"Conecte-se"
},
"forms":{
"password":"Senha",
"email":"Endereço de email",
"passwords_not_match":"As senhas não coincidem.",
"password_again":"Senha novamente",
"save":"Salvar",
"saving":"Salvando...",
"name":"Nome",
"value":"Valor",
"action":"Ações",
"is_required":"É necessário.",
"add":"Adicionar",
"set":"Definir",
"remove":"Remover",
"path":"Caminho",
"confirm_continue":"Tem certeza de continuar?",
"must_be_stopped_to_modify":"Deve ser parado para modificar.",
"port":"Porta",
"default":"predefinição",
"base_directory":"Diretório base",
"publish_directory":"Publicar diretório",
"generated_automatically_after_start":"Gerado automaticamente após o início",
"roots_password":"Senha do Root",
"root_user":"Usuário raiz",
"eg":"por exemplo",
"user":"Do utilizador",
"loading":"Carregando...",
"version":"Versão",
"host":"Hospedeiro",
"already_used_for":"<span class=\"text-red-500\">{{type}}</span> já usado para",
"configuration":"Configuração",
"engine":"Motor",
"network":"Rede",
"ip_address":"Endereço de IP",
"ssh_private_key":"Chave privada SSH",
"type":"Modelo",
"html_url":"URL HTML",
"api_url":"URL da API",
"organization":"Organização",
"new_password":"Nova Senha",
"super_secure_new_password":"Nova senha super segura",
"submit":"Enviar",
"default_email_address":"Endereço de e-mail padrão",
"default_password":"Senha padrão",
"username":"Nome de usuário",
"root_db_user":"Usuário raiz do banco de dados",
"root_db_password":"Senha do banco de dados raiz",
"api_port":"Porta API",
"verifying":"Verificando",
"verify_emails_without_smtp":"Verifique e-mails sem SMTP",
"extra_config":"Configuração extra",
"select_a_service":"Selecione um serviço",
"select_a_service_version":"Selecione uma versão do serviço",
"removing":"Removendo...",
"remove_domain":"Remover domínio",
"public_port_range":"Intervalo de portas públicas",
"public_port_range_explainer":"Portas usadas para expor bancos de dados/serviços/serviços internos.<br> Adicione-os ao seu firewall (se aplicável).<br><br>Você pode especificar um intervalo de portas, por exemplo: <span class='text-settings '> 9000-9100</span>",
"no_actions_available":"Nenhuma ação disponível",
"admin_api_key":"Chave de API de administrador"
},
"register":{
"register":"Registro",
"registering":"Registrando...",
"first_user":"Você está registrando o primeiro usuário. Será o administrador da sua instância Coolify."
},
"reset":{
"reset_password":"Redefinir",
"invalid_secret_key":"Chave secreta inválida.",
"secret_key":"Chave secreta",
"find_path_secret_key":"Você pode encontrá-lo em ~/coolify/.env (COOLIFY_SECRET_KEY)"
},
"application":{
"configuration":{
"buildpack":{
"choose_this_one":"Escolha este..."
},
"branch_already_in_use":"Esta ramificação já é usada por outro aplicativo. Os webhooks não funcionarão neste caso para ambos os aplicativos. Tem certeza de que deseja usá-lo?",
"no_repositories_configured":"Nenhum repositório configurado para seu aplicativo Git.",
"configure_it_now":"Configure agora",
"loading_repositories":"Carregando repositórios...",
"select_a_repository":"Selecione um repositório",
"loading_branches":"Carregando ramos...",
"select_a_repository_first":"Selecione um repositório primeiro",
"select_a_branch":"Selecione uma filial",
"loading_groups":"Carregando grupos...",
"select_a_group":"Selecione um grupo",
"loading_projects":"Carregando projetos...",
"select_a_project":"Por favor selecione um projeto",
"no_projects_found":"Nenhum projeto encontrado",
"no_branches_found":"Nenhuma ramificação encontrada",
"configure_build_pack":"Configurar pacote de compilação",
"scanning_repository_suggest_build_pack":"Verificando repositório para sugerir um pacote de compilação para você...",
"found_lock_file":"Arquivo de bloqueio encontrado para {{packageManager}}.<br>Usando-o para comandos de comandos predefinidos.",
"configure_destination":"Configurar destino",
"no_configurable_destination":"Nenhum destino configurável encontrado",
"select_a_repository_project":"Selecione um Repositório/Projeto",
"select_a_git_source":"Selecione uma fonte Git",
"no_configurable_git":"Nenhuma fonte Git configurável encontrada",
"configuration_missing":"Configuração ausente"
},
"build":{
"queued_waiting_exec":"Na fila e aguardando execução.",
"build_logs_of":"Construir registros de",
"running":"Corrida",
"queued":"Enfileiradas",
"finished_in":"Terminando em",
"load_more":"Carregue mais",
"no_logs":"Nenhum registro encontrado",
"waiting_logs":"Aguardando os logs..."
},
"preview":{
"need_during_buildtime":"Precisa durante o tempo de construção?",
"setup_secret_app_first":"Você pode adicionar segredos a implantações de PR/MR. Por favor, adicione segredos ao aplicativo primeiro. <br>Útil para criar ambientes de <span class='text-settings '>preparação</span>.",
"values_overwriting_app_secrets":"Esses valores substituem os segredos do aplicativo em implantações PR/MR. Útil para criar ambientes de <span class='text-settings '>preparação</span>.",
"redeploy":"Reimplantar",
"no_previews_available":"Nenhuma visualização disponível"
},
"secrets":{
"secret_saved":"Segredo salvo.",
"use_isbuildsecret":"Use isBuildSecret",
"secrets_for":"Segredos para"
},
"storage":{
"path_is_required":"O caminho é obrigatório.",
"storage_saved":"Armazenamento salvo.",
"storage_updated":"Armazenamento atualizado.",
"storage_deleted":"Armazenamento excluído.",
"persistent_storage_explainer":"Você pode especificar qualquer pasta que deseja que seja persistente nas implantações.<br><span class='text-settings '>/example</span> significa que ela preservará <span class='text-settings '>/app/ example</span> no contêiner, pois <span class='text-settings '>/app</span> é <span class='text-settings '>o diretório raiz</span> para seu aplicativo.<br> <br>Isto é útil para armazenar dados como um <span class='text-settings '>banco de dados (SQLite)</span> ou um <span class='text-settings '>cache</span>."
},
"deployment_queued":"Implantação em fila.",
"confirm_to_delete":"Tem certeza de que deseja excluir '{{name}}'?",
"stop_application":"Parar aplicativo",
"permission_denied_stop_application":"Você não tem permissão para parar o aplicativo.",
"rebuild_application":"Reconstruir aplicativo",
"permission_denied_rebuild_application":"Você não tem permissão para reconstruir o aplicativo.",
"build_and_start_application":"Implantar",
"permission_denied_build_and_start_application":"Você não tem permissão para implantar o aplicativo.",
"configurations":"Configurações",
"secret":"Segredos",
"persistent_storage":"Armazenamento persistente",
"previews":"Visualizações",
"logs":"Registros de aplicativos",
"build_logs":"Construir registros",
"delete_application":"Excluir",
"permission_denied_delete_application":"Você não tem permissão para excluir este aplicativo",
"domain_already_in_use":"O domínio {{domain}} já está em uso.",
"dns_not_set_error":"DNS não definido corretamente ou propagado para {{domain}}.<br><br>Verifique suas configurações de DNS.",
"domain_required":"O domínio é obrigatório.",
"settings_saved":"Configuração salva.",
"dns_not_set_partial_error":"DNS não definido",
"domain_not_valid":"Não foi possível resolver o domínio ou não está apontando para o endereço IP do servidor.<br><br>Verifique sua configuração de DNS e tente novamente.",
"git_source":"Fonte do Git",
"git_repository":"Repositório Git",
"build_pack":"Pacote de compilação",
"base_image":"Imagem de implantação",
"base_image_explainer":"Imagem que será usada para a implantação.",
"base_build_image":"Construir imagem",
"base_build_image_explainer":"Imagem que será usada durante o processo de compilação.",
"destination":"Destino",
"application":"Inscrição",
"url_fqdn":"URL (FQDN)",
"domain_fqdn":"Domínio (FQDN)",
"https_explainer":"Se você especificar <span class='text-settings '>https</span>, o aplicativo será acessível apenas por https. O certificado SSL será gerado para você.<br>Se você especificar <span class='text-settings '>www</span>, o aplicativo será redirecionado (302) de não www e vice-versa.<br>< br>Para modificar o domínio, você deve primeiro parar o aplicativo.<br><br><span class='text-white '>Você deve configurar seu DNS para apontar para o IP do servidor com antecedência.</span>",
"ssl_www_and_non_www":"Gerar SSL para www e não www?",
"ssl_explainer":"Ele irá gerar certificados para www e não www. <br>Você precisa ter <span class=' text-settings'>ambas as entradas DNS</span> definidas com antecedência.<br><br>Útil se você espera receber visitantes em ambas.",
"install_command":"Comando de instalação",
"build_command":"Comando de compilação",
"start_command":"Comando Iniciar",
"directory_to_use_explainer":"Diretório a ser usado como base para todos os comandos.<br>Pode ser útil com <span class='text-settings '>monorepos</span>.",
"publish_directory_explainer":"Diretório contendo todos os ativos para implantação. <br> Por exemplo: <span class='text-settings '>dist</span>,<span class='text-settings '>_site</span> ou <span class='text-settings '>public< /span>.",
"features":"Características",
"enable_automatic_deployment":"Ativar implantação automática",
"enable_auto_deploy_webhooks":"Habilite a implantação automática por meio de webhooks.",
"enable_mr_pr_previews":"Ativar visualizações de MR/PR",
"expose_a_port":"Expor uma porta",
"enable_preview_deploy_mr_pr_requests":"Habilite implantações de visualização de solicitações de pull ou mesclagem.",
"debug_logs":"Registros de depuração",
"enable_debug_log_during_build":"Ative os registros de depuração durante a fase de compilação.<br><span class='text-settings '>Informações confidenciais</span> podem ser visíveis e salvas nos registros.",
"cant_activate_auto_deploy_without_repo":"Não é possível ativar implantações automáticas até que apenas um aplicativo seja definido para este repositório/ramificação.",
"no_applications_found":"Nenhum aplicativo encontrado",
"secret__batch_dot_env":"Colar arquivo .env",
"batch_secrets":"Adicionar segredos em lote"
},
"general":"Em geral",
"database":{
"default_database":"Banco de dados padrão",
"generated_automatically_after_set_to_public":"Gerado automaticamente após definido como público",
"connection_string":"Cadeia de conexão",
"set_public":"Defina-o como público",
"warning_database_public":"Seu banco de dados estará acessível pela internet. <br>Leve a segurança a sério neste caso!",
"change_append_only_mode":"Alterar o modo somente anexar",
"warning_append_only":"Útil se você deseja restaurar dados redis de um backup.<br><span class=' text-white'>É necessário reiniciar o banco de dados.</span>",
"select_database_type":"Selecione um tipo de banco de dados",
"select_database_version":"Selecione uma versão do banco de dados",
"confirm_stop":"Tem certeza de que deseja parar {{name}}?",
"stop_database":"Pare",
"permission_denied_stop_database":"Você não tem permissão para parar o banco de dados.",
"start_database":"Começar",
"permission_denied_start_database":"Você não tem permissão para iniciar o banco de dados.",
"delete_database":"Excluir",
"permission_denied_delete_database":"Você não tem permissão para excluir um banco de dados",
"no_databases_found":"Nenhum banco de dados encontrado",
"logs":"Histórico"
},
"destination":{
"delete_destination":"Excluir",
"permission_denied_delete_destination":"Você não tem permissão para excluir este destino",
"add_to_coolify":"Adicionar ao Coolify",
"coolify_proxy_stopped":"Coolify Proxy parou!",
"coolify_proxy_started":"Coolify Proxy iniciado!",
"confirm_restart_proxy":"Tem certeza de que deseja reiniciar o proxy? Tudo será reconfigurado em ~10 segundos.",
"coolify_proxy_restarting":"Coolify Proxy reiniciando...",
"restarting_please_wait":"Reiniciando... aguarde...",
"force_restart_proxy":"Forçar reinicialização do proxy",
"use_coolify_proxy":"Usar Coolify Proxy?",
"no_destination_found":"Nenhum destino encontrado",
"new_error_network_already_exists":"Rede {{network}} já configurada para outra equipe!",
"new":{
"saving_and_configuring_proxy":"Salvando...",
"install_proxy":"Isso instalará um proxy no destino para permitir que você acesse seus aplicativos e serviços sem qualquer configuração manual (recomendado para o Docker).<br><br>Os bancos de dados terão seu próprio proxy.",
"add_new_destination":"Adicionar novo destino",
"predefined_destinations":"Destinos predefinidos"
}
},
"sources":{
"local_docker":"Docker local",
"remote_docker":"Docker remoto",
"organization_explainer":"Preencha-o se quiser usar o de uma organização como seu Git Source. Caso contrário, seu usuário será usado."
},
"source":{
"new":{
"git_source":"Adicionar nova fonte Git",
"official_providers":"Fornecedores oficiais"
},
"no_git_sources_found":"Nenhuma fonte git encontrada",
"delete_git_source":"Excluir",
"permission_denied":"Você não tem permissão para excluir uma fonte Git",
"create_new_app":"Criar novo aplicativo {{name}}",
"change_app_settings":"Alterar {{name}} configurações do aplicativo",
"install_repositories":"Instalar repositórios",
"application_id":"ID do aplicativo",
"group_name":"Nome do grupo",
"oauth_id":"ID OAuth",
"oauth_id_explainer":"O OAuth ID é o identificador exclusivo do aplicativo GitLab. <br>Você pode encontrá-lo <span class=' text-settings' >no URL</span> do seu aplicativo GitLab OAuth.",
"register_oauth_gitlab":"Registre um novo aplicativo OAuth no GitLab",
"gitlab":{
"self_hosted":"Aplicativo em toda a instância (auto-hospedado)",
"user_owned":"Aplicativo de propriedade do usuário",
"group_owned":"Aplicativo de propriedade do grupo",
"gitlab_application_type":"Tipo de aplicativo GitLab",
"already_configured":"O aplicativo GitLab já está configurado."
},
"github":{
"redirecting":"Redirecionando para o Github..."
}
},
"services":{
"all_email_verified":"Todos os e-mails são verificados. Você pode entrar agora.",
"generate_www_non_www_ssl":"Ele irá gerar certificados para www e não www. <br>Você precisa ter <span class='text-settings'>ambas as entradas DNS</span> definidas com antecedência.<br><br>O serviço precisa ser reiniciado."
},
"service":{
"stop_service":"Pare",
"permission_denied_stop_service":"Você não tem permissão para interromper o serviço.",
"start_service":"Começar",
"permission_denied_start_service":"Você não tem permissão para iniciar o serviço.",
"delete_service":"Excluir",
"permission_denied_delete_service":"Você não tem permissão para excluir um serviço.",
"no_service":"Nenhum serviço encontrado",
"logs":"Histórico"
},
"setting":{
"change_language":"Mudar idioma",
"permission_denied":"Você não tem permissão para fazer isso. \\nPeça a um administrador para modificar suas permissões.",
"domain_removed":"Domínio removido",
"ssl_explainer":"Se você especificar <span class='text-settings'>https</span>, o Coolify será acessível apenas por https. O certificado SSL será gerado para você.<br>Se você especificar <span class='text-settings '>www</span>, Coolify será redirecionado (302) de não www e vice-versa.<br><br ><span class='text-settings '>AVISO:</span> Se você alterar um domínio já definido, isso interromperá webhooks e outras integrações! Você precisa atualizá-los manualmente.",
"must_remove_domain_before_changing":"Deve remover o domínio antes de alterar esta configuração.",
"registration_allowed":"Registro permitido?",
"registration_allowed_explainer":"Permitir mais registros no aplicativo. <br>É desligado após o primeiro registro.",
"coolify_proxy_settings":"Configurações de proxy do Coolify",
"credential_stat_explainer":"Credenciais para a página de <a class=\"text-white \" href=\"{{link}}\" target=\"_blank\">estatísticas</a>.",
"auto_update_enabled":"Atualização automática habilitada?",
"auto_update_enabled_explainer":"Habilite atualizações automáticas para Coolify. Isso será feito automaticamente nos bastidores, se não houver nenhum processo de compilação em execução.",
"generate_www_non_www_ssl":"Ele irá gerar certificados para www e não www. <br>Você precisa ter <span class=' text-settings'>ambas as entradas de DNS</span> configuradas antecipadamente.",
"is_dns_check_enabled":"Verificação de DNS habilitada?",
"is_dns_check_enabled_explainer":"Você pode desabilitar a verificação de DNS antes de criar certificados SSL.<br><br>Desligá-la é útil quando o Coolify está atrás de um proxy reverso ou túnel."
},
"team":{
"pending_invitations":"Convites pendentes",
"accept":"Aceitar",
"delete":"Excluir",
"member":"membros)",
"root":"(raiz)",
"invited_with_permissions":"Convidado para <span class=\" text-settings\">{{teamName}}</span> com permissão <span class=\" text-rose-600\">{{permission}}</span>.",
"members":"Membros",
"root_team_explainer":"Esta é a equipe <span class='text-red-500 '>raiz</span>. Isso significa que os membros deste grupo podem gerenciar as configurações de toda a instância e ter todos os privilégios no Coolify (imagine como usuário root no Linux).",
"permission":"Permissão",
"you":"Você",
"promote_to":"Promover para {{grade}}",
"revoke_invitation":"Revogar convite",
"pending_invitation":"Convite pendente",
"invite_new_member":"Convidar novo membro",
"send_invitation":"Enviar convite",
"invite_only_register_explainer":"Você só pode convidar usuários registrados.",
"admin":"Administrador",
"read":"Ler"
}
}

View File

@@ -3,7 +3,7 @@ import cuid from 'cuid';
import Cookies from 'js-cookie';
import { writable, readable, type Writable } from 'svelte/store';
import { io as ioClient } from 'socket.io-client';
const socket = ioClient(dev ? 'http://localhost:3001' : '/', { auth: { token: Cookies.get('token') }, autoConnect: false });
const socket = ioClient(dev ? `http://${window.location.hostname}:3001` : '/', { auth: { token: Cookies.get('token') }, autoConnect: false });
export const io = socket;
interface AppSession {
@@ -32,6 +32,7 @@ interface AddToast {
}
export const updateLoading: Writable<boolean> = writable(false);
export const isUpdateAvailable: Writable<boolean> = writable(false);
export const latestVersion: Writable<string> = writable('latest');
export const search: any = writable('')
export const loginEmail: Writable<string | undefined> = writable()
export const appSession: Writable<AppSession> = writable({
@@ -56,14 +57,15 @@ export const appSession: Writable<AppSession> = writable({
export const disabledButton: Writable<boolean> = writable(false);
export const isDeploymentEnabled: Writable<boolean> = writable(false);
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
return (
return !!(
isAdmin &&
(application.buildPack === 'compose') ||
(application.fqdn || application.settings.isBot) &&
application.gitSource &&
application.repository &&
application.destinationDocker &&
application.buildPack
((application.gitSource &&
application.repository &&
application.buildPack) || application.simpleDockerfile) &&
application.destinationDocker
);
}
export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any) {
@@ -80,6 +82,7 @@ export const status: Writable<any> = writable({
statuses: [],
overallStatus: 'stopped',
loading: false,
restarting: false,
initialLoading: true
},
service: {

View File

@@ -1,11 +1,18 @@
import i18n from 'sveltekit-i18n';
import { derived, writable } from "svelte/store";
import lang from './lang.json';
export let currentLocale = writable("en");
export let debugTranslation = writable(false);
/** @type {import('sveltekit-i18n').Config} */
export const config = {
fallbackLocale: 'en',
translations: {
en: { lang },
es: { lang },
pt: { lang },
ko: { lang },
fr: { lang }
},
loaders: [
@@ -14,12 +21,27 @@ export const config = {
key: '',
loader: async () => (await import('./locales/en.json')).default
},
{
locale: 'es',
key: '',
loader: async () => (await import('./locales/es.json')).default
},
{
locale: 'pt',
key: '',
loader: async () => (await import('./locales/pt.json')).default
},
{
locale: 'fr',
key: '',
loader: async () => (await import('./locales/fr.json')).default
},
{
locale: 'ko',
key: '',
loader: async () => (await import('./locales/ko.json')).default
}
]
};
export const { t, locales, locale, loadTranslations } = new i18n(config);
export const { t, locales, locale, loadTranslations } = new i18n(config);

View File

@@ -64,6 +64,8 @@
</script>
<script lang="ts">
export let settings: any;
export let sentryDSN: any;
export let baseSettings: any;
export let pendingInvitations: any = 0;
@@ -95,12 +97,24 @@
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;
if (permission) $appSession.permission = permission;
if (isAdmin) $appSession.isAdmin = isAdmin;
// if (settings?.doNotTrack === false) {
// Sentry.init({
// dsn: sentryDSN,
// environment: dev ? 'development' : 'production',
// integrations: [new BrowserTracing()],
// release: $appSession.version?.toString(),
// tracesSampleRate: 0.2
// });
// }
async function logout() {
try {
Cookies.remove('token');
@@ -110,14 +124,16 @@
}
}
onMount(async () => {
io.connect();
io.on('start-service', (message) => {
const { serviceId, state } = message;
$status.service.startup[serviceId] = state;
if (state === 0 || state === 1) {
delete $status.service.startup[serviceId];
}
});
if ($appSession.userId) {
io.connect();
io.on('start-service', (message) => {
const { serviceId, state } = message;
$status.service.startup[serviceId] = state;
if (state === 0 || state === 1) {
delete $status.service.startup[serviceId];
}
});
}
});
</script>
@@ -136,10 +152,16 @@
<PageLoader />
</div>
{/if}
<div class="drawer">
<input id="main-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
{#if $appSession.userId}
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black"
>Settings</Tooltip
>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>
<nav class="nav-main hidden lg:block z-20">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
{#if !$appSession.whiteLabeled}
@@ -246,7 +268,7 @@
<a
id="settings"
sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/docker'}
class="icons hover:text-settings"
class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
@@ -292,6 +314,9 @@
<path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg>
</div>
<!-- <div class="lg:block">
<LocalePicker/>
</div> -->
<div
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
>
@@ -311,19 +336,22 @@
{/if}
{/if}
<div
class="navbar lg:hidden space-x-2 flex flex-row items-center bg-coollabs"
class="navbar lg:hidden space-x-2 flex flex-row justify-between bg-coollabs"
class:hidden={!$appSession.userId}
>
<label for="main-drawer" class="drawer-button btn btn-square btn-ghost flex-col">
<span class="burger bg-white" />
<span class="burger bg-white" />
<span class="burger bg-white" />
</label>
<div class="prose flex flex-row justify-between space-x-1 w-full items-center pr-3">
{#if !$appSession.whiteLabeled}
<h3 class="mb-0 text-white">Coolify</h3>
{/if}
<div>
<label for="main-drawer" class="drawer-button btn btn-square btn-ghost flex-col">
<span class="burger bg-white" />
<span class="burger bg-white" />
<span class="burger bg-white" />
</label>
<div class="prose flex flex-row justify-between space-x-1 w-full items-center pr-3">
{#if !$appSession.whiteLabeled}
<h3 class="mb-0 text-white">Coolify</h3>
{/if}
</div>
</div>
<!-- <LocalePicker /> -->
</div>
<main>
<div class={$appSession.userId ? 'lg:pl-16' : null}>
@@ -478,7 +506,3 @@
</ul>
</div>
</div>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>

View File

@@ -13,7 +13,7 @@
<a
id="git"
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
target="_blank noreferrer"
class="no-underline"
>
{#if application.gitSource?.type === 'gitlab'}
@@ -135,26 +135,28 @@
</svg>Persistent Volumes</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/features`}
>
<a href={`/applications/${$page.params.id}/features`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="13 3 13 10 19 10 11 21 11 14 5 14 13 3" />
</svg>Features</a
{#if !application.simpleDockerfile}
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/features`}
>
</li>
<a href={`/applications/${$page.params.id}/features`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="13 3 13 10 19 10 11 21 11 14 5 14 13 3" />
</svg>Features</a
>
</li>
{/if}
<li class="menu-title">
<span>Logs</span>
@@ -165,7 +167,9 @@
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
>
<a
href={$status.application.overallStatus !== 'stopped' ? `/applications/${$page.params.id}/logs` : ''}
href={$status.application.overallStatus !== 'stopped'
? `/applications/${$page.params.id}/logs`
: ''}
class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
@@ -216,12 +220,40 @@
<li class="menu-title">
<span>Advanced</span>
</li>
{#if application.gitSourceId}
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/revert`}
>
<a href={`/applications/${$page.params.id}/revert`} class="no-underline w-full">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 5v14l-12 -7z" />
<line x1="4" y1="5" x2="4" y2="19" />
</svg>
Revert</a
>
</li>
{/if}
<li
class="rounded"
class:text-stone-600={$status.application.overallStatus !== 'healthy'}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
>
<a href={$status.application.overallStatus === 'healthy' ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
<a
href={$status.application.overallStatus === 'healthy'
? `/applications/${$page.params.id}/usage`
: ''}
class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -237,7 +269,7 @@
</svg>Monitoring</a
>
</li>
{#if !application.settings.isBot}
{#if !application.settings.isBot && application.gitSourceId}
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/previews`}

View File

@@ -32,10 +32,10 @@
}
</script>
<div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase">name</label>
<label for="name" class="pb-2 uppercase font-bold">name</label>
{/if}
<input
@@ -50,7 +50,7 @@
</div>
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="value" class="pb-2 uppercase">value</label>
<label for="value" class="pb-2 uppercase font-bold">value</label>
{/if}
<CopyPasswordField
@@ -63,9 +63,12 @@
</div>
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
>Need during buildtime?</label
>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button
@@ -114,7 +117,7 @@
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
{/if}
<div class="flex justify-center h-full items-center pt-3">

View File

@@ -79,10 +79,10 @@
}
</script>
<div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase">name</label>
<label for="name" class="pb-2 uppercase font-bold">name</label>
{/if}
<input
@@ -101,7 +101,7 @@
</div>
<div class="flex flex-col">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="value" class="pb-2 uppercase">value</label>
<label for="value" class="pb-2 uppercase font-bold">value</label>
{/if}
<CopyPasswordField
@@ -114,9 +114,12 @@
</div>
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
>Need during buildtime?</label
>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button
@@ -166,7 +169,7 @@
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Action</label>
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
{/if}
<div class="flex justify-center h-full items-center pt-3">

View File

@@ -60,49 +60,54 @@
</script>
<div class="w-full lg:px-0 px-4">
<div class="grid grid-col-1 lg:grid-cols-3 lg:space-x-4" class:pt-8={isNew}>
{#if storage.id}
<div class="flex flex-col">
<label for="name" class="pb-2 uppercase font-bold">Volume name</label>
<input
disabled
readonly
class="w-full lg:w-64"
value="{storage.id}{storage.path.replace(/\//gi, '-')}"
/>
</div>
{/if}
<div class="flex flex-col">
<label for="name" class="pb-2 uppercase font-bold">{isNew ? 'New Path' : 'Path'}</label>
{#if storage.predefined}
<div class="flex flex-col lg:flex-row gap-4 pb-2">
<input disabled readonly class="w-full" value={storage.id} />
<input disabled readonly class="w-full" bind:value={storage.path} />
</div>
{:else}
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
{#if storage.applicationId}
{#if storage.oldPath}
<input
disabled
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
/>
{:else}
<input
disabled
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-')}"
/>
{/if}
{/if}
<input
class="w-full lg:w-64"
disabled={!isNew}
readonly={!isNew}
class="w-full"
bind:value={storage.path}
required
placeholder="eg: /sqlite.db"
placeholder="eg: /data"
/>
</div>
<div class="pt-8">
{#if isNew}
<div class="flex items-center justify-center w-full lg:w-64">
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-64">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
>{$t('forms.set')}</button
<div class="flex items-center justify-center">
{#if isNew}
<div class="w-full lg:w-64">
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex justify-center">
<button class="btn btn-sm btn-error" on:click={removeStorage}
>{$t('forms.remove')}</button
>
</div>
</div>
{/if}
{/if}
</div>
</div>
</div>
{/if}
</div>

View File

@@ -2,8 +2,14 @@
import type { Load } from '@sveltejs/kit';
function checkConfiguration(application: any): string | null {
let configurationPhase = null;
if (!application.gitSourceId) {
configurationPhase = 'source';
if (!application.gitSourceId && !application.simpleDockerfile) {
return (configurationPhase = 'source');
}
if (application.simpleDockerfile) {
if (!application.destinationDockerId) {
configurationPhase = 'destination';
}
return configurationPhase;
} else if (!application.repository && !application.branch) {
configurationPhase = 'repository';
} else if (!application.destinationDockerId) {
@@ -70,8 +76,8 @@
selectedBuildId
} from '$lib/store';
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
import Menu from './_Menu.svelte';
import { saveForm } from './utils';
let statusInterval: any;
let forceDelete = false;
@@ -96,12 +102,25 @@
async function handleDeploySubmit(forceRebuild = false) {
if (!$isDeploymentEnabled) return;
if (application.gitCommitHash && !application.settings.isPublicRepository) {
const sure = await confirm(
`Are you sure you want to deploy a specific commit (${application.gitCommitHash})? This will disable the "Automatic Deployment" feature to prevent accidental overwrites of incoming commits.`
);
if (!sure) {
return;
} else {
await post(`/applications/${id}/settings`, {
autodeploy: false
});
}
}
if (!statusInterval) {
statusInterval = setInterval(async () => {
await getStatus();
}, 2000);
}
try {
await saveForm(id, application);
const { buildId } = await post(`/applications/${id}/deploy`, {
...application,
forceRebuild
@@ -148,7 +167,8 @@
}
}
async function getStatus() {
if ($status.application.loading && stopping) return;
if (($status.application.loading && stopping) || $status.application.restarting === true)
return;
$status.application.loading = true;
const data = await get(`/applications/${id}/status`);
@@ -166,24 +186,20 @@
if ($status.application.statuses.length === 0) {
$status.application.overallStatus = 'stopped';
} else {
if ($status.application.statuses.length !== numberOfApplications) {
$status.application.overallStatus = 'degraded';
} else {
for (const oneStatus of $status.application.statuses) {
if (oneStatus.status.isExited || oneStatus.status.isRestarting) {
$status.application.overallStatus = 'degraded';
break;
}
if (oneStatus.status.isRunning) {
$status.application.overallStatus = 'healthy';
}
if (
!oneStatus.status.isExited &&
!oneStatus.status.isRestarting &&
!oneStatus.status.isRunning
) {
$status.application.overallStatus = 'stopped';
}
for (const oneStatus of $status.application.statuses) {
if (oneStatus.status.isExited || oneStatus.status.isRestarting) {
$status.application.overallStatus = 'degraded';
break;
}
if (oneStatus.status.isRunning) {
$status.application.overallStatus = 'healthy';
}
if (
!oneStatus.status.isExited &&
!oneStatus.status.isRestarting &&
!oneStatus.status.isRunning
) {
$status.application.overallStatus = 'stopped';
}
}
}
@@ -244,14 +260,14 @@
{/if}
</div>
{#if $page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
<div class="px-2">
<div class="px-4">
{#if forceDelete}
<button
on:click={() => deleteApplication(application.name, true)}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
class="btn btn-sm btn-error hover:bg-red-700 text-sm w-64"
>
Force Delete Application
</button>
@@ -261,7 +277,7 @@
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
class="btn btn-sm btn-error hover:bg-red-700 text-sm w-64"
>
Delete Application
</button>
@@ -438,7 +454,7 @@
<button
class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled}
on:click={() => handleDeploySubmit(false)}
on:click={() => handleDeploySubmit(true)}
>
{#if $status.application.overallStatus !== 'degraded'}
<svg
@@ -501,7 +517,7 @@
</div>
</div>
<div
class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1"
class="mx-auto max-w-screen-2xl px-0 lg:px-10 grid grid-cols-1"
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
>
{#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}

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